@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.
Files changed (4) hide show
  1. package/README.md +338 -326
  2. package/dist/index.d.ts +758 -914
  3. package/dist/index.js +2551 -557
  4. package/package.json +4 -5
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @rowan-agent/agent
2
2
 
3
- Core agent runtime for Rowan. Provides a configurable phase-based execution loop, tool calling, session persistence, event streaming, skills, and an extension system for plugins.
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
- Agent,
16
- createMessage,
21
+ AgentRuntime,
22
+ InMemoryRuntimeStateStore,
23
+ InMemorySessionStore,
17
24
  createCoreTools,
18
- createDispatchStream,
19
25
  } from "@rowan-agent/agent";
20
26
 
21
- const agent = new Agent({
22
- context: {
23
- systemPrompt: "You are a helpful coding assistant.",
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
- agent.subscribe((event) => console.log(event.type));
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
- const result = await agent.run();
35
- console.log(result.outcome?.message);
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
- The `Agent` class is the public facade. It drives the entire execution loop — from receiving context and tools, through phase-based iteration, to producing a terminal `Outcome`.
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
- constructor(options: AgentOptions);
45
- run(options?: RunOptions): Promise<RunResult>;
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 loading — replaces standalone loadSkills/loadPhases/loadExtensions
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 AgentOptions = {
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: LlmModelRef }) => Promise<void>;
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
- ### RunResult
121
-
122
- ```ts
123
- type RunResult = {
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
- For multi-turn use, append input to the agent's current transcript and run with the updated context. `runWithUserInput()` is the main convenience API; the lower-level append methods are useful when a UI or session layer controls message creation.
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.runWithUserInput("summarize this repository");
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.runWithMessage(createMessage("user", "what changed since last turn?"));
216
+ const second = await agent.send("now focus on the CLI package");
217
+ console.log((await second.result()).message);
142
218
  ```
143
219
 
144
- The agent uses `context.phases.entryPhaseId` only until the first successful run completes. Later turns start from the normalized `default` phase so long-lived agents do not repeat one-time entry work. Call `agent.resetInitialization()` when the next run should use `entryPhaseId` again.
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
- Transcript helpers return snapshots, so callers can inspect or edit history without accidentally mutating the agent until they call a setter.
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 messages = agent.getMessages();
150
- agent.replaceTranscript(messages.slice(-6));
151
- agent.clearMessages();
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
- ### Updating Config
246
+ ## AgentRun
155
247
 
156
- `AgentOptions` can be replaced wholesale with `setConfig()`, or updated through focused shortcuts for common orchestration flows.
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
- agent.setSessionId("ses_known");
160
- agent.setModel({ provider: "openai", id: "gpt-4.1" });
161
- agent.setTools(createCoreTools({ root: process.cwd() }));
162
- agent.setSkills(await Agent.loadSkills("./.rowan/skills"));
163
- agent.setPhases(await Agent.loadPhases("./.rowan/phases"));
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
- agent.updateContext((context) => ({
168
- ...context,
169
- systemPrompt: `${context.systemPrompt}\n\nPrefer concise answers.`,
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 = new 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 { LocalJsonlSessionManager } from "@rowan-agent/agent";
514
+ import { JsonlSessionStore } from "@rowan-agent/agent";
309
515
 
310
- const session = await LocalJsonlSessionManager.create(sessionsDir, { workspaceRoot: process.cwd() });
311
- const session = await LocalJsonlSessionManager.open(sessionsDir, sessionId);
312
- const sessions = await LocalJsonlSessionManager.list(sessionsDir);
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 an uninitialized Agent. When phases are loaded from `.rowan/phases/`, the first discovered phase becomes the entry. When none are configured, the Agent normalises to `"default"`. After a successful run, later turns start from `"default"` until `agent.resetInitialization()` is called. This field is an internal routing hint for the phase loop — it is **not** exposed to the LLM, since the agent does not need to know which phase is the entry point to make routing decisions.
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
- // Pass extensions to the Agent constructor — they are loaded and bound internally
492
- const agent = new Agent({ context, model, stream, extensions });
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
- ### ExtensionRunner
704
+ ### Extension Runtime
496
705
 
497
- `ExtensionRunner` is used internally by Agent when extensions are passed via the constructor or `run()`. The Agent manages the runner lifecycle — load, bind, invalidate — automatically.
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
- ## Configuration
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
- ```yaml
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
- ```yaml
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
- | `Agent` | Main agent facade |
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
- | `LocalJsonlSessionManager` | JSONL session manager |
766
- | `ExtensionRunner` | Extension runtime with hooks and events |
767
- | `HooksManager` / `EventBus` | Hook registry and cross-plugin event channel |
768
- | `StreamFn` / `LlmModelRef` | Model stream function and model reference |
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.5.6**
795
+ Current version: **0.6.0**