/** * CrushAgentRunner — AgentRunner impl backed by the Crush CLI * (`crush`, https://github.com/charmbracelet/crush). * * Architectural difference from the other subprocess runners: `crush run * ""` (per its public CLI docs) is a plain one-shot invocation — * text response to stdout, no documented JSON output mode. On the FIRST turn * this runner sends the system prompt + tool protocol; subsequent turns add * `--continue` (resume the most-recently-used session — confirmed by * cross-checking a second, independent public agentic-CLI wrapper's provider * table, which lists `--session ` / `--continue` as crush's resume * mechanism) instead of re-sending the full system prompt, so conversational * context is NOT re-derived from scratch every turn the way the original * (pre-cross-check) revision of this runner did. * * RESOLVED against a live v0.89.0 install (issue #180): crush's session * store is a per-project SQLite DB at `/.crush/crush.db` (created * fresh per working directory unless `--data-dir` is overridden), so `crush * session list` and `--continue`'s "most recently used session" are * inherently scoped PER CWD, not global. Verified concretely: seeded two * concurrent sessions from two different cwds with distinct secret words, * then ran `crush run --continue` in each cwd — each one correctly recalled * only its own secret, with zero cross-contamination. This runner never * passes `--data-dir`, so the safe-by-default behavior applies as long as * that stays true. The `--session ` capture-and-pass-explicitly * alternative is therefore unnecessary. * * Also caught and fixed while validating live: this runner used to pass * `--yolo` to `crush run`, which crush v0.89.0 REJECTS outright ("Unknown * flag: --yolo") — `--yolo`/`-y` is a root-only flag for the interactive * TUI and is not accepted by the `run` subcommand in any position. That * made every single invocation fail with a non-zero exit. Confirmed live * that it's also unnecessary: non-interactive `run` mode auto-approves tool * calls without it (a file-write tool call completed with zero prompting). * * Usage accounting — crush's plain-text output carries no token counts, so * this runner optionally routes crush's LLM traffic through a * UsageProxyServer (usage-proxy.ts) by setting a base-URL env var before * spawning and reading totals() after the turn. CONFIRMED BROKEN against a * live install: crush ignores `OPENAI_BASE_URL` entirely (pointed it at an * unreachable port with a real custom provider configured — crush still * reached the real upstream, proving the env var is never read). crush's * provider base_url is only configurable via a JSON config file * (`~/.config/crush/crush.json`, `providers..base_url`, confirmed * working live) — there is no runtime env-var override. `baseUrlEnvVar` as * currently implemented does nothing for crush; it fails closed (usage * stays at 0, turns aren't broken) but never actually collects usage. Real * fix: write/merge a temp provider entry into crush's JSON config (or a * `--data-dir`-scoped copy of it) pointed at the proxy before spawn, instead * of setting an env var — not implemented here, since that requires * deciding how to safely merge into a user's existing crush.json rather * than just confirming a fact. * * Streaming / liveness — WHY INCREMENTAL (#204): * This runner originally buffered ALL of crush's stdout in a local * variable until the subprocess closed, so any turn longer than * session.ts's 4-minute silent-stream watchdog (SILENT_SESSION_MS) * yielded zero messages in time — abort, retry, kill, circuit breaker. * Same bug class the codex/kimi/antigravity runners had. Unlike those, * crush's `run` subcommand has no documented JSON event stream — it's * plain text, so there's no structured envelope to segment "messages" by. * This runner now parses stdout LINE BY LINE as it arrives: a liveness * `tool_use` message is yielded the moment the subprocess spawns * (deterministically winning the first-pull race regardless of * model-thinking latency), and every non-fence line is yielded as an * `assistant` message as soon as it lands rather than after the process * exits. Lines inside a ```tool_call fence are withheld from the visible * stream (same as the old end-of-turn fence stripping) but still * accumulated into the turn's raw text for end-of-turn fence parsing — * fence parsing needs the complete, un-split fence body. The fence's * OPENING line is forwarded as a `tool_use` liveness message (crush has no * tool-execution events of its own to forward, so the org tool-call fence * itself is the closest available "tool starting" boundary). * * Org tools — FENCE PROTOCOL: same approach as the other subprocess runners. */ import { type AgentMessage, type AgentRunArgs, type AgentRunner } from './agent-runner.js'; /** Strip tool_call fences and normalize whitespace on crush's plain-text * stdout. Exported for unit testing (crush-runner.test.ts). */ export declare function parseCrushOutput(stdout: string): { text: string; rawText: string; }; /** * One incremental chunk of a streamed crush turn. * - 'assistant': one line of plain-text output, outside any tool_call * fence — yielded by run() as an assistant AgentMessage as it lands. * - 'tool': liveness — either the spawn-time "turn started" yield, or a * ```tool_call fence's opening line (the closest thing crush's plain-text * output has to a tool-execution start boundary). */ export interface CrushStreamEvent { kind: 'assistant' | 'tool'; text?: string; } export interface CrushAgentRunnerOptions { crushBin?: string; /** Enable usage-proxy accounting. Off by default since the base-URL env * var it relies on is CONFIRMED not read by crush at all (see file * header) — turning this on fails closed (usage stays 0) rather than * breaking turns, but collects nothing until the JSON-config-based fix * described in the file header is implemented. */ usageProxy?: { upstreamBaseUrl: string; baseUrlEnvVar?: string; }; } export declare class CrushAgentRunner implements AgentRunner { private crushBin?; private usageProxyOpts?; constructor(opts?: CrushAgentRunnerOptions); run(args: AgentRunArgs): AsyncIterable; /** * Run one `crush run` invocation and stream its plain-text stdout * INCREMENTALLY: each line is yielded as soon as it arrives (see the * header's "Streaming / liveness" note for why buffering until process * close was a bug, #204). End-of-turn facts (exit code, stderr tail, raw * text, hang/timeout flags) are written into `outcome`, which the caller * reads after this generator completes. */ private streamTurn; } //# sourceMappingURL=crush-runner.d.ts.map