diff --git a/AGENTS.md b/AGENTS.md index 83c519ac9..99437c689 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,3 +90,4 @@ Interchange is the standard library for this repo, consumed as published `@intx/ - `docs/PLUGINS.md` — plugin manifest system and discovery - `docs/TELEMETRY.md` — what usage telemetry is collected and why - `docs/PERFTRACE.md` — local PerfTrace and opt-in OTEL export settings +- `docs/plans/` — non-normative working notes and design spikes; check before starting related work diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 95548271e..be796a730 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -14,7 +14,19 @@ The reactor (from `@intx/agent`) drives a single agent turn-by-turn. Each turn i This repeats until the director emits `capabilities.done()`. -### Events +### Reactor Events (Partial) + +This table is not the full reactor/stream event vocabulary. `tool.done` is +here because the sub-sections below reference it; `inference.done` and +`reactor.done` are the two events that `src/agent/reactor-events.ts` +exports guards for (`onTurnBoundary` / `onReactorShutdown`) — see the +paragraph after the table. The complete set of reactor and stream event +types the TUI maps is `PRODUCTION_REACTOR_TYPES` in +`src/tui-opentui/stream-event-map.ts:62-78` (covering `message.received`, +`inference.start`, `inference.text.delta`, `inference.tool_call.start` / +`.delta` / `.end`, `tool.start`, and `connector.reply`, among others) — +treat that as canonical rather than this table or any other doc's partial +list. | Event | When it fires | |---|---| @@ -115,7 +127,7 @@ Defaults are permissive (12 / 20 turn-only thresholds, 5-minute stall timeout) s #### Main-session loop protection -The ChatDirector counts consecutive assistant turns that contain tool calls and no text (`toolOnlyStreak`), reset by any turn with text and by every fresh operator message. A dismissed `ask_operator` counts as a no-progress, tool-only turn — the decline path does not reset the streak. At `toolOnlyTurnNudgeAt` the director arms a one-shot ephemeral wrap-up nudge; at `toolOnlyTurnPauseAt` it stops issuing infers entirely and replies with a loud, operator-facing pause message ("Auto-paused after N consecutive tool-only turns... Send a message to resume"), using the same `capabilities.reply()` channel the workflow-stall message already uses to reach the TUI — no new director-to-UI channel was needed. Because a turn with pending `tool_call` blocks must be followed by tool results before anything else (a bare nudge turn on top of pending tool calls is a provider-invalid conversation), both the nudge and the pause are applied by rewriting the `infer` action that follows once those pending tools have resolved — the same one-shot rewrite shape as the sub-agent report-forced wiring below. This loop-protection rewrite runs with the **highest precedence** among the terminal/continuation rewrites in `decideInner`: it is checked before the workflow-idle, open-task, and goal-governor continuation nudges, since those exist to keep a session moving — exactly the behavior the pause guards against. Resuming is just the operator sending a new message, which resets the streak and un-pauses through the same reset path as the other nudge budgets. +The ChatDirector counts consecutive assistant turns that contain tool calls and no text (`toolOnlyStreak`), reset by any turn with text and by every fresh operator message. A dismissed `ask_operator` counts as a no-progress, tool-only turn — the decline path does not reset the streak. At `toolOnlyTurnNudgeAt` the director arms a one-shot ephemeral wrap-up nudge; at `toolOnlyTurnPauseAt` it stops issuing infers entirely and replies with a loud, operator-facing pause message ("Auto-paused: the model ran N steps in a row without explaining its progress. Send a message to resume", `src/agent/director.ts:424-426`), using the same `capabilities.reply()` channel the workflow-stall message already uses to reach the TUI — no new director-to-UI channel was needed. Because a turn with pending `tool_call` blocks must be followed by tool results before anything else (a bare nudge turn on top of pending tool calls is a provider-invalid conversation), both the nudge and the pause are applied by rewriting the `infer` action that follows once those pending tools have resolved — the same one-shot rewrite shape as the sub-agent report-forced wiring below. This loop-protection rewrite runs with the **highest precedence** among the terminal/continuation rewrites in `decideInner`: it is checked before the workflow-idle, open-task, and goal-governor continuation nudges, since those exist to keep a session moving — exactly the behavior the pause guards against. Resuming is just the operator sending a new message, which resets the streak and un-pauses through the same reset path as the other nudge budgets. #### Sub-agent stall management @@ -274,6 +286,8 @@ tool call Approval scopes offered: Allow Once (persist nothing), Allow Always for a file or its directory (file tools), or a command shape (shell). There is intentionally no "all files" rung. +**Known live gap.** A queued gate's display-dependent timers (goal-mode auto-skip, tool-budget pause ceiling) currently arm when the request is *received*, not when it is actually shown to the operator — a request sitting behind others in the queue can burn its whole timeout invisibly. `ask_operator` also has no timeout/abort safety net at all, unlike the permission gate, so a queued operator question behind a stuck overlay can hang a run. Both live in `src/tui-opentui/gate-wire.ts`'s `onPermission`/`onOperator`. This callout should be removed once that fix ships — do not let it become permanent known-issue debt. + ### TUI (`src/tui-opentui/`) OpenTUI (`@opentui/core`) is the shipping shell; the Ink/React tree has been deleted from the repo. The runner (`src/tui/runner.ts`) mounts the host via `mountRunnerHost` (`src/tui-opentui/runner-host.ts`), which mounts `mountProductHost` (`src/tui-opentui/product-host.ts`) over the shell (`src/tui-opentui/shell.ts`). diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 0d9ea820b..4bde87330 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -137,7 +137,8 @@ src/ command-catalog.ts, model-catalog.ts, chrome-state.ts, palette.ts, provider-setup.ts Onboarding provider setup flow docs/ - PRODUCT.md, ARCHITECTURE.md, IMPLEMENTATION.md, HOOKS.md, MCP.md, PLUGINS.md + PRODUCT.md, ARCHITECTURE.md, IMPLEMENTATION.md, TUI.md, HOOKS.md, MCP.md, + PLUGINS.md, TELEMETRY.md, PERFTRACE.md ``` ### Auto Mode @@ -331,13 +332,12 @@ session; that tree re-write is inherent to git and left as residual cost. ### Event Stream -`agent.stream()` emits `ReactorEmittedEvent` objects, including: -- `inference.tool_call.start` / `inference.tool_call.end` — tool call lifecycle -- `tool.start` / `tool.done` — tool execution (with `isError`) -- `inference.usage` — token usage (faremeter) -- `connector.reply` — model reply content -- `inference.error` / `reactor.error` — parse/inference and fatal errors -- `reactor.done` — loop completion +`agent.stream()` emits `ReactorEmittedEvent` objects. `docs/ARCHITECTURE.md`'s +"Reactor Events (Partial)" section names the two turn-boundary/shutdown events +the directors guard on; the full set of reactor and stream event types is +`PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:62-78` — +treat that as canonical rather than this section or any other doc's partial +list. Mid-run queue/steer/interrupt state is a pure state machine in `src/tui-opentui/session-queue.ts` (interaction contract §3): `enqueue` (kind `"queue"`) and `enqueueSteer` (kind `"steer"`) share one pending pool, drained steer-first, then queue, both FIFO within their class. The prompt hint (`src/tui-opentui/stream.ts`, `PROMPT_HINT`) reads `Enter queue · Alt+Enter steer · Ctrl+C stop`. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index cc622fc78..cf7906754 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -90,11 +90,13 @@ Config-driven `postTurn` and `postRun` hooks (TypeScript or shell) run automatic ## Failure Modes and Recovery -### Stall (idle cycles) +### Stall (tool-only turns with no narration) -**What the user sees:** The agent stops producing tool calls. After 3 idle turns the run aborts with `Agent stalled: no tool calls for 3 turns.` +**What the user sees:** The agent runs several turns in a row that are all tool calls with no explanation of what it's doing. After a one-shot nudge to explain itself, if the pattern continues the session **auto-pauses**: it stops issuing new inferences and replies with "Auto-paused: the model ran N steps in a row without explaining its progress. Send a message to resume." The session is not aborted — sending any message resumes it. -**Recovery:** State is saved; inspect `~/.corbits/projects///run.json` (or a legacy in-repo `.agent-state/` tree if not yet migrated), adjust the task or prompt, and start a new run. +The exact turn thresholds are model-family-dependent (tighter for models with observed runaway tool-only behavior); see "Main-session loop protection" in `docs/ARCHITECTURE.md`. + +**Recovery:** Send a message to resume. To inspect state first, see `~/.corbits/projects///run.json` (or a legacy in-repo `.agent-state/` tree if not yet migrated). ### Permission denied (exec)