Getting Started¶
Boot the framework end-to-end in under 5 minutes. By the end you will have the protocol types in your project, a running sidecar speaking JSON-RPC, and a working
agent.chat.streamround-trip.
Prerequisites¶
| Tool | Version | Why |
|---|---|---|
| Node.js | ≥ 22 | TypeScript packages, Vitest |
| pnpm | ≥ 10 | Workspace + dependency resolution |
| Python | ≥ 3.10 | Harness, runtime, sidecar packages |
| uv | latest | Fast Python dependency manager (replaces pip/virtualenv flow) |
1 · Install the packages¶
Python (server-side or sidecar embedder)¶
# Either pull from PyPI:
uv add steerable-agent-protocol steerable-agent-harness steerable-agent-runtime
# Or, for sidecar consumers:
uv add steerable-sidecar
TypeScript (web app or Electron renderer)¶
The TypeScript Tier-2 facade (@steerable/agent-harness) is optional — it
exists mainly for cross-language conformance tests. Production callers should
either (a) stay on the protocol layer for browser-only typing, or (b) call the
Python harness via the sidecar.
2 · Hello, ToolCall (Python)¶
The smallest useful program — classify a tool, consume budget, mark done.
from steerable_agent_protocol import ToolCall, ToolResult, SSEEvent
from steerable_agent_harness import (
BudgetLimit,
BudgetState,
consume_budget,
decide_tool_mode,
is_terminal_result,
)
call = ToolCall(id="call_1", name="read_file", arguments={"path": "README.md"})
mode = decide_tool_mode(call.name) # → "read"
state, exhausted = consume_budget(
BudgetState(),
BudgetLimit(max_tokens=5_000, max_steps=30, max_tool_calls=10),
tokens=120, step=True, tool_call=True,
)
result = ToolResult(success=True, message=f"mode={mode}", data={"tokens_used": state.tokens_used})
done = is_terminal_result(result.model_dump())
print(SSEEvent(type="done" if done else "tool_result",
payload={"callId": call.id, "result": result.model_dump()}))
3 · Hello, ToolRouter (Python — Tier 3)¶
Same idea but with a registered tool that actually executes:
import asyncio
from steerable_agent_protocol import ToolCall
from steerable_agent_runtime import ToolRouter, tool
router = ToolRouter()
@tool(router=router, description="Read a file")
async def read_file(path: str) -> dict:
return {"path": path, "content": "Hello!"}
async def main() -> None:
result = await router.dispatch(ToolCall(id="c1", name="read_file", arguments={"path": "README.md"}))
print(result.success, result.data) # True {'path': 'README.md', 'content': 'Hello!'}
asyncio.run(main())
The @tool decorator auto-classifies via decide_tool_mode("read_file") →
"read" (read-only); see Tools spec for the rules.
4 · Hello, Sidecar (Electron / shell embedder)¶
Spawn the sidecar from any language that can fork a subprocess and speak JSON-RPC over stdio:
import { SidecarSupervisor } from '@steerable/sidecar-bridge';
// ↑ Or implement the 4 lines yourself: spawn `python -m steerable_sidecar`,
// wait for `__SIDECAR_READY__:{json}` on stderr, frame JSON-RPC on stdin/stdout.
const supervisor = await SidecarSupervisor.start({
pythonExecutable: '/path/to/portable-python',
bootTimeoutMs: 30_000,
});
const health = await supervisor.ping(); // { status: 'ok', version: '0.1.0', protocolVersion: '0.1.0', … }
const tools = await supervisor.listTools(); // [] (no tools registered yet)
await supervisor.streamChat(
{
provider: 'openai_compat',
model: 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY!,
messages: [{ role: 'user', content: 'Say hi' }],
},
{
onChunk: c => process.stdout.write(c.delta ?? ''),
onDone: () => console.log('\n[done]'),
onError: e => console.error(e),
},
);
await supervisor.shutdown();
The agent.chat.stream notification → stream.chunk flow is the canonical way
to pull LLM output back through the sidecar without going through HTTP.
5 · Hello, UI (React / browser)¶
import { ChatPanel, useChatStream } from '@steerable/agent-ui';
import '@steerable/agent-ui/tailwind-preset.css'; // or use the preset in tailwind.config.js
function MyChat() {
const { messages, isStreaming, sendUserMessage, cancel } = useChatStream({
transport: {
stream: async (input, onEvent) => {
const res = await fetch('/api/v2/chats/' + chatId + '/send', {
method: 'POST',
body: JSON.stringify({ message: input.content }),
});
// Parse SSE → call onEvent({ type: 'content', content: '…' }) etc.
},
},
});
return (
<ChatPanel
messages={messages}
isStreaming={isStreaming}
onSubmit={sendUserMessage}
onCancel={cancel}
/>
);
}
useChatStream is intentionally transport-agnostic — fetch+SSE in the browser,
WebSocket in your dev panel, IPC in Electron, sidecar JSON-RPC in a desktop
shell. The hook reduces protocol SSEEvents onto a ChatMessage[] regardless.
6 · Run the local dev preview¶
The DeepPath repo (one of the framework's reference consumers) ships a
dev-only preview page at /dev/framework-preview that mounts the
framework's ChatPanel + useChatStream + SSEStreamView against the real
backend. It's the recommended way to validate framework upgrades before
swapping more of your production UI:
What next?¶
- Read the Spec Overview to understand the cross-language contract pipeline
- Read the Architecture page to understand the Tier 1–4 boundary
- Browse
examples/for runnable starter projects - Migrating an existing DeepPath-internal agent? See the migration guide