@ap3x/agent-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.d.ts +2 -6
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent.d.ts +5 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/backend.d.ts +22 -2
- package/dist/backend.d.ts.map +1 -1
- package/dist/execution-controller.d.ts +83 -0
- package/dist/execution-controller.d.ts.map +1 -0
- package/dist/harness.d.ts +5 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1306 -568
- package/dist/policy.d.ts +60 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/run-context.d.ts +26 -0
- package/dist/run-context.d.ts.map +1 -0
- package/dist/run-result.d.ts +35 -0
- package/dist/run-result.d.ts.map +1 -0
- package/dist/session-attempt.d.ts +25 -0
- package/dist/session-attempt.d.ts.map +1 -0
- package/dist/session-repo.d.ts.map +1 -1
- package/dist/session.d.ts +2 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/tools.d.ts +2 -6
- package/dist/tools.d.ts.map +1 -1
- package/dist/types.d.ts +16 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -143,6 +143,436 @@ function mapLimit(items, concurrency, fn) {
|
|
|
143
143
|
import {
|
|
144
144
|
validateToolArguments
|
|
145
145
|
} from "@ap3x/ai";
|
|
146
|
+
|
|
147
|
+
// src/execution-controller.ts
|
|
148
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
149
|
+
import {
|
|
150
|
+
withProviderExecution
|
|
151
|
+
} from "@ap3x/ai";
|
|
152
|
+
|
|
153
|
+
// src/run-context.ts
|
|
154
|
+
function createRunContext(options = {}) {
|
|
155
|
+
if (options.parent && options.tenantId !== void 0 && options.tenantId !== options.parent.tenantId) {
|
|
156
|
+
throw new Error("A child run cannot change tenant identity");
|
|
157
|
+
}
|
|
158
|
+
if (options.parent && (options.controller && options.controller !== options.parent.controller || options.policy && options.policy !== options.parent.policy)) {
|
|
159
|
+
throw new Error("A child run cannot replace execution authority");
|
|
160
|
+
}
|
|
161
|
+
const controller = options.parent?.controller ?? options.controller;
|
|
162
|
+
const policy = options.parent?.policy ?? options.policy;
|
|
163
|
+
if (policy && !Object.isFrozen(policy)) Object.freeze(policy);
|
|
164
|
+
return Object.freeze({
|
|
165
|
+
runId: options.runId ?? uuidv7(),
|
|
166
|
+
parentRunId: options.parent?.runId,
|
|
167
|
+
tenantId: options.parent?.tenantId ?? options.tenantId,
|
|
168
|
+
signal: options.parent?.signal ?? options.signal ?? controller?.signal,
|
|
169
|
+
controller,
|
|
170
|
+
policy,
|
|
171
|
+
depth: options.parent ? options.parent.depth + 1 : 0,
|
|
172
|
+
attempt: options.parent?.attempt ?? options.attempt ?? 1,
|
|
173
|
+
fence: options.parent?.fence ?? options.fence ?? 0
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/execution-controller.ts
|
|
178
|
+
var ExecutionLimitError = class extends Error {
|
|
179
|
+
constructor(limit, resources) {
|
|
180
|
+
super(`Execution refused: ${limit}`);
|
|
181
|
+
this.limit = limit;
|
|
182
|
+
this.resources = resources;
|
|
183
|
+
this.name = "ExecutionLimitError";
|
|
184
|
+
}
|
|
185
|
+
limit;
|
|
186
|
+
resources;
|
|
187
|
+
reasonCode = "execution_limit";
|
|
188
|
+
};
|
|
189
|
+
var ExecutionController = class {
|
|
190
|
+
limits;
|
|
191
|
+
signal;
|
|
192
|
+
abortController = new AbortController();
|
|
193
|
+
startedAt = Date.now();
|
|
194
|
+
values = {
|
|
195
|
+
turns: 0,
|
|
196
|
+
toolCalls: 0,
|
|
197
|
+
childAgents: 0,
|
|
198
|
+
activeChildren: 0,
|
|
199
|
+
tokens: 0,
|
|
200
|
+
estimatedUsd: 0,
|
|
201
|
+
reservedTokens: 0,
|
|
202
|
+
reservedEstimatedUsd: 0,
|
|
203
|
+
unknownPricing: false,
|
|
204
|
+
usageIncomplete: false
|
|
205
|
+
};
|
|
206
|
+
terminalLimit;
|
|
207
|
+
waiters = /* @__PURE__ */ new Set();
|
|
208
|
+
timer;
|
|
209
|
+
detach;
|
|
210
|
+
constructor(limits = {}, signal) {
|
|
211
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
212
|
+
if (value === void 0) continue;
|
|
213
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`Invalid execution limit: ${name}`);
|
|
214
|
+
if (name !== "maxEstimatedUsd" && name !== "opaqueChildEstimatedUsd" && !Number.isInteger(value))
|
|
215
|
+
throw new Error(`Execution limit must be an integer: ${name}`);
|
|
216
|
+
}
|
|
217
|
+
this.limits = Object.freeze({ ...limits });
|
|
218
|
+
this.signal = this.abortController.signal;
|
|
219
|
+
if (signal) {
|
|
220
|
+
const cancel = () => this.cancel();
|
|
221
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
222
|
+
this.detach = () => signal.removeEventListener("abort", cancel);
|
|
223
|
+
if (signal.aborted) this.cancel();
|
|
224
|
+
}
|
|
225
|
+
if (limits.maxElapsedMs !== void 0) {
|
|
226
|
+
this.timer = setTimeout(() => this.stop("maxElapsedMs"), limits.maxElapsedMs);
|
|
227
|
+
this.timer.unref?.();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
snapshot() {
|
|
231
|
+
return { ...this.values, elapsedMs: Date.now() - this.startedAt, limit: this.terminalLimit };
|
|
232
|
+
}
|
|
233
|
+
cancel() {
|
|
234
|
+
this.stop("cancelled");
|
|
235
|
+
}
|
|
236
|
+
dispose() {
|
|
237
|
+
if (this.timer) clearTimeout(this.timer);
|
|
238
|
+
this.detach?.();
|
|
239
|
+
}
|
|
240
|
+
stop(limit) {
|
|
241
|
+
this.terminalLimit ??= limit;
|
|
242
|
+
this.abortController.abort();
|
|
243
|
+
for (const wake of this.waiters) wake();
|
|
244
|
+
}
|
|
245
|
+
refuse(limit) {
|
|
246
|
+
this.stop(limit);
|
|
247
|
+
throw new ExecutionLimitError(limit, this.snapshot());
|
|
248
|
+
}
|
|
249
|
+
assertActive(signal) {
|
|
250
|
+
if (this.limits.maxElapsedMs !== void 0 && Date.now() - this.startedAt >= this.limits.maxElapsedMs)
|
|
251
|
+
this.stop("maxElapsedMs");
|
|
252
|
+
if (this.signal.aborted)
|
|
253
|
+
throw new ExecutionLimitError(this.terminalLimit ?? "cancelled", this.snapshot());
|
|
254
|
+
if (signal?.aborted) throw new ExecutionLimitError("cancelled", this.snapshot());
|
|
255
|
+
}
|
|
256
|
+
consume(key, cap) {
|
|
257
|
+
this.assertActive();
|
|
258
|
+
const maximum = this.limits[cap];
|
|
259
|
+
if (maximum !== void 0 && this.values[key] >= maximum) this.refuse(cap);
|
|
260
|
+
this.values[key]++;
|
|
261
|
+
}
|
|
262
|
+
turn() {
|
|
263
|
+
this.consume("turns", "maxTurns");
|
|
264
|
+
}
|
|
265
|
+
tool() {
|
|
266
|
+
this.consume("toolCalls", "maxToolCalls");
|
|
267
|
+
}
|
|
268
|
+
assertDepth(depth) {
|
|
269
|
+
this.assertActive();
|
|
270
|
+
if (this.limits.maxDepth !== void 0 && depth > this.limits.maxDepth) this.refuse("maxDepth");
|
|
271
|
+
}
|
|
272
|
+
async child(depth, signal) {
|
|
273
|
+
this.assertActive(signal);
|
|
274
|
+
if (this.limits.maxDepth !== void 0 && depth > this.limits.maxDepth) this.refuse("maxDepth");
|
|
275
|
+
if (this.limits.maxActiveChildren === 0) this.refuse("maxActiveChildren");
|
|
276
|
+
this.consume("childAgents", "maxChildAgents");
|
|
277
|
+
if (this.limits.maxActiveChildren !== void 0 && this.values.activeChildren >= this.limits.maxActiveChildren)
|
|
278
|
+
this.refuse("maxActiveChildren");
|
|
279
|
+
this.assertActive(signal);
|
|
280
|
+
this.values.activeChildren++;
|
|
281
|
+
let released = false;
|
|
282
|
+
return () => {
|
|
283
|
+
if (released) return;
|
|
284
|
+
released = true;
|
|
285
|
+
this.values.activeChildren--;
|
|
286
|
+
for (const wake of this.waiters) wake();
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
reserve(tokens, estimatedUsd) {
|
|
290
|
+
this.assertActive();
|
|
291
|
+
if (!Number.isFinite(tokens) || tokens < 0 || estimatedUsd !== void 0 && (!Number.isFinite(estimatedUsd) || estimatedUsd < 0))
|
|
292
|
+
throw new Error("Invalid execution reservation");
|
|
293
|
+
if (estimatedUsd === void 0) {
|
|
294
|
+
this.values.unknownPricing = true;
|
|
295
|
+
if (this.limits.maxEstimatedUsd !== void 0) this.refuse("unknown_pricing");
|
|
296
|
+
}
|
|
297
|
+
const usd = estimatedUsd ?? 0;
|
|
298
|
+
if (this.limits.maxTokens !== void 0 && this.values.tokens + this.values.reservedTokens + tokens > this.limits.maxTokens)
|
|
299
|
+
this.refuse("maxTokens");
|
|
300
|
+
if (this.limits.maxEstimatedUsd !== void 0 && this.values.estimatedUsd + this.values.reservedEstimatedUsd + usd > this.limits.maxEstimatedUsd)
|
|
301
|
+
this.refuse("maxEstimatedUsd");
|
|
302
|
+
this.values.reservedTokens += tokens;
|
|
303
|
+
this.values.reservedEstimatedUsd += usd;
|
|
304
|
+
let settled = false;
|
|
305
|
+
return {
|
|
306
|
+
settle: (usage) => {
|
|
307
|
+
if (settled) return;
|
|
308
|
+
settled = true;
|
|
309
|
+
this.values.reservedTokens -= tokens;
|
|
310
|
+
this.values.reservedEstimatedUsd -= usd;
|
|
311
|
+
if (!usage) this.values.usageIncomplete = true;
|
|
312
|
+
this.values.tokens += usage?.totalTokens ?? tokens;
|
|
313
|
+
if (estimatedUsd !== void 0) this.values.estimatedUsd += usage?.cost.total ?? usd;
|
|
314
|
+
if (this.limits.maxTokens !== void 0 && this.values.tokens + this.values.reservedTokens > this.limits.maxTokens)
|
|
315
|
+
this.stop("maxTokens");
|
|
316
|
+
if (this.limits.maxEstimatedUsd !== void 0 && this.values.estimatedUsd + this.values.reservedEstimatedUsd > this.limits.maxEstimatedUsd)
|
|
317
|
+
this.stop("maxEstimatedUsd");
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var storage = new AsyncLocalStorage();
|
|
323
|
+
function currentRunContext() {
|
|
324
|
+
return storage.getStore();
|
|
325
|
+
}
|
|
326
|
+
function withRunContext(context, fn) {
|
|
327
|
+
const parent = currentRunContext();
|
|
328
|
+
if (parent && (context.controller !== parent.controller || context.tenantId !== parent.tenantId || context.policy !== parent.policy))
|
|
329
|
+
throw new Error("Nested work cannot replace root execution authority");
|
|
330
|
+
if (parent && context.depth < parent.depth)
|
|
331
|
+
throw new Error("Nested work cannot reset execution depth");
|
|
332
|
+
return storage.run(
|
|
333
|
+
context,
|
|
334
|
+
() => withProviderExecution(
|
|
335
|
+
context.controller ? { signal: context.controller.signal, admit: reserveProviderCall } : void 0,
|
|
336
|
+
fn
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function resolveRunContext(context, signal) {
|
|
341
|
+
const parent = currentRunContext();
|
|
342
|
+
if (parent && context && (context.controller !== parent.controller || context.tenantId !== parent.tenantId || context.policy !== parent.policy))
|
|
343
|
+
throw new Error("Nested work cannot replace root execution authority");
|
|
344
|
+
if (parent) return parent;
|
|
345
|
+
return context ?? createRunContext({ signal });
|
|
346
|
+
}
|
|
347
|
+
var workflowDepth = new AsyncLocalStorage();
|
|
348
|
+
function withWorkflowContext(fn) {
|
|
349
|
+
const parent = currentRunContext();
|
|
350
|
+
const nested = (workflowDepth.getStore() ?? 0) > 0;
|
|
351
|
+
const context = parent ? nested ? createRunContext({ parent }) : parent : createRunContext();
|
|
352
|
+
context.controller?.assertDepth(context.depth);
|
|
353
|
+
return withRunContext(context, () => workflowDepth.run((workflowDepth.getStore() ?? 0) + 1, fn));
|
|
354
|
+
}
|
|
355
|
+
var opaqueScope = new AsyncLocalStorage();
|
|
356
|
+
function hasOpaqueReservation(context) {
|
|
357
|
+
return opaqueScope.getStore() === context.runId;
|
|
358
|
+
}
|
|
359
|
+
async function executeMember(agent, task, options) {
|
|
360
|
+
const parent = resolveRunContext(options?.context, options?.signal);
|
|
361
|
+
const context = createRunContext({ parent });
|
|
362
|
+
const controller = context.controller;
|
|
363
|
+
const release = await controller?.child(context.depth, options?.signal ?? context.signal);
|
|
364
|
+
let reservation;
|
|
365
|
+
try {
|
|
366
|
+
if (controller && !agent.supports?.("execution-controls")) {
|
|
367
|
+
const { maxTokens, maxEstimatedUsd, opaqueChildTokens, opaqueChildEstimatedUsd } = controller.limits;
|
|
368
|
+
if (maxTokens !== void 0 && opaqueChildTokens === void 0)
|
|
369
|
+
throw new Error("Opaque backend requires opaqueChildTokens allocation");
|
|
370
|
+
if (maxEstimatedUsd !== void 0 && opaqueChildEstimatedUsd === void 0)
|
|
371
|
+
throw new Error("Opaque backend requires opaqueChildEstimatedUsd allocation");
|
|
372
|
+
reservation = controller.reserve(opaqueChildTokens ?? 0, opaqueChildEstimatedUsd);
|
|
373
|
+
}
|
|
374
|
+
controller?.assertActive(options?.signal);
|
|
375
|
+
const memberOptions = controller || context.policy || options?.context ? { ...options, context } : options;
|
|
376
|
+
const run = () => withRunContext(context, () => agent.run(task, memberOptions));
|
|
377
|
+
const result = await (reservation ? opaqueScope.run(context.runId, run) : run());
|
|
378
|
+
reservation?.settle(
|
|
379
|
+
result.usageIncomplete ? void 0 : result.aggregateUsage ?? result.usage
|
|
380
|
+
);
|
|
381
|
+
if (controller?.snapshot().limit)
|
|
382
|
+
return {
|
|
383
|
+
...result,
|
|
384
|
+
stopReason: "length",
|
|
385
|
+
outcome: "partial",
|
|
386
|
+
reasonCode: "execution_limit",
|
|
387
|
+
execution: controller.snapshot()
|
|
388
|
+
};
|
|
389
|
+
return result;
|
|
390
|
+
} finally {
|
|
391
|
+
reservation?.settle();
|
|
392
|
+
release?.();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function reserveProviderCall(model, context, options) {
|
|
396
|
+
const controller = currentRunContext()?.controller;
|
|
397
|
+
if (!controller) return void 0;
|
|
398
|
+
controller.assertActive(options?.signal);
|
|
399
|
+
const input = Buffer.byteLength(JSON.stringify(context), "utf8") + 1024;
|
|
400
|
+
const output = options?.maxTokens ?? model.maxTokens;
|
|
401
|
+
const rates = Object.values(model.cost ?? {});
|
|
402
|
+
const known = rates.length === 4 && rates.every((rate) => Number.isFinite(rate) && rate >= 0);
|
|
403
|
+
return controller.reserve(
|
|
404
|
+
input + output,
|
|
405
|
+
known ? (input * Math.max(model.cost.input, model.cost.cacheRead, model.cost.cacheWrite) + output * model.cost.output) / 1e6 : void 0
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
function withProviderReservation(reservation, call) {
|
|
409
|
+
if (!reservation) return call();
|
|
410
|
+
let claimed = false;
|
|
411
|
+
return withProviderExecution(
|
|
412
|
+
{
|
|
413
|
+
signal: currentRunContext()?.controller?.signal,
|
|
414
|
+
admit: (model, context, options) => {
|
|
415
|
+
if (!claimed) {
|
|
416
|
+
claimed = true;
|
|
417
|
+
return reservation;
|
|
418
|
+
}
|
|
419
|
+
return reserveProviderCall(model, context, options);
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
call
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
async function controlledProviderCall(model, context, options, call) {
|
|
426
|
+
const reservation = reserveProviderCall(model, context, options);
|
|
427
|
+
try {
|
|
428
|
+
const result = await withProviderReservation(reservation, call);
|
|
429
|
+
reservation?.settle(result.usage);
|
|
430
|
+
return result;
|
|
431
|
+
} finally {
|
|
432
|
+
reservation?.settle();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/policy.ts
|
|
437
|
+
import { createHash } from "crypto";
|
|
438
|
+
var PolicyDeniedError = class extends Error {
|
|
439
|
+
reasonCode = "policy_denied";
|
|
440
|
+
constructor(message) {
|
|
441
|
+
super(message);
|
|
442
|
+
this.name = "PolicyDeniedError";
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
function canonical(value) {
|
|
446
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
447
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
448
|
+
const object = value;
|
|
449
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonical(object[key])}`).join(",")}}`;
|
|
450
|
+
}
|
|
451
|
+
function argumentsHash(args) {
|
|
452
|
+
return createHash("sha256").update(canonical(args)).digest("hex");
|
|
453
|
+
}
|
|
454
|
+
function deepFreeze(value) {
|
|
455
|
+
if (value && typeof value === "object") {
|
|
456
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
457
|
+
Object.freeze(value);
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
function immutableArguments(args) {
|
|
462
|
+
return deepFreeze(JSON.parse(JSON.stringify(args)));
|
|
463
|
+
}
|
|
464
|
+
async function authorizeTool(context, toolName, toolCallId, args, signal) {
|
|
465
|
+
context?.controller?.assertActive(signal);
|
|
466
|
+
if (signal?.aborted || context?.signal?.aborted)
|
|
467
|
+
throw new PolicyDeniedError("Tool call cancelled");
|
|
468
|
+
const policy = context?.policy;
|
|
469
|
+
if (!policy || !context) return;
|
|
470
|
+
const request = Object.freeze({
|
|
471
|
+
toolName,
|
|
472
|
+
toolCallId,
|
|
473
|
+
args,
|
|
474
|
+
argumentsHash: argumentsHash(args),
|
|
475
|
+
runId: context.runId,
|
|
476
|
+
tenantId: context.tenantId,
|
|
477
|
+
attempt: context.attempt,
|
|
478
|
+
fence: context.fence,
|
|
479
|
+
policyVersion: policy.version
|
|
480
|
+
});
|
|
481
|
+
let decision;
|
|
482
|
+
try {
|
|
483
|
+
decision = await policy.evaluate(request);
|
|
484
|
+
} catch {
|
|
485
|
+
throw new PolicyDeniedError("Execution policy failed");
|
|
486
|
+
}
|
|
487
|
+
if (decision?.action === "deny") throw new PolicyDeniedError(decision.reason);
|
|
488
|
+
if (decision?.action === "allow") {
|
|
489
|
+
context.controller?.assertActive(signal);
|
|
490
|
+
if (signal?.aborted || context.signal?.aborted)
|
|
491
|
+
throw new PolicyDeniedError("Tool call cancelled");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (decision?.action !== "require-approval" || !policy.requestApproval)
|
|
495
|
+
throw new PolicyDeniedError("No valid approval authority available");
|
|
496
|
+
const expiresInMs = decision.expiresInMs ?? 3e5;
|
|
497
|
+
if (!Number.isFinite(expiresInMs) || expiresInMs <= 0)
|
|
498
|
+
throw new PolicyDeniedError("Invalid approval expiry");
|
|
499
|
+
const now = policy.now ?? Date.now;
|
|
500
|
+
const approval = Object.freeze({
|
|
501
|
+
...request,
|
|
502
|
+
approvalId: uuidv7(),
|
|
503
|
+
expiresAt: now() + expiresInMs,
|
|
504
|
+
reason: decision.reason
|
|
505
|
+
});
|
|
506
|
+
const abort = new AbortController();
|
|
507
|
+
const signals = [
|
|
508
|
+
...new Set(
|
|
509
|
+
[signal, context.signal, context.controller?.signal].filter((s) => !!s)
|
|
510
|
+
)
|
|
511
|
+
];
|
|
512
|
+
const cancel = () => abort.abort();
|
|
513
|
+
for (const s of signals) s.addEventListener("abort", cancel, { once: true });
|
|
514
|
+
if (signals.some((s) => s.aborted)) abort.abort();
|
|
515
|
+
let timer;
|
|
516
|
+
let onAbort;
|
|
517
|
+
try {
|
|
518
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
519
|
+
onAbort = () => reject(new PolicyDeniedError("Approval cancelled or expired"));
|
|
520
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
521
|
+
if (abort.signal.aborted) onAbort();
|
|
522
|
+
timer = setTimeout(cancel, expiresInMs);
|
|
523
|
+
});
|
|
524
|
+
const resolution = await Promise.race([
|
|
525
|
+
policy.requestApproval(approval, abort.signal),
|
|
526
|
+
interrupted
|
|
527
|
+
]);
|
|
528
|
+
if (!resolution.approved || now() >= approval.expiresAt || abort.signal.aborted || signals.some((s) => s.aborted))
|
|
529
|
+
throw new PolicyDeniedError("Approval denied, expired or cancelled");
|
|
530
|
+
for (const key of [
|
|
531
|
+
"approvalId",
|
|
532
|
+
"argumentsHash",
|
|
533
|
+
"runId",
|
|
534
|
+
"attempt",
|
|
535
|
+
"fence",
|
|
536
|
+
"policyVersion"
|
|
537
|
+
])
|
|
538
|
+
if (resolution[key] !== approval[key])
|
|
539
|
+
throw new PolicyDeniedError(`Stale approval binding: ${key}`);
|
|
540
|
+
if (argumentsHash(args) !== request.argumentsHash)
|
|
541
|
+
throw new PolicyDeniedError("Approved arguments changed");
|
|
542
|
+
context.controller?.assertActive(signal);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error instanceof PolicyDeniedError) throw error;
|
|
545
|
+
throw new PolicyDeniedError("Approval authority failed");
|
|
546
|
+
} finally {
|
|
547
|
+
if (timer) clearTimeout(timer);
|
|
548
|
+
for (const s of signals) s.removeEventListener("abort", cancel);
|
|
549
|
+
if (onAbort) abort.signal.removeEventListener("abort", onAbort);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function developmentPolicy() {
|
|
553
|
+
return Object.freeze({
|
|
554
|
+
version: "development-v1",
|
|
555
|
+
evaluate: () => ({ action: "allow" })
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function readOnlyPolicy(tools = ["read", "ls", "find", "grep"]) {
|
|
559
|
+
const names = new Set(tools);
|
|
560
|
+
return Object.freeze({
|
|
561
|
+
version: `read-only-v1:${argumentsHash([...names].sort())}`,
|
|
562
|
+
evaluate: (request) => names.has(request.toolName) ? { action: "allow" } : { action: "deny", reason: "Read-only policy" }
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function productionPolicy(options) {
|
|
566
|
+
const allowed = new Set(options.allow);
|
|
567
|
+
const protectedTools = new Set(options.approval);
|
|
568
|
+
return Object.freeze({
|
|
569
|
+
version: options.version,
|
|
570
|
+
requestApproval: options.requestApproval,
|
|
571
|
+
evaluate: (request) => protectedTools.has(request.toolName) ? { action: "require-approval", reason: `Approval required for ${request.toolName}` } : allowed.has(request.toolName) ? { action: "allow" } : { action: "deny", reason: "Tool is outside the production allowlist" }
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/tools.ts
|
|
146
576
|
function defineTool(spec) {
|
|
147
577
|
return spec;
|
|
148
578
|
}
|
|
@@ -184,6 +614,7 @@ async function prepareToolCall(toolCall, tools, hooks, signal, refs) {
|
|
|
184
614
|
assistantMessage: refs.assistantMessage,
|
|
185
615
|
toolCall,
|
|
186
616
|
args,
|
|
617
|
+
runContext: refs.runContext,
|
|
187
618
|
agentContext: refs.agentContext,
|
|
188
619
|
signal
|
|
189
620
|
} : void 0;
|
|
@@ -211,11 +642,20 @@ async function prepareToolCall(toolCall, tools, hooks, signal, refs) {
|
|
|
211
642
|
if (signal?.aborted) {
|
|
212
643
|
return { kind: "immediate", result: textResult("Operation aborted", true) };
|
|
213
644
|
}
|
|
645
|
+
const runContext = refs.runContext ?? currentRunContext();
|
|
646
|
+
try {
|
|
647
|
+
args = immutableArguments(args);
|
|
648
|
+
await authorizeTool(runContext, tool.name, toolCall.id, args, signal);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
return { kind: "immediate", result: textResult(toError(error).message, true) };
|
|
651
|
+
}
|
|
214
652
|
return { kind: "ready", tool, args };
|
|
215
653
|
}
|
|
216
654
|
async function executePreparedToolCall(toolCall, prepared, signal, emitUpdate) {
|
|
217
655
|
let acceptingUpdates = true;
|
|
218
656
|
try {
|
|
657
|
+
currentRunContext()?.controller?.assertActive(signal);
|
|
658
|
+
currentRunContext()?.controller?.tool();
|
|
219
659
|
const result = await prepared.tool.execute(toolCall.id, prepared.args, signal, (update) => {
|
|
220
660
|
if (acceptingUpdates) emitUpdate(update);
|
|
221
661
|
});
|
|
@@ -235,6 +675,7 @@ async function finalizeExecutedToolCall(toolCall, result, hooks, refs) {
|
|
|
235
675
|
args: refs.args ?? toolCall.arguments,
|
|
236
676
|
result,
|
|
237
677
|
isError: result.isError ?? false,
|
|
678
|
+
runContext: refs.runContext,
|
|
238
679
|
agentContext: refs.agentContext,
|
|
239
680
|
signal: refs.signal
|
|
240
681
|
} : void 0;
|
|
@@ -258,7 +699,11 @@ function parallelToolCallLimit() {
|
|
|
258
699
|
}
|
|
259
700
|
async function runOneToolCall(toolCall, options) {
|
|
260
701
|
const { tools, hooks, signal, emit, assistantMessage, context } = options;
|
|
261
|
-
const refs = {
|
|
702
|
+
const refs = {
|
|
703
|
+
assistantMessage,
|
|
704
|
+
agentContext: context,
|
|
705
|
+
runContext: options.runContext
|
|
706
|
+
};
|
|
262
707
|
await emit({
|
|
263
708
|
type: "tool_execution_start",
|
|
264
709
|
toolCallId: toolCall.id,
|
|
@@ -283,7 +728,7 @@ async function runOneToolCall(toolCall, options) {
|
|
|
283
728
|
signal
|
|
284
729
|
});
|
|
285
730
|
}
|
|
286
|
-
async function
|
|
731
|
+
async function executeToolCallsInternal(options) {
|
|
287
732
|
const { toolCalls, signal, mode, emit } = options;
|
|
288
733
|
const sequential = mode === "sequential" || toolCalls.some((toolCall) => {
|
|
289
734
|
const tool = options.tools.find((candidate) => candidate.name === toolCall.name);
|
|
@@ -329,11 +774,16 @@ async function executeToolCalls(options) {
|
|
|
329
774
|
}
|
|
330
775
|
return { messages, terminate: shouldTerminateBatch(results) };
|
|
331
776
|
}
|
|
777
|
+
function executeToolCalls(options) {
|
|
778
|
+
const runContext = resolveRunContext(options.runContext, options.signal);
|
|
779
|
+
return withRunContext(runContext, () => executeToolCallsInternal({ ...options, runContext }));
|
|
780
|
+
}
|
|
332
781
|
|
|
333
782
|
// src/agent-loop.ts
|
|
334
783
|
import {
|
|
335
784
|
streamSimple
|
|
336
785
|
} from "@ap3x/ai";
|
|
786
|
+
import { completeSimple as completeSimple2 } from "@ap3x/ai";
|
|
337
787
|
|
|
338
788
|
// src/compaction.ts
|
|
339
789
|
import { completeSimple } from "@ap3x/ai";
|
|
@@ -685,379 +1135,17 @@ var logger = {
|
|
|
685
1135
|
}
|
|
686
1136
|
};
|
|
687
1137
|
|
|
688
|
-
// src/agent
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return
|
|
692
|
-
input: 0,
|
|
693
|
-
output: 0,
|
|
694
|
-
cacheRead: 0,
|
|
695
|
-
cacheWrite: 0,
|
|
696
|
-
totalTokens: 0,
|
|
697
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
function failureMessage(model, message) {
|
|
701
|
-
return {
|
|
702
|
-
role: "assistant",
|
|
703
|
-
content: [],
|
|
704
|
-
api: model.api,
|
|
705
|
-
provider: model.provider,
|
|
706
|
-
model: model.id,
|
|
707
|
-
usage: emptyUsage(),
|
|
708
|
-
stopReason: "error",
|
|
709
|
-
errorMessage: message,
|
|
710
|
-
timestamp: Date.now()
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
function toolCallsOf(message) {
|
|
714
|
-
return message.content.filter((block) => block.type === "toolCall");
|
|
715
|
-
}
|
|
716
|
-
function configForTurn(config, turn, messages) {
|
|
717
|
-
if (!config.selectModel) return config;
|
|
718
|
-
const model = config.selectModel({ turn, messages });
|
|
719
|
-
return model ? { ...config, model } : config;
|
|
1138
|
+
// src/agent.ts
|
|
1139
|
+
function finalTextOf(message) {
|
|
1140
|
+
if (!message) return "";
|
|
1141
|
+
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
720
1142
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
// straight through. `AgentMessage` may be wider than `Message` once a host
|
|
726
|
-
// augments CustomAgentMessages, but a host that does so is expected to map
|
|
727
|
-
// its custom types down via convertToLlm; absent that, the transcript is
|
|
728
|
-
// standard `Message`s, so this cast is a no-op on the runtime value.
|
|
729
|
-
transformed
|
|
730
|
-
);
|
|
731
|
-
const llmContext = {
|
|
732
|
-
systemPrompt: config.systemPrompt ?? context.systemPrompt,
|
|
733
|
-
messages,
|
|
734
|
-
tools: config.tools?.map((tool) => ({
|
|
735
|
-
name: tool.name,
|
|
736
|
-
description: tool.description,
|
|
737
|
-
parameters: tool.parameters
|
|
738
|
-
}))
|
|
739
|
-
};
|
|
740
|
-
const resolvedApiKey = config.hooks?.getApiKey ? await config.hooks.getApiKey(config.model.provider) : void 0;
|
|
741
|
-
const options = {
|
|
742
|
-
...config.streamOptions,
|
|
743
|
-
...resolvedApiKey ? { apiKey: resolvedApiKey } : {},
|
|
744
|
-
signal,
|
|
745
|
-
...config.reasoning ? { reasoning: config.reasoning } : {}
|
|
746
|
-
};
|
|
747
|
-
let started = false;
|
|
748
|
-
try {
|
|
749
|
-
const eventStream = streamFn(config.model, llmContext, options);
|
|
750
|
-
for await (const providerEvent of eventStream) {
|
|
751
|
-
if (providerEvent.type === "start") {
|
|
752
|
-
started = true;
|
|
753
|
-
await emit({ type: "message_start", message: providerEvent.partial });
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
if (providerEvent.type === "done" || providerEvent.type === "error") {
|
|
757
|
-
break;
|
|
758
|
-
}
|
|
759
|
-
if ("partial" in providerEvent) {
|
|
760
|
-
await emit({
|
|
761
|
-
type: "message_update",
|
|
762
|
-
message: providerEvent.partial,
|
|
763
|
-
assistantMessageEvent: providerEvent
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
const final = await eventStream.result();
|
|
768
|
-
if (!started) await emit({ type: "message_start", message: final });
|
|
769
|
-
await emit({ type: "message_end", message: final });
|
|
770
|
-
return final;
|
|
771
|
-
} catch (error) {
|
|
772
|
-
const message = failureMessage(
|
|
773
|
-
config.model,
|
|
774
|
-
error instanceof Error ? error.message : String(error)
|
|
775
|
-
);
|
|
776
|
-
if (!started) await emit({ type: "message_start", message });
|
|
777
|
-
await emit({ type: "message_end", message });
|
|
778
|
-
return message;
|
|
1143
|
+
function lastAssistant(messages) {
|
|
1144
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1145
|
+
const message = messages[i];
|
|
1146
|
+
if (message?.role === "assistant") return message;
|
|
779
1147
|
}
|
|
780
|
-
|
|
781
|
-
async function runAgentLoop(prompts, context, config, emit, signal) {
|
|
782
|
-
const streamFn = config.streamFn ?? streamSimple;
|
|
783
|
-
const maxLoops = config.maxLoops ?? DEFAULT_MAX_LOOPS;
|
|
784
|
-
const newMessages = [];
|
|
785
|
-
const working = {
|
|
786
|
-
systemPrompt: config.systemPrompt ?? context.systemPrompt,
|
|
787
|
-
messages: [...context.messages],
|
|
788
|
-
tools: context.tools
|
|
789
|
-
};
|
|
790
|
-
await emit({ type: "agent_start" });
|
|
791
|
-
const drainSteering = async () => {
|
|
792
|
-
if (signal?.aborted || !config.hooks?.getSteeringMessages) return [];
|
|
793
|
-
return await config.hooks.getSteeringMessages() ?? [];
|
|
794
|
-
};
|
|
795
|
-
const drainFollowUp = async () => {
|
|
796
|
-
if (signal?.aborted || !config.hooks?.getFollowUpMessages) return [];
|
|
797
|
-
return await config.hooks.getFollowUpMessages() ?? [];
|
|
798
|
-
};
|
|
799
|
-
const compactionDeferred = async () => {
|
|
800
|
-
if (!config.hooks?.beforeCompaction) return false;
|
|
801
|
-
try {
|
|
802
|
-
return (await config.hooks.beforeCompaction())?.defer === true;
|
|
803
|
-
} catch {
|
|
804
|
-
return false;
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
let turns = 0;
|
|
808
|
-
let hasMoreToolCalls = true;
|
|
809
|
-
let pending = [...prompts];
|
|
810
|
-
const contextWindow = config.model.contextWindow;
|
|
811
|
-
const settings = resolveCompactionSettings(
|
|
812
|
-
config.compaction ?? DEFAULT_COMPACTION_SETTINGS,
|
|
813
|
-
contextWindow
|
|
814
|
-
);
|
|
815
|
-
const compactionOn = settings.enabled && contextWindow > 0;
|
|
816
|
-
let shadow = null;
|
|
817
|
-
let anchorFloor = 0;
|
|
818
|
-
let lastSummary = config.previousSummary;
|
|
819
|
-
let sawToolContinuation = false;
|
|
820
|
-
const launchShadow = () => {
|
|
821
|
-
const snapshotLen = working.messages.length;
|
|
822
|
-
const snapshot = working.messages.slice(0, snapshotLen);
|
|
823
|
-
const abort = new AbortController();
|
|
824
|
-
const onAbort = () => abort.abort();
|
|
825
|
-
let detached = false;
|
|
826
|
-
const cleanup = () => {
|
|
827
|
-
if (detached) return;
|
|
828
|
-
detached = true;
|
|
829
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
830
|
-
};
|
|
831
|
-
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
832
|
-
const s = { snapshotLen, abort, cleanup, promise: void 0 };
|
|
833
|
-
s.promise = compact(snapshot, config.model, {
|
|
834
|
-
settings,
|
|
835
|
-
signal: abort.signal,
|
|
836
|
-
...lastSummary !== void 0 ? { previousSummary: lastSummary } : {},
|
|
837
|
-
...config.compactionComplete ? { complete: config.compactionComplete } : {}
|
|
838
|
-
}).then((r) => {
|
|
839
|
-
s.settled = r;
|
|
840
|
-
cleanup();
|
|
841
|
-
return r;
|
|
842
|
-
}).catch((e) => {
|
|
843
|
-
const r = err(
|
|
844
|
-
new CompactionError("compaction", toError(e).message, e)
|
|
845
|
-
);
|
|
846
|
-
s.settled = r;
|
|
847
|
-
cleanup();
|
|
848
|
-
return r;
|
|
849
|
-
});
|
|
850
|
-
logger.debug(`compaction: launched shadow (snapshotLen=${snapshotLen})`);
|
|
851
|
-
return s;
|
|
852
|
-
};
|
|
853
|
-
const applyShadow = async (s) => {
|
|
854
|
-
const r = s.settled;
|
|
855
|
-
if (!r) return false;
|
|
856
|
-
if (!r.ok) {
|
|
857
|
-
logger.warn(`compaction: failed, keeping context (${r.error.message})`);
|
|
858
|
-
return false;
|
|
859
|
-
}
|
|
860
|
-
const cr = r.value;
|
|
861
|
-
if (cr.summary === "" || cr.keptMessages.length >= s.snapshotLen) return false;
|
|
862
|
-
const summaryMsg = {
|
|
863
|
-
// Mirrors buildSessionContext so in-memory swap and resume read identically.
|
|
864
|
-
role: "user",
|
|
865
|
-
content: `[Earlier conversation summarized]
|
|
866
|
-
${cr.summary}`,
|
|
867
|
-
timestamp: Date.now()
|
|
868
|
-
};
|
|
869
|
-
const rebased = [
|
|
870
|
-
summaryMsg,
|
|
871
|
-
...cr.keptMessages,
|
|
872
|
-
...working.messages.slice(s.snapshotLen)
|
|
873
|
-
// the arrival tail — messages appended during the in-flight compaction
|
|
874
|
-
];
|
|
875
|
-
const sum = (ms) => ms.reduce((n, m) => n + estimateMessageTokens(m), 0);
|
|
876
|
-
const before = sum(working.messages);
|
|
877
|
-
const after = sum(rebased);
|
|
878
|
-
if (after >= before) {
|
|
879
|
-
logger.debug("compaction: not smaller, rolled back");
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
if (rebased.at(-1) !== working.messages.at(-1)) {
|
|
883
|
-
logger.debug("compaction: tail identity check failed, rolled back");
|
|
884
|
-
return false;
|
|
885
|
-
}
|
|
886
|
-
working.messages = rebased;
|
|
887
|
-
anchorFloor = rebased.length;
|
|
888
|
-
lastSummary = cr.summary;
|
|
889
|
-
await emit({
|
|
890
|
-
type: "compaction",
|
|
891
|
-
summary: cr.summary,
|
|
892
|
-
tokensBefore: cr.tokensBefore,
|
|
893
|
-
retainedAtSwap: rebased.length - 1,
|
|
894
|
-
// excludes summaryMsg
|
|
895
|
-
newMessagesAtSwap: newMessages.length
|
|
896
|
-
});
|
|
897
|
-
logger.info(`compaction: swapped ${before}->${after} tokens (kept ${cr.keptMessages.length})`);
|
|
898
|
-
return true;
|
|
899
|
-
};
|
|
900
|
-
while (hasMoreToolCalls || pending.length > 0) {
|
|
901
|
-
if (turns >= maxLoops) break;
|
|
902
|
-
await emit({ type: "turn_start" });
|
|
903
|
-
const steering = await drainSteering();
|
|
904
|
-
if (steering.length > 0) pending.push(...steering);
|
|
905
|
-
for (const prompt of pending) {
|
|
906
|
-
await emit({ type: "message_start", message: prompt });
|
|
907
|
-
working.messages.push(prompt);
|
|
908
|
-
newMessages.push(prompt);
|
|
909
|
-
await emit({ type: "message_end", message: prompt });
|
|
910
|
-
}
|
|
911
|
-
pending = [];
|
|
912
|
-
if (compactionOn) {
|
|
913
|
-
let handled = false;
|
|
914
|
-
if (shadow?.settled) {
|
|
915
|
-
await applyShadow(shadow);
|
|
916
|
-
shadow = null;
|
|
917
|
-
handled = true;
|
|
918
|
-
}
|
|
919
|
-
if (!handled) {
|
|
920
|
-
const usage = estimateContextTokens(working.messages, anchorFloor);
|
|
921
|
-
const tier = compactionTier(usage, contextWindow, settings);
|
|
922
|
-
if (tier === "hard") {
|
|
923
|
-
if (!shadow && !await compactionDeferred()) shadow = launchShadow();
|
|
924
|
-
if (shadow) {
|
|
925
|
-
await shadow.promise;
|
|
926
|
-
await applyShadow(shadow);
|
|
927
|
-
shadow = null;
|
|
928
|
-
}
|
|
929
|
-
} else if (tier === "soft" && !shadow && sawToolContinuation && !await compactionDeferred()) {
|
|
930
|
-
shadow = launchShadow();
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
const assistant = await streamAssistantResponse(
|
|
935
|
-
working,
|
|
936
|
-
configForTurn(config, turns, working.messages),
|
|
937
|
-
emit,
|
|
938
|
-
signal,
|
|
939
|
-
streamFn
|
|
940
|
-
);
|
|
941
|
-
working.messages.push(assistant);
|
|
942
|
-
newMessages.push(assistant);
|
|
943
|
-
turns++;
|
|
944
|
-
if (config.hooks?.onResponse) await config.hooks.onResponse(assistant);
|
|
945
|
-
if (assistant.stopReason === "error" || assistant.stopReason === "aborted") {
|
|
946
|
-
await emit({ type: "turn_end", message: assistant, toolResults: [] });
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
const toolCalls = toolCallsOf(assistant);
|
|
950
|
-
hasMoreToolCalls = false;
|
|
951
|
-
if (toolCalls.length === 0) {
|
|
952
|
-
await emit({ type: "turn_end", message: assistant, toolResults: [] });
|
|
953
|
-
const followUp = await drainFollowUp();
|
|
954
|
-
if (followUp.length > 0) {
|
|
955
|
-
pending = followUp;
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
break;
|
|
959
|
-
}
|
|
960
|
-
const batch = await executeToolCalls({
|
|
961
|
-
toolCalls,
|
|
962
|
-
tools: config.tools ?? [],
|
|
963
|
-
hooks: config.hooks,
|
|
964
|
-
signal,
|
|
965
|
-
mode: config.toolExecution ?? "parallel",
|
|
966
|
-
assistantMessage: assistant,
|
|
967
|
-
context: working,
|
|
968
|
-
emit: (event) => emit(event)
|
|
969
|
-
});
|
|
970
|
-
for (const result of batch.messages) {
|
|
971
|
-
await emit({ type: "message_start", message: result });
|
|
972
|
-
working.messages.push(result);
|
|
973
|
-
newMessages.push(result);
|
|
974
|
-
await emit({ type: "message_end", message: result });
|
|
975
|
-
}
|
|
976
|
-
await emit({ type: "turn_end", message: assistant, toolResults: batch.messages });
|
|
977
|
-
if (batch.terminate) {
|
|
978
|
-
const followUp = await drainFollowUp();
|
|
979
|
-
if (followUp.length > 0) {
|
|
980
|
-
pending = followUp;
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
break;
|
|
984
|
-
}
|
|
985
|
-
if (signal?.aborted) break;
|
|
986
|
-
hasMoreToolCalls = true;
|
|
987
|
-
sawToolContinuation = true;
|
|
988
|
-
}
|
|
989
|
-
if (shadow && !shadow.settled) {
|
|
990
|
-
shadow.abort.abort();
|
|
991
|
-
shadow.cleanup();
|
|
992
|
-
}
|
|
993
|
-
await emit({ type: "agent_end", messages: newMessages });
|
|
994
|
-
return newMessages;
|
|
995
|
-
}
|
|
996
|
-
async function runAgentLoopContinue(context, config, emit, signal) {
|
|
997
|
-
const last = context.messages.at(-1);
|
|
998
|
-
if (!last) throw new Error("Cannot continue: no messages in context");
|
|
999
|
-
if (last.role === "assistant") {
|
|
1000
|
-
throw new Error("Cannot continue from message role: assistant");
|
|
1001
|
-
}
|
|
1002
|
-
return runAgentLoop([], context, config, emit, signal);
|
|
1003
|
-
}
|
|
1004
|
-
function makeEventStream(run) {
|
|
1005
|
-
const queue = [];
|
|
1006
|
-
const waiters = [];
|
|
1007
|
-
let done = false;
|
|
1008
|
-
const emit = (event) => {
|
|
1009
|
-
const waiter = waiters.shift();
|
|
1010
|
-
if (waiter) waiter({ value: event, done: false });
|
|
1011
|
-
else queue.push(event);
|
|
1012
|
-
};
|
|
1013
|
-
const resultPromise = run(emit).finally(() => {
|
|
1014
|
-
done = true;
|
|
1015
|
-
while (waiters.length > 0) {
|
|
1016
|
-
waiters.shift()?.({ value: void 0, done: true });
|
|
1017
|
-
}
|
|
1018
|
-
});
|
|
1019
|
-
return {
|
|
1020
|
-
result: () => resultPromise,
|
|
1021
|
-
async *[Symbol.asyncIterator]() {
|
|
1022
|
-
while (true) {
|
|
1023
|
-
if (queue.length > 0) {
|
|
1024
|
-
yield queue.shift();
|
|
1025
|
-
} else if (done) {
|
|
1026
|
-
return;
|
|
1027
|
-
} else {
|
|
1028
|
-
const next = await new Promise((resolve2) => {
|
|
1029
|
-
waiters.push(resolve2);
|
|
1030
|
-
});
|
|
1031
|
-
if (next.done) return;
|
|
1032
|
-
yield next.value;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
function agentLoop(prompts, context, config, signal) {
|
|
1039
|
-
return makeEventStream((emit) => runAgentLoop(prompts, context, config, emit, signal));
|
|
1040
|
-
}
|
|
1041
|
-
function agentLoopContinue(context, config, signal) {
|
|
1042
|
-
const last = context.messages.at(-1);
|
|
1043
|
-
if (!last) throw new Error("Cannot continue: no messages in context");
|
|
1044
|
-
if (last.role === "assistant") {
|
|
1045
|
-
throw new Error("Cannot continue from message role: assistant");
|
|
1046
|
-
}
|
|
1047
|
-
return makeEventStream((emit) => runAgentLoopContinue(context, config, emit, signal));
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// src/agent.ts
|
|
1051
|
-
function finalTextOf(message) {
|
|
1052
|
-
if (!message) return "";
|
|
1053
|
-
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1054
|
-
}
|
|
1055
|
-
function lastAssistant(messages) {
|
|
1056
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1057
|
-
const message = messages[i];
|
|
1058
|
-
if (message?.role === "assistant") return message;
|
|
1059
|
-
}
|
|
1060
|
-
return void 0;
|
|
1148
|
+
return void 0;
|
|
1061
1149
|
}
|
|
1062
1150
|
var Agent = class {
|
|
1063
1151
|
model;
|
|
@@ -1071,6 +1159,8 @@ var Agent = class {
|
|
|
1071
1159
|
sessionId;
|
|
1072
1160
|
autoCompact;
|
|
1073
1161
|
previousSummary;
|
|
1162
|
+
runContext;
|
|
1163
|
+
compactionComplete;
|
|
1074
1164
|
toolList;
|
|
1075
1165
|
transcript;
|
|
1076
1166
|
running = false;
|
|
@@ -1089,6 +1179,7 @@ var Agent = class {
|
|
|
1089
1179
|
/** A never-aborted signal handed to listeners for out-of-run queue events. */
|
|
1090
1180
|
idleSignal = new AbortController().signal;
|
|
1091
1181
|
constructor(options) {
|
|
1182
|
+
this.compactionComplete = options.compactionComplete;
|
|
1092
1183
|
this.model = options.model;
|
|
1093
1184
|
this.systemPrompt = options.systemPrompt;
|
|
1094
1185
|
this.reasoning = options.reasoning;
|
|
@@ -1223,6 +1314,8 @@ var Agent = class {
|
|
|
1223
1314
|
}
|
|
1224
1315
|
currentConfig() {
|
|
1225
1316
|
return {
|
|
1317
|
+
runContext: this.runContext,
|
|
1318
|
+
compactionComplete: this.compactionComplete,
|
|
1226
1319
|
model: this.model,
|
|
1227
1320
|
systemPrompt: this.systemPrompt,
|
|
1228
1321
|
tools: this.toolList,
|
|
@@ -1261,93 +1354,666 @@ var Agent = class {
|
|
|
1261
1354
|
const fromUser = userHooks.getFollowUpMessages ? await userHooks.getFollowUpMessages() : [];
|
|
1262
1355
|
return [...drained, ...fromUser];
|
|
1263
1356
|
}
|
|
1264
|
-
};
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
/** Enqueue a pending message and notify observers via `queue_update`. */
|
|
1360
|
+
enqueue(mode, message) {
|
|
1361
|
+
this.pending.push({ mode, message });
|
|
1362
|
+
void this.emitQueueUpdate();
|
|
1363
|
+
}
|
|
1364
|
+
/** Remove and return (in FIFO order) the messages whose mode matches. */
|
|
1365
|
+
drainQueue(match) {
|
|
1366
|
+
if (this.pending.length === 0) return [];
|
|
1367
|
+
const drained = [];
|
|
1368
|
+
const remaining = [];
|
|
1369
|
+
for (const entry of this.pending) {
|
|
1370
|
+
if (match(entry.mode)) drained.push(entry.message);
|
|
1371
|
+
else remaining.push(entry);
|
|
1372
|
+
}
|
|
1373
|
+
this.pending = remaining;
|
|
1374
|
+
return drained;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Emit a `queue_update` snapshot to every listener. Best-effort and
|
|
1378
|
+
* out-of-band: a failing observer is isolated so it can neither break an
|
|
1379
|
+
* enqueue nor the active run. Uses the active run's signal when running, else
|
|
1380
|
+
* a never-aborted idle signal.
|
|
1381
|
+
*/
|
|
1382
|
+
async emitQueueUpdate() {
|
|
1383
|
+
const byMode = (mode) => this.pending.filter((entry) => entry.mode === mode).map((entry) => entry.message);
|
|
1384
|
+
const event = {
|
|
1385
|
+
type: "queue_update",
|
|
1386
|
+
steer: byMode("steer"),
|
|
1387
|
+
followUp: byMode("followUp"),
|
|
1388
|
+
nextTurn: byMode("nextTurn")
|
|
1389
|
+
};
|
|
1390
|
+
const signal = this.controller?.signal ?? this.idleSignal;
|
|
1391
|
+
for (const listener of this.listeners) {
|
|
1392
|
+
try {
|
|
1393
|
+
await listener(event, signal);
|
|
1394
|
+
} catch {
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
async runWithLifecycle(run) {
|
|
1399
|
+
this.running = true;
|
|
1400
|
+
this.lastError = void 0;
|
|
1401
|
+
this.controller = new AbortController();
|
|
1402
|
+
const signal = this.controller.signal;
|
|
1403
|
+
const emit = async (event) => {
|
|
1404
|
+
if (event.type === "compaction") this.previousSummary = event.summary;
|
|
1405
|
+
for (const listener of this.listeners) {
|
|
1406
|
+
await listener(event, signal);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
try {
|
|
1410
|
+
const newMessages = await run(emit, signal);
|
|
1411
|
+
this.transcript.push(...newMessages);
|
|
1412
|
+
const assistant = lastAssistant(newMessages);
|
|
1413
|
+
if (assistant?.errorMessage) this.lastError = assistant.errorMessage;
|
|
1414
|
+
if (!assistant) {
|
|
1415
|
+
throw new AgentHarnessError("invalid_state", "Run completed without an assistant message");
|
|
1416
|
+
}
|
|
1417
|
+
return assistant;
|
|
1418
|
+
} finally {
|
|
1419
|
+
this.running = false;
|
|
1420
|
+
this.controller = void 0;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
// src/agent-errors.ts
|
|
1426
|
+
var AgentError = class extends Error {
|
|
1427
|
+
/** Machine-readable, stable error code. */
|
|
1428
|
+
code;
|
|
1429
|
+
cause;
|
|
1430
|
+
constructor(message, code = "agent", cause) {
|
|
1431
|
+
super(message);
|
|
1432
|
+
this.name = new.target.name;
|
|
1433
|
+
this.code = code;
|
|
1434
|
+
this.cause = cause;
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
var AgentInitializationError = class extends AgentError {
|
|
1438
|
+
constructor(message, cause) {
|
|
1439
|
+
super(message, "agent_initialization", cause);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
var AgentRunError = class extends AgentError {
|
|
1443
|
+
constructor(message, cause) {
|
|
1444
|
+
super(message, "agent_run", cause);
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
var AgentLLMError = class extends AgentError {
|
|
1448
|
+
constructor(message, cause) {
|
|
1449
|
+
super(message, "agent_llm", cause);
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
var AgentToolError = class extends AgentError {
|
|
1453
|
+
constructor(message, cause) {
|
|
1454
|
+
super(message, "agent_tool", cause);
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
var AgentMemoryError = class extends AgentError {
|
|
1458
|
+
constructor(message, cause) {
|
|
1459
|
+
super(message, "agent_memory", cause);
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
var AgentMCPConnectionError = class extends AgentError {
|
|
1463
|
+
constructor(message, cause) {
|
|
1464
|
+
super(message, "agent_mcp_connection", cause);
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
var AgentMCPToolError = class extends AgentError {
|
|
1468
|
+
constructor(message, cause) {
|
|
1469
|
+
super(message, "agent_mcp_tool", cause);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
// src/run-result.ts
|
|
1474
|
+
function emptyUsage() {
|
|
1475
|
+
return {
|
|
1476
|
+
input: 0,
|
|
1477
|
+
output: 0,
|
|
1478
|
+
cacheRead: 0,
|
|
1479
|
+
cacheWrite: 0,
|
|
1480
|
+
totalTokens: 0,
|
|
1481
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function addUsage(total, usage) {
|
|
1485
|
+
total.input += usage.input;
|
|
1486
|
+
total.output += usage.output;
|
|
1487
|
+
total.cacheRead += usage.cacheRead;
|
|
1488
|
+
total.cacheWrite += usage.cacheWrite;
|
|
1489
|
+
total.totalTokens += usage.totalTokens;
|
|
1490
|
+
total.cost.input += usage.cost.input;
|
|
1491
|
+
total.cost.output += usage.cost.output;
|
|
1492
|
+
total.cost.cacheRead += usage.cost.cacheRead;
|
|
1493
|
+
total.cost.cacheWrite += usage.cost.cacheWrite;
|
|
1494
|
+
total.cost.total += usage.cost.total;
|
|
1495
|
+
}
|
|
1496
|
+
function runCompletion(result) {
|
|
1497
|
+
if (result.reasonCode === "execution_limit")
|
|
1498
|
+
return { outcome: "partial", reasonCode: "execution_limit" };
|
|
1499
|
+
if (result.stopReason === "error")
|
|
1500
|
+
return { outcome: "failed", reasonCode: result.reasonCode ?? "provider_error" };
|
|
1501
|
+
if (result.stopReason === "aborted") return { outcome: "cancelled", reasonCode: "cancelled" };
|
|
1502
|
+
if (result.stopReason === "length")
|
|
1503
|
+
return { outcome: "partial", reasonCode: result.reasonCode ?? "output_limit" };
|
|
1504
|
+
if (result.outcome)
|
|
1505
|
+
return {
|
|
1506
|
+
outcome: result.outcome,
|
|
1507
|
+
reasonCode: result.reasonCode ?? (result.outcome === "succeeded" ? "completed" : "execution_error")
|
|
1508
|
+
};
|
|
1509
|
+
return { outcome: "succeeded", reasonCode: "completed" };
|
|
1510
|
+
}
|
|
1511
|
+
function toRunResult(messages, metadata = {}) {
|
|
1512
|
+
const assistant = lastAssistant(messages);
|
|
1513
|
+
const aggregateUsage = metadata.aggregateUsage ?? emptyUsage();
|
|
1514
|
+
if (!metadata.aggregateUsage) {
|
|
1515
|
+
for (const message of messages)
|
|
1516
|
+
if (message.role === "assistant") addUsage(aggregateUsage, message.usage);
|
|
1517
|
+
}
|
|
1518
|
+
const stopReason = assistant?.stopReason ?? "error";
|
|
1519
|
+
const completion = assistant ? runCompletion({ stopReason, ...metadata }) : { outcome: "failed", reasonCode: "no_assistant" };
|
|
1520
|
+
return {
|
|
1521
|
+
messages,
|
|
1522
|
+
finalText: finalTextOf(assistant),
|
|
1523
|
+
usage: assistant?.usage ?? emptyUsage(),
|
|
1524
|
+
aggregateUsage,
|
|
1525
|
+
stopReason,
|
|
1526
|
+
errorMessage: assistant?.errorMessage,
|
|
1527
|
+
...metadata.usageIncomplete ? { usageIncomplete: true } : {},
|
|
1528
|
+
...metadata.backendData !== void 0 ? { backendData: metadata.backendData } : {},
|
|
1529
|
+
...completion
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
var AgentRunFailure = class extends AgentRunError {
|
|
1533
|
+
result;
|
|
1534
|
+
completion;
|
|
1535
|
+
constructor(result) {
|
|
1536
|
+
const completion = runCompletion(result);
|
|
1537
|
+
super(
|
|
1538
|
+
result.errorMessage ?? `Agent run ${completion.outcome}: ${completion.reasonCode}`,
|
|
1539
|
+
result
|
|
1540
|
+
);
|
|
1541
|
+
this.result = result;
|
|
1542
|
+
this.completion = completion;
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
function assertRunSucceeded(result) {
|
|
1546
|
+
if (runCompletion(result).outcome !== "succeeded") throw new AgentRunFailure(result);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/agent-loop.ts
|
|
1550
|
+
var DEFAULT_MAX_LOOPS = 1e3;
|
|
1551
|
+
function emptyUsage2() {
|
|
1552
|
+
return {
|
|
1553
|
+
input: 0,
|
|
1554
|
+
output: 0,
|
|
1555
|
+
cacheRead: 0,
|
|
1556
|
+
cacheWrite: 0,
|
|
1557
|
+
totalTokens: 0,
|
|
1558
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function failureMessage(model, message, stopReason = "error") {
|
|
1562
|
+
return {
|
|
1563
|
+
role: "assistant",
|
|
1564
|
+
content: [],
|
|
1565
|
+
api: model.api,
|
|
1566
|
+
provider: model.provider,
|
|
1567
|
+
model: model.id,
|
|
1568
|
+
usage: emptyUsage2(),
|
|
1569
|
+
stopReason,
|
|
1570
|
+
errorMessage: message,
|
|
1571
|
+
timestamp: Date.now()
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function toolCallsOf(message) {
|
|
1575
|
+
return message.content.filter((block) => block.type === "toolCall");
|
|
1576
|
+
}
|
|
1577
|
+
function configForTurn(config, turn, messages) {
|
|
1578
|
+
if (!config.selectModel) return config;
|
|
1579
|
+
const model = config.selectModel({ turn, messages });
|
|
1580
|
+
return model ? { ...config, model } : config;
|
|
1581
|
+
}
|
|
1582
|
+
async function streamAssistantResponse(context, config, emit, signal, streamFn) {
|
|
1583
|
+
if (signal?.aborted) {
|
|
1584
|
+
const message = failureMessage(config.model, "Agent run cancelled", "aborted");
|
|
1585
|
+
await emit({ type: "message_start", message });
|
|
1586
|
+
await emit({ type: "message_end", message });
|
|
1587
|
+
return message;
|
|
1588
|
+
}
|
|
1589
|
+
const transformed = config.hooks?.transformContext ? await config.hooks.transformContext(context.messages, signal) : context.messages;
|
|
1590
|
+
const messages = config.hooks?.convertToLlm ? await config.hooks.convertToLlm(transformed) : (
|
|
1591
|
+
// Without a convertToLlm hook the (already transformed) transcript passes
|
|
1592
|
+
// straight through. `AgentMessage` may be wider than `Message` once a host
|
|
1593
|
+
// augments CustomAgentMessages, but a host that does so is expected to map
|
|
1594
|
+
// its custom types down via convertToLlm; absent that, the transcript is
|
|
1595
|
+
// standard `Message`s, so this cast is a no-op on the runtime value.
|
|
1596
|
+
transformed
|
|
1597
|
+
);
|
|
1598
|
+
const llmContext = {
|
|
1599
|
+
systemPrompt: config.systemPrompt ?? context.systemPrompt,
|
|
1600
|
+
messages,
|
|
1601
|
+
tools: config.tools?.map((tool) => ({
|
|
1602
|
+
name: tool.name,
|
|
1603
|
+
description: tool.description,
|
|
1604
|
+
parameters: tool.parameters
|
|
1605
|
+
}))
|
|
1606
|
+
};
|
|
1607
|
+
const resolvedApiKey = config.hooks?.getApiKey ? await config.hooks.getApiKey(config.model.provider) : void 0;
|
|
1608
|
+
const options = {
|
|
1609
|
+
...config.streamOptions,
|
|
1610
|
+
...resolvedApiKey ? { apiKey: resolvedApiKey } : {},
|
|
1611
|
+
signal,
|
|
1612
|
+
...config.reasoning ? { reasoning: config.reasoning } : {}
|
|
1613
|
+
};
|
|
1614
|
+
let started = false;
|
|
1615
|
+
let reservation;
|
|
1616
|
+
try {
|
|
1617
|
+
if (signal?.aborted) {
|
|
1618
|
+
const message = failureMessage(config.model, "Agent run cancelled", "aborted");
|
|
1619
|
+
await emit({ type: "message_start", message });
|
|
1620
|
+
await emit({ type: "message_end", message });
|
|
1621
|
+
return message;
|
|
1622
|
+
}
|
|
1623
|
+
reservation = reserveProviderCall(config.model, llmContext, options);
|
|
1624
|
+
const eventStream = withProviderReservation(
|
|
1625
|
+
reservation,
|
|
1626
|
+
() => streamFn(config.model, llmContext, options)
|
|
1627
|
+
);
|
|
1628
|
+
for await (const providerEvent of eventStream) {
|
|
1629
|
+
if (providerEvent.type === "start") {
|
|
1630
|
+
started = true;
|
|
1631
|
+
await emit({ type: "message_start", message: providerEvent.partial });
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
if (providerEvent.type === "done" || providerEvent.type === "error") {
|
|
1635
|
+
break;
|
|
1636
|
+
}
|
|
1637
|
+
if ("partial" in providerEvent) {
|
|
1638
|
+
await emit({
|
|
1639
|
+
type: "message_update",
|
|
1640
|
+
message: providerEvent.partial,
|
|
1641
|
+
assistantMessageEvent: providerEvent
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const final = await eventStream.result();
|
|
1646
|
+
reservation?.settle(final.usage);
|
|
1647
|
+
if (!started) await emit({ type: "message_start", message: final });
|
|
1648
|
+
await emit({ type: "message_end", message: final });
|
|
1649
|
+
return final;
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
const message = failureMessage(
|
|
1652
|
+
config.model,
|
|
1653
|
+
error instanceof Error ? error.message : String(error)
|
|
1654
|
+
);
|
|
1655
|
+
if (!started) await emit({ type: "message_start", message });
|
|
1656
|
+
await emit({ type: "message_end", message });
|
|
1657
|
+
return message;
|
|
1658
|
+
} finally {
|
|
1659
|
+
reservation?.settle();
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async function runAgentLoopInternal(prompts, context, config, emit, signal) {
|
|
1663
|
+
const streamFn = config.streamFn ?? streamSimple;
|
|
1664
|
+
const maxLoops = config.maxLoops ?? DEFAULT_MAX_LOOPS;
|
|
1665
|
+
const newMessages = [];
|
|
1666
|
+
const aggregateUsage = emptyUsage();
|
|
1667
|
+
let reasonCode;
|
|
1668
|
+
const working = {
|
|
1669
|
+
systemPrompt: config.systemPrompt ?? context.systemPrompt,
|
|
1670
|
+
messages: [...context.messages],
|
|
1671
|
+
tools: context.tools
|
|
1672
|
+
};
|
|
1673
|
+
await emit({ type: "agent_start" });
|
|
1674
|
+
const drainSteering = async () => {
|
|
1675
|
+
if (signal?.aborted || !config.hooks?.getSteeringMessages) return [];
|
|
1676
|
+
return await config.hooks.getSteeringMessages() ?? [];
|
|
1677
|
+
};
|
|
1678
|
+
const drainFollowUp = async () => {
|
|
1679
|
+
if (signal?.aborted || !config.hooks?.getFollowUpMessages) return [];
|
|
1680
|
+
return await config.hooks.getFollowUpMessages() ?? [];
|
|
1681
|
+
};
|
|
1682
|
+
const compactionDeferred = async () => {
|
|
1683
|
+
if (!config.hooks?.beforeCompaction) return false;
|
|
1684
|
+
try {
|
|
1685
|
+
return (await config.hooks.beforeCompaction())?.defer === true;
|
|
1686
|
+
} catch {
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
let turns = 0;
|
|
1691
|
+
let hasMoreToolCalls = true;
|
|
1692
|
+
let pending = [...prompts];
|
|
1693
|
+
const contextWindow = config.model.contextWindow;
|
|
1694
|
+
const settings = resolveCompactionSettings(
|
|
1695
|
+
config.compaction ?? DEFAULT_COMPACTION_SETTINGS,
|
|
1696
|
+
contextWindow
|
|
1697
|
+
);
|
|
1698
|
+
const compactionOn = settings.enabled && contextWindow > 0;
|
|
1699
|
+
let shadow = null;
|
|
1700
|
+
let anchorFloor = 0;
|
|
1701
|
+
let lastSummary = config.previousSummary;
|
|
1702
|
+
let sawToolContinuation = false;
|
|
1703
|
+
const launchShadow = () => {
|
|
1704
|
+
const snapshotLen = working.messages.length;
|
|
1705
|
+
const snapshot = working.messages.slice(0, snapshotLen);
|
|
1706
|
+
const abort = new AbortController();
|
|
1707
|
+
const onAbort = () => abort.abort();
|
|
1708
|
+
let detached = false;
|
|
1709
|
+
const cleanup = () => {
|
|
1710
|
+
if (detached) return;
|
|
1711
|
+
detached = true;
|
|
1712
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1713
|
+
};
|
|
1714
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
1715
|
+
const s = { snapshotLen, abort, cleanup, promise: void 0 };
|
|
1716
|
+
s.promise = compact(snapshot, config.model, {
|
|
1717
|
+
settings,
|
|
1718
|
+
signal: abort.signal,
|
|
1719
|
+
...lastSummary !== void 0 ? { previousSummary: lastSummary } : {},
|
|
1720
|
+
complete: async (model, context2, options) => {
|
|
1721
|
+
const response = await controlledProviderCall(
|
|
1722
|
+
model,
|
|
1723
|
+
context2,
|
|
1724
|
+
options,
|
|
1725
|
+
() => (config.compactionComplete ?? completeSimple2)(model, context2, options)
|
|
1726
|
+
);
|
|
1727
|
+
addUsage(aggregateUsage, response.usage);
|
|
1728
|
+
return response;
|
|
1729
|
+
}
|
|
1730
|
+
}).then((r) => {
|
|
1731
|
+
s.settled = r;
|
|
1732
|
+
cleanup();
|
|
1733
|
+
return r;
|
|
1734
|
+
}).catch((e) => {
|
|
1735
|
+
const r = err(
|
|
1736
|
+
new CompactionError("compaction", toError(e).message, e)
|
|
1737
|
+
);
|
|
1738
|
+
s.settled = r;
|
|
1739
|
+
cleanup();
|
|
1740
|
+
return r;
|
|
1741
|
+
});
|
|
1742
|
+
logger.debug(`compaction: launched shadow (snapshotLen=${snapshotLen})`);
|
|
1743
|
+
return s;
|
|
1744
|
+
};
|
|
1745
|
+
const applyShadow = async (s) => {
|
|
1746
|
+
const r = s.settled;
|
|
1747
|
+
if (!r) return false;
|
|
1748
|
+
if (!r.ok) {
|
|
1749
|
+
logger.warn(`compaction: failed, keeping context (${r.error.message})`);
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
const cr = r.value;
|
|
1753
|
+
if (cr.summary === "" || cr.keptMessages.length >= s.snapshotLen) return false;
|
|
1754
|
+
const summaryMsg = {
|
|
1755
|
+
// Mirrors buildSessionContext so in-memory swap and resume read identically.
|
|
1756
|
+
role: "user",
|
|
1757
|
+
content: `[Earlier conversation summarized]
|
|
1758
|
+
${cr.summary}`,
|
|
1759
|
+
timestamp: Date.now()
|
|
1760
|
+
};
|
|
1761
|
+
const rebased = [
|
|
1762
|
+
summaryMsg,
|
|
1763
|
+
...cr.keptMessages,
|
|
1764
|
+
...working.messages.slice(s.snapshotLen)
|
|
1765
|
+
// the arrival tail — messages appended during the in-flight compaction
|
|
1766
|
+
];
|
|
1767
|
+
const sum = (ms) => ms.reduce((n, m) => n + estimateMessageTokens(m), 0);
|
|
1768
|
+
const before = sum(working.messages);
|
|
1769
|
+
const after = sum(rebased);
|
|
1770
|
+
if (after >= before) {
|
|
1771
|
+
logger.debug("compaction: not smaller, rolled back");
|
|
1772
|
+
return false;
|
|
1773
|
+
}
|
|
1774
|
+
if (rebased.at(-1) !== working.messages.at(-1)) {
|
|
1775
|
+
logger.debug("compaction: tail identity check failed, rolled back");
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
working.messages = rebased;
|
|
1779
|
+
anchorFloor = rebased.length;
|
|
1780
|
+
lastSummary = cr.summary;
|
|
1781
|
+
await emit({
|
|
1782
|
+
type: "compaction",
|
|
1783
|
+
summary: cr.summary,
|
|
1784
|
+
tokensBefore: cr.tokensBefore,
|
|
1785
|
+
retainedAtSwap: rebased.length - 1,
|
|
1786
|
+
// excludes summaryMsg
|
|
1787
|
+
newMessagesAtSwap: newMessages.length
|
|
1788
|
+
});
|
|
1789
|
+
logger.info(`compaction: swapped ${before}->${after} tokens (kept ${cr.keptMessages.length})`);
|
|
1790
|
+
return true;
|
|
1791
|
+
};
|
|
1792
|
+
try {
|
|
1793
|
+
while (hasMoreToolCalls || pending.length > 0) {
|
|
1794
|
+
if (turns >= maxLoops) {
|
|
1795
|
+
reasonCode = "max_turns";
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
await emit({ type: "turn_start" });
|
|
1799
|
+
const steering = await drainSteering();
|
|
1800
|
+
if (steering.length > 0) pending.push(...steering);
|
|
1801
|
+
for (const prompt of pending) {
|
|
1802
|
+
await emit({ type: "message_start", message: prompt });
|
|
1803
|
+
working.messages.push(prompt);
|
|
1804
|
+
newMessages.push(prompt);
|
|
1805
|
+
await emit({ type: "message_end", message: prompt });
|
|
1806
|
+
}
|
|
1807
|
+
pending = [];
|
|
1808
|
+
if (signal?.aborted) {
|
|
1809
|
+
const assistant2 = failureMessage(config.model, "Agent run cancelled", "aborted");
|
|
1810
|
+
newMessages.push(assistant2);
|
|
1811
|
+
await emit({ type: "message_start", message: assistant2 });
|
|
1812
|
+
await emit({ type: "message_end", message: assistant2 });
|
|
1813
|
+
await emit({ type: "turn_end", message: assistant2, toolResults: [] });
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
if (compactionOn) {
|
|
1817
|
+
let handled = false;
|
|
1818
|
+
if (shadow?.settled) {
|
|
1819
|
+
await applyShadow(shadow);
|
|
1820
|
+
shadow = null;
|
|
1821
|
+
handled = true;
|
|
1822
|
+
}
|
|
1823
|
+
if (!handled) {
|
|
1824
|
+
const usage = estimateContextTokens(working.messages, anchorFloor);
|
|
1825
|
+
const tier = compactionTier(usage, contextWindow, settings);
|
|
1826
|
+
if (tier === "hard") {
|
|
1827
|
+
if (!shadow && !await compactionDeferred()) shadow = launchShadow();
|
|
1828
|
+
if (shadow) {
|
|
1829
|
+
await shadow.promise;
|
|
1830
|
+
await applyShadow(shadow);
|
|
1831
|
+
shadow = null;
|
|
1832
|
+
}
|
|
1833
|
+
} else if (tier === "soft" && !shadow && sawToolContinuation && !await compactionDeferred()) {
|
|
1834
|
+
shadow = launchShadow();
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
currentRunContext()?.controller?.turn();
|
|
1839
|
+
const assistant = await streamAssistantResponse(
|
|
1840
|
+
working,
|
|
1841
|
+
configForTurn(config, turns, working.messages),
|
|
1842
|
+
emit,
|
|
1843
|
+
signal,
|
|
1844
|
+
streamFn
|
|
1845
|
+
);
|
|
1846
|
+
addUsage(aggregateUsage, assistant.usage);
|
|
1847
|
+
working.messages.push(assistant);
|
|
1848
|
+
newMessages.push(assistant);
|
|
1849
|
+
turns++;
|
|
1850
|
+
const stopDecision = await config.hooks?.onResponse?.(assistant);
|
|
1851
|
+
if (assistant.stopReason === "error" || assistant.stopReason === "aborted") {
|
|
1852
|
+
await emit({ type: "turn_end", message: assistant, toolResults: [] });
|
|
1853
|
+
break;
|
|
1854
|
+
}
|
|
1855
|
+
if (stopDecision?.stop) {
|
|
1856
|
+
await emit({ type: "turn_end", message: assistant, toolResults: [] });
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
const toolCalls = toolCallsOf(assistant);
|
|
1860
|
+
hasMoreToolCalls = false;
|
|
1861
|
+
if (toolCalls.length === 0) {
|
|
1862
|
+
await emit({ type: "turn_end", message: assistant, toolResults: [] });
|
|
1863
|
+
const followUp = await drainFollowUp();
|
|
1864
|
+
if (followUp.length > 0) {
|
|
1865
|
+
pending = followUp;
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
const batch = await executeToolCalls({
|
|
1871
|
+
toolCalls,
|
|
1872
|
+
tools: config.tools ?? [],
|
|
1873
|
+
hooks: config.hooks,
|
|
1874
|
+
signal,
|
|
1875
|
+
mode: config.toolExecution ?? "parallel",
|
|
1876
|
+
assistantMessage: assistant,
|
|
1877
|
+
context: working,
|
|
1878
|
+
runContext: config.runContext,
|
|
1879
|
+
emit: (event) => emit(event)
|
|
1880
|
+
});
|
|
1881
|
+
for (const result of batch.messages) {
|
|
1882
|
+
await emit({ type: "message_start", message: result });
|
|
1883
|
+
working.messages.push(result);
|
|
1884
|
+
newMessages.push(result);
|
|
1885
|
+
await emit({ type: "message_end", message: result });
|
|
1886
|
+
}
|
|
1887
|
+
await emit({ type: "turn_end", message: assistant, toolResults: batch.messages });
|
|
1888
|
+
if (batch.terminate) {
|
|
1889
|
+
const followUp = await drainFollowUp();
|
|
1890
|
+
if (followUp.length > 0) {
|
|
1891
|
+
pending = followUp;
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
if (signal?.aborted) break;
|
|
1897
|
+
hasMoreToolCalls = true;
|
|
1898
|
+
sawToolContinuation = true;
|
|
1899
|
+
}
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
if (!(error instanceof ExecutionLimitError)) throw error;
|
|
1902
|
+
reasonCode = "execution_limit";
|
|
1903
|
+
} finally {
|
|
1904
|
+
if (shadow) {
|
|
1905
|
+
if (!shadow.settled) shadow.abort.abort();
|
|
1906
|
+
shadow.cleanup();
|
|
1907
|
+
await shadow.promise;
|
|
1908
|
+
}
|
|
1265
1909
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1910
|
+
const last = [...newMessages].reverse().find((message) => message.role === "assistant");
|
|
1911
|
+
if (signal?.aborted && last?.stopReason !== "aborted") {
|
|
1912
|
+
const cancelled = failureMessage(config.model, "Agent run cancelled", "aborted");
|
|
1913
|
+
cancelled.content = last?.content.filter((block) => block.type !== "toolCall") ?? [];
|
|
1914
|
+
newMessages.push(cancelled);
|
|
1915
|
+
await emit({ type: "message_start", message: cancelled });
|
|
1916
|
+
await emit({ type: "message_end", message: cancelled });
|
|
1917
|
+
} else if (!last) {
|
|
1918
|
+
const failed = failureMessage(config.model, "Run completed without an assistant message");
|
|
1919
|
+
newMessages.push(failed);
|
|
1920
|
+
await emit({ type: "message_start", message: failed });
|
|
1921
|
+
await emit({ type: "message_end", message: failed });
|
|
1922
|
+
}
|
|
1923
|
+
const final = [...newMessages].reverse().find((message) => message.role === "assistant");
|
|
1924
|
+
const limit = config.runContext?.controller?.snapshot().limit;
|
|
1925
|
+
if (limit && limit !== "cancelled") reasonCode = "execution_limit";
|
|
1926
|
+
const completion = runCompletion({
|
|
1927
|
+
stopReason: final?.stopReason ?? "error",
|
|
1928
|
+
...reasonCode === "max_turns" || reasonCode === "execution_limit" ? { outcome: "partial", reasonCode } : {}
|
|
1929
|
+
});
|
|
1930
|
+
await emit({
|
|
1931
|
+
type: "agent_end",
|
|
1932
|
+
messages: newMessages,
|
|
1933
|
+
aggregateUsage,
|
|
1934
|
+
execution: config.runContext?.controller?.snapshot(),
|
|
1935
|
+
...completion
|
|
1936
|
+
});
|
|
1937
|
+
return newMessages;
|
|
1938
|
+
}
|
|
1939
|
+
async function runAgentLoop(prompts, context, config, emit, signal) {
|
|
1940
|
+
const runContext = resolveRunContext(config.runContext, signal);
|
|
1941
|
+
const controller = new AbortController();
|
|
1942
|
+
const signals = [
|
|
1943
|
+
...new Set(
|
|
1944
|
+
[signal, runContext.signal, runContext.controller?.signal].filter(
|
|
1945
|
+
(s) => !!s
|
|
1946
|
+
)
|
|
1947
|
+
)
|
|
1948
|
+
];
|
|
1949
|
+
const abort = () => controller.abort();
|
|
1950
|
+
for (const s of signals) s.addEventListener("abort", abort, { once: true });
|
|
1951
|
+
if (signals.some((s) => s.aborted)) abort();
|
|
1952
|
+
try {
|
|
1953
|
+
return await withRunContext(
|
|
1954
|
+
runContext,
|
|
1955
|
+
() => runAgentLoopInternal(prompts, context, { ...config, runContext }, emit, controller.signal)
|
|
1956
|
+
);
|
|
1957
|
+
} finally {
|
|
1958
|
+
for (const s of signals) s.removeEventListener("abort", abort);
|
|
1270
1959
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
if (match(entry.mode)) drained.push(entry.message);
|
|
1278
|
-
else remaining.push(entry);
|
|
1279
|
-
}
|
|
1280
|
-
this.pending = remaining;
|
|
1281
|
-
return drained;
|
|
1960
|
+
}
|
|
1961
|
+
async function runAgentLoopContinue(context, config, emit, signal) {
|
|
1962
|
+
const last = context.messages.at(-1);
|
|
1963
|
+
if (!last) throw new Error("Cannot continue: no messages in context");
|
|
1964
|
+
if (last.role === "assistant") {
|
|
1965
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
1282
1966
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
for (const listener of this.listeners) {
|
|
1299
|
-
try {
|
|
1300
|
-
await listener(event, signal);
|
|
1301
|
-
} catch {
|
|
1302
|
-
}
|
|
1967
|
+
return runAgentLoop([], context, config, emit, signal);
|
|
1968
|
+
}
|
|
1969
|
+
function makeEventStream(run) {
|
|
1970
|
+
const queue = [];
|
|
1971
|
+
const waiters = [];
|
|
1972
|
+
let done = false;
|
|
1973
|
+
const emit = (event) => {
|
|
1974
|
+
const waiter = waiters.shift();
|
|
1975
|
+
if (waiter) waiter({ value: event, done: false });
|
|
1976
|
+
else queue.push(event);
|
|
1977
|
+
};
|
|
1978
|
+
const resultPromise = run(emit).finally(() => {
|
|
1979
|
+
done = true;
|
|
1980
|
+
while (waiters.length > 0) {
|
|
1981
|
+
waiters.shift()?.({ value: void 0, done: true });
|
|
1303
1982
|
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (!assistant) {
|
|
1322
|
-
throw new AgentHarnessError("invalid_state", "Run completed without an assistant message");
|
|
1983
|
+
});
|
|
1984
|
+
void resultPromise.catch(() => void 0);
|
|
1985
|
+
return {
|
|
1986
|
+
result: () => resultPromise,
|
|
1987
|
+
async *[Symbol.asyncIterator]() {
|
|
1988
|
+
while (true) {
|
|
1989
|
+
if (queue.length > 0) {
|
|
1990
|
+
yield queue.shift();
|
|
1991
|
+
} else if (done) {
|
|
1992
|
+
return;
|
|
1993
|
+
} else {
|
|
1994
|
+
const next = await new Promise((resolve2) => {
|
|
1995
|
+
waiters.push(resolve2);
|
|
1996
|
+
});
|
|
1997
|
+
if (next.done) return;
|
|
1998
|
+
yield next.value;
|
|
1999
|
+
}
|
|
1323
2000
|
}
|
|
1324
|
-
return assistant;
|
|
1325
|
-
} finally {
|
|
1326
|
-
this.running = false;
|
|
1327
|
-
this.controller = void 0;
|
|
1328
2001
|
}
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function agentLoop(prompts, context, config, signal) {
|
|
2005
|
+
return makeEventStream((emit) => runAgentLoop(prompts, context, config, emit, signal));
|
|
2006
|
+
}
|
|
2007
|
+
function agentLoopContinue(context, config, signal) {
|
|
2008
|
+
const last = context.messages.at(-1);
|
|
2009
|
+
if (!last) throw new Error("Cannot continue: no messages in context");
|
|
2010
|
+
if (last.role === "assistant") {
|
|
2011
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
1329
2012
|
}
|
|
1330
|
-
|
|
2013
|
+
return makeEventStream((emit) => runAgentLoopContinue(context, config, emit, signal));
|
|
2014
|
+
}
|
|
1331
2015
|
|
|
1332
2016
|
// src/backend.ts
|
|
1333
|
-
var EMPTY_USAGE = {
|
|
1334
|
-
input: 0,
|
|
1335
|
-
output: 0,
|
|
1336
|
-
cacheRead: 0,
|
|
1337
|
-
cacheWrite: 0,
|
|
1338
|
-
totalTokens: 0,
|
|
1339
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
1340
|
-
};
|
|
1341
|
-
function toRunResult(messages) {
|
|
1342
|
-
const assistant = lastAssistant(messages);
|
|
1343
|
-
return {
|
|
1344
|
-
messages,
|
|
1345
|
-
finalText: finalTextOf(assistant),
|
|
1346
|
-
usage: assistant?.usage ?? EMPTY_USAGE,
|
|
1347
|
-
stopReason: assistant?.stopReason ?? "error",
|
|
1348
|
-
errorMessage: assistant?.errorMessage
|
|
1349
|
-
};
|
|
1350
|
-
}
|
|
1351
2017
|
function toPrompts(task) {
|
|
1352
2018
|
if (typeof task === "string") {
|
|
1353
2019
|
return [{ role: "user", content: task, timestamp: Date.now() }];
|
|
@@ -1358,7 +2024,7 @@ var Ap3xRuntimeBackend = class _Ap3xRuntimeBackend {
|
|
|
1358
2024
|
options;
|
|
1359
2025
|
ephemeral;
|
|
1360
2026
|
stateful;
|
|
1361
|
-
active;
|
|
2027
|
+
active = /* @__PURE__ */ new Set();
|
|
1362
2028
|
listeners = /* @__PURE__ */ new Set();
|
|
1363
2029
|
constructor(options) {
|
|
1364
2030
|
const { ephemeral, ...agentOptions } = options;
|
|
@@ -1374,61 +2040,52 @@ var Ap3xRuntimeBackend = class _Ap3xRuntimeBackend {
|
|
|
1374
2040
|
return this.stateful;
|
|
1375
2041
|
}
|
|
1376
2042
|
async run(task, opts) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
const
|
|
1380
|
-
(
|
|
1381
|
-
);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
2043
|
+
let terminal;
|
|
2044
|
+
const context = resolveRunContext(opts?.context, opts?.signal);
|
|
2045
|
+
const messages = await this.execute(task, { ...opts, context }, (event) => {
|
|
2046
|
+
if (event.type === "agent_end") terminal = event;
|
|
2047
|
+
});
|
|
2048
|
+
return {
|
|
2049
|
+
...toRunResult(messages, terminal),
|
|
2050
|
+
runId: context.runId,
|
|
2051
|
+
parentRunId: context.parentRunId,
|
|
2052
|
+
execution: context.controller?.snapshot()
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
runStream(task, opts) {
|
|
2056
|
+
return makeEventStream((emit) => this.execute(task, opts, emit));
|
|
2057
|
+
}
|
|
2058
|
+
/** Both entry points drive the same Agent lifecycle, including state, hooks and subscribers. */
|
|
2059
|
+
async execute(task, opts, emit) {
|
|
2060
|
+
if (!this.ephemeral && this.active.size > 0) {
|
|
2061
|
+
throw new AgentHarnessError("busy", "Stateful backend is already processing a run");
|
|
1390
2062
|
}
|
|
1391
|
-
const
|
|
2063
|
+
const agent = this.agentFor();
|
|
2064
|
+
agent.runContext = resolveRunContext(opts?.context, opts?.signal);
|
|
2065
|
+
this.active.add(agent);
|
|
2066
|
+
let produced = [];
|
|
2067
|
+
const unsubscribe = agent.subscribe(async (event) => {
|
|
2068
|
+
if (event.type === "agent_end") produced = event.messages;
|
|
2069
|
+
await emit(event);
|
|
2070
|
+
for (const listener of this.listeners) await listener(event);
|
|
2071
|
+
});
|
|
2072
|
+
const signals = [
|
|
2073
|
+
...new Set(
|
|
2074
|
+
[opts?.signal, agent.runContext.signal].filter((signal) => !!signal)
|
|
2075
|
+
)
|
|
2076
|
+
];
|
|
2077
|
+
const onAbort = () => agent.abort();
|
|
2078
|
+
for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
|
|
1392
2079
|
try {
|
|
1393
|
-
|
|
2080
|
+
const pending = withRunContext(agent.runContext, () => agent.prompt(toPrompts(task)));
|
|
2081
|
+
if (signals.some((signal) => signal.aborted)) agent.abort();
|
|
2082
|
+
await pending;
|
|
2083
|
+
return produced;
|
|
1394
2084
|
} finally {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1399
|
-
this.active = void 0;
|
|
2085
|
+
unsubscribe();
|
|
2086
|
+
for (const signal of signals) signal.removeEventListener("abort", onAbort);
|
|
2087
|
+
this.active.delete(agent);
|
|
1400
2088
|
}
|
|
1401
|
-
const messages = this.ephemeral ? agent.messages : agent.messages.slice(before);
|
|
1402
|
-
return toRunResult(messages);
|
|
1403
|
-
}
|
|
1404
|
-
runStream(task, opts) {
|
|
1405
|
-
const agent = this.agentFor();
|
|
1406
|
-
return agentLoop(
|
|
1407
|
-
toPrompts(task),
|
|
1408
|
-
{
|
|
1409
|
-
systemPrompt: this.options.systemPrompt,
|
|
1410
|
-
messages: this.ephemeral ? [] : [...agent.messages],
|
|
1411
|
-
tools: this.options.tools?.map((tool) => ({
|
|
1412
|
-
name: tool.name,
|
|
1413
|
-
description: tool.description,
|
|
1414
|
-
parameters: tool.parameters
|
|
1415
|
-
}))
|
|
1416
|
-
},
|
|
1417
|
-
{
|
|
1418
|
-
model: this.options.model,
|
|
1419
|
-
systemPrompt: this.options.systemPrompt,
|
|
1420
|
-
tools: this.options.tools,
|
|
1421
|
-
reasoning: this.options.reasoning,
|
|
1422
|
-
maxLoops: this.options.maxLoops,
|
|
1423
|
-
toolExecution: this.options.toolExecution,
|
|
1424
|
-
streamOptions: this.options.streamOptions,
|
|
1425
|
-
hooks: this.options.hooks,
|
|
1426
|
-
streamFn: this.options.streamFn,
|
|
1427
|
-
compaction: resolveAutoCompact(this.options.autoCompact),
|
|
1428
|
-
previousSummary: this.options.previousSummary
|
|
1429
|
-
},
|
|
1430
|
-
opts?.signal
|
|
1431
|
-
);
|
|
1432
2089
|
}
|
|
1433
2090
|
subscribe(listener) {
|
|
1434
2091
|
this.listeners.add(listener);
|
|
@@ -1437,7 +2094,7 @@ var Ap3xRuntimeBackend = class _Ap3xRuntimeBackend {
|
|
|
1437
2094
|
};
|
|
1438
2095
|
}
|
|
1439
2096
|
abort() {
|
|
1440
|
-
this.active
|
|
2097
|
+
for (const agent of this.active) agent.abort();
|
|
1441
2098
|
}
|
|
1442
2099
|
clone() {
|
|
1443
2100
|
return new _Ap3xRuntimeBackend({ ...this.options, ephemeral: this.ephemeral });
|
|
@@ -1448,6 +2105,7 @@ var Ap3xRuntimeBackend = class _Ap3xRuntimeBackend {
|
|
|
1448
2105
|
case "subscribe":
|
|
1449
2106
|
case "abort":
|
|
1450
2107
|
case "clone":
|
|
2108
|
+
case "execution-controls":
|
|
1451
2109
|
return true;
|
|
1452
2110
|
case "tools":
|
|
1453
2111
|
return (this.options.tools?.length ?? 0) > 0;
|
|
@@ -1455,6 +2113,9 @@ var Ap3xRuntimeBackend = class _Ap3xRuntimeBackend {
|
|
|
1455
2113
|
return false;
|
|
1456
2114
|
}
|
|
1457
2115
|
}
|
|
2116
|
+
checkpointIdentity() {
|
|
2117
|
+
return { backend: "native", configuration: this.options };
|
|
2118
|
+
}
|
|
1458
2119
|
getCurrentModel() {
|
|
1459
2120
|
return this.options.model;
|
|
1460
2121
|
}
|
|
@@ -1548,8 +2209,8 @@ ${entry.summary}`,
|
|
|
1548
2209
|
return context;
|
|
1549
2210
|
}
|
|
1550
2211
|
var Session = class {
|
|
1551
|
-
constructor(
|
|
1552
|
-
this.storage =
|
|
2212
|
+
constructor(storage2) {
|
|
2213
|
+
this.storage = storage2;
|
|
1553
2214
|
}
|
|
1554
2215
|
storage;
|
|
1555
2216
|
getStorage() {
|
|
@@ -1597,6 +2258,17 @@ var Session = class {
|
|
|
1597
2258
|
message
|
|
1598
2259
|
}));
|
|
1599
2260
|
}
|
|
2261
|
+
/** Append non-model-visible host metadata to the active branch. */
|
|
2262
|
+
appendCustom(customType, data) {
|
|
2263
|
+
return this.appendUnder(this.storage.getLeafId(), (id) => ({
|
|
2264
|
+
id,
|
|
2265
|
+
parentId: null,
|
|
2266
|
+
timestamp: 0,
|
|
2267
|
+
type: "custom",
|
|
2268
|
+
customType,
|
|
2269
|
+
data
|
|
2270
|
+
}));
|
|
2271
|
+
}
|
|
1600
2272
|
/** Append a compaction boundary. Returns the new id. */
|
|
1601
2273
|
appendCompaction(summary, firstKeptEntryId, tokensBefore) {
|
|
1602
2274
|
if (!this.storage.getEntry(firstKeptEntryId)) {
|
|
@@ -1907,6 +2579,79 @@ var StringJsonlFile = class {
|
|
|
1907
2579
|
}
|
|
1908
2580
|
};
|
|
1909
2581
|
|
|
2582
|
+
// src/session-attempt.ts
|
|
2583
|
+
var SessionAttemptRecorder = class {
|
|
2584
|
+
constructor(session) {
|
|
2585
|
+
this.session = session;
|
|
2586
|
+
session.appendCustom("run_attempt", { attemptId: this.attemptId, phase: "started" });
|
|
2587
|
+
}
|
|
2588
|
+
session;
|
|
2589
|
+
attemptId = uuidv7();
|
|
2590
|
+
count = 0;
|
|
2591
|
+
finished = false;
|
|
2592
|
+
compaction;
|
|
2593
|
+
observe(event) {
|
|
2594
|
+
if (this.finished) return;
|
|
2595
|
+
if (event.type === "message_end") this.append(event.message);
|
|
2596
|
+
if (event.type === "compaction") this.compaction = event;
|
|
2597
|
+
}
|
|
2598
|
+
append(message) {
|
|
2599
|
+
this.session.appendMessage(message);
|
|
2600
|
+
this.count++;
|
|
2601
|
+
}
|
|
2602
|
+
finish(result) {
|
|
2603
|
+
if (this.finished) return;
|
|
2604
|
+
for (const message of result.messages.slice(this.count)) this.append(message);
|
|
2605
|
+
if (this.compaction) {
|
|
2606
|
+
const kept = this.compaction.retainedAtSwap + (this.count - this.compaction.newMessagesAtSwap);
|
|
2607
|
+
const entries = this.session.getBranch().filter(producesContextMessage);
|
|
2608
|
+
const anchor = entries[entries.length - kept];
|
|
2609
|
+
if (anchor)
|
|
2610
|
+
this.session.appendCompaction(
|
|
2611
|
+
this.compaction.summary,
|
|
2612
|
+
anchor.id,
|
|
2613
|
+
this.compaction.tokensBefore
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
this.session.appendCustom("run_attempt", {
|
|
2617
|
+
attemptId: this.attemptId,
|
|
2618
|
+
phase: "finished",
|
|
2619
|
+
...runCompletion(result),
|
|
2620
|
+
errorMessage: result.errorMessage
|
|
2621
|
+
});
|
|
2622
|
+
this.finished = true;
|
|
2623
|
+
}
|
|
2624
|
+
fail(error) {
|
|
2625
|
+
if (this.finished) return;
|
|
2626
|
+
this.session.appendCustom("run_attempt", {
|
|
2627
|
+
attemptId: this.attemptId,
|
|
2628
|
+
phase: "finished",
|
|
2629
|
+
outcome: "failed",
|
|
2630
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2631
|
+
});
|
|
2632
|
+
this.finished = true;
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
function inspectSessionAttempts(session) {
|
|
2636
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
2637
|
+
for (const entry of session.getBranch()) {
|
|
2638
|
+
if (entry.type !== "custom" || entry.customType !== "run_attempt" || !entry.data || typeof entry.data !== "object")
|
|
2639
|
+
continue;
|
|
2640
|
+
const data = entry.data;
|
|
2641
|
+
if (typeof data.attemptId !== "string") continue;
|
|
2642
|
+
if (data.phase === "started")
|
|
2643
|
+
attempts.set(data.attemptId, { attemptId: data.attemptId, status: "incomplete" });
|
|
2644
|
+
else if (data.phase === "finished" && ["succeeded", "failed", "cancelled", "partial"].includes(String(data.outcome))) {
|
|
2645
|
+
attempts.set(data.attemptId, {
|
|
2646
|
+
attemptId: data.attemptId,
|
|
2647
|
+
status: data.outcome,
|
|
2648
|
+
...typeof data.errorMessage === "string" ? { errorMessage: data.errorMessage } : {}
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return [...attempts.values()];
|
|
2653
|
+
}
|
|
2654
|
+
|
|
1910
2655
|
// src/harness.ts
|
|
1911
2656
|
var AgentHarness = class {
|
|
1912
2657
|
session;
|
|
@@ -1928,8 +2673,12 @@ var AgentHarness = class {
|
|
|
1928
2673
|
*/
|
|
1929
2674
|
unpersistedSummary;
|
|
1930
2675
|
busy = false;
|
|
2676
|
+
incrementalPersistence;
|
|
2677
|
+
recorder;
|
|
2678
|
+
terminal;
|
|
1931
2679
|
constructor(options) {
|
|
1932
|
-
const { session, ...agentOptions } = options;
|
|
2680
|
+
const { session, incrementalPersistence, ...agentOptions } = options;
|
|
2681
|
+
this.incrementalPersistence = incrementalPersistence ?? false;
|
|
1933
2682
|
this.session = session;
|
|
1934
2683
|
this.agentOptions = agentOptions;
|
|
1935
2684
|
const context = session.buildContext();
|
|
@@ -1945,16 +2694,21 @@ var AgentHarness = class {
|
|
|
1945
2694
|
this.busy = true;
|
|
1946
2695
|
const before = this.agent.messages.length;
|
|
1947
2696
|
try {
|
|
2697
|
+
this.terminal = void 0;
|
|
2698
|
+
if (this.incrementalPersistence) this.recorder = new SessionAttemptRecorder(this.session);
|
|
1948
2699
|
const carried = this.pendingNextTurn;
|
|
1949
2700
|
this.pendingNextTurn = [];
|
|
1950
2701
|
const userMessage = { role: "user", content: text, timestamp: Date.now() };
|
|
1951
2702
|
const assistant = await this.agent.prompt([...carried, userMessage]);
|
|
1952
2703
|
const produced = this.agent.messages.slice(before);
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
}
|
|
2704
|
+
if (this.recorder) this.recorder.finish(toRunResult(produced, this.terminal));
|
|
2705
|
+
else for (const message of produced) this.session.appendMessage(message);
|
|
1956
2706
|
return assistant;
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
this.recorder?.fail(error);
|
|
2709
|
+
throw error;
|
|
1957
2710
|
} finally {
|
|
2711
|
+
this.recorder = void 0;
|
|
1958
2712
|
this.busy = false;
|
|
1959
2713
|
}
|
|
1960
2714
|
}
|
|
@@ -2072,6 +2826,8 @@ ${value.summary}`,
|
|
|
2072
2826
|
this.agent = new Agent({ ...this.agentOptions, messages });
|
|
2073
2827
|
if (previous) this.agent.adoptPendingFrom(previous);
|
|
2074
2828
|
this.agentUnsub = this.agent.subscribe(async (event) => {
|
|
2829
|
+
this.recorder?.observe(event);
|
|
2830
|
+
if (event.type === "agent_end") this.terminal = event;
|
|
2075
2831
|
for (const listener of this.listeners) await listener(event);
|
|
2076
2832
|
});
|
|
2077
2833
|
}
|
|
@@ -2686,9 +3442,9 @@ var FileJsonlFile = class {
|
|
|
2686
3442
|
appendFileSync(this.path, line);
|
|
2687
3443
|
}
|
|
2688
3444
|
};
|
|
2689
|
-
function entriesToFork(
|
|
2690
|
-
if (!options.entryId) return
|
|
2691
|
-
const target =
|
|
3445
|
+
function entriesToFork(storage2, options) {
|
|
3446
|
+
if (!options.entryId) return storage2.getEntries();
|
|
3447
|
+
const target = storage2.getEntry(options.entryId);
|
|
2692
3448
|
if (!target) {
|
|
2693
3449
|
throw new SessionError("not_found", `Fork target not found: ${options.entryId}`);
|
|
2694
3450
|
}
|
|
@@ -2705,7 +3461,7 @@ function entriesToFork(storage, options) {
|
|
|
2705
3461
|
effectiveLeafId = target.parentId;
|
|
2706
3462
|
}
|
|
2707
3463
|
if (effectiveLeafId === null) return [];
|
|
2708
|
-
return
|
|
3464
|
+
return storage2.getPathToRoot(effectiveLeafId);
|
|
2709
3465
|
}
|
|
2710
3466
|
var JsonlSessionRepo = class {
|
|
2711
3467
|
root;
|
|
@@ -2713,6 +3469,9 @@ var JsonlSessionRepo = class {
|
|
|
2713
3469
|
this.root = options?.root ?? join2(homedir(), ".ap3x", "sessions");
|
|
2714
3470
|
}
|
|
2715
3471
|
pathFor(id) {
|
|
3472
|
+
if (!/^[A-Za-z0-9_-]{1,100}$/.test(id)) {
|
|
3473
|
+
throw new SessionError("invalid_session", "Invalid session id");
|
|
3474
|
+
}
|
|
2716
3475
|
return join2(this.root, `${id}.jsonl`);
|
|
2717
3476
|
}
|
|
2718
3477
|
fileFor(id) {
|
|
@@ -2720,12 +3479,12 @@ var JsonlSessionRepo = class {
|
|
|
2720
3479
|
}
|
|
2721
3480
|
create(options = {}) {
|
|
2722
3481
|
const id = options.id ?? createSessionId();
|
|
2723
|
-
const
|
|
3482
|
+
const storage2 = JsonlSessionStorage.create(this.fileFor(id), {
|
|
2724
3483
|
cwd: options.cwd,
|
|
2725
3484
|
sessionId: id,
|
|
2726
3485
|
parentSessionId: options.parentSessionId
|
|
2727
3486
|
});
|
|
2728
|
-
return new Session(
|
|
3487
|
+
return new Session(storage2);
|
|
2729
3488
|
}
|
|
2730
3489
|
open(id) {
|
|
2731
3490
|
if (!existsSync(this.pathFor(id))) {
|
|
@@ -2746,13 +3505,13 @@ var JsonlSessionRepo = class {
|
|
|
2746
3505
|
if (!name.endsWith(".jsonl")) continue;
|
|
2747
3506
|
const path = join2(this.root, name);
|
|
2748
3507
|
try {
|
|
2749
|
-
const
|
|
2750
|
-
const metadata =
|
|
3508
|
+
const storage2 = JsonlSessionStorage.open(new FileJsonlFile(path));
|
|
3509
|
+
const metadata = storage2.getMetadata();
|
|
2751
3510
|
sessions.push({
|
|
2752
3511
|
id: metadata.id,
|
|
2753
3512
|
path,
|
|
2754
3513
|
cwd: metadata.cwd,
|
|
2755
|
-
name: new Session(
|
|
3514
|
+
name: new Session(storage2).buildContext().sessionName,
|
|
2756
3515
|
parentSessionId: metadata.parentSessionId,
|
|
2757
3516
|
createdAt: metadata.timestamp,
|
|
2758
3517
|
updatedAt: statSync(path).mtimeMs
|
|
@@ -3996,54 +4755,6 @@ var SafeStateManager = {
|
|
|
3996
4755
|
}
|
|
3997
4756
|
};
|
|
3998
4757
|
|
|
3999
|
-
// src/agent-errors.ts
|
|
4000
|
-
var AgentError = class extends Error {
|
|
4001
|
-
/** Machine-readable, stable error code. */
|
|
4002
|
-
code;
|
|
4003
|
-
cause;
|
|
4004
|
-
constructor(message, code = "agent", cause) {
|
|
4005
|
-
super(message);
|
|
4006
|
-
this.name = new.target.name;
|
|
4007
|
-
this.code = code;
|
|
4008
|
-
this.cause = cause;
|
|
4009
|
-
}
|
|
4010
|
-
};
|
|
4011
|
-
var AgentInitializationError = class extends AgentError {
|
|
4012
|
-
constructor(message, cause) {
|
|
4013
|
-
super(message, "agent_initialization", cause);
|
|
4014
|
-
}
|
|
4015
|
-
};
|
|
4016
|
-
var AgentRunError = class extends AgentError {
|
|
4017
|
-
constructor(message, cause) {
|
|
4018
|
-
super(message, "agent_run", cause);
|
|
4019
|
-
}
|
|
4020
|
-
};
|
|
4021
|
-
var AgentLLMError = class extends AgentError {
|
|
4022
|
-
constructor(message, cause) {
|
|
4023
|
-
super(message, "agent_llm", cause);
|
|
4024
|
-
}
|
|
4025
|
-
};
|
|
4026
|
-
var AgentToolError = class extends AgentError {
|
|
4027
|
-
constructor(message, cause) {
|
|
4028
|
-
super(message, "agent_tool", cause);
|
|
4029
|
-
}
|
|
4030
|
-
};
|
|
4031
|
-
var AgentMemoryError = class extends AgentError {
|
|
4032
|
-
constructor(message, cause) {
|
|
4033
|
-
super(message, "agent_memory", cause);
|
|
4034
|
-
}
|
|
4035
|
-
};
|
|
4036
|
-
var AgentMCPConnectionError = class extends AgentError {
|
|
4037
|
-
constructor(message, cause) {
|
|
4038
|
-
super(message, "agent_mcp_connection", cause);
|
|
4039
|
-
}
|
|
4040
|
-
};
|
|
4041
|
-
var AgentMCPToolError = class extends AgentError {
|
|
4042
|
-
constructor(message, cause) {
|
|
4043
|
-
super(message, "agent_mcp_tool", cause);
|
|
4044
|
-
}
|
|
4045
|
-
};
|
|
4046
|
-
|
|
4047
4758
|
// src/index.ts
|
|
4048
4759
|
var PACKAGE = "@ap3x/agent-core";
|
|
4049
4760
|
var VERSION = "0.1.0";
|
|
@@ -4062,6 +4773,7 @@ export {
|
|
|
4062
4773
|
AgentRegistry,
|
|
4063
4774
|
AgentRegistrySchema,
|
|
4064
4775
|
AgentRunError,
|
|
4776
|
+
AgentRunFailure,
|
|
4065
4777
|
AgentSpecSchema,
|
|
4066
4778
|
AgentToolError,
|
|
4067
4779
|
Ap3xError,
|
|
@@ -4070,7 +4782,9 @@ export {
|
|
|
4070
4782
|
Conversation,
|
|
4071
4783
|
DEFAULT_COMPACTION_SETTINGS,
|
|
4072
4784
|
DEFAULT_MAX_LOOPS,
|
|
4785
|
+
ExecutionController,
|
|
4073
4786
|
ExecutionError,
|
|
4787
|
+
ExecutionLimitError,
|
|
4074
4788
|
FileError,
|
|
4075
4789
|
FileJsonlFile,
|
|
4076
4790
|
InMemorySessionStorage,
|
|
@@ -4078,36 +4792,48 @@ export {
|
|
|
4078
4792
|
JsonlSessionStorage,
|
|
4079
4793
|
NodeExecutionEnv,
|
|
4080
4794
|
PACKAGE,
|
|
4795
|
+
PolicyDeniedError,
|
|
4081
4796
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
4082
4797
|
SYSTEM_PROMPT_SECTIONS,
|
|
4083
4798
|
SafeStateManager,
|
|
4084
4799
|
SerializableMixin,
|
|
4085
4800
|
Session,
|
|
4801
|
+
SessionAttemptRecorder,
|
|
4086
4802
|
SessionError,
|
|
4087
4803
|
SkillRegistry,
|
|
4088
4804
|
StringJsonlFile,
|
|
4089
4805
|
SystemPromptBuilder,
|
|
4090
4806
|
VERSION,
|
|
4807
|
+
addUsage,
|
|
4091
4808
|
agentLoop,
|
|
4092
4809
|
agentLoopContinue,
|
|
4810
|
+
argumentsHash,
|
|
4093
4811
|
assembleSystemPrompt,
|
|
4812
|
+
assertRunSucceeded,
|
|
4813
|
+
authorizeTool,
|
|
4094
4814
|
availableParallelism,
|
|
4095
4815
|
buildSessionContext,
|
|
4096
4816
|
compact,
|
|
4097
4817
|
compactionTier,
|
|
4818
|
+
controlledProviderCall,
|
|
4098
4819
|
createLimiter,
|
|
4820
|
+
createRunContext,
|
|
4099
4821
|
createRuntimeBackend,
|
|
4100
4822
|
createSessionId,
|
|
4101
4823
|
createStateDict,
|
|
4824
|
+
currentRunContext,
|
|
4102
4825
|
defineTool,
|
|
4103
4826
|
detectShell,
|
|
4827
|
+
developmentPolicy,
|
|
4104
4828
|
discoverSkillFiles,
|
|
4105
4829
|
discoverSkills,
|
|
4830
|
+
emptyUsage,
|
|
4106
4831
|
environmentSection,
|
|
4107
4832
|
err,
|
|
4108
4833
|
estimateContextTokens,
|
|
4109
4834
|
estimateMessageTokens,
|
|
4110
4835
|
estimateTokens,
|
|
4836
|
+
executeMember,
|
|
4111
4837
|
executeShellWithCapture,
|
|
4112
4838
|
executeToolCalls,
|
|
4113
4839
|
finalTextOf,
|
|
@@ -4118,7 +4844,10 @@ export {
|
|
|
4118
4844
|
getLogLevel,
|
|
4119
4845
|
getOrThrow,
|
|
4120
4846
|
getOrUndefined,
|
|
4847
|
+
hasOpaqueReservation,
|
|
4121
4848
|
historyOutputFormatter,
|
|
4849
|
+
immutableArguments,
|
|
4850
|
+
inspectSessionAttempts,
|
|
4122
4851
|
isBlockedCommand,
|
|
4123
4852
|
isClassInstance,
|
|
4124
4853
|
isSafeType,
|
|
@@ -4127,6 +4856,7 @@ export {
|
|
|
4127
4856
|
loadSkill,
|
|
4128
4857
|
loadSkills,
|
|
4129
4858
|
logger,
|
|
4859
|
+
makeEventStream,
|
|
4130
4860
|
mapLimit,
|
|
4131
4861
|
matchBlocklist,
|
|
4132
4862
|
ok,
|
|
@@ -4137,11 +4867,16 @@ export {
|
|
|
4137
4867
|
prepareCompaction,
|
|
4138
4868
|
preserveInstances,
|
|
4139
4869
|
producesContextMessage,
|
|
4870
|
+
productionPolicy,
|
|
4871
|
+
readOnlyPolicy,
|
|
4140
4872
|
renderPromptTemplate,
|
|
4873
|
+
reserveProviderCall,
|
|
4141
4874
|
resolveAutoCompact,
|
|
4142
4875
|
resolveCompactionSettings,
|
|
4876
|
+
resolveRunContext,
|
|
4143
4877
|
runAgentLoop,
|
|
4144
4878
|
runAgentLoopContinue,
|
|
4879
|
+
runCompletion,
|
|
4145
4880
|
serializeAttr,
|
|
4146
4881
|
serializeState,
|
|
4147
4882
|
setLogLevel,
|
|
@@ -4149,5 +4884,8 @@ export {
|
|
|
4149
4884
|
shouldCompact,
|
|
4150
4885
|
substituteArgs,
|
|
4151
4886
|
toError,
|
|
4152
|
-
|
|
4887
|
+
toRunResult,
|
|
4888
|
+
uuidv7,
|
|
4889
|
+
withRunContext,
|
|
4890
|
+
withWorkflowContext
|
|
4153
4891
|
};
|