@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/README.md +74 -0
- package/dist/index.cjs +4125 -0
- package/dist/index.d.cts +2049 -0
- package/dist/index.d.ts +2049 -0
- package/dist/index.js +4039 -0
- package/package.json +29 -0
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
+
};
|