# Agent Module `src/agent/` — Agent lifecycle management, output parsing, multi-provider support, and account failover. ## File Inventory | File | Purpose | |------|---------| | `types.ts` | Core types: `AgentInfo`, `AgentManager` interface, `SpawnOptions`, `StreamEvent` | | `manager.ts` | `MultiProviderAgentManager` — main orchestrator class | | `process-manager.ts` | `AgentProcessManager` — worktree creation, command building, detached spawn | | `output-handler.ts` | `OutputHandler` — JSONL stream parsing, completion detection, proposal creation, task dedup | | `file-tailer.ts` | `FileTailer` — watches output files, fires parser + raw content callbacks | | `file-io.ts` | Input/output file I/O: frontmatter writing, signal.json reading, tiptap conversion | | `markdown-to-tiptap.ts` | Markdown to Tiptap JSON conversion using MarkdownManager | | `index.ts` | Public exports, `ClaudeAgentManager` deprecated alias | ### Sub-modules | Directory | Purpose | |-----------|---------| | `providers/` | Provider registry, presets (7 providers), config types | | `providers/parsers/` | Provider-specific output parsers (Claude JSONL, generic line) | | `accounts/` | Account discovery, config dir setup, credential management, usage API | | `credentials/` | `AccountCredentialManager` — credential injection per account | | `lifecycle/` | `LifecycleController` — retry policy, signal recovery, missing signal instructions | | `prompts/` | Mode-specific prompt builders (execute, discuss, plan, detail, refine) | ## Key Flows ### Spawning an Agent 1. **tRPC procedure** calls `agentManager.spawn(options)` 2. Manager generates alias (adjective-animal), creates DB record 3. `AgentProcessManager.createWorktree()` — creates git worktree at `.cw-worktrees/agent//` 4. `file-io.writeInputFiles()` — writes `.cw/input/` with assignment files (initiative, pages, phase, task) and read-only context dirs (`context/phases/`, `context/tasks/`) 5. Provider config builds spawn command via `buildSpawnCommand()` 6. `spawnDetached()` — launches detached child process with file output redirection 7. `FileTailer` watches output file, fires `onEvent` (parsed stream events) and `onRawContent` (raw JSONL chunks) callbacks 8. `onRawContent` → DB insert via `createLogChunkCallback()` → `agent:output` event emitted (single emission point) 9. `OutputHandler.handleStreamEvent()` processes parsed events (session tracking, result capture — no event emission) 10. DB record updated with PID, output file path, session ID 11. `agent:spawned` event emitted ### Completion Detection 1. Polling detects process exit, `FileTailer.stop()` flushes remaining output 2. `OutputHandler.handleCompletion()` triggered 3. **Primary path**: Reads `.cw/output/signal.json` from agent worktree 4. Signal contains `{ status: "done"|"questions"|"error", result?, questions?, error? }` 5. Agent DB status updated accordingly (idle, waiting_for_input, crashed) 6. For `done`: proposals created from structured output; `agent:stopped` emitted 7. For `questions`: parsed and stored as `pendingQuestions`; `agent:waiting` emitted 8. **Fallback**: If signal.json missing, lifecycle controller retries with instruction injection ### Account Failover 1. On usage-limit error, `markAccountExhausted(id, until)` called 2. `findNextAvailable(provider)` returns least-recently-used non-exhausted account 3. Agent re-spawned with new account's credentials 4. `agent:account_switched` event emitted ### Resume Flow 1. tRPC `resumeAgent` called with `answers: Record` 2. Manager looks up agent's session ID and provider config 3. `buildResumeCommand()` creates resume command with session flag 4. `formatAnswersAsPrompt(answers)` converts answers to prompt text 5. New detached process spawned, same worktree, incremented session number ## Provider Configuration Providers defined in `providers/presets.ts`: | Provider | Command | Resume | Prompt Mode | |----------|---------|--------|-------------| | claude | `claude` | `--resume ` | native (`-p`) | | claude-code | `claude` | `--resume ` | native | | codex | `codex` | none | flag (`--prompt`) | | aider | `aider` | none | flag (`--message`) | | cline | `cline` | none | flag | | continue | `continue` | none | flag | | cursor-agent | `cursor` | none | flag | Each provider config specifies: `command`, `args`, `resumeStyle`, `promptMode`, `structuredOutput`, `sessionId` extraction, `nonInteractive` options. ## Output Parsing The `OutputHandler` processes JSONL streams from Claude CLI: - `init` event → session ID extracted and persisted - `text_delta` events → no-op in handler (output streaming handled by DB log chunks) - `result` event → final result with structured data captured on `ActiveAgent` - Signal file (`signal.json`) → authoritative completion status **Output event flow**: `FileTailer.onRawContent()` → DB `insertChunk()` → `EventBus.emit('agent:output')`. This is the single emission point — no events from `handleStreamEvent()` or `processLine()`. For providers without structured output, the generic line parser accumulates raw text. ## Credential Management `AccountCredentialManager` in `credentials/` handles OAuth token lifecycle: - `read()` — extracts `claudeAiOauth` from `.credentials.json`. Only `accessToken` is required; `refreshToken` and `expiresAt` may be null (setup tokens). - `isExpired()` — returns false when `expiresAt` is null (setup tokens never "expire" from our perspective). - `ensureValid()` — if expired and `refreshToken` exists, refreshes. If expired with no `refreshToken`, returns invalid with error. ### Setup Tokens Setup tokens (from `claude setup-token`) are long-lived OAuth access tokens with no refresh token or expiry. Register via: ```sh cw account add --token --email user@example.com ``` Stored as `credentials: {"claudeAiOauth":{"accessToken":""}}` and `configJson: {"hasCompletedOnboarding":true}`. ## Log Chunks Agent output is persisted to `agent_log_chunks` table and drives all live streaming: - `onRawContent` callback fires for every raw JSONL chunk from `FileTailer` - DB insert → `agent:output` event emission (single source of truth for UI) - No FK to agents — survives agent deletion - Session tracking: spawn=1, resume=previousMax+1 - Read path (`getAgentOutput` tRPC): concatenates all DB chunks (no file fallback) - Live path (`onAgentOutput` subscription): listens for `agent:output` events - Frontend: initial query loads from DB, subscription accumulates raw JSONL, both parsed via `parseAgentOutput()`