@exaudeus/workrail 3.35.1 → 3.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/console-ui/assets/{index-D7jQyCSD.js → index-n8cJrS4v.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +4 -0
- package/dist/daemon/workflow-runner.js +133 -0
- package/dist/manifest.json +23 -23
- package/dist/mcp/handlers/v2-advance-events.js +1 -1
- package/dist/mcp/handlers/v2-execution/start.d.ts +1 -0
- package/dist/mcp/handlers/v2-execution/start.js +3 -2
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +64 -32
- package/dist/v2/durable-core/schemas/session/events.d.ts +20 -10
- package/dist/v2/durable-core/schemas/session/events.js +1 -1
- package/dist/v2/durable-core/schemas/session/gaps.d.ts +8 -8
- package/dist/v2/durable-core/schemas/session/gaps.js +1 -1
- package/docs/ideas/backlog.md +80 -1
- package/docs/ideas/design-candidates-spawn-agent-task.md +178 -0
- package/docs/ideas/design-review-findings-spawn-agent-task.md +139 -0
- package/docs/ideas/implementation_plan_spawn_agent.md +217 -0
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Implementation Plan: spawn_agent Tool
|
|
2
|
+
|
|
3
|
+
## 1. Problem Statement
|
|
4
|
+
|
|
5
|
+
Agents running inside WorkRail daemon sessions currently delegate sub-tasks using
|
|
6
|
+
`mcp__nested-subagent__Task` (external Claude Code MCP tool). This produces no WorkRail
|
|
7
|
+
session, no audit trail, and no structured output. The delegated work is completely invisible
|
|
8
|
+
to WorkRail -- it cannot be traced, replayed, or composed into larger orchestrations.
|
|
9
|
+
|
|
10
|
+
**Three root causes to fix:**
|
|
11
|
+
1. No native WorkRail tool for spawning child sessions from within a daemon session
|
|
12
|
+
2. No `parentSessionId` link in the session event log (no parent-child graph)
|
|
13
|
+
3. No depth enforcement to prevent runaway delegation chains
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. Acceptance Criteria
|
|
18
|
+
|
|
19
|
+
- [ ] A daemon agent can call `spawn_agent({ workflowId, goal, workspacePath, context? })` from a workflow step
|
|
20
|
+
- [ ] The parent agent blocks until the child session completes (no polling, no fire-and-forget)
|
|
21
|
+
- [ ] The child session is created with `parentSessionId` in the session event log (durable, survives crashes)
|
|
22
|
+
- [ ] The tool returns `{ childSessionId, outcome: 'success'|'error'|'timeout', notes: string }` as JSON
|
|
23
|
+
- [ ] Spawning a child at depth >= `maxSubagentDepth` (default 3) returns a typed error without spawning
|
|
24
|
+
- [ ] `spawnDepth` propagates correctly through chains: root=0, child=1, grandchild=2
|
|
25
|
+
- [ ] Child sessions are created in-process (no HTTP dispatch, no semaphore involvement)
|
|
26
|
+
- [ ] `npm run build` passes with no new TypeScript errors
|
|
27
|
+
- [ ] The `spawn_agent` tool is listed in `BASE_SYSTEM_PROMPT` with usage guidance
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 3. Non-Goals
|
|
32
|
+
|
|
33
|
+
- `spawn_session` + `await_sessions` non-blocking parallel spawn (Phase 2)
|
|
34
|
+
- `maxTotalAgentsPerTask` width guardrail (Phase 2)
|
|
35
|
+
- Bare-prompt child sessions without a `workflowId` (Phase 2)
|
|
36
|
+
- Session tree query API (console DAG view reads `parentSessionId`, but no query endpoint in Phase 1)
|
|
37
|
+
- Zombie session cleanup (Phase 2)
|
|
38
|
+
- Changes to `TriggerRouter`, `dispatch()`, or the HTTP dispatch route
|
|
39
|
+
- Changes to the public `V2StartWorkflowInput` MCP schema (external callers are unaffected)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 4. Philosophy-Driven Constraints
|
|
44
|
+
|
|
45
|
+
- **Errors are data**: All 4 `WorkflowRunResult` variants (`success`, `error`, `timeout`, `delivery_failed`) must be handled exhaustively and mapped to structured JSON return values. `execute()` must NOT throw for child failures.
|
|
46
|
+
- **Immutability by default**: New `WorkflowTrigger` fields must be `readonly`.
|
|
47
|
+
- **DI for boundaries**: `ctx`, `apiKey`, `runWorkflowFn`, `emitter` injected at factory construction time. No singletons, no global state.
|
|
48
|
+
- **Validate at boundaries**: Depth check at the START of `execute()`, before any async operations.
|
|
49
|
+
- **Make illegal states unrepresentable**: `childSessionId` must always be present in the result (pre-create guarantees it). `spawnDepth` must be a typed `WorkflowTrigger` field (not in context map).
|
|
50
|
+
- **YAGNI**: Phase 1 only. No bare-prompt mode, no `await_sessions`, no width guardrails.
|
|
51
|
+
- **Document 'why'**: Add WHY comments consistent with existing code style in `workflow-runner.ts`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 5. Invariants
|
|
56
|
+
|
|
57
|
+
1. **Semaphore bypass invariant**: `spawn_agent` must call `runWorkflow()` directly. Calling `TriggerRouter.dispatch()` from within a running session would cause semaphore deadlock.
|
|
58
|
+
2. **_preAllocatedStartResponse invariant**: When `trigger._preAllocatedStartResponse` is set, `runWorkflow()` MUST NOT call `executeStartWorkflow()` again. This invariant is already documented in the `WorkflowTrigger` JSDoc.
|
|
59
|
+
3. **Depth propagation invariant**: A child session at depth N must always construct its `spawn_agent` tool with `currentDepth = N`. This is enforced by the typed `readonly spawnDepth?: number` field on `WorkflowTrigger`.
|
|
60
|
+
4. **Schema strip invariant**: `session_created.data` uses `z.object({})` strip mode. Adding `parentSessionId?: string` is a backward-compatible additive change.
|
|
61
|
+
5. **V2StartWorkflowInput unchanged**: The public MCP input schema for `start_workflow` must not be modified. `parentSessionId` flows via `internalContext` only.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 6. Selected Approach + Rationale + Runner-Up
|
|
66
|
+
|
|
67
|
+
**Selected: Candidate 2 (pre-create session with _preAllocatedStartResponse)**
|
|
68
|
+
|
|
69
|
+
`execute()` calls `executeStartWorkflow(input, ctx, { parentSessionId })`, decodes `childSessionId`
|
|
70
|
+
from the returned `continueToken` via `parseContinueTokenOrFail()`, then calls `runWorkflow()` with
|
|
71
|
+
`_preAllocatedStartResponse: startResult.value.response`. This blocks naturally -- `await runWorkflow()`
|
|
72
|
+
inside `execute()` pauses the parent's tool execution until the child completes.
|
|
73
|
+
|
|
74
|
+
**Rationale**: The `_preAllocatedStartResponse` pattern is already proven in `console-routes.ts`
|
|
75
|
+
(lines 573-624). This is a direct adaptation of stable existing machinery, not invention.
|
|
76
|
+
`childSessionId` is always deterministic (known before child runs). Crash-before-start is
|
|
77
|
+
observable (zombie session in store with `parentSessionId` intact).
|
|
78
|
+
|
|
79
|
+
**Runner-up: Candidate 1 (direct runWorkflow())**
|
|
80
|
+
Simpler (~10 fewer lines). Loses: `childSessionId` is unavailable if the run crashes before
|
|
81
|
+
AgentLoop starts. Pivot to this if `_preAllocatedStartResponse` is ever removed.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 7. Vertical Slices
|
|
86
|
+
|
|
87
|
+
### Slice 1: WorkflowTrigger schema extension
|
|
88
|
+
**File**: `src/daemon/workflow-runner.ts`
|
|
89
|
+
**Change**: Add `readonly parentSessionId?: string` and `readonly spawnDepth?: number` to the `WorkflowTrigger` interface. Update `_preAllocatedStartResponse` JSDoc to list `spawn_agent` as a legitimate internal caller (O2 fix).
|
|
90
|
+
**Acceptance**: TypeScript compiles. No existing callers broken (new fields are optional). `_preAllocatedStartResponse` comment updated.
|
|
91
|
+
**Risk**: None (additive change).
|
|
92
|
+
|
|
93
|
+
### Slice 2: session_created.data schema extension
|
|
94
|
+
**File**: `src/v2/durable-core/schemas/session/events.ts`
|
|
95
|
+
**Change**: Extend `session_created.data` from `z.object({})` to `z.object({ parentSessionId: z.string().optional() })`.
|
|
96
|
+
**File**: `src/mcp/handlers/v2-execution/start.ts`
|
|
97
|
+
**Change**: Add `parentSessionId?: string` optional parameter to `buildInitialEvents()`. When provided, include it in the `session_created` event's `data` field. Thread `parentSessionId` from `executeStartWorkflow()` (via `internalContext?.['parentSessionId']`) into `buildInitialEvents()`.
|
|
98
|
+
**Acceptance**: TypeScript compiles. Existing session creation (without `parentSessionId`) produces `data: {}` (unchanged). Session created with `parentSessionId` produces `data: { parentSessionId: 'sess_...' }`.
|
|
99
|
+
**Risk**: Low (Zod strip mode, backward-compatible).
|
|
100
|
+
|
|
101
|
+
### Slice 3: makeSpawnAgentTool() factory
|
|
102
|
+
**File**: `src/daemon/workflow-runner.ts`
|
|
103
|
+
**Change**: Add `SpawnAgentParams` JSON Schema to `getSchemas()`. Add `makeSpawnAgentTool()` factory function alongside `makeCompleteStepTool()`, `makeContinueWorkflowTool()`, etc.
|
|
104
|
+
|
|
105
|
+
Factory signature:
|
|
106
|
+
```typescript
|
|
107
|
+
export function makeSpawnAgentTool(
|
|
108
|
+
sessionId: string, // process-local UUID (for logging)
|
|
109
|
+
ctx: V2ToolContext,
|
|
110
|
+
apiKey: string,
|
|
111
|
+
thisWorkrailSessionId: string, // WorkRail sess_xxx ID (becomes parentSessionId)
|
|
112
|
+
currentDepth: number, // spawn depth of the parent session
|
|
113
|
+
maxDepth: number, // max depth before blocking spawn
|
|
114
|
+
runWorkflowFn: typeof runWorkflow,
|
|
115
|
+
emitter?: DaemonEventEmitter,
|
|
116
|
+
): AgentTool
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`execute()` logic:
|
|
120
|
+
1. Depth check: if `currentDepth >= maxDepth`, return `{ childSessionId: null, outcome: 'error', notes: 'Max spawn depth exceeded ...' }` as JSON
|
|
121
|
+
2. Call `executeStartWorkflow({ workflowId, goal, workspacePath, context }, ctx, { parentSessionId: thisWorkrailSessionId })` and match result
|
|
122
|
+
3. On start error: return `{ childSessionId: null, outcome: 'error', notes: errorMessage }` as JSON
|
|
123
|
+
4. On start success: decode `childSessionId` from `startResult.response.continueToken` via `parseContinueTokenOrFail()`
|
|
124
|
+
5. Call `runWorkflowFn({ workflowId, goal, workspacePath, context, spawnDepth: currentDepth + 1, _preAllocatedStartResponse: startResult.response }, ctx, apiKey, undefined, emitter)`
|
|
125
|
+
6. Map `WorkflowRunResult` to `{ childSessionId, outcome, notes }`:
|
|
126
|
+
- `success`: `{ outcome: 'success', notes: result.lastStepNotes ?? '' }`
|
|
127
|
+
- `error`: `{ outcome: 'error', notes: result.message }`
|
|
128
|
+
- `timeout`: `{ outcome: 'timeout', notes: result.message }`
|
|
129
|
+
- `delivery_failed`: `{ outcome: 'error', notes: result.deliveryError }`
|
|
130
|
+
7. Return `{ content: [{ type: 'text', text: JSON.stringify(resultObj) }] }`
|
|
131
|
+
|
|
132
|
+
**Acceptance**: TypeScript compiles. Depth check fires at limit. All 4 result variants handled. `childSessionId` present in all returns (null only on depth error and start error).
|
|
133
|
+
**Risk**: Low (new function, no existing code changed).
|
|
134
|
+
|
|
135
|
+
### Slice 4: Inject spawn_agent in runWorkflow()
|
|
136
|
+
**File**: `src/daemon/workflow-runner.ts`
|
|
137
|
+
**Change**: Read `trigger.spawnDepth ?? 0` and `trigger.agentConfig?.maxSubagentDepth ?? 3` in `runWorkflow()`. Add `makeSpawnAgentTool(...)` to the `tools` array, using the decoded `workrailSessionId` as `thisWorkrailSessionId`.
|
|
138
|
+
|
|
139
|
+
Note: `workrailSessionId` is decoded from `startContinueToken` AFTER `executeStartWorkflow()`. The tool must be constructed after this decode, but the existing tool list construction already happens after this point (line ~1914).
|
|
140
|
+
|
|
141
|
+
**Acceptance**: TypeScript compiles. `runWorkflow()` signature unchanged. `spawn_agent` tool is in the tools list passed to `AgentLoop`.
|
|
142
|
+
**Risk**: Low. One new tool added to existing list.
|
|
143
|
+
|
|
144
|
+
### Slice 5: BASE_SYSTEM_PROMPT update
|
|
145
|
+
**File**: `src/daemon/workflow-runner.ts`
|
|
146
|
+
**Change**: Add `spawn_agent` to the tools section of `BASE_SYSTEM_PROMPT`. Document:
|
|
147
|
+
- When to use (delegate sub-tasks to a child WorkRail session)
|
|
148
|
+
- What it returns (`{ childSessionId, outcome, notes }`)
|
|
149
|
+
- Parent clock warning (maxSessionMinutes keeps ticking)
|
|
150
|
+
- Zombie session note (best-effort; cleanup in Phase 2)
|
|
151
|
+
- Depth limit note (default max depth 3)
|
|
152
|
+
**Acceptance**: Tool listed in system prompt with accurate description.
|
|
153
|
+
**Risk**: None.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 8. Test Design
|
|
158
|
+
|
|
159
|
+
**Note**: No unit tests exist for other tool factories in `workflow-runner.ts` (all integration-style). Adding unit tests for `makeSpawnAgentTool()` is aspirational; the acceptance criterion for Phase 1 is `npm run build` passing.
|
|
160
|
+
|
|
161
|
+
**What to verify manually**:
|
|
162
|
+
1. TypeScript compiles cleanly (`npm run build`)
|
|
163
|
+
2. Depth check: create a trigger with `spawnDepth: 3`, verify tool returns error immediately
|
|
164
|
+
3. `_preAllocatedStartResponse` path: verify child session is created in store before `runWorkflow()` starts
|
|
165
|
+
|
|
166
|
+
**If existing tests exist**, run them with `npm test` after each slice.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 9. Risk Register
|
|
171
|
+
|
|
172
|
+
| Risk | Severity | Mitigation |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| Zombie session (executeStartWorkflow succeeds, runWorkflow fails before AgentLoop) | MEDIUM | Document in tool description, Phase 2 cleanup |
|
|
175
|
+
| Parent timeout while child runs | LOW | Document in tool description, user configures maxSessionMinutes |
|
|
176
|
+
| session_created.data Zod strictness | LOW | Confirmed strip mode; unverified by migration test |
|
|
177
|
+
| _preAllocatedStartResponse removed in future refactor | LOW | Update JSDoc (O2 fix) to protect against this |
|
|
178
|
+
| workrailSessionId null at tool construction time | LOW | Tool construction happens AFTER decode; if decode fails, skip spawn_agent tool or construct with empty string |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 10. PR Packaging Strategy
|
|
183
|
+
|
|
184
|
+
**SinglePR**: `feat/daemon-spawn-agent-tool`
|
|
185
|
+
|
|
186
|
+
All 5 slices go into one PR. The slices are interdependent (Slice 4 requires Slice 3, Slice 3 uses Slice 2, etc.). Splitting across multiple PRs would leave the codebase in a broken intermediate state.
|
|
187
|
+
|
|
188
|
+
PR title: `feat(workflows): add spawn_agent tool for in-process child session delegation`
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 11. Philosophy Alignment Per Slice
|
|
193
|
+
|
|
194
|
+
| Slice | Principle | Status |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| Slice 1 (WorkflowTrigger extension) | Immutability: new fields are readonly | Satisfied |
|
|
197
|
+
| Slice 1 | Make illegal states unrepresentable: spawnDepth typed, not in context map | Satisfied |
|
|
198
|
+
| Slice 2 (schema extension) | Errors are data: Zod strip mode means extension is non-breaking | Satisfied |
|
|
199
|
+
| Slice 3 (makeSpawnAgentTool) | Errors are data: all variants return JSON, no throws | Satisfied |
|
|
200
|
+
| Slice 3 | Exhaustiveness: all 4 WorkflowRunResult variants handled | Satisfied |
|
|
201
|
+
| Slice 3 | DI for boundaries: ctx, apiKey, emitter injected | Satisfied |
|
|
202
|
+
| Slice 3 | Validate at boundaries: depth check first | Satisfied |
|
|
203
|
+
| Slice 4 (inject in runWorkflow) | YAGNI: no bare-prompt, no width guardrails | Satisfied |
|
|
204
|
+
| Slice 4 | Semaphore bypass invariant: direct runWorkflow(), not dispatch() | Satisfied |
|
|
205
|
+
| Slice 5 (system prompt) | Document 'why': parent clock and zombie warnings in tool description | Satisfied |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 12. Plan Metrics
|
|
210
|
+
|
|
211
|
+
- `implementationPlan`: 5 slices across 3 files
|
|
212
|
+
- `slices`: Slice 1 (WorkflowTrigger), Slice 2 (schema + buildInitialEvents), Slice 3 (makeSpawnAgentTool), Slice 4 (inject in runWorkflow), Slice 5 (BASE_SYSTEM_PROMPT)
|
|
213
|
+
- `testDesign`: npm run build + manual depth check + manual session creation verification
|
|
214
|
+
- `estimatedPRCount`: 1
|
|
215
|
+
- `followUpTickets`: Phase 2 (spawn_session + await_sessions), zombie cleanup, session tree query API, maxTotalAgentsPerTask guardrail
|
|
216
|
+
- `unresolvedUnknownCount`: 0 (all open questions from design phase resolved)
|
|
217
|
+
- `planConfidenceBand`: High
|