@earendil-works/pi-agent-core review
Your model can request a tool call, but your application still needs to run it and feed the result back. @earendil-works/pi-agent-core 1.4.0 provides a stateful agent loop with tool execution and event streaming, built on pi-ai. The Agent class keeps a transcript in memory, executes supplied tools, and queues steering or follow-up messages. Your application supplies the model connection, storage, and authorization rules. Version 1.0.0 removed sessions, session storage, and the durable runtime from this package.
Choose @earendil-works/pi-agent-core when you want an embeddable loop and can own the tools, permissions, and storage around it. Start with a read-only tool. Before connecting a tool that writes real data, define how your application records its result and handles an interrupted run.
Answers from our run
@earendil-works/pi-agent-core or ai: which should you use?
ai: Consider Vercel's AI SDK for a shared interface across model providers. Its supplied npm description offers a single interface through the Vercel AI Gateway or direct provider connections. This supports a scope comparison, not a performance or feature-parity judgment. Choose @earendil-works/pi-agent-core when you want an embeddable loop and can own the tools, permissions, and storage around it.
When should you not use @earendil-works/pi-agent-core?
You need this package to provide durable sessions. Version 1.0.0 removed sessions, session storage, and the durable runtime.
Use it if
- You're embedding an agent in a TypeScript application and want to supply its tools and model connection.
- You need conversation state, streaming events, and tool-result feedback in one loop.
- You can enforce authorization in your application services and use beforeToolCall for an additional execution check.
- Your team can own transcript storage and recovery after an interrupted operation.
- You need this package to provide durable sessions. Version 1.0.0 removed sessions, session storage, and the durable runtime.
- Your deployment cannot meet the declared Node >=22.19.0 requirement.
- You need a finished coding assistant with a terminal interface and bundled file tools. The inspected core package instead exposes an agent loop and expects supplied tools.
- Your tools change money, permissions, or shared records, and your application cannot enforce authorization or handle interrupted writes.
- You only need a model response without conversation state or tool feedback. Consider whether a provider client is sufficient.
Setup reality
Install explicit dependencies for these imports: npm install --save-exact @earendil-works/pi-agent-core@1.4.0 @earendil-works/pi-ai@1.4.0 typebox@1.3.27. Core 1.4.0 is an ESM package with TypeScript declarations. Its registry metadata declares Node >=22.19.0 and pins pi-ai 1.4.0 and typebox 1.3.27 as runtime dependencies. Use an ESM entry point and retain the lockfile. Provider configuration, credentials, and account access remain prerequisites for a model request.
The snippets are unexecuted TypeScript illustrations based on the tagged 1.4.0 README and supplied Agent implementation. The supplied evidence contains no installation results, model-call results, benchmarks, security audit, or comparison of published tarball files against the tagged files. Later snippets reuse the agent from the first example. Try each separately while the agent is idle unless the example starts a run; configuration changes remain in effect until changed again.
Agent keeps its transcript in memory. Restoring messages is possible, but a saved transcript does not resolve an interrupted external write. Before connecting a tool that changes data, define what happens if the operation completes and the process dies before your application records the result. Enforce authorization in the service that performs the operation, even when beforeToolCall also checks it. Blocking a call supplies feedback to the model; it does not necessarily end the run.
Tool execution defaults to parallel. Set toolExecution to sequential when calls depend on order. A tool with executionMode: 'sequential' forces the entire batch to execute sequentially. This controls scheduling; your application still needs transactions when writes must succeed together. Overlapping prompt() calls on one Agent are rejected. Use steer() or followUp() to queue additional input, or wait for completion. Queuing input does not abort the active operation.
Subscribers are awaited in registration order. Agent.subscribe() listeners are awaited in registration order, and prompt() and waitForIdle() settle only after awaited agent_end listeners finish. Keep subscriber work bounded. Awaiting a new prompt or waitForIdle() inside a subscriber on the same active Agent can prevent settlement. After a prompt settles, inspect state.errorMessage and the final assistant message rather than treating promise resolution as proof of success.
The tagged changelog dates 1.4.0 to October 21, 2026 and records request identity controls. Older integrations also need to account for the removal of durable/session APIs in 1.0.0 and the replacement of shouldStopAfterTurn with finishTurn in 0.87.0.
Ratings use a documentation-based rubric from 1 to 5: 1 means insufficient support for this integration task; 2 means a narrow or poorly explained surface; 3 means usable with explicit caveats; 4 means clearly specified behavior supported by supplied documentation or source; 5 requires operational evidence beyond this record. The category rationales explain what each score credits. These are editorial judgments, not measurements of reliability, adoption, compatibility, or support response time.
Patterns
Connect a model explicitly create-agent
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');
if (!model) throw new Error('Model not found');
const agent = new Agent({
initialState: { model, systemPrompt: 'Use only the tools provided.' },
streamFn: models.streamSimple.bind(models),
});
await agent.prompt('Explain what tools you have.');Unexecuted illustration based on the tagged 1.4.0 quick start. Registration does not establish provider authorization, account access, or availability of the named model to your application.
Read text deltas stream-text
const unsubscribe = agent.subscribe((event) => {
if (event.type === 'message_update' &&
event.assistantMessageEvent.type === 'text_delta') {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
try {
await agent.prompt('Describe the next step.');
} finally {
unsubscribe();
}A message_update event concerns an assistant message, but its nested event is not always a text delta. Subscribers run in registration order and are awaited, so slow handling delays the loop and final settlement.
Expose a small lookup define-read-tool
import { Type } from 'typebox';
import type { AgentTool } from '@earendil-works/pi-agent-core';
const entries = new Map([['public', 'Published help text']]);
const lookup: AgentTool = {
name: 'lookup_entry',
label: 'Look up entry',
description: 'Read a named entry from the example catalog',
parameters: Type.Object({ key: Type.String() }),
execute: async (_callId, { key }) => {
const text = entries.get(key);
if (text === undefined) throw new Error('Unknown entry');
return { content: [{ type: 'text', text }], details: { key } };
},
};
agent.state.tools = [lookup];This unexecuted example uses local fixture data, not a database integration. The README directs tools to throw on failure; the agent reports thrown errors to the model as tool errors. Assigning state.tools replaces the executable loadout and copies the top-level array.
Select sequential tool batches choose-tool-order
agent.toolExecution = 'sequential';
await agent.prompt('Use the catalog to answer my question.');The default is parallel. A tool's executionMode: 'sequential' also forces the entire batch into sequential execution. Serial scheduling does not supply application transactions.
Deny a tool before execution block-tool-call
agent.beforeToolCall = async ({ toolCall }) => {
if (toolCall.name === 'lookup_entry') {
return { block: true, reason: 'Catalog access is disabled.' };
}
return undefined;
};This deliberately disables the lookup tool and leaves the hook installed. The hook runs after validated argument parsing. Blocking this call does not guarantee that the run ends; the model can receive the blocked result and continue.
Queue another request during a run queue-follow-up
const pending = agent.prompt('Explain the available catalog.');
agent.followUp({
role: 'user',
content: 'Then give one example question.',
timestamp: Date.now(),
});
await pending;Overlapping prompt() calls on one Agent are rejected. followUp() queues input for when the loop would otherwise stop and does not cancel the current response. A finishTurn decision of action: 'end' can leave queued input for a later run.
Signal cancellation and await settlement abort-active-run
const pending = agent.prompt('Explain the catalog entries.');
agent.abort();
await pending;
await agent.waitForIdle();
console.log(agent.state.errorMessage);abort() signals the active run. It does not undo completed external operations. waitForIdle() includes awaited agent_end listeners; awaiting it inside a listener on that same run can prevent settlement. An undefined errorMessage alone does not establish that an intended operation succeeded.
Keep an in-memory transcript checkpoint retain-transcript
await agent.waitForIdle();
const checkpoint = agent.state.messages.slice();
// Later, while idle, restore the top-level message array.
agent.state.messages = checkpoint;The setter copies the array; the message objects remain shared. This illustrates an in-memory checkpoint, not durable storage or recovery of external side effects. Preserve system messages because they carry the replayed prompt.
Stop after a completed normal turn end-after-turn
agent.finishTurn = async ({ message }) => {
if (message.stopReason === 'error' ||
message.stopReason === 'aborted') return;
return { action: 'end' };
};This ends after a normal assistant turn and its finalized tool results, before another response to those results. The hook remains installed. finishTurn replaces shouldStopAfterTurn; returning action: 'continue' unconditionally would keep requesting responses.
Clear the conversation after settlement reset-when-idle
await agent.waitForIdle();
agent.reset();reset() rejects during an active run. It clears conversation state and queues while retaining the replayed prompt/tool baseline. It does not erase every instruction or undo external side effects.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ai | npm | Consider Vercel's AI SDK for a shared interface across model providers. Its supplied npm description offers a single interface through the Vercel AI Gateway or direct provider connections. This supports a scope comparison, not a performance or feature-parity judgment. |
| openai | npm | The openai package is the official TypeScript library for the OpenAI API. Consider it when OpenAI API access is the requirement and you intend to implement the surrounding application behavior yourself. The supplied metadata does not establish feature parity with Pi's agent loop. |
More ai / ml guides
openai · mcp · huggingface-hub · tiktoken · scikit-learn · tokenizers · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.