Example

Drive one agent through three interfaces

The same scripted run through print, JSON and RPC answers identically; the observer differs, and the failure contract differs with it.

The terminal is one of five ways to drive the agent, and switching drivers changes how you watch it, not what it is. Two tiny drivers below put that claim in front of the same agent-core agent, and a shipped-binary test pushes it across print, JSON and RPC. Everything here ran on 1.0.4 under a scripted model — it exercises how each interface observes the agent, not a provider’s output.

Two drivers, one agent

The file below is the chapter’s canonical pair of interfaces over one agent, reproduced exactly:

import type { Agent } from "@earendil-works/pi-agent-core";

// Interface 1: streaming. What a terminal does: draw text as it arrives.
export async function drive_streaming(agent: Agent, prompt: string, draw: (chunk: string) => void): Promise<void> {
	const off = agent.subscribe((e) => {
		if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta") {
			draw(e.assistantMessageEvent.delta);
		}
	});
	try {
		await agent.prompt(prompt);
	} finally {
		off();
	}
}

// Interface 2: one-shot. What print mode does: no events, the final text and a status.
export async function drive_oneshot(agent: Agent, prompt: string): Promise<{ text: string; ok: boolean }> {
	await agent.prompt(prompt);
	const last = [...agent.state.messages].reverse().find((m) => m.role === "assistant") as any;
	const text = last.content.filter((b: any) => b.type === "text").map((b: any) => b.text).join("");
	return { text, ok: last.stopReason !== "error" && last.stopReason !== "aborted" };
}

Expected outcome

Two identical scripted agents, driven one way each, produced transcripts of the same shape and the same provider call count; the only difference is what each observer saw — the answer in pieces for the streaming driver, whole for the one-shot driver.

On the shipped binary the same scripted run through print, JSON and RPC ended with the same final answer, and JSON and RPC produced the same lifecycle events (tool_execution_start, tool_execution_end, turn_end, agent_end, agent_settled) in the same order with the tool running in both.

Print JSON RPC
What you observe one string, nothing else events, no command responses responses and events
A failed response non-zero exit status, message on stderr exit 0, failure in the event stream failure in the event stream; the prompt command itself succeeded
When it is finished the process exits agent_settled, then exit agent_settled, the process stays up
How you match a reply not needed not needed by request id

The agent is the same in all three columns; everything in the table belongs to the driver.

Mechanism and limitations

Documented: cli-integration.md states all modes use the same agent, sessions, resources and tools and that the SDK is “not a CLI mode”. Observed: drivers.test.ts (agent core, in-process) and binary.test.ts (shipped binary, child process) both ran on 1.0.4 against scripted responses, asserting the same answer, the same event order, the observer differences and the failure-contract differences above.

The shipped-binary run is a real subprocess with real argument parsing and a real configuration directory, but no provider — the scripted model is injected through an extension. The bounded claim is same agent, same answer, same lifecycle events behind each interface. It is not “any interface substitutes for any other”, it is not a real model’s response, and a confirmation-capable interface is not merely a different observer. The chapter’s rule: if switching the interface changes what an extension decides, the extension has mixed presentation into policy.

Full source and test .

Understand this example

Copy this prompt into your AI tool. No code runs here.

Apply this example

Copy this prompt into your AI tool. No code runs here.