Example

Run the agent loop on its own with a corpus tool

pi-agent-core's Agent runs tool then answer, records both in the transcript, and treats a miss as data: research-agent.ts under test.

research-agent.ts wraps pi-agent-core’s Agent — the class the coding agent is built on — with one corpus-reading tool and an injected stream function: layer 2, the agent core, with the coding agent’s sessions, trust gate, extensions and terminal absent. Unlike chapter 1’s self-contained printing script, this is a library function: the corpus, model and stream function come from the caller, and the transcript is data you assert on. All below is observed under scripted responses on Pi 1.0.4.

Run it

The file below is the chapter’s canonical source, reproduced exactly; it is not standalone. research-agent.test.ts scripts the provider and asserts the transcript — run it on the example repository’s test runner, Pi 1.0.4.

import { Agent, type AgentTool } from "@earendil-works/pi-agent-core";
import { Type, type Model, type Models } from "@earendil-works/pi-ai";

export interface Corpus {
	[id: string]: string;
}

// A tool is data plus one function. This one is read-only and pure, so it is
// safe to run in parallel with itself.
const LookupParams = Type.Object({ query: Type.String() });

export function lookupEvidence(corpus: Corpus): AgentTool<typeof LookupParams> {
	return {
		name: "lookup_evidence",
		label: "Look up evidence",
		description: "Return the corpus entries whose text contains the query. Use short keywords.",
		parameters: LookupParams,
		async execute(_id, params) {
			const q = params.query.toLowerCase();
			const hits = Object.entries(corpus).filter(([, text]) => text.toLowerCase().includes(q));
			const text = hits.length ? hits.map(([id, t]) => `[${id}] ${t}`).join("\n") : "no matches";
			return { content: [{ type: "text", text }], details: { query: params.query, hits: hits.map(([id]) => id) } };
		},
	};
}

// The agent core takes exactly two things from the outside: a model and a
// stream function. The prompt, the tools and the transcript are ours.
export function makeResearchAgent(models: Models, model: Model<any>, corpus: Corpus): Agent {
	return new Agent({
		initialState: {
			systemPrompt: "Research the claim using lookup_evidence. Answer in one sentence.",
			model,
			tools: [lookupEvidence(corpus)],
		},
		streamFn: models.streamSimple.bind(models),
	});
}

Expected outcome

research-agent.test.ts assertion What the loop did
Loop runs tool then answer Transcript roles were ["user", "assistant", "toolResult", "assistant"]; the provider was called twice, one request per turn
Events fire in order tool_execution_start before tool_execution_end; the last event was agent_end
A tool that finds nothing lookup_evidence returned "no matches" with isError: false — data, not a failure

In the bare core, agent_end really is the end — agent_settled and agent_before_settle are absent from the core’s types; they belong to the coding agent’s extra work after the run.

Mechanism and limitations

Documented: the Agent constructor options, streamFn, AgentTool as an extension of Tool whose execute() receives already-validated params, throw-to-fail (a thrown execute becomes a tool result with isError: true), parallel tool execution by default, and the two declared runtime dependencies (pi-agent-core README and declarations, 1.0.4). Observed: research-agent.test.ts asserts the transcript shape, the request count and the miss-as-data rule under scripted responses; the absence of agent_settled from the core’s types is a reading of the declarations, not a runtime observation.

Assigning agent.state.messages is authoritative here, with no session above to disagree — inside the coding agent, SessionManager owns the copy (chapter 34, observed). The tool is read-only and pure, so nothing guards it: executionMode: "sequential" and chapter 39’s hooks are not shown, and the scripted provider shows plumbing, not real-model tool choice.

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.