-
Notifications
You must be signed in to change notification settings - Fork 0
packages agent
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.
- Provide a reusable
Agentclass 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).
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
| 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 |
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.
-
packages/coding-agentconsumes this package as its core runtime; for examplepackages/coding-agent/src/core/tools/ipython.tsimportsAgentToolfrom@earendil-works/pi-agent-coreto define the IPython tool. - The web stack reaches it only through
packages/coding-agentviaweb/server(prime-bridge.ts); browser code never imports@earendil-works/*directly. - Apps can plug in their own
streamFn(includingstreamProxy) to control how LLM calls happen, andgetApiKeyto refresh expiring OAuth tokens per call. - Custom message types are added by extending
CustomAgentMessageswith declaration merging and handling them inconvertToLlm.
- Add tools: define an
AgentTooland assignagent.state.tools. - Add custom message types: extend
CustomAgentMessagesand convert them inconvertToLlm. - Change loop behavior without touching the loop:
beforeToolCall,afterToolCall,shouldStopAfterTurn,getContinuationMessages,transformContext. - Proxy or intercept LLM traffic:
streamFnandProxyAssistantMessageEventinpackages/agent/src/proxy.ts. - Single-turn behavior vs agent class behavior: the loop is tested directly in
packages/agent/test/agent-loop.test.ts;Agentbehavior inpackages/agent/test/agent.test.ts; end-to-end runs inpackages/agent/test/e2e.test.ts.
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!");| 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 |
- Coding agent - the full harness runtime that builds on this core
- RLM programming model - recursion and subagents
- Session runtime - IPython kernel provisioning
- Getting started - install instructions