Learn Pi Agent
s01

AgentSession

Loop & Action

pi already runs the loop

17 LOC0 toolsBuilt-in agent loop
One prompt() is a whole agent loop; pi already turns it for you.
Mechanism map
User inputBuild contextModel calltool_call gateRun tooltool_resultSession

The whole loop is built into pi; you just plug into the entry point.

s01 → s02 → s03 → s04 → ... → s12

"pi is already running the loop — you just plug in." — One prompt() equals one full agent loop.

Harness layer: the loop — the model's first connection to the real world.


The problem

If you build an agent from scratch, the first thing you write is a while True: check whether the response contains a tool_use block, run the tool, append the result, and go back to the model.

If you use pi, that loop is already implemented locally — and it handles a pile of things you haven't thought about yet: multi-turn streaming, parallel tool execution, retries, automatic context compaction, session persistence, and resuming after interruption.

Rewriting it is a waste. What this chapter makes clear is: which entry point pi hands you, and what it is actually looping over inside.

The solution

One createAgentSession() plus one session.prompt().

import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
});

session.subscribe((event) => {
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
    process.stdout.write(event.assistantMessageEvent.delta);
  }
});

try {
  await session.prompt(process.argv.slice(2).join(" ") || "Describe this project in one sentence");
  console.log();
} finally {
  session.dispose();
}

prompt() does not resolve until the whole run is finished (including every tool call and retry). What you see in subscribe is each breath of the loop.

How it works

The loop inside pi (simplified):

prompt(text)
  │
  ├─► assemble messages + tools + system prompt
  │
  └─► while True:
        response = LLM(messages, tools)
        messages.append(response)

        any tool_use?
          ├─ no  → done, prompt() resolves
          └─ yes → tool_call hook → execute tool → append tool_result
                     └──────────► back to the LLM

If you have hand-written this loop, map it piece by piece against the built-in one:

Hand-written looppi (built in)Where you intervene
client.messages.create(...)the core calls the LLMbefore_provider_request event
inspect the tool_use blockthe core decidesno intervention needed
TOOL_HANDLERS[name](**input)4 built-in tools + your custom toolsdefineTool() / pi.registerTool()
run directly, no checkrun directly, trusts the model by defaulttool_call hook (s03)
messages.append(...)AgentSession statecontext event / session.state.messages
—persistence, compaction, retriesSessionManager (s07)

The key insight: pi does not hide the loop. It exposes every phase of the loop through events (observe) and hooks (intervene). Everything you learn in the chapters ahead hangs off this one loop.

The lifecycle of the event stream (memorize this picture — every later chapter uses it):

session_start
  └─► user prompt
        ├─► agent_start
        ├─► turn_start
        │     ├─► message_start / message_update / message_end
        │     ├─► tool_execution_start → tool_call → tool_result → tool_execution_end
        │     └─► turn_end
        ├─► agent_end
        └─► agent_settled   ← the final resting point after retries, compaction and follow-ups drain

What changed

ComponentBefore (bare model)s01 (pi)
Agent loopwrite your own while + stop_reason checkbuilt into createAgentSession()
Toolsimplement and dispatch yourselfbuilt-in read / write / edit / bash
Message statemaintain messages[] by handmaintained by AgentSession
PersistencenoneSessionManager (covered in s07)
Loop—unchanged, and it stays unchanged for the rest of the course

Try it

npm run s01
npm run s01 -- "List the current directory and describe one file in a sentence"
npm run s01 -- "Create hello.txt, write 'hi from pi' into it, then read it back to confirm"

The third command lets you watch the loop turn at least twice: one write, one read, with model reasoning in between.

A few more — each time, watch the order of events inside subscribe:

  • npm run s01 -- "Find every file that mentions TODO and summarize them"
  • npm run s01 -- "Translate the first paragraph of the README into English"

Did the model read first or bash first? How many turns did one prompt take?

Key points

  • createAgentSession() gives you an AgentSession that already knows how to loop.
  • Use subscribe() to observe the loop, and built-in plus custom tools to give the model hands.
  • SessionManager.inMemory() keeps the session off disk, which is right for scripts and tests; s07 switches to persistence.

Next

A single built-in toolset is not enough. We shrink the cost of "add a tool" to the minimum — s02: Custom tools.