@exaudeus/workrail 3.35.1 → 3.37.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/config/config-file.js +2 -0
- package/dist/console-ui/assets/{index-D7jQyCSD.js → index-o-p__sHJ.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +5 -0
- package/dist/daemon/workflow-runner.js +131 -1
- package/dist/manifest.json +39 -31
- 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/trigger/notification-service.d.ts +42 -0
- package/dist/trigger/notification-service.js +164 -0
- package/dist/trigger/trigger-listener.js +7 -1
- package/dist/trigger/trigger-router.d.ts +3 -1
- package/dist/trigger/trigger-router.js +4 -1
- 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/design/agent-behavior-patterns-discovery.md +312 -0
- package/docs/design/agent-engine-communication-discovery.md +390 -0
- package/docs/design/agent-loop-architecture-alternatives-discovery.md +531 -0
- package/docs/design/agent-loop-error-handling-contract.md +238 -0
- package/docs/design/complete-step-approach-validation-discovery.md +344 -0
- package/docs/design/daemon-stuck-detection-discovery.md +174 -0
- package/docs/design/mcp-server-disconnect-discovery.md +245 -0
- package/docs/design/mcp-server-epipe-crash.md +198 -0
- package/docs/design/notification-design-candidates.md +131 -0
- package/docs/design/notification-design-review.md +84 -0
- package/docs/design/notification-implementation-plan.md +181 -0
- package/docs/design/spawn-agent-failure-modes.md +161 -0
- package/docs/design/spawn-agent-result-handling-implementation-plan.md +186 -0
- package/docs/design/stdio-simplification-design-candidates.md +341 -0
- package/docs/design/stdio-simplification-design-review.md +93 -0
- package/docs/design/stdio-simplification-implementation-plan.md +317 -0
- package/docs/design/structured-output-tools-coexist-findings.md +288 -0
- package/docs/discovery/coordinator-script-design.md +745 -0
- package/docs/discovery/coordinator-ux-discovery.md +471 -0
- package/docs/discovery/spawn-agent-failure-modes.md +309 -0
- package/docs/discovery/workflow-selection-for-discovery-tasks.md +336 -0
- package/docs/discovery/worktrain-status-briefing.md +325 -0
- package/docs/discovery/worktrain-status-design-candidates.md +202 -0
- package/docs/discovery/worktrain-status-design-review-findings.md +86 -0
- package/docs/ideas/backlog.md +688 -1
- package/docs/ideas/daemon-structured-output-vs-tool-calls.md +344 -0
- package/docs/ideas/design-candidates-backlog-consolidation.md +85 -0
- package/docs/ideas/design-candidates-spawn-agent-task.md +178 -0
- package/docs/ideas/design-review-findings-backlog-consolidation.md +39 -0
- package/docs/ideas/design-review-findings-spawn-agent-task.md +139 -0
- package/docs/ideas/implementation_plan_backlog_consolidation.md +117 -0
- package/docs/ideas/implementation_plan_spawn_agent.md +217 -0
- package/docs/plans/authoring-doc-staleness-enforcement-candidates.md +251 -0
- package/docs/plans/authoring-doc-staleness-enforcement-review.md +99 -0
- package/docs/plans/authoring-doc-staleness-enforcement.md +463 -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,117 @@
|
|
|
1
|
+
# Implementation Plan: Backlog Consolidation from docs/coordinator-and-scripts-spec
|
|
2
|
+
|
|
3
|
+
## 1. Problem Statement
|
|
4
|
+
|
|
5
|
+
Five sections were added to `origin/docs/coordinator-and-scripts-spec` (branch commit `ed9c9aaa`) and never merged to main. Main has since grown to 5330 lines with other content. The branch is stale (2088 lines total) and cannot be merged directly. The missing sections must be inserted at the correct chronological position in main's `docs/ideas/backlog.md`.
|
|
6
|
+
|
|
7
|
+
**Missing sections:**
|
|
8
|
+
1. `### Scripts-first coordinator: avoid the main agent wherever possible (Apr 15, 2026)` (branch lines 1776-1821)
|
|
9
|
+
2. `### Full development pipeline: coordinator scripts drive multi-phase autonomous work (Apr 15, 2026)` (branch lines 1822-1922)
|
|
10
|
+
3. `### Additional coordinator pipeline templates (Apr 15, 2026)` with subsections: Backlog grooming coordinator, Bug investigation + fix coordinator, Incident monitoring coordinator (branch lines 1923-2028)
|
|
11
|
+
4. `### Interactive ideation: WorkTrain as a thinking partner with full project context (Apr 15, 2026)` (branch lines 2029-2056)
|
|
12
|
+
5. `### Automatic gap and improvement detection: proactive WorkTrain (Apr 15, 2026)` (branch lines 2057-2088)
|
|
13
|
+
|
|
14
|
+
## 2. Acceptance Criteria
|
|
15
|
+
|
|
16
|
+
- All five section headings are present in `docs/ideas/backlog.md` on main after the change
|
|
17
|
+
- Line count of `docs/ideas/backlog.md` >= 5640 (5330 + ~313 inserted lines)
|
|
18
|
+
- The sections appear between the Verification/Proof records section and `### Dynamic model selection`
|
|
19
|
+
- No existing content on main is removed or altered
|
|
20
|
+
- No doubled `---` separator at the insertion boundary
|
|
21
|
+
|
|
22
|
+
## 3. Non-Goals
|
|
23
|
+
|
|
24
|
+
- Modifying any code files
|
|
25
|
+
- Processing `docs/apr-17-18-backlog-updates` branch (user confirmed fully subsumed)
|
|
26
|
+
- Deduplicating conceptually overlapping content (the backlog is a running log, not a deduplicated spec)
|
|
27
|
+
- Changing any existing section in `docs/ideas/backlog.md`
|
|
28
|
+
|
|
29
|
+
## 4. Philosophy-Driven Constraints
|
|
30
|
+
|
|
31
|
+
- **NEVER push directly to main** -- create feature branch `docs/consolidate-coordinator-specs`, commit there, open PR
|
|
32
|
+
- Commit message: `docs(backlog): consolidate missing coordinator specs from stale branch` (72 chars, no period)
|
|
33
|
+
- No em-dashes in commit message or any new written content
|
|
34
|
+
|
|
35
|
+
## 5. Invariants
|
|
36
|
+
|
|
37
|
+
- Insertion order: new content appears before `### Dynamic model selection: right model for the right task (Apr 15, 2026)` and after the `---` separator at current main line 1781
|
|
38
|
+
- No `---` separator is added to the start or end of the inserted block (main line 1781 already serves as the boundary separator)
|
|
39
|
+
- All five sections are inserted as a single contiguous block (they form a cohesive unit from a single branch commit)
|
|
40
|
+
|
|
41
|
+
## 6. Selected Approach + Rationale + Runner-Up
|
|
42
|
+
|
|
43
|
+
**Selected:** Use the Edit tool to insert the extracted block (branch lines 1776-2088) immediately before `### Dynamic model selection: right model for the right task (Apr 15, 2026)` in main's backlog. The `old_string` is the exact heading line; `new_string` is the inserted content followed by the heading.
|
|
44
|
+
|
|
45
|
+
**Rationale:** Only one file to edit. The insertion point is unambiguous and verified. Text insertion is correct because the branch file is a strict subset of main -- a git cherry-pick would regress the file.
|
|
46
|
+
|
|
47
|
+
**Runner-Up:** Insert only the two explicitly requested sections (Full dev pipeline + Backlog grooming coordinator). Lost because all five form a cohesive single-commit block; leaving three out is arbitrary.
|
|
48
|
+
|
|
49
|
+
## 7. Vertical Slices
|
|
50
|
+
|
|
51
|
+
### Slice 1: Create feature branch
|
|
52
|
+
- `git checkout -b docs/consolidate-coordinator-specs`
|
|
53
|
+
- Acceptance: branch exists, HEAD is at latest main commit
|
|
54
|
+
|
|
55
|
+
### Slice 2: Insert missing sections
|
|
56
|
+
- Edit `docs/ideas/backlog.md`: insert branch lines 1776-2088 before `### Dynamic model selection`
|
|
57
|
+
- Acceptance: all five headings present in file, line count >= 5640, no doubled `---`
|
|
58
|
+
|
|
59
|
+
### Slice 3: Commit
|
|
60
|
+
- `git add docs/ideas/backlog.md`
|
|
61
|
+
- `git commit -m "docs(backlog): consolidate missing coordinator specs from stale branch"`
|
|
62
|
+
- Acceptance: clean commit with only `docs/ideas/backlog.md` changed
|
|
63
|
+
|
|
64
|
+
### Slice 4: Push and open PR
|
|
65
|
+
- `git push -u origin docs/consolidate-coordinator-specs`
|
|
66
|
+
- `gh pr create` with appropriate title and body
|
|
67
|
+
- Acceptance: PR URL returned, PR is open, targets main
|
|
68
|
+
|
|
69
|
+
## 8. Test Design
|
|
70
|
+
|
|
71
|
+
**Post-edit verification:**
|
|
72
|
+
```bash
|
|
73
|
+
wc -l docs/ideas/backlog.md # should be >= 5640
|
|
74
|
+
grep -c "Scripts-first coordinator" docs/ideas/backlog.md # should be 1
|
|
75
|
+
grep -c "Full development pipeline: coordinator scripts drive" docs/ideas/backlog.md # should be 1
|
|
76
|
+
grep -c "Additional coordinator pipeline templates" docs/ideas/backlog.md # should be 1
|
|
77
|
+
grep -c "Interactive ideation: WorkTrain as a thinking partner" docs/ideas/backlog.md # should be 1
|
|
78
|
+
grep -c "Automatic gap and improvement detection" docs/ideas/backlog.md # should be 1
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Separator check:**
|
|
82
|
+
```bash
|
|
83
|
+
grep -n "^---" docs/ideas/backlog.md | grep -A1 -B1 "178[0-2]" # verify no doubled separator
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 9. Risk Register
|
|
87
|
+
|
|
88
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
89
|
+
|------|-----------|--------|-----------|
|
|
90
|
+
| Doubled `---` at boundary | Low | Low (visual) | Branch block verified not to start/end with `---` |
|
|
91
|
+
| Content duplication | None | Medium | All five headings confirmed absent from main via grep |
|
|
92
|
+
| Wrong insertion point | None | Medium | Main line 1782 verified as `### Dynamic model selection` |
|
|
93
|
+
|
|
94
|
+
## 10. PR Packaging Strategy
|
|
95
|
+
|
|
96
|
+
Single PR: `docs/consolidate-coordinator-specs` -> `main`
|
|
97
|
+
- Title: `docs(backlog): consolidate missing coordinator specs from stale branch`
|
|
98
|
+
- All five sections in one commit, one PR
|
|
99
|
+
|
|
100
|
+
## 11. Philosophy Alignment Per Slice
|
|
101
|
+
|
|
102
|
+
| Slice | Principle | Status |
|
|
103
|
+
|-------|-----------|--------|
|
|
104
|
+
| Create branch | NEVER push to main | Satisfied -- feature branch created |
|
|
105
|
+
| Insert sections | Surface information | Satisfied -- all missing content recovered |
|
|
106
|
+
| Commit | Commit format `docs(backlog):` | Satisfied |
|
|
107
|
+
| PR | NEVER push to main | Satisfied -- PR, not direct push |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
- `implementationPlan`: complete
|
|
112
|
+
- `slices`: 4
|
|
113
|
+
- `testDesign`: grep-based line count + heading presence checks
|
|
114
|
+
- `estimatedPRCount`: 1
|
|
115
|
+
- `followUpTickets`: none
|
|
116
|
+
- `unresolvedUnknownCount`: 0
|
|
117
|
+
- `planConfidenceBand`: High
|
|
@@ -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
|