@rowan-agent/agent 0.4.6 → 0.4.8

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 CHANGED
@@ -1,74 +1,697 @@
1
1
  # @rowan-agent/agent
2
2
 
3
- ## Main Features
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.
4
4
 
5
- `@rowan-agent/agent` is the public programming entry point and Agent core for Rowan. It wraps model execution, event subscriptions, tool registration, loop-owned thread delegation, run cancellation, and idle waiting.
5
+ ## Installation
6
6
 
7
- The package implements a phase-configured Agent loop. The loop executes a configurable sequence of phase definitions through a single base `runPhase()` runner. Built-in phases (route, thread, plan, execute, verify) preserve the default behavior, while custom phase configurations can extend or replace them.
7
+ ```bash
8
+ bun add @rowan-agent/agent
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import {
15
+ Agent,
16
+ createMessage,
17
+ createCoreTools,
18
+ resolveModel,
19
+ } from "@rowan-agent/agent";
20
+
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", name: "gpt-4.1-mini" },
29
+ stream,
30
+ });
31
+
32
+ agent.subscribe((event) => console.log(event.type));
33
+
34
+ const result = await agent.run();
35
+ console.log(result.outcome?.message);
36
+ ```
37
+
38
+ ## Agent
39
+
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`.
41
+
42
+ ```ts
43
+ class Agent {
44
+ constructor(options: AgentOptions);
45
+ run(options?: RunOptions): Promise<AgentRunResult>;
46
+ abort(): void;
47
+ subscribe(listener: AgentEventListener): () => void;
48
+ skill(name: string): Promise<void>;
49
+ phase(name: string): Promise<string>;
50
+ waitForIdle(): Promise<void>;
51
+ flushEvents(): Promise<void>;
52
+ readonly status: AgentStatus;
53
+ }
54
+ ```
55
+
56
+ ### AgentOptions
57
+
58
+ ```ts
59
+ type AgentOptions = {
60
+ context: AgentContext;
61
+ model: LlmModelRef;
62
+ stream: StreamFn;
63
+ cwd?: string;
64
+ rowanDir?: string; // project-local Rowan directory, default: ".rowan"
65
+ sessionId?: string;
66
+ phases?: PhaseRegistry;
67
+ extensions?: ExtensionRunnerRef;
68
+ signal?: AbortSignal;
69
+ maxAttempts?: number;
70
+
71
+ // Lifecycle hooks
72
+ beforeToolCall?: BeforeToolCall;
73
+ afterToolCall?: AfterToolCall;
74
+ onModelTranscript?: (transcript: ModelTranscript, meta: { phase: string; model: LlmModelRef }) => Promise<void>;
75
+ onMessage?: (message: AgentMessage) => Promise<void>;
76
+ onOutcome?: (outcome: Outcome) => Promise<void>;
77
+ };
78
+ ```
79
+
80
+ ### AgentRunResult
8
81
 
9
- ## Architecture
82
+ ```ts
83
+ type AgentRunResult = {
84
+ status: AgentStatus;
85
+ outcome?: Outcome;
86
+ metrics?: LoopMetrics;
87
+ messages: AgentMessage[];
88
+ sessionId: string;
89
+ };
90
+ ```
10
91
 
11
- `src/agent.ts` provides the `Agent` class and remains the core/facade entrypoint. It calls `src/loop.ts` directly.
92
+ ## AgentContext
12
93
 
13
- `src/loop.ts` implements the generic phase-machine loop:
14
- - Creates the loop runtime from input
15
- - Resolves the phase configuration (default built-in or custom)
16
- - Iterates through phases by following transitions (`next`, `stop`, `abort`)
17
- - Completes the run with an `AgentRunResult`
94
+ 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.
18
95
 
19
- Phase definitions live under `src/loop/`:
20
- - `phase-config.ts` `AgentPhaseDefinition`, `AgentPhaseConfig`, validation, and default config factory
21
- - `built-in-phases.ts` — built-in route, thread, plan, execute, and verify phase definitions
22
- - `phases.ts` — base `runPhase()` and `runConfiguredPhase()` runners
23
- - `routing.ts` — route scheduling helper used by the route phase
24
- - `thread.ts` — thread execution helper used by the thread phase
96
+ ```ts
97
+ type AgentContext = {
98
+ systemPrompt: string;
99
+ messages: AgentMessage[];
100
+ tools: Tool[];
101
+ skills: Skill[];
102
+ };
103
+ ```
25
104
 
26
- Mutable live runtime state and lifecycle helpers live in `src/loop.ts` with the generic phase-machine boundary.
105
+ ### AgentMessage
27
106
 
28
- `Agent` keeps a small state object:
107
+ ```ts
108
+ type AgentMessage = {
109
+ id: string;
110
+ role: "system" | "user" | "assistant" | "tool";
111
+ content: string | LlmContentPart[];
112
+ createdAt: string;
113
+ metadata?: Record<string, unknown> & { phase?: string };
114
+ };
115
+ ```
29
116
 
30
- - `sessionId` identifies the current live run/session.
31
- - `context` is the current `systemPrompt`, visible messages, tools, and skills snapshot.
32
- - `model` and `stream` describe the model identity and call path.
33
- - `tools` are the tools the runtime may expose to the model.
34
- - `isRunning`, `currentResult`, and `error` describe the current run state.
117
+ ### Outcome
35
118
 
36
- `src/task.ts` and `src/types.ts` expose task helpers, typed protocol phase outputs, `AgentState`, `createAgentState`, `createMessage`, and public Agent types. Runtime-owned core tools are exported by `@rowan-agent/runtime`. `src/index.ts` is the package entry point.
119
+ The terminal result produced when the loop completes carries the final message and all tool call results from the run.
37
120
 
38
- The loop consumes adapter-normalized `phase_output` events from `@rowan-agent/protocol` while still accepting `structured_output` events for local scripted streams. Default tool calls are executed through the event-neutral runtime primitive; `agent` translates runtime observations into ordered `AgentEvent`s, conversation messages, attempts, verification, and final `AgentRunResult`.
121
+ ```ts
122
+ type Outcome = {
123
+ id: string;
124
+ message: string;
125
+ toolResults?: Array<{
126
+ toolCallId: string;
127
+ toolName: string;
128
+ ok: boolean;
129
+ content: unknown;
130
+ error?: string;
131
+ }>;
132
+ };
133
+ ```
134
+
135
+ ## Tools
136
+
137
+ Four built-in tools cover file read/write and shell execution — the minimum needed for code-related agent work.
39
138
 
40
- `Agent` intentionally exposes the stable, application-facing run surface: context, model, stream, limits, session id, tool approval hooks, event subscriptions, and cancellation. Durable Session persistence belongs to composition roots through `@rowan-agent/session` contracts, not to `Agent`.
139
+ ```ts
140
+ import { createCoreTools } from "@rowan-agent/agent";
41
141
 
42
- ## Usage Flow
142
+ const tools = createCoreTools({
143
+ root: process.cwd(),
144
+ maxReadBytes?, // default: 64KB
145
+ bashTimeoutMs?, // default: 30s
146
+ maxBashOutputBytes?, // default: 64KB
147
+ });
148
+ // Returns: read, write, edit, bash
149
+ ```
43
150
 
44
- 1. Prepare `model`, `stream`, and `tools`. Optionally provide skills, limits, and tool approval hooks.
45
- 2. Create an `Agent` instance.
46
- 3. Use `subscribe` to listen to events. Logging modules and UIs can both consume this stream.
47
- 4. Call `run()` with an `AgentRunConfig.context` snapshot to start or continue a conversation turn.
48
- 5. Pass a loaded `sessionId` and reconstructed context before continuing an existing session, or call `abort()` to stop the current run.
151
+ ### Built-in Tools
152
+
153
+ | Tool | Description | Parameters |
154
+ |------|-------------|------------|
155
+ | `read` | Reads a text file within the workspace | `path` (required), `maxBytes?`, `type?` ("skill" \| "phase" \| "markdown" \| "code" \| "file") |
156
+ | `write` | Writes content to a file, creating parent directories as needed | `path` (required), `content` (required) |
157
+ | `edit` | Replaces exact `oldText` with `newText` in a file | `path` (required), `oldText` (required), `newText` (required), `replaceAll?` |
158
+ | `bash` | Runs a bash command within the workspace | `command` (required), `cwd?`, `timeoutMs?`, `maxOutputBytes?` |
159
+
160
+ ### Custom Tools
49
161
 
50
162
  ```ts
51
- import { Agent } from "@rowan-agent/agent";
52
- import { createMessage } from "@rowan-agent/agent";
53
- import { createCoreTools } from "@rowan-agent/runtime";
163
+ import type { Tool, ToolResult } from "@rowan-agent/agent";
164
+
165
+ const myTool: Tool = {
166
+ name: "search",
167
+ description: "Search project docs",
168
+ parameters: Type.Object({ query: Type.String() }),
169
+ executionMode: "parallel", // "parallel" | "sequential"
170
+ async execute(args, context, signal): Promise<ToolResult> {
171
+ return { toolCallId: context.toolCallId, toolName: "search", ok: true, content: "..." };
172
+ },
173
+ };
174
+ ```
175
+
176
+ ### Tool Execution Hooks
54
177
 
55
- const tools = createCoreTools({ root: process.cwd() });
178
+ `beforeToolCall` can intercept or reject tool calls (e.g. for approval flows); `afterToolCall` can modify results before they reach the model.
179
+
180
+ ```ts
56
181
  const agent = new Agent({
57
- context: {
58
- systemPrompt: "You are Rowan.",
59
- messages: [
60
- createMessage("user", "list the package structure in this project"),
61
- ],
62
- tools,
182
+ async beforeToolCall({ tool, args }) {
183
+ return { allow: true }; // or { allow: false, reason: "blocked" }
184
+ },
185
+ async afterToolCall({ tool, result }) {
186
+ return result;
63
187
  },
64
- model: { provider: "openai-compatible", name: "gpt-4.1-mini" },
65
- stream,
66
188
  });
189
+ ```
67
190
 
68
- agent.subscribe((event) => {
69
- console.error(event.type);
191
+ ## Events
192
+
193
+ 13 event types are emitted during execution — useful for logging, UI updates, or external monitoring.
194
+
195
+ ```ts
196
+ agent.subscribe((event: AgentEvent) => {
197
+ switch (event.type) {
198
+ case "agent_start": // { sessionId }
199
+ case "agent_end": // { sessionId, messages }
200
+ case "turn_start": // { content }
201
+ case "turn_end": // { content, outcome? }
202
+ case "model_requested": // { model, usage }
203
+ case "phase_start": // { phase }
204
+ case "phase_end": // { phase }
205
+ case "message_start": // { message }
206
+ case "message_update": // { message, delta }
207
+ case "message_end": // { message }
208
+ case "tool_execution_start": // { toolCallId, toolName, args }
209
+ case "tool_execution_update": // { toolCallId, toolName, args, partialResult }
210
+ case "tool_execution_end": // { toolCallId, toolName, result, isError }
211
+ }
70
212
  });
213
+ ```
71
214
 
72
- const result = await agent.run();
73
- console.log(result.outcome.message);
215
+ ### Parallel Phase Events
216
+
217
+ 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, a merged `<phase_results>` message is injected and `message_start`/`message_end` fire for it.
218
+
219
+ ## Session
220
+
221
+ JSONL-based session persistence — lets multi-turn conversations survive across process restarts. Supports create, resume, branch, and history replay.
222
+
223
+ ```ts
224
+ import { LocalJsonlSessionManager } from "@rowan-agent/agent";
225
+
226
+ const session = await LocalJsonlSessionManager.create(sessionsDir, { workspaceRoot: process.cwd() });
227
+ const session = await LocalJsonlSessionManager.open(sessionsDir, sessionId);
228
+ const sessions = await LocalJsonlSessionManager.list(sessionsDir);
229
+
230
+ await session.appendMessage(message);
231
+ await session.appendOutcome(outcome);
232
+ await session.appendExecutionTurn(turn);
233
+ const context = await session.buildAgentContext({ tools });
234
+ await session.branch(entryId);
235
+ ```
236
+
237
+ ## Skills
238
+
239
+ Skills are `SKILL.md` knowledge bundles that get injected into the agent context, extending its domain knowledge without changing code.
240
+
241
+ ```ts
242
+ import { loadSkill, loadSkills, resolveSkillPath } from "@rowan-agent/agent";
243
+
244
+ const skills = await loadSkills(workspace);
245
+ const skill = await loadSkill(path, workspace);
246
+ const path = resolveSkillPath("example", workspace);
247
+ // → <rowanDir>/skills/example/SKILL.md
248
+ ```
249
+
250
+ ## Phases
251
+
252
+ 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.
253
+
254
+ ### How It Works
255
+
256
+ 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.
257
+
258
+ ```
259
+ Per iteration:
260
+ 1. Hot-reload phase configs from disk (PHASE.md)
261
+ 2. Inject phase instructions as system message
262
+ 3. Execute phase (factory | run | LLM fallback)
263
+ 4. Extract routing decision from route tool call
264
+ 5. Transition, continue, or stop
74
265
  ```
266
+
267
+ ### Example Phase Flow
268
+
269
+ ```
270
+ ┌────────────┐
271
+ │ User Input │
272
+ └─────┬──────┘
273
+
274
+ ┌────────────┐ route("plan") ┌────────────┐
275
+ │ default │ ────────────────▶│ plan │
276
+ └────────────┘ └─────┬──────┘
277
+
278
+ route("execute")
279
+
280
+
281
+ ┌────────────┐
282
+ │ execute │◀──────────────┐
283
+ └─────┬──────┘ │
284
+ │ │
285
+ route("review") route("execute")
286
+ │ (loop: fix issues)
287
+
288
+ ┌────────────┐
289
+ │ review │
290
+ └─────┬──────┘
291
+
292
+ route({ decision: [{ phase: "lint" }, { phase: "typecheck" }] })
293
+
294
+ ┌─────────┴─────────┐
295
+ ▼ ▼
296
+ ┌──────────┐ ┌──────────┐
297
+ │ lint │ │typecheck │
298
+ └────┬─────┘ └────┬─────┘
299
+ └─────────┬─────────┘
300
+
301
+ <phase_results> merged
302
+
303
+ route("stop")
304
+
305
+
306
+ ┌──────────┐
307
+ │ Outcome │
308
+ └──────────┘
309
+ ```
310
+
311
+ Each arrow is an LLM routing decision via the `route` tool. Parallel branches run concurrently and merge back before the next transition.
312
+
313
+ ### Providing Phases
314
+
315
+ Two sources, merged by priority:
316
+
317
+ **File-based** — `<workspace>/.rowan/phases/*/PHASE.md`
318
+
319
+ ```
320
+ .rowan/phases/review/
321
+ ├── PHASE.md # YAML frontmatter + markdown body
322
+ └── index.ts # optional: factory or run function
323
+ ```
324
+
325
+ ```yaml
326
+ ---
327
+ name: Code Review
328
+ description: Review code for correctness and style
329
+ tools: [read, bash]
330
+ target: execute
331
+ ---
332
+
333
+ Review the current implementation for bugs and style issues.
334
+ ```
335
+
336
+ **Extension-registered** — via `api.registerPhase()`. Same id overrides file-based phases.
337
+
338
+ ```ts
339
+ import type { ExtensionAPI } from "@rowan-agent/agent";
340
+
341
+ export default function myPlugin(api: ExtensionAPI) {
342
+ api.registerPhase({
343
+ id: "review",
344
+ name: "Code Review",
345
+ description: "Review code for correctness",
346
+ tools: ["read", "bash"],
347
+ async run(context, execution) {
348
+ const result = await execution.invokeModel(context);
349
+ return { message: result.text, route: "stop" };
350
+ },
351
+ });
352
+ }
353
+ ```
354
+
355
+ ### Phase
356
+
357
+ ```ts
358
+ interface Phase {
359
+ id: string;
360
+ name: string;
361
+ description: string;
362
+ tools?: string[]; // restrict tools (undefined = all)
363
+ skills?: string[]; // restrict skills
364
+ target?: string; // forced next phase (overrides route tool)
365
+ isolated?: boolean; // empty context when run in parallel
366
+ content: string; // PHASE.md body
367
+ factory?: (api: ExtensionAPI) => Promise<void>;
368
+ run?: (context: PhaseContext, execution: PhaseExecution) => Promise<PhaseOutput | void>;
369
+ }
370
+ ```
371
+
372
+ ### PhaseContext / PhaseOutput
373
+
374
+ ```ts
375
+ interface PhaseContext {
376
+ systemPrompt: string;
377
+ messages: AgentMessage[];
378
+ tools: Tool[];
379
+ skills: Skill[];
380
+ state: PhaseState; // { current, available, iterations, payload }
381
+ }
382
+
383
+ type PhaseOutput = {
384
+ message: string;
385
+ route: string; // "continue" | "stop" | <phase-id>
386
+ payload?: unknown; // data passed to the next phase
387
+ };
388
+ ```
389
+
390
+ ### Parallel Execution (Fork/Join)
391
+
392
+ When the route tool returns multiple targets, phases run concurrently:
393
+
394
+ ```ts
395
+ route({ decision: [{ phase: "research" }, { phase: "analyze" }] });
396
+ ```
397
+
398
+ 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).
399
+
400
+ ## Extensions
401
+
402
+ The extension system lets plugins register lifecycle hooks, tools, phases, model providers, and cross-plugin events. Plugins are discovered from `<workspace>/.rowan/extensions`.
403
+
404
+ ```ts
405
+ import { createExtensionRunner, discoverAndLoadExtensions } from "@rowan-agent/agent";
406
+
407
+ const { extensions } = await discoverAndLoadExtensions(cwd);
408
+ const runner = createExtensionRunner({ cwd });
409
+ await runner.loadExtensions(extensions);
410
+ ```
411
+
412
+ ### ExtensionRunner
413
+
414
+ ```ts
415
+ class ExtensionRunner {
416
+ readonly hooks: HooksManager; // 19 lifecycle hook types
417
+ readonly events: EventBus; // cross-plugin event channel
418
+
419
+ loadExtensions(extensions: LoadedExtension[]): Promise<void>;
420
+ getAllRegisteredTools(): RegisteredTool[];
421
+ getPhases(): Phase[];
422
+ createPhaseRegistry(): PhaseRegistry;
423
+ signal: AbortSignal;
424
+ abort(): void;
425
+ }
426
+ ```
427
+
428
+ ### Hook Types
429
+
430
+ | Category | Hooks |
431
+ |----------|-------|
432
+ | Agent | `agent_start`, `agent_end` |
433
+ | Turn | `turn_start`, `turn_end` |
434
+ | Phase | `before_phase`, `after_phase` |
435
+ | Prompt | `before_prompt` |
436
+ | Message | `message_start`, `message_update`, `message_end` |
437
+ | Tool | `before_tool_call`, `after_tool_call`, `tool_execution_start`, `tool_execution_update`, `tool_execution_end` |
438
+ | Lifecycle | `queue_update`, `save_point`, `abort`, `settled` |
439
+
440
+ ### Plugin Format
441
+
442
+ ```
443
+ <workspace>/.rowan/extensions/my-plugin/
444
+ ├── package.json # { "rowan": { "extensions": ["./index.ts"] } }
445
+ └── index.ts
446
+ ```
447
+
448
+ ```ts
449
+ import type { ExtensionAPI } from "@rowan-agent/agent";
450
+
451
+ export default function myPlugin(rowan: ExtensionAPI) {
452
+ rowan.on("agent_start", (event) => { ... });
453
+ rowan.registerTool({ name: "my_tool", description: "...", parameters: {...}, execute: async (args) => {...} });
454
+ rowan.registerPhase({ id: "review", description: "...", run: async (ctx) => {...} });
455
+ rowan.events.emit("my-plugin:ready", {});
456
+ }
457
+ ```
458
+
459
+ > **Full reference:** [Extensions Documentation](docs/extensions.md)
460
+
461
+ ## Configuration
462
+
463
+ Multi-provider model configuration via `.rowan/config.yaml`. Supports multiple API providers, per-model settings, environment variable interpolation, and per-phase model overrides.
464
+
465
+ Config is loaded from the runtime Rowan directory, which defaults to `.rowan` and can be set when constructing `Agent` via `rowanDir`.
466
+
467
+ ### Config File
468
+
469
+ Place `config.yaml` in your `.rowan/` directory (alongside `phases/`, `skills/`, etc.):
470
+
471
+ ```
472
+ <workspace>/
473
+ └── .rowan/
474
+ ├── config.yaml # model configuration
475
+ ├── phases/ # phase definitions
476
+ ├── skills/ # skill bundles
477
+ └── extensions/ # plugins
478
+ ```
479
+
480
+ ### Schema
481
+
482
+ ```yaml
483
+ model: # optional: explicit default model override
484
+ provider: <string> # → providers[].id
485
+ id: <string> # → providers[].models[].id
486
+
487
+ logLevel: <string> # optional: run log detail (default: "info")
488
+ # one of: debug, info, warn, error, silent
489
+ # priority: --log-level flag > config > ROWAN_LOG_LEVEL env > "info"
490
+
491
+ providers: # required: at least one provider
492
+ - id: <string> # required: provider identifier
493
+ name: <string> # optional: display name
494
+ baseUrl: <string> # required: API base URL
495
+ apiKey: <string> # required: API key (supports ${VAR} interpolation)
496
+ protocol: <string> # required: API protocol (see table below)
497
+ timeoutMs: <number> # optional: request timeout (default: 60000)
498
+ maxRetries: <number> # optional: retry count (default: 4)
499
+ retryDelayMs: <number># optional: delay between retries (default: 1000)
500
+ headers: # optional: extra HTTP headers
501
+ <string>: <string>
502
+ models: # required: at least one model
503
+ - id: <string> # required: model identifier
504
+ name: <string> # optional: display name (defaults to id)
505
+ primary: <bool> # optional: mark as default agent model
506
+ reasoning: <bool> # optional: reasoning model (default: false)
507
+ input: # optional: supported input types (default: ["text"])
508
+ - "text"
509
+ - "image"
510
+ contextWindow: <number> # optional: max context tokens (default: 128000)
511
+ maxTokens: <number> # optional: max output tokens (default: 16384)
512
+ cost: # optional: per-token costs (default: all 0)
513
+ input: <number>
514
+ output: <number>
515
+ cacheRead: <number>
516
+ cacheWrite: <number>
517
+ ```
518
+
519
+ ### Protocols
520
+
521
+ | Protocol | Description |
522
+ |----------|-------------|
523
+ | `openai-completions` | OpenAI Chat Completions API (`/v1/chat/completions`) |
524
+ | `openai-responses` | OpenAI Responses API (`/v1/responses`) |
525
+ | `anthropic-messages` | Anthropic Messages API (`/v1/messages`) |
526
+
527
+ ### Environment Variable Interpolation
528
+
529
+ Use `${VAR_NAME}` syntax in any string value to reference environment variables:
530
+
531
+ ```yaml
532
+ apiKey: ${OPENAI_API_KEY}
533
+ ```
534
+
535
+ Undefined or empty variables throw an error at config load time.
536
+
537
+ ### Default Model Resolution
538
+
539
+ When no `--model` flag is passed, the default model is resolved in order:
540
+
541
+ 1. **Top-level `model:`** — explicit override in config
542
+ 2. **`primary: true`** — first model marked primary (by file order)
543
+ 3. **First model** — first model in config (by parse order)
544
+
545
+ ### Per-Phase Model Override
546
+
547
+ Override the model for a specific phase via PHASE.md frontmatter:
548
+
549
+ ```yaml
550
+ ---
551
+ name: Review
552
+ description: Deep code review
553
+ model: anthropic/claude-sonnet-4-20250514 # format: provider/id or just id
554
+ ---
555
+
556
+ Review the implementation for correctness...
557
+ ```
558
+
559
+ - `model: gpt-4.1` — wildcard provider, resolved by model ID
560
+ - `model: anthropic/claude-sonnet-4-20250514` — specific provider + model
561
+
562
+ ### Loading Config
563
+
564
+ ```ts
565
+ import {
566
+ loadConfigFile,
567
+ registerConfigModels,
568
+ resolveDefaultModel,
569
+ parseModelRef,
570
+ } from "@rowan-agent/agent";
571
+
572
+ // Load from .rowan/config.yaml (returns undefined if missing)
573
+ const config = await loadConfigFile(workspace);
574
+
575
+ // Register all configured models into the global registry
576
+ if (config) registerConfigModels(config);
577
+
578
+ // Resolve default model
579
+ const defaultModel = config ? resolveDefaultModel(config) : undefined;
580
+
581
+ // Parse a model reference string
582
+ const ref = parseModelRef("anthropic/claude-sonnet-4-20250514");
583
+ // → { provider: "anthropic", id: "claude-sonnet-4-20250514" }
584
+ ```
585
+
586
+ ### Config Types
587
+
588
+ ```ts
589
+ type AgentConfigFile = {
590
+ model?: { provider: string; id: string };
591
+ providers: ProviderConfigFromFile[];
592
+ };
593
+
594
+ type ProviderConfigFromFile = {
595
+ id: string;
596
+ name?: string;
597
+ baseUrl: string;
598
+ apiKey: string;
599
+ protocol: Protocol;
600
+ timeoutMs?: number;
601
+ maxRetries?: number;
602
+ retryDelayMs?: number;
603
+ headers?: Record<string, string>;
604
+ models: ModelConfigFromFile[];
605
+ };
606
+
607
+ type ModelConfigFromFile = {
608
+ id: string;
609
+ name?: string;
610
+ primary?: boolean;
611
+ reasoning?: boolean;
612
+ input?: ("text" | "image")[];
613
+ contextWindow?: number;
614
+ maxTokens?: number;
615
+ cost?: Partial<ModelCost>;
616
+ };
617
+ ```
618
+
619
+ ## Context & Prompt
620
+
621
+ Helpers for assembling system prompts and building model requests.
622
+
623
+ ```ts
624
+ import {
625
+ buildSystemPrompt,
626
+ buildModelRequest,
627
+ conversationMessages,
628
+ latestUserInput,
629
+ serializeSkills,
630
+ } from "@rowan-agent/agent";
631
+
632
+ const prompt = buildSystemPrompt({ systemPrompt, tools, skills, cwd });
633
+ const messages = conversationMessages(agentMessages);
634
+ const request = buildModelRequest({ systemPrompt, messages, tools });
635
+ ```
636
+
637
+ ## Workspace
638
+
639
+ Workspace resolution uses the current project for both source and binary runs. The project Rowan directory defaults to `<cwd>/.rowan`; pass `rowanDir` to resolve another project-local directory.
640
+
641
+ ```ts
642
+ import { resolveWorkspacePaths, resolveInWorkspace } from "@rowan-agent/agent";
643
+
644
+ const workspace = resolveWorkspacePaths();
645
+ // → { mode: "source" | "binary", cwd: string, rowanDir: string }
646
+
647
+ const custom = resolveWorkspacePaths({ rowanDir: ".rowan-project" });
648
+ // → custom.rowanDir is <cwd>/.rowan-project
649
+ ```
650
+
651
+ ## Loop Metrics
652
+
653
+ ```ts
654
+ type LoopMetrics = {
655
+ iterations: number;
656
+ phaseTransitions: Array<{ from: string; to: string; ts: string }>;
657
+ compactionCount: number;
658
+ retryCount: number;
659
+ startedAt: string;
660
+ durationMs?: number;
661
+ };
662
+ ```
663
+
664
+ ## Key Types
665
+
666
+ | Type | Description |
667
+ |------|-------------|
668
+ | `Agent` | Main agent facade |
669
+ | `AgentContext` | System prompt, messages, tools, skills |
670
+ | `AgentMessage` | Typed message with role, content, metadata |
671
+ | `AgentEvent` | Discriminated union of 13 event types |
672
+ | `Tool` / `ToolResult` | Tool definition and execution result |
673
+ | `Skill` | Loaded skill bundle |
674
+ | `Phase` | Phase definition with content, execution, and routing config |
675
+ | `PhaseContext` / `PhaseOutput` | Phase input and output |
676
+ | `PhaseRegistry` | Map of phase ids to Phase objects plus entry phase id |
677
+ | `Outcome` | Terminal result with message and tool results |
678
+ | `LoopMetrics` | Loop iteration, timing, and phase transition stats |
679
+ | `LocalJsonlSessionManager` | JSONL session manager |
680
+ | `ExtensionRunner` | Extension runtime with hooks and events |
681
+ | `HooksManager` / `EventBus` | Hook registry and cross-plugin event channel |
682
+ | `StreamFn` / `LlmModelRef` | Model stream function and model reference |
683
+ | `AgentConfigFile` | Parsed `.rowan/config.yaml` structure |
684
+ | `ProviderConfigFromFile` / `ModelConfigFromFile` | Provider and model config entries |
685
+ | `loadConfigFile` / `registerConfigModels` / `resolveDefaultModel` | Config loading and model registration |
686
+ | `parseModelRef` | Parse `"provider/id"` or `"id"` strings to `LlmModelRef` |
687
+
688
+ ## Documentation
689
+
690
+ | Doc | Description |
691
+ |-----|-------------|
692
+ | [Phases](docs/phases.md) | Phase lifecycle, PHASE.md format, parallel execution, routing, payload |
693
+ | [Extensions](docs/extensions.md) | Extension API, 19 hooks, custom tools/phases, model providers, event bus |
694
+
695
+ ## Version
696
+
697
+ Current version: **0.4.6**