@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/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 = { assistantMessage, agentContext: context };
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 executeToolCalls(options) {
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-loop.ts
689
- var DEFAULT_MAX_LOOPS = 1e3;
690
- function emptyUsage() {
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
- async function streamAssistantResponse(context, config, emit, signal, streamFn) {
722
- const transformed = config.hooks?.transformContext ? await config.hooks.transformContext(context.messages, signal) : context.messages;
723
- const messages = config.hooks?.convertToLlm ? await config.hooks.convertToLlm(transformed) : (
724
- // Without a convertToLlm hook the (already transformed) transcript passes
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
- /** Enqueue a pending message and notify observers via `queue_update`. */
1267
- enqueue(mode, message) {
1268
- this.pending.push({ mode, message });
1269
- void this.emitQueueUpdate();
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
- /** Remove and return (in FIFO order) the messages whose mode matches. */
1272
- drainQueue(match) {
1273
- if (this.pending.length === 0) return [];
1274
- const drained = [];
1275
- const remaining = [];
1276
- for (const entry of this.pending) {
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
- * Emit a `queue_update` snapshot to every listener. Best-effort and
1285
- * out-of-band: a failing observer is isolated so it can neither break an
1286
- * enqueue nor the active run. Uses the active run's signal when running, else
1287
- * a never-aborted idle signal.
1288
- */
1289
- async emitQueueUpdate() {
1290
- const byMode = (mode) => this.pending.filter((entry) => entry.mode === mode).map((entry) => entry.message);
1291
- const event = {
1292
- type: "queue_update",
1293
- steer: byMode("steer"),
1294
- followUp: byMode("followUp"),
1295
- nextTurn: byMode("nextTurn")
1296
- };
1297
- const signal = this.controller?.signal ?? this.idleSignal;
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
- async runWithLifecycle(run) {
1306
- this.running = true;
1307
- this.lastError = void 0;
1308
- this.controller = new AbortController();
1309
- const signal = this.controller.signal;
1310
- const emit = async (event) => {
1311
- if (event.type === "compaction") this.previousSummary = event.summary;
1312
- for (const listener of this.listeners) {
1313
- await listener(event, signal);
1314
- }
1315
- };
1316
- try {
1317
- const newMessages = await run(emit, signal);
1318
- this.transcript.push(...newMessages);
1319
- const assistant = lastAssistant(newMessages);
1320
- if (assistant?.errorMessage) this.lastError = assistant.errorMessage;
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
- const agent = this.agentFor();
1378
- this.active = agent;
1379
- const unsubscribers = [...this.listeners].map(
1380
- (listener) => agent.subscribe((event) => listener(event))
1381
- );
1382
- let onExternalAbort;
1383
- if (opts?.signal) {
1384
- onExternalAbort = () => agent.abort();
1385
- if (opts.signal.aborted) {
1386
- queueMicrotask(() => agent.abort());
1387
- } else {
1388
- opts.signal.addEventListener("abort", onExternalAbort, { once: true });
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 before = this.ephemeral ? 0 : agent.messages.length;
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
- await agent.prompt(toPrompts(task));
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
- for (const unsubscribe of unsubscribers) unsubscribe();
1396
- if (opts?.signal && onExternalAbort) {
1397
- opts.signal.removeEventListener("abort", onExternalAbort);
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?.abort();
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(storage) {
1552
- this.storage = 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
- for (const message of produced) {
1954
- this.session.appendMessage(message);
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(storage, options) {
2690
- if (!options.entryId) return storage.getEntries();
2691
- const target = storage.getEntry(options.entryId);
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 storage.getPathToRoot(effectiveLeafId);
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 storage = JsonlSessionStorage.create(this.fileFor(id), {
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(storage);
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 storage = JsonlSessionStorage.open(new FileJsonlFile(path));
2750
- const metadata = storage.getMetadata();
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(storage).buildContext().sessionName,
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
- uuidv7
4887
+ toRunResult,
4888
+ uuidv7,
4889
+ withRunContext,
4890
+ withWorkflowContext
4153
4891
  };