feat(core): thread workflow context (ctx.*) into node scope + post-merge roadmap - #23
Conversation
Post-merge roadmap + status update now that checkpoint/resume (1.R) and the human gate (1.Q) have merged. - phase-1-engine-and-llm.md: ✅ Done markers on §1.Q and §1.R; top status block records the PR #22 landing and the remaining 1.m4 lane (1.S, 1.AC). - current.md: status narrative + next-workstream pointer advanced to node retry (1.S); last-updated 2026-06-15. - CLAUDE.md: status paragraph + detailed status reflect 1.R/1.Q landed, 1.S next. - deferred-tasks.md: re-point the now-landed-context items — the structuredClone `ctx`-transport obligation moves off 1.R (the checkpoint carries no resolved ctx) to the ctx-threading work; mid-tool-loop resume noted as Phase-2 (1.R resumes at gate boundaries only); the ctx-threading fold-into-1.Q/1.R window noted closed (now its own task). New "Checkpoint/resume + human gate (1.R/1.Q) follow-ups" section captures the three confirmed Phase-2 deferrals (gate-timer re-arm on rehydration, content-hash workflow-snapshot identity guard, cross-process gate-resolve TOCTOU → store-level uniqueness). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the highest-value open engine gap (deferred-tasks): the authored `context:`
namespace was never resolved/threaded, so the sandbox scope (condition/transform/
merge_fn) and the AgentRunner prompt bound `ctx: {}` — a bare `ctx.key` JS read saw
`undefined` (a silent mis-route risk).
- NodeExecContext gains `ctx: Readonly<Record<string,string>>` — the resolved,
frozen workflow-context namespace threaded to every node.
- WorkflowEngineDeps gains `resolverCapabilities?` — the engine resolves `context:`
values (which may `{{inputs.*}}` / `read_file`) at run start through the same
purity seam the handlers use.
- The engine resolves the context ONCE right after `run:started` (the spec's
eager-once context — `#resolveContextOrFail` over `resolveContext`), before any
node runs; a resolution failure closes the run with `run:failed{validation}` (never
runs nodes against a partial context), and a cancel mid-resolve settles
`run:cancelled`. The resolved map is threaded via `NodeExecContext.ctx`.
- Cross-process resume RE-RESOLVES the context (it is deliberately NOT carried in the
checkpoint): the resume drive is unified into `beginResume`, which resolves context
then either kicks (gate already resolved) or applies the decision.
- Consumers: `buildExpressionScope` and the AgentRunner's `resolvePrompt` now read
`ctx.ctx` (was `{}`).
- No new ADR — the eager-once context is already specified (workflow-yaml-spec
`context:` + `resolveContext`); this wires it.
Tests (+5): a transform reading a bare `ctx.key`; engine e2e — context resolved +
threaded; a context-resolution failure → `run:failed{validation}` before any node;
a `read_file` context value via an injected capability; and a cross-process resume
re-resolving the context so post-gate nodes see `ctx.*`.
Docs: execution-model.md §2 (the run-start eager-once context step); the
NodeExecContext.ctx contract; deferred-tasks ctx-threading item checked off and the
structuredClone-transport obligation re-pointed (dormant — ctx is re-resolved, not
transported).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of the ctx-threading diff caught a real HIGH: the
`human_in_the_loop` handler was the one scope-building site the change missed —
it still bound `ctx: {}`, so a `{{ctx.key}}` in a gate's `message_template` /
`assignee` (a documented template field, workflow-yaml-spec §context) resolved to
`undefined` and failed the gate with `validation`. Now reads `ctx.ctx`, matching
`buildExpressionScope` and the AgentRunner's `resolvePrompt`.
- human-gate.ts: `ctx: ctx.ctx` (+ corrected the scope comment).
- execution-model.md §2: human-gate `message_template` / `assignee` added to the
list of `ctx.*` consumers (the doc had mirrored the broken state).
- Tests (+4): a gate `{{ctx.key}}` in message_template/assignee resolves (ctxFor
gains a `ctx` option — it would otherwise pass with the bug); a no-`context:`
workflow threads `ctx: {}`; a cancel racing context resolution settles
`run:cancelled` (not `run:failed{validation}` — the previously-untested
#resolveContextOrFail cancel branch); context RE-resolution failure on the resume
path closes the run `run:failed{validation}` without applying the decision; and
the context-failure test now pins `run:started` precedes `run:failed`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer's GuideThreads the resolved workflow context ( Sequence diagram for ctx.* resolution and threading on start and resumesequenceDiagram
participant Engine as WorkflowEngine
participant Exec as RunExecution
participant Resolver as resolveContext
participant Handler as NodeExecutor
participant Scope as buildExpressionScope
participant Agent as resolvePrompt
participant Gate as runHumanGate
Engine->>Exec: begin()
Exec->>Resolver: resolveContext(workflow, inputs, resolverCapabilities, signal)
alt context resolved
Exec-->>Exec: #resolvedContext = frozen ctx map
Exec->>Exec: #schedule()
Exec->>Handler: execute(NodeExecContext{ctx: #resolvedContext})
Handler->>Scope: buildExpressionScope(ctx)
Scope-->>Handler: ExpressionScope{ctx}
Handler-->>Engine: node:completed
Engine-->>Engine: emit run:completed
else resolution fails and cancelling
Exec-->>Engine: settle(run:cancelled)
else resolution fails (validation)
Exec-->>Engine: settle(run:failed{validation})
end
rect rgb(235,235,245)
Engine->>Exec: resumeFromCheckpoint()
Exec->>Exec: beginResume(gateId, decision, gateAlreadyResolved)
Exec->>Resolver: resolveContext(workflow, inputs, resolverCapabilities, signal)
alt context re-resolved
Exec-->>Exec: #resolvedContext = frozen ctx map
alt gateAlreadyResolved
Exec->>Exec: #schedule()
else apply decision
Exec->>Exec: resume(gateId, decision)
end
Exec->>Handler: execute(NodeExecContext{ctx: #resolvedContext})
Handler->>Agent: resolvePrompt(ctx)
Handler->>Gate: runHumanGate(ctx)
else re-resolution fails
Exec-->>Engine: settle(run:failed{validation})
end
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds eager resolution of the workflow Changesctx.\ Workflow Context Threading*
Roadmap and Architecture Documentation Updates
Sequence Diagram(s)sequenceDiagram
participant Caller
participant WorkflowEngine
participant RunExecution
participant resolveContext
participant NodeHandler
Caller->>WorkflowEngine: start(workflow, inputs, resolverCapabilities)
WorkflowEngine->>RunExecution: new RunExecution(resolverCapabilities)
RunExecution->>RunExecution: emit run:started
RunExecution->>resolveContext: `#resolveContextOrFail`(workflow.context)
alt cancellation races resolution
resolveContext-->>RunExecution: AbortError
RunExecution-->>Caller: run:cancelled
else resolution validation failure
resolveContext-->>RunExecution: ValidationError
RunExecution-->>Caller: run:failed{validation}
else resolved successfully
resolveContext-->>RunExecution: ctx map → `#resolvedContext`
RunExecution->>NodeHandler: execute(NodeExecContext{ctx: `#resolvedContext`})
NodeHandler-->>RunExecution: outcome
RunExecution-->>Caller: run:completed
end
Caller->>WorkflowEngine: resumeFromCheckpoint(runId, decision)
WorkflowEngine->>RunExecution: beginResume(decision)
RunExecution->>resolveContext: `#resolveContextOrFail` (re-resolve, not from checkpoint)
alt re-resolution failure
resolveContext-->>RunExecution: error
RunExecution-->>Caller: run:failed{validation}
else re-resolved
resolveContext-->>RunExecution: ctx map → `#resolvedContext`
RunExecution->>NodeHandler: execute(NodeExecContext{ctx: `#resolvedContext`})
RunExecution-->>Caller: run:completed
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="packages/core/src/engine/engine.ts" line_range="180-181" />
<code_context>
readonly #bus: RunEventBus;
readonly #onSettled: (runId: string) => void;
+ readonly #resolverCapabilities: ResolverCapabilities;
+ /** The resolved workflow `context:` (`ctx.*`), folded once at run start (or re-resolved on resume). */
+ #resolvedContext: Readonly<Record<string, string>> = {};
readonly #abort: AbortControllerLike;
</code_context>
<issue_to_address>
**issue (bug_risk):** `#resolvedContext` is declared `readonly` but later reassigned, which will break type-checking
For `#` private fields, `readonly` is enforced by TypeScript, so this reassignment will fail at compile time. Either remove `readonly` from `#resolvedContext`, or keep it immutable by resolving the context into a local variable and passing that down instead of mutating the field.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This pull request implements workflow-context (ctx.*) threading into expression and agent scopes. The WorkflowEngine now resolves the workflow context: map once at run start (using injected resolverCapabilities such as readFile) and threads the frozen ctx.* namespace to all nodes via NodeExecContext.ctx. Additionally, the context is re-resolved on cross-process resume rather than being persisted in checkpoints. This allows expressions, agent prompts, and human-gate templates to access the resolved context values directly. Comprehensive unit and integration tests have been added to verify these behaviors, including failure paths and cancel races. I have no feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/core/src/engine/agent-runner.test.ts (1)
115-130: ⚡ Quick winAdd an agent prompt regression for
{{ctx.*}}.The helper now satisfies the required field, but hardcoding
ctx: {}leaves the AgentRunner part of this PR’s ctx-threading contract untested. Let the helper accept a workflow context and add one prompt interpolation assertion.Proposed test helper and regression
function ctxFor( vertex: PlanVertex, inputs: Record<string, unknown> = DEFAULT_INPUTS, runOutputs: ReadonlyMap<string, unknown> = NO_OUTPUTS, + workflowCtx: Readonly<Record<string, string>> = {}, ): { ctx: NodeExecContext; events: NodeStreamEvent[]; } { @@ vertex, runOutputs, inputs, - ctx: {}, + ctx: workflowCtx,it('resolves {{ctx.*}} into the agent user prompt', async () => { const { provider, req } = reqCapturingProvider(); const exec = createAgentNodeExecutor(deps(provider)); const { ctx } = ctxFor( vertexFor({ kind: 'agent', node: agentNode({ prompt_template: 'Summarize {{ctx.topic}}: {{inputs.text}}' }), resolvedAgent: AGENT, }), { text: 'the body' }, NO_OUTPUTS, { topic: 'weather' }, ); await exec.execute(ctx); const userMsg = req()?.messages.find((m) => m.role === 'user'); const part = userMsg?.content[0]; expect(part?.type === 'text' ? part.text : undefined).toBe('Summarize weather: the body'); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/engine/agent-runner.test.ts` around lines 115 - 130, The ctxFor helper function currently hardcodes ctx: {} in the returned NodeExecContext, which leaves the AgentRunner's context-threading contract untested for workflow context variables. Modify the ctxFor function signature to accept an optional fourth parameter for workflow context, replace the hardcoded ctx: {} assignment with the provided context parameter (defaulting to an empty object if not provided), and add a new test case that validates the agent prompt template correctly interpolates {{ctx.*}} variables alongside {{inputs.*}} variables. The test should verify that a prompt template like 'Summarize {{ctx.topic}}: {{inputs.text}}' resolves to 'Summarize weather: the body' when passed context and inputs appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/engine/engine.ts`:
- Around line 374-383: The beginResume method currently resolves the context
before validating whether the provided gateId is actually pending, which can
cause a valid paused run to be terminally settled as run:failed{validation}
instead of properly handling an invalid resume request. Move the validation of
the gateId parameter (checking whether it is actually pending on the run) to
occur BEFORE the context resolution check in `#resolveContextOrFail`, so that
invalid resume attempts fail fast with the appropriate unknown_gate or
run_not_paused error and delete the half-initialized execution, rather than
settling the run as failed when context resolution fails.
---
Nitpick comments:
In `@packages/core/src/engine/agent-runner.test.ts`:
- Around line 115-130: The ctxFor helper function currently hardcodes ctx: {} in
the returned NodeExecContext, which leaves the AgentRunner's context-threading
contract untested for workflow context variables. Modify the ctxFor function
signature to accept an optional fourth parameter for workflow context, replace
the hardcoded ctx: {} assignment with the provided context parameter (defaulting
to an empty object if not provided), and add a new test case that validates the
agent prompt template correctly interpolates {{ctx.*}} variables alongside
{{inputs.*}} variables. The test should verify that a prompt template like
'Summarize {{ctx.topic}}: {{inputs.text}}' resolves to 'Summarize weather: the
body' when passed context and inputs appropriately.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f2b963b3-ccf2-4df1-b296-2c82560da376
📒 Files selected for processing (14)
CLAUDE.mddocs/architecture/execution-model.mddocs/roadmap/current.mddocs/roadmap/deferred-tasks.mddocs/roadmap/phases/phase-1-engine-and-llm.mdpackages/core/src/engine/agent-runner.test.tspackages/core/src/engine/agent-runner.tspackages/core/src/engine/engine.test.tspackages/core/src/engine/engine.tspackages/core/src/engine/node-executor.tspackages/core/src/engine/node-handlers/human-gate.test.tspackages/core/src/engine/node-handlers/human-gate.tspackages/core/src/engine/node-handlers/node-handlers.test.tspackages/core/src/engine/node-handlers/scope.ts
… review) Address the PR #23 review. - beginResume now validates the gate FIRST (non-kick path) — a bad gateId throws unknown_gate / run_not_paused (extracted into #assertGatePending, shared with resume()) BEFORE the side-effectful context re-resolution. Previously an invalid resume request could terminally settle a resumable run as run:failed{validation} when context resolution happened to fail; now it fails fast and the caller drops the run from #runs (a retry with the correct gateId stays possible). Test: a wrong gateId on a run whose context would fail rejects with unknown_gate and persists no new events (context resolution never runs). - agent-runner.test.ts: ctxFor gains a context param; new test asserts the agent prompt interpolates {{ctx.*}} alongside {{inputs.*}} ('Summarize {{ctx.topic}}: {{inputs.text}}' -> 'Summarize weather: the body') — the AgentRunner's ctx-threading contract, previously untested. Skipped: the "#resolvedContext is readonly" comment — a misread. The field is NOT `readonly` (only its value type is `Readonly<Record<…>>`); the reassignment typechecks (CI green). No change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|



Closes the highest-value open engine gap from
deferred-tasks.md: the authoredcontext:namespace is now resolved at run start and threaded to every node, so a barectx.keyresolves instead of readingundefined. Plus the post-1.R/1.Q roadmap update. Engine-only (@relavium/core, zero platform imports); 632 core tests green,format:check+ lint + typecheck + build clean, Leakwatch clean; put through an adversarially-verified multi-agent review (1 real HIGH found + fixed).ctx.*threading (commita9a3671+ review fix0db2a91)The gap: the
condition/transform/merge_fnsandbox scope, the AgentRunner prompt scope, and the human-gatemessage_template/assigneeall boundctx: {}— the workflowcontext:was never resolved/threaded, so a barectx.keyJS read sawundefined(a silent mis-route risk), and a{{ctx.key}}template resolved toundefined.NodeExecContext.ctx— the new seam field: the resolved, frozenctx.*namespace threaded to every node ({}when the workflow declares nocontext:).WorkflowEngineDeps.resolverCapabilities— the engine resolvescontext:values (which may{{inputs.*}}/read_file) at run start through the same purity seam the handlers use.#resolveContextOrFail): right afterrun:started, before any node — a resolution failure closes the runrun:failed{validation}(never runs nodes against a partial context); a cancel mid-resolve settlesrun:cancelled.beginResume(replacedkick()).ctx.ctx:buildExpressionScope, the AgentRunner'sresolvePrompt, and (the review-caught HIGH) the human-gate handler.context:+resolveContext); this wires it.Review trail
A 4-lens Sonnet review (correctness/concurrency, purity/non-negotiables, security — can a secret leak via
ctx.*?, tests/contracts) with refute-by-default verification found one real HIGH: the human-gate handler was the one scope site the change missed (ctx: {}), so{{ctx.key}}in a gate'smessage_template/assigneesilently failed. Fixed (ctx: ctx.ctx) with a regression test that the priorctxForwould have passed. Security lens confirmed no new secret-leak path (parse-time taint oncontext-value/node-textsinks + the input-handler masking still hold).Tests (+9)
A transform reading a bare
ctx.key; engine e2e (context resolved + threaded); a no-context:workflow →ctx: {}; aread_filecontext value via an injected capability; a context-resolution failure →run:failed{validation}before any node (withrun:started-first ordering); a cancel racing resolution →run:cancelled; cross-process resume re-resolving so post-gate nodes seectx.*; resume-path re-resolution failure →run:failed{validation}; and the human-gate{{ctx.key}}resolution.Post-merge roadmap (commit
b229555)Marks 1.R + 1.Q ✅ Done (PR #22) across
phase-1-engine-and-llm.md/current.md/CLAUDE.md, advances the next-workstream pointer to 1.S (node retry), and re-points the relevantdeferred-tasks.mditems (the ctx-threading item is checked off here; the structuredClone-transport obligation is re-pointed as dormant sincectxis re-resolved, not transported).🤖 Generated with Claude Code
Summary by Sourcery
Thread the resolved workflow
context:namespace (ctx.*) through the engine so all node types and agents can read it, and update roadmap/docs to mark checkpoint/resume and human gate workstreams as complete with follow-up items captured.New Features:
context:resolution at run start and resume, exposing a frozenctx.*namespace onNodeExecContextfor use by expressions, agents, and human-gate templates.Bug Fixes:
message_templateandassigneecorrectly resolve{{ctx.*}}values from the threaded workflow context instead of an empty namespace.run:failed{validation}while treating mid-resolution cancellations asrun:cancelled.ctx.*, failing the run withvalidationif re-resolution is not possible.Enhancements:
context:values (includingread_file) once per run.beginResumeentry that re-resolves context before applying gate decisions or driving downstream work.ctx.*.ctxis re-resolved rather than checkpointed.Documentation:
Summary by CodeRabbit
Release Notes
New Features
context(ctx.*) is resolved once per run and consistently available across expressions, transforms, agent prompts, and human-gate message fields.ctx.*is re-resolved so post-gate nodes see up-to-date values.Documentation
Tests
{{ctx.*}}and resume/gate edge cases.