@rowan-agent/agent 0.7.1 → 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.
Files changed (4) hide show
  1. package/README.md +52 -759
  2. package/dist/index.d.ts +1302 -1283
  3. package/dist/index.js +4602 -5844
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -1,793 +1,86 @@
1
1
  # @rowan-agent/agent
2
2
 
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.
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
- ## Installation
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.start({
28
- stateStore: new InMemoryRuntimeStateStore(),
29
- sessionProvider: new InMemorySessionStore(),
16
+ const runtime = await AgentRuntime.init({
17
+ store: new InMemoryStore(),
30
18
  });
31
19
 
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 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
- async beforeToolCall({ tool, args }) {
378
- return { allow: true }; // or { allow: false, reason: "blocked" }
379
- },
380
- async afterToolCall({ tool, result }) {
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
- ## Events
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
- ### Durable Runtime Events
38
+ ## Public lifecycle
429
39
 
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.
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
- ```ts
436
- const consumer = runtime.consumeEvents("deployment-observer", async (event) => {
437
- await deliverRuntimeFact(event);
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
- 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:
51
+ ## Stores
446
52
 
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
- ```
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
- 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.
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 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:
63
+ ## Tool lifecycle
464
64
 
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.
65
+ Tools are supplied through `AgentConfig.context.tools` and persist through:
495
66
 
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.
67
+ `pending → running → completed | failed | indeterminate`
504
68
 
505
- ### Parallel Phase Events
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
- 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.
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
- | Type | Description |
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
- ## Documentation
76
+ - `message_committed`
77
+ - `run_transitioned`
78
+ - `tool_state_changed`
785
79
 
786
- | Doc | Description |
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
- ## Version
83
+ ## Resources
792
84
 
793
- Current version: **0.7.1**
85
+ `loadSkills()`, `loadPhases()`, and `loadExtensions()` load workspace resources.
86
+ Pass the resulting resources through `AgentConfig.context` or `extensions`.