@vellumai/assistant 0.8.8-dev.202606071935.547b6d2 → 0.8.8-dev.202606072033.0e97ff6
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/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +6 -11
- package/src/__tests__/compaction-events.test.ts +0 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +9 -12
- package/src/agent/loop.ts +52 -70
- package/src/context/window-manager.ts +25 -12
- package/src/daemon/conversation-agent-loop.ts +10 -24
- package/src/daemon/conversation-lifecycle.ts +2 -0
- package/src/daemon/conversation.ts +4 -6
- package/src/plugins/defaults/compaction/manager-store.ts +44 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +51 -71
- package/src/runtime/__tests__/agent-wake.test.ts +6 -8
- package/src/runtime/agent-wake.ts +17 -16
package/package.json
CHANGED
|
@@ -22,8 +22,8 @@ import type {
|
|
|
22
22
|
CheckpointInfo,
|
|
23
23
|
} from "../agent/loop.js";
|
|
24
24
|
import { AgentLoop, isMaxTokensStopReason } from "../agent/loop.js";
|
|
25
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
25
26
|
import type { PostCompactionHookInput } from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
26
|
-
import type { TurnContext } from "../plugins/types.js";
|
|
27
27
|
import type {
|
|
28
28
|
Message,
|
|
29
29
|
Provider,
|
|
@@ -120,21 +120,16 @@ const userMessage: Message = {
|
|
|
120
120
|
content: [{ type: "text", text: "Hello" }],
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
// A
|
|
123
|
+
// A trust snapshot plus a context-window manager whose `maybeCompact` returns a
|
|
124
124
|
// canned result, so the loop's compaction call runs without the real
|
|
125
|
-
// orchestrator machinery.
|
|
126
|
-
//
|
|
125
|
+
// orchestrator machinery. Both are supplied to the loop as dedicated run
|
|
126
|
+
// options.
|
|
127
127
|
function fakeCompaction(result: { compacted: boolean; exhausted: boolean }): {
|
|
128
|
-
|
|
128
|
+
trust: TrustContext;
|
|
129
129
|
contextWindowManager: AgentLoopRunOptions["contextWindowManager"];
|
|
130
130
|
} {
|
|
131
131
|
return {
|
|
132
|
-
|
|
133
|
-
requestId: "req-compact",
|
|
134
|
-
conversationId: "conv-compact",
|
|
135
|
-
turnIndex: 0,
|
|
136
|
-
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
137
|
-
},
|
|
132
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
138
133
|
contextWindowManager: {
|
|
139
134
|
maybeCompact: async () => result,
|
|
140
135
|
} as unknown as AgentLoopRunOptions["contextWindowManager"],
|
|
@@ -190,7 +190,6 @@ let mockCompactResult: ContextWindowResult = {
|
|
|
190
190
|
mock.module("../context/window-manager.js", () => ({
|
|
191
191
|
ContextWindowManager: class {
|
|
192
192
|
nonPersistedPrefixCount = 0;
|
|
193
|
-
summaryIsInjected = false;
|
|
194
193
|
constructor() {}
|
|
195
194
|
updateConfig() {}
|
|
196
195
|
shouldCompact() {
|
|
@@ -1019,20 +1019,17 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1019
1019
|
expect(texts).toContain("Hello");
|
|
1020
1020
|
});
|
|
1021
1021
|
|
|
1022
|
-
test("post-compaction re-injection resolves the live conversation from the
|
|
1022
|
+
test("post-compaction re-injection resolves the live conversation from the conversation id", async () => {
|
|
1023
1023
|
// GIVEN the seeded live conversation `injection-mode-conv` whose
|
|
1024
1024
|
// conversation-scoped blocks (`<active_workspace>`, `<channel_capabilities>`)
|
|
1025
1025
|
// only resolve when `applyRuntimeInjections` finds it by conversation id
|
|
1026
|
-
// AND the agent loop hands the post-compaction hook
|
|
1027
|
-
//
|
|
1026
|
+
// AND the agent loop hands the post-compaction hook the turn-identity fields
|
|
1027
|
+
// flat (`requestId`, `conversationId`, `trust`)
|
|
1028
1028
|
const { messages: result } = await postCompactReinject({
|
|
1029
1029
|
history: baseMessages,
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
turnIndex: 0,
|
|
1034
|
-
trust: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
1035
|
-
},
|
|
1030
|
+
requestId: "reinject-req",
|
|
1031
|
+
conversationId: "injection-mode-conv",
|
|
1032
|
+
trust: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
1036
1033
|
isNonInteractive: false,
|
|
1037
1034
|
mode: "full",
|
|
1038
1035
|
modelProfile: null,
|
|
@@ -1043,9 +1040,9 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1043
1040
|
.map((b) => b.text)
|
|
1044
1041
|
.join("\n");
|
|
1045
1042
|
|
|
1046
|
-
// THEN the hook
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1043
|
+
// THEN the hook forwards `conversationId` onto the flat injection options,
|
|
1044
|
+
// so the re-injection resolves the same live conversation as the turn's
|
|
1045
|
+
// initial assembly rather than the synthetic fallback.
|
|
1049
1046
|
expect(allText).toContain("<active_workspace>");
|
|
1050
1047
|
expect(allText).toContain("<channel_capabilities>");
|
|
1051
1048
|
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -16,6 +16,10 @@ import type {
|
|
|
16
16
|
} from "../context/window-manager.js";
|
|
17
17
|
import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
|
|
18
18
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
19
|
+
import {
|
|
20
|
+
FALLBACK_TURN_TRUST,
|
|
21
|
+
type TrustContext,
|
|
22
|
+
} from "../daemon/trust-context.js";
|
|
19
23
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
20
24
|
import type {
|
|
21
25
|
AssistantMessageContext,
|
|
@@ -29,7 +33,7 @@ import {
|
|
|
29
33
|
} from "../plugins/defaults/compaction/compact.js";
|
|
30
34
|
import postCompactReinject from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
31
35
|
import { runHook } from "../plugins/pipeline.js";
|
|
32
|
-
import type { CompactionCircuitEvent
|
|
36
|
+
import type { CompactionCircuitEvent } from "../plugins/types.js";
|
|
33
37
|
import { PluginExecutionError } from "../plugins/types.js";
|
|
34
38
|
import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
|
|
35
39
|
import type {
|
|
@@ -403,37 +407,6 @@ function assistantTextOf(content: ReadonlyArray<ContentBlock>): string {
|
|
|
403
407
|
return text;
|
|
404
408
|
}
|
|
405
409
|
|
|
406
|
-
/**
|
|
407
|
-
* Build a minimal {@link TurnContext} for pipeline invocations inside the
|
|
408
|
-
* agent loop. Real production call sites thread a full `TurnContext` into
|
|
409
|
-
* `AgentLoop.run()` (see the `turnContext` parameter on
|
|
410
|
-
* {@link AgentLoop.run}); this helper is the fallback used only by unit
|
|
411
|
-
* tests that construct `AgentLoop` directly without an orchestrator.
|
|
412
|
-
*
|
|
413
|
-
* When the orchestrator-supplied context is present it is used directly so the
|
|
414
|
-
* pipeline sees the real `conversationId` and trust class.
|
|
415
|
-
* In the fallback path the returned context is still useful for pipeline
|
|
416
|
-
* logging: `requestId` surfaces in every structured record, and `turnIndex`
|
|
417
|
-
* reflects the current tool-use iteration.
|
|
418
|
-
*/
|
|
419
|
-
function buildLoopTurnContext(
|
|
420
|
-
requestId: string | undefined,
|
|
421
|
-
turnIndex: number,
|
|
422
|
-
): TurnContext {
|
|
423
|
-
return {
|
|
424
|
-
requestId: requestId ?? "agent-loop",
|
|
425
|
-
// Loop-scoped pipelines do not currently carry a conversation ID; the
|
|
426
|
-
// outer orchestrator owns that dimension. Use a fixed sentinel so log
|
|
427
|
-
// consumers can filter loop-origin records out of conversation queries.
|
|
428
|
-
conversationId: "agent-loop",
|
|
429
|
-
turnIndex,
|
|
430
|
-
trust: {
|
|
431
|
-
sourceChannel: "vellum",
|
|
432
|
-
trustClass: "unknown",
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
410
|
/**
|
|
438
411
|
* User-config HTTP status codes that should never page the on-call: billing
|
|
439
412
|
* exhaustion (402), invalid credentials (401), and forbidden/plan-gated (403).
|
|
@@ -484,13 +457,15 @@ export interface AgentLoopRunOptions {
|
|
|
484
457
|
) => CheckpointDecision | Promise<CheckpointDecision>;
|
|
485
458
|
callSite?: LLMCallSite;
|
|
486
459
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
460
|
+
* Trust classification and channel identity for the turn's inbound actor,
|
|
461
|
+
* supplied by the orchestrator as the turn-start snapshot. Read only on the
|
|
462
|
+
* mid-loop in-place compaction path — to scope the compactor's image
|
|
463
|
+
* manifest (guardian-only attachments are excluded for untrusted actors) and
|
|
464
|
+
* forwarded to {@link postCompactReinject}. Absent for callers without a
|
|
465
|
+
* compaction path (agent wakes, standalone unit tests); the loop otherwise
|
|
466
|
+
* resolves its conversation identity from its own {@link conversationId}.
|
|
492
467
|
*/
|
|
493
|
-
|
|
468
|
+
trust?: TrustContext;
|
|
494
469
|
/**
|
|
495
470
|
* The conversation's {@link ContextWindowManager}, supplied by the
|
|
496
471
|
* orchestrator so mid-loop in-place compaction can run against the real
|
|
@@ -623,6 +598,15 @@ export class AgentLoop {
|
|
|
623
598
|
| null;
|
|
624
599
|
private toolExecutor: LoopToolExecutor | null;
|
|
625
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Conversation this loop drives, or an empty string for standalone test
|
|
603
|
+
* loops constructed without one. Source of truth for the `conversationId`
|
|
604
|
+
* the loop's pipeline contexts and post-compaction re-injection resolve the
|
|
605
|
+
* live conversation through, so the loop no longer needs a turn context
|
|
606
|
+
* threaded in to know its own identity.
|
|
607
|
+
*/
|
|
608
|
+
private readonly conversationId: string;
|
|
609
|
+
|
|
626
610
|
/**
|
|
627
611
|
* Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
|
|
628
612
|
* conversation, so it is the source of truth for the cross-turn failure
|
|
@@ -652,7 +636,8 @@ export class AgentLoop {
|
|
|
652
636
|
this.resolveTools = resolveTools ?? null;
|
|
653
637
|
this.resolveSystemPrompt = resolveSystemPrompt ?? null;
|
|
654
638
|
this.toolExecutor = toolExecutor ?? null;
|
|
655
|
-
this.
|
|
639
|
+
this.conversationId = conversationId ?? "";
|
|
640
|
+
this.compactionCircuit = new CompactionCircuit(this.conversationId);
|
|
656
641
|
}
|
|
657
642
|
|
|
658
643
|
/**
|
|
@@ -704,7 +689,7 @@ export class AgentLoop {
|
|
|
704
689
|
* compaction outcome into a user-visible turn failure.
|
|
705
690
|
*/
|
|
706
691
|
private async recordCompactionOutcome(
|
|
707
|
-
|
|
692
|
+
requestId: string | undefined,
|
|
708
693
|
summaryFailed: boolean,
|
|
709
694
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
710
695
|
): Promise<void> {
|
|
@@ -712,7 +697,7 @@ export class AgentLoop {
|
|
|
712
697
|
await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
|
|
713
698
|
} catch (recordError) {
|
|
714
699
|
log.error(
|
|
715
|
-
{ err: recordError, requestId
|
|
700
|
+
{ err: recordError, requestId },
|
|
716
701
|
"Recording a compaction outcome against the circuit breaker failed; suppressing to keep the agent loop alive",
|
|
717
702
|
);
|
|
718
703
|
}
|
|
@@ -728,7 +713,8 @@ export class AgentLoop {
|
|
|
728
713
|
*/
|
|
729
714
|
private async compact(
|
|
730
715
|
history: Message[],
|
|
731
|
-
|
|
716
|
+
requestId: string | undefined,
|
|
717
|
+
trust: TrustContext,
|
|
732
718
|
manager: ContextWindowManager | undefined,
|
|
733
719
|
signal: AbortSignal | undefined,
|
|
734
720
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
@@ -752,8 +738,8 @@ export class AgentLoop {
|
|
|
752
738
|
}
|
|
753
739
|
// The mid-loop budget gate is reached only when this turn decides to
|
|
754
740
|
// compact in place, so `force` past the auto-threshold check.
|
|
755
|
-
// `actorTrustClass` comes from the turn
|
|
756
|
-
// triggered compaction) so the compactor's image manifest excludes
|
|
741
|
+
// `actorTrustClass` comes from the turn's trust snapshot (the actor whose
|
|
742
|
+
// turn triggered compaction) so the compactor's image manifest excludes
|
|
757
743
|
// guardian-only attachments for untrusted actors. `overrideProfile` is the
|
|
758
744
|
// turn's resolved inference-profile override for the summary call.
|
|
759
745
|
const compactResult = await defaultCompact({
|
|
@@ -761,7 +747,7 @@ export class AgentLoop {
|
|
|
761
747
|
messages: rawHistory,
|
|
762
748
|
signal,
|
|
763
749
|
force: true,
|
|
764
|
-
actorTrustClass:
|
|
750
|
+
actorTrustClass: trust.trustClass,
|
|
765
751
|
overrideProfile,
|
|
766
752
|
});
|
|
767
753
|
// `force: true` bypasses the auto-threshold gate, but early returns
|
|
@@ -770,7 +756,7 @@ export class AgentLoop {
|
|
|
770
756
|
// actually ran.
|
|
771
757
|
if (compactResult.summaryFailed !== undefined) {
|
|
772
758
|
await this.recordCompactionOutcome(
|
|
773
|
-
|
|
759
|
+
requestId,
|
|
774
760
|
compactResult.summaryFailed,
|
|
775
761
|
onEvent,
|
|
776
762
|
);
|
|
@@ -792,7 +778,9 @@ export class AgentLoop {
|
|
|
792
778
|
// pre-compaction history otherwise.
|
|
793
779
|
const injection = await postCompactReinject({
|
|
794
780
|
history: compactResult.compacted ? compactResult.messages : rawHistory,
|
|
795
|
-
|
|
781
|
+
requestId,
|
|
782
|
+
conversationId: this.conversationId,
|
|
783
|
+
trust,
|
|
796
784
|
isNonInteractive,
|
|
797
785
|
// Mid-loop re-injection always runs at full injection volume.
|
|
798
786
|
mode: "full",
|
|
@@ -812,7 +800,7 @@ export class AgentLoop {
|
|
|
812
800
|
requestId,
|
|
813
801
|
onCheckpoint,
|
|
814
802
|
callSite,
|
|
815
|
-
|
|
803
|
+
trust,
|
|
816
804
|
overrideProfile,
|
|
817
805
|
resolveOverrideProfile,
|
|
818
806
|
resolveContextWindow,
|
|
@@ -880,13 +868,6 @@ export class AgentLoop {
|
|
|
880
868
|
let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
|
|
881
869
|
|
|
882
870
|
try {
|
|
883
|
-
// Per-turn pipeline context. Real call sites thread a full
|
|
884
|
-
// `turnContext` into `run()` and it is used directly; standalone
|
|
885
|
-
// unit-test instantiations that never plumb a context through fall
|
|
886
|
-
// back to a synthesized placeholder scoped to the tool-use iteration.
|
|
887
|
-
const turnCtx =
|
|
888
|
-
turnContext ?? buildLoopTurnContext(requestId, toolUseTurns);
|
|
889
|
-
|
|
890
871
|
// ── Pre-call budget gate ─────────────────────────────────────
|
|
891
872
|
// When overflow recovery is enabled, estimate the running context
|
|
892
873
|
// size as it approaches the preflight budget before issuing the
|
|
@@ -929,7 +910,8 @@ export class AgentLoop {
|
|
|
929
910
|
);
|
|
930
911
|
const compacted = await this.compact(
|
|
931
912
|
history,
|
|
932
|
-
|
|
913
|
+
requestId,
|
|
914
|
+
trust ?? FALLBACK_TURN_TRUST,
|
|
933
915
|
contextWindowManager,
|
|
934
916
|
signal,
|
|
935
917
|
onEvent,
|
|
@@ -1048,14 +1030,14 @@ export class AgentLoop {
|
|
|
1048
1030
|
providerConfig.callSite = callSite;
|
|
1049
1031
|
providerConfig.usageTracking = "manual";
|
|
1050
1032
|
// Per-conversation seed for deterministic `mix`-profile expansion.
|
|
1051
|
-
// Sourced from the
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
if (
|
|
1058
|
-
providerConfig.selectionSeed =
|
|
1033
|
+
// Sourced from the loop's own conversation id so every LLM call in a
|
|
1034
|
+
// conversation resolves the same mix arm (stable across turns and
|
|
1035
|
+
// retries, and across daemon restarts since the seed is the durable
|
|
1036
|
+
// conversation id). Absent for standalone `AgentLoop` instances
|
|
1037
|
+
// (unit tests constructed without a conversation id) — those fall
|
|
1038
|
+
// back to per-call random mix selection.
|
|
1039
|
+
if (this.conversationId) {
|
|
1040
|
+
providerConfig.selectionSeed = this.conversationId;
|
|
1059
1041
|
}
|
|
1060
1042
|
}
|
|
1061
1043
|
|
|
@@ -1190,8 +1172,8 @@ export class AgentLoop {
|
|
|
1190
1172
|
// request unchanged and streaming live.
|
|
1191
1173
|
try {
|
|
1192
1174
|
const preModelCtx: PreModelCallContext = {
|
|
1193
|
-
conversationId:
|
|
1194
|
-
callSite
|
|
1175
|
+
conversationId: this.conversationId,
|
|
1176
|
+
callSite,
|
|
1195
1177
|
systemPrompt: providerOptions.systemPrompt,
|
|
1196
1178
|
deferAssistantOutput: false,
|
|
1197
1179
|
logger: rlog,
|
|
@@ -1308,8 +1290,8 @@ export class AgentLoop {
|
|
|
1308
1290
|
let finalized = message;
|
|
1309
1291
|
try {
|
|
1310
1292
|
const ctx: AssistantMessageContext = {
|
|
1311
|
-
conversationId:
|
|
1312
|
-
callSite
|
|
1293
|
+
conversationId: this.conversationId,
|
|
1294
|
+
callSite,
|
|
1313
1295
|
content: structuredClone(message.content),
|
|
1314
1296
|
stopReason: response.stopReason,
|
|
1315
1297
|
logger: rlog,
|
|
@@ -1420,7 +1402,7 @@ export class AgentLoop {
|
|
|
1420
1402
|
// follow-up turn. It receives the full history and, when it asks to
|
|
1421
1403
|
// continue, appends the follow-up turn itself.
|
|
1422
1404
|
const stopCtx: StopContext = {
|
|
1423
|
-
conversationId:
|
|
1405
|
+
conversationId: this.conversationId,
|
|
1424
1406
|
messages: [...history],
|
|
1425
1407
|
responseContent: response.content,
|
|
1426
1408
|
stopReason: response.stopReason,
|
|
@@ -1615,7 +1597,7 @@ export class AgentLoop {
|
|
|
1615
1597
|
continue;
|
|
1616
1598
|
}
|
|
1617
1599
|
const postToolUseCtx: PostToolUseContext = {
|
|
1618
|
-
conversationId:
|
|
1600
|
+
conversationId: this.conversationId,
|
|
1619
1601
|
toolResponse: block as ToolResultContent,
|
|
1620
1602
|
messages: history,
|
|
1621
1603
|
maxInputTokens: contextWindowTokens,
|
|
@@ -199,8 +199,7 @@ export class ContextWindowManager {
|
|
|
199
199
|
* `compactedMessages` so `compactedPersistedMessages` only reflects DB
|
|
200
200
|
* rows. Decremented after a successful compaction.
|
|
201
201
|
*/
|
|
202
|
-
|
|
203
|
-
summaryIsInjected = false;
|
|
202
|
+
private _nonPersistedPrefixCount = 0;
|
|
204
203
|
private _resolvedSystemPrompt: string | undefined;
|
|
205
204
|
|
|
206
205
|
constructor(options: ContextWindowManagerOptions) {
|
|
@@ -216,6 +215,21 @@ export class ContextWindowManager {
|
|
|
216
215
|
this.config = config;
|
|
217
216
|
}
|
|
218
217
|
|
|
218
|
+
/** Leading non-persisted inherited-context messages the compactor preserves. */
|
|
219
|
+
get nonPersistedPrefixCount(): number {
|
|
220
|
+
return this._nonPersistedPrefixCount;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Seed the non-persisted inherited-context prefix when a forked/sub
|
|
225
|
+
* conversation inherits its parent's in-memory context. The compactor folds
|
|
226
|
+
* this prefix into the summary and {@link consumeCompactionState} decrements
|
|
227
|
+
* it as the leading messages are compacted away.
|
|
228
|
+
*/
|
|
229
|
+
seedNonPersistedPrefix(count: number): void {
|
|
230
|
+
this._nonPersistedPrefixCount = count;
|
|
231
|
+
}
|
|
232
|
+
|
|
219
233
|
private get estimationProviderName(): string {
|
|
220
234
|
return this.provider.tokenEstimationProvider ?? this.provider.name;
|
|
221
235
|
}
|
|
@@ -353,7 +367,7 @@ export class ContextWindowManager {
|
|
|
353
367
|
signal,
|
|
354
368
|
overrideProfile: options?.overrideProfile ?? null,
|
|
355
369
|
actorTrustClass: options?.actorTrustClass,
|
|
356
|
-
nonPersistedPrefixCount: this.
|
|
370
|
+
nonPersistedPrefixCount: this._nonPersistedPrefixCount,
|
|
357
371
|
};
|
|
358
372
|
|
|
359
373
|
// Retry budget for the compactor itself. Lives here (not in the
|
|
@@ -391,7 +405,7 @@ export class ContextWindowManager {
|
|
|
391
405
|
...baseArgs,
|
|
392
406
|
messages: result.messages,
|
|
393
407
|
previousEstimatedInputTokens: previousEstimate,
|
|
394
|
-
nonPersistedPrefixCount: this.
|
|
408
|
+
nonPersistedPrefixCount: this._nonPersistedPrefixCount,
|
|
395
409
|
});
|
|
396
410
|
if (!nextResult.compacted) break;
|
|
397
411
|
const nextEstimate = this.recomputePostCompactionEstimate(
|
|
@@ -435,21 +449,20 @@ export class ContextWindowManager {
|
|
|
435
449
|
}
|
|
436
450
|
|
|
437
451
|
/**
|
|
438
|
-
* Decrement the non-persisted prefix bookkeeping
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
452
|
+
* Decrement the non-persisted prefix bookkeeping after a productive
|
|
453
|
+
* compaction. Called once per successful internal attempt so multi-attempt
|
|
454
|
+
* runs keep the count honest as the leading injected messages get folded
|
|
455
|
+
* into the summary.
|
|
442
456
|
*/
|
|
443
457
|
private consumeCompactionState(compactedMessages: number): void {
|
|
444
458
|
const compactedAway = Math.min(
|
|
445
|
-
this.
|
|
459
|
+
this._nonPersistedPrefixCount,
|
|
446
460
|
compactedMessages,
|
|
447
461
|
);
|
|
448
|
-
this.
|
|
462
|
+
this._nonPersistedPrefixCount = Math.max(
|
|
449
463
|
0,
|
|
450
|
-
this.
|
|
464
|
+
this._nonPersistedPrefixCount - compactedAway,
|
|
451
465
|
);
|
|
452
|
-
this.summaryIsInjected = false;
|
|
453
466
|
}
|
|
454
467
|
}
|
|
455
468
|
|
|
@@ -83,7 +83,6 @@ import userPromptSubmitMemoryRetrieval, {
|
|
|
83
83
|
type MemoryRetrievalHookContext,
|
|
84
84
|
} from "../plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.js";
|
|
85
85
|
import { runHook } from "../plugins/pipeline.js";
|
|
86
|
-
import type { TurnContext as PluginTurnContext } from "../plugins/types.js";
|
|
87
86
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
88
87
|
import type { Provider } from "../providers/types.js";
|
|
89
88
|
import {
|
|
@@ -185,11 +184,6 @@ function formatDiskPressureBlockedMessage(): string {
|
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
// ── Plugin pipeline helpers ──────────────────────────────────────────
|
|
188
|
-
//
|
|
189
|
-
// Canonical {@link PluginTurnContext} builder threaded into every
|
|
190
|
-
// `runHook` call inside `runAgentLoopImpl`. The orchestrator composes
|
|
191
|
-
// the context on demand at each call site from ambient state rather than
|
|
192
|
-
// carrying a persistent `TurnContext` instance across the turn.
|
|
193
187
|
|
|
194
188
|
/**
|
|
195
189
|
* Synthetic fallback trust context used when the orchestrator fires a hook
|
|
@@ -314,8 +308,8 @@ export async function runAgentLoopImpl(
|
|
|
314
308
|
// `resolveCallSiteConfig`, picking up any user overrides under
|
|
315
309
|
// `llm.callSites.mainAgent` (falling back to `llm.default` when absent).
|
|
316
310
|
const turnCallSite: LLMCallSite = options?.callSite ?? "mainAgent";
|
|
317
|
-
// Expose the turn's call site
|
|
318
|
-
//
|
|
311
|
+
// Expose the turn's call site on the live conversation so the runtime
|
|
312
|
+
// injection assembly self-resolves it for the turn's plugin contexts.
|
|
319
313
|
ctx.currentCallSite = turnCallSite;
|
|
320
314
|
|
|
321
315
|
// Optional per-turn inference-profile override. Plumbed through to every
|
|
@@ -1228,22 +1222,14 @@ export async function runAgentLoopImpl(
|
|
|
1228
1222
|
|
|
1229
1223
|
rlog.info({ callSite: turnCallSite }, "Starting agent loop run");
|
|
1230
1224
|
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
// the synthesized `"agent-loop"` placeholder. The loop clones this value
|
|
1235
|
-
// and overwrites `turnIndex` with its own tool-use iteration counter.
|
|
1236
|
-
// Trust prefers the per-turn snapshot, then the conversation-level
|
|
1225
|
+
// Trust snapshot the loop forwards to its mid-loop in-place compaction
|
|
1226
|
+
// (scoping the compactor's image manifest) and the post-compaction
|
|
1227
|
+
// re-injection. Prefers the per-turn snapshot, then the conversation-level
|
|
1237
1228
|
// context, then the fallback — matching the trust the runtime injection
|
|
1238
|
-
// assembly resolves for the same turn.
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
turnIndex: ctx.turnCount,
|
|
1243
|
-
trust:
|
|
1244
|
-
ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST,
|
|
1245
|
-
callSite: ctx.currentCallSite,
|
|
1246
|
-
};
|
|
1229
|
+
// assembly resolves for the same turn. The loop's other turn-identity
|
|
1230
|
+
// fields self-resolve from its own conversation id.
|
|
1231
|
+
const loopTrust =
|
|
1232
|
+
ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
|
|
1247
1233
|
|
|
1248
1234
|
/**
|
|
1249
1235
|
* Shared closure: runs the agent loop with the orchestrator's turn
|
|
@@ -1263,7 +1249,7 @@ export async function runAgentLoopImpl(
|
|
|
1263
1249
|
requestId: reqId,
|
|
1264
1250
|
onCheckpoint,
|
|
1265
1251
|
callSite: turnCallSite,
|
|
1266
|
-
|
|
1252
|
+
trust: loopTrust,
|
|
1267
1253
|
contextWindowManager: ctx.contextWindowManager,
|
|
1268
1254
|
overrideProfile: turnOverrideProfile,
|
|
1269
1255
|
resolveOverrideProfile: resolveCurrentOverrideProfile,
|
|
@@ -14,6 +14,7 @@ import { enqueueMemoryJob, isMemoryEnabled } from "../memory/jobs-store.js";
|
|
|
14
14
|
import { enqueueMemoryRetrospectiveIfEnabled } from "../memory/memory-retrospective-enqueue.js";
|
|
15
15
|
import type { PermissionPrompter } from "../permissions/prompter.js";
|
|
16
16
|
import type { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
17
|
+
import { disposeContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
|
|
17
18
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
18
19
|
import {
|
|
19
20
|
isUntrustedTrustClass,
|
|
@@ -241,4 +242,5 @@ export function disposeConversation(ctx: DisposeContext): void {
|
|
|
241
242
|
ctx.accumulatedSurfaceState.clear();
|
|
242
243
|
ctx.lastSurfaceAction.clear();
|
|
243
244
|
ctx.workspaceTopLevelContext = null;
|
|
245
|
+
disposeContextWindowManager(ctx.conversationId);
|
|
244
246
|
}
|
|
@@ -34,10 +34,9 @@ import { resolveCallSiteConfig } from "../config/llm-resolver.js";
|
|
|
34
34
|
import { getConfig } from "../config/loader.js";
|
|
35
35
|
import type { LLMCallSite, Speed } from "../config/schemas/llm.js";
|
|
36
36
|
import {
|
|
37
|
-
ContextWindowManager,
|
|
37
|
+
type ContextWindowManager,
|
|
38
38
|
type ContextWindowResult,
|
|
39
39
|
createContextSummaryMessage,
|
|
40
|
-
getSummaryFromContextMessage,
|
|
41
40
|
} from "../context/window-manager.js";
|
|
42
41
|
import type { CesClient } from "../credential-execution/client.js";
|
|
43
42
|
import { EventBus } from "../events/bus.js";
|
|
@@ -64,6 +63,7 @@ import { PermissionPrompter } from "../permissions/prompter.js";
|
|
|
64
63
|
import { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
65
64
|
import type { UserDecision } from "../permissions/types.js";
|
|
66
65
|
import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
|
|
66
|
+
import { createContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
|
|
67
67
|
import { repairHistory } from "../plugins/defaults/history-repair/terminal.js";
|
|
68
68
|
import { buildSystemPrompt } from "../prompts/system-prompt.js";
|
|
69
69
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
@@ -608,7 +608,7 @@ export class Conversation {
|
|
|
608
608
|
resolveTools,
|
|
609
609
|
resolveSystemPrompt: resolveSystemPromptCallback,
|
|
610
610
|
});
|
|
611
|
-
this.contextWindowManager =
|
|
611
|
+
this.contextWindowManager = createContextWindowManager({
|
|
612
612
|
provider,
|
|
613
613
|
systemPrompt: () => resolveSystemPromptCallback([]).systemPrompt,
|
|
614
614
|
config: initialContextWindowConfig,
|
|
@@ -1060,9 +1060,7 @@ export class Conversation {
|
|
|
1060
1060
|
);
|
|
1061
1061
|
}
|
|
1062
1062
|
this.messages = [...messages];
|
|
1063
|
-
this.contextWindowManager.
|
|
1064
|
-
this.contextWindowManager.summaryIsInjected =
|
|
1065
|
-
getSummaryFromContextMessage(messages[0]) != null;
|
|
1063
|
+
this.contextWindowManager.seedNonPersistedPrefix(messages.length);
|
|
1066
1064
|
}
|
|
1067
1065
|
|
|
1068
1066
|
/**
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation {@link ContextWindowManager} ownership for the default
|
|
3
|
+
* compaction module.
|
|
4
|
+
*
|
|
5
|
+
* Construction of the manager lives here rather than in the conversation so the
|
|
6
|
+
* compaction surface owns the manager's lifetime: {@link createContextWindowManager}
|
|
7
|
+
* builds the instance and records it in a per-conversation store, and
|
|
8
|
+
* {@link disposeContextWindowManager} drops it when the conversation is torn
|
|
9
|
+
* down so the store doesn't grow unbounded.
|
|
10
|
+
*
|
|
11
|
+
* This module is side-effect free: importing it only initializes an empty
|
|
12
|
+
* store and registers no plugin.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ContextWindowManager,
|
|
17
|
+
type ContextWindowManagerOptions,
|
|
18
|
+
} from "../../../context/window-manager.js";
|
|
19
|
+
|
|
20
|
+
/** Live managers keyed by conversation id. */
|
|
21
|
+
const managersByConversation = new Map<string, ContextWindowManager>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the conversation's {@link ContextWindowManager} and register it under
|
|
25
|
+
* its conversation id. Construction is owned by the compaction module; the
|
|
26
|
+
* conversation supplies the provider, prompt/tool resolvers, and initial
|
|
27
|
+
* config. Replaces any prior manager for the same conversation so each
|
|
28
|
+
* construction yields a fresh instance, matching a direct `new` call.
|
|
29
|
+
*/
|
|
30
|
+
export function createContextWindowManager(
|
|
31
|
+
options: ContextWindowManagerOptions & { conversationId: string },
|
|
32
|
+
): ContextWindowManager {
|
|
33
|
+
const manager = new ContextWindowManager(options);
|
|
34
|
+
managersByConversation.set(options.conversationId, manager);
|
|
35
|
+
return manager;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Release the manager registered for a conversation. Called from conversation
|
|
40
|
+
* teardown so the store releases the instance once the conversation is gone.
|
|
41
|
+
*/
|
|
42
|
+
export function disposeContextWindowManager(conversationId: string): void {
|
|
43
|
+
managersByConversation.delete(conversationId);
|
|
44
|
+
}
|
|
@@ -21,40 +21,62 @@
|
|
|
21
21
|
* The memory graph handle is sourced internally from the plugin's own
|
|
22
22
|
* conversation-keyed registry ({@link getLiveGraphMemory}) rather than being
|
|
23
23
|
* threaded in by the loop — it is memory-retrieval-specific state, not
|
|
24
|
-
* something the generic loop
|
|
24
|
+
* something the generic loop should carry.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
28
|
applyRuntimeInjections,
|
|
29
29
|
type InboundActorContext,
|
|
30
30
|
type InjectionMode,
|
|
31
|
-
type RuntimeInjectionOptions,
|
|
32
31
|
type RuntimeInjectionResult,
|
|
33
32
|
} from "../../../../daemon/conversation-runtime-assembly.js";
|
|
34
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
resolveTrustClass,
|
|
35
|
+
type TrustContext,
|
|
36
|
+
} from "../../../../daemon/trust-context.js";
|
|
35
37
|
import { stripHistoricalWebSearchResults } from "../../../../daemon/web-search-history.js";
|
|
36
38
|
import { getLiveGraphMemory } from "../../../../memory/graph/conversation-graph-memory.js";
|
|
37
39
|
import type { Message } from "../../../../providers/types.js";
|
|
38
40
|
import { getLogger } from "../../../../util/logger.js";
|
|
39
|
-
import type { TurnContext } from "../../../types.js";
|
|
40
41
|
|
|
41
42
|
const log = getLogger("post-compact-reinject");
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
-
* state. Re-injection inputs migrate loop-ward by growing this
|
|
46
|
-
* hands the hook an object of this shape when it calls
|
|
45
|
+
* Everything the post-compaction hook needs, supplied by the agent loop from
|
|
46
|
+
* its own working state. Re-injection inputs migrate loop-ward by growing this
|
|
47
|
+
* type; the loop hands the hook an object of this shape when it calls
|
|
47
48
|
* {@link postCompactReinject} directly.
|
|
49
|
+
*
|
|
50
|
+
* The turn-identity fields are flat here: `conversationId` is the key the
|
|
51
|
+
* re-injection resolves the live conversation through (and the only one that,
|
|
52
|
+
* together with `requestId` and `trust`, cannot be recovered from that live
|
|
53
|
+
* conversation), while `turnIndex` and `callSite` are intentionally omitted so
|
|
54
|
+
* {@link applyRuntimeInjections} self-resolves them from the live conversation.
|
|
55
|
+
* The memory graph handle is likewise sourced internally via
|
|
56
|
+
* {@link getLiveGraphMemory} rather than threaded in.
|
|
48
57
|
*/
|
|
49
58
|
export interface PostCompactionHookInput {
|
|
50
59
|
/** Compacted message history to re-inject onto. */
|
|
51
60
|
history: Message[];
|
|
52
61
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* conversation
|
|
62
|
+
* Stable ID for the current request, forwarded onto the injector turn
|
|
63
|
+
* context. The one turn-identity field that cannot be recovered from the
|
|
64
|
+
* live conversation, so the loop supplies it.
|
|
65
|
+
*/
|
|
66
|
+
requestId: string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Conversation the turn is scoped to. The key the re-injection resolves the
|
|
69
|
+
* live conversation (and its channel/trust/transcript state) through, so the
|
|
70
|
+
* loop always supplies it.
|
|
71
|
+
*/
|
|
72
|
+
conversationId: string;
|
|
73
|
+
/**
|
|
74
|
+
* Trust classification and channel identity for the inbound actor, forwarded
|
|
75
|
+
* onto the injector turn context and used to gate the memory-graph re-track.
|
|
76
|
+
* Supplied by the loop as the turn-start snapshot rather than re-resolved
|
|
77
|
+
* mid-turn.
|
|
56
78
|
*/
|
|
57
|
-
|
|
79
|
+
trust: TrustContext;
|
|
58
80
|
/**
|
|
59
81
|
* Whether the in-flight turn has no human present to answer clarification
|
|
60
82
|
* questions. Resolved once by the agent loop at turn start (from its
|
|
@@ -91,82 +113,40 @@ export interface PostCompactionHookInput {
|
|
|
91
113
|
actorContext: InboundActorContext | null;
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
/**
|
|
95
|
-
* Everything the hook needs in a single context: the loop-supplied
|
|
96
|
-
* {@link PostCompactionHookInput} plus the non-identity
|
|
97
|
-
* {@link RuntimeInjectionOptions} (spread top-level so each field stays
|
|
98
|
-
* individually addressable). The turn-identity fields (`requestId`,
|
|
99
|
-
* `conversationId`, `turnIndex`, `trust`, `callSite`) are omitted from the
|
|
100
|
-
* inherited options because the hook sources them from
|
|
101
|
-
* {@link PostCompactionHookInput.turnContext}; the memory graph handle is
|
|
102
|
-
* likewise sourced internally via {@link getLiveGraphMemory} rather than
|
|
103
|
-
* threaded in.
|
|
104
|
-
*/
|
|
105
|
-
export interface PostCompactContext
|
|
106
|
-
extends
|
|
107
|
-
Omit<
|
|
108
|
-
RuntimeInjectionOptions,
|
|
109
|
-
"requestId" | "conversationId" | "turnIndex" | "trust" | "callSite"
|
|
110
|
-
>,
|
|
111
|
-
PostCompactionHookInput {
|
|
112
|
-
/**
|
|
113
|
-
* Re-declared to reconcile the optional {@link RuntimeInjectionOptions} field
|
|
114
|
-
* with the required {@link PostCompactionHookInput} one: the hook always
|
|
115
|
-
* receives this from the loop, and it flows into {@link applyRuntimeInjections}
|
|
116
|
-
* via the spread options.
|
|
117
|
-
*/
|
|
118
|
-
isNonInteractive: boolean;
|
|
119
|
-
/**
|
|
120
|
-
* Re-declared for the same reason as {@link isNonInteractive}: required on
|
|
121
|
-
* {@link PostCompactionHookInput} but optional on {@link RuntimeInjectionOptions}.
|
|
122
|
-
*/
|
|
123
|
-
mode: InjectionMode;
|
|
124
|
-
/**
|
|
125
|
-
* Re-declared for the same reason as {@link isNonInteractive}: required on
|
|
126
|
-
* {@link PostCompactionHookInput} but optional on {@link RuntimeInjectionOptions}.
|
|
127
|
-
*/
|
|
128
|
-
modelProfile: string | null;
|
|
129
|
-
/**
|
|
130
|
-
* Re-declared for the same reason as {@link isNonInteractive}: required on
|
|
131
|
-
* {@link PostCompactionHookInput} but optional on {@link RuntimeInjectionOptions}.
|
|
132
|
-
*/
|
|
133
|
-
actorContext: InboundActorContext | null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
116
|
export default async function postCompactReinject(
|
|
137
|
-
ctx:
|
|
117
|
+
ctx: PostCompactionHookInput,
|
|
138
118
|
): Promise<RuntimeInjectionResult> {
|
|
139
|
-
const { history,
|
|
140
|
-
// The
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
119
|
+
const { history, requestId, conversationId, trust, ...options } = ctx;
|
|
120
|
+
// The remaining `options` carry the non-identity injection inputs
|
|
121
|
+
// (`isNonInteractive`, `mode`, `modelProfile`, `actorContext`). Forward the
|
|
122
|
+
// turn-identity fields the live conversation can't supply (`requestId`,
|
|
123
|
+
// `conversationId`, `trust`); `turnIndex` and `callSite` self-resolve from
|
|
124
|
+
// the live conversation so the re-injection matches the turn's initial
|
|
125
|
+
// assembly.
|
|
144
126
|
const result = await applyRuntimeInjections(history, {
|
|
145
127
|
...options,
|
|
146
|
-
requestId
|
|
147
|
-
conversationId
|
|
148
|
-
|
|
149
|
-
trust: turnContext.trust,
|
|
150
|
-
callSite: turnContext.callSite,
|
|
128
|
+
requestId,
|
|
129
|
+
conversationId,
|
|
130
|
+
trust,
|
|
151
131
|
});
|
|
152
132
|
// Re-track the nodes the memory graph last injected so they survive against
|
|
153
133
|
// the re-injected history. Untrusted actors and minimal-mode turns never
|
|
154
134
|
// received a memory-graph injection, so there is nothing to re-track. The
|
|
155
135
|
// actor's trust class is derived from the turn's own trust context (the same
|
|
156
|
-
// value the injector chain resolves)
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
const isTrustedActor = resolveTrustClass(
|
|
136
|
+
// value the injector chain resolves). The live graph handle is looked up
|
|
137
|
+
// from the plugin's own registry by the turn's conversation id — the same
|
|
138
|
+
// instance the turn's retrieval mutated, so re-tracking sees the real
|
|
139
|
+
// cached-node state.
|
|
140
|
+
const isTrustedActor = resolveTrustClass(trust) === "guardian";
|
|
161
141
|
if (isTrustedActor && options.mode !== "minimal") {
|
|
162
|
-
getLiveGraphMemory(
|
|
142
|
+
getLiveGraphMemory(conversationId)?.retrackCachedNodes();
|
|
163
143
|
}
|
|
164
144
|
const strip = stripHistoricalWebSearchResults(result.messages);
|
|
165
145
|
if (strip.stats.blocksStripped > 0) {
|
|
166
146
|
log.info(
|
|
167
147
|
{
|
|
168
148
|
phase: "mid-loop-compact",
|
|
169
|
-
conversationId
|
|
149
|
+
conversationId,
|
|
170
150
|
...strip.stats,
|
|
171
151
|
},
|
|
172
152
|
"Converted historical web_search_tool_result blocks to text summaries",
|
|
@@ -47,7 +47,7 @@ interface WakeConversationProbe {
|
|
|
47
47
|
runCalls: Array<{
|
|
48
48
|
input: Message[];
|
|
49
49
|
requestId?: string;
|
|
50
|
-
|
|
50
|
+
trust?: unknown;
|
|
51
51
|
allowedTools?: string[];
|
|
52
52
|
order: number;
|
|
53
53
|
}>;
|
|
@@ -369,7 +369,7 @@ function makeWakeConversation(options: {
|
|
|
369
369
|
probe.runCalls.push({
|
|
370
370
|
input: [...input],
|
|
371
371
|
requestId: runOptions?.requestId,
|
|
372
|
-
|
|
372
|
+
trust: runOptions?.trust,
|
|
373
373
|
allowedTools: snapshotAllowedTools(),
|
|
374
374
|
order: order++,
|
|
375
375
|
});
|
|
@@ -520,7 +520,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
520
520
|
expect(conversation.runCalls).toHaveLength(0);
|
|
521
521
|
});
|
|
522
522
|
|
|
523
|
-
test("
|
|
523
|
+
test("forwards a guardian trust snapshot for explicit local-owner cleanup-mode wakes", async () => {
|
|
524
524
|
mockDiskPressureStatus = {
|
|
525
525
|
enabled: true,
|
|
526
526
|
state: "critical",
|
|
@@ -551,11 +551,9 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
551
551
|
|
|
552
552
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
553
553
|
expect(conversation.runCalls).toHaveLength(1);
|
|
554
|
-
expect(conversation.runCalls[0]!.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
turnIndex: 0,
|
|
558
|
-
trust: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
554
|
+
expect(conversation.runCalls[0]!.trust).toEqual({
|
|
555
|
+
sourceChannel: "vellum",
|
|
556
|
+
trustClass: "guardian",
|
|
559
557
|
});
|
|
560
558
|
});
|
|
561
559
|
|
|
@@ -69,7 +69,6 @@ import {
|
|
|
69
69
|
recordRequestLog,
|
|
70
70
|
setAgentLoopExitReasonOnLatestLog,
|
|
71
71
|
} from "../memory/llm-request-log-store.js";
|
|
72
|
-
import type { TurnContext } from "../plugins/types.js";
|
|
73
72
|
import type { Message } from "../providers/types.js";
|
|
74
73
|
import { getLogger } from "../util/logger.js";
|
|
75
74
|
|
|
@@ -327,22 +326,24 @@ function classifyWakeDiskPressurePolicy(opts: WakeOptions): {
|
|
|
327
326
|
return { decision, status };
|
|
328
327
|
}
|
|
329
328
|
|
|
330
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Trust snapshot the wake hands to the agent loop. Only needed on the
|
|
331
|
+
* disk-pressure cleanup-mode path, whose guardian fallback lets the cleanup
|
|
332
|
+
* turn clear the side-effect approval gate; other wakes leave it unset (the
|
|
333
|
+
* loop has no compaction path during a wake, so trust is otherwise unread).
|
|
334
|
+
*/
|
|
335
|
+
function buildWakeTrust(
|
|
331
336
|
opts: WakeOptions,
|
|
332
337
|
decision: DiskPressureTurnPolicyDecision,
|
|
333
|
-
):
|
|
338
|
+
): TrustContext | undefined {
|
|
334
339
|
if (decision.action !== "allow-cleanup-mode") return undefined;
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
sourceChannel: opts.sourceChannel ?? "vellum",
|
|
343
|
-
trustClass: "guardian",
|
|
344
|
-
} satisfies TrustContext),
|
|
345
|
-
};
|
|
340
|
+
return (
|
|
341
|
+
opts.trustContext ??
|
|
342
|
+
({
|
|
343
|
+
sourceChannel: opts.sourceChannel ?? "vellum",
|
|
344
|
+
trustClass: "guardian",
|
|
345
|
+
} satisfies TrustContext)
|
|
346
|
+
);
|
|
346
347
|
}
|
|
347
348
|
|
|
348
349
|
/**
|
|
@@ -482,7 +483,7 @@ export async function wakeAgentForOpportunity(
|
|
|
482
483
|
// post-run would therefore include the tail we just pushed and the
|
|
483
484
|
// tail-slice math would skip every message.
|
|
484
485
|
const baselineLength = baseline.length;
|
|
485
|
-
const
|
|
486
|
+
const wakeTrust = buildWakeTrust(opts, diskPressureDecision);
|
|
486
487
|
// Build the hint injection. Three modes:
|
|
487
488
|
// - `skipHintInjection`: caller has already persisted an instruction
|
|
488
489
|
// message into the conversation history (typical for fork-based
|
|
@@ -838,7 +839,7 @@ export async function wakeAgentForOpportunity(
|
|
|
838
839
|
// short-circuit and silently drop both per-callsite config and the
|
|
839
840
|
// pinned `overrideProfile` below.
|
|
840
841
|
callSite,
|
|
841
|
-
|
|
842
|
+
trust: wakeTrust,
|
|
842
843
|
overrideProfile,
|
|
843
844
|
// Wake runs have no orchestrator-side mid-loop compaction path,
|
|
844
845
|
// so the budget gate stays disabled (`overflowRecovery.enabled =
|