Chapter 4 · Your First Agent
Chapter goals: Create a stateful agent with the
Agentclass, understandinitialStateand state machine concepts, and run through the complete round ofprompt().
4.1 What Is an Agent
The models.streamSimple from Chapter 2 is stateless — every call requires manually assembling context and handling the tool loop. The Agent class encapsulates all of this into a stateful runtime:
Agent automatically completes internally:
┌────────────────────────────────────────┐
│ prompt("...") │
│ → Appends user message to state.messages │
│ → Calls streamFn to request LLM │
│ → If model requests tool calls → executes tools │
│ → Fills in tool results → requests LLM again │
│ → Loops until model gives final answer │
│ → Emits events throughout for UI subscription │
└────────────────────────────────────────┘4.2 Creation and Configuration
import { Agent } from "@earendil-works/pi-agent-core";
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
const models = createModels();
models.setProvider(anthropicProvider());
const model = models.getModel("anthropic", "claude-sonnet-4-6")!;
const agent = new Agent({
// Initial state: defines what this Agent "is"
initialState: {
systemPrompt: "You are a rigorous TypeScript tutor.",
model, // Required: which model to use
// thinkingLevel: "medium", // Optional: reasoning intensity
// tools: [], // Optional: initial tool list (Chapter 6)
// messages: [], // Optional: pre-loaded history messages
},
// Required: streaming function. bind attaches this because it accesses the collection's routing logic internally
streamFn: models.streamSimple.bind(models),
});The complete fields of initialState include systemPrompt / model / thinkingLevel / tools / messages; other runtime options (such as toolExecution, beforeToolCall) are passed as constructor arguments and can be modified at any time afterward.
4.3 State Management
All state is集中 on agent.state, readable and writable:
// Modify identity and capabilities at any time during runtime
agent.state.systemPrompt = "Now you are a code review expert.";
agent.state.model = models.getModel("openai", "gpt-5-mini")!;
agent.state.thinkingLevel = "high";
// Read-only runtime state
console.log(agent.state.isStreaming); // Whether currently executing
console.log(agent.state.streamingMessage); // Partial message during streamingSemantics of array assignment
For state.tools / state.messages, assigning the whole array copies the top-level array before storing; but calling push() on the extracted array modifies the current state directly. Understanding this avoids confusion about "why did my push not take effect / took effect but not as expected."
4.4 prompt(): A Complete Round
// Simplest text prompt
await agent.prompt("Explain the difference between Promise.all and Promise.allSettled");
// Multimodal prompt with an image (model must support vision)
await agent.prompt("What is in this image?", [
{ type: "image", data: base64Data, mimeType: "image/jpeg" },
]);
// Pass AgentMessage directly (can carry custom fields)
await agent.prompt({
role: "user",
content: "Continue the example above",
timestamp: Date.now(),
});A single prompt() may internally trigger multiple LLM calls (when tools are present), but its return represents the complete end of this round — including all subscribers' event processing is settled.
4.5 Multi-turn Conversation: Automatic State Accumulation
// No need to manually maintain messages — the Agent accumulates context automatically
await agent.prompt("My name is Xiao Ming, and my favorite number is 7");
await agent.prompt("What is my favorite number?"); // The model can answer 7
// View accumulated history
console.log(agent.state.messages.length); // 4 messages: user + assistant, two rounds each
// Reset everything and start a fresh session
agent.reset();
console.log(agent.state.messages.length); // 04.6 Control Method Quick Reference
// Cancel the current operation mid-way (stopReason will be aborted)
agent.abort();
// Wait for the current round to fully complete (including event processing)
await agent.waitForIdle();
// Retry from existing context after an error (last message must be user or toolResult)
await agent.continue();4.7 Chapter Summary
Agent= stateful wrapper: automatically manages message history, executes tool loops, emits events;initialStatedefines identity (systemPrompt/model/thinkingLevel/tools/messages);- State is read/written at any time via
agent.state; whole-array assignment copies the array, push modifies in place; prompt()returning means the whole round is settled; multi-turn conversation accumulates context automatically,reset()clears and starts over.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Which two configurations are required when constructing an Agent?
2. What is the difference between `agent.state.messages = newArr` and `agent.state.messages.push(msg)`?
3. If a model requests 3 tools during one agent.prompt() call, how many LLM calls will occur?
4. To retry a failed round from the current context without adding a new user message, which method should you call?
🛠️ Hands-on Practice
- Create a "Socratic tutor" Agent: only asks questions, never gives answers. Have a 5-round conversation and print
state.messages.lengthafter each round. - During a conversation, call
agent.state.modelto switch to another vendor's model and verify context carries over seamlessly. - Call
agent.abort()during a long response and observe the stopReason of the last assistant message afterwaitForIdle().
The state machine is ready. Next Chapter 5 dives into event streams — the key to building real-time UIs.