@animalabs/agent-framework 0.6.6 → 0.6.7
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/README.md +78 -0
- package/dist/src/agent.d.ts +39 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +172 -56
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server.js +0 -0
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/canonical-json.d.ts +2 -0
- package/dist/src/canonical-json.d.ts.map +1 -0
- package/dist/src/canonical-json.js +23 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/framework.d.ts +147 -3
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +2719 -125
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +9 -2
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +3 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +29 -15
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +36 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +142 -28
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +12 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +661 -2
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +4 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +99 -0
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -0
- package/dist/src/recovery/discord-awareness-outbox.js +334 -0
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +45 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -0
- package/dist/src/recovery/offline-branch.js +362 -0
- package/dist/src/recovery/offline-branch.js.map +1 -0
- package/dist/src/recovery/recover-cli.d.ts +3 -0
- package/dist/src/recovery/recover-cli.d.ts.map +1 -0
- package/dist/src/recovery/recover-cli.js +129 -0
- package/dist/src/recovery/recover-cli.js.map +1 -0
- package/dist/src/types/agent.d.ts +22 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +31 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.js +200 -93
- package/dist/test/agent-runtime-settings.test.js.map +1 -1
- package/dist/test/busy-agent-scheduler.test.d.ts +2 -0
- package/dist/test/busy-agent-scheduler.test.d.ts.map +1 -0
- package/dist/test/busy-agent-scheduler.test.js +62 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts +2 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.js +47 -0
- package/dist/test/channel-lifecycle-provenance.test.js.map +1 -0
- package/dist/test/discord-awareness-outbox.test.d.ts +2 -0
- package/dist/test/discord-awareness-outbox.test.d.ts.map +1 -0
- package/dist/test/discord-awareness-outbox.test.js +248 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -0
- package/dist/test/helpers/mock-membrane.js +1 -1
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/maintenance-timer.test.js +100 -0
- package/dist/test/maintenance-timer.test.js.map +1 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +2 -0
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +845 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js +22 -3
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +15 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/offline-recovery-branch.test.d.ts +2 -0
- package/dist/test/offline-recovery-branch.test.d.ts.map +1 -0
- package/dist/test/offline-recovery-branch.test.js +226 -0
- package/dist/test/offline-recovery-branch.test.js.map +1 -0
- package/dist/test/present-while-acting.test.d.ts +3 -2
- package/dist/test/present-while-acting.test.d.ts.map +1 -1
- package/dist/test/present-while-acting.test.js +31 -3
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js +1955 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.js +753 -0
- package/dist/test/primary-summary-refusal-fallback.test.js.map +1 -0
- package/dist/test/same-round-think-policy.test.d.ts +2 -0
- package/dist/test/same-round-think-policy.test.d.ts.map +1 -0
- package/dist/test/same-round-think-policy.test.js +853 -0
- package/dist/test/same-round-think-policy.test.js.map +1 -0
- package/dist/test/workspace-read-image.test.d.ts +2 -0
- package/dist/test/workspace-read-image.test.d.ts.map +1 -0
- package/dist/test/workspace-read-image.test.js +518 -0
- package/dist/test/workspace-read-image.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -3
- package/src/agent.ts +265 -62
- package/src/api/server.ts +8 -2
- package/src/canonical-json.ts +22 -0
- package/src/framework.ts +3561 -145
- package/src/gate/event-gate.ts +15 -3
- package/src/index.ts +20 -0
- package/src/mcpl/channel-registry.ts +31 -16
- package/src/mcpl/server-connection.ts +163 -28
- package/src/modules/workspace/index.ts +759 -2
- package/src/modules/workspace/types.ts +5 -0
- package/src/recovery/discord-awareness-outbox.ts +459 -0
- package/src/recovery/offline-branch.ts +510 -0
- package/src/recovery/recover-cli.ts +130 -0
- package/src/types/agent.ts +26 -0
- package/src/types/framework.ts +17 -0
- package/src/types/module.ts +33 -0
- package/src/types/trace.ts +2 -0
|
@@ -0,0 +1,1955 @@
|
|
|
1
|
+
import { after, describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { AutobiographicalStrategy, } from '@animalabs/context-manager';
|
|
5
|
+
import { AgentFramework } from '../src/index.js';
|
|
6
|
+
import { MockYieldingStream, createMockResponse } from './helpers/mock-membrane.js';
|
|
7
|
+
const NODELESS_DATE = '2026-07-17T00:00:00.000Z';
|
|
8
|
+
const FALLBACK_STATE_ID = 'framework/primary-summary-fallback';
|
|
9
|
+
const RAW_PRIVATE_SENTINEL = 'RAW_PRIVATE_SENTINEL_2026_07_17';
|
|
10
|
+
const SUMMARY_PRIVATE_SENTINEL = 'SUMMARY_PRIVATE_SENTINEL_2026_07_17';
|
|
11
|
+
const RETRY_OUTPUT_SENTINEL = 'RETRY_OUTPUT_SENTINEL_2026_07_17';
|
|
12
|
+
const TOOL_ARG_SENTINEL = 'TOOL_ARG_SENTINEL_2026_07_17';
|
|
13
|
+
const TOOL_RESULT_SENTINEL = 'TOOL_RESULT_SENTINEL_2026_07_17';
|
|
14
|
+
const FINAL_TOOL_ARG_SENTINEL = 'FINAL_TOOL_ARG_SENTINEL_2026_07_17';
|
|
15
|
+
const NON_EXECUTED_TOOL_RESULT = '[tool not executed]';
|
|
16
|
+
const PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY = 'primarySummarySettlement';
|
|
17
|
+
const paths = [];
|
|
18
|
+
let fixtureSequence = 0;
|
|
19
|
+
function freshPath() {
|
|
20
|
+
const path = `./test-primary-summary-refusal-fallback-int-${fixtureSequence++}`;
|
|
21
|
+
paths.push(path);
|
|
22
|
+
return path;
|
|
23
|
+
}
|
|
24
|
+
after(() => {
|
|
25
|
+
for (const path of paths) {
|
|
26
|
+
if (existsSync(path))
|
|
27
|
+
rmSync(path, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
class ProbeStrategy extends AutobiographicalStrategy {
|
|
31
|
+
seed(entry) {
|
|
32
|
+
this.pushSummary(entry);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class ScriptedMembrane {
|
|
36
|
+
streams;
|
|
37
|
+
calls = [];
|
|
38
|
+
constructor(streams) {
|
|
39
|
+
this.streams = streams;
|
|
40
|
+
}
|
|
41
|
+
complete() {
|
|
42
|
+
throw new Error('complete() is not used by the primary streaming path');
|
|
43
|
+
}
|
|
44
|
+
streamYielding(request) {
|
|
45
|
+
this.calls.push(structuredClone(request));
|
|
46
|
+
const next = this.streams.shift();
|
|
47
|
+
if (!next)
|
|
48
|
+
return new MockYieldingStream([createMockResponse([{ type: 'text', text: 'default' }])]);
|
|
49
|
+
return Array.isArray(next) ? new MockYieldingStream(next) : next;
|
|
50
|
+
}
|
|
51
|
+
asMembrane() {
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class ErrorStream {
|
|
56
|
+
message;
|
|
57
|
+
isWaitingForTools = false;
|
|
58
|
+
pendingToolCallIds = [];
|
|
59
|
+
toolDepth = 0;
|
|
60
|
+
constructor(message) {
|
|
61
|
+
this.message = message;
|
|
62
|
+
}
|
|
63
|
+
provideToolResults() {
|
|
64
|
+
throw new Error('ErrorStream does not support tool rounds');
|
|
65
|
+
}
|
|
66
|
+
cancel() { }
|
|
67
|
+
async *[Symbol.asyncIterator]() {
|
|
68
|
+
yield { type: 'error', error: new Error(this.message) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
class AbortedStream {
|
|
72
|
+
reason;
|
|
73
|
+
isWaitingForTools = false;
|
|
74
|
+
pendingToolCallIds = [];
|
|
75
|
+
toolDepth = 0;
|
|
76
|
+
constructor(reason) {
|
|
77
|
+
this.reason = reason;
|
|
78
|
+
}
|
|
79
|
+
provideToolResults() {
|
|
80
|
+
throw new Error('AbortedStream does not support tool rounds');
|
|
81
|
+
}
|
|
82
|
+
cancel() { }
|
|
83
|
+
async *[Symbol.asyncIterator]() {
|
|
84
|
+
yield { type: 'aborted', reason: this.reason };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
class HookedCompleteStream {
|
|
88
|
+
response;
|
|
89
|
+
beforeYield;
|
|
90
|
+
isWaitingForTools = false;
|
|
91
|
+
pendingToolCallIds = [];
|
|
92
|
+
toolDepth = 0;
|
|
93
|
+
constructor(response, beforeYield) {
|
|
94
|
+
this.response = response;
|
|
95
|
+
this.beforeYield = beforeYield;
|
|
96
|
+
}
|
|
97
|
+
provideToolResults() {
|
|
98
|
+
throw new Error('HookedCompleteStream does not support tool rounds');
|
|
99
|
+
}
|
|
100
|
+
cancel() { }
|
|
101
|
+
async *[Symbol.asyncIterator]() {
|
|
102
|
+
await this.beforeYield?.();
|
|
103
|
+
yield { type: 'complete', response: this.response };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
class EchoModule {
|
|
107
|
+
isQuarantineVisible;
|
|
108
|
+
name = 'toolbox';
|
|
109
|
+
ctx = null;
|
|
110
|
+
calls = [];
|
|
111
|
+
quarantineObserved = [];
|
|
112
|
+
constructor(isQuarantineVisible) {
|
|
113
|
+
this.isQuarantineVisible = isQuarantineVisible;
|
|
114
|
+
}
|
|
115
|
+
async start(ctx) {
|
|
116
|
+
this.ctx = ctx;
|
|
117
|
+
}
|
|
118
|
+
async stop() {
|
|
119
|
+
this.ctx = null;
|
|
120
|
+
}
|
|
121
|
+
getTools() {
|
|
122
|
+
return [{
|
|
123
|
+
name: 'echo',
|
|
124
|
+
description: 'Echoes a sentinel payload',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
message: { type: 'string' },
|
|
129
|
+
},
|
|
130
|
+
required: ['message'],
|
|
131
|
+
},
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
async handleToolCall(call) {
|
|
135
|
+
this.calls.push(call);
|
|
136
|
+
this.quarantineObserved.push(this.isQuarantineVisible());
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
data: { echoed: TOOL_RESULT_SENTINEL },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async onProcess(_event, _state) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function refusalResponse(content = []) {
|
|
147
|
+
return {
|
|
148
|
+
...createMockResponse(content, 'refusal'),
|
|
149
|
+
content,
|
|
150
|
+
stopReason: 'refusal',
|
|
151
|
+
rawAssistantText: '',
|
|
152
|
+
toolCalls: [],
|
|
153
|
+
usage: { inputTokens: 40, outputTokens: 0 },
|
|
154
|
+
details: {
|
|
155
|
+
usage: { inputTokens: 40, outputTokens: 0 },
|
|
156
|
+
stop: { reason: 'refusal', wasTruncated: false },
|
|
157
|
+
},
|
|
158
|
+
raw: {
|
|
159
|
+
response: {
|
|
160
|
+
stop_details: { category: 'reasoning_extraction' },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function toolRoundResponse() {
|
|
166
|
+
return createMockResponse([
|
|
167
|
+
{ type: 'text', text: 'checking tool path' },
|
|
168
|
+
{ type: 'tool_use', id: 'call-1', name: 'toolbox--echo', input: { message: TOOL_ARG_SENTINEL } },
|
|
169
|
+
], 'tool_use');
|
|
170
|
+
}
|
|
171
|
+
function outputResponse(text = RETRY_OUTPUT_SENTINEL) {
|
|
172
|
+
return createMockResponse([{ type: 'text', text }]);
|
|
173
|
+
}
|
|
174
|
+
function summary(id, sourceIds, first, last, content) {
|
|
175
|
+
return {
|
|
176
|
+
id,
|
|
177
|
+
level: 1,
|
|
178
|
+
content,
|
|
179
|
+
tokens: 20,
|
|
180
|
+
sourceLevel: 0,
|
|
181
|
+
sourceIds,
|
|
182
|
+
sourceRange: { first, last },
|
|
183
|
+
created: 1,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function fallbackState(framework) {
|
|
187
|
+
const raw = framework.getStore().getStateJson(FALLBACK_STATE_ID);
|
|
188
|
+
return raw && typeof raw === 'object'
|
|
189
|
+
? raw
|
|
190
|
+
: { requests: [] };
|
|
191
|
+
}
|
|
192
|
+
function primarySummaryQuarantineState(framework, agentName) {
|
|
193
|
+
return framework.getStore().getStateJson(`agents/${agentName}/autobio:primary-summary-quarantine`);
|
|
194
|
+
}
|
|
195
|
+
function jsonContains(value, needle) {
|
|
196
|
+
return JSON.stringify(value ?? null).includes(needle);
|
|
197
|
+
}
|
|
198
|
+
function countMessagesContaining(messages, needle) {
|
|
199
|
+
return messages.filter((message) => JSON.stringify(message.content).includes(needle)).length;
|
|
200
|
+
}
|
|
201
|
+
function chronicleMessages(agent) {
|
|
202
|
+
return agent.getContextManager().getAllMessages();
|
|
203
|
+
}
|
|
204
|
+
function settlementMessages(agent, requestId) {
|
|
205
|
+
return chronicleMessages(agent).filter((message) => (message.metadata?.[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY]?.requestId) === requestId);
|
|
206
|
+
}
|
|
207
|
+
function shardingStrategy(targetChunkTokens = 8) {
|
|
208
|
+
return new ProbeStrategy({
|
|
209
|
+
compressionModel: 'same-model',
|
|
210
|
+
targetChunkTokens,
|
|
211
|
+
recentWindowTokens: 0,
|
|
212
|
+
headWindowTokens: 0,
|
|
213
|
+
autoTickOnNewMessage: false,
|
|
214
|
+
minChunkCharsForLLM: 0,
|
|
215
|
+
mergeThreshold: 99,
|
|
216
|
+
adaptiveResolution: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function longShardText(label, repeat = 240) {
|
|
220
|
+
return `${label} ${'segment '.repeat(repeat)}`;
|
|
221
|
+
}
|
|
222
|
+
function shardIngressContent(strategy, participant, content) {
|
|
223
|
+
const sharded = strategy.chunkIngressMessage(participant, content);
|
|
224
|
+
assert.ok(sharded, 'expected ingress sharding to activate');
|
|
225
|
+
assert.ok(sharded.shards.length > 1, 'expected multiple physical shards');
|
|
226
|
+
return sharded;
|
|
227
|
+
}
|
|
228
|
+
function appendPhysicalMessage(agent, participant, content, metadata, extra) {
|
|
229
|
+
const message = agent.getContextManager().messageStore.append(participant, content, metadata, undefined, extra);
|
|
230
|
+
return message.id;
|
|
231
|
+
}
|
|
232
|
+
function settlementMetadata(options) {
|
|
233
|
+
const dispatchKind = options.dispatchKind ?? 'primary';
|
|
234
|
+
const outcome = options.outcome ?? 'held';
|
|
235
|
+
return {
|
|
236
|
+
[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY]: {
|
|
237
|
+
version: 1,
|
|
238
|
+
requestId: options.requestId,
|
|
239
|
+
settlementId: options.settlementId ?? `${dispatchKind}:${outcome}:terminal:v1`,
|
|
240
|
+
agentName: options.agentName ?? 'assistant',
|
|
241
|
+
dispatchKind,
|
|
242
|
+
outcome,
|
|
243
|
+
...(options.holdReason ? { holdReason: options.holdReason } : {}),
|
|
244
|
+
...(options.stopReason ? { stopReason: options.stopReason } : {}),
|
|
245
|
+
...(options.providerInputTokens !== undefined ? { providerInputTokens: options.providerInputTokens } : {}),
|
|
246
|
+
visibleAssistantOutput: options.visibleAssistantOutput ?? true,
|
|
247
|
+
executedToolCalls: options.executedToolCalls ?? 0,
|
|
248
|
+
branch: {
|
|
249
|
+
id: options.branchId ?? 'main',
|
|
250
|
+
name: options.branchName ?? options.branchId ?? 'main',
|
|
251
|
+
generation: options.branchGeneration ?? 1,
|
|
252
|
+
},
|
|
253
|
+
role: options.role,
|
|
254
|
+
kind: options.kind,
|
|
255
|
+
entryIndex: options.entryIndex,
|
|
256
|
+
entryCount: options.entryCount,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function branchInfo(id = 'main', generation = 1) {
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
name: id,
|
|
264
|
+
head: 1,
|
|
265
|
+
generation,
|
|
266
|
+
created: new Date(NODELESS_DATE),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function projection(branchId, generation, ids) {
|
|
270
|
+
return {
|
|
271
|
+
namespace: 'default',
|
|
272
|
+
branch: { id: branchId, name: branchId, generation },
|
|
273
|
+
selectedSummaries: ids.map((id, index) => ({
|
|
274
|
+
identity: {
|
|
275
|
+
id,
|
|
276
|
+
contentHash: `content-${id}`,
|
|
277
|
+
carrierHash: `carrier-${id}`,
|
|
278
|
+
sourceLeafHash: `leaf-${id}`,
|
|
279
|
+
},
|
|
280
|
+
level: 1,
|
|
281
|
+
orderedSourceIds: [`src-${id}-1`, `src-${id}-2`],
|
|
282
|
+
renderedAs: 'summary_pair',
|
|
283
|
+
pairRange: { start: index * 2, end: index * 2 + 1 },
|
|
284
|
+
})),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function fallbackRecords(framework) {
|
|
288
|
+
return fallbackState(framework).requests;
|
|
289
|
+
}
|
|
290
|
+
async function createFrameworkFixture(options) {
|
|
291
|
+
const path = options?.path ?? freshPath();
|
|
292
|
+
const strategy = options?.strategy ?? new ProbeStrategy({
|
|
293
|
+
compressionModel: 'same-model',
|
|
294
|
+
targetChunkTokens: 100,
|
|
295
|
+
recentWindowTokens: 0,
|
|
296
|
+
headWindowTokens: 0,
|
|
297
|
+
autoTickOnNewMessage: false,
|
|
298
|
+
minChunkCharsForLLM: 0,
|
|
299
|
+
mergeThreshold: 99,
|
|
300
|
+
});
|
|
301
|
+
const membrane = options?.membrane ?? new ScriptedMembrane([]);
|
|
302
|
+
const agentName = options?.agentName ?? 'assistant';
|
|
303
|
+
const agents = [
|
|
304
|
+
{
|
|
305
|
+
name: agentName,
|
|
306
|
+
model: 'test-model',
|
|
307
|
+
systemPrompt: 'system',
|
|
308
|
+
strategy,
|
|
309
|
+
...(options?.maxStreamTokens !== undefined ? { maxStreamTokens: options.maxStreamTokens } : {}),
|
|
310
|
+
refusalHandling: {
|
|
311
|
+
primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 100_000 },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
...((options?.extraAgents ?? []).map((entry) => ({
|
|
315
|
+
name: entry.name,
|
|
316
|
+
model: 'test-model',
|
|
317
|
+
systemPrompt: 'system',
|
|
318
|
+
strategy: entry.strategy,
|
|
319
|
+
refusalHandling: {
|
|
320
|
+
primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 100_000 },
|
|
321
|
+
},
|
|
322
|
+
}))),
|
|
323
|
+
];
|
|
324
|
+
const framework = await AgentFramework.create({
|
|
325
|
+
storePath: path,
|
|
326
|
+
membrane: membrane.asMembrane(),
|
|
327
|
+
agents,
|
|
328
|
+
modules: options?.modules ?? [],
|
|
329
|
+
});
|
|
330
|
+
const agent = framework.getAgent(agentName);
|
|
331
|
+
return { path, framework, agent, strategy, membrane };
|
|
332
|
+
}
|
|
333
|
+
function addSourcePair(agent, label) {
|
|
334
|
+
const manager = agent.getContextManager();
|
|
335
|
+
return [
|
|
336
|
+
manager.addMessage('User', [{ type: 'text', text: `${RAW_PRIVATE_SENTINEL}:${label}:user ${'u '.repeat(30)}` }]),
|
|
337
|
+
manager.addMessage(agent.name, [{ type: 'text', text: `${RAW_PRIVATE_SENTINEL}:${label}:assistant ${'a '.repeat(30)}` }]),
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
function addLatestPrompt(agent, label) {
|
|
341
|
+
return agent.getContextManager().addMessage('User', [{ type: 'text', text: `latest ${label} ${'l '.repeat(20)}` }]);
|
|
342
|
+
}
|
|
343
|
+
function seedSummary(strategy, id, sourceIds) {
|
|
344
|
+
strategy.seed(summary(id, sourceIds, sourceIds[0], sourceIds[sourceIds.length - 1], `${SUMMARY_PRIVATE_SENTINEL}:${id}`));
|
|
345
|
+
}
|
|
346
|
+
async function persistHealthyBaseline(framework, agent, requestId = 'baseline-request') {
|
|
347
|
+
const tools = framework.getAllTools().filter((tool) => agent.canUseTool(tool.name));
|
|
348
|
+
const prepared = await agent.prepareActivationRequest(tools);
|
|
349
|
+
const build = framework.buildPrimarySummaryRequestRecord.bind(framework);
|
|
350
|
+
const persist = framework.persistPrimarySummaryRequestRecord.bind(framework);
|
|
351
|
+
const record = build(agent, requestId, 'primary', prepared.request, prepared.artifacts, agent.getCurrentBranchGeneration() ?? branchInfo(), 'end_turn', true, 0, 30, 'success');
|
|
352
|
+
await persist(record);
|
|
353
|
+
}
|
|
354
|
+
async function enqueueAndDrain(framework, agentName = 'assistant') {
|
|
355
|
+
framework.pendingRequests.push({
|
|
356
|
+
agentName,
|
|
357
|
+
reason: 'test',
|
|
358
|
+
source: 'test',
|
|
359
|
+
timestamp: Date.now(),
|
|
360
|
+
});
|
|
361
|
+
await framework.runUntilIdle();
|
|
362
|
+
}
|
|
363
|
+
async function runPrimaryOnly(framework, agent) {
|
|
364
|
+
await framework.startAgentStream(agent, {
|
|
365
|
+
agentName: agent.name,
|
|
366
|
+
reason: 'test',
|
|
367
|
+
source: 'test',
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
});
|
|
370
|
+
const active = framework.activeStreams.get(agent.name);
|
|
371
|
+
if (active)
|
|
372
|
+
await active;
|
|
373
|
+
}
|
|
374
|
+
function fallbackRetryRecords(framework) {
|
|
375
|
+
return fallbackRecords(framework).filter((record) => record.dispatchKind === 'primary_summary_fallback_retry');
|
|
376
|
+
}
|
|
377
|
+
function latestPrimaryRecord(framework, requestIdExclusions = ['baseline-request']) {
|
|
378
|
+
return fallbackRecords(framework)
|
|
379
|
+
.filter((record) => record.dispatchKind === 'primary'
|
|
380
|
+
&& !requestIdExclusions.includes(String(record.requestId)))
|
|
381
|
+
.at(-1);
|
|
382
|
+
}
|
|
383
|
+
function manualPrimaryRecord(options) {
|
|
384
|
+
const branchId = options.branchId ?? 'main';
|
|
385
|
+
const branchName = options.branchName ?? branchId;
|
|
386
|
+
const branchGeneration = options.branchGeneration ?? 1;
|
|
387
|
+
return {
|
|
388
|
+
requestId: options.requestId,
|
|
389
|
+
agentName: 'assistant',
|
|
390
|
+
namespace: 'default',
|
|
391
|
+
timestamp: Date.now(),
|
|
392
|
+
dispatchKind: 'primary',
|
|
393
|
+
branch: { id: branchId, name: branchName, generation: branchGeneration },
|
|
394
|
+
projection: projection(branchId, branchGeneration, options.projectionIds ?? ['L1-X']),
|
|
395
|
+
requestInputBoundTokens: 100,
|
|
396
|
+
requestCompleteBoundTokens: 200,
|
|
397
|
+
providerInputTokens: 40,
|
|
398
|
+
systemHash: 'system-shared',
|
|
399
|
+
modelConfigHash: 'model-shared',
|
|
400
|
+
toolContractHash: 'tools-shared',
|
|
401
|
+
stopReason: 'refusal',
|
|
402
|
+
visibleAssistantOutput: true,
|
|
403
|
+
executedToolCalls: 0,
|
|
404
|
+
finalStatus: options.finalStatus ?? 'refusal',
|
|
405
|
+
...(options.fallbackStatus ? { fallbackStatus: options.fallbackStatus } : {}),
|
|
406
|
+
...(options.fallbackHeldReason ? { fallbackHeldReason: options.fallbackHeldReason } : {}),
|
|
407
|
+
...(options.fallbackIntent ? { fallbackIntent: options.fallbackIntent } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function resolvedFallbackRetryLogs(framework) {
|
|
411
|
+
return framework.queryInferenceLogs({ limit: 100 }).entries
|
|
412
|
+
.map(({ sequence }) => framework.getInferenceLog(sequence, true))
|
|
413
|
+
.filter((entry) => entry.entry.request?.kind === 'primary_summary_fallback_retry'
|
|
414
|
+
|| entry.entry.response?.kind === 'primary_summary_fallback_retry')
|
|
415
|
+
.map((entry) => entry.entry);
|
|
416
|
+
}
|
|
417
|
+
describe('primary summary refusal fallback integration', () => {
|
|
418
|
+
it('holds a retry stream error without entering the generic retry loop and survives restart', async () => {
|
|
419
|
+
const membrane = new ScriptedMembrane([
|
|
420
|
+
[refusalResponse()],
|
|
421
|
+
new ErrorStream('transport broke during retry'),
|
|
422
|
+
]);
|
|
423
|
+
const { path, framework, agent, strategy } = await createFrameworkFixture({ membrane });
|
|
424
|
+
try {
|
|
425
|
+
const a = addSourcePair(agent, 'A');
|
|
426
|
+
seedSummary(strategy, 'L1-A', a);
|
|
427
|
+
addLatestPrompt(agent, 'baseline');
|
|
428
|
+
await persistHealthyBaseline(framework, agent);
|
|
429
|
+
const b = addSourcePair(agent, 'B');
|
|
430
|
+
seedSummary(strategy, 'L1-B', b);
|
|
431
|
+
addLatestPrompt(agent, 'current');
|
|
432
|
+
await enqueueAndDrain(framework);
|
|
433
|
+
assert.equal(membrane.calls.length, 2, 'one refusal family may dispatch the provider at most twice');
|
|
434
|
+
assert.equal(fallbackRetryRecords(framework).length, 1, 'retry record must stay singular');
|
|
435
|
+
assert.equal(framework.pendingRequests.length, 0);
|
|
436
|
+
const records = fallbackRecords(framework);
|
|
437
|
+
const original = records.find((record) => record.requestId !== 'baseline-request' && record.dispatchKind === 'primary');
|
|
438
|
+
const retry = records.find((record) => record.dispatchKind === 'primary_summary_fallback_retry');
|
|
439
|
+
assert.equal(original.fallbackStatus, 'held');
|
|
440
|
+
assert.equal(original.fallbackHeldReason, 'primary_summary_fallback_retry_stream_error');
|
|
441
|
+
assert.equal(retry.finalStatus, 'held');
|
|
442
|
+
assert.equal(retry.fallbackHeldReason, 'primary_summary_fallback_retry_stream_error');
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
await framework.stop();
|
|
446
|
+
}
|
|
447
|
+
const restarted = await createFrameworkFixture({
|
|
448
|
+
path,
|
|
449
|
+
membrane: new ScriptedMembrane([]),
|
|
450
|
+
strategy: new ProbeStrategy({
|
|
451
|
+
compressionModel: 'same-model',
|
|
452
|
+
targetChunkTokens: 100,
|
|
453
|
+
recentWindowTokens: 0,
|
|
454
|
+
headWindowTokens: 0,
|
|
455
|
+
autoTickOnNewMessage: false,
|
|
456
|
+
minChunkCharsForLLM: 0,
|
|
457
|
+
mergeThreshold: 99,
|
|
458
|
+
}),
|
|
459
|
+
});
|
|
460
|
+
try {
|
|
461
|
+
assert.equal(restarted.membrane.calls.length, 0, 'restart must not resend an ambiguous fallback retry');
|
|
462
|
+
const records = fallbackRecords(restarted.framework);
|
|
463
|
+
const original = records.find((record) => record.requestId !== 'baseline-request' && record.dispatchKind === 'primary');
|
|
464
|
+
const retry = records.find((record) => record.dispatchKind === 'primary_summary_fallback_retry');
|
|
465
|
+
assert.equal(original.fallbackStatus, 'held');
|
|
466
|
+
assert.equal(retry.finalStatus, 'held');
|
|
467
|
+
assert.deepEqual(restarted.framework.healthSnapshot().primarySummaryFallback, { requests: 3, pendingDispatches: 0, unresolvedIntents: 0, held: 2 });
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
await restarted.framework.stop();
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
it('holds an aborted retry as held and never resends it on restart', async () => {
|
|
474
|
+
const membrane = new ScriptedMembrane([
|
|
475
|
+
[refusalResponse()],
|
|
476
|
+
new AbortedStream('timeout'),
|
|
477
|
+
]);
|
|
478
|
+
const { path, framework, agent, strategy } = await createFrameworkFixture({ membrane });
|
|
479
|
+
try {
|
|
480
|
+
const a = addSourcePair(agent, 'A');
|
|
481
|
+
seedSummary(strategy, 'L1-A', a);
|
|
482
|
+
addLatestPrompt(agent, 'baseline');
|
|
483
|
+
await persistHealthyBaseline(framework, agent);
|
|
484
|
+
const b = addSourcePair(agent, 'B');
|
|
485
|
+
seedSummary(strategy, 'L1-B', b);
|
|
486
|
+
addLatestPrompt(agent, 'current');
|
|
487
|
+
await enqueueAndDrain(framework);
|
|
488
|
+
assert.equal(membrane.calls.length, 2);
|
|
489
|
+
const original = latestPrimaryRecord(framework);
|
|
490
|
+
assert.equal(original.fallbackHeldReason, 'stream_aborted:timeout');
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
await framework.stop();
|
|
494
|
+
}
|
|
495
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
496
|
+
try {
|
|
497
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
498
|
+
const original = latestPrimaryRecord(restarted.framework);
|
|
499
|
+
assert.equal(original.fallbackHeldReason, 'stream_aborted:timeout');
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
await restarted.framework.stop();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
it('excludes successful fallback retries from the healthy baseline search', async () => {
|
|
506
|
+
const membrane = new ScriptedMembrane([
|
|
507
|
+
[refusalResponse()],
|
|
508
|
+
[outputResponse('recovered-p2')],
|
|
509
|
+
[refusalResponse()],
|
|
510
|
+
]);
|
|
511
|
+
const { framework, agent, strategy } = await createFrameworkFixture({ membrane });
|
|
512
|
+
try {
|
|
513
|
+
const a = addSourcePair(agent, 'A');
|
|
514
|
+
seedSummary(strategy, 'L1-A', a);
|
|
515
|
+
addLatestPrompt(agent, 'baseline');
|
|
516
|
+
await persistHealthyBaseline(framework, agent);
|
|
517
|
+
const b = addSourcePair(agent, 'B');
|
|
518
|
+
seedSummary(strategy, 'L1-B', b);
|
|
519
|
+
addLatestPrompt(agent, 'p2');
|
|
520
|
+
await enqueueAndDrain(framework);
|
|
521
|
+
const c = addSourcePair(agent, 'C');
|
|
522
|
+
seedSummary(strategy, 'L1-C', c);
|
|
523
|
+
addLatestPrompt(agent, 'p3');
|
|
524
|
+
await runPrimaryOnly(framework, agent);
|
|
525
|
+
const p3 = latestPrimaryRecord(framework);
|
|
526
|
+
const candidateIds = (p3.fallbackIntent.candidateSummaries ?? [])
|
|
527
|
+
.map((item) => item.id);
|
|
528
|
+
assert.deepEqual(candidateIds, ['L1-B', 'L1-C']);
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
await framework.stop();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
it('persists quarantine before tool dispatch, stores canonical output exactly once, and keeps fallback logs metadata-only', async () => {
|
|
535
|
+
let frameworkRef = null;
|
|
536
|
+
const module = new EchoModule(() => jsonContains(primarySummaryQuarantineState(frameworkRef, 'assistant'), 'L1-B'));
|
|
537
|
+
const membrane = new ScriptedMembrane([
|
|
538
|
+
[refusalResponse()],
|
|
539
|
+
[toolRoundResponse(), outputResponse()],
|
|
540
|
+
]);
|
|
541
|
+
const { framework, agent, strategy } = await createFrameworkFixture({ membrane, modules: [module] });
|
|
542
|
+
frameworkRef = framework;
|
|
543
|
+
try {
|
|
544
|
+
const a = addSourcePair(agent, 'A');
|
|
545
|
+
seedSummary(strategy, 'L1-A', a);
|
|
546
|
+
addLatestPrompt(agent, 'baseline');
|
|
547
|
+
await persistHealthyBaseline(framework, agent);
|
|
548
|
+
const b = addSourcePair(agent, 'B');
|
|
549
|
+
seedSummary(strategy, 'L1-B', b);
|
|
550
|
+
addLatestPrompt(agent, 'tool-retry');
|
|
551
|
+
await enqueueAndDrain(framework);
|
|
552
|
+
assert.equal(membrane.calls.length, 2);
|
|
553
|
+
assert.equal(module.calls.length, 1, 'tool executes exactly once');
|
|
554
|
+
assert.deepEqual(module.quarantineObserved, [true], 'quarantine must be durable before tool dispatch');
|
|
555
|
+
const messages = agent.getContextManager().getAllMessages();
|
|
556
|
+
assert.equal(countMessagesContaining(messages, TOOL_ARG_SENTINEL), 1, 'tool_use assistant output is persisted exactly once');
|
|
557
|
+
assert.equal(countMessagesContaining(messages, TOOL_RESULT_SENTINEL), 1, 'tool_result is persisted exactly once');
|
|
558
|
+
assert.equal(countMessagesContaining(messages, RETRY_OUTPUT_SENTINEL), 1, 'trailing assistant output is persisted exactly once');
|
|
559
|
+
const retryLogs = resolvedFallbackRetryLogs(framework);
|
|
560
|
+
assert.ok(retryLogs.length >= 1, 'fallback retry should produce an inference log entry');
|
|
561
|
+
for (const entry of retryLogs) {
|
|
562
|
+
assert.equal(jsonContains(entry, RAW_PRIVATE_SENTINEL), false);
|
|
563
|
+
assert.equal(jsonContains(entry, SUMMARY_PRIVATE_SENTINEL), false);
|
|
564
|
+
assert.equal(jsonContains(entry, TOOL_ARG_SENTINEL), false);
|
|
565
|
+
assert.equal(jsonContains(entry, TOOL_RESULT_SENTINEL), false);
|
|
566
|
+
}
|
|
567
|
+
const observabilityMaterial = [
|
|
568
|
+
fallbackState(framework),
|
|
569
|
+
primarySummaryQuarantineState(framework, 'assistant'),
|
|
570
|
+
...retryLogs,
|
|
571
|
+
];
|
|
572
|
+
for (const material of observabilityMaterial) {
|
|
573
|
+
assert.equal(jsonContains(material, RAW_PRIVATE_SENTINEL), false);
|
|
574
|
+
assert.equal(jsonContains(material, SUMMARY_PRIVATE_SENTINEL), false);
|
|
575
|
+
assert.equal(jsonContains(material, TOOL_ARG_SENTINEL), false);
|
|
576
|
+
assert.equal(jsonContains(material, TOOL_RESULT_SENTINEL), false);
|
|
577
|
+
}
|
|
578
|
+
assert.equal(jsonContains(messages, TOOL_ARG_SENTINEL), true, 'canonical Chronicle output still carries the lived tool input');
|
|
579
|
+
assert.equal(jsonContains(messages, TOOL_RESULT_SENTINEL), true, 'canonical Chronicle output still carries the lived tool result');
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
await framework.stop();
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
it('holds retry text + tool_use output without executing tools, preserves pairing, and keeps fallback telemetry metadata-only', async () => {
|
|
586
|
+
const module = new EchoModule(() => false);
|
|
587
|
+
const membrane = new ScriptedMembrane([
|
|
588
|
+
[refusalResponse()],
|
|
589
|
+
[refusalResponse([
|
|
590
|
+
{ type: 'thinking', thinking: 'reasoning carrier', signature: 'sig-1' },
|
|
591
|
+
{ type: 'text', text: RETRY_OUTPUT_SENTINEL },
|
|
592
|
+
{ type: 'tool_use', id: 'call-1', name: 'toolbox--echo', input: { message: TOOL_ARG_SENTINEL } },
|
|
593
|
+
])],
|
|
594
|
+
]);
|
|
595
|
+
const { framework, agent, strategy } = await createFrameworkFixture({ membrane, modules: [module] });
|
|
596
|
+
try {
|
|
597
|
+
const a = addSourcePair(agent, 'A');
|
|
598
|
+
seedSummary(strategy, 'L1-A', a);
|
|
599
|
+
addLatestPrompt(agent, 'baseline');
|
|
600
|
+
await persistHealthyBaseline(framework, agent);
|
|
601
|
+
const b = addSourcePair(agent, 'B');
|
|
602
|
+
seedSummary(strategy, 'L1-B', b);
|
|
603
|
+
addLatestPrompt(agent, 'held-tool-use');
|
|
604
|
+
await enqueueAndDrain(framework);
|
|
605
|
+
assert.equal(membrane.calls.length, 2);
|
|
606
|
+
assert.equal(module.calls.length, 0, 'held retry output must not execute tools');
|
|
607
|
+
const messages = agent.getContextManager().getAllMessages();
|
|
608
|
+
assert.equal(countMessagesContaining(messages, RETRY_OUTPUT_SENTINEL), 1);
|
|
609
|
+
assert.equal(countMessagesContaining(messages, TOOL_ARG_SENTINEL), 1);
|
|
610
|
+
assert.equal(countMessagesContaining(messages, NON_EXECUTED_TOOL_RESULT), 1);
|
|
611
|
+
const assistantIndex = messages.findIndex((message) => JSON.stringify(message.content).includes(RETRY_OUTPUT_SENTINEL));
|
|
612
|
+
assert.ok(assistantIndex >= 0);
|
|
613
|
+
assert.deepEqual(messages[assistantIndex].content.map((block) => block.type), ['thinking', 'text', 'tool_use']);
|
|
614
|
+
const persistedToolUse = messages[assistantIndex].content.find((block) => block.type === 'tool_use');
|
|
615
|
+
assert.equal(persistedToolUse.id, 'call-1');
|
|
616
|
+
assert.equal(persistedToolUse.input.message, TOOL_ARG_SENTINEL);
|
|
617
|
+
const placeholder = messages[assistantIndex + 1].content[0];
|
|
618
|
+
assert.equal(placeholder.type, 'tool_result');
|
|
619
|
+
assert.equal(placeholder.toolUseId, 'call-1');
|
|
620
|
+
assert.equal(placeholder.content, NON_EXECUTED_TOOL_RESULT);
|
|
621
|
+
assert.equal(placeholder.isError, true);
|
|
622
|
+
const records = fallbackRecords(framework);
|
|
623
|
+
const original = records.find((record) => record.requestId !== 'baseline-request' && record.dispatchKind === 'primary');
|
|
624
|
+
const retry = records.find((record) => record.dispatchKind === 'primary_summary_fallback_retry');
|
|
625
|
+
assert.equal(original.fallbackHeldReason, 'primary_summary_fallback_retry_partial_output');
|
|
626
|
+
assert.equal(retry.fallbackHeldReason, 'primary_summary_fallback_retry_partial_output');
|
|
627
|
+
const retryLogs = resolvedFallbackRetryLogs(framework);
|
|
628
|
+
const observabilityMaterial = [
|
|
629
|
+
fallbackState(framework),
|
|
630
|
+
...retryLogs,
|
|
631
|
+
];
|
|
632
|
+
for (const material of observabilityMaterial) {
|
|
633
|
+
assert.equal(jsonContains(material, RETRY_OUTPUT_SENTINEL), false);
|
|
634
|
+
assert.equal(jsonContains(material, TOOL_ARG_SENTINEL), false);
|
|
635
|
+
assert.equal(jsonContains(material, NON_EXECUTED_TOOL_RESULT), false);
|
|
636
|
+
}
|
|
637
|
+
assert.equal(jsonContains(messages, RETRY_OUTPUT_SENTINEL), true);
|
|
638
|
+
assert.equal(jsonContains(messages, TOOL_ARG_SENTINEL), true);
|
|
639
|
+
assert.equal(jsonContains(messages, NON_EXECUTED_TOOL_RESULT), true);
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
await framework.stop();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
it('persists an initial refusal partial text exactly once, never retries, and holds', async () => {
|
|
646
|
+
const membrane = new ScriptedMembrane([[refusalResponse([{ type: 'text', text: RETRY_OUTPUT_SENTINEL }])]]);
|
|
647
|
+
const { framework, agent } = await createFrameworkFixture({ membrane });
|
|
648
|
+
try {
|
|
649
|
+
await enqueueAndDrain(framework);
|
|
650
|
+
assert.equal(membrane.calls.length, 1);
|
|
651
|
+
const messages = chronicleMessages(agent);
|
|
652
|
+
assert.equal(countMessagesContaining(messages, RETRY_OUTPUT_SENTINEL), 1);
|
|
653
|
+
assert.equal(settlementMessages(agent, String(latestPrimaryRecord(framework).requestId)).length, 1);
|
|
654
|
+
const original = latestPrimaryRecord(framework);
|
|
655
|
+
assert.equal(original.finalStatus, 'held');
|
|
656
|
+
assert.equal(original.fallbackHeldReason, 'primary_summary_refusal_partial_output');
|
|
657
|
+
assert.equal(framework.pendingRequests.length, 0);
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
await framework.stop();
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
it('persists a retry refusal partial text exactly once, stops at two provider calls, and holds', async () => {
|
|
664
|
+
const membrane = new ScriptedMembrane([
|
|
665
|
+
[refusalResponse()],
|
|
666
|
+
[refusalResponse([{ type: 'text', text: RETRY_OUTPUT_SENTINEL }])],
|
|
667
|
+
]);
|
|
668
|
+
const { framework, agent, strategy } = await createFrameworkFixture({ membrane });
|
|
669
|
+
try {
|
|
670
|
+
const a = addSourcePair(agent, 'A');
|
|
671
|
+
seedSummary(strategy, 'L1-A', a);
|
|
672
|
+
addLatestPrompt(agent, 'baseline');
|
|
673
|
+
await persistHealthyBaseline(framework, agent);
|
|
674
|
+
const b = addSourcePair(agent, 'B');
|
|
675
|
+
seedSummary(strategy, 'L1-B', b);
|
|
676
|
+
addLatestPrompt(agent, 'retry-partial');
|
|
677
|
+
await enqueueAndDrain(framework);
|
|
678
|
+
assert.equal(membrane.calls.length, 2);
|
|
679
|
+
const messages = chronicleMessages(agent);
|
|
680
|
+
assert.equal(countMessagesContaining(messages, RETRY_OUTPUT_SENTINEL), 1);
|
|
681
|
+
const records = fallbackRecords(framework);
|
|
682
|
+
const original = records.find((record) => record.requestId !== 'baseline-request' && record.dispatchKind === 'primary');
|
|
683
|
+
const retry = records.find((record) => record.dispatchKind === 'primary_summary_fallback_retry');
|
|
684
|
+
assert.equal(original.fallbackHeldReason, 'primary_summary_fallback_retry_partial_output');
|
|
685
|
+
assert.equal(retry.fallbackHeldReason, 'primary_summary_fallback_retry_partial_output');
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
await framework.stop();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
it('holds an initial thinking + tool_use refusal without executing tools and preserves provider block order', async () => {
|
|
692
|
+
const module = new EchoModule(() => false);
|
|
693
|
+
const refusalBlocks = [
|
|
694
|
+
{ type: 'thinking', thinking: 'initial reasoning', signature: 'sig-initial' },
|
|
695
|
+
{ type: 'tool_use', id: 'call-initial', name: 'toolbox--echo', input: { message: TOOL_ARG_SENTINEL } },
|
|
696
|
+
];
|
|
697
|
+
const membrane = new ScriptedMembrane([[refusalResponse(refusalBlocks)]]);
|
|
698
|
+
const { framework, agent } = await createFrameworkFixture({ membrane, modules: [module] });
|
|
699
|
+
try {
|
|
700
|
+
await enqueueAndDrain(framework);
|
|
701
|
+
assert.equal(membrane.calls.length, 1);
|
|
702
|
+
assert.equal(module.calls.length, 0);
|
|
703
|
+
const requestId = String(latestPrimaryRecord(framework).requestId);
|
|
704
|
+
const settled = settlementMessages(agent, requestId);
|
|
705
|
+
assert.equal(settled.length, 2);
|
|
706
|
+
assert.deepEqual(settled[0].content.map((block) => block.type), ['thinking', 'tool_use']);
|
|
707
|
+
const placeholder = settled[1].content[0];
|
|
708
|
+
assert.equal(placeholder.type, 'tool_result');
|
|
709
|
+
assert.equal(placeholder.toolUseId, 'call-initial');
|
|
710
|
+
assert.equal(placeholder.content, NON_EXECUTED_TOOL_RESULT);
|
|
711
|
+
assert.equal(placeholder.isError, true);
|
|
712
|
+
}
|
|
713
|
+
finally {
|
|
714
|
+
await framework.stop();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
it('preserves all text before, between, and after refused tool_use blocks without silently dropping lived blocks', async () => {
|
|
718
|
+
const refusalBlocks = [
|
|
719
|
+
{ type: 'text', text: 'before' },
|
|
720
|
+
{ type: 'thinking', thinking: 'reasoning', signature: 'sig-mixed' },
|
|
721
|
+
{ type: 'tool_use', id: 'call-a', name: 'toolbox--echo', input: { message: 'A' } },
|
|
722
|
+
{ type: 'text', text: 'between' },
|
|
723
|
+
{ type: 'tool_use', id: 'call-b', name: 'toolbox--echo', input: { message: 'B' } },
|
|
724
|
+
{ type: 'text', text: 'after' },
|
|
725
|
+
];
|
|
726
|
+
const membrane = new ScriptedMembrane([[refusalResponse(refusalBlocks)]]);
|
|
727
|
+
const { framework, agent } = await createFrameworkFixture({ membrane });
|
|
728
|
+
try {
|
|
729
|
+
await enqueueAndDrain(framework);
|
|
730
|
+
const requestId = String(latestPrimaryRecord(framework).requestId);
|
|
731
|
+
const settled = settlementMessages(agent, requestId);
|
|
732
|
+
assert.equal(settled.length, 2);
|
|
733
|
+
assert.deepEqual(settled[0].content.map((block) => block.type), refusalBlocks.map((block) => block.type));
|
|
734
|
+
assert.equal(JSON.stringify(settled[0].content).includes('before'), true);
|
|
735
|
+
assert.equal(JSON.stringify(settled[0].content).includes('between'), true);
|
|
736
|
+
assert.equal(JSON.stringify(settled[0].content).includes('after'), true);
|
|
737
|
+
assert.equal(settled[1].content[0].toolUseId, 'call-a');
|
|
738
|
+
assert.equal(settled[1].content[1].toolUseId, 'call-b');
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
await framework.stop();
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
it('executes earlier retry tool rounds exactly once but never executes the final refused tool_use round', async () => {
|
|
745
|
+
const module = new EchoModule(() => true);
|
|
746
|
+
const membrane = new ScriptedMembrane([
|
|
747
|
+
[refusalResponse()],
|
|
748
|
+
[toolRoundResponse(), refusalResponse([
|
|
749
|
+
{ type: 'thinking', thinking: 'final reasoning', signature: 'sig-final' },
|
|
750
|
+
{ type: 'text', text: RETRY_OUTPUT_SENTINEL },
|
|
751
|
+
{ type: 'tool_use', id: 'call-final', name: 'toolbox--echo', input: { message: FINAL_TOOL_ARG_SENTINEL } },
|
|
752
|
+
])],
|
|
753
|
+
]);
|
|
754
|
+
const { framework, agent, strategy } = await createFrameworkFixture({ membrane, modules: [module] });
|
|
755
|
+
try {
|
|
756
|
+
const a = addSourcePair(agent, 'A');
|
|
757
|
+
seedSummary(strategy, 'L1-A', a);
|
|
758
|
+
addLatestPrompt(agent, 'baseline');
|
|
759
|
+
await persistHealthyBaseline(framework, agent);
|
|
760
|
+
const b = addSourcePair(agent, 'B');
|
|
761
|
+
seedSummary(strategy, 'L1-B', b);
|
|
762
|
+
addLatestPrompt(agent, 'final-refusal-after-tool');
|
|
763
|
+
await enqueueAndDrain(framework);
|
|
764
|
+
assert.equal(membrane.calls.length, 2);
|
|
765
|
+
assert.equal(module.calls.length, 1, 'only the successful earlier tool round may execute');
|
|
766
|
+
const messages = chronicleMessages(agent);
|
|
767
|
+
assert.equal(countMessagesContaining(messages, TOOL_RESULT_SENTINEL), 1, 'earlier tool result persists exactly once');
|
|
768
|
+
assert.equal(countMessagesContaining(messages, FINAL_TOOL_ARG_SENTINEL), 1, 'final refused tool_use persists exactly once');
|
|
769
|
+
const retry = fallbackRetryRecords(framework).at(-1);
|
|
770
|
+
const settled = settlementMessages(agent, String(retry.requestId));
|
|
771
|
+
assert.equal(settled.length, 2);
|
|
772
|
+
assert.deepEqual(settled[0].content.map((block) => block.type), ['thinking', 'text', 'tool_use']);
|
|
773
|
+
const placeholder = settled[1].content[0];
|
|
774
|
+
assert.equal(placeholder.toolUseId, 'call-final');
|
|
775
|
+
assert.equal(placeholder.content, NON_EXECUTED_TOOL_RESULT);
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
await framework.stop();
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
it('holds a fallback retry instead of queueing a context-budget restart after a tool result', async () => {
|
|
782
|
+
let frameworkRef = null;
|
|
783
|
+
const module = new EchoModule(() => jsonContains(primarySummaryQuarantineState(frameworkRef, 'assistant'), 'L1-B'));
|
|
784
|
+
const membrane = new ScriptedMembrane([
|
|
785
|
+
[refusalResponse()],
|
|
786
|
+
[toolRoundResponse()],
|
|
787
|
+
]);
|
|
788
|
+
const { framework, agent, strategy } = await createFrameworkFixture({
|
|
789
|
+
membrane,
|
|
790
|
+
modules: [module],
|
|
791
|
+
maxStreamTokens: 5,
|
|
792
|
+
});
|
|
793
|
+
frameworkRef = framework;
|
|
794
|
+
try {
|
|
795
|
+
const a = addSourcePair(agent, 'A');
|
|
796
|
+
seedSummary(strategy, 'L1-A', a);
|
|
797
|
+
addLatestPrompt(agent, 'baseline');
|
|
798
|
+
await persistHealthyBaseline(framework, agent);
|
|
799
|
+
const b = addSourcePair(agent, 'B');
|
|
800
|
+
seedSummary(strategy, 'L1-B', b);
|
|
801
|
+
addLatestPrompt(agent, 'budget-restart-repro');
|
|
802
|
+
await enqueueAndDrain(framework);
|
|
803
|
+
assert.equal(membrane.calls.length, 2, 'fallback tool-result over-budget must not trigger a third provider call');
|
|
804
|
+
assert.equal(module.calls.length, 1, 'tool must execute exactly once');
|
|
805
|
+
assert.deepEqual(module.quarantineObserved, [true], 'tool dispatch still runs against durable quarantine state');
|
|
806
|
+
const queuedRequests = framework.pendingRequests;
|
|
807
|
+
assert.equal(queuedRequests.filter((request) => request.reason === 'context_budget_restart').length, 0, 'no context_budget_restart may remain queued for the held fallback family');
|
|
808
|
+
const records = fallbackRecords(framework);
|
|
809
|
+
const original = records.find((record) => record.requestId !== 'baseline-request' && record.dispatchKind === 'primary');
|
|
810
|
+
const retry = records.find((record) => record.dispatchKind === 'primary_summary_fallback_retry');
|
|
811
|
+
assert.equal(original.finalStatus, 'held');
|
|
812
|
+
assert.equal(original.fallbackStatus, 'held');
|
|
813
|
+
assert.equal(original.fallbackHeldReason, 'primary_summary_fallback_context_budget_restart_required');
|
|
814
|
+
assert.equal(retry.finalStatus, 'held');
|
|
815
|
+
assert.equal(retry.fallbackHeldReason, 'primary_summary_fallback_context_budget_restart_required');
|
|
816
|
+
assert.equal(fallbackRetryRecords(framework).length, 1, 'the held retry family must remain singular');
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
await framework.stop();
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
it('discards a completed retry result after a branch switch mid-await', async () => {
|
|
823
|
+
const membrane = new ScriptedMembrane([
|
|
824
|
+
[refusalResponse()],
|
|
825
|
+
new HookedCompleteStream(outputResponse(), async () => {
|
|
826
|
+
const manager = agentRef.getContextManager();
|
|
827
|
+
const firstMessageId = manager.getAllMessages()[0].id;
|
|
828
|
+
const fork = manager.branchAt(firstMessageId, 'branch-switch-fork');
|
|
829
|
+
await manager.switchBranch(fork);
|
|
830
|
+
}),
|
|
831
|
+
]);
|
|
832
|
+
let agentRef = null;
|
|
833
|
+
const { framework, agent, strategy } = await createFrameworkFixture({ membrane });
|
|
834
|
+
agentRef = agent;
|
|
835
|
+
try {
|
|
836
|
+
const mainBranch = agent.getContextManager().currentBranch().name;
|
|
837
|
+
const a = addSourcePair(agent, 'A');
|
|
838
|
+
seedSummary(strategy, 'L1-A', a);
|
|
839
|
+
addLatestPrompt(agent, 'baseline');
|
|
840
|
+
await persistHealthyBaseline(framework, agent);
|
|
841
|
+
const b = addSourcePair(agent, 'B');
|
|
842
|
+
seedSummary(strategy, 'L1-B', b);
|
|
843
|
+
addLatestPrompt(agent, 'current');
|
|
844
|
+
await enqueueAndDrain(framework);
|
|
845
|
+
assert.equal(jsonContains(primarySummaryQuarantineState(framework, 'assistant'), 'L1-B'), false);
|
|
846
|
+
const messages = agent.getContextManager().getAllMessages();
|
|
847
|
+
assert.equal(countMessagesContaining(messages, RETRY_OUTPUT_SENTINEL), 0);
|
|
848
|
+
assert.equal(fallbackRecords(framework).length, 0, 'fork branch must not receive fallback-family state writes');
|
|
849
|
+
assert.equal(membrane.calls.length, 2, 'the retry provider call may happen once, but must not be replayed');
|
|
850
|
+
await agent.getContextManager().switchBranch(mainBranch);
|
|
851
|
+
const mainMessages = agent.getContextManager().getAllMessages();
|
|
852
|
+
assert.equal(countMessagesContaining(mainMessages, RETRY_OUTPUT_SENTINEL), 0, 'source branch must not receive stale retry output');
|
|
853
|
+
}
|
|
854
|
+
finally {
|
|
855
|
+
await framework.stop();
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
it('keeps quarantine namespace-local across agents', async () => {
|
|
859
|
+
const membrane = new ScriptedMembrane([
|
|
860
|
+
[refusalResponse()],
|
|
861
|
+
[outputResponse('agent-a retry success')],
|
|
862
|
+
]);
|
|
863
|
+
const strategyA = new ProbeStrategy({
|
|
864
|
+
compressionModel: 'same-model',
|
|
865
|
+
targetChunkTokens: 100,
|
|
866
|
+
recentWindowTokens: 0,
|
|
867
|
+
headWindowTokens: 0,
|
|
868
|
+
autoTickOnNewMessage: false,
|
|
869
|
+
minChunkCharsForLLM: 0,
|
|
870
|
+
mergeThreshold: 99,
|
|
871
|
+
});
|
|
872
|
+
const strategyB = new ProbeStrategy({
|
|
873
|
+
compressionModel: 'same-model',
|
|
874
|
+
targetChunkTokens: 100,
|
|
875
|
+
recentWindowTokens: 0,
|
|
876
|
+
headWindowTokens: 0,
|
|
877
|
+
autoTickOnNewMessage: false,
|
|
878
|
+
minChunkCharsForLLM: 0,
|
|
879
|
+
mergeThreshold: 99,
|
|
880
|
+
});
|
|
881
|
+
const { framework, agent, strategy } = await createFrameworkFixture({
|
|
882
|
+
membrane,
|
|
883
|
+
strategy: strategyA,
|
|
884
|
+
extraAgents: [{ name: 'observer', strategy: strategyB }],
|
|
885
|
+
});
|
|
886
|
+
try {
|
|
887
|
+
const observer = framework.getAgent('observer');
|
|
888
|
+
const a = addSourcePair(agent, 'A');
|
|
889
|
+
seedSummary(strategy, 'L1-A', a);
|
|
890
|
+
seedSummary(strategyB, 'L1-A', a);
|
|
891
|
+
addLatestPrompt(agent, 'baseline');
|
|
892
|
+
addLatestPrompt(observer, 'baseline');
|
|
893
|
+
await persistHealthyBaseline(framework, agent);
|
|
894
|
+
const b = addSourcePair(agent, 'B');
|
|
895
|
+
seedSummary(strategy, 'L1-B', b);
|
|
896
|
+
seedSummary(strategyB, 'L1-B', b);
|
|
897
|
+
addLatestPrompt(agent, 'current');
|
|
898
|
+
addLatestPrompt(observer, 'current');
|
|
899
|
+
await enqueueAndDrain(framework);
|
|
900
|
+
assert.ok(primarySummaryQuarantineState(framework, 'assistant'));
|
|
901
|
+
assert.equal(primarySummaryQuarantineState(framework, 'observer'), null, 'another namespace must not inherit the retry quarantine');
|
|
902
|
+
}
|
|
903
|
+
finally {
|
|
904
|
+
await framework.stop();
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
it('repairs a missing held placeholder on restart from the exact stored assistant settlement', async () => {
|
|
908
|
+
const requestId = 'repair-request';
|
|
909
|
+
const path = freshPath();
|
|
910
|
+
const first = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
911
|
+
try {
|
|
912
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
913
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
914
|
+
requests: [manualPrimaryRecord({
|
|
915
|
+
requestId,
|
|
916
|
+
finalStatus: 'refusal',
|
|
917
|
+
branchId: branch.id,
|
|
918
|
+
branchName: branch.name,
|
|
919
|
+
branchGeneration: branch.generation,
|
|
920
|
+
})],
|
|
921
|
+
});
|
|
922
|
+
first.agent.getContextManager().addMessage('assistant', [
|
|
923
|
+
{ type: 'thinking', thinking: 'repair reasoning', signature: 'sig-repair' },
|
|
924
|
+
{ type: 'tool_use', id: 'repair-call', name: 'toolbox--echo', input: { message: FINAL_TOOL_ARG_SENTINEL } },
|
|
925
|
+
], settlementMetadata({
|
|
926
|
+
requestId,
|
|
927
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
928
|
+
stopReason: 'refusal',
|
|
929
|
+
providerInputTokens: 40,
|
|
930
|
+
visibleAssistantOutput: true,
|
|
931
|
+
executedToolCalls: 0,
|
|
932
|
+
branchId: branch.id,
|
|
933
|
+
branchName: branch.name,
|
|
934
|
+
branchGeneration: branch.generation,
|
|
935
|
+
entryIndex: 0,
|
|
936
|
+
entryCount: 2,
|
|
937
|
+
role: 'assistant',
|
|
938
|
+
kind: 'assistant_output',
|
|
939
|
+
}));
|
|
940
|
+
}
|
|
941
|
+
finally {
|
|
942
|
+
await first.framework.stop();
|
|
943
|
+
}
|
|
944
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
945
|
+
try {
|
|
946
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
947
|
+
const settled = settlementMessages(restarted.agent, requestId);
|
|
948
|
+
assert.equal(settled.length, 2);
|
|
949
|
+
assert.equal(countMessagesContaining(settled, NON_EXECUTED_TOOL_RESULT), 1);
|
|
950
|
+
const record = latestPrimaryRecord(restarted.framework, ['baseline-request']);
|
|
951
|
+
assert.equal(record.finalStatus, 'held');
|
|
952
|
+
assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
await restarted.framework.stop();
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
it('finalizes an already-paired held settlement on restart without duplicating writes', async () => {
|
|
959
|
+
const requestId = 'paired-request';
|
|
960
|
+
const path = freshPath();
|
|
961
|
+
const first = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
962
|
+
try {
|
|
963
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
964
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
965
|
+
requests: [manualPrimaryRecord({
|
|
966
|
+
requestId,
|
|
967
|
+
finalStatus: 'refusal',
|
|
968
|
+
branchId: branch.id,
|
|
969
|
+
branchName: branch.name,
|
|
970
|
+
branchGeneration: branch.generation,
|
|
971
|
+
})],
|
|
972
|
+
});
|
|
973
|
+
first.agent.getContextManager().addMessage('assistant', [{ type: 'tool_use', id: 'paired-call', name: 'toolbox--echo', input: { message: FINAL_TOOL_ARG_SENTINEL } }], settlementMetadata({
|
|
974
|
+
requestId,
|
|
975
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
976
|
+
stopReason: 'refusal',
|
|
977
|
+
providerInputTokens: 40,
|
|
978
|
+
visibleAssistantOutput: true,
|
|
979
|
+
executedToolCalls: 0,
|
|
980
|
+
branchId: branch.id,
|
|
981
|
+
branchName: branch.name,
|
|
982
|
+
branchGeneration: branch.generation,
|
|
983
|
+
entryIndex: 0,
|
|
984
|
+
entryCount: 2,
|
|
985
|
+
role: 'assistant',
|
|
986
|
+
kind: 'assistant_output',
|
|
987
|
+
}));
|
|
988
|
+
first.agent.getContextManager().addMessage('user', [{ type: 'tool_result', toolUseId: 'paired-call', content: NON_EXECUTED_TOOL_RESULT, isError: true }], settlementMetadata({
|
|
989
|
+
requestId,
|
|
990
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
991
|
+
stopReason: 'refusal',
|
|
992
|
+
providerInputTokens: 40,
|
|
993
|
+
visibleAssistantOutput: true,
|
|
994
|
+
executedToolCalls: 0,
|
|
995
|
+
branchId: branch.id,
|
|
996
|
+
branchName: branch.name,
|
|
997
|
+
branchGeneration: branch.generation,
|
|
998
|
+
entryIndex: 1,
|
|
999
|
+
entryCount: 2,
|
|
1000
|
+
role: 'user',
|
|
1001
|
+
kind: 'generated_tool_result',
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
1004
|
+
finally {
|
|
1005
|
+
await first.framework.stop();
|
|
1006
|
+
}
|
|
1007
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1008
|
+
try {
|
|
1009
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1010
|
+
const settled = settlementMessages(restarted.agent, requestId);
|
|
1011
|
+
assert.equal(settled.length, 2);
|
|
1012
|
+
assert.equal(countMessagesContaining(settled, NON_EXECUTED_TOOL_RESULT), 1);
|
|
1013
|
+
const record = latestPrimaryRecord(restarted.framework, ['baseline-request']);
|
|
1014
|
+
assert.equal(record.finalStatus, 'held');
|
|
1015
|
+
assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
|
|
1016
|
+
}
|
|
1017
|
+
finally {
|
|
1018
|
+
await restarted.framework.stop();
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
it('does not let a historical identical block sequence satisfy a different request settlement', async () => {
|
|
1022
|
+
const path = freshPath();
|
|
1023
|
+
const first = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1024
|
+
try {
|
|
1025
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1026
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1027
|
+
requests: [
|
|
1028
|
+
manualPrimaryRecord({
|
|
1029
|
+
requestId: 'historical-request',
|
|
1030
|
+
finalStatus: 'held',
|
|
1031
|
+
fallbackHeldReason: 'primary_summary_refusal_partial_output',
|
|
1032
|
+
branchId: branch.id,
|
|
1033
|
+
branchName: branch.name,
|
|
1034
|
+
branchGeneration: branch.generation,
|
|
1035
|
+
}),
|
|
1036
|
+
manualPrimaryRecord({
|
|
1037
|
+
requestId: 'current-request',
|
|
1038
|
+
finalStatus: 'refusal',
|
|
1039
|
+
branchId: branch.id,
|
|
1040
|
+
branchName: branch.name,
|
|
1041
|
+
branchGeneration: branch.generation,
|
|
1042
|
+
}),
|
|
1043
|
+
],
|
|
1044
|
+
});
|
|
1045
|
+
first.agent.getContextManager().addMessage('assistant', [{ type: 'text', text: RETRY_OUTPUT_SENTINEL }], settlementMetadata({
|
|
1046
|
+
requestId: 'historical-request',
|
|
1047
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1048
|
+
stopReason: 'refusal',
|
|
1049
|
+
branchId: branch.id,
|
|
1050
|
+
branchName: branch.name,
|
|
1051
|
+
branchGeneration: branch.generation,
|
|
1052
|
+
entryIndex: 0,
|
|
1053
|
+
entryCount: 1,
|
|
1054
|
+
role: 'assistant',
|
|
1055
|
+
kind: 'assistant_output',
|
|
1056
|
+
}));
|
|
1057
|
+
}
|
|
1058
|
+
finally {
|
|
1059
|
+
await first.framework.stop();
|
|
1060
|
+
}
|
|
1061
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1062
|
+
try {
|
|
1063
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1064
|
+
assert.equal(settlementMessages(restarted.agent, 'current-request').length, 0);
|
|
1065
|
+
const current = fallbackRecords(restarted.framework).find((record) => record.requestId === 'current-request');
|
|
1066
|
+
assert.equal(current.finalStatus, 'held');
|
|
1067
|
+
assert.equal(current.fallbackHeldReason, 'primary_summary_request_unresolved_on_restart');
|
|
1068
|
+
}
|
|
1069
|
+
finally {
|
|
1070
|
+
await restarted.framework.stop();
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
it('fails closed on restart when settlement metadata branch generation mismatches the request record', async () => {
|
|
1074
|
+
const requestId = 'branch-mismatch-request';
|
|
1075
|
+
const path = freshPath();
|
|
1076
|
+
const first = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1077
|
+
try {
|
|
1078
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1079
|
+
requests: [manualPrimaryRecord({ requestId, finalStatus: 'refusal', branchGeneration: 2 })],
|
|
1080
|
+
});
|
|
1081
|
+
first.agent.getContextManager().addMessage('assistant', [{ type: 'text', text: RETRY_OUTPUT_SENTINEL }], settlementMetadata({
|
|
1082
|
+
requestId,
|
|
1083
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1084
|
+
stopReason: 'refusal',
|
|
1085
|
+
branchGeneration: 1,
|
|
1086
|
+
entryIndex: 0,
|
|
1087
|
+
entryCount: 1,
|
|
1088
|
+
role: 'assistant',
|
|
1089
|
+
kind: 'assistant_output',
|
|
1090
|
+
}));
|
|
1091
|
+
}
|
|
1092
|
+
finally {
|
|
1093
|
+
await first.framework.stop();
|
|
1094
|
+
}
|
|
1095
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1096
|
+
try {
|
|
1097
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1098
|
+
const current = fallbackRecords(restarted.framework).find((record) => record.requestId === requestId);
|
|
1099
|
+
assert.equal(current.finalStatus, 'held');
|
|
1100
|
+
assert.equal(current.fallbackHeldReason, 'primary_summary_settlement_branch_mismatch_on_restart');
|
|
1101
|
+
assert.equal(settlementMessages(restarted.agent, requestId).length, 1, 'restart must not rewrite or finalize against the wrong branch generation');
|
|
1102
|
+
}
|
|
1103
|
+
finally {
|
|
1104
|
+
await restarted.framework.stop();
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
it('finalizes a long partial-text held settlement written as ingress shards exactly once on restart', async () => {
|
|
1108
|
+
const requestId = 'sharded-partial-text-request';
|
|
1109
|
+
const path = freshPath();
|
|
1110
|
+
let assistantShardCount = 0;
|
|
1111
|
+
const first = await createFrameworkFixture({
|
|
1112
|
+
path,
|
|
1113
|
+
membrane: new ScriptedMembrane([]),
|
|
1114
|
+
strategy: shardingStrategy(),
|
|
1115
|
+
});
|
|
1116
|
+
try {
|
|
1117
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1118
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1119
|
+
requests: [manualPrimaryRecord({
|
|
1120
|
+
requestId,
|
|
1121
|
+
finalStatus: 'refusal',
|
|
1122
|
+
branchId: branch.id,
|
|
1123
|
+
branchName: branch.name,
|
|
1124
|
+
branchGeneration: branch.generation,
|
|
1125
|
+
})],
|
|
1126
|
+
});
|
|
1127
|
+
first.agent.getContextManager().addMessage('assistant', [{ type: 'text', text: longShardText('long-held-text') }], settlementMetadata({
|
|
1128
|
+
requestId,
|
|
1129
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1130
|
+
stopReason: 'refusal',
|
|
1131
|
+
providerInputTokens: 40,
|
|
1132
|
+
visibleAssistantOutput: true,
|
|
1133
|
+
executedToolCalls: 0,
|
|
1134
|
+
branchId: branch.id,
|
|
1135
|
+
branchName: branch.name,
|
|
1136
|
+
branchGeneration: branch.generation,
|
|
1137
|
+
entryIndex: 0,
|
|
1138
|
+
entryCount: 1,
|
|
1139
|
+
role: 'assistant',
|
|
1140
|
+
kind: 'assistant_output',
|
|
1141
|
+
}));
|
|
1142
|
+
const persisted = settlementMessages(first.agent, requestId);
|
|
1143
|
+
assistantShardCount = persisted.length;
|
|
1144
|
+
assert.ok(assistantShardCount > 1, 'assistant settlement must shard physically');
|
|
1145
|
+
assert.ok(persisted.every((message, index) => message.bodyGroupId === persisted[0].bodyGroupId && message.shardIndex === index));
|
|
1146
|
+
}
|
|
1147
|
+
finally {
|
|
1148
|
+
await first.framework.stop();
|
|
1149
|
+
}
|
|
1150
|
+
const restarted = await createFrameworkFixture({
|
|
1151
|
+
path,
|
|
1152
|
+
membrane: new ScriptedMembrane([]),
|
|
1153
|
+
strategy: shardingStrategy(),
|
|
1154
|
+
});
|
|
1155
|
+
try {
|
|
1156
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1157
|
+
const settled = settlementMessages(restarted.agent, requestId);
|
|
1158
|
+
assert.equal(settled.length, assistantShardCount);
|
|
1159
|
+
assert.equal(countMessagesContaining(settled, NON_EXECUTED_TOOL_RESULT), 0);
|
|
1160
|
+
const record = fallbackRecords(restarted.framework).find((entry) => entry.requestId === requestId);
|
|
1161
|
+
assert.equal(record.finalStatus, 'held');
|
|
1162
|
+
assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
|
|
1163
|
+
}
|
|
1164
|
+
finally {
|
|
1165
|
+
await restarted.framework.stop();
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
it('repairs one missing placeholder after a sharded assistant settlement restart and never executes refused tools', async () => {
|
|
1169
|
+
const requestId = 'sharded-placeholder-repair-request';
|
|
1170
|
+
const path = freshPath();
|
|
1171
|
+
let assistantShardCount = 0;
|
|
1172
|
+
const first = await createFrameworkFixture({
|
|
1173
|
+
path,
|
|
1174
|
+
membrane: new ScriptedMembrane([]),
|
|
1175
|
+
strategy: shardingStrategy(),
|
|
1176
|
+
});
|
|
1177
|
+
try {
|
|
1178
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1179
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1180
|
+
requests: [manualPrimaryRecord({
|
|
1181
|
+
requestId,
|
|
1182
|
+
finalStatus: 'refusal',
|
|
1183
|
+
branchId: branch.id,
|
|
1184
|
+
branchName: branch.name,
|
|
1185
|
+
branchGeneration: branch.generation,
|
|
1186
|
+
})],
|
|
1187
|
+
});
|
|
1188
|
+
first.agent.getContextManager().addMessage('assistant', [
|
|
1189
|
+
{ type: 'thinking', thinking: 'repair reasoning', signature: 'sig-sharded-repair' },
|
|
1190
|
+
{ type: 'tool_use', id: 'repair-call', name: 'toolbox--echo', input: { message: FINAL_TOOL_ARG_SENTINEL } },
|
|
1191
|
+
{ type: 'text', text: longShardText('repair-placeholder') },
|
|
1192
|
+
], settlementMetadata({
|
|
1193
|
+
requestId,
|
|
1194
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1195
|
+
stopReason: 'refusal',
|
|
1196
|
+
providerInputTokens: 40,
|
|
1197
|
+
visibleAssistantOutput: true,
|
|
1198
|
+
executedToolCalls: 0,
|
|
1199
|
+
branchId: branch.id,
|
|
1200
|
+
branchName: branch.name,
|
|
1201
|
+
branchGeneration: branch.generation,
|
|
1202
|
+
entryIndex: 0,
|
|
1203
|
+
entryCount: 2,
|
|
1204
|
+
role: 'assistant',
|
|
1205
|
+
kind: 'assistant_output',
|
|
1206
|
+
}));
|
|
1207
|
+
assistantShardCount = settlementMessages(first.agent, requestId).length;
|
|
1208
|
+
assert.ok(assistantShardCount > 1, 'assistant settlement must shard physically');
|
|
1209
|
+
}
|
|
1210
|
+
finally {
|
|
1211
|
+
await first.framework.stop();
|
|
1212
|
+
}
|
|
1213
|
+
const module = new EchoModule(() => false);
|
|
1214
|
+
const restarted = await createFrameworkFixture({
|
|
1215
|
+
path,
|
|
1216
|
+
membrane: new ScriptedMembrane([]),
|
|
1217
|
+
modules: [module],
|
|
1218
|
+
strategy: shardingStrategy(),
|
|
1219
|
+
});
|
|
1220
|
+
try {
|
|
1221
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1222
|
+
assert.equal(module.calls.length, 0, 'restart repair must not execute refused tools');
|
|
1223
|
+
const settled = settlementMessages(restarted.agent, requestId);
|
|
1224
|
+
assert.equal(settled.length, assistantShardCount + 1);
|
|
1225
|
+
const placeholders = settled.filter((message) => message.participant === 'user');
|
|
1226
|
+
assert.equal(placeholders.length, 1);
|
|
1227
|
+
const placeholder = placeholders[0].content[0];
|
|
1228
|
+
assert.equal(placeholder.type, 'tool_result');
|
|
1229
|
+
assert.equal(placeholder.toolUseId, 'repair-call');
|
|
1230
|
+
assert.equal(placeholder.content, NON_EXECUTED_TOOL_RESULT);
|
|
1231
|
+
assert.equal(placeholder.isError, true);
|
|
1232
|
+
const record = fallbackRecords(restarted.framework).find((entry) => entry.requestId === requestId);
|
|
1233
|
+
assert.equal(record.finalStatus, 'held');
|
|
1234
|
+
assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
|
|
1235
|
+
}
|
|
1236
|
+
finally {
|
|
1237
|
+
await restarted.framework.stop();
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
it('finalizes a complete sharded assistant plus placeholder settlement on restart without duplicate writes or effects', async () => {
|
|
1241
|
+
const requestId = 'sharded-complete-request';
|
|
1242
|
+
const path = freshPath();
|
|
1243
|
+
let persistedCount = 0;
|
|
1244
|
+
const first = await createFrameworkFixture({
|
|
1245
|
+
path,
|
|
1246
|
+
membrane: new ScriptedMembrane([]),
|
|
1247
|
+
strategy: shardingStrategy(),
|
|
1248
|
+
});
|
|
1249
|
+
try {
|
|
1250
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1251
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1252
|
+
requests: [manualPrimaryRecord({
|
|
1253
|
+
requestId,
|
|
1254
|
+
finalStatus: 'refusal',
|
|
1255
|
+
branchId: branch.id,
|
|
1256
|
+
branchName: branch.name,
|
|
1257
|
+
branchGeneration: branch.generation,
|
|
1258
|
+
})],
|
|
1259
|
+
});
|
|
1260
|
+
first.agent.getContextManager().addMessage('assistant', [
|
|
1261
|
+
{ type: 'thinking', thinking: 'paired reasoning', signature: 'sig-sharded-paired' },
|
|
1262
|
+
{ type: 'tool_use', id: 'paired-call', name: 'toolbox--echo', input: { message: FINAL_TOOL_ARG_SENTINEL } },
|
|
1263
|
+
{ type: 'text', text: longShardText('paired-placeholder') },
|
|
1264
|
+
], settlementMetadata({
|
|
1265
|
+
requestId,
|
|
1266
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1267
|
+
stopReason: 'refusal',
|
|
1268
|
+
providerInputTokens: 40,
|
|
1269
|
+
visibleAssistantOutput: true,
|
|
1270
|
+
executedToolCalls: 0,
|
|
1271
|
+
branchId: branch.id,
|
|
1272
|
+
branchName: branch.name,
|
|
1273
|
+
branchGeneration: branch.generation,
|
|
1274
|
+
entryIndex: 0,
|
|
1275
|
+
entryCount: 2,
|
|
1276
|
+
role: 'assistant',
|
|
1277
|
+
kind: 'assistant_output',
|
|
1278
|
+
}));
|
|
1279
|
+
first.agent.getContextManager().addMessage('user', [{ type: 'tool_result', toolUseId: 'paired-call', content: NON_EXECUTED_TOOL_RESULT, isError: true }], settlementMetadata({
|
|
1280
|
+
requestId,
|
|
1281
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1282
|
+
stopReason: 'refusal',
|
|
1283
|
+
providerInputTokens: 40,
|
|
1284
|
+
visibleAssistantOutput: true,
|
|
1285
|
+
executedToolCalls: 0,
|
|
1286
|
+
branchId: branch.id,
|
|
1287
|
+
branchName: branch.name,
|
|
1288
|
+
branchGeneration: branch.generation,
|
|
1289
|
+
entryIndex: 1,
|
|
1290
|
+
entryCount: 2,
|
|
1291
|
+
role: 'user',
|
|
1292
|
+
kind: 'generated_tool_result',
|
|
1293
|
+
}));
|
|
1294
|
+
persistedCount = settlementMessages(first.agent, requestId).length;
|
|
1295
|
+
assert.ok(persistedCount > 2, 'assistant shards plus the placeholder must be persisted');
|
|
1296
|
+
}
|
|
1297
|
+
finally {
|
|
1298
|
+
await first.framework.stop();
|
|
1299
|
+
}
|
|
1300
|
+
const module = new EchoModule(() => false);
|
|
1301
|
+
const restarted = await createFrameworkFixture({
|
|
1302
|
+
path,
|
|
1303
|
+
membrane: new ScriptedMembrane([]),
|
|
1304
|
+
modules: [module],
|
|
1305
|
+
strategy: shardingStrategy(),
|
|
1306
|
+
});
|
|
1307
|
+
try {
|
|
1308
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1309
|
+
assert.equal(module.calls.length, 0);
|
|
1310
|
+
const settled = settlementMessages(restarted.agent, requestId);
|
|
1311
|
+
assert.equal(settled.length, persistedCount);
|
|
1312
|
+
assert.equal(countMessagesContaining(settled, NON_EXECUTED_TOOL_RESULT), 1);
|
|
1313
|
+
const record = fallbackRecords(restarted.framework).find((entry) => entry.requestId === requestId);
|
|
1314
|
+
assert.equal(record.finalStatus, 'held');
|
|
1315
|
+
assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
|
|
1316
|
+
}
|
|
1317
|
+
finally {
|
|
1318
|
+
await restarted.framework.stop();
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
it('fails closed on restart for invalid sharded settlement layouts', async () => {
|
|
1322
|
+
const cases = [
|
|
1323
|
+
{
|
|
1324
|
+
name: 'missing shard',
|
|
1325
|
+
expectedReason: 'primary_summary_settlement_shard_group_invalid_on_restart',
|
|
1326
|
+
persist: (fixture, requestId) => {
|
|
1327
|
+
const branch = fixture.agent.getCurrentBranchGeneration();
|
|
1328
|
+
const metadata = settlementMetadata({
|
|
1329
|
+
requestId,
|
|
1330
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1331
|
+
stopReason: 'refusal',
|
|
1332
|
+
branchId: branch.id,
|
|
1333
|
+
branchName: branch.name,
|
|
1334
|
+
branchGeneration: branch.generation,
|
|
1335
|
+
entryIndex: 0,
|
|
1336
|
+
entryCount: 1,
|
|
1337
|
+
role: 'assistant',
|
|
1338
|
+
kind: 'assistant_output',
|
|
1339
|
+
});
|
|
1340
|
+
const shards = shardIngressContent(fixture.strategy, 'assistant', [{ type: 'text', text: longShardText('missing-shard-case') }]);
|
|
1341
|
+
assert.ok(shards.shards.length > 2);
|
|
1342
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[0].content, metadata, {
|
|
1343
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1344
|
+
shardIndex: 0,
|
|
1345
|
+
});
|
|
1346
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[2].content, metadata, {
|
|
1347
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1348
|
+
shardIndex: 2,
|
|
1349
|
+
});
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
name: 'duplicate shard index',
|
|
1354
|
+
expectedReason: 'primary_summary_settlement_shard_group_invalid_on_restart',
|
|
1355
|
+
persist: (fixture, requestId) => {
|
|
1356
|
+
const branch = fixture.agent.getCurrentBranchGeneration();
|
|
1357
|
+
const metadata = settlementMetadata({
|
|
1358
|
+
requestId,
|
|
1359
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1360
|
+
stopReason: 'refusal',
|
|
1361
|
+
branchId: branch.id,
|
|
1362
|
+
branchName: branch.name,
|
|
1363
|
+
branchGeneration: branch.generation,
|
|
1364
|
+
entryIndex: 0,
|
|
1365
|
+
entryCount: 1,
|
|
1366
|
+
role: 'assistant',
|
|
1367
|
+
kind: 'assistant_output',
|
|
1368
|
+
});
|
|
1369
|
+
const shards = shardIngressContent(fixture.strategy, 'assistant', [{ type: 'text', text: longShardText('duplicate-shard-case') }]);
|
|
1370
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[0].content, metadata, {
|
|
1371
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1372
|
+
shardIndex: 0,
|
|
1373
|
+
});
|
|
1374
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[1].content, metadata, {
|
|
1375
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1376
|
+
shardIndex: 0,
|
|
1377
|
+
});
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
name: 'noncontiguous interleaved shard group',
|
|
1382
|
+
expectedReason: 'primary_summary_settlement_shard_group_invalid_on_restart',
|
|
1383
|
+
persist: (fixture, requestId) => {
|
|
1384
|
+
const branch = fixture.agent.getCurrentBranchGeneration();
|
|
1385
|
+
const metadata = settlementMetadata({
|
|
1386
|
+
requestId,
|
|
1387
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1388
|
+
stopReason: 'refusal',
|
|
1389
|
+
branchId: branch.id,
|
|
1390
|
+
branchName: branch.name,
|
|
1391
|
+
branchGeneration: branch.generation,
|
|
1392
|
+
entryIndex: 0,
|
|
1393
|
+
entryCount: 1,
|
|
1394
|
+
role: 'assistant',
|
|
1395
|
+
kind: 'assistant_output',
|
|
1396
|
+
});
|
|
1397
|
+
const shards = shardIngressContent(fixture.strategy, 'assistant', [{ type: 'text', text: longShardText('interleaved-shard-case') }]);
|
|
1398
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[0].content, metadata, {
|
|
1399
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1400
|
+
shardIndex: 0,
|
|
1401
|
+
});
|
|
1402
|
+
fixture.agent.getContextManager().addMessage('User', [{ type: 'text', text: 'interleaved unrelated record' }]);
|
|
1403
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[1].content, metadata, {
|
|
1404
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1405
|
+
shardIndex: 1,
|
|
1406
|
+
});
|
|
1407
|
+
},
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
name: 'conflicting envelope',
|
|
1411
|
+
expectedReason: 'primary_summary_settlement_shard_group_invalid_on_restart',
|
|
1412
|
+
persist: (fixture, requestId) => {
|
|
1413
|
+
const branch = fixture.agent.getCurrentBranchGeneration();
|
|
1414
|
+
const metadata = settlementMetadata({
|
|
1415
|
+
requestId,
|
|
1416
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1417
|
+
stopReason: 'refusal',
|
|
1418
|
+
branchId: branch.id,
|
|
1419
|
+
branchName: branch.name,
|
|
1420
|
+
branchGeneration: branch.generation,
|
|
1421
|
+
entryIndex: 0,
|
|
1422
|
+
entryCount: 1,
|
|
1423
|
+
role: 'assistant',
|
|
1424
|
+
kind: 'assistant_output',
|
|
1425
|
+
});
|
|
1426
|
+
const conflictingMetadata = settlementMetadata({
|
|
1427
|
+
requestId,
|
|
1428
|
+
settlementId: 'conflicting:v1',
|
|
1429
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1430
|
+
stopReason: 'refusal',
|
|
1431
|
+
branchId: branch.id,
|
|
1432
|
+
branchName: branch.name,
|
|
1433
|
+
branchGeneration: branch.generation,
|
|
1434
|
+
entryIndex: 0,
|
|
1435
|
+
entryCount: 1,
|
|
1436
|
+
role: 'assistant',
|
|
1437
|
+
kind: 'assistant_output',
|
|
1438
|
+
});
|
|
1439
|
+
const shards = shardIngressContent(fixture.strategy, 'assistant', [{ type: 'text', text: longShardText('conflicting-envelope-case') }]);
|
|
1440
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[0].content, metadata, {
|
|
1441
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1442
|
+
shardIndex: 0,
|
|
1443
|
+
});
|
|
1444
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[1].content, conflictingMetadata, {
|
|
1445
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1446
|
+
shardIndex: 1,
|
|
1447
|
+
});
|
|
1448
|
+
},
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
name: 'cross participant shard',
|
|
1452
|
+
expectedReason: 'primary_summary_settlement_shard_group_invalid_on_restart',
|
|
1453
|
+
persist: (fixture, requestId) => {
|
|
1454
|
+
const branch = fixture.agent.getCurrentBranchGeneration();
|
|
1455
|
+
const metadata = settlementMetadata({
|
|
1456
|
+
requestId,
|
|
1457
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1458
|
+
stopReason: 'refusal',
|
|
1459
|
+
branchId: branch.id,
|
|
1460
|
+
branchName: branch.name,
|
|
1461
|
+
branchGeneration: branch.generation,
|
|
1462
|
+
entryIndex: 0,
|
|
1463
|
+
entryCount: 1,
|
|
1464
|
+
role: 'assistant',
|
|
1465
|
+
kind: 'assistant_output',
|
|
1466
|
+
});
|
|
1467
|
+
const shards = shardIngressContent(fixture.strategy, 'assistant', [{ type: 'text', text: longShardText('cross-participant-case') }]);
|
|
1468
|
+
appendPhysicalMessage(fixture.agent, 'assistant', shards.shards[0].content, metadata, {
|
|
1469
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1470
|
+
shardIndex: 0,
|
|
1471
|
+
});
|
|
1472
|
+
appendPhysicalMessage(fixture.agent, 'user', shards.shards[1].content, metadata, {
|
|
1473
|
+
bodyGroupId: shards.bodyGroupId,
|
|
1474
|
+
shardIndex: 1,
|
|
1475
|
+
});
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
name: 'unsharded plus sharded duplicate entry index',
|
|
1480
|
+
expectedReason: 'primary_summary_settlement_duplicate_entry_on_restart',
|
|
1481
|
+
persist: (fixture, requestId) => {
|
|
1482
|
+
const branch = fixture.agent.getCurrentBranchGeneration();
|
|
1483
|
+
const metadata = settlementMetadata({
|
|
1484
|
+
requestId,
|
|
1485
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1486
|
+
stopReason: 'refusal',
|
|
1487
|
+
branchId: branch.id,
|
|
1488
|
+
branchName: branch.name,
|
|
1489
|
+
branchGeneration: branch.generation,
|
|
1490
|
+
entryIndex: 0,
|
|
1491
|
+
entryCount: 1,
|
|
1492
|
+
role: 'assistant',
|
|
1493
|
+
kind: 'assistant_output',
|
|
1494
|
+
});
|
|
1495
|
+
fixture.agent.getContextManager().addMessage('assistant', [{ type: 'text', text: longShardText('duplicate-entry-case') }], metadata);
|
|
1496
|
+
fixture.agent.getContextManager().addMessage('assistant', [{ type: 'text', text: 'duplicate logical entry' }], metadata);
|
|
1497
|
+
},
|
|
1498
|
+
},
|
|
1499
|
+
];
|
|
1500
|
+
for (const testCase of cases) {
|
|
1501
|
+
const requestId = `invalid-layout-${testCase.name.replace(/[^a-z]+/gi, '-')}`;
|
|
1502
|
+
const path = freshPath();
|
|
1503
|
+
const first = await createFrameworkFixture({
|
|
1504
|
+
path,
|
|
1505
|
+
membrane: new ScriptedMembrane([]),
|
|
1506
|
+
strategy: shardingStrategy(),
|
|
1507
|
+
});
|
|
1508
|
+
try {
|
|
1509
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1510
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1511
|
+
requests: [manualPrimaryRecord({
|
|
1512
|
+
requestId,
|
|
1513
|
+
finalStatus: 'refusal',
|
|
1514
|
+
branchId: branch.id,
|
|
1515
|
+
branchName: branch.name,
|
|
1516
|
+
branchGeneration: branch.generation,
|
|
1517
|
+
})],
|
|
1518
|
+
});
|
|
1519
|
+
testCase.persist(first, requestId);
|
|
1520
|
+
}
|
|
1521
|
+
finally {
|
|
1522
|
+
await first.framework.stop();
|
|
1523
|
+
}
|
|
1524
|
+
const restarted = await createFrameworkFixture({
|
|
1525
|
+
path,
|
|
1526
|
+
membrane: new ScriptedMembrane([]),
|
|
1527
|
+
strategy: shardingStrategy(),
|
|
1528
|
+
});
|
|
1529
|
+
try {
|
|
1530
|
+
assert.equal(restarted.membrane.calls.length, 0, `${testCase.name}: restart must not redispatch the provider`);
|
|
1531
|
+
const current = fallbackRecords(restarted.framework).find((record) => record.requestId === requestId);
|
|
1532
|
+
assert.equal(current.finalStatus, 'held', testCase.name);
|
|
1533
|
+
assert.equal(current.fallbackHeldReason, testCase.expectedReason, testCase.name);
|
|
1534
|
+
}
|
|
1535
|
+
finally {
|
|
1536
|
+
await restarted.framework.stop();
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
it('does not let historical identical sharded content under another request satisfy the current settlement', async () => {
|
|
1541
|
+
const path = freshPath();
|
|
1542
|
+
const first = await createFrameworkFixture({
|
|
1543
|
+
path,
|
|
1544
|
+
membrane: new ScriptedMembrane([]),
|
|
1545
|
+
strategy: shardingStrategy(),
|
|
1546
|
+
});
|
|
1547
|
+
try {
|
|
1548
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1549
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1550
|
+
requests: [
|
|
1551
|
+
manualPrimaryRecord({
|
|
1552
|
+
requestId: 'historical-sharded-request',
|
|
1553
|
+
finalStatus: 'held',
|
|
1554
|
+
fallbackHeldReason: 'primary_summary_refusal_partial_output',
|
|
1555
|
+
branchId: branch.id,
|
|
1556
|
+
branchName: branch.name,
|
|
1557
|
+
branchGeneration: branch.generation,
|
|
1558
|
+
}),
|
|
1559
|
+
manualPrimaryRecord({
|
|
1560
|
+
requestId: 'current-sharded-request',
|
|
1561
|
+
finalStatus: 'refusal',
|
|
1562
|
+
branchId: branch.id,
|
|
1563
|
+
branchName: branch.name,
|
|
1564
|
+
branchGeneration: branch.generation,
|
|
1565
|
+
}),
|
|
1566
|
+
],
|
|
1567
|
+
});
|
|
1568
|
+
first.agent.getContextManager().addMessage('assistant', [{ type: 'text', text: longShardText('historical-identical-content') }], settlementMetadata({
|
|
1569
|
+
requestId: 'historical-sharded-request',
|
|
1570
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1571
|
+
stopReason: 'refusal',
|
|
1572
|
+
branchId: branch.id,
|
|
1573
|
+
branchName: branch.name,
|
|
1574
|
+
branchGeneration: branch.generation,
|
|
1575
|
+
entryIndex: 0,
|
|
1576
|
+
entryCount: 1,
|
|
1577
|
+
role: 'assistant',
|
|
1578
|
+
kind: 'assistant_output',
|
|
1579
|
+
}));
|
|
1580
|
+
assert.ok(settlementMessages(first.agent, 'historical-sharded-request').length > 1);
|
|
1581
|
+
}
|
|
1582
|
+
finally {
|
|
1583
|
+
await first.framework.stop();
|
|
1584
|
+
}
|
|
1585
|
+
const restarted = await createFrameworkFixture({
|
|
1586
|
+
path,
|
|
1587
|
+
membrane: new ScriptedMembrane([]),
|
|
1588
|
+
strategy: shardingStrategy(),
|
|
1589
|
+
});
|
|
1590
|
+
try {
|
|
1591
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1592
|
+
assert.equal(settlementMessages(restarted.agent, 'current-sharded-request').length, 0);
|
|
1593
|
+
const current = fallbackRecords(restarted.framework).find((record) => record.requestId === 'current-sharded-request');
|
|
1594
|
+
assert.equal(current.finalStatus, 'held');
|
|
1595
|
+
assert.equal(current.fallbackHeldReason, 'primary_summary_request_unresolved_on_restart');
|
|
1596
|
+
}
|
|
1597
|
+
finally {
|
|
1598
|
+
await restarted.framework.stop();
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
it('repairs exactly one truthful placeholder result per tool_use on shard 0', async () => {
|
|
1602
|
+
const requestId = 'multi-tool-use-request';
|
|
1603
|
+
const path = freshPath();
|
|
1604
|
+
const first = await createFrameworkFixture({
|
|
1605
|
+
path,
|
|
1606
|
+
membrane: new ScriptedMembrane([]),
|
|
1607
|
+
strategy: shardingStrategy(),
|
|
1608
|
+
});
|
|
1609
|
+
try {
|
|
1610
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1611
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1612
|
+
requests: [manualPrimaryRecord({
|
|
1613
|
+
requestId,
|
|
1614
|
+
finalStatus: 'refusal',
|
|
1615
|
+
branchId: branch.id,
|
|
1616
|
+
branchName: branch.name,
|
|
1617
|
+
branchGeneration: branch.generation,
|
|
1618
|
+
})],
|
|
1619
|
+
});
|
|
1620
|
+
first.agent.getContextManager().addMessage('assistant', [
|
|
1621
|
+
{ type: 'thinking', thinking: 'multi-tool reasoning', signature: 'sig-multi-tool' },
|
|
1622
|
+
{ type: 'tool_use', id: 'call-a', name: 'toolbox--echo', input: { message: 'a' } },
|
|
1623
|
+
{ type: 'tool_use', id: 'call-b', name: 'toolbox--echo', input: { message: 'b' } },
|
|
1624
|
+
{ type: 'text', text: longShardText('multi-tool-placeholder') },
|
|
1625
|
+
], settlementMetadata({
|
|
1626
|
+
requestId,
|
|
1627
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1628
|
+
stopReason: 'refusal',
|
|
1629
|
+
providerInputTokens: 40,
|
|
1630
|
+
visibleAssistantOutput: true,
|
|
1631
|
+
executedToolCalls: 0,
|
|
1632
|
+
branchId: branch.id,
|
|
1633
|
+
branchName: branch.name,
|
|
1634
|
+
branchGeneration: branch.generation,
|
|
1635
|
+
entryIndex: 0,
|
|
1636
|
+
entryCount: 2,
|
|
1637
|
+
role: 'assistant',
|
|
1638
|
+
kind: 'assistant_output',
|
|
1639
|
+
}));
|
|
1640
|
+
const shards = settlementMessages(first.agent, requestId);
|
|
1641
|
+
assert.ok(shards.length > 1);
|
|
1642
|
+
assert.equal(shards.filter((message) => message.content.some((block) => block.type === 'tool_use')).length, 1, 'all tool_use blocks must stay on shard 0');
|
|
1643
|
+
}
|
|
1644
|
+
finally {
|
|
1645
|
+
await first.framework.stop();
|
|
1646
|
+
}
|
|
1647
|
+
const restarted = await createFrameworkFixture({
|
|
1648
|
+
path,
|
|
1649
|
+
membrane: new ScriptedMembrane([]),
|
|
1650
|
+
strategy: shardingStrategy(),
|
|
1651
|
+
});
|
|
1652
|
+
try {
|
|
1653
|
+
const placeholders = settlementMessages(restarted.agent, requestId)
|
|
1654
|
+
.filter((message) => message.participant === 'user');
|
|
1655
|
+
assert.equal(placeholders.length, 1);
|
|
1656
|
+
const results = placeholders[0].content.filter((block) => block.type === 'tool_result');
|
|
1657
|
+
assert.equal(results.length, 2);
|
|
1658
|
+
assert.deepEqual(results.map((block) => block.toolUseId), ['call-a', 'call-b']);
|
|
1659
|
+
assert.ok(results.every((block) => block.content === NON_EXECUTED_TOOL_RESULT && block.isError === true));
|
|
1660
|
+
}
|
|
1661
|
+
finally {
|
|
1662
|
+
await restarted.framework.stop();
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
it('fails closed on malformed optional and numeric settlement metadata without copying bad values into durable state or logs', async () => {
|
|
1666
|
+
const oversizedStopReason = `OVERSIZED_STOP_REASON_SENTINEL_${'x'.repeat(320)}`;
|
|
1667
|
+
const cases = [
|
|
1668
|
+
{
|
|
1669
|
+
name: 'oversized stopReason',
|
|
1670
|
+
mutate: (metadata) => {
|
|
1671
|
+
(metadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY].stopReason) = oversizedStopReason;
|
|
1672
|
+
},
|
|
1673
|
+
forbidden: oversizedStopReason,
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
name: 'missing held holdReason',
|
|
1677
|
+
mutate: (metadata) => {
|
|
1678
|
+
delete metadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY].holdReason;
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
name: 'unsafe providerInputTokens',
|
|
1683
|
+
mutate: (metadata) => {
|
|
1684
|
+
(metadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY].providerInputTokens) = Number.MAX_SAFE_INTEGER + 1;
|
|
1685
|
+
},
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
name: 'negative executedToolCalls',
|
|
1689
|
+
mutate: (metadata) => {
|
|
1690
|
+
(metadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY].executedToolCalls) = -1;
|
|
1691
|
+
},
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
name: 'negative branch generation',
|
|
1695
|
+
mutate: (metadata) => {
|
|
1696
|
+
(metadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY].branch.generation) = -1;
|
|
1697
|
+
},
|
|
1698
|
+
},
|
|
1699
|
+
];
|
|
1700
|
+
for (const testCase of cases) {
|
|
1701
|
+
const requestId = `malformed-metadata-${testCase.name.replace(/[^a-z]+/gi, '-')}`;
|
|
1702
|
+
const path = freshPath();
|
|
1703
|
+
const first = await createFrameworkFixture({
|
|
1704
|
+
path,
|
|
1705
|
+
membrane: new ScriptedMembrane([]),
|
|
1706
|
+
strategy: shardingStrategy(),
|
|
1707
|
+
});
|
|
1708
|
+
try {
|
|
1709
|
+
const branch = first.agent.getCurrentBranchGeneration();
|
|
1710
|
+
first.framework.getStore().setStateJson(FALLBACK_STATE_ID, {
|
|
1711
|
+
requests: [manualPrimaryRecord({
|
|
1712
|
+
requestId,
|
|
1713
|
+
finalStatus: 'refusal',
|
|
1714
|
+
branchId: branch.id,
|
|
1715
|
+
branchName: branch.name,
|
|
1716
|
+
branchGeneration: branch.generation,
|
|
1717
|
+
})],
|
|
1718
|
+
});
|
|
1719
|
+
const metadata = settlementMetadata({
|
|
1720
|
+
requestId,
|
|
1721
|
+
holdReason: 'primary_summary_refusal_partial_output',
|
|
1722
|
+
stopReason: 'refusal',
|
|
1723
|
+
providerInputTokens: 40,
|
|
1724
|
+
visibleAssistantOutput: true,
|
|
1725
|
+
executedToolCalls: 0,
|
|
1726
|
+
branchId: branch.id,
|
|
1727
|
+
branchName: branch.name,
|
|
1728
|
+
branchGeneration: branch.generation,
|
|
1729
|
+
entryIndex: 0,
|
|
1730
|
+
entryCount: 1,
|
|
1731
|
+
role: 'assistant',
|
|
1732
|
+
kind: 'assistant_output',
|
|
1733
|
+
});
|
|
1734
|
+
testCase.mutate(metadata);
|
|
1735
|
+
first.agent.getContextManager().addMessage('assistant', [{ type: 'text', text: longShardText(`malformed-${testCase.name}`) }], metadata);
|
|
1736
|
+
}
|
|
1737
|
+
finally {
|
|
1738
|
+
await first.framework.stop();
|
|
1739
|
+
}
|
|
1740
|
+
const restarted = await createFrameworkFixture({
|
|
1741
|
+
path,
|
|
1742
|
+
membrane: new ScriptedMembrane([]),
|
|
1743
|
+
strategy: shardingStrategy(),
|
|
1744
|
+
});
|
|
1745
|
+
try {
|
|
1746
|
+
const current = fallbackRecords(restarted.framework).find((record) => record.requestId === requestId);
|
|
1747
|
+
assert.equal(current.finalStatus, 'held', testCase.name);
|
|
1748
|
+
assert.equal(current.fallbackHeldReason, 'primary_summary_settlement_metadata_invalid_on_restart', testCase.name);
|
|
1749
|
+
assert.equal(current.stopReason, 'refusal', testCase.name);
|
|
1750
|
+
assert.equal(current.providerInputTokens, 40, testCase.name);
|
|
1751
|
+
assert.equal(current.executedToolCalls, 0, testCase.name);
|
|
1752
|
+
if (testCase.forbidden) {
|
|
1753
|
+
assert.equal(jsonContains(fallbackState(restarted.framework), testCase.forbidden), false, testCase.name);
|
|
1754
|
+
assert.equal(jsonContains(restarted.framework.healthSnapshot(), testCase.forbidden), false, testCase.name);
|
|
1755
|
+
assert.equal(jsonContains(restarted.framework.queryInferenceLogs({ limit: 25 }), testCase.forbidden), false, testCase.name);
|
|
1756
|
+
assert.equal(jsonContains(restarted.framework.queryProcessLogs({ limit: 25 }), testCase.forbidden), false, testCase.name);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
finally {
|
|
1760
|
+
await restarted.framework.stop();
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
describe('crash phases', () => {
|
|
1765
|
+
async function setupTextRetryScenario() {
|
|
1766
|
+
const membrane = new ScriptedMembrane([
|
|
1767
|
+
[refusalResponse()],
|
|
1768
|
+
[outputResponse()],
|
|
1769
|
+
]);
|
|
1770
|
+
const fixture = await createFrameworkFixture({ membrane });
|
|
1771
|
+
const { framework, agent, strategy } = fixture;
|
|
1772
|
+
const a = addSourcePair(agent, 'A');
|
|
1773
|
+
seedSummary(strategy, 'L1-A', a);
|
|
1774
|
+
addLatestPrompt(agent, 'baseline');
|
|
1775
|
+
await persistHealthyBaseline(framework, agent);
|
|
1776
|
+
const b = addSourcePair(agent, 'B');
|
|
1777
|
+
seedSummary(strategy, 'L1-B', b);
|
|
1778
|
+
addLatestPrompt(agent, 'current');
|
|
1779
|
+
return fixture;
|
|
1780
|
+
}
|
|
1781
|
+
async function setupToolRetryScenario() {
|
|
1782
|
+
const module = new EchoModule(() => true);
|
|
1783
|
+
const membrane = new ScriptedMembrane([
|
|
1784
|
+
[refusalResponse()],
|
|
1785
|
+
[toolRoundResponse(), outputResponse()],
|
|
1786
|
+
]);
|
|
1787
|
+
const fixture = await createFrameworkFixture({ membrane, modules: [module] });
|
|
1788
|
+
const { framework, agent, strategy } = fixture;
|
|
1789
|
+
const a = addSourcePair(agent, 'A');
|
|
1790
|
+
seedSummary(strategy, 'L1-A', a);
|
|
1791
|
+
addLatestPrompt(agent, 'baseline');
|
|
1792
|
+
await persistHealthyBaseline(framework, agent);
|
|
1793
|
+
const b = addSourcePair(agent, 'B');
|
|
1794
|
+
seedSummary(strategy, 'L1-B', b);
|
|
1795
|
+
addLatestPrompt(agent, 'current');
|
|
1796
|
+
return { ...fixture, module };
|
|
1797
|
+
}
|
|
1798
|
+
it('holds on restart if the process crashes after the provider response is obtained', async () => {
|
|
1799
|
+
const { path, framework, agent } = await setupTextRetryScenario();
|
|
1800
|
+
try {
|
|
1801
|
+
const manager = agent.getContextManager();
|
|
1802
|
+
const original = manager.quarantinePrimarySummaryForPrimaryLane.bind(manager);
|
|
1803
|
+
manager.quarantinePrimarySummaryForPrimaryLane = async (...args) => {
|
|
1804
|
+
throw new Error('crash-before-quarantine');
|
|
1805
|
+
};
|
|
1806
|
+
await enqueueAndDrain(framework);
|
|
1807
|
+
assert.equal(countMessagesContaining(manager.getAllMessages(), RETRY_OUTPUT_SENTINEL), 0);
|
|
1808
|
+
manager.quarantinePrimarySummaryForPrimaryLane = original;
|
|
1809
|
+
}
|
|
1810
|
+
finally {
|
|
1811
|
+
await framework.stop();
|
|
1812
|
+
}
|
|
1813
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1814
|
+
try {
|
|
1815
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1816
|
+
const original = latestPrimaryRecord(restarted.framework);
|
|
1817
|
+
assert.equal(original.fallbackStatus, 'held');
|
|
1818
|
+
}
|
|
1819
|
+
finally {
|
|
1820
|
+
await restarted.framework.stop();
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
it('holds on restart if the process crashes after quarantine is persisted', async () => {
|
|
1824
|
+
const { path, framework, agent } = await setupTextRetryScenario();
|
|
1825
|
+
try {
|
|
1826
|
+
const manager = agent.getContextManager();
|
|
1827
|
+
const original = manager.quarantinePrimarySummaryForPrimaryLane.bind(manager);
|
|
1828
|
+
manager.quarantinePrimarySummaryForPrimaryLane = async (...args) => {
|
|
1829
|
+
await original(...args);
|
|
1830
|
+
throw new Error('crash-after-quarantine');
|
|
1831
|
+
};
|
|
1832
|
+
await enqueueAndDrain(framework);
|
|
1833
|
+
manager.quarantinePrimarySummaryForPrimaryLane = original;
|
|
1834
|
+
}
|
|
1835
|
+
finally {
|
|
1836
|
+
await framework.stop();
|
|
1837
|
+
}
|
|
1838
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1839
|
+
try {
|
|
1840
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1841
|
+
assert.equal(jsonContains(primarySummaryQuarantineState(restarted.framework, 'assistant'), 'L1-B'), true);
|
|
1842
|
+
const messages = restarted.agent.getContextManager().getAllMessages();
|
|
1843
|
+
assert.equal(countMessagesContaining(messages, RETRY_OUTPUT_SENTINEL), 0);
|
|
1844
|
+
}
|
|
1845
|
+
finally {
|
|
1846
|
+
await restarted.framework.stop();
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
it('holds on restart if the process crashes after assistant output is persisted', async () => {
|
|
1850
|
+
const { path, framework, agent } = await setupTextRetryScenario();
|
|
1851
|
+
try {
|
|
1852
|
+
const manager = agent.getContextManager();
|
|
1853
|
+
const original = manager.addMessage.bind(manager);
|
|
1854
|
+
manager.addMessage = (...args) => {
|
|
1855
|
+
const messageId = original(...args);
|
|
1856
|
+
if (typeof args[0] === 'string' && args[0] === agent.name && jsonContains(args[1], RETRY_OUTPUT_SENTINEL)) {
|
|
1857
|
+
throw new Error('crash-after-output');
|
|
1858
|
+
}
|
|
1859
|
+
return messageId;
|
|
1860
|
+
};
|
|
1861
|
+
await enqueueAndDrain(framework);
|
|
1862
|
+
manager.addMessage = original;
|
|
1863
|
+
}
|
|
1864
|
+
finally {
|
|
1865
|
+
await framework.stop();
|
|
1866
|
+
}
|
|
1867
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1868
|
+
try {
|
|
1869
|
+
const messages = restarted.agent.getContextManager().getAllMessages();
|
|
1870
|
+
assert.equal(countMessagesContaining(messages, RETRY_OUTPUT_SENTINEL), 1);
|
|
1871
|
+
const original = latestPrimaryRecord(restarted.framework);
|
|
1872
|
+
assert.equal(original.fallbackStatus, 'held');
|
|
1873
|
+
}
|
|
1874
|
+
finally {
|
|
1875
|
+
await restarted.framework.stop();
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
it('preserves success after a post-success crash and never resends on restart', async () => {
|
|
1879
|
+
const { path, framework } = await setupTextRetryScenario();
|
|
1880
|
+
try {
|
|
1881
|
+
const registry = framework.moduleRegistry;
|
|
1882
|
+
const original = registry.dispatchSpeech.bind(registry);
|
|
1883
|
+
registry.dispatchSpeech = async (...args) => {
|
|
1884
|
+
throw new Error('crash-after-success-markers');
|
|
1885
|
+
};
|
|
1886
|
+
await enqueueAndDrain(framework);
|
|
1887
|
+
registry.dispatchSpeech = original;
|
|
1888
|
+
}
|
|
1889
|
+
finally {
|
|
1890
|
+
await framework.stop();
|
|
1891
|
+
}
|
|
1892
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1893
|
+
try {
|
|
1894
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1895
|
+
const retry = fallbackRetryRecords(restarted.framework).at(-1);
|
|
1896
|
+
const original = latestPrimaryRecord(restarted.framework);
|
|
1897
|
+
assert.equal(retry.finalStatus, 'success');
|
|
1898
|
+
assert.equal(original.fallbackStatus, 'success');
|
|
1899
|
+
}
|
|
1900
|
+
finally {
|
|
1901
|
+
await restarted.framework.stop();
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
it('holds without executing a tool if the process crashes before tool dispatch', async () => {
|
|
1905
|
+
const { path, framework, module } = await setupToolRetryScenario();
|
|
1906
|
+
try {
|
|
1907
|
+
const original = framework.dispatchToolCall.bind(framework);
|
|
1908
|
+
framework.dispatchToolCall = (_agentName, _call) => {
|
|
1909
|
+
throw new Error('crash-before-tool-dispatch');
|
|
1910
|
+
};
|
|
1911
|
+
await enqueueAndDrain(framework);
|
|
1912
|
+
assert.equal(module.calls.length, 0);
|
|
1913
|
+
}
|
|
1914
|
+
finally {
|
|
1915
|
+
await framework.stop();
|
|
1916
|
+
}
|
|
1917
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1918
|
+
try {
|
|
1919
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1920
|
+
const original = latestPrimaryRecord(restarted.framework);
|
|
1921
|
+
assert.equal(original.fallbackStatus, 'held');
|
|
1922
|
+
}
|
|
1923
|
+
finally {
|
|
1924
|
+
await restarted.framework.stop();
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
it('holds without executing a tool if the process crashes after the tool dispatch record is persisted', async () => {
|
|
1928
|
+
const { path, framework, module } = await setupToolRetryScenario();
|
|
1929
|
+
try {
|
|
1930
|
+
const original = framework.recordPrimarySummaryRetryToolDispatch.bind(framework);
|
|
1931
|
+
framework.recordPrimarySummaryRetryToolDispatch = async (...args) => {
|
|
1932
|
+
await original(...args);
|
|
1933
|
+
throw new Error('crash-after-tool-dispatch-record');
|
|
1934
|
+
};
|
|
1935
|
+
await enqueueAndDrain(framework);
|
|
1936
|
+
assert.equal(module.calls.length, 0);
|
|
1937
|
+
}
|
|
1938
|
+
finally {
|
|
1939
|
+
await framework.stop();
|
|
1940
|
+
}
|
|
1941
|
+
const restarted = await createFrameworkFixture({ path, membrane: new ScriptedMembrane([]) });
|
|
1942
|
+
try {
|
|
1943
|
+
const original = latestPrimaryRecord(restarted.framework);
|
|
1944
|
+
const retry = fallbackRetryRecords(restarted.framework).at(-1);
|
|
1945
|
+
assert.equal(restarted.membrane.calls.length, 0);
|
|
1946
|
+
assert.equal(original.fallbackStatus, 'held');
|
|
1947
|
+
assert.equal(jsonContains(retry, 'call-1'), false, 'retry record should only keep hashed dispatch metadata');
|
|
1948
|
+
}
|
|
1949
|
+
finally {
|
|
1950
|
+
await restarted.framework.stop();
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
});
|
|
1954
|
+
});
|
|
1955
|
+
//# sourceMappingURL=primary-summary-refusal-fallback-integration.test.js.map
|