Skip to content

Chapter 4 · Your First Agent ​

Chapter goals: Create a stateful agent with the Agent class, understand initialState and state machine concepts, and run through the complete round of prompt().

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:

text
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 ​

typescript
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:

typescript
// 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 streaming

Semantics 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 ​

typescript
// 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 ​

typescript
// 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);  // 0

4.6 Control Method Quick Reference ​

typescript
// 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;
  • initialState defines 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 ​

  1. Create a "Socratic tutor" Agent: only asks questions, never gives answers. Have a 5-round conversation and print state.messages.length after each round.
  2. During a conversation, call agent.state.model to switch to another vendor's model and verify context carries over seamlessly.
  3. Call agent.abort() during a long response and observe the stopReason of the last assistant message after waitForIdle().

The state machine is ready. Next Chapter 5 dives into event streams — the key to building real-time UIs.