@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 +671 -48
- package/dist/chunk-2FIII4GP.js +142 -0
- package/dist/chunk-APSIAOUD.js +505 -0
- package/dist/index.cjs +3989 -3646
- package/dist/index.d.cts +484 -689
- package/dist/index.d.ts +484 -689
- package/dist/index.js +3372 -3687
- package/dist/loader-MVD5JAKM.js +13 -0
- package/dist/path-XORRK2EC.js +28 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -1,74 +1,697 @@
|
|
|
1
1
|
# @rowan-agent/agent
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
## AgentContext
|
|
12
93
|
|
|
13
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
96
|
+
```ts
|
|
97
|
+
type AgentContext = {
|
|
98
|
+
systemPrompt: string;
|
|
99
|
+
messages: AgentMessage[];
|
|
100
|
+
tools: Tool[];
|
|
101
|
+
skills: Skill[];
|
|
102
|
+
};
|
|
103
|
+
```
|
|
25
104
|
|
|
26
|
-
|
|
105
|
+
### AgentMessage
|
|
27
106
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
The terminal result produced when the loop completes — carries the final message and all tool call results from the run.
|
|
37
120
|
|
|
38
|
-
|
|
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
|
-
|
|
139
|
+
```ts
|
|
140
|
+
import { createCoreTools } from "@rowan-agent/agent";
|
|
41
141
|
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 {
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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**
|