Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ agent:
lru:
size: 100
ttl: 600000
process:
subAgent:
timeout: 600000
maxConcurrent: 4
sessionMode: isolated
defaultStrategy: parallel
defaultOnError: continue
persistence:
mode: memory
sqlite_path: memory/checkpoints.db
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-21
94 changes: 94 additions & 0 deletions openspec/changes/archive/2026-06-21-subagent-tool/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
## Context

The madz agent system currently supports single-agent execution via `node index.js "PROMPT" sessionsDir`. The compaction tool already demonstrates this pattern — it spawns a child process, captures stdout, and parses it using a marker-based approach. The user wants to generalize this pattern into a reusable `subAgent` tool that enables hierarchical agent execution: the parent agent can decompose complex tasks, spawn sub-agents to handle independent sub-tasks, and aggregate results.

The closest existing pattern is `src/tools/compaction.js`, which uses `spawn("node", [indexPath, `"${command}"`, sessionsDir])` with a `# Compaction` marker. The terminal tool provides `processTracker` for PID tracking and process lifecycle management.

## Goals / Non-Goals

**Goals:**
- Single execution mode: spawn one sub-agent with delegation + context, return structured result
- Fan-out mode: spawn multiple sub-agents in parallel or sequential batches with configurable concurrency
- Marker-based stdout parsing: sub-agents prepend `# SubAgent` marker; parent splits on it
- Response contract: `{ ok, result, error? }` matching compaction tool pattern
- Process tracking via shared `processTracker` from terminal.js
- Configurable timeouts (per-call > env var > config default)
- Optional parameter extraction from JSON results via `returnParams`
- Session isolation modes: isolated (fresh), forked (compaction in new session), shared (parent session)

**Non-Goals:**
- Changes to `index.js` entry point — leverages existing pattern
- TUI changes or UI modifications
- Skill system changes
- HTTP-based sub-agents (rejected in issue — don't give full tool ecosystem access)
- In-process sub-agents (rejected — no process isolation)
- Full session dump to sub-agents (rejected — too much context, wastes tokens)

## Decisions

### Decision 1: Marker-based stdout parsing (not JSON lines or HTTP)
**Choice:** Use `# SubAgent` marker prefix, split stdout on marker to extract result.
**Rationale:** Mirrors compaction tool pattern exactly. Simple, reliable, works with any sub-agent output format. The sub-agent prepends the marker; the parent splits and takes everything after the first occurrence.
**Alternatives considered:**
- JSON lines: Requires sub-agent to format output as JSON, adds parsing complexity
- HTTP endpoint: Rejected in issue — loses full tool ecosystem access
- In-process: Rejected — no isolation, state corruption risk

### Decision 2: Shared processTracker from terminal.js
**Choice:** Reuse `processTracker` Map and `trackProcess` function from `src/tools/terminal.js`.
**Rationale:** Avoids duplicating process management logic. Enables consistent PID tracking, status reporting, and lifecycle management across all process-spawning tools.
**Trade-off:** Tighter coupling between terminal.js and subAgent.js, but both are internal tools with shared lifecycle.

### Decision 3: Timeout resolution priority
**Choice:** Per-call `timeout` parameter > `MADZ_SUBAGENT_TIMEOUT` env var > `config.yaml` default.
**Rationale:** Follows the principle of least surprise — explicit per-call overrides take precedence, environment variables provide runtime flexibility, config provides sensible defaults.
**Implementation:** Resolve timeout in this order at spawn time; pass to `spawn()` options.

### Decision 4: Fan-out concurrency control
**Choice:** Use a semaphore pattern with `maxConcurrent` limit. Tasks are queued; when a slot opens (process exits), the next task starts.
**Rationale:** Prevents resource exhaustion. For parallel mode, bounded by `maxConcurrent`. For sequential mode, effectively `maxConcurrent: 1`.
**Implementation:** Track active processes; when count < maxConcurrent, dequeue next task.

### Decision 5: Prompt structure with `|||` separator
**Choice:** `[context] ||| [delegation]` — sub-agent recognizes `|||` and treats everything after as the task.
**Rationale:** Simple, unambiguous separator. Sub-agent can parse it and know what context vs instruction is. Works with any content in either section.
**Escaping:** Full prompt is shell-escaped (quotes, backticks, dollar signs, newlines) before passing to spawn.

### Decision 6: Session isolation modes
**Choice:** Three modes — `isolated` (fresh session, default), `forked` (compaction in new session), `shared` (parent session).
**Rationale:** Different use cases need different isolation levels. Isolated is safest (no state leakage). Forked provides context without full session dump. Shared is for special cases where sub-agent needs parent's full context (not recommended for fan-out).
**Implementation:** Pass session mode to spawned process; index.js respects it when initializing agent.

## Risks / Trade-offs

### Risk: Shell injection via prompt content
**Mitigation:** Full shell escaping of prompt before passing to spawn. The command is passed as a quoted argument: `spawn("node", [indexPath, `"${escapedPrompt}"`, sessionsDir])`. This is the same pattern used by compaction tool.

### Risk: Boot overhead for fan-out
**Mitigation:** Each `node index.js` spawn loads config, tools, skills (~seconds). Bounded by `maxConcurrent` to prevent resource exhaustion. For frequent small tasks, consider batching.

### Risk: Recursive sub-agent spawning
**Mitigation:** Sub-agents can spawn their own sub-agents. Track depth counter; decrement on each recursive spawn. Max depth bounded by `maxConcurrent` (not n+1 per level).

### Risk: Large sub-agent outputs
**Mitigation:** `returnParams` allows filtering to specific keys. If output is very large and no params specified, parent can truncate or summarize. Consider adding a `maxOutputSize` config option in future.

### Risk: Process cleanup on error
**Mitigation:** SIGTERM → SIGKILL graceful termination. ProcessTracker tracks all spawned processes; cleanup on parent exit. Timeout enforcement prevents hung processes.

## Migration Plan

This is a new feature with no migration required. The tool is opt-in — it only registers when `process:spawn` permission is enabled in config. Existing agents and tools are unaffected.

1. Add `src/tools/subAgent.js`
2. Register in `src/tools/index.js` (TOOL_PERMISSIONS, TOOL_FACTORIES)
3. Add `process.subAgent` config to `config.yaml`
4. Add unit tests
5. Verify existing test suite passes
6. Verify application starts without crashing

## Open Questions

1. **Session mode default:** `isolated` is the safest default, but `forked` might be more useful for most use cases. Current decision: `isolated` as default, user can change in config.
2. **Recursive depth limit:** Should there be a hard max depth (e.g., 3 levels)? Current approach: bounded by `maxConcurrent` which implicitly limits depth. May need explicit depth counter in future.
3. **Output size limits:** Should there be a configurable max output size? Current approach: no limit, but `returnParams` helps filter. May add `maxOutputSize` config in future.
28 changes: 28 additions & 0 deletions openspec/changes/archive/2026-06-21-subagent-tool/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Why

The user wants to enable hierarchical agent execution — spawning sub-agents as child processes to handle discrete tasks. This allows for single delegation (offload a complex task with full context), fan-out (spawn multiple sub-agents in parallel or sequence to handle independent sub-tasks), and intelligent orchestration (parent decomposes complex tasks, decides which are independent vs dependent, and aggregates results).

## What Changes

- Add new `subAgent` tool that spawns `node index.js "PROMPT"` as child processes
- Support single execution mode with delegation instruction and optional context
- Support fan-out mode with parallel/sequential strategies, configurable concurrency, and error handling
- Add marker-based stdout parsing (`# SubAgent` marker) for result extraction
- Add `process.subAgent` configuration section to `config.yaml`
- Register tool in `src/tools/index.js` with `process:spawn` permission gate

## Capabilities

### New Capabilities
- `subagent`: Tool for spawning child-process agents with single execution and fan-out modes, marker-based result parsing, configurable timeouts and concurrency

### Modified Capabilities
<!-- None — no existing spec-level behavior changes -->

## Impact

- **New file:** `src/tools/subAgent.js` — tool implementation
- **Modified files:** `src/tools/index.js` — TOOL_PERMISSIONS, TOOL_FACTORIES, buildToolConfig
- **Modified files:** `config.yaml` — add `process.subAgent` configuration section
- **Dependencies:** Reuses `processTracker` from `src/tools/terminal.js`, mirrors spawn pattern from `src/tools/compaction.js`
- **Entry point:** No changes to `index.js` — leverages existing `node index.js "PROMPT" sessionsDir` pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
## ADDED Requirements

### Requirement: Sub-agent tool spawns child processes
The subAgent tool SHALL spawn `node index.js "PROMPT" sessionsDir` as an independent child process, inheriting the parent's environment variables while maintaining session isolation.

#### Scenario: Single execution spawns process
- **WHEN** user calls subAgent with a delegation instruction and context
- **THEN** the tool spawns a node process with the constructed prompt and returns a structured result

#### Scenario: Process inherits environment
- **WHEN** a sub-agent is spawned
- **THEN** it inherits the parent process's environment variables (API keys, config paths)

### Requirement: Prompt construction with separator
The subAgent tool SHALL construct the sub-agent prompt by combining context and delegation instruction, separated by `|||`. The sub-agent recognizes this separator and treats everything after it as the delegation instruction.

#### Scenario: Prompt includes context and delegation
- **WHEN** subAgent is called with context and delegation
- **THEN** the prompt is constructed as `[context] ||| [delegation]`

#### Scenario: Prompt is shell-escaped
- **WHEN** the prompt contains quotes, backticks, dollar signs, or newlines
- **THEN** the prompt is properly escaped for shell argument passing

### Requirement: Marker-based stdout parsing
The subAgent tool SHALL parse sub-agent output by splitting stdout on the `# SubAgent` marker. If the marker is missing, the tool returns `{ ok: false, error: "..." }`.

#### Scenario: Valid marker returns result
- **WHEN** sub-agent output contains `# SubAgent` marker
- **THEN** the tool returns `{ ok: true, result: "<content after marker>" }`

#### Scenario: Missing marker returns error
- **WHEN** sub-agent output does not contain `# SubAgent` marker
- **THEN** the tool returns `{ ok: false, error: "Marker not found in output" }`

#### Scenario: Empty result after marker returns error
- **WHEN** marker is present but no content follows
- **THEN** the tool returns `{ ok: false, error: "No content after marker" }`

### Requirement: Response contract
The subAgent tool SHALL return a structured result matching the compaction tool pattern: `{ ok: boolean, result: string, error?: string }`.

#### Scenario: Successful execution
- **WHEN** sub-agent completes successfully
- **THEN** result is `{ ok: true, result: "<sub-agent output>" }`

#### Scenario: Failed execution
- **WHEN** sub-agent fails or times out
- **THEN** result is `{ ok: false, error: "<error description>" }`

### Requirement: Single execution mode
The subAgent tool SHALL support single execution mode with optional `returnParams` for JSON result filtering.

#### Scenario: Single execution without returnParams
- **WHEN** subAgent is called with delegation and context but no returnParams
- **THEN** the full sub-agent output is returned as the result

#### Scenario: Single execution with returnParams
- **WHEN** subAgent is called with returnParams `["findings", "recommendations"]`
- **THEN** the result is filtered to only include those keys from the JSON output

#### Scenario: returnParams with non-JSON output
- **WHEN** sub-agent output is not valid JSON and returnParams is specified
- **THEN** the tool falls back to returning full text

### Requirement: Fan-out mode — parallel execution
The subAgent tool SHALL support fan-out mode with parallel strategy, bounded by `maxConcurrent` limit.

#### Scenario: Parallel fan-out respects maxConcurrent
- **WHEN** subAgent is called with tasks, strategy "parallel", and maxConcurrent 3
- **THEN** at most 3 sub-agents run simultaneously; remaining tasks queue

#### Scenario: Parallel fan-out aggregates results
- **WHEN** all parallel tasks complete
- **THEN** results are combined into a single aggregated result string

#### Scenario: Parallel fan-out with continue on error
- **WHEN** a task fails and onError is "continue"
- **THEN** remaining tasks continue executing; failed task result is marked with error

#### Scenario: Parallel fan-out with fail-fast on error
- **WHEN** a task fails and onError is "fail-fast"
- **THEN** remaining queued tasks are cancelled; partial results are returned

### Requirement: Fan-out mode — sequential execution
The subAgent tool SHALL support fan-out mode with sequential strategy, running tasks one at a time.

#### Scenario: Sequential fan-out runs one at a time
- **WHEN** subAgent is called with tasks and strategy "sequential"
- **THEN** tasks execute one after another in order

#### Scenario: Sequential fan-out with continue on error
- **WHEN** a task fails and onError is "continue"
- **THEN** remaining tasks continue executing

#### Scenario: Sequential fan-out with fail-fast on error
- **WHEN** a task fails and onError is "fail-fast"
- **THEN** remaining tasks are cancelled; partial results are returned

### Requirement: Timeout enforcement
The subAgent tool SHALL enforce timeouts with priority: per-call `timeout` parameter > `MADZ_SUBAGENT_TIMEOUT` env var > `config.yaml` default.

#### Scenario: Per-call timeout overrides config
- **WHEN** subAgent is called with timeout 30000 and config default is 60000
- **THEN** the sub-agent uses 30000ms timeout

#### Scenario: Env var overrides config
- **WHEN** MADZ_SUBAGENT_TIMEOUT is set to 45000 and config default is 60000
- **THEN** the sub-agent uses 45000ms timeout (no per-call override)

#### Scenario: Timeout kills process
- **WHEN** sub-agent exceeds its timeout
- **THEN** the process receives SIGTERM, then SIGKILL after 5 seconds

### Requirement: Process tracking
The subAgent tool SHALL track spawned processes using the shared `processTracker` from terminal.js, enabling PID tracking, status reporting, and graceful termination.

#### Scenario: Process is tracked on spawn
- **WHEN** a sub-agent is spawned
- **THEN** it is recorded in processTracker with PID, command, status "running", and startTime

#### Scenario: Process status updates on exit
- **WHEN** a tracked sub-agent exits
- **THEN** its status is updated to "exited" (code 0) or "exited:<code>" (non-zero)

### Requirement: Session isolation modes
The subAgent tool SHALL support three session isolation modes: `isolated` (fresh session), `forked` (compaction in new session), `shared` (parent session).

#### Scenario: Isolated mode creates fresh session
- **WHEN** sessionMode is "isolated" (default)
- **THEN** the sub-agent receives a fresh, empty session

#### Scenario: Forked mode passes compaction
- **WHEN** sessionMode is "forked"
- **THEN** the sub-agent receives a compaction of the parent's context in a new short-lived session

#### Scenario: Shared mode uses parent session
- **WHEN** sessionMode is "shared"
- **THEN** the sub-agent writes to the parent's session (not recommended for fan-out)

### Requirement: Tool registration
The subAgent tool SHALL be registered in `src/tools/index.js` with `process:spawn` permission gate, following the factory pattern used by other tools.

#### Scenario: Tool registers when permission enabled
- **WHEN** `process:spawn` is in the enabled permissions set
- **THEN** the subAgent tool is included in the built tool array

#### Scenario: Tool does not register when permission disabled
- **WHEN** `process:spawn` is not in the enabled permissions set
- **THEN** the subAgent tool is excluded from the built tool array

### Requirement: Configuration
The subAgent tool SHALL be configured via `config.yaml` under `process.subAgent` with settings for timeout, maxConcurrent, sessionMode, defaultStrategy, and defaultOnError.

#### Scenario: Config section exists
- **WHEN** config.yaml is loaded
- **THEN** `process.subAgent` section contains timeout, maxConcurrent, sessionMode, defaultStrategy, defaultOnError

#### Scenario: Config defaults are applied
- **WHEN** no per-call or env var overrides are provided
- **THEN** config defaults are used for timeout, maxConcurrent, sessionMode, defaultStrategy, defaultOnError
59 changes: 59 additions & 0 deletions openspec/changes/archive/2026-06-21-subagent-tool/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## 1. Setup and Configuration

- [x] 1.1 Add `process.subAgent` configuration section to config.yaml (timeout, maxConcurrent, sessionMode, defaultStrategy, defaultOnError)
- [x] 1.2 Create src/tools/subAgent.js module with subAgent implementation
- [x] 1.3 Export createSubAgentTool factory from subAgent.js

## 2. Core Implementation — Single Execution

- [x] 2.1 Implement prompt construction: combine context + delegation with `|||` separator
- [x] 2.2 Implement prompt shell escaping (quotes, backticks, dollar signs, newlines)
- [x] 2.3 Implement spawn logic: `spawn("node", [indexPath, escapedPrompt, sessionsDir])` mirroring compaction tool
- [x] 2.4 Implement marker-based stdout parsing: split on `# SubAgent`, return `{ ok, result, error? }`
- [x] 2.5 Implement timeout resolution: per-call > env var > config default
- [x] 2.6 Implement process tracking via shared processTracker from terminal.js
- [x] 2.7 Implement graceful termination: SIGTERM → SIGKILL on timeout

## 3. Core Implementation — Fan-out Mode

- [x] 3.1 Implement fan-out task queue with parallel/sequential strategy support
- [x] 3.2 Implement maxConcurrent semaphore for parallel mode
- [x] 3.3 Implement onError handling: "continue" vs "fail-fast"
- [x] 3.4 Implement result aggregation for fan-out mode

## 4. Core Implementation — Parameter Extraction

- [x] 4.1 Implement returnParams filtering: parse JSON result, filter to specified keys
- [x] 4.2 Implement fallback to full text when output is not valid JSON

## 5. Core Implementation — Session Isolation

- [x] 5.1 Implement sessionMode: "isolated" (fresh session)
- [x] 5.2 Implement sessionMode: "forked" (compaction in new session)
- [x] 5.3 Implement sessionMode: "shared" (parent session)

## 6. Tool Registration

- [x] 6.1 Add `subAgent` to TOOL_PERMISSIONS in src/tools/index.js (requires process:spawn)
- [x] 6.2 Add `subAgent` to TOOL_FACTORIES in src/tools/index.js
- [x] 6.3 Verify tool registers when process:spawn permission is enabled
- [x] 6.4 Verify tool does not register when process:spawn permission is disabled

## 7. Testing

- [x] 7.1 Write unit tests for single execution success case
- [x] 7.2 Write unit tests for single execution failure case (missing marker, empty result)
- [x] 7.3 Write unit tests for fan-out parallel mode
- [x] 7.4 Write unit tests for fan-out sequential mode
- [x] 7.5 Write unit tests for timeout enforcement (per-call, env var, config default)
- [x] 7.6 Write unit tests for returnParams filtering
- [x] 7.7 Write unit tests for process tracking (spawn, exit, error)
- [x] 7.8 Write unit tests for prompt escaping (quotes, backticks, dollar signs, newlines)
- [x] 7.9 Write unit tests for session isolation modes

## 8. Verification

- [x] 8.1 Run full test suite and verify all tests pass
- [x] 8.2 Run lint and verify no issues
- [x] 8.3 Verify application starts without crashing
- [x] 8.4 Verify no regressions in existing functionality
Loading