|
1 | 1 | # @agentic-kit/agent |
2 | 2 |
|
3 | | -Minimal stateful agent runtime for `agentic-kit`. |
| 3 | +<p align="center" width="100%"> |
| 4 | + <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" /> |
| 5 | +</p> |
4 | 6 |
|
5 | | -This package provides: |
| 7 | +<p align="center" width="100%"> |
| 8 | + <a href="https://github.com/constructive-io/agentic-kit/actions/workflows/run-tests.yaml"> |
| 9 | + <img height="20" src="https://github.com/constructive-io/agentic-kit/actions/workflows/run-tests.yaml/badge.svg" /> |
| 10 | + </a> |
| 11 | + <a href="https://github.com/constructive-io/agentic-kit/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a> |
| 12 | + <a href="https://www.npmjs.com/package/@agentic-kit/agent"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/agentic-kit?filename=packages%2Fagent%2Fpackage.json"/></a> |
| 13 | +</p> |
6 | 14 |
|
7 | | -- sequential tool execution |
8 | | -- lifecycle events for UI and orchestration |
9 | | -- abort and continue semantics |
10 | | -- pluggable context transforms |
| 15 | +Minimal stateful agent runtime built on `agentic-kit`. The `Agent` class drives |
| 16 | +a sequential model/tool loop, emits structured lifecycle events, and exposes a |
| 17 | +run handle that can be consumed as async events, a `ReadableStream`, or an SSE |
| 18 | +`Response` for transport to a frontend. |
11 | 19 |
|
12 | | -It is intentionally minimal in v1 and sits on top of the lower-level |
13 | | -`agentic-kit` provider portability layer. |
| 20 | +## Installation |
| 21 | + |
| 22 | +```bash |
| 23 | +npm install @agentic-kit/agent agentic-kit |
| 24 | +``` |
| 25 | + |
| 26 | +## Quick Start |
| 27 | + |
| 28 | +```ts |
| 29 | +import { Agent } from '@agentic-kit/agent'; |
| 30 | +import { getModel } from 'agentic-kit'; |
| 31 | + |
| 32 | +const agent = new Agent({ |
| 33 | + initialState: { |
| 34 | + model: getModel('openai', 'gpt-5.4-mini')!, |
| 35 | + systemPrompt: 'You are a helpful assistant.', |
| 36 | + tools: [], |
| 37 | + }, |
| 38 | +}); |
| 39 | + |
| 40 | +await agent.prompt('What is 2 + 2?'); |
| 41 | +console.log(agent.state.messages); |
| 42 | +``` |
| 43 | + |
| 44 | +## Streaming a Run |
| 45 | + |
| 46 | +The `prompt()` and `continue()` methods return an `AgentRunHandle`. Awaiting it |
| 47 | +runs to completion; treating it as a stream yields lifecycle events. |
| 48 | + |
| 49 | +```ts |
| 50 | +const handle = agent.prompt('Plan a trip to Lisbon.'); |
| 51 | + |
| 52 | +for await (const event of handle.events()) { |
| 53 | + if (event.type === 'message_update') { |
| 54 | + process.stdout.write(JSON.stringify(event.assistantMessageEvent)); |
| 55 | + } |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +A handle can be consumed exactly once, in one of these ways: |
| 60 | + |
| 61 | +- `await handle` — run to completion without observing events. |
| 62 | +- `handle.events()` — iterate `AgentEvent`s. |
| 63 | +- `handle.toReadableStream()` — wrap events in a `ReadableStream<AgentEvent>`. |
| 64 | +- `handle.toResponse(init?)` — wrap events as an SSE `Response`, ready to |
| 65 | + return from a Next.js / Hono / Express handler. |
| 66 | + |
| 67 | +## SSE Transport |
| 68 | + |
| 69 | +`toResponse()` serializes events as `data: <json>\n\n` frames. On the client, |
| 70 | +parse them back into `AgentEvent`s with `parseSSEStream`: |
| 71 | + |
| 72 | +```ts |
| 73 | +import { parseSSEStream } from '@agentic-kit/agent'; |
| 74 | + |
| 75 | +const response = await fetch('/api/chat', { method: 'POST', body }); |
| 76 | +for await (const event of parseSSEStream(response.body!)) { |
| 77 | + // event is a typed AgentEvent |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +## Tools, Decisions, and Pauses |
| 82 | + |
| 83 | +Tools extend the base `ToolDefinition` from `agentic-kit` with an executor and |
| 84 | +an optional human-in-the-loop `decision` schema. When a tool with a `decision` |
| 85 | +schema is called and no decision is attached, the agent emits a |
| 86 | +`tool_decision_pending` event and pauses. Attach the decision to the matching |
| 87 | +`toolCall` block and call `continue()` to resume. |
| 88 | + |
| 89 | +```ts |
| 90 | +const sendEmail: AgentTool = { |
| 91 | + name: 'send_email', |
| 92 | + label: 'Send email', |
| 93 | + description: 'Send an email to a recipient.', |
| 94 | + parameters: { |
| 95 | + type: 'object', |
| 96 | + properties: { to: { type: 'string' }, body: { type: 'string' } }, |
| 97 | + required: ['to', 'body'], |
| 98 | + }, |
| 99 | + decision: { |
| 100 | + type: 'object', |
| 101 | + properties: { approved: { type: 'boolean' } }, |
| 102 | + required: ['approved'], |
| 103 | + }, |
| 104 | + execute: async (toolCallId, args, decision) => { |
| 105 | + if (!(decision as { approved: boolean }).approved) { |
| 106 | + return { content: [{ type: 'text', text: 'Cancelled by user.' }] }; |
| 107 | + } |
| 108 | + // ... send email |
| 109 | + return { content: [{ type: 'text', text: 'Sent.' }] }; |
| 110 | + }, |
| 111 | +}; |
| 112 | +``` |
| 113 | + |
| 114 | +## Agent API |
| 115 | + |
| 116 | +```ts |
| 117 | +new Agent(options: AgentOptions) |
| 118 | +``` |
| 119 | + |
| 120 | +`AgentOptions`: |
| 121 | + |
| 122 | +- `initialState` — must include a `model`; `systemPrompt`, `tools`, and |
| 123 | + `messages` are optional. |
| 124 | +- `maxSteps` — cap on model invocations per run. Resets in `prompt()`, |
| 125 | + persists across `continue()`. |
| 126 | +- `streamFn` — override the underlying stream function (defaults to |
| 127 | + `stream` from `agentic-kit`). |
| 128 | +- `transformContext(messages, signal)` — async hook to rewrite the message |
| 129 | + list before each model call (compaction, summarization, retrieval). |
| 130 | +- `validateToolArguments(schema, args)` — override tool argument validation. |
| 131 | + Default uses a built-in JSON Schema subset. |
| 132 | + |
| 133 | +State mutation: |
| 134 | + |
| 135 | +- `setModel`, `setSystemPrompt`, `setTools`, `setStreamOptions` |
| 136 | +- `replaceMessages`, `appendMessage`, `clearMessages`, `reset` |
| 137 | + |
| 138 | +Execution: |
| 139 | + |
| 140 | +- `prompt(input, opts?)` — start a new run from a user message. |
| 141 | +- `continue(opts?)` — resume after a paused decision or after the messages |
| 142 | + array was edited externally. |
| 143 | +- `abort()` — cancel the active run. |
| 144 | +- `waitForIdle()` — resolves when the current run finishes. |
| 145 | +- `subscribe(listener)` — receive `AgentEvent`s without consuming the handle. |
| 146 | + |
| 147 | +## Event Types |
| 148 | + |
| 149 | +`AgentEvent` is a discriminated union covering the full lifecycle: |
| 150 | + |
| 151 | +- `agent_start`, `agent_end` (with `stopReason: 'completed' | 'max_steps'`) |
| 152 | +- `turn_start`, `turn_end` |
| 153 | +- `message_start`, `message_update`, `message_end` |
| 154 | +- `tool_execution_start`, `tool_execution_update`, `tool_execution_end` |
| 155 | +- `tool_decision_pending` (carries `input` and `schema`) |
| 156 | + |
| 157 | +Every `message_update` includes the underlying `assistantMessageEvent` from |
| 158 | +the provider stream (text/thinking/toolcall deltas), so consumers can render |
| 159 | +streaming text without re-deriving it from the partial message. |
0 commit comments