@exaudeus/workrail 3.35.0 → 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-B10Bn8qC.js → index-n8cJrS4v.js} +2 -2
- 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 +24 -24
- 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 +250 -0
- 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,139 @@
|
|
|
1
|
+
# Design Review Findings: spawn_agent Tool Implementation
|
|
2
|
+
|
|
3
|
+
_Concise, actionable findings for main-agent synthesis. Design: Candidate 2 (pre-create session with _preAllocatedStartResponse, then blocking runWorkflow())._
|
|
4
|
+
|
|
5
|
+
> Note: Full discovery-phase review is in `design-review-findings-spawn-agent.md`. This file is for the current coding task review pass.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tradeoff Review
|
|
10
|
+
|
|
11
|
+
### T1: Parent clock keeps ticking while child runs
|
|
12
|
+
- Confirmed acceptable. Success criterion 2 ('parent does not advance until child completes') is satisfied even on timeout (parent aborts, not advances).
|
|
13
|
+
- When parent times out, child continues as orphaned session. Work is preserved in session store. Session tree preserves the parent-child link.
|
|
14
|
+
- Mitigation needed: document in tool description.
|
|
15
|
+
- **Status: ACCEPTED.**
|
|
16
|
+
|
|
17
|
+
### T2: _preAllocatedStartResponse comment needs update
|
|
18
|
+
- Current comment: 'set only by the dispatch HTTP handler.' spawn_agent will be another legitimate internal caller.
|
|
19
|
+
- If not updated, future developer may remove spawn_agent support as accidental usage.
|
|
20
|
+
- **Status: REQUIRED FIX (low effort, Step 1.1).**
|
|
21
|
+
|
|
22
|
+
### T3: One extra async call in execute()
|
|
23
|
+
- executeStartWorkflow() is ~10-50ms (no LLM call). Negligible for a tool that blocks 1-30 minutes.
|
|
24
|
+
- **Status: ACCEPTED.**
|
|
25
|
+
|
|
26
|
+
### T4: session_created.data extension
|
|
27
|
+
- Confirmed `z.object({})` uses strip mode (not `.strict()`). Extension with `parentSessionId?: z.string().optional()` is backward-compatible.
|
|
28
|
+
- `buildInitialEvents()` currently hardcodes `data: {}` -- requires threading `parentSessionId` parameter.
|
|
29
|
+
- **Status: REQUIRED, LOW RISK.**
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Failure Mode Review
|
|
34
|
+
|
|
35
|
+
### FM1: Parent timeout while child is running
|
|
36
|
+
- Severity: LOW. Child completes normally, work preserved, session tree intact.
|
|
37
|
+
- Design coverage: adequate. Orphaned child traceable via parentSessionId.
|
|
38
|
+
- **No revision required.**
|
|
39
|
+
|
|
40
|
+
### FM2: executeStartWorkflow() succeeds, runWorkflow() fails before AgentLoop starts
|
|
41
|
+
- Severity: MEDIUM. Zombie session in store (shows as 'running' indefinitely).
|
|
42
|
+
- Design coverage: partial. Parent gets `{ childSessionId, outcome: 'error', notes: errorMessage }` -- child session is observable. But zombie cleanup is deferred.
|
|
43
|
+
- Mitigation for Phase 1: document as known edge case. Phase 2: session timeout/zombie cleanup.
|
|
44
|
+
- **Status: ACCEPTED for Phase 1.**
|
|
45
|
+
|
|
46
|
+
### FM3: spawnDepth propagation failure
|
|
47
|
+
- Severity: HIGH if unmitigated. FULLY MITIGATED by using typed `readonly spawnDepth?: number` field on `WorkflowTrigger`.
|
|
48
|
+
- After fix: severity drops to LOW (depth is typed, cannot be accidentally lost).
|
|
49
|
+
- **Status: MITIGATED.**
|
|
50
|
+
|
|
51
|
+
### FM4: Depth bypass via width (sequential spawning)
|
|
52
|
+
- Severity: LOW for Phase 1. `maxSessionMinutes` on parent is the practical limit.
|
|
53
|
+
- **Status: ACCEPTED, deferred to Phase 2.**
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Runner-Up / Simpler Alternative Review
|
|
58
|
+
|
|
59
|
+
**Candidate 1 (direct runWorkflow, no pre-create):** Close alternative. Simpler execute() -- no executeStartWorkflow() call. Loses: `childSessionId` is unknown until after runWorkflow() starts; crash-before-start has no childSessionId to return.
|
|
60
|
+
|
|
61
|
+
**No elements worth borrowing from Candidate 1.** C2 already does everything C1 does plus the session-ID-upfront guarantee.
|
|
62
|
+
|
|
63
|
+
**Could skip session_created.data extension?** Technically yes -- `parentSessionId` in `context_set` events is still durable and queryable. But the extension is ~8 lines total and future-proofs DAG queries. Keep it.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Philosophy Alignment
|
|
68
|
+
|
|
69
|
+
### Clearly satisfied
|
|
70
|
+
- Errors as data: discriminated union return, no throws
|
|
71
|
+
- DI for boundaries: ctx, apiKey, emitter all injected at construction time
|
|
72
|
+
- Immutability: WorkflowTrigger fully readonly, new fields also readonly
|
|
73
|
+
- Exhaustiveness: WorkflowRunResult match handles all 4 variants
|
|
74
|
+
- Validate at boundaries: depth check at start of execute()
|
|
75
|
+
- YAGNI: Phase 1 only; non-blocking spawn deferred
|
|
76
|
+
- Make illegal states unrepresentable: childSessionId always present (pre-create guarantees it)
|
|
77
|
+
|
|
78
|
+
### Under tension (acceptable)
|
|
79
|
+
- Architectural fixes over patches: parentSessionId via internalContext is somewhat patch-like. Acceptable because internalContext is an established pattern for daemon-internal injection (is_autonomous, workspacePath). Tension is low.
|
|
80
|
+
- Compose with small pure functions: execute() has two async operations (~50 lines). Complexity is necessary and bounded.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Findings
|
|
85
|
+
|
|
86
|
+
### Red (blocking)
|
|
87
|
+
_None._
|
|
88
|
+
|
|
89
|
+
### Orange (required before implementation)
|
|
90
|
+
|
|
91
|
+
**O1: Use `readonly spawnDepth?: number` on WorkflowTrigger (not `context.spawnDepth`)**
|
|
92
|
+
Rationale: generic context map can be silently overwritten by trigger system or other callers, breaking depth enforcement. Typed field makes the invariant explicit and compiler-checked.
|
|
93
|
+
Files: `src/daemon/workflow-runner.ts` (WorkflowTrigger type definition)
|
|
94
|
+
**Status: Design already incorporates this fix.**
|
|
95
|
+
|
|
96
|
+
**O2: Update `_preAllocatedStartResponse` comment to list spawn_agent as legitimate caller**
|
|
97
|
+
Rationale: current comment misleads future developers. Without this update, spawn_agent support could be removed as accidental.
|
|
98
|
+
Files: `src/daemon/workflow-runner.ts` (WorkflowTrigger._preAllocatedStartResponse JSDoc)
|
|
99
|
+
**Status: Must be applied during implementation.**
|
|
100
|
+
|
|
101
|
+
**O3: Thread parentSessionId into buildInitialEvents() for session_created.data**
|
|
102
|
+
Rationale: the `internalContext` injection only reaches `context_set` events, not `session_created.data`. For the typed schema extension to work, `buildInitialEvents()` needs a new optional parameter.
|
|
103
|
+
Files: `src/mcp/handlers/v2-execution/start.ts` (`buildInitialEvents()` signature and call site)
|
|
104
|
+
**Status: Required -- not in original design review, discovered during implementation analysis.**
|
|
105
|
+
|
|
106
|
+
### Yellow (should-fix, not blocking)
|
|
107
|
+
|
|
108
|
+
**Y1: Document parent-clock behavior in tool description**
|
|
109
|
+
The spawn_agent tool description should note that the parent session's maxSessionMinutes clock runs while the child executes. Workflow authors must configure the parent's timeout to be longer than the expected child duration.
|
|
110
|
+
|
|
111
|
+
**Y2: Document zombie session edge case**
|
|
112
|
+
The spawn_agent tool description should note that if runWorkflow() fails before the AgentLoop starts, a zombie session may exist in the store. Phase 2 will add cleanup.
|
|
113
|
+
|
|
114
|
+
**Y3: maxSubagentDepth source**
|
|
115
|
+
For Phase 1, default to 3 if `WorkflowTrigger.agentConfig?.maxSubagentDepth` is not set. Document in tool description.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Recommended Revisions
|
|
120
|
+
|
|
121
|
+
1. Use `readonly spawnDepth?: number` on WorkflowTrigger (O1 -- design already incorporates)
|
|
122
|
+
2. Update `_preAllocatedStartResponse` JSDoc to list spawn_agent as a legitimate internal caller (O2)
|
|
123
|
+
3. Add `parentSessionId?: string` parameter to `buildInitialEvents()` and thread it into `session_created.data` (O3)
|
|
124
|
+
4. Add parent-clock behavior documentation to spawn_agent tool description (Y1)
|
|
125
|
+
5. Add zombie session documentation to spawn_agent tool description (Y2)
|
|
126
|
+
6. Use `trigger.agentConfig?.maxSubagentDepth ?? 3` as maxDepth (Y3)
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Residual Concerns
|
|
131
|
+
|
|
132
|
+
**RC1: Session tree query infrastructure deferred to Phase 2**
|
|
133
|
+
`parentSessionId` is written to the session store but no query path exists to read 'all children of session X'. The console DAG view cannot render the tree until Phase 2. This is by design -- Phase 1 writes the data; Phase 2 builds the reader.
|
|
134
|
+
|
|
135
|
+
**RC2: Zod strictness on session_created.data**
|
|
136
|
+
Confirmed that `z.object({})` uses strip mode (not strict). Extension with `parentSessionId?: z.string().optional()` is backward-compatible. Unverified by an actual migration run -- low risk but unvalidated.
|
|
137
|
+
|
|
138
|
+
**RC3: maxTotalAgentsPerTask guardrail deferred**
|
|
139
|
+
Phase 1 enforces depth only. Wide spawning is not caught by depth limits. Phase 2 adds the concurrency registry. For Phase 1, `maxSessionMinutes` on the parent session is the practical limit on total work done.
|
|
@@ -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
|