@rowan-agent/agent 0.4.4

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/dist/index.js ADDED
@@ -0,0 +1,4039 @@
1
+ // src/utils.ts
2
+ function createId(prefix) {
3
+ return `${prefix}_${crypto.randomUUID().slice(0, 8)}`;
4
+ }
5
+ function padDatePart(value, length = 2) {
6
+ return String(value).padStart(length, "0");
7
+ }
8
+ function formatLocalTimestamp(date) {
9
+ const offsetMinutes = -date.getTimezoneOffset();
10
+ const offsetSign = offsetMinutes >= 0 ? "+" : "-";
11
+ const offsetAbsolute = Math.abs(offsetMinutes);
12
+ const offsetHours = Math.floor(offsetAbsolute / 60);
13
+ const offsetRemainingMinutes = offsetAbsolute % 60;
14
+ return [
15
+ `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`,
16
+ "T",
17
+ `${padDatePart(date.getHours())}${padDatePart(date.getMinutes())}${padDatePart(date.getSeconds())}`,
18
+ "-",
19
+ padDatePart(Math.floor(date.getMilliseconds() / 10)),
20
+ offsetSign,
21
+ padDatePart(offsetHours),
22
+ ":",
23
+ padDatePart(offsetRemainingMinutes)
24
+ ].join("");
25
+ }
26
+ function createTimestamp(date = /* @__PURE__ */ new Date()) {
27
+ return formatLocalTimestamp(date);
28
+ }
29
+ var createJson = {
30
+ new(value) {
31
+ return JSON.parse(JSON.stringify(value));
32
+ },
33
+ stringify(value) {
34
+ return JSON.stringify(value, null, 2);
35
+ }
36
+ };
37
+
38
+ // src/types.ts
39
+ var AGENT_STATE_SCHEMA_VERSION = "0.4.4";
40
+ function createMessage(role, content, metadata) {
41
+ return {
42
+ id: createId("msg"),
43
+ role,
44
+ content,
45
+ createdAt: createTimestamp(),
46
+ ...metadata ? { metadata } : {}
47
+ };
48
+ }
49
+ function createAgentState(input) {
50
+ const createdAt = createTimestamp();
51
+ const messages = input.messages?.map(createJson.new) ?? [
52
+ createMessage("user", input.input)
53
+ ];
54
+ return {
55
+ version: AGENT_STATE_SCHEMA_VERSION,
56
+ id: input.id ?? createId("ses"),
57
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
58
+ systemPrompt: input.systemPrompt,
59
+ input: input.input,
60
+ messages,
61
+ skills: input.skills?.map(createJson.new) ?? [],
62
+ createdAt,
63
+ updatedAt: createdAt,
64
+ ...input.title ? { title: input.title } : {}
65
+ };
66
+ }
67
+
68
+ // src/loop/phases/built-in/chat/index.ts
69
+ function chat_default(api) {
70
+ api.registerPhase({
71
+ ...api.manifest?.phase,
72
+ id: "chat",
73
+ async run(context, input) {
74
+ const collected = await context.turn(() => context.model.invoke({ input }));
75
+ if (collected.stopReason === "aborted") {
76
+ return { message: collected.text, route: "stop", toolCalls: collected.toolCalls };
77
+ }
78
+ const nonRouteToolCalls = collected.toolCalls.filter((t) => t.name !== "route");
79
+ if (nonRouteToolCalls.length > 0) {
80
+ return { message: collected.text.trim() || "Executing tools.", route: "execute", toolCalls: collected.toolCalls };
81
+ }
82
+ return { message: collected.text.trim() || "Done.", route: "stop", toolCalls: collected.toolCalls };
83
+ }
84
+ });
85
+ }
86
+
87
+ // src/loop/phases/built-in/execute/index.ts
88
+ function execute_default(api) {
89
+ api.registerPhase({
90
+ ...api.manifest?.phase,
91
+ id: "execute",
92
+ prompt: {
93
+ instructions: [
94
+ "Phase: execute",
95
+ "",
96
+ "Execute the task by calling the appropriate tools.",
97
+ "If more tool calls are needed, continue calling tools.",
98
+ "If execution is complete, respond with a brief summary and call the 'route' tool."
99
+ ]
100
+ },
101
+ async run(context, input) {
102
+ context.incrementAttempt();
103
+ const maxAttempts = context.maxAttempts ?? 2;
104
+ let collected;
105
+ try {
106
+ collected = await context.turn(() => context.model.invoke({
107
+ input,
108
+ autoExecuteTools: true,
109
+ excludeTools: ["route"]
110
+ }));
111
+ } catch (error) {
112
+ if (context.state.attempt < maxAttempts) {
113
+ return {
114
+ message: "Execution error, retrying.",
115
+ route: "execute"
116
+ };
117
+ }
118
+ return {
119
+ message: "Execution error, no retries remaining.",
120
+ route: "stop"
121
+ };
122
+ }
123
+ if (collected.stopReason === "aborted") {
124
+ return { message: collected.text, route: "stop", toolCalls: collected.toolCalls };
125
+ }
126
+ if (collected.toolCalls.length > 0) {
127
+ return { message: collected.text ?? "", route: "stop", toolCalls: collected.toolCalls };
128
+ }
129
+ return { message: collected.text ?? "", route: "chat" };
130
+ }
131
+ });
132
+ }
133
+
134
+ // src/loop/phases/built-in/plan/index.ts
135
+ function isRecord(value) {
136
+ return typeof value === "object" && value !== null && !Array.isArray(value);
137
+ }
138
+ function normalizeTask(value) {
139
+ if (!isRecord(value)) throw new Error("Expected task to be an object.");
140
+ const status = value.status;
141
+ if (status !== "pending" && status !== "running" && status !== "passed" && status !== "failed") {
142
+ throw new Error("Expected task status to be pending, running, passed, or failed.");
143
+ }
144
+ return {
145
+ id: typeof value.id === "string" ? value.id : "",
146
+ title: typeof value.title === "string" ? value.title : "",
147
+ instruction: typeof value.instruction === "string" ? value.instruction : "",
148
+ acceptanceCriteria: Array.isArray(value.acceptanceCriteria) ? value.acceptanceCriteria.map(
149
+ (c) => typeof c === "string" ? c : isRecord(c) && typeof c.description === "string" ? c.description : String(c)
150
+ ) : [],
151
+ toolNames: Array.isArray(value.toolNames) ? value.toolNames.filter((t) => typeof t === "string") : [],
152
+ skillIds: Array.isArray(value.skillIds) ? value.skillIds.filter((s) => typeof s === "string") : [],
153
+ status,
154
+ attempts: typeof value.attempts === "number" ? value.attempts : 0
155
+ };
156
+ }
157
+ function plan_default(api) {
158
+ api.registerPhase({
159
+ ...api.manifest?.phase,
160
+ id: "plan",
161
+ prompt: {
162
+ instructions: [
163
+ "Phase: plan",
164
+ "",
165
+ "Analyze the user's request and create a task plan.",
166
+ 'Output a JSON object: { "task": { ... }, "message": "explanation" }',
167
+ "Task fields: title, instruction, acceptanceCriteria, toolNames, skillIds, status, attempts.",
168
+ 'Prefer setting task.status to "pending" and task.attempts to 0.',
169
+ "Use toolNames only from the available tools. Use skillIds only from the loaded skills.",
170
+ "After outputting the task JSON, call the 'route' tool to indicate the next phase."
171
+ ]
172
+ },
173
+ async run(context, input) {
174
+ const collected = await context.turn(() => context.model.invoke({ input }));
175
+ let raw;
176
+ try {
177
+ raw = JSON.parse(collected.text);
178
+ } catch {
179
+ return {
180
+ message: "",
181
+ route: "stop",
182
+ toolCalls: collected.toolCalls
183
+ };
184
+ }
185
+ const rawTask = raw?.task ?? raw;
186
+ if (!rawTask) {
187
+ throw new Error("Planner did not produce a structured task.");
188
+ }
189
+ normalizeTask(rawTask);
190
+ const message = raw?.message ?? "";
191
+ return {
192
+ message,
193
+ route: "stop",
194
+ toolCalls: collected.toolCalls
195
+ };
196
+ }
197
+ });
198
+ }
199
+
200
+ // src/loop/phases/built-in/verify/index.ts
201
+ function verify_default(api) {
202
+ api.registerPhase({
203
+ ...api.manifest?.phase,
204
+ id: "verify",
205
+ prompt: {
206
+ instructions: [
207
+ "Phase: verify",
208
+ "",
209
+ "Review the task output against the acceptance criteria.",
210
+ "If the criteria are met, confirm and call the 'route' tool to stop or proceed.",
211
+ "If more work is needed, call tools to fix issues, then call the 'route' tool."
212
+ ]
213
+ },
214
+ async run(context, input) {
215
+ const maxAttempts = context.maxAttempts ?? 2;
216
+ let collected;
217
+ try {
218
+ collected = await context.turn(() => context.model.invoke({ input }));
219
+ } catch (error) {
220
+ if (context.state.attempt < maxAttempts) {
221
+ return {
222
+ message: "Verification error, retrying.",
223
+ route: "execute"
224
+ };
225
+ }
226
+ return {
227
+ message: "Verification error, no retries remaining.",
228
+ route: "stop"
229
+ };
230
+ }
231
+ const nonRouteToolCalls = collected.toolCalls.filter((t) => t.name !== "route");
232
+ if (nonRouteToolCalls.length > 0) {
233
+ return {
234
+ message: collected.text || "Fixing issues.",
235
+ route: "execute",
236
+ toolCalls: collected.toolCalls
237
+ };
238
+ }
239
+ return {
240
+ message: collected.text.trim() || "Verification complete.",
241
+ route: "stop",
242
+ toolCalls: collected.toolCalls
243
+ };
244
+ }
245
+ });
246
+ }
247
+
248
+ // src/loop/phases/built-in/index.ts
249
+ var builtinPhases = [
250
+ chat_default,
251
+ plan_default,
252
+ execute_default,
253
+ verify_default
254
+ ];
255
+
256
+ // src/loop/phases/registry.ts
257
+ function definePhase(definition) {
258
+ return definition;
259
+ }
260
+ function validatePhaseRegistry(registry) {
261
+ if (!registry.entryPhaseId || registry.entryPhaseId.trim().length === 0) {
262
+ throw new Error("Phase registry must have a non-empty entryPhaseId.");
263
+ }
264
+ if (!Array.isArray(registry.phases) || registry.phases.length === 0) {
265
+ throw new Error("Phase registry must include at least one phase definition.");
266
+ }
267
+ const ids = /* @__PURE__ */ new Set();
268
+ for (const phase of registry.phases) {
269
+ if (!phase.id || phase.id.trim().length === 0) {
270
+ throw new Error("Each phase definition must have a non-empty id.");
271
+ }
272
+ if (ids.has(phase.id)) {
273
+ throw new Error(`Duplicate phase id: ${phase.id}`);
274
+ }
275
+ ids.add(phase.id);
276
+ }
277
+ if (!ids.has(registry.entryPhaseId)) {
278
+ throw new Error(`Entry phase id "${registry.entryPhaseId}" is not defined in phases.`);
279
+ }
280
+ }
281
+ function createPhaseRegistry(input) {
282
+ const phases = [...input.phases ?? []];
283
+ const registry = {
284
+ entryPhaseId: input.entryPhaseId ?? phases[0]?.id ?? "",
285
+ phases
286
+ };
287
+ validatePhaseRegistry(registry);
288
+ return registry;
289
+ }
290
+ function resolvePhaseEntry(registry, phaseId) {
291
+ const phase = registry.phases.find((p) => p.id === phaseId);
292
+ if (!phase) {
293
+ throw new Error(`Phase "${phaseId}" is not defined in the phase registry.`);
294
+ }
295
+ return phase;
296
+ }
297
+ function ensurePhaseRegistry(registry) {
298
+ validatePhaseRegistry(registry);
299
+ return registry;
300
+ }
301
+ var DEFAULT_PHASE_ID = process.env.ROWAN_DEFAULT_PHASE ?? "chat";
302
+
303
+ // src/extensions/hooks.ts
304
+ var HookError = class extends Error {
305
+ constructor(eventType, message, cause) {
306
+ super(message, cause === void 0 ? void 0 : { cause });
307
+ this.eventType = eventType;
308
+ this.name = "HookError";
309
+ }
310
+ eventType;
311
+ };
312
+ var HooksManager = class {
313
+ handlers = /* @__PURE__ */ new Map();
314
+ /**
315
+ * Register a hook handler.
316
+ * Handlers execute in registration order.
317
+ *
318
+ * @param eventType - Event type
319
+ * @param handler - Handler function
320
+ */
321
+ on(eventType, handler) {
322
+ const handlers = this.handlers.get(eventType) ?? [];
323
+ handlers.push(handler);
324
+ this.handlers.set(eventType, handlers);
325
+ }
326
+ /**
327
+ * Unregister a hook handler.
328
+ */
329
+ off(eventType, handler) {
330
+ const handlers = this.handlers.get(eventType);
331
+ if (!handlers) return;
332
+ const index = handlers.indexOf(handler);
333
+ if (index >= 0) handlers.splice(index, 1);
334
+ }
335
+ /**
336
+ * Clear all handlers, or clear handlers for specified event type.
337
+ */
338
+ clear(eventType) {
339
+ if (eventType) {
340
+ this.handlers.delete(eventType);
341
+ } else {
342
+ this.handlers.clear();
343
+ }
344
+ }
345
+ /**
346
+ * Check if there are handlers registered for specified event type.
347
+ */
348
+ has(eventType) {
349
+ const handlers = this.handlers.get(eventType);
350
+ return handlers !== void 0 && handlers.length > 0;
351
+ }
352
+ /**
353
+ * Get handler count for specified event type.
354
+ */
355
+ count(eventType) {
356
+ return this.handlers.get(eventType)?.length ?? 0;
357
+ }
358
+ /**
359
+ * Emit event (listen-only, ignore return values).
360
+ * Errors are collected and thrown.
361
+ *
362
+ * @param eventType - Event type
363
+ * @param event - Event object
364
+ */
365
+ async emit(eventType, event) {
366
+ const handlers = this.handlers.get(eventType);
367
+ if (!handlers?.length) return;
368
+ const errors = [];
369
+ await Promise.allSettled(
370
+ handlers.map(async (handler) => {
371
+ try {
372
+ await handler(event);
373
+ } catch (error) {
374
+ errors.push(
375
+ error instanceof Error ? error : new Error(String(error))
376
+ );
377
+ }
378
+ })
379
+ );
380
+ if (errors.length > 0) {
381
+ throw new HookError(
382
+ eventType,
383
+ `${errors.length} handler(s) failed for "${eventType}"`,
384
+ errors[0]
385
+ );
386
+ }
387
+ }
388
+ /**
389
+ * Emit event and collect all non-undefined results.
390
+ *
391
+ * @param eventType - Event type
392
+ * @param event - Event object
393
+ * @returns Array of non-undefined results
394
+ */
395
+ async emitCollect(eventType, event) {
396
+ const handlers = this.handlers.get(eventType);
397
+ if (!handlers?.length) return [];
398
+ const results = [];
399
+ const errors = [];
400
+ for (const handler of handlers) {
401
+ try {
402
+ const result = await handler(event);
403
+ if (result !== void 0) {
404
+ results.push(result);
405
+ }
406
+ } catch (error) {
407
+ errors.push(
408
+ error instanceof Error ? error : new Error(String(error))
409
+ );
410
+ }
411
+ }
412
+ if (errors.length > 0) {
413
+ throw new HookError(
414
+ eventType,
415
+ `${errors.length} handler(s) failed for "${eventType}"`,
416
+ errors[0]
417
+ );
418
+ }
419
+ return results;
420
+ }
421
+ /**
422
+ * Emit event and return first non-undefined result (short-circuit).
423
+ *
424
+ * @param eventType - Event type
425
+ * @param event - Event object
426
+ * @returns First non-undefined result, or undefined
427
+ */
428
+ async emitFirst(eventType, event) {
429
+ const handlers = this.handlers.get(eventType);
430
+ if (!handlers?.length) return void 0;
431
+ for (const handler of handlers) {
432
+ try {
433
+ const result = await handler(event);
434
+ if (result !== void 0) return result;
435
+ } catch (error) {
436
+ throw new HookError(
437
+ eventType,
438
+ `Handler failed for "${eventType}"`,
439
+ error instanceof Error ? error : new Error(String(error))
440
+ );
441
+ }
442
+ }
443
+ return void 0;
444
+ }
445
+ /**
446
+ * Emit event and aggregate results with reducer.
447
+ *
448
+ * @param eventType - Event type
449
+ * @param event - Event object
450
+ * @param reducer - Aggregation function
451
+ * @param initial - Initial value
452
+ * @returns Aggregated result
453
+ */
454
+ async emitReduce(eventType, event, reducer, initial) {
455
+ const results = await this.emitCollect(eventType, event);
456
+ return results.reduce(reducer, initial);
457
+ }
458
+ };
459
+ var _globalHooks;
460
+ function getGlobalHooks() {
461
+ _globalHooks ??= new HooksManager();
462
+ return _globalHooks;
463
+ }
464
+ function resetGlobalHooks() {
465
+ _globalHooks = void 0;
466
+ }
467
+
468
+ // src/extensions/runner.ts
469
+ import { execFile } from "child_process";
470
+
471
+ // src/harness/context/section-formatter.ts
472
+ function escapeXml(value) {
473
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
474
+ }
475
+ function buildStructuredSection(tag, items) {
476
+ const lines = [];
477
+ for (const item of items) {
478
+ lines.push(` <${tag}>`);
479
+ for (const [key, value] of Object.entries(item)) {
480
+ lines.push(` <${key}>${escapeXml(value)}</${key}>`);
481
+ }
482
+ lines.push(` </${tag}>`);
483
+ }
484
+ return lines.join("\n");
485
+ }
486
+ function buildSkillsDescription(skills) {
487
+ const lines = ["<available_skills>"];
488
+ for (const skill of skills) {
489
+ lines.push(" <skill>");
490
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
491
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
492
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
493
+ lines.push(" </skill>");
494
+ }
495
+ lines.push("</available_skills>");
496
+ return lines.join("\n");
497
+ }
498
+
499
+ // src/harness/context/system-prompt.ts
500
+ function buildSystemPrompt(options) {
501
+ const { systemPrompt, promptGuidelines, appendSystemPrompt, tools, skills, cwd } = options;
502
+ const date = createTimestamp();
503
+ const guidelinesList = [];
504
+ const guidelinesSet = /* @__PURE__ */ new Set();
505
+ const addGuideline = (guideline) => {
506
+ const normalized = guideline.trim();
507
+ if (normalized.length === 0 || guidelinesSet.has(normalized)) return;
508
+ guidelinesSet.add(normalized);
509
+ guidelinesList.push(normalized);
510
+ };
511
+ const visibleTools = (tools ?? []).filter((t) => !!t.promptSnippet);
512
+ const toolsList = visibleTools.length > 0 ? visibleTools.map((t) => `- ${t.name}: ${t.promptSnippet}`).join("\n") : "(none)";
513
+ for (const tool of tools ?? []) {
514
+ for (const g of tool.promptGuidelines ?? []) {
515
+ addGuideline(g);
516
+ }
517
+ }
518
+ const skillsBlock = skills && skills.length > 0 ? buildSkillsDescription(skills) : "";
519
+ for (const g of promptGuidelines ?? []) {
520
+ addGuideline(g);
521
+ }
522
+ const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
523
+ let prompt = `${systemPrompt}
524
+
525
+ **Important:** Tool and skill availability varies by phase. Only use tools that are available in your current phase context.
526
+
527
+ Available tools:
528
+ ${toolsList}
529
+
530
+ Guidelines:
531
+ ${guidelines}`;
532
+ if (skillsBlock) {
533
+ prompt += `
534
+
535
+ ${skillsBlock}`;
536
+ }
537
+ if (date || cwd) {
538
+ const contextParts = [];
539
+ if (date) contextParts.push(`Current date: ${date}`);
540
+ if (cwd) contextParts.push(`Working directory: ${cwd}`);
541
+ prompt += `
542
+
543
+ ${contextParts.join("\n")}`;
544
+ }
545
+ if (appendSystemPrompt) {
546
+ prompt += `
547
+
548
+ ${appendSystemPrompt}`;
549
+ }
550
+ return prompt;
551
+ }
552
+
553
+ // src/harness/context/prompt-builder.ts
554
+ function serializeSkills(skills) {
555
+ return skills.map((skill) => ({
556
+ name: skill.name,
557
+ description: skill.description,
558
+ filePath: skill.filePath
559
+ }));
560
+ }
561
+ function latestUserInput(input) {
562
+ for (let index = input.messages.length - 1; index >= 0; index -= 1) {
563
+ const message = input.messages[index];
564
+ if (message.role === "user") {
565
+ return message.content;
566
+ }
567
+ }
568
+ return "";
569
+ }
570
+ function conversationMessages(messages) {
571
+ return messages.flatMap((message) => {
572
+ if (message.role === "user") {
573
+ return [{ role: "user", content: message.content }];
574
+ }
575
+ if (message.role === "assistant") {
576
+ const toolCalls = message.metadata?.toolCalls;
577
+ if (!toolCalls?.length) {
578
+ return [{ role: "assistant", content: message.content }];
579
+ }
580
+ }
581
+ if (message.role === "assistant" && Array.isArray(message.metadata?.toolCalls)) {
582
+ const toolCalls = message.metadata.toolCalls;
583
+ const content = [];
584
+ if (message.content) {
585
+ content.push({ type: "text", text: message.content });
586
+ }
587
+ for (const tc of toolCalls) {
588
+ content.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.args });
589
+ }
590
+ return [{ role: "assistant", content }];
591
+ }
592
+ if (message.role === "tool") {
593
+ const toolCallId = message.metadata?.toolCallId ?? "";
594
+ const isError = message.metadata?.isError;
595
+ const content = [
596
+ { type: "tool_result", toolUseId: toolCallId, content: message.content, isError }
597
+ ];
598
+ return [{ role: "tool", content }];
599
+ }
600
+ return [];
601
+ });
602
+ }
603
+ function buildModelRequest(input, options) {
604
+ const toolMeta = input.tools.map((t) => ({
605
+ name: t.name,
606
+ description: t.description,
607
+ promptSnippet: t.promptSnippet,
608
+ promptGuidelines: t.promptGuidelines
609
+ }));
610
+ let systemText = buildSystemPrompt({
611
+ systemPrompt: input.systemPrompt,
612
+ tools: toolMeta,
613
+ skills: input.skills.length > 0 ? serializeSkills(input.skills) : void 0,
614
+ promptGuidelines: input.promptGuidelines,
615
+ appendSystemPrompt: input.appendSystemPrompt
616
+ });
617
+ const messages = [...conversationMessages(input.messages)];
618
+ const modelTools = (input.phaseTools ?? input.tools).map((t) => ({
619
+ name: t.name,
620
+ description: t.description,
621
+ parameters: t.parameters
622
+ }));
623
+ return {
624
+ model: options?.model ?? { provider: "", name: "" },
625
+ system: systemText,
626
+ messages,
627
+ tools: modelTools.length > 0 ? modelTools : void 0
628
+ };
629
+ }
630
+
631
+ // src/extensions/runner.ts
632
+ import {
633
+ registerModel,
634
+ unregisterProviderModels,
635
+ registerApiProvider
636
+ } from "@rowan-agent/models";
637
+
638
+ // src/extensions/source-info.ts
639
+ function createSourceInfo(extensionPath, options = {}) {
640
+ const source = options.source ?? (extensionPath.startsWith("<") ? "synthetic" : "local");
641
+ const displayName = extensionPath.startsWith("<") ? extensionPath.slice(1, -1) : extensionPath.split("/").pop() ?? extensionPath;
642
+ return {
643
+ source,
644
+ baseDir: options.baseDir,
645
+ displayName
646
+ };
647
+ }
648
+
649
+ // src/extensions/types.ts
650
+ function createExtension(extensionPath, resolvedPath, sourceInfo) {
651
+ return {
652
+ path: extensionPath,
653
+ resolvedPath,
654
+ sourceInfo,
655
+ handlers: /* @__PURE__ */ new Map(),
656
+ tools: /* @__PURE__ */ new Map(),
657
+ phases: /* @__PURE__ */ new Map()
658
+ };
659
+ }
660
+ function createExtensionRuntime() {
661
+ const state = {};
662
+ const assertActive = () => {
663
+ if (state.staleMessage) {
664
+ throw new Error(state.staleMessage);
665
+ }
666
+ };
667
+ const runtime = {
668
+ assertActive,
669
+ invalidate: (message) => {
670
+ state.staleMessage ??= message ?? "This extension context is stale after session replacement or reload. Do not use a captured extension API after the runner has been replaced.";
671
+ },
672
+ pendingProviderRegistrations: [],
673
+ // Pre-bind: queue registrations so bind() can flush them once the
674
+ // model registry is available. bind() replaces both with direct calls.
675
+ registerProvider: (name, config, extensionPath = "<unknown>") => {
676
+ runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
677
+ },
678
+ unregisterProvider: (name) => {
679
+ runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter(
680
+ (r) => r.name !== name
681
+ );
682
+ }
683
+ };
684
+ return runtime;
685
+ }
686
+
687
+ // src/extensions/event-bus.ts
688
+ function createEventBus() {
689
+ const listeners = /* @__PURE__ */ new Map();
690
+ return {
691
+ on(event, listener) {
692
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
693
+ set.add(listener);
694
+ listeners.set(event, set);
695
+ return () => {
696
+ set.delete(listener);
697
+ if (set.size === 0) listeners.delete(event);
698
+ };
699
+ },
700
+ emit(event, ...args) {
701
+ const set = listeners.get(event);
702
+ if (!set) return;
703
+ for (const listener of set) {
704
+ try {
705
+ listener(...args);
706
+ } catch (err) {
707
+ console.error(`[event-bus] Listener error for "${event}":`, err);
708
+ }
709
+ }
710
+ },
711
+ off(event) {
712
+ if (event) {
713
+ listeners.delete(event);
714
+ } else {
715
+ listeners.clear();
716
+ }
717
+ },
718
+ has(event) {
719
+ const set = listeners.get(event);
720
+ return set !== void 0 && set.size > 0;
721
+ },
722
+ count(event) {
723
+ return listeners.get(event)?.size ?? 0;
724
+ }
725
+ };
726
+ }
727
+
728
+ // src/extensions/context.ts
729
+ function createExtensionAPI(hooks, _extensionPath, options, runtime, eventBus) {
730
+ let idCounter = 0;
731
+ const createId3 = (prefix) => {
732
+ idCounter++;
733
+ return `${prefix}_${Date.now().toString(36)}_${idCounter}`;
734
+ };
735
+ const formatJson = (value) => {
736
+ try {
737
+ return JSON.stringify(value, null, 2) ?? "undefined";
738
+ } catch {
739
+ return "[unserializable]";
740
+ }
741
+ };
742
+ const createPromptBuilder = (instructions) => {
743
+ return (input) => {
744
+ const req = buildModelRequest(input);
745
+ if (instructions.length > 0) {
746
+ req.messages.push({
747
+ role: "user",
748
+ content: instructions.join("\n")
749
+ });
750
+ }
751
+ return req;
752
+ };
753
+ };
754
+ return {
755
+ on: (eventType, handler) => {
756
+ runtime.assertActive();
757
+ hooks.on(eventType, handler);
758
+ },
759
+ off: (eventType, handler) => {
760
+ runtime.assertActive();
761
+ hooks.off(eventType, handler);
762
+ },
763
+ registerTool: (tool) => {
764
+ runtime.assertActive();
765
+ options.registerTool(tool);
766
+ },
767
+ registerPhase: (registration) => {
768
+ runtime.assertActive();
769
+ options.registerPhase(registration);
770
+ },
771
+ registerProvider: (config) => {
772
+ runtime.assertActive();
773
+ options.registerProvider(config);
774
+ },
775
+ unregisterProvider: (name) => {
776
+ runtime.assertActive();
777
+ options.unregisterProvider(name);
778
+ },
779
+ manifest: options.manifest,
780
+ utils: {
781
+ createId: createId3,
782
+ formatJson,
783
+ buildModelRequest,
784
+ createPromptBuilder
785
+ },
786
+ context: options.context,
787
+ events: eventBus
788
+ };
789
+ }
790
+
791
+ // src/extensions/runner.ts
792
+ async function execCommand(command, args, cwd, options) {
793
+ return new Promise((resolve7, reject) => {
794
+ const child = execFile(
795
+ command,
796
+ args,
797
+ {
798
+ cwd: options?.cwd ?? cwd,
799
+ env: options?.env ? { ...process.env, ...options.env } : void 0,
800
+ timeout: options?.timeout,
801
+ maxBuffer: 10 * 1024 * 1024
802
+ },
803
+ (error, stdout, stderr) => {
804
+ if (error && error.killed && options?.signal?.aborted) {
805
+ reject(new Error("Command was aborted"));
806
+ return;
807
+ }
808
+ resolve7({
809
+ exitCode: typeof error?.code === "number" ? error.code : error ? 1 : 0,
810
+ stdout: stdout ?? "",
811
+ stderr: stderr ?? ""
812
+ });
813
+ }
814
+ );
815
+ if (options?.signal) {
816
+ options.signal.addEventListener(
817
+ "abort",
818
+ () => {
819
+ child.kill("SIGTERM");
820
+ },
821
+ { once: true }
822
+ );
823
+ }
824
+ });
825
+ }
826
+ function applyProviderRegistration(config) {
827
+ if (config.streamSimple) {
828
+ registerApiProvider({ api: config.api, stream: config.streamSimple });
829
+ }
830
+ for (const modelConfig of config.models) {
831
+ registerModel({
832
+ id: modelConfig.id,
833
+ name: modelConfig.name,
834
+ api: config.api,
835
+ provider: config.name,
836
+ baseUrl: config.baseUrl,
837
+ reasoning: modelConfig.reasoning,
838
+ input: modelConfig.input,
839
+ cost: modelConfig.cost,
840
+ contextWindow: modelConfig.contextWindow,
841
+ maxTokens: modelConfig.maxTokens,
842
+ ...config.headers ? { headers: config.headers } : {}
843
+ });
844
+ }
845
+ }
846
+ function applyProviderUnregistration(name) {
847
+ unregisterProviderModels(name);
848
+ }
849
+ var ExtensionRunner = class {
850
+ hooks;
851
+ runtime;
852
+ events;
853
+ validatePhaseOverride;
854
+ cwd;
855
+ abortController = new AbortController();
856
+ _idle = true;
857
+ // Per-extension tracking
858
+ extensions = [];
859
+ // Phase management
860
+ phases = /* @__PURE__ */ new Map();
861
+ _phaseCache = null;
862
+ // Provider management
863
+ pendingProviders = [];
864
+ bound = false;
865
+ // Error listeners
866
+ errorListeners = /* @__PURE__ */ new Set();
867
+ // Loaded extension metadata (pre-initialization form)
868
+ loadedExtensions = [];
869
+ constructor(options) {
870
+ this.hooks = new HooksManager();
871
+ this.runtime = createExtensionRuntime();
872
+ this.events = createEventBus();
873
+ this.validatePhaseOverride = options?.validatePhaseOverride;
874
+ this.cwd = options?.cwd ?? process.cwd();
875
+ }
876
+ /** Whether the agent is currently idle (not streaming). */
877
+ get isIdle() {
878
+ return this._idle;
879
+ }
880
+ /** Set idle state — called by the agent loop. */
881
+ setIdle(idle) {
882
+ this._idle = idle;
883
+ }
884
+ /** Abort signal for the current runner instance. */
885
+ get signal() {
886
+ return this.abortController.signal;
887
+ }
888
+ /** Abort the current runner operation. */
889
+ abort() {
890
+ this.abortController.abort();
891
+ }
892
+ // ---------------------------------------------------------------------------
893
+ // Error handling
894
+ // ---------------------------------------------------------------------------
895
+ /**
896
+ * Register an error listener.
897
+ * Returns an unsubscribe function.
898
+ */
899
+ onError(listener) {
900
+ this.errorListeners.add(listener);
901
+ return () => this.errorListeners.delete(listener);
902
+ }
903
+ /**
904
+ * Emit a structured extension error to all listeners.
905
+ */
906
+ emitError(error) {
907
+ for (const listener of this.errorListeners) {
908
+ try {
909
+ listener(error);
910
+ } catch (err) {
911
+ console.error("[extension-runner] Error listener failed:", err);
912
+ }
913
+ }
914
+ }
915
+ // ---------------------------------------------------------------------------
916
+ // Lifecycle: invalidate / assertActive
917
+ // ---------------------------------------------------------------------------
918
+ /**
919
+ * Mark all extension contexts as stale.
920
+ * After calling this, any captured ExtensionAPI or ExtensionContext will throw
921
+ * on use. Used during session replacement or reload.
922
+ */
923
+ invalidate(message) {
924
+ this.runtime.invalidate(message);
925
+ }
926
+ // ---------------------------------------------------------------------------
927
+ // Direct hook API
928
+ // ---------------------------------------------------------------------------
929
+ /**
930
+ * Subscribe to a specific hook event type.
931
+ * Returns an unsubscribe function.
932
+ *
933
+ * @example
934
+ * ```ts
935
+ * const unsub = runner.on("before_tool_call", (event) => {
936
+ * return { allow: false, reason: "Blocked" };
937
+ * });
938
+ * unsub(); // Cancel subscription
939
+ * ```
940
+ */
941
+ on(type, handler) {
942
+ this.hooks.on(type, handler);
943
+ return () => this.hooks.off(type, handler);
944
+ }
945
+ /**
946
+ * Subscribe to all events (read-only).
947
+ * Returns an unsubscribe function.
948
+ *
949
+ * @example
950
+ * ```ts
951
+ * const unsub = runner.subscribe((event) => {
952
+ * console.log(event.type);
953
+ * });
954
+ * ```
955
+ */
956
+ subscribe(listener) {
957
+ const handlers = /* @__PURE__ */ new Map();
958
+ for (const eventType of this.getAllEventTypes()) {
959
+ const handler = (event) => listener(event);
960
+ handlers.set(eventType, handler);
961
+ this.hooks.on(eventType, handler);
962
+ }
963
+ return () => {
964
+ for (const [eventType, handler] of handlers) {
965
+ this.hooks.off(eventType, handler);
966
+ }
967
+ };
968
+ }
969
+ getAllEventTypes() {
970
+ return [
971
+ "before_phase",
972
+ "after_phase",
973
+ "before_prompt",
974
+ "before_tool_call",
975
+ "after_tool_call",
976
+ "agent_start",
977
+ "agent_end",
978
+ "turn_start",
979
+ "turn_end",
980
+ "message_start",
981
+ "message_update",
982
+ "message_end",
983
+ "tool_execution_start",
984
+ "tool_execution_update",
985
+ "tool_execution_end",
986
+ "queue_update",
987
+ "save_point",
988
+ "abort",
989
+ "settled"
990
+ ];
991
+ }
992
+ // ---------------------------------------------------------------------------
993
+ // Extension loading
994
+ // ---------------------------------------------------------------------------
995
+ /**
996
+ * Load and initialize extensions.
997
+ * Creates Extension tracking objects and calls each factory with an ExtensionAPI.
998
+ */
999
+ async loadExtensions(extensions) {
1000
+ for (const ext of extensions) {
1001
+ try {
1002
+ const sourceInfo = createSourceInfo(ext.path, {
1003
+ source: ext.path.startsWith("<builtin:") ? "builtin" : "local",
1004
+ baseDir: ext.resolvedPath.startsWith("<") ? void 0 : ext.resolvedPath
1005
+ });
1006
+ const extension = createExtension(ext.path, ext.resolvedPath, sourceInfo);
1007
+ const api = this.createExtensionAPI(extension, ext.manifest);
1008
+ await ext.factory(api);
1009
+ this.extensions.push(extension);
1010
+ this.loadedExtensions.push(ext);
1011
+ this._phaseCache = null;
1012
+ } catch (error) {
1013
+ const message = error instanceof Error ? error.message : String(error);
1014
+ this.emitError({
1015
+ extensionPath: ext.path,
1016
+ event: "load",
1017
+ error: message,
1018
+ stack: error instanceof Error ? error.stack : void 0
1019
+ });
1020
+ throw error;
1021
+ }
1022
+ }
1023
+ }
1024
+ // ---------------------------------------------------------------------------
1025
+ // Tool management
1026
+ // ---------------------------------------------------------------------------
1027
+ /**
1028
+ * Get all registered tools from all extensions (first registration per name wins).
1029
+ */
1030
+ getAllRegisteredTools() {
1031
+ const toolsByName = /* @__PURE__ */ new Map();
1032
+ for (const ext of this.extensions) {
1033
+ for (const tool of ext.tools.values()) {
1034
+ if (!toolsByName.has(tool.definition.name)) {
1035
+ toolsByName.set(tool.definition.name, tool);
1036
+ }
1037
+ }
1038
+ }
1039
+ return Array.from(toolsByName.values());
1040
+ }
1041
+ /**
1042
+ * Get a tool definition by name. Returns undefined if not found.
1043
+ */
1044
+ getToolDefinition(toolName) {
1045
+ for (const ext of this.extensions) {
1046
+ const tool = ext.tools.get(toolName);
1047
+ if (tool) return tool.definition;
1048
+ }
1049
+ return void 0;
1050
+ }
1051
+ // ---------------------------------------------------------------------------
1052
+ // Handler queries
1053
+ // ---------------------------------------------------------------------------
1054
+ /**
1055
+ * Check if there are handlers registered for specified event type.
1056
+ */
1057
+ hasHandlers(eventType) {
1058
+ return this.hooks.has(eventType);
1059
+ }
1060
+ /**
1061
+ * Get the number of handlers for specified event type.
1062
+ */
1063
+ handlerCount(eventType) {
1064
+ return this.hooks.count(eventType);
1065
+ }
1066
+ // ---------------------------------------------------------------------------
1067
+ // Phase management
1068
+ // ---------------------------------------------------------------------------
1069
+ getPhase(id) {
1070
+ return this.getRegisteredPhase(id)?.definition;
1071
+ }
1072
+ getPhases() {
1073
+ return [...this.collectRegisteredPhases().values()].map(
1074
+ (p) => p.definition
1075
+ );
1076
+ }
1077
+ getPhaseHandler(id) {
1078
+ return this.getRegisteredPhase(id)?.handler;
1079
+ }
1080
+ createPhaseRegistry(input = {}) {
1081
+ const registered = this.collectRegisteredPhases();
1082
+ return createPhaseRegistry({
1083
+ entryPhaseId: input.entryPhaseId,
1084
+ phases: [...registered.values()].map((p) => p.definition)
1085
+ });
1086
+ }
1087
+ // ---------------------------------------------------------------------------
1088
+ // Lifecycle
1089
+ // ---------------------------------------------------------------------------
1090
+ /**
1091
+ * Bind the runner — flushes pending provider registrations and
1092
+ * replaces runtime stubs with real implementations.
1093
+ */
1094
+ bind() {
1095
+ if (this.bound) return;
1096
+ this.bound = true;
1097
+ this.flushPendingProviders();
1098
+ this.runtime.registerProvider = (_name, config) => {
1099
+ applyProviderRegistration(config);
1100
+ };
1101
+ this.runtime.unregisterProvider = (_name) => {
1102
+ applyProviderUnregistration(_name);
1103
+ };
1104
+ }
1105
+ // ---------------------------------------------------------------------------
1106
+ // Unified hook emission
1107
+ // ---------------------------------------------------------------------------
1108
+ /**
1109
+ * Generic emit — fire-and-forget for any event type.
1110
+ */
1111
+ async emit(eventType, event) {
1112
+ await this.hooks.emit(eventType, event);
1113
+ }
1114
+ /**
1115
+ * Unified hook emission — returns the first non-undefined result.
1116
+ */
1117
+ async emitHook(type, event) {
1118
+ return this.hooks.emitFirst(type, event);
1119
+ }
1120
+ // ---------------------------------------------------------------------------
1121
+ // Phase hooks (with inline processing)
1122
+ // ---------------------------------------------------------------------------
1123
+ async emitBeforePhase(phaseId, input) {
1124
+ const result = await this.emitHook("before_phase", {
1125
+ type: "before_phase",
1126
+ phaseId,
1127
+ input
1128
+ });
1129
+ return result ?? {};
1130
+ }
1131
+ async emitAfterPhase(phaseId, output) {
1132
+ const result = await this.emitHook("after_phase", {
1133
+ type: "after_phase",
1134
+ phaseId,
1135
+ output
1136
+ });
1137
+ return result ?? {};
1138
+ }
1139
+ async emitBeforePrompt(phaseId, input) {
1140
+ const result = await this.emitHook("before_prompt", {
1141
+ type: "before_prompt",
1142
+ phaseId,
1143
+ input
1144
+ });
1145
+ return result?.input ?? input;
1146
+ }
1147
+ async emitBeforeToolCall(tool, args) {
1148
+ const result = await this.emitHook("before_tool_call", {
1149
+ type: "before_tool_call",
1150
+ tool,
1151
+ args
1152
+ });
1153
+ return result ?? { allow: true };
1154
+ }
1155
+ async emitAfterToolCall(tool, result) {
1156
+ const hookResult = await this.emitHook("after_tool_call", {
1157
+ type: "after_tool_call",
1158
+ tool,
1159
+ result
1160
+ });
1161
+ return hookResult?.result ?? result;
1162
+ }
1163
+ // ---------------------------------------------------------------------------
1164
+ // Agent event hooks (fire-and-forget)
1165
+ // ---------------------------------------------------------------------------
1166
+ async emitAgentStart(sessionId) {
1167
+ await this.hooks.emit("agent_start", { type: "agent_start", sessionId });
1168
+ }
1169
+ async emitAgentEnd(sessionId, outcome, messages) {
1170
+ await this.hooks.emit("agent_end", { type: "agent_end", sessionId, outcome, messages });
1171
+ }
1172
+ async emitTurnStart(messages) {
1173
+ await this.hooks.emit("turn_start", { type: "turn_start", messages });
1174
+ }
1175
+ async emitTurnEnd(messages, outcome) {
1176
+ await this.hooks.emit("turn_end", { type: "turn_end", messages, outcome });
1177
+ }
1178
+ async emitMessageStart(message) {
1179
+ await this.hooks.emit("message_start", { type: "message_start", message });
1180
+ }
1181
+ async emitMessageUpdate(message, delta) {
1182
+ await this.hooks.emit("message_update", { type: "message_update", message, delta });
1183
+ }
1184
+ async emitMessageEnd(message) {
1185
+ await this.hooks.emit("message_end", { type: "message_end", message });
1186
+ }
1187
+ async emitToolExecutionStart(toolCallId, toolName, args) {
1188
+ await this.hooks.emit("tool_execution_start", {
1189
+ type: "tool_execution_start",
1190
+ toolCallId,
1191
+ toolName,
1192
+ args
1193
+ });
1194
+ }
1195
+ async emitToolExecutionUpdate(toolCallId, toolName, _progress) {
1196
+ await this.hooks.emit("tool_execution_update", {
1197
+ type: "tool_execution_update",
1198
+ toolCallId,
1199
+ toolName
1200
+ });
1201
+ }
1202
+ async emitToolExecutionEnd(toolCallId, toolName, result) {
1203
+ await this.hooks.emit("tool_execution_end", {
1204
+ type: "tool_execution_end",
1205
+ toolCallId,
1206
+ toolName,
1207
+ result
1208
+ });
1209
+ }
1210
+ async emitSavePoint(hadPendingMutations) {
1211
+ await this.hooks.emit("save_point", { type: "save_point", hadPendingMutations });
1212
+ }
1213
+ async emitAbort(reason) {
1214
+ await this.hooks.emit("abort", { type: "abort", reason });
1215
+ }
1216
+ async emitSettled() {
1217
+ await this.hooks.emit("settled", { type: "settled" });
1218
+ }
1219
+ // ---------------------------------------------------------------------------
1220
+ // AgentEvent bridge (backward compatibility)
1221
+ // ---------------------------------------------------------------------------
1222
+ /**
1223
+ * Emit an AgentEvent by routing to the appropriate typed hook.
1224
+ */
1225
+ async emitAgentEvent(event) {
1226
+ switch (event.type) {
1227
+ case "agent_start":
1228
+ await this.emitAgentStart(event.sessionId);
1229
+ break;
1230
+ case "agent_end":
1231
+ await this.emitAgentEnd(event.sessionId, event.outcome, event.messages);
1232
+ break;
1233
+ case "turn_start":
1234
+ await this.emitTurnStart(event.messages);
1235
+ break;
1236
+ case "turn_end":
1237
+ await this.emitTurnEnd(event.messages, event.outcome);
1238
+ break;
1239
+ case "message_start":
1240
+ await this.emitMessageStart(event.message);
1241
+ break;
1242
+ case "message_update":
1243
+ await this.emitMessageUpdate(event.message, event.delta);
1244
+ break;
1245
+ case "message_end":
1246
+ await this.emitMessageEnd(event.message);
1247
+ break;
1248
+ case "tool_execution_start":
1249
+ await this.emitToolExecutionStart(event.toolCallId, event.toolName, event.args);
1250
+ break;
1251
+ case "tool_execution_update":
1252
+ await this.emitToolExecutionUpdate(event.toolCallId, event.toolName);
1253
+ break;
1254
+ case "tool_execution_end":
1255
+ await this.emitToolExecutionEnd(event.toolCallId, event.toolName, event.result);
1256
+ break;
1257
+ }
1258
+ }
1259
+ // ---------------------------------------------------------------------------
1260
+ // Internal helpers
1261
+ // ---------------------------------------------------------------------------
1262
+ /**
1263
+ * Create an ExtensionAPI for a specific extension.
1264
+ * Registration methods write to the extension tracking object.
1265
+ * Action methods delegate to the shared runtime.
1266
+ */
1267
+ createExtensionAPI(extension, manifest) {
1268
+ const runner = this;
1269
+ const extContext = {
1270
+ get cwd() {
1271
+ return runner.cwd;
1272
+ },
1273
+ get signal() {
1274
+ return runner.abortController.signal;
1275
+ },
1276
+ isIdle() {
1277
+ return runner._idle;
1278
+ },
1279
+ abort() {
1280
+ runner.abortController.abort();
1281
+ },
1282
+ exec(command, args, options) {
1283
+ return execCommand(command, args, runner.cwd, options);
1284
+ },
1285
+ manifest
1286
+ };
1287
+ return createExtensionAPI(this.hooks, extension.path, {
1288
+ registerPhase: (registration) => this.registerPhase(extension, registration),
1289
+ registerProvider: (config) => this.registerProvider(config),
1290
+ unregisterProvider: (name) => this.unregisterProvider(name),
1291
+ registerTool: (tool) => this.registerTool(extension, tool),
1292
+ context: extContext,
1293
+ manifest
1294
+ }, this.runtime, this.events);
1295
+ }
1296
+ registerTool(extension, tool) {
1297
+ for (const ext of this.extensions) {
1298
+ if (ext.tools.has(tool.name)) {
1299
+ this.emitError({
1300
+ extensionPath: extension.path,
1301
+ event: "register_tool",
1302
+ error: `Tool "${tool.name}" is already registered by extension ${ext.path}`
1303
+ });
1304
+ return;
1305
+ }
1306
+ }
1307
+ const sourceInfo = createSourceInfo(extension.path);
1308
+ extension.tools.set(tool.name, {
1309
+ definition: tool,
1310
+ sourceInfo
1311
+ });
1312
+ }
1313
+ registerPhase(extension, registration) {
1314
+ if (!registration.id) {
1315
+ throw new Error(`Phase registration requires an "id" field.`);
1316
+ }
1317
+ if (this.validatePhaseOverride?.(registration.id, extension.path)) {
1318
+ throw new Error(
1319
+ `External extension cannot override built-in phase: ${registration.id}`
1320
+ );
1321
+ }
1322
+ if (this.phases.has(registration.id)) {
1323
+ throw new Error(`Duplicate phase id: ${registration.id}`);
1324
+ }
1325
+ let buildPrompt = registration.buildPrompt;
1326
+ if (!buildPrompt) {
1327
+ const promptConfig = registration.prompt;
1328
+ if (promptConfig?.instructions?.length) {
1329
+ buildPrompt = (input) => {
1330
+ const req = buildModelRequest(input);
1331
+ req.messages.push({
1332
+ role: "user",
1333
+ content: promptConfig.instructions.join("\n")
1334
+ });
1335
+ return req;
1336
+ };
1337
+ } else {
1338
+ buildPrompt = (input) => buildModelRequest(input);
1339
+ }
1340
+ }
1341
+ const definition = {
1342
+ id: registration.id,
1343
+ name: registration.name ?? registration.id,
1344
+ description: registration.description ?? "",
1345
+ run: registration.run,
1346
+ buildPrompt
1347
+ };
1348
+ this.phases.set(registration.id, {
1349
+ definition,
1350
+ handler: { buildPrompt },
1351
+ source: { extensionPath: extension.path }
1352
+ });
1353
+ extension.phases.set(registration.id, {
1354
+ definition,
1355
+ handler: { buildPrompt },
1356
+ source: { extensionPath: extension.path }
1357
+ });
1358
+ this._phaseCache = null;
1359
+ }
1360
+ registerProvider(config) {
1361
+ if (this.bound) {
1362
+ applyProviderRegistration(config);
1363
+ } else {
1364
+ this.pendingProviders.push({ kind: "register", config });
1365
+ }
1366
+ }
1367
+ unregisterProvider(name) {
1368
+ if (this.bound) {
1369
+ applyProviderUnregistration(name);
1370
+ } else {
1371
+ this.pendingProviders.push({ kind: "unregister", name });
1372
+ }
1373
+ }
1374
+ flushPendingProviders() {
1375
+ for (const action of this.pendingProviders) {
1376
+ if (action.kind === "register") {
1377
+ applyProviderRegistration(action.config);
1378
+ } else {
1379
+ applyProviderUnregistration(action.name);
1380
+ }
1381
+ }
1382
+ this.pendingProviders.length = 0;
1383
+ }
1384
+ getRegisteredPhase(id) {
1385
+ return this.collectRegisteredPhases().get(id);
1386
+ }
1387
+ collectRegisteredPhases() {
1388
+ if (this._phaseCache) return this._phaseCache;
1389
+ this._phaseCache = new Map(this.phases);
1390
+ return this._phaseCache;
1391
+ }
1392
+ };
1393
+ function createExtensionRunner(options) {
1394
+ return new ExtensionRunner(options);
1395
+ }
1396
+
1397
+ // src/extensions/builtin.ts
1398
+ import { resolve as resolve2, dirname as dirname2 } from "path";
1399
+ import { fileURLToPath as fileURLToPath2 } from "url";
1400
+
1401
+ // src/extensions/loader.ts
1402
+ import { existsSync, readFileSync } from "fs";
1403
+ import { readdir, readFile, stat } from "fs/promises";
1404
+ import { fileURLToPath } from "url";
1405
+ import { dirname, extname, join, resolve } from "path";
1406
+ import { createJiti } from "jiti";
1407
+ var ROWAN_DIR = ".rowan";
1408
+ var EXTENSIONS_DIR = "extensions";
1409
+ function isExtensionFile(path) {
1410
+ const extension = extname(path).toLowerCase();
1411
+ return extension === ".ts" || extension === ".js";
1412
+ }
1413
+ function isSyntheticPath(path) {
1414
+ return path.startsWith("<") && path.endsWith(">");
1415
+ }
1416
+ function readManifestSync(dir) {
1417
+ const manifestPath = join(dir, "package.json");
1418
+ if (!existsSync(manifestPath)) {
1419
+ return void 0;
1420
+ }
1421
+ try {
1422
+ const content = readFileSync(manifestPath, "utf8");
1423
+ const pkg = JSON.parse(content);
1424
+ const rowan = pkg.rowan;
1425
+ if (!rowan) return void 0;
1426
+ return {
1427
+ entry: rowan.extensions?.[0],
1428
+ phase: rowan.phase
1429
+ };
1430
+ } catch {
1431
+ return void 0;
1432
+ }
1433
+ }
1434
+ function jitiAliases() {
1435
+ return {
1436
+ "@rowan-agent/agent": fileURLToPath(new URL("../index.ts", import.meta.url)),
1437
+ "@rowan-agent/models": fileURLToPath(new URL("../../../models/src/index.ts", import.meta.url))
1438
+ };
1439
+ }
1440
+ var sharedJiti;
1441
+ function getJiti() {
1442
+ sharedJiti ??= createJiti(import.meta.url, {
1443
+ moduleCache: false,
1444
+ alias: jitiAliases()
1445
+ });
1446
+ return sharedJiti;
1447
+ }
1448
+ async function loadExtensionModule(extensionPath) {
1449
+ const jiti = getJiti();
1450
+ const module = await jiti.import(extensionPath, { default: true });
1451
+ return typeof module === "function" ? module : void 0;
1452
+ }
1453
+ async function readPackageManifest(path) {
1454
+ if (!existsSync(path)) {
1455
+ return void 0;
1456
+ }
1457
+ const content = await readFile(path, "utf8");
1458
+ return JSON.parse(content);
1459
+ }
1460
+ async function resolveExtensionEntries(dir) {
1461
+ const manifest = await readPackageManifest(join(dir, "package.json"));
1462
+ const declared = manifest?.rowan?.extensions;
1463
+ if (declared && declared.length > 0) {
1464
+ return declared.map((entry) => resolve(dir, entry));
1465
+ }
1466
+ for (const name of ["index.ts", "index.js"]) {
1467
+ const entry = join(dir, name);
1468
+ if (existsSync(entry)) {
1469
+ return [entry];
1470
+ }
1471
+ }
1472
+ return void 0;
1473
+ }
1474
+ async function discoverExtensionsInDir(dir) {
1475
+ const entries = await readdir(dir, { withFileTypes: true }).catch((error) => {
1476
+ if (error.code === "ENOENT") {
1477
+ return [];
1478
+ }
1479
+ throw error;
1480
+ });
1481
+ const paths = [];
1482
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
1483
+ const entryPath = join(dir, entry.name);
1484
+ if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
1485
+ paths.push(entryPath);
1486
+ continue;
1487
+ }
1488
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
1489
+ const info = entry.isSymbolicLink() ? await stat(entryPath).catch(() => void 0) : void 0;
1490
+ if (entry.isDirectory() || info?.isDirectory()) {
1491
+ const resolvedEntries = await resolveExtensionEntries(entryPath);
1492
+ if (resolvedEntries) {
1493
+ paths.push(...resolvedEntries);
1494
+ }
1495
+ }
1496
+ }
1497
+ }
1498
+ return paths;
1499
+ }
1500
+ function loadExtensionFromFactory(factory, cwd, extensionPath = "<inline>") {
1501
+ const resolvedCwd = resolve(cwd);
1502
+ const resolvedPath = isSyntheticPath(extensionPath) ? extensionPath : resolve(resolvedCwd, extensionPath);
1503
+ const name = isSyntheticPath(extensionPath) ? extensionPath : dirname(resolvedPath).split("/").pop() ?? "unknown";
1504
+ const manifestDir = isSyntheticPath(extensionPath) ? resolvedCwd : dirname(resolvedPath);
1505
+ const manifest = readManifestSync(manifestDir);
1506
+ return {
1507
+ path: extensionPath,
1508
+ resolvedPath,
1509
+ name,
1510
+ factory,
1511
+ manifest
1512
+ };
1513
+ }
1514
+ var loadExtensionFromFactorySync = loadExtensionFromFactory;
1515
+ async function loadExtensions(paths, cwd) {
1516
+ const extensions = [];
1517
+ const errors = [];
1518
+ const resolvedCwd = resolve(cwd);
1519
+ for (const path of paths) {
1520
+ const resolvedPath = resolve(resolvedCwd, path);
1521
+ try {
1522
+ const factory = await loadExtensionModule(resolvedPath);
1523
+ if (!factory) {
1524
+ errors.push({
1525
+ path: resolvedPath,
1526
+ error: `Extension does not export a valid factory function: ${path}`
1527
+ });
1528
+ continue;
1529
+ }
1530
+ extensions.push(loadExtensionFromFactory(factory, resolvedCwd, resolvedPath));
1531
+ } catch (error) {
1532
+ errors.push({
1533
+ path: resolvedPath,
1534
+ error: error instanceof Error ? error.message : String(error)
1535
+ });
1536
+ }
1537
+ }
1538
+ return { extensions, errors };
1539
+ }
1540
+ async function discoverAndLoadExtensions(cwd) {
1541
+ const extensionsDir = join(resolve(cwd), ROWAN_DIR, EXTENSIONS_DIR);
1542
+ const paths = await discoverExtensionsInDir(extensionsDir);
1543
+ return loadExtensions(paths, cwd);
1544
+ }
1545
+
1546
+ // src/extensions/builtin.ts
1547
+ var BUILTIN_PHASE_IDS = /* @__PURE__ */ new Set(["chat", "plan", "execute", "verify"]);
1548
+ var __dirname = dirname2(fileURLToPath2(import.meta.url));
1549
+ var builtinPhaseDirs = [
1550
+ resolve2(__dirname, "../loop/phases/built-in/chat"),
1551
+ resolve2(__dirname, "../loop/phases/built-in/plan"),
1552
+ resolve2(__dirname, "../loop/phases/built-in/execute"),
1553
+ resolve2(__dirname, "../loop/phases/built-in/verify")
1554
+ ];
1555
+ var builtinPhaseNames = ["chat", "plan", "execute", "verify"];
1556
+ function isBuiltinSource(path) {
1557
+ return path.startsWith("<builtin:");
1558
+ }
1559
+ function isBuiltinPhaseOverride(phaseId, extensionPath) {
1560
+ return BUILTIN_PHASE_IDS.has(phaseId) && !isBuiltinSource(extensionPath);
1561
+ }
1562
+ var _builtinRunner;
1563
+ var _builtinExtensions = [];
1564
+ async function ensureBuiltin() {
1565
+ if (!_builtinRunner) {
1566
+ const extensions = builtinPhases.map(
1567
+ (factory, index) => loadExtensionFromFactory(factory, builtinPhaseDirs[index], `<builtin:phase:${builtinPhaseNames[index]}>`)
1568
+ );
1569
+ _builtinRunner = createExtensionRunner({
1570
+ validatePhaseOverride: isBuiltinPhaseOverride
1571
+ });
1572
+ await _builtinRunner.loadExtensions(extensions);
1573
+ _builtinRunner.bind();
1574
+ _builtinExtensions = extensions;
1575
+ }
1576
+ return { runner: _builtinRunner, extensions: _builtinExtensions };
1577
+ }
1578
+ async function getBuiltinExtensions() {
1579
+ const { extensions } = await ensureBuiltin();
1580
+ return [...extensions];
1581
+ }
1582
+ function getBuiltinRunner() {
1583
+ if (!_builtinRunner) {
1584
+ throw new Error("Builtin runner not initialized. Call getBuiltinExtensions() first.");
1585
+ }
1586
+ return _builtinRunner;
1587
+ }
1588
+ async function createBuiltinPhaseRegistry(input = {}) {
1589
+ const { runner } = await ensureBuiltin();
1590
+ return runner.createPhaseRegistry({ entryPhaseId: input.entryPhaseId ?? DEFAULT_PHASE_ID });
1591
+ }
1592
+ async function createDefaultPhaseRegistry(options = {}) {
1593
+ const cwd = resolve2(options.cwd ?? process.cwd());
1594
+ const runner = createExtensionRunner({
1595
+ validatePhaseOverride: isBuiltinPhaseOverride
1596
+ });
1597
+ const builtinExts = builtinPhases.map(
1598
+ (factory, index) => loadExtensionFromFactory(factory, builtinPhaseDirs[index], `<builtin:phase:${builtinPhaseNames[index]}>`)
1599
+ );
1600
+ await runner.loadExtensions(builtinExts);
1601
+ const result = await discoverAndLoadExtensions(cwd);
1602
+ if (result.errors.length > 0) {
1603
+ const details = result.errors.map((error) => `${error.path}: ${error.error}`).join("; ");
1604
+ throw new Error(`Failed to load Rowan extensions: ${details}`);
1605
+ }
1606
+ await runner.loadExtensions(result.extensions);
1607
+ runner.bind();
1608
+ return runner.createPhaseRegistry({ entryPhaseId: options.entryPhaseId ?? DEFAULT_PHASE_ID });
1609
+ }
1610
+
1611
+ // src/harness/tools/index.ts
1612
+ import { mkdir, readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
1613
+ import { dirname as dirname3, isAbsolute, relative, resolve as resolve3, sep } from "path";
1614
+ import Type3 from "typebox";
1615
+ import Schema from "typebox/schema";
1616
+
1617
+ // src/harness/tools/route-tool.ts
1618
+ import Type from "typebox";
1619
+ var PhaseRouteTool = "route";
1620
+ function buildRouteDescription(availablePhases) {
1621
+ const phasesBlock = buildStructuredSection("phase", [
1622
+ ...availablePhases.map((p) => ({ id: p.id, description: p.description })),
1623
+ { id: "stop", description: "End execution and return the result to the user" }
1624
+ ]);
1625
+ return [
1626
+ "Decide the next step in the workflow by routing to a specific phase.",
1627
+ "",
1628
+ "You MUST call this tool when you have completed the current phase's work",
1629
+ "and are ready to hand off to the next phase or end execution.",
1630
+ "",
1631
+ "<available_phases>",
1632
+ phasesBlock,
1633
+ "</available_phases>",
1634
+ "",
1635
+ "Choose the phase that best matches what needs to happen next.",
1636
+ "Use the 'reason' field to briefly explain your routing decision."
1637
+ ].join("\n");
1638
+ }
1639
+ function createRouteTool(availablePhases) {
1640
+ return {
1641
+ name: PhaseRouteTool,
1642
+ description: buildRouteDescription(availablePhases),
1643
+ parameters: Type.Object({
1644
+ route: Type.Union([
1645
+ ...availablePhases.map((p) => Type.Literal(p.id)),
1646
+ Type.Literal("stop")
1647
+ ], { description: "Target phase id, or 'stop' to end" }),
1648
+ reason: Type.Optional(Type.String({ description: "Brief reason for the routing decision" }))
1649
+ }),
1650
+ // No-op: this tool is intercepted by phases, never executed via tool execution
1651
+ execute: async (args, context) => ({
1652
+ toolCallId: context.toolCallId,
1653
+ toolName: PhaseRouteTool,
1654
+ ok: true,
1655
+ content: ""
1656
+ })
1657
+ };
1658
+ }
1659
+ function extractRouteCall(toolCalls) {
1660
+ const routeCall = toolCalls.find((t) => t.name === PhaseRouteTool);
1661
+ if (!routeCall) return void 0;
1662
+ const args = routeCall.args;
1663
+ return {
1664
+ route: typeof args.route === "string" ? args.route : "stop",
1665
+ reason: typeof args.reason === "string" ? args.reason : void 0
1666
+ };
1667
+ }
1668
+
1669
+ // src/harness/tools/thread-tool.ts
1670
+ import Type2 from "typebox";
1671
+ var ThreadTool = "thread";
1672
+ function buildThreadDescription(availableSkills) {
1673
+ const lines = [
1674
+ "Spawn a sub-agent to handle an independent subtask.",
1675
+ "",
1676
+ "The sub-agent runs a full agent loop on the given prompt,",
1677
+ "then returns the result. Use this to delegate well-scoped, parallelizable work",
1678
+ "that would otherwise clutter the current context.",
1679
+ "",
1680
+ "When to use:",
1681
+ "- A self-contained subtask that doesn't need back-and-forth with the current conversation.",
1682
+ "- Parallel work that can run independently (e.g. research, file generation, testing).",
1683
+ "- When the current context is getting long and a fresh scope would be cleaner.",
1684
+ "",
1685
+ "When NOT to use:",
1686
+ "- Tasks that require access to the current conversation's full history.",
1687
+ "- Simple tool calls that can be done directly in the current phase.",
1688
+ "- Tasks that need real-time interaction with the user."
1689
+ ];
1690
+ if (availableSkills.length > 0) {
1691
+ const skillsBlock = buildStructuredSection(
1692
+ "skill",
1693
+ availableSkills.map((s) => ({ name: s.name, description: s.description }))
1694
+ );
1695
+ lines.push("");
1696
+ lines.push("<available_skills>");
1697
+ lines.push(skillsBlock);
1698
+ lines.push("</available_skills>");
1699
+ }
1700
+ lines.push("");
1701
+ lines.push("The sub-agent inherits the current model and system prompt.");
1702
+ return lines.join("\n");
1703
+ }
1704
+ function extractThreadSummary(result) {
1705
+ for (let i = result.messages.length - 1; i >= 0; i--) {
1706
+ const msg = result.messages[i];
1707
+ if (msg.role === "assistant" && msg.content.trim().length > 0) {
1708
+ return msg.content.trim();
1709
+ }
1710
+ }
1711
+ if (result.outcome.message) {
1712
+ return result.outcome.message;
1713
+ }
1714
+ return `Sub-agent completed with outcome: ${result.outcome.id}`;
1715
+ }
1716
+ function resolveSkills(names, available) {
1717
+ const byName = new Map(available.map((s) => [s.name, s]));
1718
+ const resolved = [];
1719
+ for (const name of names) {
1720
+ const skill = byName.get(name);
1721
+ if (skill) {
1722
+ resolved.push(skill);
1723
+ }
1724
+ }
1725
+ return resolved;
1726
+ }
1727
+ function resolveTools(names, available) {
1728
+ const byName = new Map(available.map((t) => [t.name, t]));
1729
+ const resolved = [];
1730
+ for (const name of names) {
1731
+ const tool = byName.get(name);
1732
+ if (tool) {
1733
+ resolved.push(tool);
1734
+ }
1735
+ }
1736
+ return resolved;
1737
+ }
1738
+ function createThreadTool(availableTools, availableSkills, spawnThread) {
1739
+ return {
1740
+ name: ThreadTool,
1741
+ description: buildThreadDescription(availableSkills),
1742
+ parameters: Type2.Object({
1743
+ prompt: Type2.String({
1744
+ description: "Clear, self-contained instructions for the sub-agent. Include all context needed \u2014 the sub-agent does NOT see the current conversation."
1745
+ }),
1746
+ tools: Type2.Optional(Type2.Array(
1747
+ Type2.String(),
1748
+ { description: "Tool names to make available to the sub-agent. Only include tools the subtask actually needs." }
1749
+ )),
1750
+ skills: Type2.Optional(Type2.Array(
1751
+ Type2.String(),
1752
+ { description: "Skill names to make available to the sub-agent. Only include skills the subtask actually needs." }
1753
+ )),
1754
+ limits: Type2.Optional(Type2.Object({
1755
+ maxIterations: Type2.Optional(Type2.Number({
1756
+ description: "Maximum phase iterations the sub-agent is allowed. Default: 50."
1757
+ }))
1758
+ }, { description: "Resource limits for the sub-agent. Omit to inherit defaults." }))
1759
+ }),
1760
+ execute: async (args, context) => {
1761
+ const { prompt, tools: toolNames, skills: skillNames, limits } = args;
1762
+ if (!prompt || prompt.trim().length === 0) {
1763
+ return {
1764
+ toolCallId: context.toolCallId,
1765
+ toolName: ThreadTool,
1766
+ ok: false,
1767
+ content: "",
1768
+ error: "Thread prompt must not be empty."
1769
+ };
1770
+ }
1771
+ const resolvedTools = toolNames ? resolveTools(toolNames, availableTools) : void 0;
1772
+ const resolvedSkills = skillNames ? resolveSkills(skillNames, context.state.skills) : void 0;
1773
+ const result = await spawnThread({
1774
+ prompt: prompt.trim(),
1775
+ ...resolvedTools && resolvedTools.length > 0 ? { tools: resolvedTools } : {},
1776
+ ...resolvedSkills && resolvedSkills.length > 0 ? { skills: resolvedSkills } : {},
1777
+ ...limits ? { limits } : {}
1778
+ });
1779
+ const summary = extractThreadSummary(result);
1780
+ const ok = result.outcome.id !== "aborted";
1781
+ return {
1782
+ toolCallId: context.toolCallId,
1783
+ toolName: ThreadTool,
1784
+ ok,
1785
+ content: JSON.stringify({
1786
+ summary,
1787
+ outcome: result.outcome.id,
1788
+ sessionId: result.sessionId,
1789
+ messageCount: result.messages.length
1790
+ }),
1791
+ ...ok ? {} : { error: result.outcome.message || `Sub-agent ended with outcome: ${result.outcome.id}` }
1792
+ };
1793
+ }
1794
+ };
1795
+ }
1796
+
1797
+ // src/harness/tools/index.ts
1798
+ var DEFAULT_MAX_READ_BYTES = 64e3;
1799
+ var DEFAULT_BASH_TIMEOUT_MS = 3e4;
1800
+ var DEFAULT_MAX_BASH_OUTPUT_BYTES = 64e3;
1801
+ var ReadArgsSchema = Type3.Object({
1802
+ path: Type3.String(),
1803
+ maxBytes: Type3.Optional(Type3.Number())
1804
+ });
1805
+ var ReadArgsValidator = Schema.Compile(ReadArgsSchema);
1806
+ var WriteArgsSchema = Type3.Object({
1807
+ path: Type3.String(),
1808
+ content: Type3.String()
1809
+ });
1810
+ var WriteArgsValidator = Schema.Compile(WriteArgsSchema);
1811
+ var EditArgsSchema = Type3.Object({
1812
+ path: Type3.String(),
1813
+ oldText: Type3.String(),
1814
+ newText: Type3.String(),
1815
+ replaceAll: Type3.Optional(Type3.Boolean())
1816
+ });
1817
+ var EditArgsValidator = Schema.Compile(EditArgsSchema);
1818
+ var BashArgsSchema = Type3.Object({
1819
+ command: Type3.String(),
1820
+ cwd: Type3.Optional(Type3.String()),
1821
+ timeoutMs: Type3.Optional(Type3.Number()),
1822
+ maxOutputBytes: Type3.Optional(Type3.Number())
1823
+ });
1824
+ var BashArgsValidator = Schema.Compile(BashArgsSchema);
1825
+ var validatorCache = /* @__PURE__ */ new WeakMap();
1826
+ function validatorFor(schema) {
1827
+ const cached = validatorCache.get(schema);
1828
+ if (cached) {
1829
+ return cached;
1830
+ }
1831
+ const validator = Schema.Compile(schema);
1832
+ validatorCache.set(schema, validator);
1833
+ return validator;
1834
+ }
1835
+ function normalizeRelativePath(path) {
1836
+ return path.split(sep).join("/");
1837
+ }
1838
+ function normalizeCoreToolInputPath(path = ".") {
1839
+ const trimmed = path.trim();
1840
+ if (!trimmed || trimmed === "/" || trimmed === "\\") {
1841
+ return ".";
1842
+ }
1843
+ return path;
1844
+ }
1845
+ function createCoreToolContext(input = {}) {
1846
+ return {
1847
+ root: resolve3(input.root ?? process.cwd()),
1848
+ maxReadBytes: input.maxReadBytes ?? DEFAULT_MAX_READ_BYTES,
1849
+ bashTimeoutMs: input.bashTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS,
1850
+ maxBashOutputBytes: input.maxBashOutputBytes ?? DEFAULT_MAX_BASH_OUTPUT_BYTES
1851
+ };
1852
+ }
1853
+ function resolveCoreToolPath(context, path = ".") {
1854
+ const root = resolve3(context.root);
1855
+ const inputPath = normalizeCoreToolInputPath(path);
1856
+ const absolutePath = resolve3(root, inputPath);
1857
+ const relativePath = relative(root, absolutePath);
1858
+ if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
1859
+ throw new Error(`Path escapes workspace root: ${path}`);
1860
+ }
1861
+ return {
1862
+ root,
1863
+ inputPath,
1864
+ absolutePath,
1865
+ relativePath: normalizeRelativePath(relativePath || ".")
1866
+ };
1867
+ }
1868
+ function toolResult(input) {
1869
+ return {
1870
+ toolCallId: input.context.toolCallId,
1871
+ toolName: input.toolName,
1872
+ ok: input.ok,
1873
+ content: input.content,
1874
+ ...input.error ? { error: input.error } : {}
1875
+ };
1876
+ }
1877
+ async function executeRuntimeToolCall(input) {
1878
+ const tool = input.tools.find((candidate) => candidate.name === input.toolCall.name);
1879
+ if (!tool) {
1880
+ const result = toolResult({
1881
+ context: input.toolContext,
1882
+ toolName: input.toolCall.name,
1883
+ ok: false,
1884
+ content: null,
1885
+ error: `Unknown tool: ${input.toolCall.name}`
1886
+ });
1887
+ await input.observe?.({ type: "tool_end", toolName: input.toolCall.name, result });
1888
+ return result;
1889
+ }
1890
+ let args;
1891
+ try {
1892
+ args = validatorFor(tool.parameters).Parse(input.toolCall.args);
1893
+ } catch (error) {
1894
+ const result = toolResult({
1895
+ context: input.toolContext,
1896
+ toolName: tool.name,
1897
+ ok: false,
1898
+ content: null,
1899
+ error: error instanceof Error ? error.message : String(error)
1900
+ });
1901
+ await input.observe?.({ type: "tool_end", toolName: tool.name, result });
1902
+ return result;
1903
+ }
1904
+ let decision;
1905
+ if (input.beforeToolCall) {
1906
+ await input.observe?.({ type: "approval_requested", tool, args });
1907
+ decision = await input.beforeToolCall({ tool, args });
1908
+ await input.observe?.({
1909
+ type: "approval_result",
1910
+ tool,
1911
+ args,
1912
+ decision: decision ?? { allow: true }
1913
+ });
1914
+ }
1915
+ if (decision && !decision.allow) {
1916
+ const result = toolResult({
1917
+ context: input.toolContext,
1918
+ toolName: tool.name,
1919
+ ok: false,
1920
+ content: null,
1921
+ error: decision.reason
1922
+ });
1923
+ await input.observe?.({ type: "tool_blocked", tool, reason: decision.reason });
1924
+ return result;
1925
+ }
1926
+ await input.observe?.({ type: "tool_start", tool, args });
1927
+ try {
1928
+ const rawResult = await tool.execute(args, input.toolContext, input.signal);
1929
+ let result = {
1930
+ ...rawResult,
1931
+ toolCallId: input.toolCall.id,
1932
+ toolName: tool.name
1933
+ };
1934
+ if (input.afterToolCall) {
1935
+ await input.observe?.({
1936
+ type: "result_review_requested",
1937
+ tool,
1938
+ result
1939
+ });
1940
+ result = await input.afterToolCall({ tool, result });
1941
+ await input.observe?.({
1942
+ type: "result_review_result",
1943
+ tool,
1944
+ result
1945
+ });
1946
+ }
1947
+ await input.observe?.({ type: "tool_end", toolName: tool.name, result });
1948
+ return result;
1949
+ } catch (error) {
1950
+ const result = toolResult({
1951
+ context: input.toolContext,
1952
+ toolName: tool.name,
1953
+ ok: false,
1954
+ content: null,
1955
+ error: error instanceof Error ? error.message : "Tool execution failed."
1956
+ });
1957
+ await input.observe?.({ type: "tool_end", toolName: tool.name, result });
1958
+ return result;
1959
+ }
1960
+ }
1961
+ function positiveNumber(value, name) {
1962
+ if (!Number.isFinite(value) || value <= 0) {
1963
+ return `${name} must be a positive number.`;
1964
+ }
1965
+ return void 0;
1966
+ }
1967
+ async function captureStream(stream, maxBytes) {
1968
+ const reader = stream.getReader();
1969
+ const chunks = [];
1970
+ let totalBytes = 0;
1971
+ let truncated = false;
1972
+ while (true) {
1973
+ const { done, value } = await reader.read();
1974
+ if (done) {
1975
+ break;
1976
+ }
1977
+ if (!value) {
1978
+ continue;
1979
+ }
1980
+ const remainingBytes = maxBytes - totalBytes;
1981
+ if (remainingBytes > 0) {
1982
+ const kept = value.subarray(0, remainingBytes);
1983
+ chunks.push(kept);
1984
+ totalBytes += kept.byteLength;
1985
+ }
1986
+ if (value.byteLength > remainingBytes || totalBytes >= maxBytes) {
1987
+ truncated = true;
1988
+ await reader.cancel().catch(() => void 0);
1989
+ break;
1990
+ }
1991
+ }
1992
+ const bytes = new Uint8Array(totalBytes);
1993
+ let offset = 0;
1994
+ for (const chunk of chunks) {
1995
+ bytes.set(chunk, offset);
1996
+ offset += chunk.byteLength;
1997
+ }
1998
+ return {
1999
+ text: new TextDecoder().decode(bytes),
2000
+ truncated
2001
+ };
2002
+ }
2003
+ function createReadTool(context) {
2004
+ return {
2005
+ name: "read",
2006
+ description: "Reads a text file within the workspace.",
2007
+ parameters: ReadArgsSchema,
2008
+ promptSnippet: "Read a text file (max 64KB default).",
2009
+ promptGuidelines: [
2010
+ "Always read a file before editing or writing to understand its current content.",
2011
+ "Use maxBytes to limit output for large files."
2012
+ ],
2013
+ async execute(args, toolContext) {
2014
+ const parsed = ReadArgsValidator.Parse(args);
2015
+ const resolved = resolveCoreToolPath(context, parsed.path);
2016
+ const maxBytes = parsed.maxBytes ?? context.maxReadBytes;
2017
+ const invalidLimit = positiveNumber(maxBytes, "maxBytes");
2018
+ if (invalidLimit) {
2019
+ return toolResult({
2020
+ context: toolContext,
2021
+ toolName: "read",
2022
+ ok: false,
2023
+ content: null,
2024
+ error: invalidLimit
2025
+ });
2026
+ }
2027
+ const fileStat = await stat2(resolved.absolutePath);
2028
+ if (!fileStat.isFile()) {
2029
+ return toolResult({
2030
+ context: toolContext,
2031
+ toolName: "read",
2032
+ ok: false,
2033
+ content: null,
2034
+ error: `Not a file: ${resolved.relativePath}`
2035
+ });
2036
+ }
2037
+ const bytes = await readFile2(resolved.absolutePath);
2038
+ const sliced = bytes.subarray(0, maxBytes);
2039
+ return toolResult({
2040
+ context: toolContext,
2041
+ toolName: "read",
2042
+ ok: true,
2043
+ content: {
2044
+ path: resolved.relativePath,
2045
+ content: new TextDecoder().decode(sliced),
2046
+ sizeBytes: bytes.byteLength,
2047
+ truncated: bytes.byteLength > maxBytes
2048
+ }
2049
+ });
2050
+ }
2051
+ };
2052
+ }
2053
+ function createWriteTool(context) {
2054
+ return {
2055
+ name: "write",
2056
+ description: "Writes provided text content to a workspace file, creating parent directories as needed.",
2057
+ parameters: WriteArgsSchema,
2058
+ promptSnippet: "Write content to a file (creates parent dirs automatically).",
2059
+ promptGuidelines: [
2060
+ "Use write for new files or full file rewrites.",
2061
+ "For partial edits, prefer the edit tool to avoid overwriting unchanged content."
2062
+ ],
2063
+ async execute(args, toolContext) {
2064
+ const parsed = WriteArgsValidator.Parse(args);
2065
+ const resolved = resolveCoreToolPath(context, parsed.path);
2066
+ await mkdir(dirname3(resolved.absolutePath), { recursive: true });
2067
+ await writeFile(resolved.absolutePath, parsed.content, "utf8");
2068
+ return toolResult({
2069
+ context: toolContext,
2070
+ toolName: "write",
2071
+ ok: true,
2072
+ content: {
2073
+ path: resolved.relativePath,
2074
+ bytesWritten: new TextEncoder().encode(parsed.content).byteLength
2075
+ }
2076
+ });
2077
+ }
2078
+ };
2079
+ }
2080
+ function createEditTool(context) {
2081
+ return {
2082
+ name: "edit",
2083
+ description: "Edits a workspace text file by replacing exact oldText with newText.",
2084
+ parameters: EditArgsSchema,
2085
+ promptSnippet: "Replace exact text in a file (oldText \u2192 newText).",
2086
+ promptGuidelines: [
2087
+ "Read the file first to get the exact oldText to replace.",
2088
+ "oldText must be an exact match including whitespace and indentation.",
2089
+ "If oldText appears multiple times, set replaceAll=true or provide more surrounding context."
2090
+ ],
2091
+ async execute(args, toolContext) {
2092
+ const parsed = EditArgsValidator.Parse(args);
2093
+ if (!parsed.oldText) {
2094
+ return toolResult({
2095
+ context: toolContext,
2096
+ toolName: "edit",
2097
+ ok: false,
2098
+ content: null,
2099
+ error: "oldText must not be empty."
2100
+ });
2101
+ }
2102
+ const resolved = resolveCoreToolPath(context, parsed.path);
2103
+ const current = await readFile2(resolved.absolutePath, "utf8");
2104
+ if (!current.includes(parsed.oldText)) {
2105
+ return toolResult({
2106
+ context: toolContext,
2107
+ toolName: "edit",
2108
+ ok: false,
2109
+ content: null,
2110
+ error: `oldText not found in ${resolved.relativePath}.`
2111
+ });
2112
+ }
2113
+ const matches = current.split(parsed.oldText).length - 1;
2114
+ if (matches > 1 && !parsed.replaceAll) {
2115
+ return toolResult({
2116
+ context: toolContext,
2117
+ toolName: "edit",
2118
+ ok: false,
2119
+ content: null,
2120
+ error: `oldText appears ${matches} times in ${resolved.relativePath}; set replaceAll=true or provide more context.`
2121
+ });
2122
+ }
2123
+ const replacements = parsed.replaceAll ? matches : 1;
2124
+ const next = parsed.replaceAll ? current.split(parsed.oldText).join(parsed.newText) : current.replace(parsed.oldText, parsed.newText);
2125
+ await writeFile(resolved.absolutePath, next, "utf8");
2126
+ return toolResult({
2127
+ context: toolContext,
2128
+ toolName: "edit",
2129
+ ok: true,
2130
+ content: {
2131
+ path: resolved.relativePath,
2132
+ replacements,
2133
+ bytesWritten: new TextEncoder().encode(next).byteLength
2134
+ }
2135
+ });
2136
+ }
2137
+ };
2138
+ }
2139
+ function createBashTool(context) {
2140
+ return {
2141
+ name: "bash",
2142
+ description: "Runs a bash command within the workspace.",
2143
+ parameters: BashArgsSchema,
2144
+ promptSnippet: "Execute a bash command (30s timeout, 64KB output limit).",
2145
+ promptGuidelines: [
2146
+ "Use bash for build commands, tests, git operations, and system tools.",
2147
+ "Prefer dedicated tools (read/write/edit) for file operations.",
2148
+ "Set timeoutMs for long-running commands."
2149
+ ],
2150
+ async execute(args, toolContext, signal) {
2151
+ const parsed = BashArgsValidator.Parse(args);
2152
+ const timeoutMs = parsed.timeoutMs ?? context.bashTimeoutMs;
2153
+ const maxOutputBytes = parsed.maxOutputBytes ?? context.maxBashOutputBytes;
2154
+ const invalidTimeout = positiveNumber(timeoutMs, "timeoutMs");
2155
+ const invalidOutputLimit = positiveNumber(maxOutputBytes, "maxOutputBytes");
2156
+ if (invalidTimeout || invalidOutputLimit) {
2157
+ return toolResult({
2158
+ context: toolContext,
2159
+ toolName: "bash",
2160
+ ok: false,
2161
+ content: null,
2162
+ error: invalidTimeout ?? invalidOutputLimit
2163
+ });
2164
+ }
2165
+ const cwd = resolveCoreToolPath(context, parsed.cwd ?? ".");
2166
+ let timedOut = false;
2167
+ let aborted = false;
2168
+ const proc = Bun.spawn(["bash", "-lc", parsed.command], {
2169
+ cwd: cwd.absolutePath,
2170
+ stdout: "pipe",
2171
+ stderr: "pipe"
2172
+ });
2173
+ const kill = () => {
2174
+ proc.kill();
2175
+ };
2176
+ const timeout = setTimeout(() => {
2177
+ timedOut = true;
2178
+ kill();
2179
+ }, timeoutMs);
2180
+ const onAbort = () => {
2181
+ aborted = true;
2182
+ kill();
2183
+ };
2184
+ signal?.addEventListener("abort", onAbort, { once: true });
2185
+ try {
2186
+ const [stdout, stderr, exitCode] = await Promise.all([
2187
+ captureStream(proc.stdout, maxOutputBytes),
2188
+ captureStream(proc.stderr, maxOutputBytes),
2189
+ proc.exited
2190
+ ]);
2191
+ const ok = exitCode === 0 && !timedOut && !aborted;
2192
+ return toolResult({
2193
+ context: toolContext,
2194
+ toolName: "bash",
2195
+ ok,
2196
+ content: {
2197
+ command: parsed.command,
2198
+ cwd: cwd.relativePath,
2199
+ exitCode,
2200
+ stdout: stdout.text,
2201
+ stderr: stderr.text,
2202
+ stdoutTruncated: stdout.truncated,
2203
+ stderrTruncated: stderr.truncated
2204
+ },
2205
+ ...ok ? {} : {
2206
+ error: timedOut ? `Command timed out after ${timeoutMs}ms.` : aborted ? "Command aborted." : `Command exited with ${exitCode}.`
2207
+ }
2208
+ });
2209
+ } finally {
2210
+ clearTimeout(timeout);
2211
+ signal?.removeEventListener("abort", onAbort);
2212
+ }
2213
+ }
2214
+ };
2215
+ }
2216
+ function createCoreTools(input = {}) {
2217
+ const context = createCoreToolContext(input);
2218
+ return [
2219
+ createReadTool(context),
2220
+ createWriteTool(context),
2221
+ createEditTool(context),
2222
+ createBashTool(context)
2223
+ ];
2224
+ }
2225
+
2226
+ // src/loop/errors.ts
2227
+ var LoopGuard = {
2228
+ /** Returns abort result if signal is aborted */
2229
+ checkAbort(signal) {
2230
+ if (signal?.aborted) {
2231
+ return { stopReason: "aborted", message: "Agent run aborted." };
2232
+ }
2233
+ return { stopReason: "none" };
2234
+ }
2235
+ };
2236
+
2237
+ // src/loop/outcomes.ts
2238
+ var createOutcome = {
2239
+ fromResult(result) {
2240
+ if (result.stopReason === "none") {
2241
+ return { id: createId("out"), message: "Completed." };
2242
+ }
2243
+ if (result.stopReason === "aborted") {
2244
+ return createOutcome.aborted();
2245
+ }
2246
+ return createOutcome.error(result.message);
2247
+ },
2248
+ phase() {
2249
+ return { id: "default", message: "Phase completed." };
2250
+ },
2251
+ default(output) {
2252
+ return { id: createId("out"), message: output.message || "Completed." };
2253
+ },
2254
+ aborted() {
2255
+ return { id: createId("out"), message: "Agent run aborted." };
2256
+ },
2257
+ error(message) {
2258
+ return { id: createId("out"), message };
2259
+ }
2260
+ };
2261
+
2262
+ // src/loop/state.ts
2263
+ function snapshotMessage(message) {
2264
+ return {
2265
+ ...message,
2266
+ ...message.metadata ? { metadata: { ...message.metadata } } : {}
2267
+ };
2268
+ }
2269
+ function snapshotMessages(messages) {
2270
+ return messages.map(snapshotMessage);
2271
+ }
2272
+
2273
+ // src/loop/compaction.ts
2274
+ function needsCompaction(messages, options = {}) {
2275
+ const maxMessages = options.maxMessages ?? 50;
2276
+ return messages.length > maxMessages;
2277
+ }
2278
+ function buildSummary(messages) {
2279
+ const parts = [];
2280
+ let currentRole;
2281
+ let currentContent = [];
2282
+ function flush() {
2283
+ if (currentRole && currentContent.length > 0) {
2284
+ const combined = currentContent.join("\n");
2285
+ const truncated = combined.length > 500 ? combined.slice(0, 500) + "..." : combined;
2286
+ parts.push(`[${currentRole}]: ${truncated}`);
2287
+ }
2288
+ currentContent = [];
2289
+ }
2290
+ for (const msg of messages) {
2291
+ if (msg.role !== currentRole) {
2292
+ flush();
2293
+ currentRole = msg.role;
2294
+ }
2295
+ currentContent.push(msg.content);
2296
+ }
2297
+ flush();
2298
+ return parts.join("\n\n");
2299
+ }
2300
+ function compactMessages(messages, options = {}) {
2301
+ if (!needsCompaction(messages, options)) {
2302
+ return { compacted: false, messages };
2303
+ }
2304
+ const keepRecent = options.keepRecent ?? 10;
2305
+ const minCompact = options.minCompact ?? 20;
2306
+ const firstUserIdx = messages.findIndex(
2307
+ (m) => m.role === "user"
2308
+ );
2309
+ const recentStart = Math.max(messages.length - keepRecent, firstUserIdx + 1);
2310
+ const oldMessages = messages.slice(firstUserIdx >= 0 ? firstUserIdx + 1 : 0, recentStart);
2311
+ if (oldMessages.length < minCompact) {
2312
+ return { compacted: false, messages };
2313
+ }
2314
+ const recentMessages = messages.slice(recentStart);
2315
+ const summary = buildSummary(oldMessages);
2316
+ const result = [];
2317
+ if (firstUserIdx >= 0) {
2318
+ result.push(messages[firstUserIdx]);
2319
+ }
2320
+ result.push(
2321
+ createMessage("assistant", `[Context compaction summary]
2322
+
2323
+ ${summary}`, {
2324
+ type: "compaction_summary",
2325
+ compactedCount: oldMessages.length
2326
+ })
2327
+ );
2328
+ result.push(...recentMessages);
2329
+ return {
2330
+ compacted: true,
2331
+ messages: result,
2332
+ summarizedCount: oldMessages.length,
2333
+ summary
2334
+ };
2335
+ }
2336
+
2337
+ // src/agent-loop.ts
2338
+ function resolvePhaseOutput(result, state) {
2339
+ if (result) return result;
2340
+ return {
2341
+ message: state.transcript.filter((m) => m.role === "assistant").pop()?.content ?? "",
2342
+ route: "stop"
2343
+ };
2344
+ }
2345
+ function createLoopLifecycle(input) {
2346
+ const context = contextFromLoopInput(input);
2347
+ if (!context) {
2348
+ throw new Error("Agent loop runs require either context or state.");
2349
+ }
2350
+ const agentState = input.state ? syncStateFromContext(input.state, context) : createStateFromContext(context, { id: input.sessionId });
2351
+ const config = {
2352
+ model: input.model,
2353
+ stream: input.stream,
2354
+ tools: input.tools ?? context.tools ?? [],
2355
+ maxAttempts: input.maxAttempts ?? 2,
2356
+ limits: input.limits,
2357
+ signal: input.signal,
2358
+ runtime: input.runtime,
2359
+ beforeToolCall: input.beforeToolCall,
2360
+ afterToolCall: input.afterToolCall,
2361
+ beforePhase: input.beforePhase,
2362
+ afterPhase: input.afterPhase,
2363
+ emit: input.emit,
2364
+ phaseConfig: input.phaseConfig
2365
+ };
2366
+ const state = {
2367
+ agentState,
2368
+ currentPhase: "",
2369
+ attempt: 0,
2370
+ transcript: snapshotMessages(agentState.messages),
2371
+ metrics: {
2372
+ iterations: 0,
2373
+ phaseTransitions: [],
2374
+ compactionCount: 0,
2375
+ retryCount: 0,
2376
+ startedAt: createTimestamp(),
2377
+ startedAtMs: Date.now()
2378
+ }
2379
+ };
2380
+ return { config, state };
2381
+ }
2382
+ function emit(state, emitFn, event) {
2383
+ state.agentState.updatedAt = event.ts;
2384
+ emitFn?.(event);
2385
+ }
2386
+ function emitTurn(state, emitFn, type, extra) {
2387
+ emit(state, emitFn, {
2388
+ type,
2389
+ content: snapshotMessages(state.transcript),
2390
+ ...extra,
2391
+ ts: createTimestamp()
2392
+ });
2393
+ }
2394
+ function appendMessage(state, message, toState = false) {
2395
+ if (toState) {
2396
+ state.agentState.messages.push(message);
2397
+ }
2398
+ state.transcript.push(message);
2399
+ }
2400
+ function createRunResult(state, outcome) {
2401
+ return {
2402
+ sessionId: state.agentState.id,
2403
+ messages: snapshotMessages(state.agentState.messages),
2404
+ outcome,
2405
+ metrics: state.metrics
2406
+ };
2407
+ }
2408
+ function completeRun(state, outcome) {
2409
+ state.metrics.endedAt = createTimestamp();
2410
+ state.metrics.durationMs = Date.now() - state.metrics.startedAtMs;
2411
+ return createRunResult(state, outcome);
2412
+ }
2413
+ function createAgentLoopContext(config, state, availablePhases) {
2414
+ const routeTool = createRouteTool(availablePhases);
2415
+ const threadTool = createThreadTool(config.tools, state.agentState.skills, async (input) => {
2416
+ const result = await runAgentLoop({
2417
+ context: {
2418
+ systemPrompt: state.agentState.systemPrompt,
2419
+ messages: [createMessage("user", input.prompt)],
2420
+ tools: input.tools?.slice() ?? config.tools.slice(),
2421
+ skills: input.skills?.slice() ?? state.agentState.skills.slice()
2422
+ },
2423
+ model: config.model,
2424
+ stream: config.stream,
2425
+ maxAttempts: config.maxAttempts,
2426
+ limits: input.limits ?? config.limits,
2427
+ signal: config.signal,
2428
+ runtime: config.runtime,
2429
+ beforeToolCall: config.beforeToolCall,
2430
+ afterToolCall: config.afterToolCall,
2431
+ beforePhase: config.beforePhase,
2432
+ afterPhase: config.afterPhase,
2433
+ beforePrompt: config.beforePrompt,
2434
+ emit: config.emit,
2435
+ phaseConfig: config.phaseConfig
2436
+ });
2437
+ return result;
2438
+ });
2439
+ return {
2440
+ systemPrompt: state.agentState.systemPrompt,
2441
+ messages: snapshotMessages(state.agentState.messages),
2442
+ tools: [...config.tools, routeTool, threadTool],
2443
+ skills: state.agentState.skills.slice(),
2444
+ config,
2445
+ state,
2446
+ ...config.signal ? { signal: config.signal } : {},
2447
+ emit: (event) => emit(state, config.emit, event),
2448
+ appendMessage: (message) => appendMessage(state, message),
2449
+ appendStateMessage: (message) => appendMessage(state, message, true)
2450
+ };
2451
+ }
2452
+ function cloneContext(context) {
2453
+ return {
2454
+ systemPrompt: context.systemPrompt,
2455
+ messages: snapshotMessages(context.messages),
2456
+ ...context.tools ? { tools: context.tools.slice() } : {},
2457
+ ...context.skills ? { skills: context.skills.slice() } : {}
2458
+ };
2459
+ }
2460
+ function contextFromState(state, tools) {
2461
+ return {
2462
+ systemPrompt: state.systemPrompt,
2463
+ messages: snapshotMessages(state.messages),
2464
+ tools: tools?.slice() ?? [],
2465
+ skills: state.skills.slice()
2466
+ };
2467
+ }
2468
+ function contextFromLoopInput(input) {
2469
+ if (input.context) return cloneContext(input.context);
2470
+ if (input.state) return contextFromState(input.state, input.tools);
2471
+ return void 0;
2472
+ }
2473
+ function createStateFromContext(context, meta = {}) {
2474
+ const firstUser = context.messages.find((m) => m.role === "user");
2475
+ if (!firstUser) throw new Error("Agent context must include at least one user message.");
2476
+ const state = createAgentState({
2477
+ ...meta.id ? { id: meta.id } : {},
2478
+ systemPrompt: context.systemPrompt,
2479
+ input: meta.input ?? firstUser.content,
2480
+ skills: context.skills ?? [],
2481
+ ...meta.parentSessionId ? { parentSessionId: meta.parentSessionId } : {}
2482
+ });
2483
+ if (context.messages.length > 0) {
2484
+ state.messages = snapshotMessages(context.messages);
2485
+ }
2486
+ state.skills = context.skills?.slice() ?? [];
2487
+ state.updatedAt = createTimestamp();
2488
+ return state;
2489
+ }
2490
+ function syncStateFromContext(state, context) {
2491
+ state.systemPrompt = context.systemPrompt;
2492
+ if (context.messages.length > 0) {
2493
+ state.messages = snapshotMessages(context.messages);
2494
+ }
2495
+ state.skills = context.skills?.slice() ?? state.skills;
2496
+ state.updatedAt = createTimestamp();
2497
+ return state;
2498
+ }
2499
+ async function runAgentLoop(input) {
2500
+ const { config: initialConfig, state } = createLoopLifecycle(input);
2501
+ const config = { ...initialConfig };
2502
+ const emitFn = config.emit;
2503
+ emit(state, emitFn, { type: "agent_start", sessionId: state.agentState.id, ts: createTimestamp() });
2504
+ try {
2505
+ const abortResult = LoopGuard.checkAbort(config.signal);
2506
+ if (abortResult.stopReason !== "none") {
2507
+ return completeRun(state, createOutcome.aborted());
2508
+ }
2509
+ const result = await runLoop(config, state);
2510
+ return result;
2511
+ } finally {
2512
+ emit(state, emitFn, {
2513
+ type: "agent_end",
2514
+ sessionId: state.agentState.id,
2515
+ messages: snapshotMessages(state.agentState.messages),
2516
+ ts: createTimestamp()
2517
+ });
2518
+ }
2519
+ }
2520
+ async function runLoop(config, state) {
2521
+ const phaseConfig = config.phaseConfig ?? await createBuiltinPhaseRegistry();
2522
+ if (config.phaseConfig) ensurePhaseRegistry(phaseConfig);
2523
+ config.phaseConfig = phaseConfig;
2524
+ const availablePhases = phaseConfig.phases.map((p) => ({ id: p.id, name: p.name, description: p.description }));
2525
+ let currentPhaseId = phaseConfig.entryPhaseId;
2526
+ const maxIterations = config.limits?.maxIterations ?? 50;
2527
+ const maxPhaseRounds = config.limits?.maxPhaseRounds ?? 10;
2528
+ let phaseRounds = 0;
2529
+ let isContinuing = false;
2530
+ while (currentPhaseId) {
2531
+ const abortResult = LoopGuard.checkAbort(config.signal);
2532
+ if (abortResult.stopReason !== "none") {
2533
+ return completeRun(state, createOutcome.aborted());
2534
+ }
2535
+ state.metrics.iterations++;
2536
+ if (state.metrics.iterations > maxIterations) {
2537
+ return completeRun(state, {
2538
+ id: "max_iterations",
2539
+ message: `Loop exceeded maximum iterations (${maxIterations}). Stopping to prevent infinite loop.`
2540
+ });
2541
+ }
2542
+ if (needsCompaction(state.transcript)) {
2543
+ const compacted = compactMessages(state.transcript);
2544
+ if (compacted.compacted) {
2545
+ state.transcript = compacted.messages;
2546
+ state.agentState.messages = compacted.messages;
2547
+ state.metrics.compactionCount++;
2548
+ emit(state, config.emit, {
2549
+ type: "message_start",
2550
+ message: {
2551
+ id: "compaction",
2552
+ role: "assistant",
2553
+ content: `[Compacted ${compacted.summarizedCount} older messages to stay within context limits]`,
2554
+ createdAt: createTimestamp(),
2555
+ metadata: { type: "compaction_notice" }
2556
+ },
2557
+ ts: createTimestamp()
2558
+ });
2559
+ }
2560
+ }
2561
+ const phase = resolvePhaseEntry(phaseConfig, currentPhaseId);
2562
+ state.currentPhase = currentPhaseId;
2563
+ const loopContext = createAgentLoopContext(config, state, availablePhases);
2564
+ const context = createPhaseContext(config, state, phase, loopContext, availablePhases);
2565
+ const phaseTools = phase.tools ? loopContext.tools.filter((t) => phase.tools.includes(t.name)) : loopContext.tools;
2566
+ const phaseSkills = phase.skills ? state.agentState.skills.filter((s) => phase.skills.includes(s.name)) : state.agentState.skills;
2567
+ let phaseInput = {
2568
+ phase: currentPhaseId,
2569
+ systemPrompt: loopContext.systemPrompt,
2570
+ messages: context.messages.visible(),
2571
+ tools: loopContext.tools,
2572
+ // All tools (for systemPrompt, cache-friendly)
2573
+ skills: loopContext.skills,
2574
+ // All skills (for systemPrompt)
2575
+ phaseTools,
2576
+ // Phase-filtered tools (for LlmRequest.tools)
2577
+ phaseSkills
2578
+ // Phase-filtered skills
2579
+ };
2580
+ if (!isContinuing) {
2581
+ emit(state, config.emit, { type: "phase_start", phase: currentPhaseId, ts: createTimestamp() });
2582
+ }
2583
+ isContinuing = false;
2584
+ if (config.beforePhase) {
2585
+ const extBefore = await config.beforePhase(currentPhaseId, phaseInput);
2586
+ if (extBefore.abort) {
2587
+ emit(state, config.emit, { type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
2588
+ return completeRun(state, extBefore.abort);
2589
+ }
2590
+ if (extBefore.skip) {
2591
+ emit(state, config.emit, { type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
2592
+ if (extBefore.skip.route === "stop") {
2593
+ return completeRun(state, {
2594
+ id: "skip",
2595
+ message: extBefore.skip.message || "Skipped."
2596
+ });
2597
+ }
2598
+ currentPhaseId = extBefore.skip.route;
2599
+ continue;
2600
+ }
2601
+ if (extBefore.input) {
2602
+ phaseInput = extBefore.input;
2603
+ }
2604
+ }
2605
+ let output;
2606
+ if (phase.run) {
2607
+ output = resolvePhaseOutput(await phase.run(context, phaseInput), state);
2608
+ } else {
2609
+ const collected = await context.turn(() => context.model.invoke({ input: phaseInput }));
2610
+ output = {
2611
+ message: collected.text,
2612
+ route: "stop",
2613
+ toolCalls: collected.toolCalls
2614
+ };
2615
+ }
2616
+ if (phaseInput.toolChoice && typeof phaseInput.toolChoice === "object" && phaseInput.toolChoice.type === "tool") {
2617
+ const requiredTool = phaseInput.toolChoice.name;
2618
+ const hasRequiredTool = output.toolCalls?.some((tc) => tc.name === requiredTool);
2619
+ if (!hasRequiredTool) {
2620
+ state.metrics.retryCount++;
2621
+ }
2622
+ }
2623
+ if (output.toolCalls && output.toolCalls.length > 0) {
2624
+ const routeDecision = context.routeDecision(output.toolCalls);
2625
+ if (routeDecision) {
2626
+ output.route = routeDecision.route;
2627
+ if (routeDecision.reason) {
2628
+ output.routeReason = routeDecision.reason;
2629
+ }
2630
+ }
2631
+ }
2632
+ if (config.afterPhase) {
2633
+ const extAfter = await config.afterPhase(currentPhaseId, output);
2634
+ if (extAfter.abort) {
2635
+ emit(state, config.emit, { type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
2636
+ return completeRun(state, extAfter.abort);
2637
+ }
2638
+ if (extAfter.retry && phase.run) {
2639
+ output = resolvePhaseOutput(await phase.run(context, extAfter.retry), state);
2640
+ if (output.toolCalls && output.toolCalls.length > 0) {
2641
+ const routeDecision = context.routeDecision(output.toolCalls);
2642
+ if (routeDecision) {
2643
+ output.route = routeDecision.route;
2644
+ if (routeDecision.reason) {
2645
+ output.routeReason = routeDecision.reason;
2646
+ }
2647
+ }
2648
+ }
2649
+ }
2650
+ if (extAfter.output) {
2651
+ output = extAfter.output;
2652
+ }
2653
+ }
2654
+ if (output.route === "continue") {
2655
+ phaseRounds++;
2656
+ if (phaseRounds > maxPhaseRounds) {
2657
+ emit(state, config.emit, { type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
2658
+ phaseRounds = 0;
2659
+ currentPhaseId = "chat";
2660
+ continue;
2661
+ }
2662
+ isContinuing = true;
2663
+ state.metrics.iterations++;
2664
+ continue;
2665
+ }
2666
+ phaseRounds = 0;
2667
+ emit(state, config.emit, { type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
2668
+ if (output.route === "stop") {
2669
+ const outcome = createOutcome.default(output);
2670
+ return completeRun(state, outcome);
2671
+ }
2672
+ if (!phaseConfig.phases.some((p) => p.id === output.route)) {
2673
+ return completeRun(state, createOutcome.phase());
2674
+ }
2675
+ state.metrics.phaseTransitions.push({
2676
+ from: currentPhaseId,
2677
+ to: output.route,
2678
+ ts: createTimestamp()
2679
+ });
2680
+ currentPhaseId = output.route;
2681
+ }
2682
+ throw new Error("Phase machine exited without a stop or abort transition.");
2683
+ }
2684
+ var DEFAULT_MAX_RETRIES = 3;
2685
+ var DEFAULT_BASE_DELAY_MS = 1e3;
2686
+ var DEFAULT_MAX_DELAY_MS = 3e4;
2687
+ function isRetryableError(error) {
2688
+ if (!(error instanceof Error)) return false;
2689
+ const message = error.message.toLowerCase();
2690
+ if (message.includes("rate limit") || message.includes("429")) return true;
2691
+ if (message.includes("overloaded") || message.includes("529")) return true;
2692
+ if (message.includes("server error") || message.includes("500")) return true;
2693
+ if (message.includes("bad gateway") || message.includes("502")) return true;
2694
+ if (message.includes("service unavailable") || message.includes("503")) return true;
2695
+ if (message.includes("gateway timeout") || message.includes("504")) return true;
2696
+ if (message.includes("econnreset") || message.includes("econnrefused")) return true;
2697
+ return false;
2698
+ }
2699
+ function getRetryDelay(attempt, baseMs, maxMs) {
2700
+ const exponential = baseMs * Math.pow(2, attempt);
2701
+ const jitter = exponential * (0.5 + Math.random() * 0.5);
2702
+ return Math.min(jitter, maxMs);
2703
+ }
2704
+ async function withRetry(fn, options = {}) {
2705
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
2706
+ const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
2707
+ const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
2708
+ let lastError;
2709
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2710
+ try {
2711
+ return await fn();
2712
+ } catch (error) {
2713
+ lastError = error;
2714
+ if (attempt >= maxRetries || !isRetryableError(error)) {
2715
+ throw error;
2716
+ }
2717
+ if (options.signal?.aborted) {
2718
+ throw error;
2719
+ }
2720
+ const delayMs = getRetryDelay(attempt, baseDelayMs, maxDelayMs);
2721
+ options.onRetry?.(attempt, error, delayMs);
2722
+ await new Promise((resolve7) => setTimeout(resolve7, delayMs));
2723
+ if (options.signal?.aborted) {
2724
+ throw lastError;
2725
+ }
2726
+ }
2727
+ }
2728
+ throw lastError;
2729
+ }
2730
+ async function collectStructured(input) {
2731
+ let activeMessageId;
2732
+ let lastPartial;
2733
+ let stopReason;
2734
+ for await (const event of input.events) {
2735
+ const abortResult = LoopGuard.checkAbort(input.context.signal);
2736
+ if (abortResult.stopReason !== "none") {
2737
+ return { text: abortResult.message, contentBlocks: [], toolCalls: [], stopReason: "aborted" };
2738
+ }
2739
+ if (event.type === "model_requested") {
2740
+ input.context.emit({
2741
+ type: "model_requested",
2742
+ model: event.model,
2743
+ usage: event.usage,
2744
+ ts: createTimestamp()
2745
+ });
2746
+ }
2747
+ if (event.type === "error") {
2748
+ throw event.error;
2749
+ }
2750
+ if (event.type === "start") {
2751
+ lastPartial = event.partial;
2752
+ if (!activeMessageId) {
2753
+ activeMessageId = input.message.start("assistant", "", {
2754
+ phase: input.metadataPhase
2755
+ });
2756
+ }
2757
+ }
2758
+ if (event.type === "text_delta") {
2759
+ lastPartial = event.partial;
2760
+ if (!activeMessageId) {
2761
+ activeMessageId = input.message.start("assistant", event.text, {
2762
+ phase: input.metadataPhase
2763
+ });
2764
+ } else {
2765
+ await input.message.update(activeMessageId, event.text);
2766
+ }
2767
+ }
2768
+ if (event.type === "tool_call_start" || event.type === "tool_call_delta" || event.type === "tool_call_end") {
2769
+ lastPartial = event.partial;
2770
+ }
2771
+ if (event.type === "thinking_delta") {
2772
+ lastPartial = event.partial;
2773
+ }
2774
+ if (event.type === "done") {
2775
+ stopReason = event.response?.stopReason;
2776
+ const toolCallBlocks = lastPartial?.contentBlocks?.filter((b) => b.type === "tool_call") ?? [];
2777
+ if (!activeMessageId && toolCallBlocks.length > 0) {
2778
+ activeMessageId = input.message.start("assistant", "", {
2779
+ phase: input.metadataPhase,
2780
+ toolCalls: toolCallBlocks.map((tc) => ({ id: tc.id, name: tc.name, args: tc.args }))
2781
+ });
2782
+ }
2783
+ if (activeMessageId) {
2784
+ await input.message.end(activeMessageId);
2785
+ activeMessageId = void 0;
2786
+ }
2787
+ }
2788
+ }
2789
+ if (activeMessageId) {
2790
+ await input.message.end(activeMessageId);
2791
+ }
2792
+ const contentBlocks = lastPartial?.contentBlocks ?? [];
2793
+ const text = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2794
+ const toolCalls = contentBlocks.filter((b) => b.type === "tool_call").map((b) => {
2795
+ let parsedArgs = b.args;
2796
+ try {
2797
+ parsedArgs = JSON.parse(b.args);
2798
+ } catch {
2799
+ }
2800
+ return { id: b.id, name: b.name, args: parsedArgs };
2801
+ });
2802
+ return { text, contentBlocks, toolCalls, stopReason };
2803
+ }
2804
+ async function executeToolCall(input) {
2805
+ if (input.context.config.runtime?.tools) {
2806
+ return input.context.config.runtime.tools({
2807
+ context: input.context,
2808
+ toolCall: input.toolCall
2809
+ });
2810
+ }
2811
+ const toolContext = {
2812
+ state: input.context.state.agentState,
2813
+ toolCallId: input.toolCall.id
2814
+ };
2815
+ return executeRuntimeToolCall({
2816
+ tools: input.context.tools,
2817
+ toolCall: input.toolCall,
2818
+ toolContext,
2819
+ beforeToolCall: input.context.config.beforeToolCall,
2820
+ afterToolCall: input.context.config.afterToolCall,
2821
+ signal: input.context.signal
2822
+ });
2823
+ }
2824
+ function createPhaseContext(config, state, phase, loopContext, availablePhases) {
2825
+ const activeMessages = /* @__PURE__ */ new Map();
2826
+ let turnDepth = 0;
2827
+ let autoTurnCount = 0;
2828
+ function beginAutoTurn() {
2829
+ if (turnDepth === 0) {
2830
+ autoTurnCount++;
2831
+ if (autoTurnCount === 1) {
2832
+ emitTurn(state, config.emit, "turn_start");
2833
+ }
2834
+ }
2835
+ }
2836
+ function endAutoTurn() {
2837
+ if (turnDepth === 0 && autoTurnCount > 0) {
2838
+ autoTurnCount--;
2839
+ if (autoTurnCount === 0) {
2840
+ emitTurn(state, config.emit, "turn_end");
2841
+ }
2842
+ }
2843
+ }
2844
+ const messageManager = {
2845
+ visible: () => [...state.transcript],
2846
+ start(role, content, metadata) {
2847
+ const msg = createMessage(role, content, metadata);
2848
+ activeMessages.set(msg.id, msg);
2849
+ beginAutoTurn();
2850
+ emit(state, config.emit, { type: "message_start", message: snapshotMessage(msg), ts: createTimestamp() });
2851
+ return msg.id;
2852
+ },
2853
+ async update(messageId, delta) {
2854
+ const msg = activeMessages.get(messageId);
2855
+ if (!msg) return;
2856
+ msg.content += delta;
2857
+ emit(state, config.emit, {
2858
+ type: "message_update",
2859
+ message: snapshotMessage(msg),
2860
+ delta,
2861
+ ts: createTimestamp()
2862
+ });
2863
+ },
2864
+ async end(messageId) {
2865
+ const msg = activeMessages.get(messageId);
2866
+ if (!msg) return;
2867
+ activeMessages.delete(messageId);
2868
+ state.transcript.push(msg);
2869
+ state.agentState.messages.push(msg);
2870
+ emit(state, config.emit, { type: "message_end", message: snapshotMessage(msg), ts: createTimestamp() });
2871
+ endAutoTurn();
2872
+ },
2873
+ snapshot() {
2874
+ return {
2875
+ transcriptLength: state.transcript.length,
2876
+ stateMessagesLength: state.agentState.messages.length
2877
+ };
2878
+ },
2879
+ restore(snap) {
2880
+ state.transcript.length = snap.transcriptLength;
2881
+ state.agentState.messages.length = snap.stateMessagesLength;
2882
+ activeMessages.clear();
2883
+ },
2884
+ delete(target) {
2885
+ const transcriptIdx = typeof target === "number" ? target : state.transcript.findIndex((m) => m.id === target);
2886
+ if (transcriptIdx >= 0 && transcriptIdx < state.transcript.length) {
2887
+ const msg = state.transcript[transcriptIdx];
2888
+ state.transcript.splice(transcriptIdx, 1);
2889
+ const stateIdx = state.agentState.messages.findIndex((m) => m.id === msg.id);
2890
+ if (stateIdx !== -1) {
2891
+ state.agentState.messages.splice(stateIdx, 1);
2892
+ }
2893
+ activeMessages.delete(msg.id);
2894
+ }
2895
+ },
2896
+ insert(target, message) {
2897
+ const idx = typeof target === "number" ? target : state.transcript.findIndex((m) => m.id === target);
2898
+ const insertIdx = idx >= 0 ? idx : state.transcript.length;
2899
+ state.transcript.splice(insertIdx, 0, message);
2900
+ state.agentState.messages.push(message);
2901
+ },
2902
+ clear() {
2903
+ state.transcript.length = 0;
2904
+ state.agentState.messages.length = 0;
2905
+ activeMessages.clear();
2906
+ }
2907
+ };
2908
+ const toolExecutionManager = {
2909
+ async start(toolCallId, toolName, args) {
2910
+ beginAutoTurn();
2911
+ emit(state, config.emit, {
2912
+ type: "tool_execution_start",
2913
+ toolCallId,
2914
+ toolName,
2915
+ args,
2916
+ ts: createTimestamp()
2917
+ });
2918
+ },
2919
+ async update(_toolCallId, _partialResult) {
2920
+ },
2921
+ async end(toolCallId, toolName, result, isError) {
2922
+ emit(state, config.emit, {
2923
+ type: "tool_execution_end",
2924
+ toolCallId,
2925
+ toolName,
2926
+ result,
2927
+ isError,
2928
+ ts: createTimestamp()
2929
+ });
2930
+ endAutoTurn();
2931
+ }
2932
+ };
2933
+ return {
2934
+ phaseId: phase.id,
2935
+ state: loopContext.state,
2936
+ messages: messageManager,
2937
+ toolExecution: toolExecutionManager,
2938
+ model: {
2939
+ invoke: async (input) => {
2940
+ const { autoExecuteTools, maxToolRounds = 10, excludeTools = [] } = input;
2941
+ const invokeOnce = async (phaseInput) => {
2942
+ if (loopContext.config.beforePrompt) {
2943
+ phaseInput = await loopContext.config.beforePrompt(phase.id, phaseInput);
2944
+ }
2945
+ const request = phase.buildPrompt(phaseInput);
2946
+ request.model = loopContext.config.model;
2947
+ if (!request.tools) {
2948
+ const modelTools = phaseInput.phaseTools ?? phaseInput.tools;
2949
+ if (modelTools.length > 0) {
2950
+ request.tools = modelTools.map((t) => ({
2951
+ name: t.name,
2952
+ description: t.description,
2953
+ parameters: t.parameters
2954
+ }));
2955
+ }
2956
+ }
2957
+ if (phaseInput.toolChoice && !request.toolChoice) {
2958
+ request.toolChoice = phaseInput.toolChoice;
2959
+ }
2960
+ return withRetry(
2961
+ () => collectStructured({
2962
+ context: loopContext,
2963
+ message: messageManager,
2964
+ events: loopContext.config.stream(request, { signal: loopContext.signal }),
2965
+ metadataPhase: phase.id
2966
+ }),
2967
+ {
2968
+ signal: loopContext.signal,
2969
+ onRetry: (attempt, error, delayMs) => {
2970
+ state.metrics.retryCount++;
2971
+ const errMsg = error instanceof Error ? error.message : String(error);
2972
+ loopContext.emit({
2973
+ type: "message_start",
2974
+ message: {
2975
+ id: `retry_${attempt}`,
2976
+ role: "assistant",
2977
+ content: `[Retry ${attempt + 1}/${DEFAULT_MAX_RETRIES}] Transient error: ${errMsg}. Retrying in ${Math.round(delayMs)}ms...`,
2978
+ createdAt: createTimestamp(),
2979
+ metadata: { type: "retry_notice" }
2980
+ },
2981
+ ts: createTimestamp()
2982
+ });
2983
+ }
2984
+ }
2985
+ );
2986
+ };
2987
+ if (!autoExecuteTools) {
2988
+ return invokeOnce(input.input);
2989
+ }
2990
+ let currentInput = input.input;
2991
+ let lastResult;
2992
+ for (let round = 0; round < maxToolRounds; round++) {
2993
+ lastResult = await invokeOnce(currentInput);
2994
+ const executableToolCalls = lastResult.toolCalls.filter(
2995
+ (tc) => !excludeTools.includes(tc.name)
2996
+ );
2997
+ if (executableToolCalls.length === 0) {
2998
+ break;
2999
+ }
3000
+ for (const toolCall of executableToolCalls) {
3001
+ await toolExecutionManager.start(toolCall.id, toolCall.name, toolCall.args);
3002
+ const result = await executeToolCall({ context: loopContext, toolCall });
3003
+ await toolExecutionManager.end(result.toolCallId, result.toolName, result, !result.ok);
3004
+ const toolResultContent = JSON.stringify({
3005
+ toolName: result.toolName,
3006
+ ok: result.ok,
3007
+ content: result.content,
3008
+ ...result.error ? { error: result.error } : {}
3009
+ });
3010
+ const toolMsgId = messageManager.start("tool", toolResultContent, {
3011
+ toolCallId: result.toolCallId,
3012
+ toolName: result.toolName,
3013
+ isError: !result.ok
3014
+ });
3015
+ await messageManager.end(toolMsgId);
3016
+ }
3017
+ currentInput = {
3018
+ ...currentInput,
3019
+ messages: messageManager.visible()
3020
+ };
3021
+ }
3022
+ return lastResult;
3023
+ }
3024
+ },
3025
+ tools: {
3026
+ execute: async (input) => {
3027
+ return executeToolCall({
3028
+ context: loopContext,
3029
+ toolCall: input.toolCall
3030
+ });
3031
+ }
3032
+ },
3033
+ skills: state.agentState.skills.slice(),
3034
+ turn: async (fn) => {
3035
+ turnDepth++;
3036
+ emitTurn(state, config.emit, "turn_start");
3037
+ try {
3038
+ return await fn();
3039
+ } finally {
3040
+ turnDepth--;
3041
+ emitTurn(state, config.emit, "turn_end");
3042
+ }
3043
+ },
3044
+ maxAttempts: config.maxAttempts,
3045
+ incrementAttempt() {
3046
+ state.attempt += 1;
3047
+ loopContext.state.attempt = state.attempt;
3048
+ },
3049
+ availablePhases,
3050
+ routeDecision(toolCalls) {
3051
+ return extractRouteCall(toolCalls);
3052
+ }
3053
+ };
3054
+ }
3055
+
3056
+ // src/agent.ts
3057
+ var Agent = class {
3058
+ state;
3059
+ options;
3060
+ listeners = /* @__PURE__ */ new Set();
3061
+ pendingListenerTasks = /* @__PURE__ */ new Set();
3062
+ listenerErrors = [];
3063
+ activeRun;
3064
+ constructor(options) {
3065
+ this.options = {
3066
+ ...options,
3067
+ context: cloneAgentContext(options.context)
3068
+ };
3069
+ this.state = {
3070
+ ...this.options.sessionId ? { sessionId: this.options.sessionId } : {},
3071
+ context: cloneAgentContext(this.options.context),
3072
+ model: this.options.model,
3073
+ tools: this.options.context.tools ?? [],
3074
+ isRunning: false
3075
+ };
3076
+ }
3077
+ subscribe(listener) {
3078
+ this.listeners.add(listener);
3079
+ return () => {
3080
+ this.listeners.delete(listener);
3081
+ };
3082
+ }
3083
+ emitToListeners(event) {
3084
+ for (const listener of this.listeners) {
3085
+ try {
3086
+ const result = listener(event);
3087
+ if (result && typeof result === "object" && "then" in result) {
3088
+ const task = Promise.resolve(result).catch((error) => {
3089
+ this.listenerErrors.push(error);
3090
+ }).finally(() => {
3091
+ this.pendingListenerTasks.delete(task);
3092
+ });
3093
+ this.pendingListenerTasks.add(task);
3094
+ }
3095
+ } catch (error) {
3096
+ this.listenerErrors.push(error);
3097
+ }
3098
+ }
3099
+ }
3100
+ async flushEvents() {
3101
+ while (this.pendingListenerTasks.size > 0) {
3102
+ await Promise.all([...this.pendingListenerTasks]);
3103
+ }
3104
+ for (const listener of this.listeners) {
3105
+ try {
3106
+ await listener.flush?.();
3107
+ } catch (error) {
3108
+ this.listenerErrors.push(error);
3109
+ }
3110
+ }
3111
+ if (this.listenerErrors.length > 0) {
3112
+ const [error] = this.listenerErrors;
3113
+ throw error instanceof Error ? error : new Error(String(error));
3114
+ }
3115
+ }
3116
+ processEvents(event) {
3117
+ switch (event.type) {
3118
+ case "message_start":
3119
+ this.state.currentResult = void 0;
3120
+ break;
3121
+ case "message_end":
3122
+ break;
3123
+ case "agent_end":
3124
+ break;
3125
+ }
3126
+ this.emitToListeners(event);
3127
+ this.options.extensionRunnerRef?.current?.emitAgentEvent(event);
3128
+ }
3129
+ /**
3130
+ * Hook for before_tool_call — called before a tool executes.
3131
+ * Extensions can block execution by setting allow=false.
3132
+ */
3133
+ async handleBeforeToolCall(tool, args) {
3134
+ const runner = this.options.extensionRunnerRef?.current;
3135
+ if (!runner) return { allow: true };
3136
+ return runner.emitBeforeToolCall(tool, args);
3137
+ }
3138
+ /**
3139
+ * Hook for after_tool_call — called after a tool executes.
3140
+ * Extensions can mutate the result.
3141
+ */
3142
+ async handleAfterToolCall(tool, result) {
3143
+ const runner = this.options.extensionRunnerRef?.current;
3144
+ if (!runner) return result;
3145
+ return runner.emitAfterToolCall(tool, result);
3146
+ }
3147
+ /**
3148
+ * Hook for before_phase — called before a phase executes.
3149
+ * Extensions can abort, skip, or replace the phase input.
3150
+ */
3151
+ async handleBeforePhase(phaseId, input) {
3152
+ const runner = this.options.extensionRunnerRef?.current;
3153
+ if (!runner) return {};
3154
+ return runner.emitBeforePhase(phaseId, input);
3155
+ }
3156
+ /**
3157
+ * Hook for after_phase — called after a phase executes.
3158
+ * Extensions can abort, retry, or replace the output.
3159
+ */
3160
+ async handleAfterPhase(phaseId, output) {
3161
+ const runner = this.options.extensionRunnerRef?.current;
3162
+ if (!runner) return {};
3163
+ return runner.emitAfterPhase(phaseId, output);
3164
+ }
3165
+ /**
3166
+ * Hook for before_prompt — called before buildPrompt, allowing extensions to transform PhaseInput.
3167
+ * Extensions can transform the PhaseInput (messages, tools, systemPrompt, etc.).
3168
+ */
3169
+ async handleBeforePrompt(phaseId, input) {
3170
+ const runner = this.options.extensionRunnerRef?.current;
3171
+ if (!runner) return input;
3172
+ return runner.emitBeforePrompt(phaseId, input);
3173
+ }
3174
+ handleRunFailure(error, aborted) {
3175
+ const message = error instanceof Error ? error.message : "Agent run failed.";
3176
+ this.state.error = message;
3177
+ }
3178
+ async runWithLifecycle(executor) {
3179
+ if (this.activeRun) {
3180
+ throw new Error("Agent is already running.");
3181
+ }
3182
+ let resolvePromise;
3183
+ const abortController = new AbortController();
3184
+ const promise = new Promise((resolve7) => {
3185
+ resolvePromise = resolve7;
3186
+ });
3187
+ this.activeRun = { promise, resolve: resolvePromise, abortController };
3188
+ this.state.isRunning = true;
3189
+ try {
3190
+ const result = await executor(abortController.signal);
3191
+ resolvePromise(result);
3192
+ return result;
3193
+ } catch (error) {
3194
+ this.handleRunFailure(error, abortController.signal.aborted);
3195
+ resolvePromise(void 0);
3196
+ throw error;
3197
+ } finally {
3198
+ this.finishRun();
3199
+ }
3200
+ }
3201
+ finishRun() {
3202
+ this.state.isRunning = false;
3203
+ this.activeRun = void 0;
3204
+ }
3205
+ async run(config) {
3206
+ const resolved = this.resolveRunConfig(config);
3207
+ const previousSessionId = this.state.sessionId ?? this.options.sessionId;
3208
+ const sessionId = resolved.sessionId ?? this.state.sessionId;
3209
+ const hadExistingSession = Boolean(sessionId && previousSessionId === sessionId);
3210
+ this.options = resolved;
3211
+ if (sessionId) {
3212
+ this.state.sessionId = sessionId;
3213
+ }
3214
+ this.state.context = cloneAgentContext(resolved.context);
3215
+ this.state.model = resolved.model;
3216
+ this.state.tools = resolved.context.tools ?? [];
3217
+ this.state.currentResult = void 0;
3218
+ this.state.error = void 0;
3219
+ return this.runWithLifecycle(async (signal) => {
3220
+ const emit2 = (event) => {
3221
+ this.processEvents(event);
3222
+ };
3223
+ const phaseConfig = resolved.phaseConfig ?? await createDefaultPhaseRegistry({
3224
+ cwd: resolved.cwd ?? process.cwd()
3225
+ });
3226
+ const beforeToolCall = async (input) => {
3227
+ if (resolved.beforeToolCall) {
3228
+ const userResult = await resolved.beforeToolCall(input);
3229
+ if (!userResult.allow) return userResult;
3230
+ }
3231
+ const extResult = await this.handleBeforeToolCall(input.tool, input.args);
3232
+ if (!extResult.allow) {
3233
+ return { allow: false, reason: extResult.reason ?? "Blocked by extension" };
3234
+ }
3235
+ return { allow: true };
3236
+ };
3237
+ const afterToolCall = async (input) => {
3238
+ let result2 = input.result;
3239
+ if (resolved.afterToolCall) {
3240
+ result2 = await resolved.afterToolCall({ tool: input.tool, result: result2 });
3241
+ }
3242
+ return this.handleAfterToolCall(input.tool, result2);
3243
+ };
3244
+ const result = await runAgentLoop({
3245
+ context: resolved.context,
3246
+ ...sessionId ? { sessionId } : {},
3247
+ model: resolved.model,
3248
+ stream: resolved.stream,
3249
+ maxAttempts: resolved.maxAttempts,
3250
+ limits: resolved.limits,
3251
+ signal,
3252
+ beforeToolCall,
3253
+ afterToolCall,
3254
+ beforePhase: (phaseId, input) => this.handleBeforePhase(phaseId, input),
3255
+ afterPhase: (phaseId, output) => this.handleAfterPhase(phaseId, output),
3256
+ beforePrompt: (phaseId, input) => this.handleBeforePrompt(phaseId, input),
3257
+ phaseConfig,
3258
+ emit: emit2
3259
+ });
3260
+ this.state.sessionId = result.sessionId;
3261
+ this.state.context = {
3262
+ ...cloneAgentContext(resolved.context),
3263
+ messages: snapshotMessages(result.messages)
3264
+ };
3265
+ this.state.currentResult = result;
3266
+ this.options = {
3267
+ ...resolved,
3268
+ sessionId: result.sessionId,
3269
+ context: cloneAgentContext(this.state.context)
3270
+ };
3271
+ return result;
3272
+ });
3273
+ }
3274
+ abort(reason = "Aborted by caller.") {
3275
+ this.activeRun?.abortController.abort(reason);
3276
+ }
3277
+ async waitForIdle() {
3278
+ if (!this.activeRun) {
3279
+ return;
3280
+ }
3281
+ await this.activeRun.promise.catch(() => void 0);
3282
+ await this.flushEvents().catch(() => void 0);
3283
+ }
3284
+ resolveRunConfig(config) {
3285
+ const context = cloneAgentContext(config?.context ?? this.createContextSnapshot());
3286
+ return {
3287
+ ...this.options,
3288
+ ...config,
3289
+ context,
3290
+ sessionId: config?.sessionId ?? this.state.sessionId ?? this.options.sessionId
3291
+ };
3292
+ }
3293
+ createContextSnapshot() {
3294
+ return cloneAgentContext(this.state.context);
3295
+ }
3296
+ };
3297
+ function cloneAgentContext(context) {
3298
+ return {
3299
+ systemPrompt: context.systemPrompt,
3300
+ messages: snapshotMessages(context.messages),
3301
+ ...context.tools ? { tools: context.tools.slice() } : {},
3302
+ ...context.skills ? { skills: context.skills.slice() } : {}
3303
+ };
3304
+ }
3305
+
3306
+ // src/event-stream.ts
3307
+ var EventStream = class {
3308
+ queue = [];
3309
+ waiting = [];
3310
+ done = false;
3311
+ finalResultPromise;
3312
+ resolveFinalResult;
3313
+ isComplete;
3314
+ extractResult;
3315
+ constructor(isComplete, extractResult) {
3316
+ this.isComplete = isComplete;
3317
+ this.extractResult = extractResult;
3318
+ this.finalResultPromise = new Promise((resolve7) => {
3319
+ this.resolveFinalResult = resolve7;
3320
+ });
3321
+ }
3322
+ push(event) {
3323
+ if (this.done) return;
3324
+ if (this.isComplete(event)) {
3325
+ this.done = true;
3326
+ this.resolveFinalResult(this.extractResult(event));
3327
+ }
3328
+ const waiter = this.waiting.shift();
3329
+ if (waiter) {
3330
+ waiter({ value: event, done: false });
3331
+ } else {
3332
+ this.queue.push(event);
3333
+ }
3334
+ }
3335
+ end(result) {
3336
+ this.done = true;
3337
+ if (result !== void 0) {
3338
+ this.resolveFinalResult(result);
3339
+ }
3340
+ while (this.waiting.length > 0) {
3341
+ const waiter = this.waiting.shift();
3342
+ waiter({ value: void 0, done: true });
3343
+ }
3344
+ }
3345
+ async *[Symbol.asyncIterator]() {
3346
+ while (true) {
3347
+ if (this.queue.length > 0) {
3348
+ yield this.queue.shift();
3349
+ } else if (this.done) {
3350
+ return;
3351
+ } else {
3352
+ const result = await new Promise((resolve7) => this.waiting.push(resolve7));
3353
+ if (result.done) return;
3354
+ yield result.value;
3355
+ }
3356
+ }
3357
+ }
3358
+ result() {
3359
+ return this.finalResultPromise;
3360
+ }
3361
+ };
3362
+ var AgentEventStream = class extends EventStream {
3363
+ constructor() {
3364
+ super(
3365
+ (event) => event.type === "agent_end",
3366
+ (event) => event.type === "agent_end" ? event.messages : []
3367
+ );
3368
+ }
3369
+ };
3370
+
3371
+ // src/harness/session/session.ts
3372
+ import Type4 from "typebox";
3373
+ var SESSION_SCHEMA_VERSION = "0.4.4";
3374
+ var AgentMessageSchema = Type4.Object({
3375
+ id: Type4.String(),
3376
+ role: Type4.Union([
3377
+ Type4.Literal("system"),
3378
+ Type4.Literal("user"),
3379
+ Type4.Literal("assistant"),
3380
+ Type4.Literal("tool")
3381
+ ]),
3382
+ content: Type4.String(),
3383
+ createdAt: Type4.String(),
3384
+ metadata: Type4.Optional(Type4.Record(Type4.String(), Type4.Unknown()))
3385
+ });
3386
+ var SkillSchema = Type4.Object({
3387
+ name: Type4.String(),
3388
+ description: Type4.String(),
3389
+ filePath: Type4.String(),
3390
+ baseDir: Type4.String(),
3391
+ disableModelInvocation: Type4.Boolean()
3392
+ });
3393
+ function createId2(prefix) {
3394
+ return `${prefix}_${crypto.randomUUID().slice(0, 8)}`;
3395
+ }
3396
+ function padDatePart2(value, length = 2) {
3397
+ return String(value).padStart(length, "0");
3398
+ }
3399
+ function formatLocalTimestamp2(date = /* @__PURE__ */ new Date()) {
3400
+ const offsetMinutes = -date.getTimezoneOffset();
3401
+ const offsetSign = offsetMinutes >= 0 ? "+" : "-";
3402
+ const offsetAbsolute = Math.abs(offsetMinutes);
3403
+ const offsetHours = Math.floor(offsetAbsolute / 60);
3404
+ const offsetRemainingMinutes = offsetAbsolute % 60;
3405
+ return [
3406
+ `${date.getFullYear()}-${padDatePart2(date.getMonth() + 1)}-${padDatePart2(date.getDate())}`,
3407
+ "T",
3408
+ `${padDatePart2(date.getHours())}${padDatePart2(date.getMinutes())}${padDatePart2(date.getSeconds())}`,
3409
+ "-",
3410
+ padDatePart2(Math.floor(date.getMilliseconds() / 10)),
3411
+ offsetSign,
3412
+ padDatePart2(offsetHours),
3413
+ ":",
3414
+ padDatePart2(offsetRemainingMinutes)
3415
+ ].join("");
3416
+ }
3417
+ function nowIso() {
3418
+ return formatLocalTimestamp2();
3419
+ }
3420
+ function createMessage2(role, content, metadata) {
3421
+ return {
3422
+ id: createId2("msg"),
3423
+ role,
3424
+ content,
3425
+ createdAt: nowIso(),
3426
+ ...metadata ? { metadata } : {}
3427
+ };
3428
+ }
3429
+ function createSession(input) {
3430
+ const createdAt = nowIso();
3431
+ const messages = [
3432
+ createMessage2("user", input.input)
3433
+ ];
3434
+ return {
3435
+ version: SESSION_SCHEMA_VERSION,
3436
+ id: input.id ?? createId2("ses"),
3437
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
3438
+ systemPrompt: input.systemPrompt,
3439
+ input: input.input,
3440
+ messages,
3441
+ log: [],
3442
+ skills: input.skills ?? [],
3443
+ createdAt,
3444
+ updatedAt: createdAt,
3445
+ ...input.title ? { title: input.title } : {}
3446
+ };
3447
+ }
3448
+ function appendUserTurn(session, input) {
3449
+ session.messages.push(createMessage2("user", input));
3450
+ session.updatedAt = nowIso();
3451
+ return session;
3452
+ }
3453
+
3454
+ // src/harness/session/session-manager.ts
3455
+ var SESSION_MANAGER_SCHEMA_VERSION = "0.4.4";
3456
+ function clone(value) {
3457
+ return JSON.parse(JSON.stringify(value));
3458
+ }
3459
+ function createSessionHeader(input) {
3460
+ const createdAt = nowIso();
3461
+ return {
3462
+ type: "header",
3463
+ id: input.id ?? createId2("ses"),
3464
+ version: SESSION_MANAGER_SCHEMA_VERSION,
3465
+ createdAt,
3466
+ updatedAt: createdAt,
3467
+ systemPrompt: input.systemPrompt,
3468
+ input: input.input,
3469
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
3470
+ skills: input.skills?.map(clone) ?? [],
3471
+ ...input.title ? { title: input.title } : {},
3472
+ currentLeafId: null
3473
+ };
3474
+ }
3475
+ function filterExecutionTurns(steps, filter = {}) {
3476
+ return steps.filter((step) => {
3477
+ if (filter.phase && step.phase !== filter.phase) return false;
3478
+ if (filter.afterMs !== void 0 && step.requestedAtMs < filter.afterMs) return false;
3479
+ return true;
3480
+ }).map(clone);
3481
+ }
3482
+ var InMemorySessionManager = class _InMemorySessionManager {
3483
+ constructor(header, entries = []) {
3484
+ this.header = header;
3485
+ this.entries = entries;
3486
+ }
3487
+ header;
3488
+ entries;
3489
+ static create(input) {
3490
+ return new _InMemorySessionManager(createSessionHeader(input));
3491
+ }
3492
+ static fromRecords(records) {
3493
+ const [header, ...entries] = records;
3494
+ if (!header || header.type !== "header") {
3495
+ throw new Error("Session records must start with a header.");
3496
+ }
3497
+ return new _InMemorySessionManager(clone(header), clone(entries));
3498
+ }
3499
+ getSessionId() {
3500
+ return this.header.id;
3501
+ }
3502
+ getSessionFile() {
3503
+ return void 0;
3504
+ }
3505
+ async getHeader() {
3506
+ return clone(this.header);
3507
+ }
3508
+ async appendMessage(message) {
3509
+ return this.appendEntry({ type: "message", message: clone(message) });
3510
+ }
3511
+ async appendOutcome(outcome) {
3512
+ return this.appendEntry({ type: "outcome", outcome: clone(outcome) });
3513
+ }
3514
+ async appendExecutionTurn(turn) {
3515
+ return this.appendEntry({
3516
+ type: "execution_turn",
3517
+ turn: clone({
3518
+ ...turn,
3519
+ sessionId: this.header.id
3520
+ })
3521
+ });
3522
+ }
3523
+ async appendCompaction(input) {
3524
+ return this.appendEntry({ type: "compaction", ...input });
3525
+ }
3526
+ async appendBranchSummary(input) {
3527
+ return this.appendEntry({ type: "branch_summary", ...input });
3528
+ }
3529
+ async appendSessionInfo(input) {
3530
+ this.header = {
3531
+ ...this.header,
3532
+ title: input.title,
3533
+ updatedAt: nowIso()
3534
+ };
3535
+ return this.appendEntry({ type: "session_info", title: input.title });
3536
+ }
3537
+ async appendCustom(input) {
3538
+ return this.appendEntry({ type: "custom", customType: input.customType, data: clone(input.data) });
3539
+ }
3540
+ async branch(entryId) {
3541
+ if (entryId !== null && !this.entries.some((entry) => entry.id === entryId)) {
3542
+ throw new Error(`Session entry not found: ${entryId}`);
3543
+ }
3544
+ this.header = {
3545
+ ...this.header,
3546
+ currentLeafId: entryId,
3547
+ updatedAt: nowIso()
3548
+ };
3549
+ }
3550
+ async buildAgentContext(input = {}) {
3551
+ const entries = this.entriesForLeaf(input.leafId ?? this.header.currentLeafId ?? null);
3552
+ return {
3553
+ systemPrompt: this.header.systemPrompt,
3554
+ messages: entries.filter((entry) => entry.type === "message").map((entry) => entry.message).map(clone),
3555
+ tools: input.tools?.slice() ?? [],
3556
+ skills: input.skills?.map(clone) ?? this.header.skills.map(clone)
3557
+ };
3558
+ }
3559
+ async listEntries() {
3560
+ return this.entries.map(clone);
3561
+ }
3562
+ async loadExecutionTurns(filter) {
3563
+ return filterExecutionTurns(
3564
+ this.entries.filter((entry) => entry.type === "execution_turn").map((entry) => entry.turn),
3565
+ filter
3566
+ );
3567
+ }
3568
+ appendImportedEntry(entry) {
3569
+ this.entries.push(clone(entry));
3570
+ this.header = {
3571
+ ...this.header,
3572
+ currentLeafId: entry.id,
3573
+ updatedAt: entry.timestamp
3574
+ };
3575
+ }
3576
+ appendEntry(input) {
3577
+ const timestamp = nowIso();
3578
+ const entry = {
3579
+ id: createId2("entry"),
3580
+ parentId: this.header.currentLeafId ?? null,
3581
+ timestamp,
3582
+ ...input
3583
+ };
3584
+ this.entries.push(entry);
3585
+ this.header = {
3586
+ ...this.header,
3587
+ currentLeafId: entry.id,
3588
+ updatedAt: timestamp
3589
+ };
3590
+ return entry.id;
3591
+ }
3592
+ entriesForLeaf(leafId) {
3593
+ if (!leafId) {
3594
+ return [];
3595
+ }
3596
+ const byId = new Map(this.entries.map((entry) => [entry.id, entry]));
3597
+ const ordered = [];
3598
+ let currentId = leafId;
3599
+ while (currentId) {
3600
+ const entry = byId.get(currentId);
3601
+ if (!entry) {
3602
+ throw new Error(`Session entry not found: ${currentId}`);
3603
+ }
3604
+ ordered.unshift(entry);
3605
+ currentId = entry.parentId;
3606
+ }
3607
+ return ordered;
3608
+ }
3609
+ };
3610
+ function summarizeSessionManagerRecords(records) {
3611
+ const [header, ...entries] = records;
3612
+ if (!header || header.type !== "header") {
3613
+ throw new Error("Session records must start with a header.");
3614
+ }
3615
+ const messages = entries.filter((entry) => entry.type === "message").map((entry) => entry.message);
3616
+ const latestMessage = messages.at(-1)?.content;
3617
+ return {
3618
+ id: header.id,
3619
+ ...header.title ? { title: header.title } : {},
3620
+ createdAt: header.createdAt,
3621
+ updatedAt: header.updatedAt,
3622
+ messageCount: messages.length,
3623
+ ...latestMessage ? { latestMessage } : {}
3624
+ };
3625
+ }
3626
+
3627
+ // src/harness/session/jsonl.ts
3628
+ import { appendFile, mkdir as mkdir2, readFile as readFile3, readdir as readdir2, stat as stat3, unlink, writeFile as writeFile2 } from "fs/promises";
3629
+ import { join as join2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
3630
+ var SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_-]+$/;
3631
+ function isPathInside(parent, child) {
3632
+ const relativePath = relative2(parent, child);
3633
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !relativePath.includes(`..${sep2}`);
3634
+ }
3635
+ function safeSessionPath(sessionsDir, id) {
3636
+ if (!SESSION_ID_PATTERN.test(id)) {
3637
+ return void 0;
3638
+ }
3639
+ const root = resolve4(sessionsDir);
3640
+ const path = resolve4(root, `${id}.jsonl`);
3641
+ return isPathInside(root, path) ? path : void 0;
3642
+ }
3643
+ function parseRecord(line) {
3644
+ const value = JSON.parse(line);
3645
+ if (!value || typeof value !== "object" || !("type" in value)) {
3646
+ throw new Error("Invalid session JSONL record.");
3647
+ }
3648
+ return value;
3649
+ }
3650
+ async function readRecords(path) {
3651
+ const text = await readFile3(path, "utf8").catch((error) => {
3652
+ if (error.code === "ENOENT") {
3653
+ return void 0;
3654
+ }
3655
+ throw error;
3656
+ });
3657
+ if (!text) {
3658
+ return void 0;
3659
+ }
3660
+ const records = text.split("\n").map((line) => line.trim()).filter(Boolean).map(parseRecord);
3661
+ const header = records[0];
3662
+ if (!header || header.type !== "header") {
3663
+ throw new Error("Session JSONL must start with a header record.");
3664
+ }
3665
+ const lastEntry = records.slice(1).reverse().find((record) => record.type !== "header");
3666
+ return [
3667
+ {
3668
+ ...header,
3669
+ updatedAt: lastEntry?.timestamp ?? header.updatedAt,
3670
+ currentLeafId: lastEntry?.id ?? header.currentLeafId ?? null
3671
+ },
3672
+ ...records.slice(1)
3673
+ ];
3674
+ }
3675
+ async function appendRecord(path, record) {
3676
+ await appendFile(path, `${JSON.stringify(record)}
3677
+ `, "utf8");
3678
+ }
3679
+ var LocalJsonlSessionManager = class _LocalJsonlSessionManager {
3680
+ constructor(sessionsDir, filePath, inner) {
3681
+ this.sessionsDir = sessionsDir;
3682
+ this.filePath = filePath;
3683
+ this.inner = inner;
3684
+ }
3685
+ sessionsDir;
3686
+ filePath;
3687
+ inner;
3688
+ static async create(sessionsDir, input) {
3689
+ const header = createSessionHeader(input);
3690
+ const path = safeSessionPath(sessionsDir, header.id);
3691
+ if (!path) {
3692
+ throw new Error(`Invalid session id: ${header.id}`);
3693
+ }
3694
+ const exists = await stat3(path).then(() => true).catch((error) => {
3695
+ if (error.code === "ENOENT") {
3696
+ return false;
3697
+ }
3698
+ throw error;
3699
+ });
3700
+ if (exists) {
3701
+ throw new Error(`Session already exists: ${header.id}`);
3702
+ }
3703
+ await mkdir2(sessionsDir, { recursive: true });
3704
+ await writeFile2(path, `${JSON.stringify(header)}
3705
+ `, "utf8");
3706
+ return new _LocalJsonlSessionManager(
3707
+ sessionsDir,
3708
+ path,
3709
+ InMemorySessionManager.fromRecords([header])
3710
+ );
3711
+ }
3712
+ static async open(sessionsDir, id) {
3713
+ const path = safeSessionPath(sessionsDir, id);
3714
+ if (!path) {
3715
+ return void 0;
3716
+ }
3717
+ const records = await readRecords(path);
3718
+ if (!records) {
3719
+ return void 0;
3720
+ }
3721
+ const header = records[0];
3722
+ if (header.id !== id) {
3723
+ throw new Error(`Session id mismatch: expected ${id}, found ${header.id}`);
3724
+ }
3725
+ return new _LocalJsonlSessionManager(
3726
+ sessionsDir,
3727
+ path,
3728
+ InMemorySessionManager.fromRecords(records)
3729
+ );
3730
+ }
3731
+ static async list(sessionsDir) {
3732
+ const entries = await readdir2(sessionsDir, { withFileTypes: true }).catch((error) => {
3733
+ if (error.code === "ENOENT") {
3734
+ return [];
3735
+ }
3736
+ throw error;
3737
+ });
3738
+ const sessions = await Promise.all(
3739
+ entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map(async (entry) => {
3740
+ const path = join2(sessionsDir, entry.name);
3741
+ const records = await readRecords(path);
3742
+ return records ? summarizeSessionManagerRecords(records) : void 0;
3743
+ })
3744
+ );
3745
+ return sessions.filter((session) => Boolean(session)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
3746
+ }
3747
+ static async delete(sessionsDir, id) {
3748
+ const path = safeSessionPath(sessionsDir, id);
3749
+ if (!path) {
3750
+ return false;
3751
+ }
3752
+ return unlink(path).then(() => true).catch((error) => {
3753
+ if (error.code === "ENOENT") {
3754
+ return false;
3755
+ }
3756
+ throw error;
3757
+ });
3758
+ }
3759
+ getSessionId() {
3760
+ return this.inner.getSessionId();
3761
+ }
3762
+ getSessionFile() {
3763
+ return this.filePath;
3764
+ }
3765
+ async getHeader() {
3766
+ return this.inner.getHeader();
3767
+ }
3768
+ async appendMessage(message) {
3769
+ return this.appendThroughInner(() => this.inner.appendMessage(message));
3770
+ }
3771
+ async appendOutcome(outcome) {
3772
+ return this.appendThroughInner(() => this.inner.appendOutcome(outcome));
3773
+ }
3774
+ async appendExecutionTurn(turn) {
3775
+ return this.appendThroughInner(() => this.inner.appendExecutionTurn(turn));
3776
+ }
3777
+ async appendCompaction(input) {
3778
+ return this.appendThroughInner(() => this.inner.appendCompaction(input));
3779
+ }
3780
+ async appendBranchSummary(input) {
3781
+ return this.appendThroughInner(() => this.inner.appendBranchSummary(input));
3782
+ }
3783
+ async appendSessionInfo(input) {
3784
+ return this.appendThroughInner(() => this.inner.appendSessionInfo(input));
3785
+ }
3786
+ async appendCustom(input) {
3787
+ return this.appendThroughInner(() => this.inner.appendCustom(input));
3788
+ }
3789
+ async branch(entryId) {
3790
+ await this.inner.branch(entryId);
3791
+ if (entryId) {
3792
+ await this.appendThroughInner(() => this.inner.appendBranchSummary({
3793
+ fromId: entryId,
3794
+ summary: `Selected branch at ${entryId}`
3795
+ }));
3796
+ }
3797
+ }
3798
+ async buildAgentContext(input) {
3799
+ return this.inner.buildAgentContext(input);
3800
+ }
3801
+ async listEntries() {
3802
+ return this.inner.listEntries();
3803
+ }
3804
+ async loadExecutionTurns(filter) {
3805
+ return this.inner.loadExecutionTurns(filter);
3806
+ }
3807
+ async appendThroughInner(append) {
3808
+ const before = await this.inner.listEntries();
3809
+ const entryId = await append();
3810
+ const after = await this.inner.listEntries();
3811
+ const entry = after.find((candidate) => candidate.id === entryId);
3812
+ if (!entry || before.some((candidate) => candidate.id === entry.id)) {
3813
+ throw new Error(`Session entry was not appended: ${entryId}`);
3814
+ }
3815
+ await mkdir2(this.sessionsDir, { recursive: true });
3816
+ await appendRecord(this.filePath, entry);
3817
+ return entryId;
3818
+ }
3819
+ };
3820
+
3821
+ // src/harness/skills.ts
3822
+ import { existsSync as existsSync3 } from "fs";
3823
+ import { readFile as readFile4 } from "fs/promises";
3824
+ import { basename as basename2, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, join as join4, resolve as resolve6 } from "path";
3825
+
3826
+ // src/harness/env/path.ts
3827
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
3828
+ import { homedir } from "os";
3829
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute2, join as join3, parse, relative as relative3, resolve as resolve5, sep as sep3 } from "path";
3830
+ var WORKSPACE_ENV = "ROWAN_WORKSPACE";
3831
+ var RUNTIME_ENV = "ROWAN_RUNTIME";
3832
+ var PACKAGED_ENV = "ROWAN_PACKAGED";
3833
+ var BINARY_WORKSPACE_DIR = ".rowan";
3834
+ function nonEmptyEnv(env, key) {
3835
+ const value = env[key]?.trim();
3836
+ return value ? value : void 0;
3837
+ }
3838
+ function isTruthyEnvValue(value) {
3839
+ return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
3840
+ }
3841
+ function resolveUserPath(path, homeDir) {
3842
+ if (path === "~") {
3843
+ return homeDir;
3844
+ }
3845
+ if (path.startsWith("~/") || path.startsWith("~\\")) {
3846
+ return join3(homeDir, path.slice(2));
3847
+ }
3848
+ return resolve5(path);
3849
+ }
3850
+ function isSourceWorkspaceRoot(path) {
3851
+ const packagePath = join3(path, "package.json");
3852
+ if (!existsSync2(packagePath)) {
3853
+ return false;
3854
+ }
3855
+ try {
3856
+ const manifest = JSON.parse(readFileSync2(packagePath, "utf8"));
3857
+ return manifest.name === "rowan-agent" || Array.isArray(manifest.workspaces);
3858
+ } catch {
3859
+ return false;
3860
+ }
3861
+ }
3862
+ function findSourceWorkspaceRoot(startDir = process.cwd()) {
3863
+ let current = resolve5(startDir);
3864
+ const { root } = parse(current);
3865
+ while (true) {
3866
+ if (isSourceWorkspaceRoot(current)) {
3867
+ return current;
3868
+ }
3869
+ if (current === root) {
3870
+ return resolve5(startDir);
3871
+ }
3872
+ current = dirname4(current);
3873
+ }
3874
+ }
3875
+ function detectRuntimeMode(input = {}) {
3876
+ const env = input.env ?? process.env;
3877
+ const explicitMode = nonEmptyEnv(env, RUNTIME_ENV)?.toLowerCase();
3878
+ if (explicitMode === "source" || explicitMode === "binary") {
3879
+ return explicitMode;
3880
+ }
3881
+ if (isTruthyEnvValue(nonEmptyEnv(env, PACKAGED_ENV))) {
3882
+ return "binary";
3883
+ }
3884
+ const executable = basename(input.execPath ?? process.execPath).toLowerCase().replace(/\.exe$/, "");
3885
+ return executable === "bun" ? "source" : "binary";
3886
+ }
3887
+ function defaultSourceStartDir(options) {
3888
+ if (options.cwd) {
3889
+ return options.cwd;
3890
+ }
3891
+ const entrypoint = options.entrypoint ?? process.argv[1];
3892
+ if (entrypoint) {
3893
+ const entrypointPath = resolve5(process.cwd(), entrypoint);
3894
+ if (existsSync2(entrypointPath)) {
3895
+ return dirname4(entrypointPath);
3896
+ }
3897
+ }
3898
+ return process.cwd();
3899
+ }
3900
+ function resolveWorkspaceRoot(options = {}) {
3901
+ const env = options.env ?? process.env;
3902
+ const homeDir = options.homeDir ?? homedir();
3903
+ const override = nonEmptyEnv(env, WORKSPACE_ENV);
3904
+ if (override) {
3905
+ return resolveUserPath(override, homeDir);
3906
+ }
3907
+ const mode = options.mode ?? detectRuntimeMode(options);
3908
+ if (mode === "binary") {
3909
+ return homeDir;
3910
+ }
3911
+ return findSourceWorkspaceRoot(defaultSourceStartDir(options));
3912
+ }
3913
+ function resolveWorkspacePaths(options = {}) {
3914
+ const mode = options.mode ?? detectRuntimeMode(options);
3915
+ const cwd = resolveWorkspaceRoot({ ...options, mode });
3916
+ return {
3917
+ mode,
3918
+ cwd,
3919
+ rowanDir: join3(cwd, BINARY_WORKSPACE_DIR)
3920
+ };
3921
+ }
3922
+ function resolveInWorkspace(path, rootOrPaths) {
3923
+ if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) {
3924
+ return resolveUserPath(path, homedir());
3925
+ }
3926
+ if (isAbsolute2(path)) {
3927
+ return path;
3928
+ }
3929
+ const root = typeof rootOrPaths === "string" ? rootOrPaths : rootOrPaths.cwd;
3930
+ return resolve5(root, path);
3931
+ }
3932
+
3933
+ // src/harness/skills.ts
3934
+ function inferSkillName(path) {
3935
+ const file = basename2(path);
3936
+ if (file.toLowerCase() === "skill.md") {
3937
+ return basename2(dirname5(path));
3938
+ }
3939
+ const extension = extname2(file);
3940
+ return extension ? file.slice(0, -extension.length) : file;
3941
+ }
3942
+ function isExplicitPath(input) {
3943
+ return input.includes("/") || input.includes("\\") || Boolean(extname2(input));
3944
+ }
3945
+ function resolveSkillPath(input, workspace = resolveWorkspacePaths()) {
3946
+ if (isAbsolute3(input)) {
3947
+ return input;
3948
+ }
3949
+ if (!isExplicitPath(input)) {
3950
+ return join4(workspace.rowanDir, "skills", input, "SKILL.md");
3951
+ }
3952
+ const workspacePath = resolveInWorkspace(input, workspace);
3953
+ if (existsSync3(workspacePath)) {
3954
+ return workspacePath;
3955
+ }
3956
+ return resolve6(input);
3957
+ }
3958
+ function parseFrontmatter(raw) {
3959
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
3960
+ if (!match) return { frontmatter: {}, body: raw };
3961
+ const frontmatter = {};
3962
+ for (const line of match[1].split("\n")) {
3963
+ const idx = line.indexOf(":");
3964
+ if (idx === -1) continue;
3965
+ const key = line.slice(0, idx).trim();
3966
+ const value = line.slice(idx + 1).trim();
3967
+ if (key) frontmatter[key] = value;
3968
+ }
3969
+ return { frontmatter, body: raw.slice(match[0].length) };
3970
+ }
3971
+ async function loadSkill(path, workspace) {
3972
+ const resolved = resolveSkillPath(path, workspace);
3973
+ const raw = await readFile4(resolved, "utf8");
3974
+ const { frontmatter } = parseFrontmatter(raw);
3975
+ return {
3976
+ name: frontmatter.name ?? inferSkillName(resolved),
3977
+ description: frontmatter.description ?? "",
3978
+ filePath: resolved,
3979
+ baseDir: dirname5(resolved),
3980
+ disableModelInvocation: frontmatter["disable-model-invocation"] === "true"
3981
+ };
3982
+ }
3983
+ async function loadSkills(paths = [], workspace) {
3984
+ return Promise.all(paths.map((path) => loadSkill(path, workspace)));
3985
+ }
3986
+ export {
3987
+ AGENT_STATE_SCHEMA_VERSION,
3988
+ Agent,
3989
+ AgentEventStream,
3990
+ AgentMessageSchema,
3991
+ DEFAULT_PHASE_ID,
3992
+ EventStream,
3993
+ ExtensionRunner,
3994
+ HooksManager,
3995
+ InMemorySessionManager,
3996
+ LocalJsonlSessionManager,
3997
+ SESSION_MANAGER_SCHEMA_VERSION,
3998
+ SESSION_SCHEMA_VERSION,
3999
+ SkillSchema,
4000
+ appendUserTurn,
4001
+ buildModelRequest,
4002
+ buildSystemPrompt,
4003
+ conversationMessages,
4004
+ createAgentState,
4005
+ createBuiltinPhaseRegistry,
4006
+ createCoreTools,
4007
+ createDefaultPhaseRegistry,
4008
+ createEventBus,
4009
+ createExtension,
4010
+ createExtensionRunner,
4011
+ createExtensionRuntime,
4012
+ createId,
4013
+ createJson,
4014
+ createMessage,
4015
+ createPhaseRegistry,
4016
+ createSession,
4017
+ createSessionHeader,
4018
+ createSourceInfo,
4019
+ createTimestamp,
4020
+ definePhase,
4021
+ discoverAndLoadExtensions,
4022
+ getBuiltinExtensions,
4023
+ getBuiltinRunner,
4024
+ getGlobalHooks,
4025
+ isBuiltinPhaseOverride,
4026
+ isBuiltinSource,
4027
+ latestUserInput,
4028
+ loadExtensionFromFactory,
4029
+ loadExtensionFromFactorySync,
4030
+ loadExtensions,
4031
+ loadSkill,
4032
+ loadSkills,
4033
+ resetGlobalHooks,
4034
+ resolveInWorkspace,
4035
+ resolveSkillPath,
4036
+ resolveWorkspacePaths,
4037
+ serializeSkills,
4038
+ summarizeSessionManagerRecords
4039
+ };