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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Types that stay in THIS repo because they're runtime-shaped (coupled to a runnin
## Code map — the loop kernel & the recursive atom (src/runtime/)

- `run-loop.ts` — `runAgentRounds`, the round-synchronous leaf kernel. Per round: `driver.plan()`→N tasks→one sandbox/iteration (bounded by `maxConcurrency`, round-robin `agentRuns`)→`streamPrompt`→`output.parse`→`validator.validate`→`driver.decide`. Owns iteration accounting, concurrency, abort, cost+token aggregation, trace emission, box teardown. Exports `defaultSelectWinner` (best-valid-score, ties→earliest) — the single-sourced selection the personify combinators reuse.
- `supervise/` — the recursive execution atom (keystone): `Scope` + `Supervisor` over the open `Executor` port, spawn/settle on a **conserved budget pool** so equal-compute holds by construction; the journal replays completed settlements, but live supervised-tree recovery after coordinator restart is not implemented. `runtime.ts` also holds `createExecutor({backend})` — the ONE built-in executor (backend-as-data: `router`/`router-tools`/`bridge`/`cli`/`sandbox`; `router-tools` is the off-box tool-using agentic loop — chat→tool_calls→`executeToolCall`→repeat — over the router's tool-calling, no sandbox); the per-backend bodies are internal case-arms, BYO agents implement `Executor` directly.
- `supervise/` — the recursive execution atom (keystone): `Scope` + `Supervisor` over the open `Executor` port, spawn/settle on a **conserved budget pool** so equal-compute holds by construction; the journal replays completed settlements, but live supervised-tree recovery after coordinator restart is not implemented. `runtime.ts` also holds `createExecutor({backend})` — the ONE built-in executor (backend-as-data: `router`/`router-tools`/`bridge`/`cli`/`cli-worktree`/`provider`/`sandbox`; `router-tools` is the off-box tool-using agentic loop — chat→tool_calls→`executeToolCall`→repeat — over the router's tool-calling, no sandbox); the per-backend bodies are internal case-arms, BYO agents implement `Executor` directly.
- `personify/` — the content-free generic combinators (`fanout`/`loopUntil`/`widen`/`panel`/`verify`/`pipeline`) + `definePersona`/`runPersonified` + the cross-run `Corpus` + `createScopeAnalyst` (the analyst-on-scope steer firewall).
- the **agent-driver** is the canonical "drive an agent" path: an `AgentProfile` driving another `AgentProfile` via the coordination toolbox (`createCoordinationTools`, `src/mcp/tools/coordination.ts`) over the `Scope`/`Supervisor`, plus `runAgentic`/`defineStrategy`/`runPersonified` (`strategy.ts`/`personify/persona.ts`) on the Supervisor. Child→parent messages ride ONE typed pipe — `createEventBus` (`supervise/event-bus.ts`): settled outputs, `ask_parent` questions, and analyst findings are all `CoordinationEvent` kinds, delivered pass-through (`subscribe`/`onEvent`, immediate) AND queued for the driver to pull (`await_event({kinds?})` — the ONE wait verb; `kinds:['settled']` = next finished worker, omit = also questions/findings). The pull queue is **priority-ordered** — a blocking question (urgency→priority: `blocks-run`=20/`blocks-step`=10) is bumped ahead of queued settles/findings; ties FIFO by `seq`. The bus is **bidirectional**: UP (settled/question/finding) is queued+pullable; DOWN (`steer_agent` for any live worker — instruction/correction/continuation; `answer_question` routes an answer down) goes to the child inbox via `scope.send`→`deliver` AND records a `queue:false` event (history + subscribers, never pulled back). The receive end is `createInbox` (`supervise/inbox.ts`), which the owned tool-loop executor (`routerToolsInlineExecutor`) exposes as `Executor.deliver`: QUEUED messages flush at each step boundary AND before the worker may settle (it can't finish with an unread steer); a FORCEFUL `steer_agent({interrupt:true})` aborts the in-flight turn so the worker re-plans immediately. Black-box CLI harnesses can't be interrupted mid-step, so there the down-leg degrades to the next spawn. Observability is first-class: every event both ways is stamped (`seq`/`at`/`priority`), the full `history()` is an audit/replay trail, `stats()` counts throughput (both surfaced on `CoordinationTools` and the MCP handle). `analyzeOnSettle` auto-fires trace analysts when a worker settles `done`, re-entering each result as a `finding` on the same bus (cost-governed opt-in; the firewall stays in the analyst registry). Trace analysis is **substrate- AND harness-agnostic** via `TraceSource` (`supervise/trace-source.ts`) — a worker's tool calls as agent-eval `ToolSpan`s from EITHER an owned loop (`createPushTraceSource`; `routerToolsInlineExecutor`'s `onToolStep` feeds `record`) OR a sandbox/fleet box (`sandboxSessionTraceSource(box, sessionId, {harness})` reads `box.messages()` session parts). Harness wire-shapes are decoded by a **per-harness adapter registry** (`toolPartDecoders`): `decodeOpencodePart` decodes against the **canonical `ToolPart`/`ToolState` published by `@tangle-network/agent-interface`** (the type every adc sdk-provider normalizes into — single source of truth, so a status adc adds/renames is a compile error here, not a silent miss; terminal-state + callId-dedup; also live-box-validated), `decodeAnthropicPart` (claude-code/kimi `{type:'tool_use', id|tool_use_id, name|tool, input}` confirmed vs `cli-bridge/src/backends/claude.ts`+`kimi.ts`), `decodeOpenAiPart` (router/kimi top-level `function`). **codex emits NO structured tool calls** (bridge `codex.ts` never yields `tool_calls` — text+shell only) → per-tool detection unavailable for codex from any path (harness property, not a gap). `decodeToolPart(part, harness?)` picks the adapter or tries all. Add a harness = add a decoder + one entry, validated against the cli-bridge backend. Two consumers ride a source: ONLINE `watchTrace` (`detector-monitor.ts`) folds live spans through agent-eval's published streaming kernel (`repeatedActionDetector`/`errorStreakDetector`, the SAME kernel `control-runtime` folds) → `onSignal` → a `finding`; SETTLE `analyzeTrace` (`trajectory-recorder.ts`) collects the spans and runs the published BATCH analyzers (`buildTrajectory`/`stuckLoopView`/`toolWasteView`). `ToolSpan` is the common currency; detection logic + the failure taxonomy live in agent-eval — never reimplement here. Production target = sandbox/fleet; the owned-loop push path is for local/router/cli-bridge. The in-process queue and a future cross-box durable mailbox share this one interface. `assertTraceDerivedFindings` (`personify/analyst.ts`) is the steer-firewall (selector≠judge). `types.ts` holds `Driver`/`AgentRunSpec`/`OutputAdapter`/`Validator`/`Iteration`/`LoopResult`/`SandboxClient` + the `LoopTraceEvent` union. `sandbox-run.ts` is `openSandboxRun` — the one run/stream/resume sandbox seam; `inline-sandbox-client.ts` is `inlineSandboxClient` — the one adapter presenting any non-box `Executor` as a `SandboxClient` for `runAgentRounds`. `loop-dispatch.ts` adapts `runAgentRounds`→agent-eval campaigns; `report-usage.ts` forwards token usage so the integrity guard sees a real backend.

Expand Down
2 changes: 1 addition & 1 deletion docs/api/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -21737,7 +21737,7 @@ The stores a supervised run needs, in-memory or file-backed. `InMemoryRunContext

Config for [createExecutor](#createexecutor): the backend is DATA — the cost dial a profile,
an experiment config, or a replay journal can name — not an import choice. Each
variant carries its backend's seam (router/router-tools/bridge/cli/cli-worktree/sandbox).
variant carries its backend's seam.

***

Expand Down
7 changes: 4 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,10 @@ Salience filtering and the cross-box durable mailbox are not built; see **§13.6
composed as one leaf execution backend. Everything above it is the same `act`/`Scope`
atom, observable as one lifecycle stream (`scope.spawn`/settle →
`agent.spawn`/`agent.child`).
- **REAL** — every node materializes in its backend (sandbox / cli-bridge / router /
worktree-cli) via the one backend-as-data factory `createExecutor({ backend })`
(`src/runtime/supervise/runtime.ts:1517`). The profile says what it is; the executor
- **REAL** — every node materializes in its backend (`router`, `router-tools`, `bridge`,
`cli`, `cli-worktree`, `provider`, `sandbox`) via the one backend-as-data factory
`createExecutor({ backend })` (`src/runtime/supervise/runtime.ts:4735`). A name outside
that set is refused there by `ValidationError`. The profile says what it is; the executor
says where it runs.
- **REAL** — the supervisor **authoring** child profiles is the AgentProfile law (§1,
and `canonical-api.md` §1.5): a supervisor's intelligence is *writing full
Expand Down
18 changes: 16 additions & 2 deletions src/runtime/supervise/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ import type {
UsageEvent,
WorkerInteractiveSession,
} from './types'
import { workerTraceEnv, workerTraceHeaders } from './worker-trace'
import { WORKER_TRACE_PROPAGATION, workerTraceEnv, workerTraceHeaders } from './worker-trace'
import { createWorktreeCliExecutor } from './worktree-cli-executor'

// ── Seam contracts (read off ExecutorContext.seams, narrowed per built-in) ─────
Expand Down Expand Up @@ -4519,7 +4519,7 @@ export const cliWorktreeExecutor: ExecutorFactory<unknown> = (spec, ctx) => {
/**
* Config for {@link createExecutor}: the backend is DATA — the cost dial a profile,
* an experiment config, or a replay journal can name — not an import choice. Each
* variant carries its backend's seam (router/router-tools/bridge/cli/cli-worktree/sandbox).
* variant carries its backend's seam.
*/
export type ExecutorConfig =
| ({ backend: 'router' } & RouterSeam)
Expand Down Expand Up @@ -4670,6 +4670,20 @@ export function snapshotExecutorConfig(config: ExecutorConfig): ExecutorConfig {
}
case 'cli':
return detachedSnapshot(config, `createExecutor ${config.backend} config`)
default: {
// The backend is DATA — a profile, an experiment config, or a replay journal names it — so
// a value outside the union reaches here untyped. Without this arm the switch returns
// `undefined`, `createExecutor` hands back a working-looking factory, and the failure lands
// one call later as a TypeError that never names the backend that caused it.
const named = (config as { backend?: unknown }).backend
// The supported list is read off the trace-propagation table rather than written out again:
// that table is `satisfies Record<ExecutorConfig['backend'], boolean>`, so it is the one
// copy the compiler already forces to hold every arm.
const supported = Object.keys(WORKER_TRACE_PROPAGATION).sort().join(', ')
throw new ValidationError(
`createExecutor: no backend named ${JSON.stringify(named)}; supported backends are ${supported}`,
)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/runtime/supervise/supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
* - Join barrier: when `act()` settles (resolve OR reject), every still-live child is
* torn down before `run` returns — the generalization of the kernel's
* `finally{ Promise.allSettled(destroy) }` barrier (run-loop.ts) from boxes to the
* whole sub-tree. A teardown failure is `allSettled`'d and journaled as a
* `cancelled` event; it NEVER masks act()'s own outcome. act()'s rejection is the
* whole sub-tree. A teardown failure is `allSettled`'d and journaled per node as a
* `teardown-unconfirmed` event; it NEVER masks act()'s own outcome. act()'s rejection is the
* PRIMARY error (the kernel's firstError precedence), so a teardown throw during the
* barrier can never overwrite the real failure.
* - Abort cascade: a root abort (caller signal, `RootHandle.abort`, a tripped breaker,
Expand Down
10 changes: 10 additions & 0 deletions tests/runtime/executor-config-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,14 @@ describe('createExecutor config intake', () => {
runId: 'execution-b',
})
})

it('refuses a backend nothing implements, by name, at the call that names it', () => {
// The backend is data a profile, an experiment config, or a replay journal can carry, so a
// name outside the union reaches this factory untyped. Without a refusal here the switch
// returns undefined, createExecutor hands back a working-looking factory, and the failure
// lands one call later as a TypeError that never mentions the backend.
expect(() => createExecutor({ backend: 'bridge-worktree' } as never)).toThrow(
/no backend named "bridge-worktree"; supported backends are bridge, cli, cli-worktree, provider, router, router-tools, sandbox/,
)
})
})