Skip to content

Runtime Spec

The runtime layer adds the data structures the framework needs to persist an agent run across processes: agent sessions and harness traces. The wire formats below are protocol-level (Tier 1); the in-memory dispatch implementations live in steerable-agent-runtime (Tier 3).

AgentSession

A row that tracks "this user is in the middle of a multi-step run with this chat / project / scenario". Used by the agent.session.* sidecar methods and by useAgentSession on the UI side.

Field Type Required Notes
sessionId string yes Globally unique
userId string yes Owning user
chatId string yes Owning chat
currentStage string yes Free-form stage tag (e.g. PLANNING)
nextStage string \| null no Pre-computed next stage if any
scenario string no Product-defined scenario tag
stageData Record<string, unknown> no Stage-specific scratch state
isActive boolean yes False once the run terminates
projectId string \| null no Owning project
id string no DB row id (storage-implementation specific)
createdAt / updatedAt string (ISO 8601) yes Audit timestamps

HarnessTrace

The metadata for one run-of-the-loop. Pairs with N TraceSpans.

Field Type Required Notes
traceId string yes Unique across the deployment
sessionId string yes Owning AgentSession
chatId string yes
userId string yes
startedAt string yes
endedAt string no Set when the loop returns
outcome 'ok' \| 'error' \| 'budget_exhausted' \| 'cancelled' no Run-level summary

TraceSpan

One unit of work — typically one LLM call or one tool dispatch. Maps 1:1 to OpenTelemetry's notion of a span.

Field Type Required Notes
spanId string yes
name string yes e.g. llm.complete, tool.dispatch:read_file
startAt string yes ISO 8601
endAt string no
parentId string no Parent span; absent for the root span
attrs Record<string, unknown> no Stage-specific attributes (token counts, tool name, etc.)

TraceEvent

A point-in-time annotation inside a span (vs. spans which have duration).

Field Type Required Notes
eventId string yes
spanId string yes Parent span
name string yes e.g. llm.token, policy.denied
at string yes ISO 8601
attrs Record<string, unknown> no

Adapter interfaces (Tier 3)

These are Python-only. The protocol-level types above are what crosses the wire; the adapters below are what your server / sidecar implements.

LLMProvider

class LLMProvider(Protocol):
    async def stream(
        self,
        messages: Sequence[LLMMessage],
        *,
        tools: Iterable[dict] | None = None,
        temperature: float | None = None,
    ) -> AsyncIterator[LLMStreamChunk]: ...

Reference implementations: OpenAICompatProvider, AnthropicProvider. Ollama is just OpenAICompatProvider pointed at a local base URL.

ToolRouter

router = ToolRouter()

@tool(router=router, description="Read a file", mode="read")
async def read_file(path: str) -> dict:
    return {"path": path, "content": "..."}

result = await router.dispatch(
    ToolCall(id="c1", name="read_file", arguments={"path": "README.md"}),
    consent_granted=False,                # required for `local` tools
)

StorageAdapter

class StorageAdapter(Protocol):
    async def upsert_session(self, session: AgentSession) -> AgentSession: ...
    async def get_session(self, session_id: str) -> AgentSession | None: ...
    async def list_sessions(
        self, *, user_id: str | None = None, chat_id: str | None = None,
        active_only: bool = False,
    ) -> list[AgentSession]: ...
    # … plus harness trace + chat-message persistence methods

Reference implementations: InMemoryStorage (for tests / sidecar) and SqlAlchemyStorage (for the FastAPI server).

TransportAdapter

class TransportAdapter(Protocol):
    async def emit(self, event: SSEEvent) -> None: ...
    async def receive(self) -> AsyncIterator[SSEEvent]: ...

Reference implementations: FastAPISseTransport (HTTP SSE) and StdioJsonRpcTransport (used by the sidecar).