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
179 changes: 179 additions & 0 deletions docs/ai/design/feature-agent-send-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
---
phase: design
title: Agent Send Wait Design
description: Technical design for waiting on and printing responses after agent send
---

# Agent Send Wait Design

## Architecture Overview

```mermaid
graph TD
CLI["agent send <message> --id <agent> --wait"]
Manager["AgentManager"]
Resolve["resolveAgent(id, agents)"]
Terminal["TerminalFocusManager.findTerminal(pid)"]
Writer["TtyWriter.send(location, message)"]
Waiter["AgentResponseWaiter"]
Adapter["AgentAdapter.getConversation(sessionFilePath)"]
Status["AgentManager.listAgents()"]
Stderr["stderr: status/errors"]
Output["stdout: assistant response"]

CLI --> Manager
Manager --> Resolve
Resolve --> Terminal
Terminal --> Writer
CLI --> Waiter
Waiter --> Adapter
Waiter --> Status
Adapter --> Waiter
Status --> Waiter
Waiter --> Stderr
Waiter --> Output
```

The command flow is:

1. Resolve the target agent using the existing `--id` logic.
2. Find the terminal and validate `sessionFilePath` when `--wait` is enabled.
3. Seed the transcript cursor from the current conversation length before sending.
4. Send the message with `TtyWriter.send()`.
5. Poll the conversation for new messages and print assistant text content that appears after the seed cursor.
6. Poll agent status until the original target returns to `waiting`, disappears, or the 10-minute safety cap is reached.

## Data Models

No persistent data model is required.

Internal wait result:

```typescript
interface AgentSendWaitResult {
agentName: string;
agentType: AgentType;
pid: number;
sessionId: string;
sessionFilePath: string;
messages: ConversationMessage[];
finalStatus: AgentStatus;
elapsedMs: number;
}
```

Internal wait options:

```typescript
interface AgentSendWaitOptions {
pollIntervalMs: number;
maxWaitMs: number;
}
```

For this feature, `maxWaitMs` is fixed at 10 minutes. The later `--timeout` backlog item can make it configurable without changing the wait-loop contract.

## API Design

CLI interface:

```bash
npx ai-devkit agent send <message> --id <identifier> --wait
```

Behavior:

- `--wait` is optional and defaults to `false`.
- Without `--wait`, the command keeps the current fire-and-forget path.
- With `--wait`, stdout is reserved for assistant response text.
- With `--wait`, status/progress/warnings/errors must go to stderr. Current `ui.info`, `ui.warning`, and `ui.success` write to stdout, so the wait path should use a small stderr writer or direct `process.stderr.write()` for non-response messages.
- With `--wait`, do not print the existing success line (`Sent message to ...`) to stdout after delivery.

Internal helper:

```typescript
async function waitForAgentResponse(params: {
manager: AgentManager;
adapter: AgentAdapter;
target: {
id: string;
name: string;
type: AgentType;
pid: number;
sessionId: string;
sessionFilePath: string;
};
initialMessageCount: number;
options: AgentSendWaitOptions;
onAssistantMessage: (message: ConversationMessage) => void;
onStatus?: (message: string) => void;
}): Promise<AgentSendWaitResult>;
```

`target.id` is the original `--id` value for user-facing context, but the wait loop should prefer `pid` and `sessionId` when finding the same running agent on later polls. This avoids accidentally switching to a different process if a partial name becomes ambiguous while waiting.

## Component Breakdown

### CLI command

File: `packages/cli/src/commands/agent.ts`

- Add `.option('--wait', 'Wait for and print the agent response')`.
- Before sending, validate `agent.sessionFilePath` when `options.wait` is true.
- Find the owning adapter with existing `manager.getAdapter(agent.type)`.
- Seed `initialMessageCount` from `adapter.getConversation(agent.sessionFilePath, { verbose: false }).length`.
- Send via `TtyWriter.send()`.
- If `--wait`, call the wait helper and print assistant messages as complete transcript messages are detected.
- If `--wait`, use stderr for pre-send warnings and wait status messages so stdout remains response-only.

### Wait helper

Preferred location: `packages/cli/src/services/agent/agent.service.ts`

Responsibilities:

- Poll conversation and status.
- Track already emitted transcript indexes.
- Emit only messages where `role === 'assistant'` and `content` is non-empty.
- Stop when the resolved target returns to `AgentStatus.WAITING`.
- Also stop on `AgentStatus.IDLE` after at least one new assistant message has been emitted for this send; Claude Code can move from busy to idle after writing the response instead of reporting waiting.
- Do not stop on `AgentStatus.WAITING` until a transcript read succeeds for the current loop; this avoids missing a final response when the transcript is observed mid-write.
- Fail if the target disappears.
- Fail if the 10-minute safety cap is reached.
- Write a no-response status note to stderr if the target returns to waiting without new assistant text.
- Return structured result for tests and future JSON output.

### Tests

Primary test file: `packages/cli/src/__tests__/commands/agent.test.ts`

Add focused service tests if the wait helper is extracted.

## Design Decisions

| Decision | Choice | Rationale |
|---|---|---|
| Response source | Transcript via `getConversation()` | Existing adapter contract already normalizes Claude, Codex, and Gemini transcripts. |
| Completion signal | Agent returns to `waiting` | Matches existing agent status model and user expectation that the turn is done when input is accepted again. |
| Transcript cursor | Seed by current conversation length before send | Prevents historical messages from being printed. |
| Output default | Assistant text content only on stdout | Keeps the command useful in scripts and close to `claude -p` ergonomics. |
| Status output | stderr for wait mode | Existing `ui.info`, `ui.warning`, and `ui.success` write to stdout, so wait mode needs a response-safe status path. |
| Polling | Interval polling | Existing channel bridge already uses polling; no new daemon or file watcher is needed. |
| Target tracking | Prefer original PID and session ID | Avoids changing targets if a partial name resolves differently during the wait. |
| Safety cap | Fixed 10 minutes | Prevents indefinite hangs while keeping configurable `--timeout` as a later backlog item. |
| Scope | `--wait` only | Keeps item 1 small and leaves timeout/json/stdin/ask for separate backlog items. |

Alternatives considered:

- **Block on terminal output:** rejected because terminal screen capture is brittle and terminal-specific.
- **Use Claude Agent SDK / `claude -p`:** rejected because the feature goal is to use interactive Claude Code sessions.
- **Add `agent ask` first:** rejected because `agent ask` should be a wrapper once `agent send --wait` is reliable.

## Non-Functional Requirements

- Polling must avoid tight loops; use an interval around 1-2 seconds.
- The command must not mutate transcript files.
- The command must not introduce shell interpolation. Message delivery remains handled by `TtyWriter`.
- The command must exit non-zero for delivery failure, missing transcript support, target termination, or safety-cap timeout.
- The command must keep stdout response-only in wait mode.
- Implementation should keep response waiting testable without real terminals or real Claude Code sessions.
76 changes: 76 additions & 0 deletions docs/ai/implementation/feature-agent-send-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
phase: implementation
title: Agent Send Wait Implementation
description: Technical implementation notes for adding --wait to agent send
---

# Agent Send Wait Implementation

## Development Setup

- Worktree: `.worktrees/feature-agent-send-wait`
- Branch: `feature-agent-send-wait`
- Required Node: use Node 20-25. Local bootstrap succeeded with `/opt/homebrew/Cellar/node/24.3.0/bin` first in `PATH`.
- Build command used during setup: `PATH=/opt/homebrew/Cellar/node/24.3.0/bin:$PATH npm run build`
- Lint command used during setup: `PATH=/opt/homebrew/Cellar/node/24.3.0/bin:$PATH node packages/cli/dist/cli.js lint`

## Code Structure

Likely files:

- `packages/cli/src/commands/agent.ts`: add `--wait` option and connect command flow.
- `packages/cli/src/services/agent/agent.service.ts`: new agent service with the wait helper.
- `packages/cli/src/__tests__/commands/agent.test.ts`: command-level regression tests.
- `packages/cli/src/__tests__/services/agent/agent.service.test.ts`: helper-level tests for the agent service.

## Implementation Notes

### Core Features

- Seed transcript length before sending:
```typescript
const initialMessageCount = adapter.getConversation(agent.sessionFilePath, { verbose: false }).length;
await TtyWriter.send(location, message);
```
- Poll conversation and emit `conversation.slice(lastSeenCount)` entries where `role !== 'user'` and `content` is non-empty.
- Poll agent status by listing agents again and resolving the original `--id`.
- Stop when the resolved target status is `AgentStatus.WAITING`.
- Also stop on `AgentStatus.IDLE` after new assistant output has been observed for the current send.
- If a transcript read fails during a poll, do not complete even if status is already `WAITING`; retry until a read succeeds, the agent disappears, or the safety cap is reached.
- Return non-zero for missing transcript path, terminated target, delivery failure, and defensive timeout.

### Patterns & Best Practices

- Keep terminal delivery in `TtyWriter`; do not add new terminal-specific send logic.
- Keep transcript parsing inside adapters.
- Keep stdout focused on assistant content so scripts can consume it.
- Keep fixed wait defaults local to this feature so `--timeout` can replace them cleanly later.

## Integration Points

- `AgentManager.listAgents()` for target detection and status refresh.
- `AgentManager.resolveAgent()` for target consistency.
- `AgentAdapter.getConversation()` for transcript polling.
- `TerminalFocusManager.findTerminal()` and `TtyWriter.send()` for existing delivery flow.

## Error Handling

- Missing target: existing command behavior.
- Ambiguous target: existing command behavior.
- Unsupported terminal or terminal not found: existing command behavior.
- Missing `sessionFilePath` in wait mode: print clear error and exit non-zero.
- Transcript read error: continue polling for transient reads; fail after defensive cap or termination.
- Target disappears: fail non-zero with a clear termination message.

## Performance Considerations

- Poll around every 1-2 seconds.
- Read only the normalized conversation array through the adapter.
- Track `lastSeenCount` to avoid repeated output work.
- Avoid long-running busy loops.

## Security Notes

- Continue using `TtyWriter.send()` and its `execFile`-based delivery.
- Do not introduce shell execution for prompts.
- Do not write prompt or response content to persistent storage for this feature.
88 changes: 88 additions & 0 deletions docs/ai/planning/feature-agent-send-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
phase: planning
title: Agent Send Wait Planning
description: Task breakdown for adding --wait to agent send
---

# Agent Send Wait Planning

## Milestones

- [x] Milestone 1: CLI option and transcript seed
- [x] Milestone 2: Wait helper and response output
- [x] Milestone 3: Failure handling and tests
- [x] Milestone 4: Documentation and verification

## Task Breakdown

### Phase 1: CLI foundation

- [x] Task 1.1: Add `--wait` option to `agent send`.
- [x] Task 1.2: Preserve existing non-wait behavior and tests.
- [x] Task 1.3: Resolve the target adapter for the selected agent type.
- [x] Task 1.4: Validate `sessionFilePath` before waiting and return a clear error when unavailable.
- [x] Task 1.5: Seed transcript length before `TtyWriter.send()`.

### Phase 2: Wait helper

- [x] Task 2.1: Add `waitForAgentResponse()` helper under CLI services.
- [x] Task 2.2: Poll `getConversation()` and emit only new assistant messages.
- [x] Task 2.3: Poll `AgentManager.listAgents()` and resolve the same target to detect `waiting`.
- [x] Task 2.4: Stop successfully when the agent returns to `AgentStatus.WAITING`.
- [x] Task 2.5: Return structured wait results for future `--json` support.

### Phase 3: Failure modes

- [x] Task 3.1: Handle agent termination while waiting with a non-zero exit.
- [x] Task 3.2: Handle transcript read errors without crashing on the first transient failure.
- [x] Task 3.3: Add a fixed defensive max wait duration until the separate `--timeout` item is implemented.
- [x] Task 3.4: Ensure status/progress does not pollute stdout response output.

### Phase 4: Tests and docs

- [x] Task 4.1: Add tests for historical transcript seeding.
- [x] Task 4.2: Add tests for assistant-only output filtering.
- [x] Task 4.3: Add tests for missing `sessionFilePath`.
- [x] Task 4.4: Add tests for target termination and defensive timeout.
- [x] Task 4.5: Update user-facing docs/help text if command documentation exists.

## Progress Summary

Phase 4 implementation completed the first backlog item for `agent send --wait`. The CLI now seeds transcript length before sending, validates wait-mode transcript support, resolves the target adapter, suppresses the normal success line in wait mode, writes assistant response text to stdout, and uses a dedicated wait helper to poll transcript/status until the original agent returns to `waiting`, returns to `idle` after new assistant output, disappears, or reaches the fixed 10-minute safety cap. Phase 6 review tightened the wait loop so it does not complete on `waiting` status until a transcript read succeeds, completes on `idle` only after response output, treats initial idle agents as safe to send without a busy warning, and sanitizes wait-mode stderr status messages. Focused command and wait-helper tests cover historical transcript seeding, assistant-only output, missing session files, target termination, transient transcript read errors, session-ID target fallback, no-response status reporting, sanitized stderr status output, idle-after-response completion, idle-before-output timeout, and defensive timeout.

## Dependencies

- Existing `AgentManager.resolveAgent()` behavior.
- Existing `AgentAdapter.getConversation()` implementations.
- Existing `TtyWriter.send()` terminal delivery.
- Existing `AgentStatus.WAITING` detection.
- Node 20-25 for local dependency installation because `better-sqlite3@12.6.2` does not build under Node 26.

## Timeline & Estimates

- CLI option and seed logic: 0.5 day.
- Wait helper: 1 day.
- Failure handling: 0.5 day.
- Unit tests and docs: 1 day.

Estimated total: 2-3 engineering days.

## Risks & Mitigation

- **Risk:** Agent status may lag transcript writes.
**Mitigation:** Continue polling until both new messages are captured and status returns to waiting.

- **Risk:** Some adapters may not provide reliable `sessionFilePath`.
**Mitigation:** Fail clearly in wait mode and keep fire-and-forget send available.

- **Risk:** Transcript parsing can throw while the agent is writing.
**Mitigation:** Treat occasional read errors as transient during the wait loop.

- **Risk:** Command can hang without configurable timeout.
**Mitigation:** Add a fixed defensive cap in item 1; implement user-facing `--timeout` as the next backlog item.

## Resources Needed

- Unit-test fixtures for conversation messages.
- Mock `AgentManager`, `AgentAdapter`, and `TtyWriter` behavior.
- Existing agent command tests as regression coverage.
Loading
Loading