@sema-agent/core 5.62.0 → 5.63.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 +40 -0
- package/dist/agents/subagent.d.ts +12 -2
- package/dist/agents/subagent.js +3 -2
- package/dist/core/auto-compaction.d.ts +6 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +36 -29
- package/dist/core/context-edit.js +3 -3
- package/dist/core/hooks.d.ts +8 -5
- package/dist/core/memory-engine/engine.d.ts +11 -0
- package/dist/core/memory-engine/engine.js +29 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/origin-clearance.d.ts +28 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -2
- package/dist/core/runner/prepare-config-doors.js +11 -8
- package/dist/core/runner/prepare-task.d.ts +67 -5
- package/dist/core/runner/prepare-task.js +162 -89
- package/dist/core/runner/runtask.js +184 -139
- package/dist/core/trace.d.ts +5 -4
- package/dist/core/types.d.ts +25 -17
- package/dist/engine/harness/agent-harness.js +20 -5
- package/dist/engine/harness/types.d.ts +38 -0
- package/dist/engine/loop/agent-loop.js +20 -1
- package/dist/engine/loop/types.d.ts +41 -1
- package/dist/index.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +48 -1
- package/dist/orchestration/workflow-types.js +12 -4
- package/dist/orchestration/workflow.d.ts +12 -1
- package/dist/orchestration/workflow.js +44 -19
- package/dist/prompts/default.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +3 -1
|
@@ -3030,6 +3030,105 @@ export class Runner {
|
|
|
3030
3030
|
return messages;
|
|
3031
3031
|
});
|
|
3032
3032
|
}
|
|
3033
|
+
const runForcedCompactionPass = async (lane, turnSignal) => {
|
|
3034
|
+
if (!(spec.compaction?.enabled ?? true))
|
|
3035
|
+
return false;
|
|
3036
|
+
if (compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES)
|
|
3037
|
+
return false;
|
|
3038
|
+
const passSignal = turnSignal !== undefined ? AbortSignal.any([prepared.abortController.signal, turnSignal]) : prepared.abortController.signal;
|
|
3039
|
+
try {
|
|
3040
|
+
const comp = await maybeCompact({
|
|
3041
|
+
session: prepared.session,
|
|
3042
|
+
epochDeclaredSections: prepared.epochDeclaredSections,
|
|
3043
|
+
...centerAdoptionOption(prepared),
|
|
3044
|
+
model: prepared.harness.getModel(),
|
|
3045
|
+
compactionModel: prepared.compModel,
|
|
3046
|
+
...forkContextOption(prepared, true),
|
|
3047
|
+
brain: compactionBrain,
|
|
3048
|
+
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
3049
|
+
thinking: prepared.thinking,
|
|
3050
|
+
settings: { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction },
|
|
3051
|
+
customInstructions: spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
|
|
3052
|
+
signal: passSignal,
|
|
3053
|
+
minTokens: 0,
|
|
3054
|
+
force: true,
|
|
3055
|
+
overheadTokens: prepared.promptOverheadTokens,
|
|
3056
|
+
...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
|
|
3057
|
+
onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
|
|
3058
|
+
workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
|
|
3059
|
+
...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
|
|
3060
|
+
...this.seamCCompactionOptions(prepared),
|
|
3061
|
+
...gitRestateOption(prepared),
|
|
3062
|
+
...windowSafetyOptions(prepared.harness.getModel()),
|
|
3063
|
+
...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: passSignal }),
|
|
3064
|
+
});
|
|
3065
|
+
if (comp.compacted) {
|
|
3066
|
+
compactionBreaker.failures = 0;
|
|
3067
|
+
this.recordCompactionReuse(prepared, comp);
|
|
3068
|
+
if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
|
|
3069
|
+
const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
|
|
3070
|
+
rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
|
|
3071
|
+
}
|
|
3072
|
+
queue.push({
|
|
3073
|
+
type: "compacted",
|
|
3074
|
+
trigger: "forced",
|
|
3075
|
+
tokensBefore: comp.tokensBefore ?? 0,
|
|
3076
|
+
...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
|
|
3077
|
+
...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
|
|
3078
|
+
...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
|
|
3079
|
+
...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
|
|
3080
|
+
...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
|
|
3081
|
+
...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
|
|
3082
|
+
...(comp.modelFallback ? { modelFallback: true } : {}),
|
|
3083
|
+
...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
|
|
3084
|
+
...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
|
|
3085
|
+
...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
|
|
3086
|
+
...ident(),
|
|
3087
|
+
});
|
|
3088
|
+
if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
|
|
3089
|
+
const pd = comp.phaseDurations;
|
|
3090
|
+
const pdDur = comp.durationMs;
|
|
3091
|
+
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
|
|
3092
|
+
}
|
|
3093
|
+
prepared.cacheBreakDetector?.notifyCompaction();
|
|
3094
|
+
if (rs.attach.attachState !== undefined) {
|
|
3095
|
+
rs.attach.attachState.postCompactPending = true;
|
|
3096
|
+
rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
else {
|
|
3100
|
+
if (comp.noop) {
|
|
3101
|
+
compactionBreaker.failures = 0;
|
|
3102
|
+
}
|
|
3103
|
+
else {
|
|
3104
|
+
compactionBreaker.failures += 1;
|
|
3105
|
+
}
|
|
3106
|
+
const declineReason = lane === "rejection" ? "prompt-too-long recovery pass did not land" : "guard-chain forced compaction pass did not land";
|
|
3107
|
+
if (!comp.noop) {
|
|
3108
|
+
this.deps.onError?.(new Error(declineReason), { phase: "compaction", sessionId: prepared.sessionId });
|
|
3109
|
+
}
|
|
3110
|
+
queue.push({
|
|
3111
|
+
type: "compaction_outcome",
|
|
3112
|
+
outcome: comp.noop ? "noop" : "failed",
|
|
3113
|
+
trigger: "forced",
|
|
3114
|
+
reason: declineReason,
|
|
3115
|
+
...ident(),
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
return comp.compacted === true;
|
|
3119
|
+
}
|
|
3120
|
+
catch (err) {
|
|
3121
|
+
if (turnSignal?.aborted === true && !prepared.abortController.signal.aborted) {
|
|
3122
|
+
queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: "guard-chain forced compaction cut short by a turn interrupt", ...ident() });
|
|
3123
|
+
return false;
|
|
3124
|
+
}
|
|
3125
|
+
compactionBreaker.failures += 1;
|
|
3126
|
+
this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "compaction", sessionId: prepared.sessionId });
|
|
3127
|
+
const msg = String(err instanceof Error ? err.message : err);
|
|
3128
|
+
queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: msg.length > 512 ? `${msg.slice(0, 512)}…` : msg, ...ident() });
|
|
3129
|
+
return false;
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3033
3132
|
prepared.harness.setLoopRecovery({
|
|
3034
3133
|
truncatedOutput: {},
|
|
3035
3134
|
malformedToolUse: {},
|
|
@@ -3075,85 +3174,23 @@ export class Runner {
|
|
|
3075
3174
|
return true;
|
|
3076
3175
|
}
|
|
3077
3176
|
}
|
|
3078
|
-
|
|
3079
|
-
return false;
|
|
3080
|
-
if (compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES)
|
|
3081
|
-
return false;
|
|
3082
|
-
try {
|
|
3083
|
-
const comp = await maybeCompact({
|
|
3084
|
-
session: prepared.session,
|
|
3085
|
-
epochDeclaredSections: prepared.epochDeclaredSections,
|
|
3086
|
-
...centerAdoptionOption(prepared),
|
|
3087
|
-
model: prepared.harness.getModel(),
|
|
3088
|
-
compactionModel: prepared.compModel,
|
|
3089
|
-
...forkContextOption(prepared, true),
|
|
3090
|
-
brain: compactionBrain,
|
|
3091
|
-
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
3092
|
-
thinking: prepared.thinking,
|
|
3093
|
-
settings: { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction },
|
|
3094
|
-
customInstructions: spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
|
|
3095
|
-
signal: prepared.abortController.signal,
|
|
3096
|
-
minTokens: 0,
|
|
3097
|
-
force: true,
|
|
3098
|
-
overheadTokens: prepared.promptOverheadTokens,
|
|
3099
|
-
...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
|
|
3100
|
-
onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
|
|
3101
|
-
workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
|
|
3102
|
-
...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
|
|
3103
|
-
...this.seamCCompactionOptions(prepared),
|
|
3104
|
-
...gitRestateOption(prepared),
|
|
3105
|
-
...windowSafetyOptions(prepared.harness.getModel()),
|
|
3106
|
-
...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
|
|
3107
|
-
});
|
|
3108
|
-
if (comp.compacted) {
|
|
3109
|
-
compactionBreaker.failures = 0;
|
|
3110
|
-
this.recordCompactionReuse(prepared, comp);
|
|
3111
|
-
if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
|
|
3112
|
-
const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
|
|
3113
|
-
rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
|
|
3114
|
-
}
|
|
3115
|
-
queue.push({
|
|
3116
|
-
type: "compacted",
|
|
3117
|
-
trigger: "forced",
|
|
3118
|
-
tokensBefore: comp.tokensBefore ?? 0,
|
|
3119
|
-
...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
|
|
3120
|
-
...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
|
|
3121
|
-
...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
|
|
3122
|
-
...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
|
|
3123
|
-
...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
|
|
3124
|
-
...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
|
|
3125
|
-
...(comp.modelFallback ? { modelFallback: true } : {}),
|
|
3126
|
-
...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
|
|
3127
|
-
...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
|
|
3128
|
-
...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
|
|
3129
|
-
...ident(),
|
|
3130
|
-
});
|
|
3131
|
-
if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
|
|
3132
|
-
const pd = comp.phaseDurations;
|
|
3133
|
-
const pdDur = comp.durationMs;
|
|
3134
|
-
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
|
|
3135
|
-
}
|
|
3136
|
-
prepared.cacheBreakDetector?.notifyCompaction();
|
|
3137
|
-
if (rs.attach.attachState !== undefined) {
|
|
3138
|
-
rs.attach.attachState.postCompactPending = true;
|
|
3139
|
-
rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
else {
|
|
3143
|
-
compactionBreaker.failures += 1;
|
|
3144
|
-
queue.push({ type: "compaction_outcome", outcome: comp.noop ? "noop" : "failed", trigger: "forced", reason: "prompt-too-long recovery pass did not land", ...ident() });
|
|
3145
|
-
}
|
|
3146
|
-
return comp.compacted === true;
|
|
3147
|
-
}
|
|
3148
|
-
catch (err) {
|
|
3149
|
-
compactionBreaker.failures += 1;
|
|
3150
|
-
const msg = String(err instanceof Error ? err.message : err);
|
|
3151
|
-
queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: msg.length > 512 ? `${msg.slice(0, 512)}…` : msg, ...ident() });
|
|
3152
|
-
return false;
|
|
3153
|
-
}
|
|
3177
|
+
return runForcedCompactionPass("rejection");
|
|
3154
3178
|
},
|
|
3155
3179
|
},
|
|
3156
3180
|
});
|
|
3181
|
+
prepared.microCompact.inTurnCompactionRef.current = async (turnSignal, anchoredEstimate) => {
|
|
3182
|
+
if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
|
|
3183
|
+
return false;
|
|
3184
|
+
if (turnSignal?.aborted === true)
|
|
3185
|
+
return false;
|
|
3186
|
+
if (anchoredEstimate !== undefined && rs.counters.compactionFloor > 0 && anchoredEstimate < rs.counters.compactionFloor) {
|
|
3187
|
+
const floor = rs.counters.compactionFloor;
|
|
3188
|
+
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.suppressed", version: 1, taskId: rs.telemetry.taskId, estTokens: anchoredEstimate, floor, ts: Date.now() }));
|
|
3189
|
+
queue.push({ type: "compaction_outcome", outcome: "suppressed", trigger: "forced", ...ident() });
|
|
3190
|
+
return false;
|
|
3191
|
+
}
|
|
3192
|
+
return runForcedCompactionPass("guard", turnSignal);
|
|
3193
|
+
};
|
|
3157
3194
|
const rapidRefill = createRapidRefillState();
|
|
3158
3195
|
const drainManualCompact = (outcome) => {
|
|
3159
3196
|
manualCompactRef.requested = false;
|
|
@@ -4336,62 +4373,6 @@ export class Runner {
|
|
|
4336
4373
|
}
|
|
4337
4374
|
if (suppliedMessage !== undefined) {
|
|
4338
4375
|
wakeMessage = validatePendingSteer(suppliedMessage);
|
|
4339
|
-
{
|
|
4340
|
-
const resumeScreenHooks = taskConfig.hooks ?? this.deps.hooks;
|
|
4341
|
-
const screen = resumeScreenHooks?.userPromptSubmit;
|
|
4342
|
-
if (screen !== undefined && resumeSignal?.aborted !== true) {
|
|
4343
|
-
const screenedMessage = wakeMessage;
|
|
4344
|
-
const identity = mintHookInvocationIdentity({
|
|
4345
|
-
sessionId: cp.sessionId,
|
|
4346
|
-
taskId: taskConfig.taskId ?? cp.sessionId,
|
|
4347
|
-
legKind: "resume",
|
|
4348
|
-
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
|
|
4349
|
-
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
4350
|
-
...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
|
|
4351
|
-
...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
|
|
4352
|
-
});
|
|
4353
|
-
const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
|
|
4354
|
-
try {
|
|
4355
|
-
this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
4356
|
-
}
|
|
4357
|
-
catch {
|
|
4358
|
-
}
|
|
4359
|
-
}, resumeScreenHooks);
|
|
4360
|
-
let decision;
|
|
4361
|
-
try {
|
|
4362
|
-
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
|
|
4363
|
-
identity,
|
|
4364
|
-
signal: sig,
|
|
4365
|
-
source: "resume_message",
|
|
4366
|
-
...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
|
|
4367
|
-
...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
|
|
4368
|
-
}));
|
|
4369
|
-
if (seat.expired) {
|
|
4370
|
-
throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
|
|
4371
|
-
? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
|
|
4372
|
-
: `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
|
|
4373
|
-
}
|
|
4374
|
-
decision = seat.value;
|
|
4375
|
-
}
|
|
4376
|
-
catch (hookErr) {
|
|
4377
|
-
if (hookErr instanceof CheckpointError)
|
|
4378
|
-
throw hookErr;
|
|
4379
|
-
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
4380
|
-
try {
|
|
4381
|
-
this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
4382
|
-
}
|
|
4383
|
-
catch {
|
|
4384
|
-
}
|
|
4385
|
-
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
|
|
4386
|
-
}
|
|
4387
|
-
if (decision?.block !== undefined && decision.block !== "") {
|
|
4388
|
-
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
|
|
4389
|
-
}
|
|
4390
|
-
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
|
|
4391
|
-
wakeHookContext = decision.additionalContext;
|
|
4392
|
-
}
|
|
4393
|
-
}
|
|
4394
|
-
}
|
|
4395
4376
|
}
|
|
4396
4377
|
else if (readPendingSteerQueue(cp.state).length === 0) {
|
|
4397
4378
|
throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
|
|
@@ -4698,6 +4679,7 @@ export class Runner {
|
|
|
4698
4679
|
throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
|
|
4699
4680
|
"resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
|
|
4700
4681
|
}
|
|
4682
|
+
let recheckGovernanceWindow;
|
|
4701
4683
|
{
|
|
4702
4684
|
const rowPrincipal = cp.principal || undefined;
|
|
4703
4685
|
const suppliedPrincipal = taskConfig.principal || undefined;
|
|
@@ -4709,16 +4691,79 @@ export class Runner {
|
|
|
4709
4691
|
const owesDelivery = outcomeGate !== "resource_limit" ||
|
|
4710
4692
|
readPendingSteerQueue(cp.state).length > 0 ||
|
|
4711
4693
|
(cp.state.runningBackgroundTasks?.length ?? 0) > 0;
|
|
4712
|
-
|
|
4694
|
+
const usageWindowStore = this.deps.usageWindowStore;
|
|
4695
|
+
if (owesDelivery && usageWindowStore !== undefined) {
|
|
4713
4696
|
const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
|
|
4714
4697
|
if (preCasWindows !== undefined && preCasWindows.length > 0) {
|
|
4715
4698
|
const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
|
|
4716
|
-
const
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4699
|
+
const assertWindowOpen = async () => {
|
|
4700
|
+
const wait = usageRetryAfterMs(await usageWindowStore.read(ledgerKey, preCasWindows, Date.now()), preCasWindows);
|
|
4701
|
+
if (wait !== undefined) {
|
|
4702
|
+
throw new CheckpointError("resume.usage_window_exhausted", `a deployment usage window for ledger key ${JSON.stringify(ledgerKey)} is exhausted, and this checkpoint still owes a delivery a re-mint cannot carry — ` +
|
|
4703
|
+
`refused pre-CAS (nothing consumed, nothing unpinned): the same token and the same decision are redeemable once the window frees, in ${String(wait)}ms`, { retryAfterMs: wait });
|
|
4704
|
+
}
|
|
4705
|
+
};
|
|
4706
|
+
recheckGovernanceWindow = assertWindowOpen;
|
|
4707
|
+
await assertWindowOpen();
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
if (outcomeGate === "wake" && wakeMessage !== undefined) {
|
|
4712
|
+
const resumeScreenHooks = taskConfig.hooks ?? this.deps.hooks;
|
|
4713
|
+
const screen = resumeScreenHooks?.userPromptSubmit;
|
|
4714
|
+
if (screen !== undefined && resumeSignal?.aborted !== true) {
|
|
4715
|
+
const screenedMessage = wakeMessage;
|
|
4716
|
+
const identity = mintHookInvocationIdentity({
|
|
4717
|
+
sessionId: cp.sessionId,
|
|
4718
|
+
taskId: taskConfig.taskId ?? cp.sessionId,
|
|
4719
|
+
legKind: "resume",
|
|
4720
|
+
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
|
|
4721
|
+
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
4722
|
+
...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
|
|
4723
|
+
...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
|
|
4724
|
+
});
|
|
4725
|
+
const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
|
|
4726
|
+
try {
|
|
4727
|
+
this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
4720
4728
|
}
|
|
4729
|
+
catch {
|
|
4730
|
+
}
|
|
4731
|
+
}, resumeScreenHooks);
|
|
4732
|
+
let decision;
|
|
4733
|
+
try {
|
|
4734
|
+
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
|
|
4735
|
+
identity,
|
|
4736
|
+
signal: sig,
|
|
4737
|
+
source: "resume_message",
|
|
4738
|
+
...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
|
|
4739
|
+
...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
|
|
4740
|
+
}));
|
|
4741
|
+
if (seat.expired) {
|
|
4742
|
+
throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
|
|
4743
|
+
? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
|
|
4744
|
+
: `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
|
|
4745
|
+
}
|
|
4746
|
+
decision = seat.value;
|
|
4747
|
+
}
|
|
4748
|
+
catch (hookErr) {
|
|
4749
|
+
if (hookErr instanceof CheckpointError)
|
|
4750
|
+
throw hookErr;
|
|
4751
|
+
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
4752
|
+
try {
|
|
4753
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
4754
|
+
}
|
|
4755
|
+
catch {
|
|
4756
|
+
}
|
|
4757
|
+
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
|
|
4758
|
+
}
|
|
4759
|
+
if (decision?.block !== undefined && decision.block !== "") {
|
|
4760
|
+
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
|
|
4761
|
+
}
|
|
4762
|
+
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
|
|
4763
|
+
wakeHookContext = decision.additionalContext;
|
|
4721
4764
|
}
|
|
4765
|
+
if (recheckGovernanceWindow !== undefined)
|
|
4766
|
+
await recheckGovernanceWindow();
|
|
4722
4767
|
}
|
|
4723
4768
|
}
|
|
4724
4769
|
if (plainPolicyOutcome !== undefined &&
|
|
@@ -4853,10 +4898,10 @@ export class Runner {
|
|
|
4853
4898
|
}
|
|
4854
4899
|
}
|
|
4855
4900
|
: undefined;
|
|
4856
|
-
if (
|
|
4901
|
+
if (resumeSignal?.aborted === true && consumeFlipDone) {
|
|
4857
4902
|
throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted after its row was already claimed — the checkpoint stays consumed and the caller's rollback settles the outcome-unknown terminal (reopening here would strand a pending approval no redemption path can reach)");
|
|
4858
4903
|
}
|
|
4859
|
-
if (
|
|
4904
|
+
if (resumeSignal?.aborted === true) {
|
|
4860
4905
|
let reopenOutcome = "refused";
|
|
4861
4906
|
if (onEnvRestoreFailed !== undefined) {
|
|
4862
4907
|
try {
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -734,10 +734,11 @@ export type TraceEvent = {
|
|
|
734
734
|
* occurrences were replaced with markers in one pass. `trigger` names the arm: `"frontier"`
|
|
735
735
|
* = the proactive request-build pass (anchored estimate crossed the edit budget),
|
|
736
736
|
* `"refusal"` = the MC-R rejection-recovery arm (provider said input-too-long), `"blocking"`
|
|
737
|
-
* = the slice-3
|
|
738
|
-
*
|
|
739
|
-
* `microCompact.machine: "cc"
|
|
740
|
-
* the
|
|
737
|
+
* = the slice-3 guard-chain arm A (the machine's one pre-guard shot when the frontier pass
|
|
738
|
+
* is off, `microCompact.machine: "off"`). SCOPE (X1): emitted only by the cc machine — the
|
|
739
|
+
* `"frontier"` arm requires `microCompact.machine: "cc"` (the default since the slice-3
|
|
740
|
+
* flip), the `"refusal"` arm requires `microCompact.clearOnRejection` (also default-on);
|
|
741
|
+
* the legacy OPT-OUT machine clears silently exactly as pre-374 (its clears are ledger-less
|
|
741
742
|
* and re-fire per request, so a frame there would re-count the same occurrences and break
|
|
742
743
|
* this frame's cardinality clause). Cardinality: exactly ONE frame per firing pass — a retry
|
|
743
744
|
* chain re-sending an already-cleared view emits none. A `"refusal"` frame also marks one
|
package/dist/core/types.d.ts
CHANGED
|
@@ -2658,9 +2658,12 @@ export interface TaskSpec {
|
|
|
2658
2658
|
* Scope caveat (design/141 examples 批实测): `instructions` rides the WHOLE-TURN summarization
|
|
2659
2659
|
* request only; a split-turn cut point (mid-turn prefix summarization) uses the engine-owned
|
|
2660
2660
|
* turn-prefix prompt and does not carry it (same A2 boundary as `RunnerDeps.summaryProvider`).
|
|
2661
|
-
* `withinTask: false` keeps end-of-task compaction but disables the within-task
|
|
2662
|
-
* trigger — the escape hatch for the §25 (A) control-flow change; the system
|
|
2663
|
-
* claiming mid-task summarization (§6.3 honesty).
|
|
2661
|
+
* `withinTask: false` keeps end-of-task compaction but disables the ROUTINE within-task
|
|
2662
|
+
* (turn-boundary) trigger — the escape hatch for the §25 (A) control-flow change; the system
|
|
2663
|
+
* prompt then stops claiming ROUTINE mid-task summarization (§6.3 honesty). It does NOT silence
|
|
2664
|
+
* the recovery-class forced lanes (design/374 slice 3): a prompt-too-long recovery or the guard
|
|
2665
|
+
* chain's arm B may still legitimately compact WITHIN the task — error/pressure recovery is not
|
|
2666
|
+
* a routine boundary pass, so a within-task compaction under this flag is contract-conforming.
|
|
2664
2667
|
* `attachWorkingFiles` (LONGRUN-2; **default ON since 2026-07-03** — CC 198 hard-codes its
|
|
2665
2668
|
* post-compact file restore, and LONGRUN-2 measured ≈2.3 extra read round-trips per compaction
|
|
2666
2669
|
* without it): after each compaction, re-read the task's most recently READ files (CC
|
|
@@ -6345,22 +6348,26 @@ export interface RunnerDeps {
|
|
|
6345
6348
|
*/
|
|
6346
6349
|
toolResultThresholdChars?: number;
|
|
6347
6350
|
/**
|
|
6348
|
-
* design/374 — microCompact machine-alignment knobs
|
|
6349
|
-
*
|
|
6350
|
-
*
|
|
6351
|
+
* design/374 — microCompact machine-alignment knobs. Since the slice-3 default flip BOTH knobs
|
|
6352
|
+
* default ON (`machine: "cc"`, `clearOnRejection: true`); the pre-374 behavior is the explicit
|
|
6353
|
+
* opt-out `{ machine: "legacy", clearOnRejection: false }`.
|
|
6351
6354
|
*
|
|
6352
6355
|
* - `machine`: which stale-tool-result clearing machine the request pipeline runs —
|
|
6353
|
-
* `"
|
|
6354
|
-
*
|
|
6355
|
-
*
|
|
6356
|
-
*
|
|
6357
|
-
*
|
|
6358
|
-
*
|
|
6356
|
+
* `"cc"` (default: the CC 2.1.223 rejection-leg form — keep 5, ≥20k minimum-savings gate, one
|
|
6357
|
+
* deep clear beyond the keep window, CC marker bytes), `"legacy"` (the historical keep-3 /
|
|
6358
|
+
* clear-to-budget machine, kept as the compatibility opt-out; it also keeps the pre-374
|
|
6359
|
+
* backstop order — guard trim as the ordinary second line), or `"off"` (no proactive frontier
|
|
6360
|
+
* clearing at all — the D-2 off switch; the unified machine then gets its ONE shot at the
|
|
6361
|
+
* blocking point instead, the slice-3 arm A, trace trigger `"blocking"`). The pre-flip P-form
|
|
6362
|
+
* warning on {@link import("./context-edit.js").ContextEditMachine} is resolved: the slice-3
|
|
6363
|
+
* fallback re-ordering shipped with this default (blocking-point re-run → in-turn forced
|
|
6364
|
+
* compaction behind the adopt seam → trim demoted to the disaster-only last resort), so an
|
|
6365
|
+
* under-20k refusal no longer falls straight into a message-dropping trim.
|
|
6359
6366
|
* - `clearOnRejection` (MC-R, slice 2): on a provider input-too-long rejection, run ONE cheap
|
|
6360
6367
|
* deterministic clear over the rejected projection (same cc machine, savings ≥20k or nothing)
|
|
6361
|
-
* and retry inside the turn BEFORE the forced-compaction recovery. Default
|
|
6362
|
-
*
|
|
6363
|
-
*
|
|
6368
|
+
* and retry inside the turn BEFORE the forced-compaction recovery. Default true (flipped with
|
|
6369
|
+
* the machine default in slice 3; X2's dark-landing clause is spent). Independent of
|
|
6370
|
+
* `machine` — an enabled MC-R always clears in the cc form (the rejection arm has no
|
|
6364
6371
|
* budget coordinate for the legacy incremental form to stop at). BUDGET ACCOUNTING (design/374
|
|
6365
6372
|
* §3.2.1, stated here because it is otherwise invisible to a deployment): a successful MC-R
|
|
6366
6373
|
* clear-and-retry SPENDS one attempt of the shared prompt-too-long recovery budget (default 2
|
|
@@ -6372,10 +6379,11 @@ export interface RunnerDeps {
|
|
|
6372
6379
|
* A declaration outside the closed vocabulary (a `machine` string not in the union, a
|
|
6373
6380
|
* non-boolean `clearOnRejection` — JSON/env-derived config the type cannot guard) refuses the
|
|
6374
6381
|
* whole prepare loudly (`code: "config.microcompact_invalid"`, no silent re-default): folding it
|
|
6375
|
-
* would run the
|
|
6382
|
+
* would silently run the DEFAULT machine while the deployment believes its declaration took
|
|
6383
|
+
* effect.
|
|
6376
6384
|
*/
|
|
6377
6385
|
microCompact?: {
|
|
6378
|
-
machine?: "legacy" | "cc";
|
|
6386
|
+
machine?: "off" | "legacy" | "cc";
|
|
6379
6387
|
clearOnRejection?: boolean;
|
|
6380
6388
|
};
|
|
6381
6389
|
/**
|
|
@@ -547,7 +547,7 @@ export class AgentHarness {
|
|
|
547
547
|
userInputParkRecords.delete(message);
|
|
548
548
|
engineNotePayloads.delete(message);
|
|
549
549
|
}
|
|
550
|
-
createLoopConfig(getTurnState, setTurnState) {
|
|
550
|
+
createLoopConfig(getTurnState, setTurnState, runPromptOverride) {
|
|
551
551
|
const turnState = getTurnState();
|
|
552
552
|
const self = this;
|
|
553
553
|
return {
|
|
@@ -564,9 +564,24 @@ export class AgentHarness {
|
|
|
564
564
|
: {}),
|
|
565
565
|
convertToLlm: (messages) => stripEngineMetadata(convertToLlm(messages)),
|
|
566
566
|
shouldStopAfterTurn: () => this._stopAfterTurn,
|
|
567
|
-
transformContext: async (messages) => {
|
|
568
|
-
const result = await this.emitHook({ type: "context", messages: [...messages] });
|
|
569
|
-
|
|
567
|
+
transformContext: async (messages, signal) => {
|
|
568
|
+
const result = await this.emitHook({ type: "context", messages: [...messages], ...(signal !== undefined ? { signal } : {}) });
|
|
569
|
+
if (result?.adoptSessionRebuild !== true) {
|
|
570
|
+
return result?.messages ?? messages;
|
|
571
|
+
}
|
|
572
|
+
await this.flushPendingSessionWrites();
|
|
573
|
+
const nextTurnState = await this.createTurnState();
|
|
574
|
+
setTurnState(nextTurnState);
|
|
575
|
+
const rebuiltContext = this.createContext(nextTurnState, runPromptOverride);
|
|
576
|
+
const recheck = await this.emitHook({ type: "context", messages: [...rebuiltContext.messages], ...(signal !== undefined ? { signal } : {}), recheck: true });
|
|
577
|
+
return {
|
|
578
|
+
messages: recheck?.messages ?? rebuiltContext.messages,
|
|
579
|
+
adoptedContext: {
|
|
580
|
+
messages: rebuiltContext.messages,
|
|
581
|
+
systemPrompt: rebuiltContext.systemPrompt,
|
|
582
|
+
...(rebuiltContext.systemBlocks !== undefined ? { systemBlocks: rebuiltContext.systemBlocks } : {}),
|
|
583
|
+
},
|
|
584
|
+
};
|
|
570
585
|
},
|
|
571
586
|
beforeToolCall: async ({ toolCall, args }) => {
|
|
572
587
|
const result = await this.emitHook({
|
|
@@ -780,7 +795,7 @@ export class AgentHarness {
|
|
|
780
795
|
this.runAbortController = abortController;
|
|
781
796
|
const runResultPromise = (async () => {
|
|
782
797
|
try {
|
|
783
|
-
return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
|
|
798
|
+
return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
|
|
784
799
|
}
|
|
785
800
|
catch (error) {
|
|
786
801
|
try {
|
|
@@ -919,6 +919,25 @@ export interface BeforeAgentStartEvent<TSkill extends Skill = Skill, TPromptTemp
|
|
|
919
919
|
export interface ContextEvent {
|
|
920
920
|
type: "context";
|
|
921
921
|
messages: AgentMessage[];
|
|
922
|
+
/**
|
|
923
|
+
* The TURN-scoped abort signal of the request build being transformed (present when the loop
|
|
924
|
+
* handed one — it always does in production; absent only for bare-harness callers). A handler
|
|
925
|
+
* that runs an EXPENSIVE reduction (the slice-3 arm-B in-turn compaction is the standing case)
|
|
926
|
+
* must honor it: a turn interrupt (design/373) fires this signal, and a reduction that only
|
|
927
|
+
* watches the RUN-level signal would make the interrupt wait out a long summary call and commit
|
|
928
|
+
* a session mutation the interrupted turn no longer needs (adversarial review r3).
|
|
929
|
+
*/
|
|
930
|
+
signal?: AbortSignal;
|
|
931
|
+
/**
|
|
932
|
+
* design/374 slice 3 — set on the ONE bounded re-dispatch of the adopt seam
|
|
933
|
+
* ({@link ContextResult.adoptSessionRebuild}): `messages` are the just-adopted session rebuild,
|
|
934
|
+
* re-presented so the handler's request pipeline applies to what the provider will actually
|
|
935
|
+
* receive (the "recheck" step of build→reduce→adopt→recheck). A handler must not request a
|
|
936
|
+
* second adoption on this dispatch — the harness does not honor it (see the result member's
|
|
937
|
+
* doc) — so a reduction that is still over budget takes the handler's own next arm (the trim
|
|
938
|
+
* last resort) instead of looping. Absent on every ordinary dispatch.
|
|
939
|
+
*/
|
|
940
|
+
recheck?: boolean;
|
|
922
941
|
}
|
|
923
942
|
export interface TurnBoundaryEvent {
|
|
924
943
|
/** Fires between model-request boundaries, after session flush, before the context rebuild. */
|
|
@@ -997,6 +1016,25 @@ export interface BeforeAgentStartResult {
|
|
|
997
1016
|
/** Hook result for replacing the full context message list before provider conversion. */
|
|
998
1017
|
export interface ContextResult {
|
|
999
1018
|
messages: AgentMessage[];
|
|
1019
|
+
/**
|
|
1020
|
+
* design/374 slice 3 (arm B, the adopt seam) — the handler PERSISTED a session-level reduction
|
|
1021
|
+
* during this dispatch (e.g. an in-turn forced compaction appended to the session) and asks the
|
|
1022
|
+
* harness to adopt it MID-BUILD, the same three-step form as the two existing adoption flows
|
|
1023
|
+
* (turn_boundary / prompt-too-long recovery): flush pending session writes, rebuild the turn
|
|
1024
|
+
* context from the session (`createTurnState`), adopt it (`setTurnState` + the loop-context
|
|
1025
|
+
* adoption via {@link import("../loop/types.js").TransformedContext}), then re-dispatch the
|
|
1026
|
+
* context hook ONCE on the rebuilt view (`ContextEvent.recheck`) so the request pipeline applies
|
|
1027
|
+
* to it. Result: the provider request, every subsequent hook dispatch, and the active turn state
|
|
1028
|
+
* all see the SAME reduced transcript — the three-party co-view the seam exists for. `messages`
|
|
1029
|
+
* on this arm are the handler's best unadopted view and are superseded by the rebuild.
|
|
1030
|
+
*
|
|
1031
|
+
* Honored AT MOST ONCE per request build: on the recheck dispatch this member is ignored (its
|
|
1032
|
+
* `messages` are used verbatim) — the bound that keeps a reduction that cannot get under budget
|
|
1033
|
+
* from re-entering forever. Typed on the hook RESULT deliberately (not a closure side channel):
|
|
1034
|
+
* the adoption is part of the hook's answer, and the one prior closure-ref transport in this
|
|
1035
|
+
* area (`requestLossyRef`) is a recorded bypass shape, not a precedent to grow.
|
|
1036
|
+
*/
|
|
1037
|
+
adoptSessionRebuild?: boolean;
|
|
1000
1038
|
}
|
|
1001
1039
|
/** Hook result for patching provider request options before payload construction. */
|
|
1002
1040
|
export interface BeforeProviderRequestResult {
|
|
@@ -524,7 +524,26 @@ async function settleInterruptedTurn(state, message, executor, committedResults,
|
|
|
524
524
|
async function streamAssistantResponse(context, config, signal, emit, streamFn, runtime, executor, staticReasoningCutDowngrade) {
|
|
525
525
|
let messages = context.messages;
|
|
526
526
|
if (config.transformContext) {
|
|
527
|
-
|
|
527
|
+
const transformed = await config.transformContext(messages, signal);
|
|
528
|
+
if (Array.isArray(transformed)) {
|
|
529
|
+
messages = transformed;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
const adopted = transformed.adoptedContext;
|
|
533
|
+
if (adopted !== undefined) {
|
|
534
|
+
context.messages = adopted.messages;
|
|
535
|
+
if (adopted.systemPrompt !== undefined) {
|
|
536
|
+
context.systemPrompt = adopted.systemPrompt;
|
|
537
|
+
if (adopted.systemBlocks !== undefined) {
|
|
538
|
+
context.systemBlocks = adopted.systemBlocks;
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
delete context.systemBlocks;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
messages = transformed.messages;
|
|
546
|
+
}
|
|
528
547
|
}
|
|
529
548
|
const llmMessages = await config.convertToLlm(messages);
|
|
530
549
|
const llmContext = {
|