Skip to content

Steerable

The agent plumbing you'd otherwise rewrite.

Typed wire protocol · pluggable LLM runtime · embeddable Python sidecar · headless React chat UI. Pick any subset, skip the rest — every layer ships on its own.

License: Apache 2.0 CI npm: @steerable/agent-ui PyPI: steerable-agent-runtime

Why Steerable

Building an LLM agent product means rewriting the same five things every time. Steerable is the layered library you'd build on day 30 — shipped on day 0.

One wire protocol

One JSON Schema → generated TypeScript types + Pydantic models, released in lockstep. content, tool_call, tool_result, error, done, budget_exhausted — all standardised, with a conformance suite keeping both SDKs byte-compatible.

Pure-function harness

Policy, budget, retry, completion, tracing, safety patterns. Zero I/O coupling — drop into FastAPI, Celery, or a notebook. 105 unit + golden tests.

Pluggable runtime

One LLMProvider interface with Ollama / OpenAI-compatible / Anthropic adapters, @tool decorator, ToolRouter, SSE-over-HTTP and stdio JSON-RPC transports.

Embeddable sidecar

A portable, signed CPython binary speaking JSON-RPC over stdio. Ship local LLMs inside Electron / Tauri / Wails — macOS notarised, Windows code-signed.

Headless React UI

5 components + 3 hooks + Tailwind preset. Every state covered by Storybook, axe a11y, and visual-regression baselines locked in CI.

Lockstep releases

All 7 publishable packages share one X.Y.Z, gated by CI on every tag push. npm tarballs ship sigstore provenance attestations.

Quickstart — pick your path

uv add steerable-agent-protocol steerable-agent-harness steerable-agent-runtime
from steerable_agent_runtime import ToolRouter, tool
from steerable_agent_protocol import ToolCall

router = ToolRouter()

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

result = await router.dispatch(
    ToolCall(id="c1", name="read_file", arguments={"path": "README.md"})
)
# result.success, result.data, result.error — all typed.
pnpm add @steerable/agent-protocol @steerable/agent-ui
import { ChatPanel, useChatStream } from '@steerable/agent-ui';

export function Chat() {
  const { messages, send, isStreaming } = useChatStream({
    endpoint: '/api/chats/123/send',
  });
  return <ChatPanel messages={messages} onSubmit={send} isStreaming={isStreaming} />;
}
pnpm add @steerable/agent-protocol @steerable/agent-ui
# Bundle the sidecar binary into resources/python-runtime/<platform>/
import { spawn } from 'node:child_process';

const proc = spawn(sidecarPath, [], { stdio: ['pipe', 'pipe', 'inherit'] });
proc.stdin.write(JSON.stringify({
  jsonrpc: '2.0', id: 1, method: 'agent.chat.stream',
  params: { messages: [{ role: 'user', content: 'hi' }] },
}) + '\n');
// SSE-over-JSON-RPC events stream back on stdout, one per line.

Architecture

Four tiers, strict no-upward-imports rule. Tier N never imports Tier N+1 — adopting any layer means inheriting only the layers below it.

graph BT
  T4["<b>Tier 4 · UI</b> (TypeScript / React)<br/>@steerable/agent-ui<br/>Hooks: useChatStream · useToolCallStatus · useAgentSession<br/>Components: ChatPanel · MessageList · OrchestrationPlanCard ·<br/>ToolCallRenderer · SSEStreamView<br/>Tailwind preset (dark-mode aware)"]

  T3S["<b>Tier 3 · Sidecar</b> (portable CPython binary)<br/>steerable-sidecar<br/>JSON-RPC over stdio · ready marker · graceful shutdown<br/>agent.chat.stream · tool.invoke · agent.session.* · trace.fetch"]

  T3R["<b>Tier 3 · Runtime</b> (Python only)<br/>steerable-agent-runtime<br/>LLMProvider (OpenAI-compat / Anthropic / Ollama)<br/>ToolRouter · StorageAdapter · TransportAdapter (FastAPI SSE)"]

  T2["<b>Tier 2 · Harness</b> (Python — single source of truth)<br/>steerable-agent-harness<br/>Policy · Budget · Retry · Completion · Tracing · Safety<br/><i>thin TS facade @steerable/agent-harness exists for parity tests</i>"]

  T1["<b>Tier 1 · Protocol</b> (TypeScript + Python, lock-step versions)<br/>@steerable/agent-protocol · steerable-agent-protocol<br/>SSEEvent · ChatMessage · ToolCall · ToolResult · AgentSession<br/>HarnessTrace · TraceSpan · SidecarRequest/Response/Notification"]

  T4 -.->|"shared wire types"| T1
  T4 -->|"spawns + JSON-RPC stdio (Electron)"| T3S
  T3S -->|"embeds"| T2
  T3S -->|"validates against"| T1
  T3R -->|"embeds"| T2
  T3R -->|"validates against"| T1
  T2 -->|"imports types"| T1

  classDef tier fill:#f4f4f5,stroke:#3f3f46,color:#18181b,rx:6,ry:6,padding:12;
  class T1,T2,T3R,T3S,T4 tier;

What's in the box

Package Tier What you get
@steerable/agent-protocol · steerable-agent-protocol 1 SSEEvent envelope, ToolCall / ToolResult, ChatMessage, sidecar JSON-RPC types — codegen from spec/, drift-checked in CI
@steerable/agent-harness · steerable-agent-harness 2 decide_tool_mode, consume_budget, next_retry_delay_ms, is_terminal_result, command-safety patterns
steerable-agent-runtime 3 LLMProvider adapters, ToolRouter + @tool, storage & transport adapters (FastAPI SSE, stdio JSON-RPC)
steerable-sidecar 3 Portable CPython binary — boots in <1s, macOS notarised, Windows signed
@steerable/agent-ui 4 ChatPanel, MessageList, OrchestrationPlanCard, ToolCallRenderer, SSEStreamView + hooks + Tailwind preset

Who's using it

DeepPath — web (agent-protocol + agent-ui), API (all three Python packages), Electron desktop (sidecar + UI). The framework was extracted from this codebase and is dogfooded back into it on every release.

Explore