Skip to content

Commit 60457a5

Browse files
committed
fix: suppress subagent output in print mode (-p)
wave -p was dumping auto-memory subagent system prompts, file manifests, and streaming output to stdout. Remove all subagent callbacks from print-cli.ts so only the main agent's response is printed, matching Claude Code's behavior. Add spec 035-print-mode.
1 parent 64ab445 commit 60457a5

10 files changed

Lines changed: 457 additions & 41 deletions

File tree

packages/code/src/print-cli.ts

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,15 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
6565
let agent: Agent;
6666
let isReasoning = false;
6767
let isContent = false;
68-
const subagentReasoningStates = new Map<string, boolean>();
69-
const subagentContentStates = new Map<string, boolean>();
7068

7169
// Setup callbacks for agent
70+
// Print mode only outputs the main agent's response (matching Claude Code's
71+
// behavior). Subagent output is internal — the main agent incorporates
72+
// relevant results in its own response.
7273
const callbacks: AgentCallbacks = {
7374
onAssistantMessageAdded: () => {
7475
isReasoning = false;
7576
isContent = false;
76-
// Assistant message started - no content to output yet
7777
},
7878
onAssistantReasoningUpdated: (chunk: string) => {
7979
if (!isReasoning) {
@@ -91,43 +91,9 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
9191
}
9292
isContent = true;
9393
}
94-
// FR-001: Stream content updates for real-time display - output only the new chunk
9594
process.stdout.write(chunk);
9695
},
9796

98-
// Subagent message callbacks
99-
onSubagentAssistantMessageAdded: (subagentId: string) => {
100-
subagentReasoningStates.set(subagentId, false);
101-
subagentContentStates.set(subagentId, false);
102-
},
103-
onSubagentAssistantReasoningUpdated: (
104-
subagentId: string,
105-
chunk: string,
106-
) => {
107-
if (!subagentReasoningStates.get(subagentId)) {
108-
process.stdout.write("\n 💭 Reasoning: ");
109-
subagentReasoningStates.set(subagentId, true);
110-
}
111-
process.stdout.write(chunk);
112-
},
113-
onSubagentAssistantContentUpdated: (subagentId: string, chunk: string) => {
114-
if (!subagentContentStates.get(subagentId)) {
115-
if (subagentReasoningStates.get(subagentId)) {
116-
process.stdout.write("\n 📝 Response: ");
117-
} else {
118-
process.stdout.write("\n ");
119-
}
120-
subagentContentStates.set(subagentId, true);
121-
}
122-
process.stdout.write(chunk);
123-
},
124-
onSubagentUserMessageAdded: (
125-
_subagentId: string,
126-
params: { content: string },
127-
) => {
128-
process.stdout.write(`\n 👤 User: ${params.content}\n`);
129-
},
130-
13197
// Tool block callback - display tool name when tool starts
13298
onToolBlockUpdated: (params) => {
13399
// Print tool name only during 'running' stage (happens once per tool call)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Specification Quality Checklist: Print Mode
2+
3+
**Purpose**: Validate specification completeness and quality before proceeding to planning
4+
**Created**: 2026-06-09
5+
**Feature**: [Link to spec.md](../spec.md)
6+
7+
## Content Quality
8+
9+
- [x] No implementation details (languages, frameworks, APIs)
10+
- [x] Focused on user value and business needs
11+
- [x] Written for non-technical stakeholders
12+
- [x] All mandatory sections completed
13+
14+
## Requirement Completeness
15+
16+
- [x] No [NEEDS CLARIFICATION] markers remain
17+
- [x] Requirements are testable and unambiguous
18+
- [x] All acceptance scenarios are defined
19+
- [x] Edge cases are identified
20+
- [x] Scope is clearly bounded
21+
- [x] Dependencies and assumptions identified
22+
23+
## Feature Readiness
24+
25+
- [x] All functional requirements have clear acceptance criteria
26+
- [x] User scenarios cover primary flows
27+
- [x] No implementation details leak into specification
28+
29+
## Notes
30+
31+
- Feature is already implemented; this spec documents the existing behavior
32+
- Claude Code's print mode behavior served as the reference implementation
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Print Mode Callback Contracts
2+
3+
## AgentCallbacks Configuration (Print Mode)
4+
5+
The `startPrintCli` function creates an `Agent` with a specific set of callbacks. Only main-agent callbacks are registered; subagent callbacks are omitted.
6+
7+
### Active Callbacks
8+
9+
```typescript
10+
const callbacks: AgentCallbacks = {
11+
onAssistantMessageAdded: () => { /* reset state flags */ },
12+
onAssistantReasoningUpdated: (chunk: string) => {
13+
process.stdout.write(/* reasoning header + chunk */)
14+
},
15+
onAssistantContentUpdated: (chunk: string) => {
16+
process.stdout.write(/* response header + chunk */)
17+
},
18+
onToolBlockUpdated: (params) => {
19+
if (params.stage === "running") process.stdout.write(/* tool indicator */)
20+
},
21+
onErrorBlockAdded: (error: string) => {
22+
process.stdout.write(/* error display */)
23+
},
24+
}
25+
```
26+
27+
### Suppressed Callbacks (Not Registered)
28+
29+
```typescript
30+
// NOT registered — subagent output is internal
31+
// onSubagentUserMessageAdded
32+
// onSubagentAssistantMessageAdded
33+
// onSubagentAssistantReasoningUpdated
34+
// onSubagentAssistantContentUpdated
35+
// onSubagentToolBlockUpdated
36+
```
37+
38+
## Output Format
39+
40+
```
41+
[💭 Reasoning:\n]
42+
[<reasoning_chunk>...]
43+
[\n\n📝 Response:\n | \n]
44+
[<content_chunk>...]
45+
[\n🔧 <tool_name> <compact_params>\n...]
46+
[\n❌ Error: <error_message>\n...]
47+
\n
48+
```
49+
50+
## Streaming Flow
51+
52+
```
53+
Agent.sendMessage(prompt)
54+
55+
onAssistantMessageAdded() → reset flags
56+
57+
[onAssistantReasoningUpdated(chunk)] → "💭 Reasoning:\n" + chunk (once + per-chunk)
58+
59+
onAssistantContentUpdated(chunk) → "📝 Response:\n" + chunk (once + per-chunk)
60+
61+
[onToolBlockUpdated(params)] → "🔧 <name> <params>\n" (when stage=running)
62+
63+
[onErrorBlockAdded(error)] → "❌ Error: <error>\n"
64+
65+
process.stdout.write("\n") → final newline
66+
```

specs/035-print-mode/data-model.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Data Model: Print Mode
2+
3+
## Entities
4+
5+
### PrintCliCallbacks (Configuration)
6+
7+
The set of `AgentCallbacks` used by print mode. No new types — this documents which callbacks are active.
8+
9+
| Callback | Active | Output | Description |
10+
|----------|--------|--------|-------------|
11+
| `onAssistantMessageAdded` | Yes | None | Resets reasoning/content state flags |
12+
| `onAssistantReasoningUpdated` | Yes | `💭 Reasoning:\n` + chunk | Main agent reasoning |
13+
| `onAssistantContentUpdated` | Yes | `📝 Response:\n` + chunk | Main agent response text |
14+
| `onToolBlockUpdated` | Yes | `🔧 <name> <params>\n` | Tool call indicator |
15+
| `onErrorBlockAdded` | Yes | `❌ Error: <msg>\n` | Error display |
16+
| `onSubagentUserMessageAdded` | No | — | Suppressed: contains system prompts |
17+
| `onSubagentAssistantMessageAdded` | No | — | Suppressed: internal state |
18+
| `onSubagentAssistantReasoningUpdated` | No | — | Suppressed: internal reasoning |
19+
| `onSubagentAssistantContentUpdated` | No | — | Suppressed: internal content |
20+
| `onSubagentToolBlockUpdated` | No | — | Suppressed: internal tool calls |
21+
22+
### StreamingState
23+
24+
Internal state tracking for the main agent's output stream.
25+
26+
| Field | Type | Description |
27+
|-------|------|-------------|
28+
| `isReasoning` | `boolean` | Whether the main agent is currently streaming reasoning |
29+
| `isContent` | `boolean` | Whether the main agent is currently streaming content |
30+
31+
## Relationships
32+
33+
- `PrintCliCallbacks` uses `StreamingState` to track when to emit headers (`💭 Reasoning:`, `📝 Response:`).
34+
- Suppressed callbacks are simply omitted from the callbacks object — no sentinel values needed.
35+
36+
## Validation Rules
37+
38+
- `onSubagentUserMessageAdded` MUST NOT be registered in print mode (FR-002).
39+
- `onSubagentAssistantReasoningUpdated` MUST NOT be registered in print mode (FR-003).
40+
- `onSubagentAssistantContentUpdated` MUST NOT be registered in print mode (FR-004).

specs/035-print-mode/plan.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Implementation Plan: Print Mode
2+
3+
**Branch**: `035-print-mode` | **Status**: Implemented | **Date**: 2026-06-09 | **Spec**: [spec.md](./spec.md)
4+
**Input**: Feature specification from `/specs/035-print-mode/spec.md`
5+
6+
## Summary
7+
8+
Ensure `wave -p` (print mode) only outputs the main agent's response to stdout, suppressing all subagent output (user messages, reasoning, content). This matches Claude Code's print mode behavior where subagent output is internal and the main agent incorporates results in its own response.
9+
10+
## Technical Context
11+
12+
**Language/Version**: TypeScript (Node.js)
13+
**Primary Dependencies**: Ink (React for CLI — not used in print mode), agent-sdk callbacks
14+
**State Management**: Callback-driven streaming via `AgentCallbacks`
15+
**Testing**: Vitest
16+
**Target Platform**: Linux/macOS/Windows (Terminal CLI)
17+
**Project Type**: Monorepo (agent-sdk + code)
18+
**Performance Goals**: No additional latency; print mode should be as fast as interactive mode
19+
**Constraints**: Print mode must not print any subagent internal output
20+
**Scale/Scope**: Single file (`print-cli.ts`) plus existing callback infrastructure
21+
22+
## Constitution Check
23+
24+
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
25+
26+
1. **Package-First Architecture**: Changes in `code` package only (callback consumer). Pass.
27+
2. **TypeScript Excellence**: No new types needed — removing callback implementations. Pass.
28+
3. **Test Alignment**: Existing print-cli tests updated. Pass.
29+
4. **Build Dependencies**: No agent-sdk changes required. Pass.
30+
5. **Documentation Minimalism**: No extra .md files beyond spec companion docs. Pass.
31+
6. **Quality Gates**: `type-check` and `lint` required. Pass.
32+
7. **Source Code Structure**: Changes in `packages/code/src/print-cli.ts`. Pass.
33+
8. **Data Model Minimalism**: No new data entities. Pass.
34+
35+
## Project Structure
36+
37+
### Documentation (this feature)
38+
39+
```
40+
specs/035-print-mode/
41+
├── plan.md # This file
42+
├── research.md # Design decisions
43+
├── data-model.md # Callback entities
44+
├── quickstart.md # User guide
45+
├── contracts/ # API contracts
46+
├── checklists/ # Quality checks
47+
└── tasks.md # Implementation tasks
48+
```
49+
50+
### Source Code (repository root)
51+
52+
```
53+
packages/
54+
└── code/
55+
└── src/
56+
└── print-cli.ts # Print mode entry point — callback configuration
57+
```
58+
59+
**Structure Decision**: Single-file change in `code` package. Agent-sdk's `AgentCallbacks` interface already supports optional subagent callbacks — simply omitting them achieves the desired behavior.
60+
61+
## Complexity Tracking
62+
63+
*Fill ONLY if Constitution Check has violations that must be justified*
64+
65+
| Violation | Why Needed | Simpler Alternative Rejected Because |
66+
|-----------|------------|-------------------------------------|
67+
| None | N/A | N/A |

specs/035-print-mode/quickstart.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Quickstart: Print Mode
2+
3+
## Overview
4+
Print mode (`wave -p 'message'`) runs the agent non-interactively and outputs only the main agent's response to stdout. It is designed for scripting and piping.
5+
6+
## How to use
7+
8+
### Basic Usage
9+
10+
```bash
11+
# Simple prompt
12+
wave -p 'Explain what this project does'
13+
14+
# Pipe output to a file
15+
wave -p 'List all TODO comments' > todos.txt
16+
17+
# Chain with other commands
18+
wave -p 'What is the main entry point?' | grep -i index
19+
```
20+
21+
### What you'll see
22+
23+
Print mode outputs:
24+
- `💭 Reasoning:` — if the agent reasons before responding
25+
- `📝 Response:` — the main agent's response text
26+
- `🔧 <tool_name> <params>` — tool calls made by the main agent
27+
- `❌ Error: <message>` — any errors
28+
29+
### What you won't see
30+
31+
Print mode suppresses all subagent output:
32+
- No subagent system prompts or instructions
33+
- No subagent file manifests (e.g., auto-memory extraction file lists)
34+
- No subagent reasoning or streaming content
35+
- No subagent tool call indicators
36+
37+
The main agent incorporates relevant subagent results in its own response.
38+
39+
## Example Session
40+
41+
```text
42+
$ wave -p 'What does the build script do?'
43+
44+
💭 Reasoning:
45+
Let me check the build configuration.
46+
47+
🔧 Read package.json
48+
49+
📝 Response:
50+
The build script runs `rimraf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json`, which cleans the output directory, compiles TypeScript, and resolves path aliases.
51+
```
52+
53+
## Exit Codes
54+
55+
| Code | Meaning |
56+
|------|---------|
57+
| 0 | Success — agent responded |
58+
| 1 | Error — agent failed or message was empty |

specs/035-print-mode/research.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Research: Print Mode
2+
3+
## Decision: Suppress all subagent output in print mode
4+
- **Rationale**: Claude Code's print mode (`claude -p`) only outputs the main agent's final response. Subagent messages (both regular Agent tool and forked background agents like memory extraction) are internal — their results return to the main agent as tool_result content and get incorporated into the final answer. Printing subagent internals (system prompts, file manifests, reasoning) produces noisy, unusable output.
5+
- **Alternatives considered**:
6+
- Print subagent output with indentation: Rejected because subagent user messages contain massive system prompts (e.g., auto-memory extraction lists 200+ files) that pollute stdout and break piping.
7+
- Selective suppression (only forked agents): Rejected because regular Agent tool subagent output is also internal — the main agent summarizes and incorporates it. Printing it separately creates duplicate/conflicting output.
8+
9+
## Decision: Remove `onSubagentUserMessageAdded` callback entirely
10+
- **Rationale**: This callback received `params.content` which contained the full subagent user message — including system prompts with instructions and file manifests. Dumping this to stdout was the primary source of the bug report.
11+
- **Alternatives considered**:
12+
- Truncate content: Rejected because even truncated system prompts are noise, and truncation doesn't solve the fundamental problem of leaking internal instructions.
13+
- Check content length: Rejected because arbitrary length thresholds are fragile and still print unnecessary output.
14+
15+
## Decision: Remove `onSubagentAssistantContentUpdated` and `onSubagentAssistantReasoningUpdated` callbacks
16+
- **Rationale**: Subagent streaming output (reasoning, content) is not useful in print mode. The main agent receives subagent results as tool_result and incorporates them in its own response. Printing subagent content separately creates confusing, interleaved output.
17+
- **Alternatives considered**:
18+
- Keep reasoning only: Rejected because subagent reasoning is also internal and not user-facing.
19+
- Add a "verbose" flag for subagent output: Rejected as over-engineering; no user has requested this and it adds configuration complexity.
20+
21+
## Decision: Keep main agent tool block indicators
22+
- **Rationale**: Showing `🔧 Agent Explore: <description>` for the main agent's tool calls provides useful progress feedback in print mode without leaking internals. This is a lightweight indicator, not subagent output.
23+
- **Alternatives considered**:
24+
- Remove all tool indicators: Rejected because users benefit from knowing which tools the main agent is calling, especially for long-running operations.
25+
26+
## Integration Points
27+
- `print-cli.ts`: Consumes `AgentCallbacks` — controls what gets printed to stdout.
28+
- `AgentCallbacks` (agent-sdk): Interface defines optional subagent callbacks; omitting them is valid.
29+
- `SubagentManager` (agent-sdk): Forwards subagent events via callbacks; if no callback is registered, events are silently ignored.
30+
- `ForkedAgentManager` (agent-sdk): Runs background agents (auto-memory) through SubagentManager; events flow through the same callback path.

0 commit comments

Comments
 (0)