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.
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.