@arnilo/prism 0.9.0 → 0.11.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/CHANGELOG.md +50 -1
- package/README.md +19 -16
- package/dist/agent-approval.d.ts +7 -1
- package/dist/agent-approval.js +15 -6
- package/dist/agent-run-lifecycle.d.ts +2 -1
- package/dist/agent-run-lifecycle.js +20 -6
- package/dist/agent-run-state.d.ts +26 -5
- package/dist/agent-run-state.js +97 -1
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/session/assemble.js +165 -16
- package/dist/agent-session/session/persist.js +11 -5
- package/dist/agent-session/session/provider-round.js +54 -13
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +86 -23
- package/dist/agent-session/session/types.d.ts +21 -2
- package/dist/agent-session/session.d.ts +66 -4
- package/dist/agent-session/session.js +159 -18
- package/dist/checkpoint-restore.d.ts +50 -14
- package/dist/checkpoint-restore.js +104 -28
- package/dist/context-budget.d.ts +11 -0
- package/dist/context-budget.js +33 -2
- package/dist/contracts-core/agent.d.ts +26 -5
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +8 -3
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +6 -1
- package/dist/contracts-core/run-limits.d.ts +10 -1
- package/dist/contracts-core/session.d.ts +2 -1
- package/dist/contracts-protocol.d.ts +6 -4
- package/dist/contracts-run-state.d.ts +48 -6
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/types.d.ts +10 -0
- package/dist/guardrail-packs/validation-respect.js +16 -0
- package/dist/guardrails.d.ts +42 -1
- package/dist/guardrails.js +124 -15
- package/dist/index.d.ts +7 -7
- package/dist/index.js +4 -4
- package/dist/leases.js +32 -6
- package/dist/middleware.d.ts +1 -1
- package/dist/node/contribution-discovery.d.ts +16 -1
- package/dist/node/contribution-discovery.js +47 -0
- package/dist/node/session-store-jsonl.js +67 -17
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +4 -1
- package/dist/run-limits.d.ts +11 -5
- package/dist/run-limits.js +13 -0
- package/dist/session-stores.js +61 -12
- package/dist/testing/prefix-stability-conformance.d.ts +73 -1
- package/dist/testing/prefix-stability-conformance.js +158 -27
- package/dist/tools.js +10 -3
- package/dist/usage-estimation.d.ts +7 -1
- package/dist/usage-estimation.js +16 -10
- package/docs/acp.md +2 -2
- package/docs/agent-events.md +15 -10
- package/docs/agent-session-runtime.md +10 -7
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-tools.md +7 -11
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +21 -1
- package/docs/context-and-skills.md +6 -7
- package/docs/contribution-discovery.md +13 -0
- package/docs/durable-runs.md +14 -6
- package/docs/embeddings.md +7 -1
- package/docs/execution-timeline.md +9 -2
- package/docs/extensions.md +21 -5
- package/docs/guardrails.md +16 -6
- package/docs/hooks.md +282 -0
- package/docs/impeccable.md +1 -2
- package/docs/index.md +28 -21
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/instruction-injection.md +1 -0
- package/docs/live-testing.md +3 -2
- package/docs/memory-fabric.md +29 -0
- package/docs/middleware-hooks.md +54 -4
- package/docs/migrate-to-0.11.md +65 -0
- package/docs/migration.md +24 -0
- package/docs/node-jsonl-session-store.md +4 -3
- package/docs/operations.md +1 -1
- package/docs/options-index.md +3 -1
- package/docs/peer-dependencies.md +3 -5
- package/docs/policy-and-audit.md +15 -2
- package/docs/prefix-stability-conformance.md +82 -9
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +2 -1
- package/docs/rag.md +94 -7
- package/docs/release-and-install.md +62 -59
- package/docs/runs-and-usage.md +21 -10
- package/docs/scoped-agent-memory.md +17 -9
- package/docs/scoped-memory.md +138 -0
- package/docs/session-stores.md +2 -2
- package/docs/supervisors.md +14 -6
- package/docs/testing.md +17 -9
- package/docs/tools.md +1 -1
- package/docs/wiki.md +4 -2
- package/docs/workflows.md +2 -2
- package/package.json +8 -5
- package/docs/caveman.md +0 -130
- package/docs/graft.md +0 -149
- package/docs/ponytail.md +0 -129
|
@@ -56,6 +56,104 @@ class TurnPolicyError extends Error {
|
|
|
56
56
|
this.name = "TurnPolicyError";
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
/** Stop-hook misuse: a throwing or malformed hook fails the run closed (plan 106 R1). */
|
|
60
|
+
class StopHookError extends Error {
|
|
61
|
+
code = "ERR_PRISM_STOP_HOOK";
|
|
62
|
+
constructor(message, options) {
|
|
63
|
+
super(message, options);
|
|
64
|
+
this.name = "StopHookError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Validate the merged stop-hook list once per run, before any provider turn (plan 106 R1). */
|
|
68
|
+
function assertStopHooks(hooks) {
|
|
69
|
+
for (const hook of hooks) {
|
|
70
|
+
if (typeof hook !== "object" ||
|
|
71
|
+
hook === null ||
|
|
72
|
+
typeof hook.name !== "string" ||
|
|
73
|
+
hook.name.length === 0 ||
|
|
74
|
+
typeof hook.decide !== "function") {
|
|
75
|
+
throw new TypeError("stopHooks entries must be StopHook objects with a non-empty name and a decide function");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run stop hooks in order at a natural loop end (plan 106 R1). The first `continue` wins; every
|
|
81
|
+
* `stop` (or no hook continuing) leaves the run finished. Hook context is metadata plus the live
|
|
82
|
+
* transcript — tool arguments, prompts, and results are never reshaped by core.
|
|
83
|
+
*/
|
|
84
|
+
async function evaluateStopHooks(ctx, stopHookActive) {
|
|
85
|
+
const context = {
|
|
86
|
+
sessionId: ctx.session.id,
|
|
87
|
+
runId: ctx.runId,
|
|
88
|
+
turn: ctx.limits.snapshot().turns,
|
|
89
|
+
history: ctx.loopCtx.history,
|
|
90
|
+
metadata: ctx.metadata,
|
|
91
|
+
signal: ctx.controller.signal,
|
|
92
|
+
stopHookActive,
|
|
93
|
+
};
|
|
94
|
+
for (const hook of ctx.stopHooks) {
|
|
95
|
+
let decision;
|
|
96
|
+
try {
|
|
97
|
+
decision = await hook.decide(context);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new StopHookError(`Stop hook "${hook.name}" threw`, { cause: error });
|
|
101
|
+
}
|
|
102
|
+
if (decision === null || typeof decision !== "object") {
|
|
103
|
+
throw new StopHookError(`Stop hook "${hook.name}" must return a StopHookDecision`);
|
|
104
|
+
}
|
|
105
|
+
const action = decision.action;
|
|
106
|
+
if (action === "stop")
|
|
107
|
+
continue;
|
|
108
|
+
if (action !== "continue") {
|
|
109
|
+
throw new StopHookError(`Stop hook "${hook.name}" decision action must be "stop" or "continue"`);
|
|
110
|
+
}
|
|
111
|
+
const reason = decision.reason;
|
|
112
|
+
if (typeof reason !== "string" || reason.length === 0) {
|
|
113
|
+
throw new StopHookError(`Stop hook "${hook.name}" continue decision requires a non-empty reason string`);
|
|
114
|
+
}
|
|
115
|
+
const steer = decision.steer;
|
|
116
|
+
if (!isStopHookSteer(steer)) {
|
|
117
|
+
throw new StopHookError(`Stop hook "${hook.name}" steer must be a string or Message`);
|
|
118
|
+
}
|
|
119
|
+
return steer === undefined ? { reason } : { reason, steer };
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
function isStopHookSteer(value) {
|
|
124
|
+
if (value === undefined || typeof value === "string")
|
|
125
|
+
return true;
|
|
126
|
+
if (typeof value !== "object" || value === null)
|
|
127
|
+
return false;
|
|
128
|
+
const message = value;
|
|
129
|
+
return typeof message.role === "string" && Array.isArray(message.content);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Queue a continuation through the host steer path (plan 106 R1): same redaction, same 8-message /
|
|
133
|
+
* 64 KiB caps, and the same input-guardrail re-check when the loop drains it. A queue failure fails
|
|
134
|
+
* the run closed — the hook asked for something the run cannot deliver.
|
|
135
|
+
*/
|
|
136
|
+
function queueStopHookContinuation(ctx, decision) {
|
|
137
|
+
const messages = [{ role: "user", content: [{ type: "text", text: decision.reason }] }];
|
|
138
|
+
if (decision.steer !== undefined) {
|
|
139
|
+
messages.push(typeof decision.steer === "string" ? { role: "user", content: [{ type: "text", text: decision.steer }] } : decision.steer);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
ctx.session.steer(messages);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
throw new StopHookError("Stop hook continuation could not be queued", { cause: error });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The generate-validate-revise loop promises a validated artifact; a bare return is a failure. */
|
|
149
|
+
function assertArtifactOutcome(ctx) {
|
|
150
|
+
if (ctx.loop.name === "generate-validate-revise" && !ctx.artifactFinished) {
|
|
151
|
+
throw Object.assign(new Error(ctx.artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
|
|
152
|
+
name: "ArtifactFailed",
|
|
153
|
+
code: ctx.artifactFailedInfo?.code ?? "artifact_failed",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
59
157
|
/** Validate `RunOptions.turnPolicy` once, before any provider turn (plan 084 Task 2). */
|
|
60
158
|
function assertTurnPolicy(policy, resolvedLimits) {
|
|
61
159
|
if (policy === undefined)
|
|
@@ -146,7 +244,7 @@ function assertPromptVersionRef(ref) {
|
|
|
146
244
|
return ref;
|
|
147
245
|
}
|
|
148
246
|
async function assembleRoundContext(params) {
|
|
149
|
-
const { session, input, options, runId, resumed, controller, model, startedAt, promptVersion, metadata, limits, runUsage } = params;
|
|
247
|
+
const { session, input, options, runId, resumed, controller, model, startedAt, promptVersion, metadata, limits, runUsage, stopHooks } = params;
|
|
150
248
|
session.resolveRunProvider(options);
|
|
151
249
|
throwIfAborted(controller.signal);
|
|
152
250
|
session.emit({ type: "agent_started", sessionId: session.id, runId });
|
|
@@ -158,6 +256,9 @@ async function assembleRoundContext(params) {
|
|
|
158
256
|
version: resumed.version,
|
|
159
257
|
...(resumed.restore ? { restore: resumed.restore } : {}),
|
|
160
258
|
});
|
|
259
|
+
// Plan 106 R2: first run start of this session opens it. Awaited after the two emits above so the
|
|
260
|
+
// run's synchronous announce burst stays intact; middleware error policy owns failures.
|
|
261
|
+
await session.openSession(runId);
|
|
161
262
|
const startRecord = {
|
|
162
263
|
id: runId,
|
|
163
264
|
sessionId: session.id,
|
|
@@ -304,6 +405,7 @@ async function assembleRoundContext(params) {
|
|
|
304
405
|
artifactFailedInfo: undefined,
|
|
305
406
|
toolCalls: 0,
|
|
306
407
|
toolResults: [],
|
|
408
|
+
stopHooks,
|
|
307
409
|
runUsage,
|
|
308
410
|
loopCtx: undefined,
|
|
309
411
|
};
|
|
@@ -373,6 +475,7 @@ async function assembleRoundContext(params) {
|
|
|
373
475
|
toolsSearch: session.agent.config.toolsSearch,
|
|
374
476
|
activatedTools: session.activatedTools,
|
|
375
477
|
toolResultFold: resolveToolResultFold(options.toolResultFold, session.agent.config.toolResultFold),
|
|
478
|
+
contextBudget: session.agent.config.contextBudget,
|
|
376
479
|
attentionCompiler,
|
|
377
480
|
// Session-owned: a stub made earlier stays applied even on a later under-ratio turn, so
|
|
378
481
|
// the prompt-cache prefix is not rewritten (C10). Undefined when the compiler is off.
|
|
@@ -469,6 +572,50 @@ async function assembleRoundContext(params) {
|
|
|
469
572
|
ctx.loopCtx = loopCtx;
|
|
470
573
|
return ctx;
|
|
471
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Run the loop to settlement, then apply stop hooks at the natural loop end (plan 106 R1). Each
|
|
577
|
+
* `continue` queues its reason through the steer path and re-enters the loop with a continuation
|
|
578
|
+
* context whose `input`/`inputMessages` are empty — the continuation message is already in
|
|
579
|
+
* `history`, and replaying run-start input would duplicate it. A loop ceiling, a host turn-policy
|
|
580
|
+
* stop, or an artifact failure is not a natural end: hooks never run there, and a continuation leg
|
|
581
|
+
* that hits a ceiling ends the run instead of asking again. `limits.maxStopContinuations`
|
|
582
|
+
* (default 3; `0` observes only; `null` uncapped) bounds continuations as a clean `hook_limit` stop.
|
|
583
|
+
*/
|
|
584
|
+
async function runLoopWithStopHooks(ctx) {
|
|
585
|
+
let usage = await runLoopUntilSettled(ctx);
|
|
586
|
+
assertArtifactOutcome(ctx);
|
|
587
|
+
// Zero overhead when nothing is configured: no wrapper state, no reads.
|
|
588
|
+
if (ctx.stopHooks.length === 0)
|
|
589
|
+
return usage;
|
|
590
|
+
const cap = ctx.limits.limits.maxStopContinuations;
|
|
591
|
+
let continuations = 0;
|
|
592
|
+
let stopHookActive = false;
|
|
593
|
+
for (;;) {
|
|
594
|
+
if (ctx.runStop !== undefined || ctx.loopCtx.finishReason !== undefined)
|
|
595
|
+
return usage;
|
|
596
|
+
const decision = await evaluateStopHooks(ctx, stopHookActive);
|
|
597
|
+
if (!decision)
|
|
598
|
+
return usage;
|
|
599
|
+
if (cap !== null && continuations >= cap) {
|
|
600
|
+
ctx.loopCtx.finishReason = "hook_limit";
|
|
601
|
+
return usage;
|
|
602
|
+
}
|
|
603
|
+
continuations += 1;
|
|
604
|
+
stopHookActive = true;
|
|
605
|
+
queueStopHookContinuation(ctx, decision);
|
|
606
|
+
const continuationCtx = { ...ctx.loopCtx, input: [], inputMessages: [], continuation: true };
|
|
607
|
+
try {
|
|
608
|
+
usage = await runLoopUntilSettled({ ...ctx, loopCtx: continuationCtx });
|
|
609
|
+
}
|
|
610
|
+
finally {
|
|
611
|
+
// The loops set `finishReason` on the context they receive; carry it back so the ceiling
|
|
612
|
+
// survives onto the result, the finish record, and `persistSucceeded`.
|
|
613
|
+
if (continuationCtx.finishReason !== undefined)
|
|
614
|
+
ctx.loopCtx.finishReason = continuationCtx.finishReason;
|
|
615
|
+
}
|
|
616
|
+
assertArtifactOutcome(ctx);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
472
619
|
export async function executeRun(session, input, options, runId, resumed) {
|
|
473
620
|
const legacyMaxToolRounds = options.maxToolRounds;
|
|
474
621
|
if (legacyMaxToolRounds !== undefined) {
|
|
@@ -486,6 +633,8 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
486
633
|
const requestedLimits = options.limits;
|
|
487
634
|
const resolvedLimits = resolveRunLimits(session.agent.config.limits, requestedLimits);
|
|
488
635
|
assertTurnPolicy(options.turnPolicy, resolvedLimits);
|
|
636
|
+
const stopHooks = [...(session.agent.config.stopHooks ?? []), ...(options.stopHooks ?? [])];
|
|
637
|
+
assertStopHooks(stopHooks);
|
|
489
638
|
const durableOptions = options.runState ?? session.agent.config.runState;
|
|
490
639
|
if (session.agent.config.runState && options.runState && session.agent.config.runState !== options.runState) {
|
|
491
640
|
throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
|
|
@@ -519,8 +668,11 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
519
668
|
if (session.activeIdentity && !session.activeOwnership)
|
|
520
669
|
session.activeOwnership = ownershipFromIdentity(session.activeIdentity);
|
|
521
670
|
session.activeIdempotencyKey = options.idempotencyKey ?? session.agent.config.idempotencyKey;
|
|
522
|
-
session.activeGuardrails = mergeGuardrails(mergeGuardrails(session.agent.config.guardrails, session.packGuardrails), options.guardrails);
|
|
523
671
|
session.activeDurable = resumed ?? (durableOptions ? { options: durableOptions, version: 0 } : undefined);
|
|
672
|
+
// Plan 104 T3: an `ask` rule is gated at charge time when the run can suspend; a run that cannot
|
|
673
|
+
// suspend enforces the same rule as a plain block, so it joins the ordinary stage guardrails.
|
|
674
|
+
const packGuardrails = session.activeDurable ? session.packGuardrails : mergeGuardrails(session.packGuardrails, session.packAskBlocks);
|
|
675
|
+
session.activeGuardrails = mergeGuardrails(mergeGuardrails(session.agent.config.guardrails, packGuardrails), options.guardrails);
|
|
524
676
|
// Plan 086 T3: reset here, so a suspension before the compiler is resolved (input guardrail)
|
|
525
677
|
// cannot inherit the previous run's durable-folding flag. `assembleRoundContext` sets it true.
|
|
526
678
|
session.attentionDurable = false;
|
|
@@ -570,6 +722,7 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
570
722
|
metadata,
|
|
571
723
|
limits,
|
|
572
724
|
runUsage,
|
|
725
|
+
stopHooks,
|
|
573
726
|
});
|
|
574
727
|
await replayDurableNestedAndPending(ctx);
|
|
575
728
|
const resumedLoopState = resumed?.state?.loopState;
|
|
@@ -579,7 +732,7 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
579
732
|
}
|
|
580
733
|
ctx.loop.restore?.(resumedLoopState.snapshot);
|
|
581
734
|
}
|
|
582
|
-
const loopUsage = await
|
|
735
|
+
const loopUsage = await runLoopWithStopHooks(ctx).catch((error) => {
|
|
583
736
|
// Host turn-policy stop (plan 084 Task 2): the loop was unwound on purpose at a turn
|
|
584
737
|
// boundary. Not an error — the run settles cleanly and stays resumable.
|
|
585
738
|
if (error instanceof AgentRunStopped)
|
|
@@ -588,12 +741,6 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
588
741
|
});
|
|
589
742
|
stopReason = ctx.runStop?.reason ?? ctx.loopCtx.finishReason;
|
|
590
743
|
stopDetail = ctx.runStop?.detail;
|
|
591
|
-
if (!ctx.runStop && ctx.loop.name === "generate-validate-revise" && !ctx.artifactFinished) {
|
|
592
|
-
throw Object.assign(new Error(ctx.artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
|
|
593
|
-
name: "ArtifactFailed",
|
|
594
|
-
code: ctx.artifactFailedInfo?.code ?? "artifact_failed",
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
744
|
usage = runUsage.value() ?? loopUsage;
|
|
598
745
|
return await persistSucceeded(ctx, loopUsage);
|
|
599
746
|
}
|
|
@@ -606,15 +753,12 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
606
753
|
}
|
|
607
754
|
runError = errorToErrorInfo(error);
|
|
608
755
|
const breach = error instanceof RunLimitError ? error.breach : limits.breach;
|
|
756
|
+
// Built once: the event and the terminal result carry the same attribution payload (plan 108 T5).
|
|
757
|
+
const exhaustion = breach ? describeBudgetExhaustion(limits, breach, session.activeRecentToolCalls ?? []) : undefined;
|
|
609
758
|
// Terminal attribution before the terminal `error`/finish records, so a subscriber that stops
|
|
610
759
|
// at the first terminal event still sees why the run died (plan 087 T2).
|
|
611
|
-
if (
|
|
612
|
-
session.emit({
|
|
613
|
-
type: "budget_exhausted",
|
|
614
|
-
sessionId: session.id,
|
|
615
|
-
runId,
|
|
616
|
-
...describeBudgetExhaustion(limits, breach, session.activeRecentToolCalls ?? []),
|
|
617
|
-
});
|
|
760
|
+
if (exhaustion) {
|
|
761
|
+
session.emit({ type: "budget_exhausted", sessionId: session.id, runId, ...exhaustion });
|
|
618
762
|
}
|
|
619
763
|
session.emit({ type: "error", sessionId: session.id, runId, error: runError });
|
|
620
764
|
runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
|
|
@@ -634,6 +778,11 @@ export async function executeRun(session, input, options, runId, resumed) {
|
|
|
634
778
|
status: runStatus,
|
|
635
779
|
usage: runUsage.value() ?? usage,
|
|
636
780
|
limit: breach,
|
|
781
|
+
attribution: exhaustion && {
|
|
782
|
+
consumed: exhaustion.consumed,
|
|
783
|
+
closestOtherAxes: exhaustion.closestOtherAxes,
|
|
784
|
+
recentToolCalls: exhaustion.recentToolCalls,
|
|
785
|
+
},
|
|
637
786
|
error: runError,
|
|
638
787
|
abortReason: !breach && controller.signal.aborted ? String(controller.signal.reason) : undefined,
|
|
639
788
|
runState,
|
|
@@ -16,10 +16,14 @@ export async function persistDurable(session, state) {
|
|
|
16
16
|
// did not keeps exactly today's bytes, where the frontier rides `persistSessionState`.
|
|
17
17
|
const attentionSticky = persistSessionState || session.attentionDurable ? session.serializedAttentionSticky() : undefined;
|
|
18
18
|
const attentionFold = session.attentionDurable ? session.serializedAttentionFold() : undefined;
|
|
19
|
+
// Plan 104 T2: pack refs and pack-owned state; the key only exists under the same opt-in, so a
|
|
20
|
+
// default checkpoint keeps exactly today's bytes.
|
|
21
|
+
const guardrailPacks = persistSessionState ? session.serializedGuardrailPackState() : undefined;
|
|
19
22
|
const sessionState = {
|
|
20
23
|
...(persistSessionState
|
|
21
24
|
? {
|
|
22
25
|
loadedSkillNames: session.loadedSkills.list(),
|
|
26
|
+
...(guardrailPacks ? { guardrailPacks } : {}),
|
|
23
27
|
...(session.activatedTools.list().length ? { activatedToolNames: session.activatedTools.list() } : {}),
|
|
24
28
|
...(durable.options.includeSkillBodies
|
|
25
29
|
? {
|
|
@@ -160,19 +164,21 @@ export async function persistSucceeded(ctx, loopUsage) {
|
|
|
160
164
|
await session.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, session.activeRedactor));
|
|
161
165
|
}
|
|
162
166
|
await session.drainLedger();
|
|
167
|
+
// Plan 084 Task 2 / plan 106 R1: a clean run-end stop keeps the frontier and marks the state
|
|
168
|
+
// continuable — a host turn-policy stop (`host_policy`) or a stop-hook continuation cap
|
|
169
|
+
// (`hook_limit`). Every other succeeded state drops its loop state and is final.
|
|
170
|
+
const continuableStop = stop ? "host_policy" : ctx.loopCtx.finishReason === "hook_limit" ? "hook_limit" : undefined;
|
|
163
171
|
const runState = session.activeDurable?.state
|
|
164
172
|
? await persistDurable(session, {
|
|
165
173
|
...session.activeDurable.state,
|
|
166
174
|
status: "succeeded",
|
|
167
|
-
|
|
168
|
-
// intact — the loop state is kept and the state is marked continuable.
|
|
169
|
-
...(stop ? { stopReason: "host_policy", leafId: session.currentLeafId } : {}),
|
|
175
|
+
...(continuableStop ? { stopReason: continuableStop, leafId: session.currentLeafId } : {}),
|
|
170
176
|
pending: undefined,
|
|
171
177
|
pendingCalls: undefined,
|
|
172
178
|
nestedRuns: undefined,
|
|
173
179
|
stickyDecisions: undefined,
|
|
174
180
|
interruption: undefined,
|
|
175
|
-
...(
|
|
181
|
+
...(continuableStop ? {} : { loopState: undefined }),
|
|
176
182
|
})
|
|
177
183
|
: undefined;
|
|
178
184
|
session.emit({
|
|
@@ -246,7 +252,7 @@ export async function cleanupRun(input) {
|
|
|
246
252
|
session.activeRedactor = undefined;
|
|
247
253
|
session.activeProvider = undefined;
|
|
248
254
|
cleanupSignal();
|
|
249
|
-
session.
|
|
255
|
+
session.closeRunSubscribers();
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
//# sourceMappingURL=persist.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Provider-round phase of runInternal (plan 059). */
|
|
2
2
|
import { resolveInputCap } from "../../attention-compiler.js";
|
|
3
3
|
import { cacheUsageReport } from "../../cache-helpers.js";
|
|
4
|
-
import { estimateMessageTokens } from "../../context-budget.js";
|
|
4
|
+
import { estimateMessageTokens, estimateRequestExtrasTokens, getContextBudgetReport, resolveHostTokenEstimator, } from "../../context-budget.js";
|
|
5
5
|
import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "../../guardrails.js";
|
|
6
6
|
import { validateDeterministicTurnAnswer } from "../../middleware.js";
|
|
7
7
|
import { createProviderTurnMetadata, readProviderHttpStatus } from "../../observability.js";
|
|
@@ -48,7 +48,9 @@ function turnBudgets(session, model, usage) {
|
|
|
48
48
|
const inputCap = resolveTurnInputCap(session, model);
|
|
49
49
|
const runInputBudget = tracker.limits.maxInputTokens;
|
|
50
50
|
return {
|
|
51
|
-
...(usage?.inputTokens === undefined
|
|
51
|
+
...(usage?.inputTokens === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { inputTokens: usage.inputTokens, inputTokensSource: usage.estimated === true ? "estimated" : "reported" }),
|
|
52
54
|
...(inputCap === undefined ? {} : { inputCap }),
|
|
53
55
|
...(runInputBudget === null ? {} : { runInputBudget }),
|
|
54
56
|
runInputUsed: snapshot.inputTokens,
|
|
@@ -125,21 +127,52 @@ export async function recordProviderUsage(ctx, turnUsage, turn, attempt, request
|
|
|
125
127
|
return effective;
|
|
126
128
|
}
|
|
127
129
|
/**
|
|
128
|
-
* Plan 091 T2 missing-usage fallback
|
|
129
|
-
* agent did not turn estimation off, label
|
|
130
|
-
*
|
|
131
|
-
*
|
|
130
|
+
* Plan 091 T2 missing-usage fallback, plan 103 T6 exact-measurement reuse: when the provider
|
|
131
|
+
* reported nothing and the agent did not turn estimation off, label one estimate of the turn's
|
|
132
|
+
* own request, preferring the most exact measurement that already exists —
|
|
133
|
+
* 1. the budget pass's own `ContextBudgetReport.keptTokens` (whole request, post-eviction,
|
|
134
|
+
* the same figure that decided evictions; excludes content added after the budget pass),
|
|
135
|
+
* 2. the host's `contextBudget.tokenEstimator`, projecting messages plus tool/context portions
|
|
136
|
+
* through the assembler's own `measureAll` text shapes,
|
|
137
|
+
* 3. the plan-091 family heuristic for messages plus those same assembler shapes for extras
|
|
138
|
+
* (no `JSON.stringify` of the schemas, so no drift from what the assembler measured).
|
|
139
|
+
* A host tokenizer's count is still an estimate (`confidence: "high"`, never `"reported"`);
|
|
140
|
+
* a report measured by the built-in ÷4 basis is honestly `"low"`. Returns `undefined` when
|
|
141
|
+
* estimation is not the fallback (`"off"` / `"strict"`) or the request is unavailable —
|
|
142
|
+
* absent stays absent.
|
|
132
143
|
*/
|
|
133
144
|
function estimateTurnUsage(session, model, request) {
|
|
134
|
-
|
|
145
|
+
// Omitted is the documented default (`"fallback"`), not a reason to skip estimation.
|
|
146
|
+
const mode = session.agent.config.usageEstimation ?? "fallback";
|
|
147
|
+
if (!request || mode !== "fallback")
|
|
135
148
|
return undefined;
|
|
149
|
+
const hostEstimator = resolveHostTokenEstimator(session.agent.config.contextBudget);
|
|
150
|
+
const report = getContextBudgetReport(request);
|
|
151
|
+
if (report) {
|
|
152
|
+
return { inputTokens: report.keptTokens, estimated: true, confidence: hostEstimator === undefined ? "low" : "high" };
|
|
153
|
+
}
|
|
154
|
+
if (hostEstimator) {
|
|
155
|
+
let tokens = estimateRequestExtrasTokens(request.tools, request.context, hostEstimator);
|
|
156
|
+
for (const message of request.messages)
|
|
157
|
+
tokens += estimateMessageTokens(message, hostEstimator);
|
|
158
|
+
return { inputTokens: tokens, estimated: true, confidence: "high" };
|
|
159
|
+
}
|
|
136
160
|
const estimate = estimateMessageTokens(request.messages, model.model);
|
|
137
|
-
const extras = request.tools
|
|
138
|
-
return {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
161
|
+
const extras = estimateRequestExtrasTokens(request.tools, request.context, (text) => estimateTextTokensForFamily(text, model.model));
|
|
162
|
+
return { inputTokens: estimate.tokens + extras, estimated: true, confidence: estimate.confidence };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Plan 103 T5: the refusal `usageEstimation: "strict"` gives a completed turn that reported no
|
|
166
|
+
* usage. It rides the existing observable-failure path (one attempt, terminal `error` event) and
|
|
167
|
+
* stamps no `failureClass` — a harness refusal is not a provider failure, so `name`/`code` are
|
|
168
|
+
* what a host matches on. The info carries the turn number and mode only, never request content.
|
|
169
|
+
*/
|
|
170
|
+
function usageMissingFailure(turn) {
|
|
171
|
+
return new ProviderTurnFailure({
|
|
172
|
+
name: "UsageMissingError",
|
|
173
|
+
code: "usage_missing",
|
|
174
|
+
message: `provider reported no usage on turn ${turn} and usageEstimation is "strict"`,
|
|
175
|
+
}, true);
|
|
143
176
|
}
|
|
144
177
|
/** Latest user-role text in the assembled request; steered messages included. */
|
|
145
178
|
function lastUserText(messages) {
|
|
@@ -333,6 +366,14 @@ export async function generateProviderTurn(session, request, runId, signal, secr
|
|
|
333
366
|
calls.push(call);
|
|
334
367
|
emitOutput({ type: "message_delta", sessionId: session.id, runId, content: call });
|
|
335
368
|
}
|
|
369
|
+
// Plan 103 T5: strict refuses a completed turn that reported no usage *before* the usage seam
|
|
370
|
+
// runs, so no estimate is projected, the cost catalog is not consulted, and the fail-closed
|
|
371
|
+
// `recordUsage(undefined)` maxCost breach cannot preempt the refusal. Marking the seam
|
|
372
|
+
// consulted keeps the catch below from re-entering it with the same missing usage.
|
|
373
|
+
if (usage === undefined && session.agent.config.usageEstimation === "strict") {
|
|
374
|
+
usageRecorded = true;
|
|
375
|
+
throw usageMissingFailure(turn);
|
|
376
|
+
}
|
|
336
377
|
await recordTurnUsage();
|
|
337
378
|
if (session.activeGuardrails?.output?.length) {
|
|
338
379
|
assertGuardrailsAllowed(await runGuardrails({
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** Tool-round phase of runInternal (plan 059). */
|
|
2
|
-
import type { AgentRunRef, LoopContext, NestedRunRef, PendingDecision, ResumeNestedRun, StickyDecision, ToolCallContent, ToolRegistry, ToolResult, Usage } from "../../contracts.js";
|
|
2
|
+
import type { AgentRunRef, GuardrailRecord, LoopContext, NestedRunRef, PendingDecision, ResumeNestedRun, StickyDecision, ToolCallContent, ToolRegistry, ToolResult, Usage } from "../../contracts.js";
|
|
3
3
|
import { AgentDelegationSuspendedError } from "../../contracts.js";
|
|
4
4
|
import type { RoundContext, SessionHost } from "./types.js";
|
|
5
5
|
export declare function matchNestedSticky(session: SessionHost, decision: PendingDecision): StickyDecision | undefined;
|
|
6
6
|
export declare function matchStickyDecision(session: SessionHost, call: ToolCallContent, registry: ToolRegistry): StickyDecision | undefined;
|
|
7
|
-
export declare function buildPendingDecision(session: SessionHost, call: ToolCallContent, approvalId: string, registry: ToolRegistry, runId: string, metadata: Readonly<Record<string, unknown>>, signal: AbortSignal): PendingDecision;
|
|
7
|
+
export declare function buildPendingDecision(session: SessionHost, call: ToolCallContent, approvalId: string, registry: ToolRegistry, runId: string, metadata: Readonly<Record<string, unknown>>, signal: AbortSignal, ask?: GuardrailRecord): PendingDecision;
|
|
8
8
|
export declare function applyNestedRun(session: SessionHost, input: {
|
|
9
9
|
ref: AgentRunRef;
|
|
10
10
|
toolCall: ToolCallContent;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { AgentRunSuspended, decisionIdentityRef, decisionScopesEqual, nestedApprovalId, nestedOutcomeToolResult, pathsEqual, } from "../../agent-approval.js";
|
|
3
3
|
import { toolElicitationRequest } from "../../agent-tool-dispatch.js";
|
|
4
4
|
import { AgentDecisionError, AgentDelegationSuspendedError, AgentRunStateError, DEFAULT_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_DECISIONS, MAX_ATTRIBUTION_DEPTH, } from "../../contracts.js";
|
|
5
|
+
import { runGuardrails } from "../../guardrails.js";
|
|
5
6
|
import { toToolResultMessage } from "../../input.js";
|
|
6
7
|
import { canonicalToolEffectJson, toolEffectArgumentsHash } from "../../tool-effects.js";
|
|
7
8
|
import { dispatchToolCall, resolveToolEffectDeclaration } from "../../tools.js";
|
|
@@ -55,7 +56,7 @@ export function matchStickyDecision(session, call, registry) {
|
|
|
55
56
|
return true;
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
|
-
export function buildPendingDecision(session, call, approvalId, registry, runId, metadata, signal) {
|
|
59
|
+
export function buildPendingDecision(session, call, approvalId, registry, runId, metadata, signal, ask) {
|
|
59
60
|
const tool = registry.get(call.name);
|
|
60
61
|
const declaration = tool?.effect
|
|
61
62
|
? resolveToolEffectDeclaration(tool, call.arguments, {
|
|
@@ -74,6 +75,11 @@ export function buildPendingDecision(session, call, approvalId, registry, runId,
|
|
|
74
75
|
signal,
|
|
75
76
|
metadata,
|
|
76
77
|
});
|
|
78
|
+
// Plan 104 T3: the pack `ask` rule that gated this call is named in the bounded reason and carried
|
|
79
|
+
// machine-readably, so a host never parses the name to know which rule raised the approval.
|
|
80
|
+
const pack = ask?.metadata?.pack;
|
|
81
|
+
const rule = ask?.metadata?.rule;
|
|
82
|
+
const guardrailRule = typeof pack === "string" && typeof rule === "string" ? { pack, rule } : undefined;
|
|
77
83
|
return {
|
|
78
84
|
approvalId,
|
|
79
85
|
kind: elicitation ? "elicitation" : "tool_approval",
|
|
@@ -84,10 +90,22 @@ export function buildPendingDecision(session, call, approvalId, registry, runId,
|
|
|
84
90
|
...(declaration && declaration.kind !== "none" ? { effectKind: declaration.kind } : {}),
|
|
85
91
|
...(identityRef ? { identity: identityRef } : {}),
|
|
86
92
|
},
|
|
87
|
-
reason: elicitation?.reason ?? "Tool side effect requires approval",
|
|
93
|
+
reason: elicitation?.reason ?? (ask ? askDecisionReason(ask) : "Tool side effect requires approval"),
|
|
88
94
|
...(elicitation ? { elicitationSchema: elicitation.schema } : {}),
|
|
95
|
+
...(ask && guardrailRule ? { guardrail: ask.guardrail, guardrailRule } : {}),
|
|
89
96
|
};
|
|
90
97
|
}
|
|
98
|
+
const MAX_ASK_DECISION_REASON_BYTES = 200;
|
|
99
|
+
/** `pack:<pack>/<rule>` plus the pack's own reason, bounded like every other decision field. */
|
|
100
|
+
function askDecisionReason(ask) {
|
|
101
|
+
const pack = ask.metadata?.pack;
|
|
102
|
+
const rule = ask.metadata?.rule;
|
|
103
|
+
const defaultReason = typeof pack === "string" && typeof rule === "string" ? `guardrail pack rule ${pack}/${rule}` : undefined;
|
|
104
|
+
const line = `Approval required by guardrail rule ${ask.guardrail}`;
|
|
105
|
+
const text = ask.reason && ask.reason !== defaultReason ? `${line}: ${ask.reason}` : line;
|
|
106
|
+
const bytes = new TextEncoder().encode(text);
|
|
107
|
+
return bytes.length <= MAX_ASK_DECISION_REASON_BYTES ? text : new TextDecoder().decode(bytes.subarray(0, MAX_ASK_DECISION_REASON_BYTES));
|
|
108
|
+
}
|
|
91
109
|
export async function applyNestedRun(session, input) {
|
|
92
110
|
let current = input.pending;
|
|
93
111
|
for (let depth = 0;; depth += 1) {
|
|
@@ -130,6 +148,16 @@ export async function applyNestedRun(session, input) {
|
|
|
130
148
|
return { toolResult: nestedOutcomeToolResult(outcome, input.toolCall.id, input.toolCall.name) };
|
|
131
149
|
}
|
|
132
150
|
}
|
|
151
|
+
function buildRunInterruption(input) {
|
|
152
|
+
return {
|
|
153
|
+
kind: input.kind,
|
|
154
|
+
reason: input.reason,
|
|
155
|
+
...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
|
|
156
|
+
...(input.toolName ? { toolName: input.toolName } : {}),
|
|
157
|
+
...(input.guardrail ? { guardrail: input.guardrail } : {}),
|
|
158
|
+
pendingDecisions: input.decisions,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
133
161
|
export async function suspendGatedRound(ctx) {
|
|
134
162
|
const gated = ctx.session.activeGatedRound;
|
|
135
163
|
if (!gated?.size)
|
|
@@ -137,13 +165,14 @@ export async function suspendGatedRound(ctx) {
|
|
|
137
165
|
const entries = [...gated.values()];
|
|
138
166
|
const decisions = entries.map((gatedCall) => gatedCall.decision);
|
|
139
167
|
const single = decisions.length === 1 ? decisions[0] : undefined;
|
|
140
|
-
const interruption = {
|
|
168
|
+
const interruption = buildRunInterruption({
|
|
169
|
+
decisions,
|
|
141
170
|
kind: single?.kind === "elicitation" ? "elicitation" : "tool_approval",
|
|
142
171
|
reason: single ? single.reason : `${decisions.length} tool side effects require approval`,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
};
|
|
172
|
+
toolCallId: single?.toolCallId,
|
|
173
|
+
toolName: single?.scope.toolName,
|
|
174
|
+
guardrail: single?.guardrail,
|
|
175
|
+
});
|
|
147
176
|
throw new AgentRunSuspended(await suspendDurable(ctx.session, {
|
|
148
177
|
runId: ctx.runId,
|
|
149
178
|
model: ctx.model,
|
|
@@ -176,13 +205,13 @@ export async function suspendNested(ctx, nested) {
|
|
|
176
205
|
throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Pending decisions exceed ${HARD_MAX_PENDING_DECISIONS} per run`);
|
|
177
206
|
}
|
|
178
207
|
const single = decisions.length === 1 ? decisions[0] : undefined;
|
|
179
|
-
const interruption = {
|
|
208
|
+
const interruption = buildRunInterruption({
|
|
209
|
+
decisions,
|
|
180
210
|
kind: single?.kind ?? "tool_approval",
|
|
181
211
|
reason: single ? single.reason : `${decisions.length} approval request(s) need a decision`,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
};
|
|
212
|
+
toolCallId: single?.toolCallId,
|
|
213
|
+
toolName: single?.scope.toolName,
|
|
214
|
+
});
|
|
186
215
|
throw new AgentRunSuspended(await suspendDurable(ctx.session, {
|
|
187
216
|
runId: ctx.runId,
|
|
188
217
|
model: ctx.model,
|
|
@@ -217,20 +246,27 @@ export async function handleNestedSignal(ctx, error) {
|
|
|
217
246
|
await suspendNested(ctx, { entry: applied.entry, toolCall: error.toolCall, pending: applied.pending });
|
|
218
247
|
}
|
|
219
248
|
export function bindChargeToolRound(ctx) {
|
|
220
|
-
return (calls) => {
|
|
249
|
+
return async (calls) => {
|
|
221
250
|
if (calls.length > 0)
|
|
222
251
|
ctx.limits.charge("maxToolRounds");
|
|
223
252
|
const durable = ctx.session.activeDurable;
|
|
224
|
-
|
|
253
|
+
// A run that cannot suspend never gates here: `activeGuardrails` already carries the pack `ask`
|
|
254
|
+
// rules as plain blocks (assemble.ts), so the ordinary stage path refuses the call.
|
|
255
|
+
if (!durable || calls.length === 0)
|
|
256
|
+
return;
|
|
257
|
+
if (!ctx.session.packAskGate && !durable.options.interruptBeforeTool)
|
|
225
258
|
return;
|
|
226
259
|
for (const call of calls) {
|
|
227
260
|
if (matchStickyDecision(ctx.session, call, ctx.registry))
|
|
228
261
|
continue;
|
|
262
|
+
const ask = await matchAskGate(ctx, call);
|
|
263
|
+
if (!ask && !durable.options.interruptBeforeTool)
|
|
264
|
+
continue;
|
|
229
265
|
const approvalId = randomId("approval");
|
|
230
266
|
ctx.session.activeGatedRound ??= new Map();
|
|
231
267
|
ctx.session.activeGatedRound.set(call.id, {
|
|
232
268
|
entry: { call, status: "ready", approvalId },
|
|
233
|
-
decision: buildPendingDecision(ctx.session, call, approvalId, ctx.registry, ctx.runId, ctx.metadata, ctx.controller.signal),
|
|
269
|
+
decision: buildPendingDecision(ctx.session, call, approvalId, ctx.registry, ctx.runId, ctx.metadata, ctx.controller.signal, ask),
|
|
234
270
|
});
|
|
235
271
|
}
|
|
236
272
|
if (ctx.session.activeGatedRound && ctx.session.activeGatedRound.size > DEFAULT_MAX_PENDING_DECISIONS) {
|
|
@@ -238,6 +274,33 @@ export function bindChargeToolRound(ctx) {
|
|
|
238
274
|
}
|
|
239
275
|
};
|
|
240
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Plan 104 T3: evaluate the pack `ask` rules for one call at charge time. The rules run through the
|
|
279
|
+
* same compiler and stage runner as every other pack rule, so matching, bounds, and redaction are
|
|
280
|
+
* shared; a match emits its `guardrail_decision` (`interrupt`: awaiting a decision) and gates the
|
|
281
|
+
* call before it can dispatch.
|
|
282
|
+
*/
|
|
283
|
+
async function matchAskGate(ctx, call) {
|
|
284
|
+
const gate = ctx.session.packAskGate;
|
|
285
|
+
if (!gate)
|
|
286
|
+
return undefined;
|
|
287
|
+
const result = await runGuardrails({
|
|
288
|
+
stage: "tool_input",
|
|
289
|
+
guardrails: gate,
|
|
290
|
+
value: call,
|
|
291
|
+
context: {
|
|
292
|
+
sessionId: ctx.session.id,
|
|
293
|
+
runId: ctx.runId,
|
|
294
|
+
toolCallId: call.id,
|
|
295
|
+
toolName: call.name,
|
|
296
|
+
metadata: ctx.metadata,
|
|
297
|
+
signal: ctx.controller.signal,
|
|
298
|
+
},
|
|
299
|
+
redactor: ctx.session.activeRedactor,
|
|
300
|
+
emit: (event) => ctx.session.emit(event),
|
|
301
|
+
});
|
|
302
|
+
return result.terminal;
|
|
303
|
+
}
|
|
241
304
|
/** Last-N dispatched tool calls kept for `budget_exhausted` attribution (plan 087 T2); the hash
|
|
242
305
|
* is the same canonical arguments hash the effect store uses, so raw args never enter events. */
|
|
243
306
|
const RECENT_TOOL_CALL_LIMIT = 10;
|
|
@@ -331,13 +394,13 @@ export function bindDispatchToolCall(ctx) {
|
|
|
331
394
|
return;
|
|
332
395
|
const approvalId = randomId("approval");
|
|
333
396
|
const decision = buildPendingDecision(ctx.session, mediatedCall, approvalId, ctx.registry, ctx.runId, ctx.metadata, ctx.controller.signal);
|
|
334
|
-
const interruption = {
|
|
397
|
+
const interruption = buildRunInterruption({
|
|
398
|
+
decisions: [decision],
|
|
335
399
|
kind: "tool_approval",
|
|
336
400
|
reason: decision.reason,
|
|
337
401
|
toolCallId: mediatedCall.id,
|
|
338
402
|
toolName: mediatedCall.name,
|
|
339
|
-
|
|
340
|
-
};
|
|
403
|
+
});
|
|
341
404
|
throw new AgentRunSuspended(await suspendDurable(ctx.session, {
|
|
342
405
|
runId: ctx.runId,
|
|
343
406
|
model: ctx.model,
|
|
@@ -424,13 +487,13 @@ export async function replayDurableNestedAndPending(ctx) {
|
|
|
424
487
|
if (remainingOwn.length > 0 || surfacedPending.length > 0) {
|
|
425
488
|
const pendingDecisions = [...remainingOwn, ...surfacedPending];
|
|
426
489
|
const single = pendingDecisions.length === 1 ? pendingDecisions[0] : undefined;
|
|
427
|
-
const interruption = {
|
|
490
|
+
const interruption = buildRunInterruption({
|
|
491
|
+
decisions: pendingDecisions,
|
|
428
492
|
kind: single?.kind ?? "tool_approval",
|
|
429
493
|
reason: `${pendingDecisions.length} approval request(s) remain`,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
};
|
|
494
|
+
toolCallId: single?.toolCallId,
|
|
495
|
+
toolName: single?.scope.toolName,
|
|
496
|
+
});
|
|
434
497
|
throw new AgentRunSuspended(await suspendDurable(session, {
|
|
435
498
|
runId: ctx.runId,
|
|
436
499
|
model: ctx.model,
|