Skip to content

packages agent

Zachary BENSALEM edited this page Aug 15, 2026 · 1 revision

Agent core

Active contributors: Mario Zechner, kt, Kevin Thomas

packages/agent is @earendil-works/pi-agent-core, the small general-purpose agent runtime at the heart of Prime Agent. It is a stateful wrapper around a low-level turn loop: it owns the conversation transcript, emits lifecycle events for UI updates, executes tool calls, and exposes queueing APIs for steering and follow-up messages. The real production harness is packages/coding-agent (the full session runtime); this package is the smaller core that packages/coding-agent builds on and that other apps can use standalone.

The package has 5 source files and 3 test files. It depends only on @earendil-works/pi-ai (providers and streaming) and typebox (tool schemas). Release docs and the README call it prime-agent-core; the workspace manifest name is @earendil-works/pi-agent-core.

Purpose

  • Provide a reusable Agent class that keeps agent state (systemPrompt, model, tools, messages) and runs prompts against an LLM.
  • Work with AgentMessage, an extensible message union, and convert to plain LLM messages only at the provider boundary.
  • Emit a typed event stream (AgentEvent) so any UI can render progress: streaming deltas, tool execution, and turn boundaries.
  • Support queueing messages into a running or finished run (steer, followUp) for interruption and follow-up work.
  • Route LLM calls through an app server when clients cannot hold provider credentials (streamProxy).

Directory layout

packages/agent/
├── src/
│   ├── agent.ts          # Agent class, state, event reduction, queues
│   ├── agent-loop.ts     # Turn loop: LLM streaming, tool execution, queue polling
│   ├── proxy.ts          # streamProxy: stream function for server-routed LLM calls
│   ├── types.ts          # AgentMessage, AgentState, AgentTool, AgentEvent, AgentLoopConfig
│   └── index.ts          # Re-exports
├── test/
│   ├── agent.test.ts     # Agent class behavior
│   ├── agent-loop.test.ts # Turn loop behavior
│   ├── e2e.test.ts       # End-to-end loop tests
│   └── utils/            # Shared fake tools (calculate.ts, get-current-time.ts)
├── README.md
├── CHANGELOG.md
└── package.json

Key abstractions

Type Full path Description
Agent packages/agent/src/agent.ts Stateful wrapper: owns transcript and state, emits events, executes tools, drains steering and follow-up queues
runAgentLoop / runAgentLoopContinue packages/agent/src/agent-loop.ts Async loop entry points that run prompts or continue from existing context; agentLoop / agentLoopContinue return the same loop as an EventStream
AgentMessage packages/agent/src/types.ts Union of LLM messages (user, assistant, toolResult) plus custom message types
CustomAgentMessages packages/agent/src/types.ts Empty interface that apps extend via declaration merging to add custom message types
AgentTool packages/agent/src/types.ts Tool definition: label, optional prepareArguments, execute(toolCallId, params, signal, onUpdate), per-tool executionMode
AgentEvent packages/agent/src/types.ts Lifecycle events emitted for UI: agent_start/end, turn_start/end, message_*, tool_execution_*
AgentLoopConfig packages/agent/src/types.ts Loop options: convertToLlm, transformContext, getApiKey, beforeToolCall, afterToolCall, shouldStopAfterTurn, queue polling, toolExecution
StreamFn packages/agent/src/types.ts Stream function contract matching streamSimple; failures must be encoded in the stream, not thrown
streamProxy packages/agent/src/proxy.ts Stream function that proxies LLM calls through a server (POST /api/stream) and rebuilds partial messages client-side

How it works

The message pipeline is AgentMessage[] -> transformContext() -> AgentMessage[] -> convertToLlm() -> Message[] -> LLM. transformContext (optional) prunes or injects context at the AgentMessage level; convertToLlm filters UI-only messages and converts custom types to messages the provider accepts. Both run before every LLM call.

Agent.prompt() and Agent.continue() wrap the loop in runWithLifecycle (packages/agent/src/agent.ts): they set isStreaming, run the loop, reduce every emitted event into agent state, and finally resolve after all awaited agent_end listeners settle. Failures are converted into an assistant message with stopReason "error" or "aborted" plus a diagnostic, rather than leaving the run in an undefined state.

The loop itself (runLoop in packages/agent/src/agent-loop.ts) has an inner and an outer loop. The inner loop streams one assistant response, executes any tool calls it contains, and checks for steering messages after each turn. The outer loop continues while follow-up or host-provided continuation messages arrive, so queued work runs after the agent would otherwise stop. Each turn streams through streamAssistantResponse, which applies the context transform, converts messages, resolves the API key, and drives the chosen streamFn, emitting message_update events with streaming deltas.

Tool calls are executed by executeToolCalls. The default mode is parallel: tool calls are preflighted sequentially (argument preparation, schema validation via validateToolArguments, then the beforeToolCall hook, which can block), then allowed tools run concurrently; tool_execution_end events follow completion order while persisted toolResult messages keep assistant source order. A tool with executionMode: "sequential" forces the whole batch sequential. The afterToolCall hook can override content, details, error flag, and the terminate hint. The loop stops early after a tool batch only when every finalized result sets terminate: true.

streamProxy (packages/agent/src/proxy.ts) is a StreamFn for apps that route LLM calls through a backend. It sends model, context, and options to <proxyUrl>/api/stream with a bearer token, parses data: NDJSON lines carrying bandwidth-reduced ProxyAssistantMessageEvents, and reconstructs the partial assistant message locally before re-emitting standard events.

Integration points

  • packages/coding-agent consumes this package as its core runtime; for example packages/coding-agent/src/core/tools/ipython.ts imports AgentTool from @earendil-works/pi-agent-core to define the IPython tool.
  • The web stack reaches it only through packages/coding-agent via web/server (prime-bridge.ts); browser code never imports @earendil-works/* directly.
  • Apps can plug in their own streamFn (including streamProxy) to control how LLM calls happen, and getApiKey to refresh expiring OAuth tokens per call.
  • Custom message types are added by extending CustomAgentMessages with declaration merging and handling them in convertToLlm.

Entry points for modification

  • Add tools: define an AgentTool and assign agent.state.tools.
  • Add custom message types: extend CustomAgentMessages and convert them in convertToLlm.
  • Change loop behavior without touching the loop: beforeToolCall, afterToolCall, shouldStopAfterTurn, getContinuationMessages, transformContext.
  • Proxy or intercept LLM traffic: streamFn and ProxyAssistantMessageEvent in packages/agent/src/proxy.ts.
  • Single-turn behavior vs agent class behavior: the loop is tested directly in packages/agent/test/agent-loop.test.ts; Agent behavior in packages/agent/test/agent.test.ts; end-to-end runs in packages/agent/test/e2e.test.ts.

Quick Start

import { Agent } from "prime-agent-core";
import { getModel } from "prime-agent-ai";

const agent = new Agent({
  initialState: {
    systemPrompt: "You are a helpful assistant.",
    model: getModel("anthropic", "claude-sonnet-4-20250514"),
  },
});

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

await agent.prompt("Hello!");

Key source files

Path Purpose
packages/agent/src/agent.ts Agent class, mutable state, subscribe, prompt/continue, steering and follow-up queues, lifecycle and event reduction
packages/agent/src/agent-loop.ts Turn loop: streaming assistant responses, tool call execution (parallel/sequential), queue polling, continuation
packages/agent/src/proxy.ts streamProxy and ProxyAssistantMessageEvent for server-routed LLM calls
packages/agent/src/types.ts All core types: AgentMessage, AgentState, AgentTool, AgentEvent, AgentLoopConfig, StreamFn
packages/agent/src/index.ts Public exports
packages/agent/test/agent.test.ts Agent class tests
packages/agent/test/agent-loop.test.ts Loop tests
packages/agent/test/e2e.test.ts End-to-end loop tests
packages/agent/README.md Usage reference, event sequences, and API documentation

Related pages

Clone this wiki locally