Example

Watch the agent loop call a tool

Run the real Pi 1.0.4 agent core with a scripted provider, a word-count tool and two assistant turns.

You can observe the loop without credentials. The provider scripts the assistant’s responses; the real Agent executes the tool and emits the events. This proves orchestration, not a real model’s choice of tool or interpretation of its result.

Run it

Use Node.js 22.18 or later with built-in TypeScript stripping. In an empty folder:

npm init -y
npm pkg set type=module
npm install @earendil-works/pi-agent-core@1.0.4 @earendil-works/pi-ai@1.0.4

Save the complete file below as agent-core.ts, then run node agent-core.ts. It is an exact copy of the book’s canonical example, not a replacement harness.

import { Agent, type AgentTool } from "@earendil-works/pi-agent-core";
import {
	Type,
	createModels,
	fauxAssistantMessage,
	fauxProvider,
	fauxText,
	fauxToolCall,
} from "@earendil-works/pi-ai";

// The provider layer: a scripted stand-in for a model.
const faux = fauxProvider();
const models = createModels();
models.setProvider(faux.provider);

// A tool is a name, a schema and an execute(). Nothing here knows about Pi.
const WordCountParams = Type.Object({ text: Type.String() });

const wordCount: AgentTool<typeof WordCountParams> = {
	name: "word_count",
	label: "Word Count",
	description: "Count the words in a string.",
	parameters: WordCountParams,
	async execute(_id, params) {
		const n = params.text.trim().split(/\s+/).filter(Boolean).length;
		return { content: [{ type: "text", text: String(n) }], details: { words: n } };
	},
};

// Turn 1 asks for the tool. Turn 2 answers from its result.
faux.setResponses([
	fauxAssistantMessage([fauxToolCall("word_count", { text: "the quick brown fox" })], { stopReason: "toolUse" }),
	fauxAssistantMessage([fauxText("Four words.")]),
]);

// The agent core: loop, messages, state, tool execution, events.
const agent = new Agent({
	initialState: { systemPrompt: "You count words.", model: faux.getModel(), tools: [wordCount] },
	streamFn: models.streamSimple.bind(models),
});

agent.subscribe((e) => {
	if (e.type === "tool_execution_end") console.log("tool:", e.toolName, JSON.stringify(e.result.details));
	if (e.type === "message_end" && e.message.role === "assistant") console.log("stop:", e.message.stopReason);
});

await agent.prompt("How many words in 'the quick brown fox'?");

Expected outcome

The existing Chapter 1 test asserts this exact stdout at Pi 1.0.4:

stop: toolUse
tool: word_count {"words":4}
stop: stop
Event What you learn
First assistant stops with toolUse The scripted request names word_count
Tool execution ends The real tool returned { "words": 4 }
Second assistant stops A subsequent assistant turn completed

Mechanism and limitations

Documented: the pinned agent-core exported declarations describe Agent, AgentTool, event subscriptions and tool execution. Observed: the canonical agent-core.test.ts runs this file and checks the three lines above. The second response is scripted as “Four words.”; it is not evidence of model reasoning.

Full source and test are maintained in the Pi repository. The site provenance check catches source drift.

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.