13. One Kernel, Many Shells
Once the agent kernel is finished, you'll want to add more entry points: an interactive CLI, a one-shot print mode, a JSON event mode, an SDK, RPC, a TUI. Don't write a separate agent for each entry point. Every shell should subscribe to the same kernel event stream and differ only in its input/output protocol.
How shells differ
An interactive CLI cares about the editor, keyboard shortcuts, history, and the status line. Print mode cares about "give me a final answer and signal success or failure with the exit code." JSON mode cares about stdout discipline: every line is a machine-parseable event, with no human-readable logs mixed in. The SDK cares about a composable API. RPC cares about request ids, connection lifecycles, and concurrent sessions.
These are all product-layer differences and should not enter the agent loop. The kernel only emits events, receives user input, and maintains state.
Stdout discipline
Machine-readable modes are the easiest to break. Print a single debug line to stdout and downstream parsers fail. Suggested rules:
- In JSON mode, stdout emits JSONL events only.
- Human logs, debug output, and warnings go to stderr.
- Every event carries a
type, atimestamp, and an optionalid. - The final result is an explicit
doneorerrorevent.
This discipline shapes your entire code structure. Don't let low-level tools call console.log at will; tool output should be returned through events, and the shell decides how to display it.
The SDK is not a leak of internal objects
The SDK should not expose internal state to users as-is. It should provide stable methods:
type AgentSession = {
prompt(text: string): Promise<void>;
steer(text: string): Promise<void>;
followUp(text: string): Promise<void>;
abort(): void;
subscribe(listener: (event: AgentEvent) => void): () => void;
};
If SDK users can mutate the messages array directly, you can no longer maintain consistency across the log, compaction, and events. When advanced capabilities are needed, expose them through explicit APIs too — for example setModel, setActiveTools, compact.
The basic shape of RPC
RPC can run over JSONL on stdio, WebSocket, or HTTP SSE. The key is correlating requests with events:
{"id":"1","method":"prompt","params":{"text":"fix lint"}}
{"id":"1","event":{"type":"turn_start"}}
{"id":"1","event":{"type":"tool_execution_started","name":"bash"}}
{"id":"1","result":{"status":"completed"}}
The RPC layer should not reinterpret model messages. It merely serializes kernel events to the remote end and turns remote commands into kernel method calls.
The TUI is a projection of the event stream
A terminal UI looks complicated, but at its core it is still a projection of events. Assistant text deltas update the text block, tool events update the tool rows, queue events update the status bar, session events update the history tree. The TUI should not decide whether the agent's next step is a tool call; it only displays kernel facts and collects user input.
This is also why the earlier chapters insist on a unified event stream. If the TUI needs private state to work, JSON mode and the SDK will quickly fall behind.
Exercises
Implement two shells:
tiny-agent -p "task": print mode, outputting only the final answer.tiny-agent --json -p "task": JSONL mode, outputting events.
Acceptance criteria:
- Both modes use the same agent kernel.
- In JSON mode, stdout contains no non-JSON content.
- Tool output is delivered through events, not printed directly by tools.
- In print mode, a tool error yields a non-zero exit code or an explicit error event.
- Events the SDK subscribes to have the same semantics as JSON-mode events.