# @f5-sales-demo/pi-agent-core

> General-purpose agent with transport abstraction, state management, and attachment support

Latest version **23.9.4** (published 2026-10-20) · MIT license · 0 weekly downloads

## Install

```sh
npm install @f5-sales-demo/pi-agent-core
pnpm add @f5-sales-demo/pi-agent-core
yarn add @f5-sales-demo/pi-agent-core
bun add @f5-sales-demo/pi-agent-core
```

## Health

**Score 55/100 (C)** — status: active.

Positive: no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; no esm support.

## Facts

| | |
|---|---|
| Version | 23.9.4 |
| Published | 2026-10-20 |
| First published | 2026-06-26 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 0 |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 0 |
| Author | Can Boluk |
| Maintainers | robinmordasiewicz |
| Keywords | ai, agent, llm, transport, state-management |

## Links

- npm: https://www.npmjs.com/package/@f5-sales-demo/pi-agent-core
- Repository: https://github.com/f5-sales-demo/xcsh
- Issues: https://github.com/f5-sales-demo/xcsh/issues
- npm.io page: https://npm.io/package/@f5-sales-demo/pi-agent-core

## Alternatives

- [@expo/fingerprint](https://npm.io/package/@expo/fingerprint.md) — 6.2M weekly downloads
- [@azure/monitor-opentelemetry-exporter](https://npm.io/package/@azure/monitor-opentelemetry-exporter.md) — 850.0K weekly downloads
- [@azure/monitor-opentelemetry](https://npm.io/package/@azure/monitor-opentelemetry.md) — 624.0K weekly downloads
- [@posthog/ai](https://npm.io/package/@posthog/ai.md) — 423.3K weekly downloads
- [fakefilter](https://npm.io/package/fakefilter.md) — 63.9K weekly downloads

## Recent versions

- 23.9.4 (latest) — 2026-10-20
- 23.3.4 (backfill) — 2026-10-14
- 23.9.3 — 2026-10-19
- 23.9.2 — 2026-10-19
- 23.9.1 — 2026-10-19
- 23.9.0 — 2026-10-19
- 23.8.0 — 2026-10-18
- 23.7.0 — 2026-10-18
- 23.6.9 — 2026-10-18
- 23.6.8 — 2026-10-18
- 23.6.0 — 2026-10-18
- 23.5.2 — 2026-10-16
- 23.5.1 — 2026-10-16
- 23.5.0 — 2026-10-15
- 23.4.2 — 2026-10-14
- … 424 more at https://npm.io/package/@f5-sales-demo/pi-agent-core/versions

## README

# @f5-sales-demo/pi-agent

Stateful agent with tool execution and event streaming. Built on `@f5-sales-demo/pi-ai`.

## Installation

```bash
npm install @f5-sales-demo/pi-agent
```

## Quick Start

```typescript
import { Agent } from "@f5-sales-demo/pi-agent";
import { getModel } from "@f5-sales-demo/pi-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") {
  // Stream just the new text chunk
  process.stdout.write(event.assistantMessageEvent.delta);
 }
});

await agent.prompt("Hello!");
```

## Core Concepts

### AgentMessage vs LLM Message

The agent works with `AgentMessage`, a flexible type that can include:

- Standard LLM messages (`user`, `assistant`, `toolResult`)
- Custom app-specific message types via declaration merging

LLMs only understand `user`, `assistant`, and `toolResult`. The `convertToLlm` function bridges this gap by filtering and transforming messages before each LLM call.

### Message Flow

```
AgentMessage[] → transformContext() → AgentMessage[] → convertToLlm() → Message[] → LLM
                    (optional)                           (required)
```

1. **transformContext**: Prune old messages, inject external context
2. **convertToLlm**: Filter out UI-only messages, convert custom types to LLM format

## Event Flow

The agent emits events for UI updates. Understanding the event sequence helps build responsive interfaces.

### prompt() Event Sequence

When you call `prompt("Hello")`:

```
prompt("Hello")
├─ agent_start
├─ turn_start
├─ message_start   { message: userMessage }      // Your prompt
├─ message_end     { message: userMessage }
├─ message_start   { message: assistantMessage } // LLM starts responding
├─ message_update  { message: partial... }       // Streaming chunks
├─ message_update  { message: partial... }
├─ message_end     { message: assistantMessage } // Complete response
├─ turn_end        { message, toolResults: [] }
└─ agent_end       { messages: [...] }
```

### With Tool Calls

If the assistant calls tools, the loop continues:

```
prompt("Read config.json")
├─ agent_start
├─ turn_start
├─ message_start/end  { userMessage }
├─ message_start      { assistantMessage with toolCall }
├─ message_update...
├─ message_end        { assistantMessage }
├─ tool_execution_start  { toolCallId, toolName, args }
├─ tool_execution_update { partialResult }           // If tool streams
├─ tool_execution_end    { toolCallId, result }
├─ message_start/end  { toolResultMessage }
├─ turn_end           { message, toolResults: [toolResult] }
│
├─ turn_start                                        // Next turn
├─ message_start      { assistantMessage }           // LLM responds to tool result
├─ message_update...
├─ message_end
├─ turn_end
└─ agent_end
```

### continue() Event Sequence

`continue()` resumes from existing context without adding a new message. Use it for retries after errors.

```typescript
// After an error, retry from current state
await agent.continue();
```

The last message in context must be `user` or `toolResult` (not `assistant`).

### Event Types

| Event                   | Description                                                     |
| ----------------------- | --------------------------------------------------------------- |
| `agent_start`           | Agent begins processing                                         |
| `agent_end`             | Agent completes with all new messages                           |
| `turn_start`            | New turn begins (one LLM call + tool executions)                |
| `turn_end`              | Turn completes with assistant message and tool results          |
| `message_start`         | Any message begins (user, assistant, toolResult)                |
| `message_update`        | **Assistant only.** Includes `assistantMessageEvent` with delta |
| `message_end`           | Message completes                                               |
| `tool_execution_start`  | Tool begins                                                     |
| `tool_execution_update` | Tool streams progress                                           |
| `tool_execution_end`    | Tool completes                                                  |

## Agent Options

```typescript
const agent = new Agent({
  // Initial state
  initialState: {
    systemPrompt: string,
    model: Model,
    thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh",
    tools: AgentTool<any>[],
    messages: AgentMessage[],
  },

  // Convert AgentMessage[] to LLM Message[] (required for custom message types)
  convertToLlm: (messages) => messages.filter(...),

  // Transform context before convertToLlm (for pruning, compaction)
  transformContext: async (messages, signal) => pruneOldMessages(messages),

  // How to handle queued messages: "one-at-a-time" (default) or "all"
  queueMode: "one-at-a-time",

  // Custom stream function (for proxy backends)
  streamFn: streamProxy,

  // Dynamic API key resolution (for expiring OAuth tokens)
  getApiKey: async (provider) => refreshToken(),

  // Tool execution context (late-bound UI/session access)
  getToolContext: () => ({ /* app-defined */ }),
});
```

## Agent State

```typescript
interface AgentState {
 systemPrompt: string;
 model: Model;
 thinkingLevel: ThinkingLevel;
 tools: AgentTool<any>[];
 messages: AgentMessage[];
 isStreaming: boolean;
 streamMessage: AgentMessage | null; // Current partial during streaming
 pendingToolCalls: Set<string>;
 error?: string;
}
```

Access via `agent.state`. During streaming, `streamMessage` contains the partial assistant message.

## Methods

### Prompting

```typescript
// Text prompt
await agent.prompt("Hello");

// With images
await agent.prompt("What's in this image?", [{ type: "image", data: base64Data, mimeType: "image/jpeg" }]);

// AgentMessage directly
await agent.prompt({ role: "user", content: "Hello", timestamp: Date.now() });

// Continue from current context (last message must be user or toolResult)
await agent.continue();
```

### State Management

```typescript
agent.setSystemPrompt("New prompt");
agent.setModel(getModel("openai", "gpt-4o"));
agent.setThinkingLevel("medium");
agent.setTools([myTool]);
agent.replaceMessages(newMessages);
agent.appendMessage(message);
agent.clearMessages();
agent.reset(); // Clear everything
```

### Control

```typescript
agent.abort(); // Cancel current operation
await agent.waitForIdle(); // Wait for completion
```

### Events

```typescript
const unsubscribe = agent.subscribe((event) => {
 console.log(event.type);
});
unsubscribe();
```

## Steering & Follow-up

Queue messages to inject during tool execution (steering) or after the agent would otherwise stop (follow-up):

```typescript
agent.setSteeringMode("one-at-a-time");
agent.setInterruptMode("immediate");

// While agent is running tools
agent.steer({
 role: "user",
 content: "Stop! Do this instead.",
 timestamp: Date.now(),
});

// Queue a follow-up to run after the current turn completes
agent.followUp({
 role: "user",
 content: "After that, summarize the changes.",
 timestamp: Date.now(),
});
```

Steering messages are checked after each tool call by default. Set `interruptMode` to `"wait"` to defer
steering until the current turn completes.

## Custom Message Types

Extend `AgentMessage` via declaration merging:

```typescript
declare module "@f5-sales-demo/pi-agent" {
 interface CustomAgentMessages {
  notification: { role: "notification"; text: string; timestamp: number };
 }
}

// Now valid
const msg: AgentMessage = { role: "notification", text: "Info", timestamp: Date.now() };
```

Handle custom types in `convertToLlm`:

```typescript
const agent = new Agent({
 convertToLlm: (messages) =>
  messages.flatMap((m) => {
   if (m.role === "notification") return []; // Filter out
   return [m];
  }),
});
```

## Tools

Define tools using `AgentTool`:

```typescript
import { Type } from "@sinclair/typebox";

const readFileTool: AgentTool = {
 name: "read_file",
 label: "Read File", // For UI display
 description: "Read a file's contents",
 parameters: Type.Object({
  path: Type.String({ description: "File path" }),
 }),
 execute: async (toolCallId, params, signal, onUpdate, context) => {
  const content = await fs.readFile(params.path, "utf-8");

  // Optional: stream progress
  onUpdate?.({ content: [{ type: "text", text: "Reading..." }], details: {} });

  return {
   content: [{ type: "text", text: content }],
   details: { path: params.path, size: content.length },
  };
 },
};

agent.setTools([readFileTool]);
```

### Error Handling

**Throw an error** when a tool fails. Do not return error messages as content.

```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
 if (!fs.existsSync(params.path)) {
  throw new Error(`File not found: ${params.path}`);
 }
 // Return content only on success
 return { content: [{ type: "text", text: "..." }] };
};
```

Thrown errors are caught by the agent and reported to the LLM as tool errors with `isError: true`.

## Proxy Usage

For browser apps that proxy through a backend:

```typescript
import { Agent, streamProxy } from "@f5-sales-demo/pi-agent";

const agent = new Agent({
 streamFn: (model, context, options) =>
  streamProxy(model, context, {
   ...options,
   authToken: "...",
   proxyUrl: "https://your-server.com",
  }),
});
```

## Low-Level API

For direct control without the Agent class:

```typescript
import { agentLoop, agentLoopContinue } from "@f5-sales-demo/pi-agent";

const context: AgentContext = {
 systemPrompt: "You are helpful.",
 messages: [],
 tools: [],
};

const config: AgentLoopConfig = {
 model: getModel("openai", "gpt-4o"),
 convertToLlm: (msgs) => msgs.filter((m) => ["user", "assistant", "toolResult"].includes(m.role)),
};

const userMessage = { role: "user", content: "Hello", timestamp: Date.now() };

for await (const event of agentLoop([userMessage], context, config)) {
 console.log(event.type);
}

// Continue from existing context
for await (const event of agentLoopContinue(context, config)) {
 console.log(event.type);
}
```

## License

MIT

---
_Source: https://npm.io/package/@f5-sales-demo/pi-agent-core · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
