Subsystemssubsystems/runtime

Runtime

src/runtime is the L2 Agent layer — it owns the model conversation loop. The host-facing createAgent returns an Agent that drives one prompt to settlement. Imported as indusagi/runtime, or as the runtime namespace from indusagi.

The runtime is split into a pure transition machine (the cadence reducer, which returns Effects and never side-effects) and a single stateful conductor (which performs those effects: reach the model, run tools, persist, condense, publish). A host only needs createAgent; advanced hosts can drive the FSM directly.

Table of Contents

Public exports

From src/runtime/index.ts:

Export Kind Source Purpose
createAgent function conductor/agent.ts The host-facing factory; returns an Agent
Agent type conductor/agent.ts The driveable handle (submit/subscribe/abort/snapshot/resume)
AgentDeps type conductor/agent.ts Injectable collaborators (invokeModel/store/ledger)
AgentEventHandler type conductor/agent.ts The subscribe callback shape
runError function contract Construct a RunError
step function cadence Alias of cadence — the bound step(state, signal) factory
cadence function cadence The pure transition machine factory
initialSnapshot function cadence Seed a fresh RunSnapshot

Public types branched on by callers: RunSnapshot, RunPhase, PendingTool, RunEvent, AgentConfig, CompactionPolicy, ToolBox, ToolRunner, ToolCall, ToolOutcome, RunError, RunErrorKind, ModelInvoker (all from contract), plus StepFn and Transition (from cadence).

Note: the gateway's types (Conversation, Turn, Block, ToolDescriptor, Reply, Emission, Channel, StreamOptions, …) are intentionally not re-exported here — import those from indusagi/llmgateway or .../llmgateway/contract.

Sub-directories

Directory Holds
conductor/ agent.ts — the only stateful, side-effecting layer; createAgent lives here
cadence/ reducer.ts, fold.ts — the pure FSM; the cadence(config) factory yields step
contract/ The type vocabulary: config.ts, run-state.ts, events.ts, signal.ts, effect.ts, session.ts, tools.ts, errors.ts
turn/ driver.ts — adapts a gateway Channel into runtime Signals (driveTurn)
dispatch/ scheduler.ts — bounded-concurrency tool dispatch (createScheduler)
memory/ compactor.ts, estimate.ts — context condensation (compact/findCutPoint/shouldCompact)
ledger/ bus.ts, accumulator.ts — the RunLedger event hub and its subscribers
store/ dag.ts, hash.ts, persist.ts — the content-addressed SessionGraph / SessionStore
wire/ projectors.ts — projections of run state onto the wire protocol

Driving an agent

import { runtime, capabilities } from "indusagi";

const agent = runtime.createAgent({
  model: "claude-sonnet-4",
  tools: capabilities.toolBox("coding"),
  // optional: system, maxOutputTokens, thinking, compaction, maxTurns
});

const unsubscribe = agent.subscribe((event) => {
  if (event.kind === "text_delta") process.stdout.write(event.delta);
});

const snapshot = await agent.submit("explain this repo");
console.log(snapshot.phase); // "settled" or "faulted"
unsubscribe();

The Agent handle exposes five methods: submit(input) drives one prompt (Turn[] or a bare string) to settlement and resolves the terminal RunSnapshot — a submit landing while a run is in flight is folded in as steering rather than starting a competing run; subscribe(handler) taps the live RunEvent stream and returns a disposer; abort() cancels the model stream and tool dispatch and faults the run; snapshot() reads the latest state; and resume(sessionId) rehydrates an earlier session from the store.

AgentConfig is the immutable setup: model (required), optional system, tools (a ToolBox), maxOutputTokens, thinking (a ThinkingLevel), compaction (a CompactionPolicy with triggerRatio and keepRecent), and maxTurns — the per-run model-turn ceiling that force-faults a looping model, defaulting to 64.

The model entrypoint is injectable through AgentDeps: by default the conductor binds the gateway's stream to config.model, but a test passes a scripted ModelInvoker so a run is deterministic with no network. A store (a SessionStore) enables persistence and resume; a ledger (a RunLedger) receives published events.

The pure FSM seam

The conductor threads a serial stream of Signals through the pure reducer, which returns a Transition ({ state, effects }). The conductor performs each Effect (publish, invoke_model, run_tool, compact, persist) and folds the resulting signals back in, turning over until a terminal RunPhase (settled or faulted). Advanced hosts can drive that machine themselves:

import { cadence, initialSnapshot } from "indusagi/runtime";

const step = cadence(config);            // StepFn bound to the config
let snapshot = initialSnapshot(sessionId, config.model);
const transition = step(snapshot, { kind: "submit", input: turns });
snapshot = transition.state;             // apply, then perform transition.effects

Before each invoke_model, the conductor checks accumulated history against the model's context window (shouldCompact) and condenses the older prefix when it crosses triggerRatio, preserving the most recent keepRecent turns — but only when the cut genuinely shrinks history, so a model is never trapped in a compaction loop.

Relationship to neighbors

The runtime reaches the LLM Gateway for every model turn and the Capabilities layer for tool execution (its ToolBox / ToolRunner / ToolCall / ToolOutcome contracts are the boundary tools satisfy). Tracing consumes the runtime's RunEvent stream through traceAgentRun; Swarm and Smithy both build their agents with createAgent; and the Shell App runners wrap an Agent for each mode.

Back to the Architecture overview.