@rowan-agent/agent 0.5.6 → 0.6.1
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 +338 -326
- package/dist/index.d.ts +758 -914
- package/dist/index.js +2551 -557
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @rowan-agent/agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Embedded durable agent runtime for Rowan. It owns Agent lifecycle, scheduling,
|
|
4
|
+
recovery, Tool Calls, and Runtime Events while preserving the configurable
|
|
5
|
+
phase loop, Sessions, skills, and extension system.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -10,86 +12,159 @@ bun add @rowan-agent/agent
|
|
|
10
12
|
|
|
11
13
|
## Quick Start
|
|
12
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`.
|
|
18
|
+
|
|
13
19
|
```ts
|
|
14
20
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
AgentRuntime,
|
|
22
|
+
InMemoryRuntimeStateStore,
|
|
23
|
+
InMemorySessionStore,
|
|
17
24
|
createCoreTools,
|
|
18
|
-
createDispatchStream,
|
|
19
25
|
} from "@rowan-agent/agent";
|
|
20
26
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
messages: [createMessage("user", "list the files in this project")],
|
|
25
|
-
tools: createCoreTools({ root: process.cwd() }),
|
|
26
|
-
skills: [],
|
|
27
|
-
},
|
|
28
|
-
model: { provider: "openai", id: "gpt-4.1-mini" },
|
|
29
|
-
stream: createDispatchStream(),
|
|
27
|
+
const runtime = await AgentRuntime.start({
|
|
28
|
+
stateStore: new InMemoryRuntimeStateStore(),
|
|
29
|
+
sessionProvider: new InMemorySessionStore(),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
try {
|
|
33
|
+
const agent = await runtime.createAgent({
|
|
34
|
+
context: {
|
|
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 recovers abandoned Leases into durable queued work without
|
|
140
|
+
constructing Agent Bindings. The host supplies its current executable resources
|
|
141
|
+
when it reconstructs an Agent:
|
|
33
142
|
|
|
34
|
-
|
|
35
|
-
|
|
143
|
+
```ts
|
|
144
|
+
const runtime = await AgentRuntime.start({ stateStore, sessionProvider });
|
|
145
|
+
const agent = await runtime.reconstructAgent(agentId, currentAgentOptions);
|
|
36
146
|
```
|
|
37
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
|
+
|
|
38
152
|
## Agent
|
|
39
153
|
|
|
40
|
-
|
|
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.
|
|
41
158
|
|
|
42
159
|
```ts
|
|
43
160
|
class Agent {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// User input / conversation continuation
|
|
48
|
-
appendUserMessage(input: string): void;
|
|
49
|
-
appendMessage(message: AgentMessage): void;
|
|
50
|
-
appendMessages(messages: AgentMessage[]): void;
|
|
51
|
-
runWithUserInput(input: string, options?: RunOptions): Promise<RunResult>;
|
|
52
|
-
runWithMessage(message: AgentMessage, options?: RunOptions): Promise<RunResult>;
|
|
53
|
-
resetInitialization(): void;
|
|
54
|
-
|
|
55
|
-
// Context and transcript
|
|
56
|
-
getContext(): AgentContext;
|
|
57
|
-
setContext(context: AgentContext): void;
|
|
58
|
-
updateContext(updater: (context: AgentContext) => AgentContext): void;
|
|
59
|
-
forkContext(overrides?: Partial<AgentContext>): AgentContext;
|
|
60
|
-
getMessages(): AgentMessage[];
|
|
61
|
-
setMessages(messages: AgentMessage[]): void;
|
|
62
|
-
clearMessages(): void;
|
|
63
|
-
getTranscript(): AgentMessage[];
|
|
64
|
-
replaceTranscript(messages: AgentMessage[]): void;
|
|
65
|
-
|
|
66
|
-
// Config access and shortcuts
|
|
67
|
-
getConfig(): AgentOptions;
|
|
68
|
-
setConfig(config: AgentOptions): void;
|
|
69
|
-
updateConfig(updater: (config: AgentOptions) => AgentOptions): void;
|
|
70
|
-
setSessionId(sessionId: string): void;
|
|
71
|
-
getSessionId(): string | undefined;
|
|
72
|
-
setModel(model: LlmModelRef): void;
|
|
73
|
-
setTools(tools: Tool[]): void;
|
|
74
|
-
setSkills(skills: Skill[]): void;
|
|
75
|
-
setPhases(phases: PhaseRegistry): void;
|
|
76
|
-
setCwd(cwd: string): void;
|
|
77
|
-
setStream(stream: StreamFn): void;
|
|
78
|
-
getModel(): LlmModelRef;
|
|
79
|
-
getTools(): Tool[];
|
|
80
|
-
getSkills(): Skill[];
|
|
81
|
-
getPhases(): PhaseRegistry | undefined;
|
|
82
|
-
getCwd(): string | undefined;
|
|
83
|
-
|
|
84
|
-
abort(): void;
|
|
161
|
+
readonly id: AgentId;
|
|
162
|
+
readonly sessionId: string;
|
|
163
|
+
send(input: string | AgentMessage): Promise<AgentRun>;
|
|
85
164
|
subscribe(listener: AgentEventListener): () => void;
|
|
86
|
-
skill(name: string, additionalInstructions?: string): string;
|
|
87
|
-
phase(name: string): Promise<string>;
|
|
88
|
-
waitForIdle(): Promise<void>;
|
|
89
165
|
flushEvents(): Promise<void>;
|
|
90
|
-
readonly state: AgentStatus;
|
|
91
166
|
|
|
92
|
-
// Resource
|
|
167
|
+
// Resource discovery helpers
|
|
93
168
|
static loadSkills(targetPath: string): Promise<Skill[]>;
|
|
94
169
|
static loadPhases(targetPath: string): Promise<PhaseRegistry>;
|
|
95
170
|
static loadExtensions(targetPath: string): Promise<LoadExtensionsResult>;
|
|
@@ -99,77 +174,110 @@ class Agent {
|
|
|
99
174
|
### AgentOptions
|
|
100
175
|
|
|
101
176
|
```ts
|
|
102
|
-
type
|
|
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 = {
|
|
103
188
|
context: AgentContext;
|
|
104
|
-
model: LlmModelRef;
|
|
105
|
-
stream: StreamFn;
|
|
106
189
|
cwd?: string;
|
|
107
190
|
extensions?: LoadedExtension[];
|
|
108
|
-
sessionId?: string;
|
|
109
191
|
maxAttempts?: number;
|
|
110
192
|
|
|
111
193
|
// Lifecycle hooks
|
|
112
194
|
beforeToolCall?: BeforeToolCall;
|
|
113
195
|
afterToolCall?: AfterToolCall;
|
|
114
|
-
onModelTranscript?: (transcript: ModelTranscript, meta: { phase: string; model:
|
|
196
|
+
onModelTranscript?: (transcript: ModelTranscript, meta: { phase: string; model: ModelRef }) => Promise<void>;
|
|
115
197
|
onMessage?: (message: AgentMessage) => Promise<void>;
|
|
116
198
|
onOutcome?: (outcome: Outcome) => Promise<void>;
|
|
117
199
|
};
|
|
118
|
-
```
|
|
119
200
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
sessionId: string;
|
|
125
|
-
messages: AgentMessage[];
|
|
126
|
-
outcome: Outcome;
|
|
127
|
-
metrics: LoopMetrics;
|
|
128
|
-
};
|
|
201
|
+
type AgentOptions = AgentCommonOptions & (
|
|
202
|
+
| { model: ModelConfig; stream?: never }
|
|
203
|
+
| { model: ModelRef; stream: StreamFn }
|
|
204
|
+
);
|
|
129
205
|
```
|
|
130
206
|
|
|
131
207
|
### Conversation Continuation
|
|
132
208
|
|
|
133
|
-
|
|
209
|
+
Every turn enters through `send()`. The Runtime persists the input before it
|
|
210
|
+
returns an `AgentRun`; Session history is restored during reconstruction.
|
|
134
211
|
|
|
135
212
|
```ts
|
|
136
|
-
const first = await agent.
|
|
137
|
-
|
|
138
|
-
agent.appendUserMessage("now focus on the CLI package");
|
|
139
|
-
const second = await agent.run();
|
|
213
|
+
const first = await agent.send("summarize this repository");
|
|
214
|
+
console.log((await first.result()).message);
|
|
140
215
|
|
|
141
|
-
await agent.
|
|
216
|
+
const second = await agent.send("now focus on the CLI package");
|
|
217
|
+
console.log((await second.result()).message);
|
|
142
218
|
```
|
|
143
219
|
|
|
144
|
-
|
|
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
|
|
145
229
|
|
|
146
|
-
|
|
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.
|
|
147
234
|
|
|
148
235
|
```ts
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
});
|
|
152
244
|
```
|
|
153
245
|
|
|
154
|
-
|
|
246
|
+
## AgentRun
|
|
155
247
|
|
|
156
|
-
`
|
|
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.
|
|
157
251
|
|
|
158
252
|
```ts
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
agent.setCwd(process.cwd());
|
|
165
|
-
agent.setStream(createDispatchStream());
|
|
253
|
+
type AgentInputRequest = {
|
|
254
|
+
phase: string;
|
|
255
|
+
prompt: string;
|
|
256
|
+
requestedAt: string;
|
|
257
|
+
};
|
|
166
258
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
275
|
```
|
|
172
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
|
+
|
|
173
281
|
## AgentContext
|
|
174
282
|
|
|
175
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.
|
|
@@ -262,7 +370,10 @@ const myTool: Tool = {
|
|
|
262
370
|
`beforeToolCall` can intercept or reject tool calls (e.g. for approval flows); `afterToolCall` can modify results before they reach the model.
|
|
263
371
|
|
|
264
372
|
```ts
|
|
265
|
-
const agent =
|
|
373
|
+
const agent = await runtime.createAgent({
|
|
374
|
+
context,
|
|
375
|
+
model,
|
|
376
|
+
stream,
|
|
266
377
|
async beforeToolCall({ tool, args }) {
|
|
267
378
|
return { allow: true }; // or { allow: false, reason: "blocked" }
|
|
268
379
|
},
|
|
@@ -272,6 +383,24 @@ const agent = new Agent({
|
|
|
272
383
|
});
|
|
273
384
|
```
|
|
274
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 },
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
275
404
|
## Events
|
|
276
405
|
|
|
277
406
|
13 event types are emitted during execution — useful for logging, UI updates, or external monitoring.
|
|
@@ -296,6 +425,83 @@ agent.subscribe((event: AgentEvent) => {
|
|
|
296
425
|
});
|
|
297
426
|
```
|
|
298
427
|
|
|
428
|
+
### Durable Runtime Events
|
|
429
|
+
|
|
430
|
+
Runtime State transitions are a separate durable stream. Give each consumer a
|
|
431
|
+
stable ID; its Checkpoint advances only after the listener succeeds. Delivery
|
|
432
|
+
is asynchronous, so a slow or unavailable consumer does not block state
|
|
433
|
+
transitions.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
const consumer = runtime.consumeEvents("deployment-observer", async (event) => {
|
|
437
|
+
await deliverRuntimeFact(event);
|
|
438
|
+
});
|
|
439
|
+
await consumer.caughtUp;
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Runtime Messages and their related Events are committed together. A durable
|
|
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:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const indexConsumer = runtime.consumeEvents("everyield-run-index", async (event) => {
|
|
449
|
+
if (event.kind !== "run_enqueued" || !event.messageId || !event.runId) return;
|
|
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
|
+
```
|
|
456
|
+
|
|
457
|
+
If the listener fails, Rowan leaves the Checkpoint unchanged and redelivers the
|
|
458
|
+
Event after the consumer restarts. `getMessage()` is read-only; Rowan treats the
|
|
459
|
+
Agent Message metadata as opaque host data.
|
|
460
|
+
|
|
461
|
+
Tool Call Events can likewise be resolved to their durable record. This is
|
|
462
|
+
especially useful when a `tool_call_indeterminate` Event requires host recovery
|
|
463
|
+
or human review:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
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.
|
|
495
|
+
|
|
496
|
+
`AgentRunMetadata` is optional opaque host data on `AgentMessage.metadata`. It
|
|
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.
|
|
504
|
+
|
|
299
505
|
### Parallel Phase Events
|
|
300
506
|
|
|
301
507
|
When multiple phases run concurrently (via multi-target `route`), each branch emits its own `turn_*`, `message_*`, and `tool_execution_*` events into the shared event stream — they are interleaved, not sequenced. Individual parallel phases do **not** emit `phase_start`/`phase_end`; those only fire for serial phases. After all branches complete, their outputs are stashed and surfaced in the next iteration's phase entry message (under `<prev_phase_outputs>`); the `message_start`/`message_end` you observe for that entry message carry the merged results.
|
|
@@ -305,17 +511,20 @@ When multiple phases run concurrently (via multi-target `route`), each branch em
|
|
|
305
511
|
JSONL-based session persistence — lets multi-turn conversations survive across process restarts. Supports create, resume, branch, and history replay.
|
|
306
512
|
|
|
307
513
|
```ts
|
|
308
|
-
import {
|
|
514
|
+
import { JsonlSessionStore } from "@rowan-agent/agent";
|
|
309
515
|
|
|
310
|
-
const
|
|
311
|
-
const session = await
|
|
312
|
-
|
|
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();
|
|
313
524
|
|
|
314
525
|
await session.appendMessage(message);
|
|
315
526
|
await session.appendOutcome(outcome);
|
|
316
|
-
await session.appendExecutionTurn(turn);
|
|
317
527
|
const context = await session.buildAgentContext({ tools });
|
|
318
|
-
await session.branch(entryId);
|
|
319
528
|
```
|
|
320
529
|
|
|
321
530
|
## Skills
|
|
@@ -345,7 +554,7 @@ Per iteration:
|
|
|
345
554
|
5. Transition, continue, or stop
|
|
346
555
|
```
|
|
347
556
|
|
|
348
|
-
**`entryPhaseId`** specifies which phase the loop enters for
|
|
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.
|
|
349
558
|
|
|
350
559
|
### Example Phase Flow
|
|
351
560
|
|
|
@@ -488,27 +697,13 @@ The extension system lets plugins register lifecycle hooks, tools, phases, model
|
|
|
488
697
|
import { Agent } from "@rowan-agent/agent";
|
|
489
698
|
|
|
490
699
|
const { extensions } = await Agent.loadExtensions(`${cwd}/.rowan/extensions`);
|
|
491
|
-
//
|
|
492
|
-
const agent =
|
|
700
|
+
// Extensions are fixed for this Runtime-owned Agent Binding.
|
|
701
|
+
const agent = await runtime.createAgent({ context, model, stream, extensions });
|
|
493
702
|
```
|
|
494
703
|
|
|
495
|
-
###
|
|
704
|
+
### Extension Runtime
|
|
496
705
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
```ts
|
|
500
|
-
class ExtensionRunner {
|
|
501
|
-
readonly hooks: HooksManager; // 19 lifecycle hook types
|
|
502
|
-
readonly events: EventBus; // cross-plugin event channel
|
|
503
|
-
|
|
504
|
-
loadExtensions(extensions: LoadedExtension[]): Promise<void>;
|
|
505
|
-
getAllRegisteredTools(): RegisteredTool[];
|
|
506
|
-
getPhases(): Phase[];
|
|
507
|
-
createPhaseRegistry(): PhaseRegistry;
|
|
508
|
-
signal: AbortSignal;
|
|
509
|
-
abort(): void;
|
|
510
|
-
}
|
|
511
|
-
```
|
|
706
|
+
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.
|
|
512
707
|
|
|
513
708
|
### Hook Types
|
|
514
709
|
|
|
@@ -543,196 +738,11 @@ export default function myPlugin(rowan: ExtensionAPI) {
|
|
|
543
738
|
|
|
544
739
|
> **Full reference:** [Extensions Documentation](docs/extensions.md)
|
|
545
740
|
|
|
546
|
-
##
|
|
547
|
-
|
|
548
|
-
Multi-provider model configuration via `.rowan/config.yaml`. Supports multiple API providers, per-model settings, environment variable interpolation, and per-phase model overrides.
|
|
549
|
-
|
|
550
|
-
Config is loaded from the runtime Rowan directory, which defaults to `.rowan`.
|
|
551
|
-
|
|
552
|
-
### Config File
|
|
553
|
-
|
|
554
|
-
Place `config.yaml` in your `.rowan/` directory (alongside `phases/`, `skills/`, etc.):
|
|
555
|
-
|
|
556
|
-
```
|
|
557
|
-
<workspace>/
|
|
558
|
-
└── .rowan/
|
|
559
|
-
├── config.yaml # model configuration
|
|
560
|
-
├── phases/ # phase definitions
|
|
561
|
-
├── skills/ # skill bundles
|
|
562
|
-
└── extensions/ # plugins
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
### Schema
|
|
741
|
+
## Model Selection
|
|
566
742
|
|
|
567
|
-
|
|
568
|
-
model: # optional: explicit default model override
|
|
569
|
-
provider: <string> # → providers[].id
|
|
570
|
-
id: <string> # → providers[].models[].id
|
|
571
|
-
|
|
572
|
-
logLevel: <string> # optional: run log detail (default: "info")
|
|
573
|
-
# one of: debug, info, warn, error, silent
|
|
574
|
-
# priority: --log-level flag > config > ROWAN_LOG_LEVEL env > "info"
|
|
575
|
-
|
|
576
|
-
providers: # required: at least one provider
|
|
577
|
-
- id: <string> # required: provider identifier
|
|
578
|
-
name: <string> # optional: display name
|
|
579
|
-
baseUrl: <string> # required: API base URL
|
|
580
|
-
apiKey: <string> # required: API key (supports ${VAR} interpolation)
|
|
581
|
-
protocol: <string> # required: API protocol (see table below)
|
|
582
|
-
timeoutMs: <number> # optional: streaming idle timeout after first byte (default: 60000)
|
|
583
|
-
maxRetries: <number> # optional: retry count (default: 4)
|
|
584
|
-
retryDelayMs: <number># optional: delay between retries (default: 1000)
|
|
585
|
-
headers: # optional: extra HTTP headers
|
|
586
|
-
<string>: <string>
|
|
587
|
-
models: # required: at least one model
|
|
588
|
-
- id: <string> # required: model identifier
|
|
589
|
-
name: <string> # optional: display name (defaults to id)
|
|
590
|
-
primary: <bool> # optional: mark as default agent model
|
|
591
|
-
reasoning: <bool> # optional: reasoning model (default: false)
|
|
592
|
-
input: # optional: supported input types (default: ["text"])
|
|
593
|
-
- "text"
|
|
594
|
-
- "image"
|
|
595
|
-
contextWindow: <number> # optional: max context tokens (default: 128000)
|
|
596
|
-
maxTokens: <number> # optional: max output tokens (default: 16384)
|
|
597
|
-
cost: # optional: per-token costs (default: all 0)
|
|
598
|
-
input: <number>
|
|
599
|
-
output: <number>
|
|
600
|
-
cacheRead: <number>
|
|
601
|
-
cacheWrite: <number>
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
### Protocols
|
|
605
|
-
|
|
606
|
-
| Protocol | Description |
|
|
607
|
-
|----------|-------------|
|
|
608
|
-
| `openai-completions` | OpenAI Chat Completions API (`/v1/chat/completions`) |
|
|
609
|
-
| `openai-responses` | OpenAI Responses API (`/v1/responses`) |
|
|
610
|
-
| `anthropic-messages` | Anthropic Messages API (`/v1/messages`) |
|
|
611
|
-
|
|
612
|
-
### Environment Variable Interpolation
|
|
613
|
-
|
|
614
|
-
Use `${VAR_NAME}` syntax in any string value to reference environment variables:
|
|
615
|
-
|
|
616
|
-
```yaml
|
|
617
|
-
apiKey: ${OPENAI_API_KEY}
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
Undefined or empty variables throw an error at config load time.
|
|
621
|
-
|
|
622
|
-
### Default Model Resolution
|
|
623
|
-
|
|
624
|
-
When no `--model` flag is passed, the default model is resolved in order:
|
|
625
|
-
|
|
626
|
-
1. **Top-level `model:`** — explicit override in config
|
|
627
|
-
2. **`primary: true`** — first model marked primary (by file order)
|
|
628
|
-
3. **First model** — first model in config (by parse order)
|
|
629
|
-
|
|
630
|
-
### Per-Phase Model Override
|
|
631
|
-
|
|
632
|
-
Override the model for a specific phase via PHASE.md frontmatter:
|
|
743
|
+
`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.
|
|
633
744
|
|
|
634
|
-
|
|
635
|
-
---
|
|
636
|
-
name: Review
|
|
637
|
-
description: Deep code review
|
|
638
|
-
model: anthropic/claude-sonnet-4-20250514 # format: provider/id or just id
|
|
639
|
-
---
|
|
640
|
-
|
|
641
|
-
Review the implementation for correctness...
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
- `model: gpt-4.1` — wildcard provider, resolved by model ID
|
|
645
|
-
- `model: anthropic/claude-sonnet-4-20250514` — specific provider + model
|
|
646
|
-
|
|
647
|
-
### Loading Config
|
|
648
|
-
|
|
649
|
-
```ts
|
|
650
|
-
import {
|
|
651
|
-
loadConfigFile,
|
|
652
|
-
registerConfigModels,
|
|
653
|
-
resolveDefaultModel,
|
|
654
|
-
parseModelRef,
|
|
655
|
-
} from "@rowan-agent/agent";
|
|
656
|
-
|
|
657
|
-
// Load from .rowan/config.yaml (returns undefined if missing)
|
|
658
|
-
const config = await loadConfigFile(workspace);
|
|
659
|
-
|
|
660
|
-
// Register all configured models into the global registry
|
|
661
|
-
if (config) registerConfigModels(config);
|
|
662
|
-
|
|
663
|
-
// Resolve default model
|
|
664
|
-
const defaultModel = config ? resolveDefaultModel(config) : undefined;
|
|
665
|
-
|
|
666
|
-
// Parse a model reference string
|
|
667
|
-
const ref = parseModelRef("anthropic/claude-sonnet-4-20250514");
|
|
668
|
-
// → { provider: "anthropic", id: "claude-sonnet-4-20250514" }
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
### Config Types
|
|
672
|
-
|
|
673
|
-
```ts
|
|
674
|
-
type AgentConfigFile = {
|
|
675
|
-
model?: { provider: string; id: string };
|
|
676
|
-
providers: ProviderConfigFromFile[];
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
type ProviderConfigFromFile = {
|
|
680
|
-
id: string;
|
|
681
|
-
name?: string;
|
|
682
|
-
baseUrl: string;
|
|
683
|
-
apiKey: string;
|
|
684
|
-
protocol: Protocol;
|
|
685
|
-
/** Maximum idle gap between response bytes after the first byte. */
|
|
686
|
-
timeoutMs?: number;
|
|
687
|
-
maxRetries?: number;
|
|
688
|
-
retryDelayMs?: number;
|
|
689
|
-
headers?: Record<string, string>;
|
|
690
|
-
models: ModelConfigFromFile[];
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
type ModelConfigFromFile = {
|
|
694
|
-
id: string;
|
|
695
|
-
name?: string;
|
|
696
|
-
primary?: boolean;
|
|
697
|
-
reasoning?: boolean;
|
|
698
|
-
input?: ("text" | "image")[];
|
|
699
|
-
contextWindow?: number;
|
|
700
|
-
maxTokens?: number;
|
|
701
|
-
cost?: Partial<ModelCost>;
|
|
702
|
-
};
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
## Context & Prompt
|
|
706
|
-
|
|
707
|
-
Helpers for assembling system prompts and building model requests.
|
|
708
|
-
|
|
709
|
-
```ts
|
|
710
|
-
import {
|
|
711
|
-
buildSystemPrompt,
|
|
712
|
-
buildModelRequest,
|
|
713
|
-
conversationMessages,
|
|
714
|
-
latestUserInput,
|
|
715
|
-
serializeSkills,
|
|
716
|
-
} from "@rowan-agent/agent";
|
|
717
|
-
|
|
718
|
-
const prompt = buildSystemPrompt({ systemPrompt, tools, skills, cwd });
|
|
719
|
-
const messages = conversationMessages(agentMessages);
|
|
720
|
-
const request = buildModelRequest({ systemPrompt, messages, tools });
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
## Workspace
|
|
724
|
-
|
|
725
|
-
Workspace resolution uses the current project root. The project Rowan directory defaults to `<cwd>/.rowan`; pass `rowanDir` to resolve another project-local directory.
|
|
726
|
-
|
|
727
|
-
```ts
|
|
728
|
-
import { resolveWorkspacePaths, resolveInWorkspace } from "@rowan-agent/agent";
|
|
729
|
-
|
|
730
|
-
const workspace = resolveWorkspacePaths();
|
|
731
|
-
// → { cwd: string, rowanDir: string }
|
|
732
|
-
|
|
733
|
-
const custom = resolveWorkspacePaths({ rowanDir: ".rowan-project" });
|
|
734
|
-
// → custom.rowanDir is <cwd>/.rowan-project
|
|
735
|
-
```
|
|
745
|
+
CLI-specific `.rowan/config.yaml` loading and workspace discovery belong to [`@rowan-agent/cli`](../cli/README.md).
|
|
736
746
|
|
|
737
747
|
## Loop Metrics
|
|
738
748
|
|
|
@@ -751,7 +761,13 @@ type LoopMetrics = {
|
|
|
751
761
|
|
|
752
762
|
| Type | Description |
|
|
753
763
|
|------|-------------|
|
|
754
|
-
| `
|
|
764
|
+
| `AgentRuntime` | Process-wide lifecycle, scheduling, recovery, Event, and Tool owner |
|
|
765
|
+
| `Agent` | Runtime-owned facade for input and transient Stream Events |
|
|
766
|
+
| `AgentRun` | Durable Run handle for state, terminal Outcome, observation, and abort |
|
|
767
|
+
| `SqliteRuntimeStateStore` / `InMemoryRuntimeStateStore` | Durable and test Runtime Store adapters |
|
|
768
|
+
| `RuntimeEvent` / consumer ID string | Durable lifecycle facts and checkpointed consumer identity |
|
|
769
|
+
| `RuntimeMessage` / `RuntimeMessageId` | Durable Agent Input and its stable lookup identity |
|
|
770
|
+
| `RuntimeToolCall` / `RuntimeToolCallId` | Durable Tool Call state and its stable lookup identity |
|
|
755
771
|
| `AgentContext` | System prompt, messages, tools, skills, phases |
|
|
756
772
|
| `AgentMessage` | Typed message with role, content, metadata |
|
|
757
773
|
| `AgentEvent` | Discriminated union of 13 event types |
|
|
@@ -762,14 +778,10 @@ type LoopMetrics = {
|
|
|
762
778
|
| `PhaseRegistry` | Map of phase ids to Phase objects plus entry phase id |
|
|
763
779
|
| `Outcome` | Terminal result with message and tool results |
|
|
764
780
|
| `LoopMetrics` | Loop iteration, timing, and phase transition stats |
|
|
765
|
-
| `
|
|
766
|
-
| `
|
|
767
|
-
| `
|
|
768
|
-
| `StreamFn` / `
|
|
769
|
-
| `AgentConfigFile` | Parsed `.rowan/config.yaml` structure |
|
|
770
|
-
| `ProviderConfigFromFile` / `ModelConfigFromFile` | Provider and model config entries |
|
|
771
|
-
| `loadConfigFile` / `registerConfigModels` / `resolveDefaultModel` | Config loading and model registration |
|
|
772
|
-
| `parseModelRef` | Parse `"provider/id"` or `"id"` strings to `LlmModelRef` |
|
|
781
|
+
| `SessionManagerProvider` | Session lifecycle seam used by the Runtime |
|
|
782
|
+
| `JsonlSessionStore` / `InMemorySessionStore` | JSONL and in-memory Session adapters |
|
|
783
|
+
| `ExtensionAPI` / `ExtensionFactory` | Extension developer interface |
|
|
784
|
+
| `StreamFn` / `ModelRef` | Model stream function and model reference |
|
|
773
785
|
|
|
774
786
|
## Documentation
|
|
775
787
|
|
|
@@ -780,4 +792,4 @@ type LoopMetrics = {
|
|
|
780
792
|
|
|
781
793
|
## Version
|
|
782
794
|
|
|
783
|
-
Current version: **0.
|
|
795
|
+
Current version: **0.6.0**
|