@rowan-agent/agent 0.7.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -759
- package/dist/index.d.ts +1302 -1283
- package/dist/index.js +4736 -5982
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,793 +1,86 @@
|
|
|
1
1
|
# @rowan-agent/agent
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Durable Agent Runtime. The public API consists of `AgentRuntime`, Durable
|
|
4
|
+
Stores, Config Providers, Run handles, and Durable Run Events. An Agent is a
|
|
5
|
+
persistent identity, not a process-local Session object.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
bun add @rowan-agent/agent
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
The durable lifecycle is the public entrypoint: start one `AgentRuntime`, create
|
|
16
|
-
or reconstruct an `Agent`, submit input with `send()`, and wait on the returned
|
|
17
|
-
`AgentRun`.
|
|
7
|
+
## Quick start
|
|
18
8
|
|
|
19
9
|
```ts
|
|
20
10
|
import {
|
|
21
11
|
AgentRuntime,
|
|
22
|
-
InMemoryRuntimeStateStore,
|
|
23
|
-
InMemorySessionStore,
|
|
24
12
|
createCoreTools,
|
|
13
|
+
InMemoryStore,
|
|
25
14
|
} from "@rowan-agent/agent";
|
|
26
15
|
|
|
27
|
-
const runtime = await AgentRuntime.
|
|
28
|
-
|
|
29
|
-
sessionProvider: new InMemorySessionStore(),
|
|
16
|
+
const runtime = await AgentRuntime.init({
|
|
17
|
+
store: new InMemoryStore(),
|
|
30
18
|
});
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
systemPrompt: "You are a helpful coding assistant.",
|
|
36
|
-
messages: [],
|
|
37
|
-
tools: createCoreTools({ root: process.cwd() }),
|
|
38
|
-
skills: [],
|
|
39
|
-
phases: {
|
|
40
|
-
phases: new Map(),
|
|
41
|
-
entryPhaseId: default,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
model: {
|
|
45
|
-
provider: "openai",
|
|
46
|
-
id: "gpt-4.1-mini",
|
|
47
|
-
protocol: "openai-completions",
|
|
48
|
-
baseUrl: "https://api.openai.com/v1",
|
|
49
|
-
apiKey: process.env.OPENAI_API_KEY!,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
agent.subscribe((event) => console.log(event.type));
|
|
54
|
-
const run = await agent.send("list the files in this project");
|
|
55
|
-
console.log((await run.result()).message);
|
|
56
|
-
} finally {
|
|
57
|
-
await runtime.stop();
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Use `runtime.reconstructAgent(agentId, currentOptions)` to bind an existing
|
|
62
|
-
durable Agent to its Session with current resources. `send()` is non-blocking;
|
|
63
|
-
`AgentRun.result()` waits for its durable terminal Outcome.
|
|
64
|
-
|
|
65
|
-
The example uses in-memory adapters. Use `SqliteRuntimeStateStore` and a
|
|
66
|
-
persistent Session provider when state must survive a process restart.
|
|
67
|
-
|
|
68
|
-
## AgentRuntime
|
|
69
|
-
|
|
70
|
-
Exactly one `AgentRuntime` may be active in a process. It is the sole owner of
|
|
71
|
-
Agent creation and reconstruction, durable input, scheduling, leases, Runtime
|
|
72
|
-
Events, and Tool Call control. Always stop it during host shutdown.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
type RuntimeEventConsumer = {
|
|
76
|
-
caughtUp: Promise<void>;
|
|
77
|
-
stop(): void;
|
|
78
|
-
};
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
type AgentRuntimeOptions = {
|
|
83
|
-
stateStore: InMemoryRuntimeStateStore | SqliteRuntimeStateStore;
|
|
84
|
-
sessionProvider?: InMemorySessionStore | JsonlSessionStore;
|
|
85
|
-
toolPolicy?: ToolRuntimePolicy;
|
|
86
|
-
maxConcurrentRuns?: number;
|
|
87
|
-
maxInfrastructureAttempts?: number;
|
|
88
|
-
leaseDurationMs?: number;
|
|
89
|
-
leaseRenewalIntervalMs?: number;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
class AgentRuntime {
|
|
93
|
-
static start(options: AgentRuntimeOptions): Promise<AgentRuntime>;
|
|
94
|
-
createAgent(options: AgentOptions): Promise<Agent>;
|
|
95
|
-
reconstructAgent(agentId: AgentId, options: AgentOptions): Promise<Agent>;
|
|
96
|
-
pauseAgent(agentId: AgentId): Promise<void>;
|
|
97
|
-
resumeAgent(agentId: AgentId): Promise<void>;
|
|
98
|
-
getMessage(messageId: RuntimeMessageId): Promise<RuntimeMessage | undefined>;
|
|
99
|
-
getToolCall(toolCallId: RuntimeToolCallId): Promise<RuntimeToolCall | undefined>;
|
|
100
|
-
getRun(runId: AgentRunId): Promise<AgentRunRecord | undefined>;
|
|
101
|
-
listRuns(input?: { agentId?: AgentId; states?: AgentRunRecord["state"][] }): Promise<AgentRunRecord[]>;
|
|
102
|
-
listActiveRuns(): Promise<AgentRunRecord[]>;
|
|
103
|
-
abortRun(runId: AgentRunId, reason?: string): Promise<void>;
|
|
104
|
-
consumeEvents(
|
|
105
|
-
consumerId: string,
|
|
106
|
-
listener: RuntimeEventListener,
|
|
107
|
-
): RuntimeEventConsumer;
|
|
108
|
-
listEvents(cursor?: RuntimeEventCursor): Promise<RuntimeEvent[]>;
|
|
109
|
-
stop(): Promise<void>;
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
A Session provider is required by `createAgent()` and `reconstructAgent()`.
|
|
114
|
-
Runtime State and conversation history are deliberately separate:
|
|
115
|
-
|
|
116
|
-
| Concern | Durable adapter | In-memory adapter |
|
|
117
|
-
|---------|-----------------|-------------------|
|
|
118
|
-
| Agent records, Messages, Runs, Leases, Runtime Events, Tool Calls | `SqliteRuntimeStateStore` | `InMemoryRuntimeStateStore` |
|
|
119
|
-
| Conversation messages, model transcripts, Outcomes | `JsonlSessionStore` | `InMemorySessionStore` |
|
|
120
|
-
|
|
121
|
-
The SQLite Runtime schema has no compatibility migration. Replace an older
|
|
122
|
-
Runtime database when adopting a breaking schema; Session JSONL records remain
|
|
123
|
-
separate.
|
|
124
|
-
|
|
125
|
-
### Scheduling and Runtime Commands
|
|
126
|
-
|
|
127
|
-
The Scheduler runs at most one Run for each Agent and up to
|
|
128
|
-
`maxConcurrentRuns` across different Agents. `pauseAgent()` gates queued and new
|
|
129
|
-
work without cancelling a Run that is already executing; `resumeAgent()` opens
|
|
130
|
-
that gate. `abortRun()` targets one precise Run.
|
|
131
|
-
|
|
132
|
-
Lease failures and model/provider errors marked `retryable: true` are retried.
|
|
133
|
-
The Runtime renews active leases and retries them up to
|
|
134
|
-
`maxInfrastructureAttempts`; exhausted work fails and its triggering Message is
|
|
135
|
-
dead-lettered.
|
|
136
|
-
|
|
137
|
-
### Process Recovery
|
|
138
|
-
|
|
139
|
-
Runtime startup and periodic recovery return expired Leases to durable queued
|
|
140
|
-
work without disturbing unexpired Leases owned by another process. The host
|
|
141
|
-
supplies its current executable resources when it reconstructs an Agent:
|
|
142
|
-
|
|
143
|
-
```ts
|
|
144
|
-
const runtime = await AgentRuntime.start({ stateStore, sessionProvider });
|
|
145
|
-
const agent = await runtime.reconstructAgent(agentId, currentAgentOptions);
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Reconstruction preserves the Agent ID and Session ID. Establishing the Binding
|
|
149
|
-
automatically schedules queued Runs. A suspended Agent may remain unbound until
|
|
150
|
-
the host has new input, then reconstruct before calling `send()`.
|
|
151
|
-
|
|
152
|
-
## Agent
|
|
153
|
-
|
|
154
|
-
`AgentRuntime` is the only lifecycle owner. `Agent` is a bound facade: it cannot
|
|
155
|
-
be directly constructed and has no independent `run()` path.
|
|
156
|
-
|
|
157
|
-
The `Agent` class is the public facade for one Runtime-owned Agent Binding.
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
class Agent {
|
|
161
|
-
readonly id: AgentId;
|
|
162
|
-
readonly sessionId: string;
|
|
163
|
-
send(input: string | AgentMessage): Promise<AgentRun>;
|
|
164
|
-
subscribe(listener: AgentEventListener): () => void;
|
|
165
|
-
flushEvents(): Promise<void>;
|
|
166
|
-
|
|
167
|
-
// Resource discovery helpers
|
|
168
|
-
static loadSkills(targetPath: string): Promise<Skill[]>;
|
|
169
|
-
static loadPhases(targetPath: string): Promise<PhaseRegistry>;
|
|
170
|
-
static loadExtensions(targetPath: string): Promise<LoadExtensionsResult>;
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### AgentOptions
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
type ModelConfig = ModelRef & {
|
|
178
|
-
protocol: Protocol;
|
|
179
|
-
baseUrl: string;
|
|
180
|
-
apiKey: string;
|
|
181
|
-
headers?: Record<string, string>;
|
|
182
|
-
timeoutMs?: number;
|
|
183
|
-
maxRetries?: number;
|
|
184
|
-
retryDelayMs?: number;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
type AgentCommonOptions = {
|
|
188
|
-
context: AgentContext;
|
|
189
|
-
cwd?: string;
|
|
190
|
-
extensions?: LoadedExtension[];
|
|
191
|
-
maxAttempts?: number;
|
|
192
|
-
|
|
193
|
-
// Lifecycle hooks
|
|
194
|
-
beforeToolCall?: BeforeToolCall;
|
|
195
|
-
afterToolCall?: AfterToolCall;
|
|
196
|
-
onModelTranscript?: (transcript: ModelTranscript, meta: { phase: string; model: ModelRef }) => Promise<void>;
|
|
197
|
-
onMessage?: (message: AgentMessage) => Promise<void>;
|
|
198
|
-
onOutcome?: (outcome: Outcome) => Promise<void>;
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
type AgentOptions = AgentCommonOptions & (
|
|
202
|
-
| { model: ModelConfig; stream?: never }
|
|
203
|
-
| { model: ModelRef; stream: StreamFn }
|
|
204
|
-
);
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Conversation Continuation
|
|
208
|
-
|
|
209
|
-
Every turn enters through `send()`. The Runtime persists the input before it
|
|
210
|
-
returns an `AgentRun`; Session history is restored during reconstruction.
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
const first = await agent.send("summarize this repository");
|
|
214
|
-
console.log((await first.result()).message);
|
|
215
|
-
|
|
216
|
-
const second = await agent.send("now focus on the CLI package");
|
|
217
|
-
console.log((await second.result()).message);
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
If a Run is suspended waiting for input, the next `send()` to that Agent resumes
|
|
221
|
-
the same Run instead of creating a second one.
|
|
222
|
-
|
|
223
|
-
Suspended Runs persist the current input request in `AgentRunRecord.inputRequest`
|
|
224
|
-
so a reconstructed Runtime can show the question without replaying a transient
|
|
225
|
-
Agent Event. The request contains its phase, prompt, and timestamp; it is cleared
|
|
226
|
-
when the Run resumes.
|
|
227
|
-
|
|
228
|
-
### Updating Runtime Resources
|
|
229
|
-
|
|
230
|
-
Resources are fixed for a live Agent Binding. Apply a new model, prompt, Tool
|
|
231
|
-
set, Skill set, Phase registry, or Extension set during reconstruction. A
|
|
232
|
-
duplicate live Binding is rejected, so explicit reconstruction normally occurs
|
|
233
|
-
after the previous process or Runtime has stopped.
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
const agentId = agent.id;
|
|
237
|
-
await runtime.stop();
|
|
238
|
-
|
|
239
|
-
const nextRuntime = await AgentRuntime.start({ stateStore, sessionProvider });
|
|
240
|
-
const reconstructed = await nextRuntime.reconstructAgent(agentId, {
|
|
241
|
-
context: currentContext,
|
|
242
|
-
model: currentModelConfig,
|
|
243
|
-
});
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
## AgentRun
|
|
247
|
-
|
|
248
|
-
`send()` returns a Runtime-owned handle immediately after the input and Run are
|
|
249
|
-
durable. The handle exposes cached state for synchronous inspection and can
|
|
250
|
-
refresh from the Runtime Store when needed.
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
type AgentInputRequest = {
|
|
254
|
-
phase: string;
|
|
255
|
-
prompt: string;
|
|
256
|
-
requestedAt: string;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
class AgentRun {
|
|
260
|
-
readonly id: AgentRunId;
|
|
261
|
-
readonly messageId: string;
|
|
262
|
-
readonly status: AgentRunState;
|
|
263
|
-
readonly state: AgentRunState; // alias of status
|
|
264
|
-
readonly inputRequest?: AgentInputRequest;
|
|
265
|
-
|
|
266
|
-
getStatus(): Promise<AgentRunState>;
|
|
267
|
-
subscribe(listener: AgentRunListener): () => void;
|
|
268
|
-
consumeRuntimeEvents(
|
|
269
|
-
consumerId: string,
|
|
270
|
-
listener: (event: RuntimeEvent) => void | Promise<void>,
|
|
271
|
-
): RuntimeEventConsumer;
|
|
272
|
-
result(): Promise<Outcome>;
|
|
273
|
-
abort(reason?: string): Promise<void>;
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
`result()` waits through queued, running, and suspended states. Completed,
|
|
278
|
-
failed, and cancelled Runs all resolve to their persisted terminal `Outcome`;
|
|
279
|
-
inspect `status` when the distinction matters. `abort()` affects only this Run.
|
|
280
|
-
|
|
281
|
-
## AgentContext
|
|
282
|
-
|
|
283
|
-
The context snapshot that defines what the agent can see and do — the system prompt sets the role, messages form the conversation history, and tools/skills define the capability boundary.
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
type AgentContext = {
|
|
287
|
-
systemPrompt: string;
|
|
288
|
-
messages: AgentMessage[];
|
|
289
|
-
tools: Tool[];
|
|
290
|
-
skills: Skill[];
|
|
291
|
-
// Optional custom phases; Agent merges them with its built-in "default" phase.
|
|
292
|
-
phases?: PhaseRegistry;
|
|
293
|
-
};
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### AgentMessage
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
type AgentMessage = {
|
|
300
|
-
id: string;
|
|
301
|
-
role: "system" | "user" | "assistant" | "tool";
|
|
302
|
-
content: string | LlmContentPart[];
|
|
303
|
-
createdAt: string;
|
|
304
|
-
metadata?: Record<string, unknown> & { phase?: string };
|
|
305
|
-
};
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### Outcome
|
|
309
|
-
|
|
310
|
-
The terminal result produced when the loop completes — carries the final message and all tool call results from the run.
|
|
311
|
-
|
|
312
|
-
```ts
|
|
313
|
-
type Outcome = {
|
|
314
|
-
id: string;
|
|
315
|
-
message: string;
|
|
316
|
-
payload?: unknown;
|
|
317
|
-
toolResults?: Array<{
|
|
318
|
-
toolCallId: string;
|
|
319
|
-
toolName: string;
|
|
320
|
-
ok: boolean;
|
|
321
|
-
content: unknown;
|
|
322
|
-
error?: string;
|
|
323
|
-
}>;
|
|
324
|
-
};
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
## Tools
|
|
328
|
-
|
|
329
|
-
Four built-in tools cover file read/write and shell execution — the minimum needed for code-related agent work.
|
|
330
|
-
|
|
331
|
-
```ts
|
|
332
|
-
import { createCoreTools } from "@rowan-agent/agent";
|
|
333
|
-
|
|
334
|
-
const tools = createCoreTools({
|
|
335
|
-
root: process.cwd(),
|
|
336
|
-
maxReadBytes?, // default: 64KB
|
|
337
|
-
bashTimeoutMs?, // default: 30s
|
|
338
|
-
maxBashOutputBytes?, // default: 64KB
|
|
339
|
-
});
|
|
340
|
-
// Returns: read, bash, edit, write
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### Built-in Tools
|
|
344
|
-
|
|
345
|
-
| Tool | Description | Parameters |
|
|
346
|
-
|------|-------------|------------|
|
|
347
|
-
| `read` | Reads a file, optionally by line range | `path` (required), `offset?`, `limit?` |
|
|
348
|
-
| `bash` | Runs a bash command in the workspace | `command` (required), `timeout?` (seconds) |
|
|
349
|
-
| `edit` | Applies exact replacements, including multiple disjoint edits in one call | `path` (required), `edits[]` (each with `oldText` and `newText`) |
|
|
350
|
-
| `write` | Creates or overwrites a file | `path` (required), `content` (required) |
|
|
351
|
-
|
|
352
|
-
### Custom Tools
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
import type { Tool, ToolResult } from "@rowan-agent/agent";
|
|
356
|
-
|
|
357
|
-
const myTool: Tool = {
|
|
358
|
-
name: "search",
|
|
359
|
-
description: "Search project docs",
|
|
360
|
-
parameters: Type.Object({ query: Type.String() }),
|
|
361
|
-
executionMode: "parallel", // "parallel" | "sequential"
|
|
362
|
-
async execute(args, context, signal): Promise<ToolResult> {
|
|
363
|
-
return { toolCallId: context.toolCallId, toolName: "search", ok: true, content: "..." };
|
|
364
|
-
},
|
|
365
|
-
};
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### Tool Execution Hooks
|
|
369
|
-
|
|
370
|
-
`beforeToolCall` can intercept or reject tool calls (e.g. for approval flows); `afterToolCall` can modify results before they reach the model.
|
|
371
|
-
|
|
372
|
-
```ts
|
|
373
|
-
const agent = await runtime.createAgent({
|
|
374
|
-
context,
|
|
375
|
-
model,
|
|
20
|
+
const agentId = await runtime.createAgent({
|
|
21
|
+
identity: "example:v1", // Stable config snapshot identity, not the Agent ID
|
|
22
|
+
model: { provider: "openai", id: "gpt-4o" },
|
|
376
23
|
stream,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return result;
|
|
382
|
-
},
|
|
383
|
-
});
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### Runtime Tool Policy
|
|
387
|
-
|
|
388
|
-
Every managed Tool Call passes through the Runtime before its adapter executes.
|
|
389
|
-
Runtime policy can narrow the Agent's Tool set and cap concurrency, but it can
|
|
390
|
-
never add a capability that was not supplied in `AgentContext`.
|
|
391
|
-
|
|
392
|
-
```ts
|
|
393
|
-
const runtime = await AgentRuntime.start({
|
|
394
|
-
stateStore,
|
|
395
|
-
sessionProvider,
|
|
396
|
-
toolPolicy: {
|
|
397
|
-
allowedTools: ["read", "bash", "task_manage", "resource_manage"],
|
|
398
|
-
maxConcurrent: 8,
|
|
399
|
-
perToolMaxConcurrent: { bash: 2 },
|
|
24
|
+
context: {
|
|
25
|
+
systemPrompt: "You are helpful.",
|
|
26
|
+
tools: createCoreTools({ root: process.cwd() }),
|
|
27
|
+
skills: [],
|
|
400
28
|
},
|
|
401
29
|
});
|
|
402
|
-
```
|
|
403
30
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
13 event types are emitted during execution — useful for logging, UI updates, or external monitoring.
|
|
407
|
-
|
|
408
|
-
```ts
|
|
409
|
-
agent.subscribe((event: AgentEvent) => {
|
|
410
|
-
switch (event.type) {
|
|
411
|
-
case "agent_start": // { sessionId }
|
|
412
|
-
case "agent_end": // { sessionId, messages }
|
|
413
|
-
case "turn_start": // { content }
|
|
414
|
-
case "turn_end": // { content, outcome? }
|
|
415
|
-
case "model_requested": // { model, usage }
|
|
416
|
-
case "phase_start": // { phase }
|
|
417
|
-
case "phase_end": // { phase }
|
|
418
|
-
case "message_start": // { message }
|
|
419
|
-
case "message_update": // { message, delta }
|
|
420
|
-
case "message_end": // { message }
|
|
421
|
-
case "tool_execution_start": // { toolCallId, toolName, args }
|
|
422
|
-
case "tool_execution_update": // { toolCallId, toolName, args, partialResult }
|
|
423
|
-
case "tool_execution_end": // { toolCallId, toolName, result, isError }
|
|
424
|
-
}
|
|
31
|
+
const run = await runtime.start(agentId, "Summarize the workspace.", {
|
|
32
|
+
idempotencyKey: "run-example", // One Agent can have multiple independent Runs
|
|
425
33
|
});
|
|
34
|
+
const boundary = await run.wait();
|
|
35
|
+
await runtime.close();
|
|
426
36
|
```
|
|
427
37
|
|
|
428
|
-
|
|
38
|
+
## Public lifecycle
|
|
429
39
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
40
|
+
1. `AgentRuntime.init({ store })` opens a Runtime Owner with an in-memory Config Provider by default. Pass `configs` when configuration must survive process boundaries.
|
|
41
|
+
2. `createAgent()` creates a persistent Agent identity and binds a configuration snapshot.
|
|
42
|
+
3. `start()` creates a queued Run; `run(runId)` returns a stateless Run handle.
|
|
43
|
+
4. `observe()` reads replayable `DurableRunEvent` values; `wait()` waits for a boundary.
|
|
44
|
+
5. `respond()` continues an `input_required` Run; `cancel()` terminates an unfinished Run.
|
|
45
|
+
6. `close()` seals the Owner and releases the Store.
|
|
434
46
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
439
|
-
await consumer.caughtUp;
|
|
440
|
-
```
|
|
47
|
+
`AgentRuntime` does not expose process-local Agents, Sessions, Bindings,
|
|
48
|
+
Mailboxes, or compatibility factories. The Durable Store is the source of truth;
|
|
49
|
+
Run handles do not hold business state.
|
|
441
50
|
|
|
442
|
-
|
|
443
|
-
consumer can use a `run_enqueued` Event as an outbox signal, read immutable
|
|
444
|
-
business correlation metadata from its Message, and update an external index
|
|
445
|
-
before its Checkpoint advances:
|
|
51
|
+
## Stores
|
|
446
52
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const message = await runtime.getMessage(event.messageId);
|
|
451
|
-
if (!message) throw new Error(`Runtime Message not found: ${event.messageId}`);
|
|
452
|
-
await upsertRunIndex(event.runId, message.input.metadata);
|
|
453
|
-
});
|
|
454
|
-
await indexConsumer.caughtUp;
|
|
455
|
-
```
|
|
53
|
+
- `InMemoryStore`: tests and single-process embedding.
|
|
54
|
+
- `SqliteStore`: local persistence; the database is initialized on the first `openOwner()`.
|
|
55
|
+
- `InMemoryConfigProvider`: tests and embeddings without an external config service.
|
|
456
56
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
57
|
+
The Runtime generates an idempotency key for ordinary Agent creation. Callers
|
|
58
|
+
that need to retry the same creation after an unknown result pass a stable
|
|
59
|
+
`idempotencyKey` explicitly. Other write commands retain their documented
|
|
60
|
+
idempotency identities. The Store provides atomicity for Runs, events, Tool
|
|
61
|
+
lifecycles, and Owner fencing.
|
|
460
62
|
|
|
461
|
-
Tool
|
|
462
|
-
especially useful when a `tool_call_indeterminate` Event requires host recovery
|
|
463
|
-
or human review:
|
|
63
|
+
## Tool lifecycle
|
|
464
64
|
|
|
465
|
-
|
|
466
|
-
if (event.kind === "tool_call_indeterminate" && event.toolCallId) {
|
|
467
|
-
const toolCall = await runtime.getToolCall(event.toolCallId);
|
|
468
|
-
await reviewIndeterminateToolCall(toolCall);
|
|
469
|
-
}
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
A consumer may instead return an `enqueue` disposition to turn the current
|
|
473
|
-
Event into Agent Input. Rowan enqueues the input and advances the Consumer
|
|
474
|
-
Checkpoint in one Runtime State transaction, then schedules the target Agent.
|
|
475
|
-
|
|
476
|
-
```ts
|
|
477
|
-
const routingConsumer = runtime.consumeEvents("delegated-results", (event) => {
|
|
478
|
-
if (event.kind !== "run_completed" || !event.agentId) return;
|
|
479
|
-
return {
|
|
480
|
-
type: "enqueue",
|
|
481
|
-
agentId: targetAgentId,
|
|
482
|
-
input: createMessage("user", JSON.stringify(event.payload), {
|
|
483
|
-
sourceEventId: event.id,
|
|
484
|
-
}),
|
|
485
|
-
};
|
|
486
|
-
});
|
|
487
|
-
await routingConsumer.caughtUp;
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
Only one live subscription may use a Consumer ID. If delivery fails, its
|
|
491
|
-
Checkpoint stays put and the Event is delivered again when that Consumer is
|
|
492
|
-
started later. `runtime.listEvents()` inspects the durable stream without
|
|
493
|
-
advancing a Consumer Checkpoint. Use `run.consumeRuntimeEvents()` for the same
|
|
494
|
-
delivery contract filtered to one Run.
|
|
65
|
+
Tools are supplied through `AgentConfig.context.tools` and persist through:
|
|
495
66
|
|
|
496
|
-
`
|
|
497
|
-
is persisted with the Run and echoed on `run_enqueued`, suspension, completion,
|
|
498
|
-
and abort Event payloads. `listActiveRuns()` returns queued, running, and
|
|
499
|
-
suspended Runs for host-side Agent reconstruction with current `AgentOptions`.
|
|
500
|
-
Use the returned `consumer.caughtUp` Promise when startup must wait until all
|
|
501
|
-
Events through the durable Consumer checkpoint have been delivered before
|
|
502
|
-
recovery continues. Call `consumer.stop()` during shutdown. `run.consumeRuntimeEvents()`
|
|
503
|
-
returns the same handle shape.
|
|
67
|
+
`pending → running → completed | failed | indeterminate`
|
|
504
68
|
|
|
505
|
-
|
|
69
|
+
When an external side effect cannot be confirmed, the Tool must become
|
|
70
|
+
`indeterminate`; the Run then fails and is never automatically retried.
|
|
506
71
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
## Session
|
|
510
|
-
|
|
511
|
-
JSONL-based session persistence — lets multi-turn conversations survive across process restarts. Supports create, resume, branch, and history replay.
|
|
512
|
-
|
|
513
|
-
```ts
|
|
514
|
-
import { JsonlSessionStore } from "@rowan-agent/agent";
|
|
515
|
-
|
|
516
|
-
const sessions = new JsonlSessionStore(sessionsDir);
|
|
517
|
-
const session = await sessions.create({
|
|
518
|
-
systemPrompt,
|
|
519
|
-
input: "",
|
|
520
|
-
skills: [],
|
|
521
|
-
});
|
|
522
|
-
const resumed = await sessions.open(sessionId);
|
|
523
|
-
const savedSessions = await sessions.list();
|
|
524
|
-
|
|
525
|
-
await session.appendMessage(message);
|
|
526
|
-
await session.appendOutcome(outcome);
|
|
527
|
-
const context = await session.buildAgentContext({ tools });
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
## Skills
|
|
531
|
-
|
|
532
|
-
Skills are `SKILL.md` knowledge bundles that get injected into the agent context, extending its domain knowledge without changing code.
|
|
533
|
-
|
|
534
|
-
```ts
|
|
535
|
-
import { Agent } from "@rowan-agent/agent";
|
|
536
|
-
|
|
537
|
-
const skills = await Agent.loadSkills("/User/Skills");
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
## Phases
|
|
541
|
-
|
|
542
|
-
Phases are the basic units of the execution loop. There are no built-in phases — when none are configured, a `"default"` phase lets the LLM drive execution and routing directly.
|
|
543
|
-
|
|
544
|
-
### How It Works
|
|
545
|
-
|
|
546
|
-
Each phase's `PHASE.md` content is injected as a system message, giving the LLM phase-specific instructions. A `route` tool is automatically added — the LLM calls it to decide what happens next: continue, stop, or transition to another phase.
|
|
547
|
-
|
|
548
|
-
```
|
|
549
|
-
Per iteration:
|
|
550
|
-
1. Read Agent-normalized `context.phases`
|
|
551
|
-
2. Inject phase instructions as system message
|
|
552
|
-
3. Execute phase (factory | run | LLM fallback)
|
|
553
|
-
4. Extract routing decision from route tool call
|
|
554
|
-
5. Transition, continue, or stop
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
**`entryPhaseId`** specifies which phase the loop enters for a newly bound Agent. When phases are loaded from `.rowan/phases/`, the first discovered phase becomes the entry. When none are configured, the Agent normalises to `"default"`. Later turns start from the normalized default phase. This field is an internal routing hint and is not exposed to the model.
|
|
558
|
-
|
|
559
|
-
### Example Phase Flow
|
|
560
|
-
|
|
561
|
-
```
|
|
562
|
-
┌────────────┐
|
|
563
|
-
│ User Input │
|
|
564
|
-
└─────┬──────┘
|
|
565
|
-
▼
|
|
566
|
-
┌────────────┐ route("plan") ┌────────────┐
|
|
567
|
-
│ default │ ────────────────▶│ plan │
|
|
568
|
-
└────────────┘ └─────┬──────┘
|
|
569
|
-
│
|
|
570
|
-
route("execute")
|
|
571
|
-
│
|
|
572
|
-
▼
|
|
573
|
-
┌────────────┐
|
|
574
|
-
│ execute │◀──────────────┐
|
|
575
|
-
└─────┬──────┘ │
|
|
576
|
-
│ │
|
|
577
|
-
route("review") route("execute")
|
|
578
|
-
│ (loop: fix issues)
|
|
579
|
-
▼
|
|
580
|
-
┌────────────┐
|
|
581
|
-
│ review │
|
|
582
|
-
└─────┬──────┘
|
|
583
|
-
│
|
|
584
|
-
route({ decision: [{ phase: "lint" }, { phase: "typecheck" }] })
|
|
585
|
-
│
|
|
586
|
-
┌─────────┴─────────┐
|
|
587
|
-
▼ ▼
|
|
588
|
-
┌──────────┐ ┌──────────┐
|
|
589
|
-
│ lint │ │typecheck │
|
|
590
|
-
└────┬─────┘ └────┬─────┘
|
|
591
|
-
└─────────┬─────────┘
|
|
592
|
-
▼
|
|
593
|
-
merged into <prev_phase_outputs>
|
|
594
|
-
│
|
|
595
|
-
route("stop")
|
|
596
|
-
│
|
|
597
|
-
▼
|
|
598
|
-
┌──────────┐
|
|
599
|
-
│ Outcome │
|
|
600
|
-
└──────────┘
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
Each arrow is an LLM routing decision via the `route` tool. Parallel branches run concurrently and merge back before the next transition.
|
|
604
|
-
|
|
605
|
-
### Providing Phases
|
|
606
|
-
|
|
607
|
-
Two sources, merged by priority:
|
|
608
|
-
|
|
609
|
-
**File-based** — `<workspace>/.rowan/phases/*/PHASE.md`
|
|
610
|
-
|
|
611
|
-
```
|
|
612
|
-
.rowan/phases/review/
|
|
613
|
-
├── PHASE.md # YAML frontmatter + markdown body
|
|
614
|
-
└── index.ts # optional: factory or run function
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
```yaml
|
|
618
|
-
---
|
|
619
|
-
name: review
|
|
620
|
-
description: Review code for correctness and style
|
|
621
|
-
tools: [read, bash]
|
|
622
|
-
target: execute
|
|
623
|
-
---
|
|
624
|
-
|
|
625
|
-
Review the current implementation for bugs and style issues.
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
**Extension-registered** — via `api.registerPhase()`. The phase name is used as its identity.
|
|
629
|
-
|
|
630
|
-
```ts
|
|
631
|
-
import type { ExtensionAPI } from "@rowan-agent/agent";
|
|
632
|
-
|
|
633
|
-
export default function myPlugin(api: ExtensionAPI) {
|
|
634
|
-
api.registerPhase({
|
|
635
|
-
name: "review",
|
|
636
|
-
description: "Review code for correctness",
|
|
637
|
-
tools: ["read", "bash"],
|
|
638
|
-
async run(context, execution) {
|
|
639
|
-
const result = await execution.invokeModel(context);
|
|
640
|
-
return { message: result.text, route: "stop" };
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
### Phase
|
|
647
|
-
|
|
648
|
-
```ts
|
|
649
|
-
interface Phase {
|
|
650
|
-
name: string; // unique identity and display name
|
|
651
|
-
description: string;
|
|
652
|
-
tools?: string[]; // restrict tools (undefined = all)
|
|
653
|
-
skills?: string[]; // restrict skills
|
|
654
|
-
target?: string; // forced next phase (overrides route tool)
|
|
655
|
-
isolated?: boolean; // empty context when run in parallel
|
|
656
|
-
content: string; // PHASE.md body
|
|
657
|
-
factory?: (api: ExtensionAPI) => Promise<void>;
|
|
658
|
-
run?: (context: PhaseContext, execution: PhaseExecution) => Promise<PhaseOutput | void>;
|
|
659
|
-
}
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
### PhaseContext / PhaseOutput
|
|
663
|
-
|
|
664
|
-
```ts
|
|
665
|
-
interface PhaseContext {
|
|
666
|
-
systemPrompt: string;
|
|
667
|
-
messages: AgentMessage[];
|
|
668
|
-
tools: Tool[];
|
|
669
|
-
skills: Skill[];
|
|
670
|
-
state: PhaseState; // { current, available, entryPhaseId, iterations, payload }
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
type PhaseOutput = {
|
|
674
|
-
message: string;
|
|
675
|
-
route: string; // "continue" | "stop" | <phase-name>
|
|
676
|
-
payload?: unknown; // data passed to the next phase
|
|
677
|
-
};
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
### Parallel Execution (Fork/Join)
|
|
681
|
-
|
|
682
|
-
When the route tool returns multiple targets, phases run concurrently:
|
|
683
|
-
|
|
684
|
-
```ts
|
|
685
|
-
route({ decision: [{ phase: "research" }, { phase: "analyze" }] });
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
Each target gets a forked copy of the current messages (or empty if `isolated: true`), runs concurrently via `Promise.allSettled()`, and results are merged back into the conversation. See [docs/phases.md](docs/phases.md).
|
|
689
|
-
|
|
690
|
-
## Extensions
|
|
691
|
-
|
|
692
|
-
The extension system lets plugins register lifecycle hooks, tools, phases, model providers, and cross-plugin events. Plugins are discovered from `<workspace>/.rowan/extensions`.
|
|
693
|
-
|
|
694
|
-
```ts
|
|
695
|
-
import { Agent } from "@rowan-agent/agent";
|
|
696
|
-
|
|
697
|
-
const { extensions } = await Agent.loadExtensions(`${cwd}/.rowan/extensions`);
|
|
698
|
-
// Extensions are fixed for this Runtime-owned Agent Binding.
|
|
699
|
-
const agent = await runtime.createAgent({ context, model, stream, extensions });
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
### Extension Runtime
|
|
703
|
-
|
|
704
|
-
Extension orchestration is internal. The Runtime-owned Agent Binding loads extensions, binds their hooks and events, invalidates their context, and aborts them with the run.
|
|
705
|
-
|
|
706
|
-
### Hook Types
|
|
707
|
-
|
|
708
|
-
| Category | Hooks |
|
|
709
|
-
|----------|-------|
|
|
710
|
-
| Agent | `agent_start`, `agent_end` |
|
|
711
|
-
| Turn | `turn_start`, `turn_end` |
|
|
712
|
-
| Phase | `before_phase`, `after_phase` |
|
|
713
|
-
| Prompt | `before_prompt` |
|
|
714
|
-
| Message | `message_start`, `message_update`, `message_end` |
|
|
715
|
-
| Tool | `before_tool_call`, `after_tool_call`, `tool_execution_start`, `tool_execution_update`, `tool_execution_end` |
|
|
716
|
-
| Lifecycle | `queue_update`, `save_point`, `abort`, `settled` |
|
|
717
|
-
|
|
718
|
-
### Plugin Format
|
|
719
|
-
|
|
720
|
-
```
|
|
721
|
-
<workspace>/.rowan/extensions/my-plugin/
|
|
722
|
-
├── package.json # { "rowan": { "extensions": ["./index.ts"] } }
|
|
723
|
-
└── index.ts
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
```ts
|
|
727
|
-
import type { ExtensionAPI } from "@rowan-agent/agent";
|
|
728
|
-
|
|
729
|
-
export default function myPlugin(rowan: ExtensionAPI) {
|
|
730
|
-
rowan.on("agent_start", (event) => { ... });
|
|
731
|
-
rowan.registerTool({ name: "my_tool", description: "...", parameters: {...}, execute: async (args) => {...} });
|
|
732
|
-
rowan.registerPhase({ name: "review", description: "...", run: async (ctx) => {...} });
|
|
733
|
-
rowan.events.emit("my-plugin:ready", {});
|
|
734
|
-
}
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
> **Full reference:** [Extensions Documentation](docs/extensions.md)
|
|
738
|
-
|
|
739
|
-
## Model Selection
|
|
740
|
-
|
|
741
|
-
`AgentOptions` accepts either one complete `ModelConfig`, which Rowan binds to an Agent-local default stream, or a model reference plus a custom `StreamFn`. A phase model override therefore requires a custom stream that can resolve the override.
|
|
742
|
-
|
|
743
|
-
CLI-specific `.rowan/config.yaml` loading and workspace discovery belong to [`@rowan-agent/cli`](../cli/README.md).
|
|
744
|
-
|
|
745
|
-
## Loop Metrics
|
|
746
|
-
|
|
747
|
-
```ts
|
|
748
|
-
type LoopMetrics = {
|
|
749
|
-
iterations: number;
|
|
750
|
-
phaseTransitions: Array<{ from: string; to: string; ts: string }>;
|
|
751
|
-
compactionCount: number;
|
|
752
|
-
retryCount: number;
|
|
753
|
-
startedAt: string;
|
|
754
|
-
durationMs?: number;
|
|
755
|
-
};
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
## Key Types
|
|
72
|
+
## Events
|
|
759
73
|
|
|
760
|
-
|
|
761
|
-
|------|-------------|
|
|
762
|
-
| `AgentRuntime` | Process-wide lifecycle, scheduling, recovery, Event, and Tool owner |
|
|
763
|
-
| `Agent` | Runtime-owned facade for input and transient Stream Events |
|
|
764
|
-
| `AgentRun` | Durable Run handle for state, terminal Outcome, observation, and abort |
|
|
765
|
-
| `SqliteRuntimeStateStore` / `InMemoryRuntimeStateStore` | Durable and test Runtime Store adapters |
|
|
766
|
-
| `RuntimeEvent` / consumer ID string | Durable lifecycle facts and checkpointed consumer identity |
|
|
767
|
-
| `RuntimeMessage` / `RuntimeMessageId` | Durable Agent Input and its stable lookup identity |
|
|
768
|
-
| `RuntimeToolCall` / `RuntimeToolCallId` | Durable Tool Call state and its stable lookup identity |
|
|
769
|
-
| `AgentContext` | System prompt, messages, tools, skills, phases |
|
|
770
|
-
| `AgentMessage` | Typed message with role, content, metadata |
|
|
771
|
-
| `AgentEvent` | Discriminated union of 13 event types |
|
|
772
|
-
| `Tool` / `ToolResult` | Tool definition and execution result |
|
|
773
|
-
| `Skill` | Loaded skill bundle |
|
|
774
|
-
| `Phase` | Phase definition with content, execution, and routing config |
|
|
775
|
-
| `PhaseContext` / `PhaseOutput` | Phase input and output |
|
|
776
|
-
| `PhaseRegistry` | Map of phase names to Phase objects plus entry phase name |
|
|
777
|
-
| `Outcome` | Terminal result with message and tool results |
|
|
778
|
-
| `LoopMetrics` | Loop iteration, timing, and phase transition stats |
|
|
779
|
-
| `SessionManagerProvider` | Session lifecycle seam used by the Runtime |
|
|
780
|
-
| `JsonlSessionStore` / `InMemorySessionStore` | JSONL and in-memory Session adapters |
|
|
781
|
-
| `ExtensionAPI` / `ExtensionFactory` | Extension developer interface |
|
|
782
|
-
| `StreamFn` / `ModelRef` | Model stream function and model reference |
|
|
74
|
+
`run.observe()` and `runtime.consume()` deliver only Durable Run Events:
|
|
783
75
|
|
|
784
|
-
|
|
76
|
+
- `message_committed`
|
|
77
|
+
- `run_transitioned`
|
|
78
|
+
- `tool_state_changed`
|
|
785
79
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
| [Phases](docs/phases.md) | Phase lifecycle, PHASE.md format, parallel execution, routing, payload |
|
|
789
|
-
| [Extensions](docs/extensions.md) | Extension API, 19 hooks, custom tools/phases, model providers, event bus |
|
|
80
|
+
Events and their corresponding Run aggregate changes commit in one Store
|
|
81
|
+
transaction. Consumers persist their progress through cursors and checkpoints.
|
|
790
82
|
|
|
791
|
-
##
|
|
83
|
+
## Resources
|
|
792
84
|
|
|
793
|
-
|
|
85
|
+
`loadSkills()`, `loadPhases()`, and `loadExtensions()` load workspace resources.
|
|
86
|
+
Pass the resulting resources through `AgentConfig.context` or `extensions`.
|