@animalabs/agent-framework 0.6.7 → 0.6.10

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.
Files changed (54) hide show
  1. package/dist/src/agent.d.ts +5 -34
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +62 -137
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +14 -56
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +192 -1742
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/modules/workspace/index.d.ts +10 -0
  10. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  11. package/dist/src/modules/workspace/index.js +48 -2
  12. package/dist/src/modules/workspace/index.js.map +1 -1
  13. package/dist/src/modules/workspace/types.d.ts +2 -0
  14. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  15. package/dist/src/modules/workspace/types.js.map +1 -1
  16. package/dist/src/modules/workspace/watcher.d.ts +17 -0
  17. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  18. package/dist/src/modules/workspace/watcher.js +143 -7
  19. package/dist/src/modules/workspace/watcher.js.map +1 -1
  20. package/dist/src/types/agent.d.ts +15 -12
  21. package/dist/src/types/agent.d.ts.map +1 -1
  22. package/dist/test/helpers/mock-membrane.js +1 -1
  23. package/dist/test/helpers/mock-membrane.js.map +1 -1
  24. package/dist/test/mcpl-awareness-barrier.test.js +1 -228
  25. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  26. package/dist/test/refusal-single-call.test.d.ts +2 -0
  27. package/dist/test/refusal-single-call.test.d.ts.map +1 -0
  28. package/dist/test/refusal-single-call.test.js +177 -0
  29. package/dist/test/refusal-single-call.test.js.map +1 -0
  30. package/dist/test/trunk-channel-routing.test.js +5 -1
  31. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  32. package/dist/test/workspace-watcher.test.js +161 -9
  33. package/dist/test/workspace-watcher.test.js.map +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +4 -4
  36. package/src/agent.ts +67 -226
  37. package/src/framework.ts +226 -2394
  38. package/src/modules/workspace/index.ts +47 -2
  39. package/src/modules/workspace/types.ts +2 -0
  40. package/src/modules/workspace/watcher.ts +160 -7
  41. package/src/types/agent.ts +17 -12
  42. package/dist/src/canonical-json.d.ts +0 -2
  43. package/dist/src/canonical-json.d.ts.map +0 -1
  44. package/dist/src/canonical-json.js +0 -23
  45. package/dist/src/canonical-json.js.map +0 -1
  46. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +0 -2
  47. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +0 -1
  48. package/dist/test/primary-summary-refusal-fallback-integration.test.js +0 -1955
  49. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +0 -1
  50. package/dist/test/primary-summary-refusal-fallback.test.d.ts +0 -2
  51. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +0 -1
  52. package/dist/test/primary-summary-refusal-fallback.test.js +0 -753
  53. package/dist/test/primary-summary-refusal-fallback.test.js.map +0 -1
  54. package/src/canonical-json.ts +0 -22
@@ -1,753 +0,0 @@
1
- import { afterEach, describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdtempSync, rmSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
- import { AgentFramework } from '../src/index.js';
7
- import { MockYieldingStream, createMockResponse } from './helpers/mock-membrane.js';
8
- const FALLBACK_STATE_ID = 'framework/primary-summary-fallback';
9
- afterEach(() => {
10
- // Individual tests clean their own tempdirs; this hook exists so a failed
11
- // assertion does not leave the last path behind if cleanup was skipped.
12
- });
13
- function identity(id) {
14
- return {
15
- id,
16
- contentHash: `content-${id}`,
17
- carrierHash: `carrier-${id}`,
18
- sourceLeafHash: `leaf-${id}`,
19
- };
20
- }
21
- function projection(branchId, generation, ids) {
22
- return {
23
- namespace: 'default',
24
- branch: { id: branchId, name: 'main', generation },
25
- selectedSummaries: ids.map((id, index) => ({
26
- identity: identity(id),
27
- level: 1,
28
- orderedSourceIds: [`src-${id}-1`, `src-${id}-2`],
29
- renderedAs: 'summary_pair',
30
- pairRange: { start: index * 2, end: index * 2 + 1 },
31
- })),
32
- };
33
- }
34
- function request(system, text) {
35
- return {
36
- system,
37
- messages: [{ participant: 'user', content: [{ type: 'text', text }] }],
38
- config: { model: 'test-model', maxTokens: 256 },
39
- assistantParticipant: 'assistant',
40
- promptCaching: true,
41
- cacheTtl: '1h',
42
- };
43
- }
44
- function artifacts(systemTag, summaryIds, branchId = 'branch-a', generation = 1) {
45
- return {
46
- compileResult: {
47
- messages: [
48
- { participant: 'Context Manager', content: [{ type: 'text', text: 'What do you remember from earlier?' }] },
49
- { participant: 'assistant', content: [{ type: 'text', text: `summary ${summaryIds.join(',')}` }] },
50
- { participant: 'user', content: [{ type: 'text', text: 'latest input' }] },
51
- ],
52
- systemInjections: [],
53
- primarySummaryProjection: projection(branchId, generation, summaryIds),
54
- },
55
- contract: {
56
- systemHash: `system-${systemTag}`,
57
- modelConfigHash: `model-${systemTag}`,
58
- toolContractHash: `tools-${systemTag}`,
59
- },
60
- };
61
- }
62
- function refusalResponse(content = []) {
63
- return {
64
- ...createMockResponse(content, 'refusal'),
65
- content,
66
- stopReason: 'refusal',
67
- rawAssistantText: '',
68
- toolCalls: [],
69
- usage: { inputTokens: 40, outputTokens: 0 },
70
- details: {
71
- usage: { inputTokens: 40, outputTokens: 0 },
72
- stop: { reason: 'refusal', wasTruncated: false },
73
- },
74
- raw: {
75
- response: {
76
- stop_details: { category: 'reasoning_extraction' },
77
- },
78
- },
79
- };
80
- }
81
- function toolUseBlock(id = 'tool-1', message = 'hello') {
82
- return {
83
- type: 'tool_use',
84
- id,
85
- name: 'echo',
86
- input: { message },
87
- };
88
- }
89
- class ScriptedMembrane {
90
- responsesByCall;
91
- calls = [];
92
- constructor(responsesByCall) {
93
- this.responsesByCall = responsesByCall;
94
- }
95
- complete() {
96
- throw new Error('complete() should not be used by the primary streaming fallback path');
97
- }
98
- streamYielding(request) {
99
- this.calls.push(structuredClone(request));
100
- const next = this.responsesByCall.shift();
101
- if (!next)
102
- return new MockYieldingStream([createMockResponse([{ type: 'text', text: 'default' }])]);
103
- return Array.isArray(next) ? new MockYieldingStream(next) : next;
104
- }
105
- asMembrane() {
106
- return this;
107
- }
108
- }
109
- class LazyStream {
110
- started = false;
111
- onStart;
112
- isWaitingForTools = false;
113
- pendingToolCallIds = [];
114
- toolDepth = 0;
115
- provideToolResults() {
116
- throw new Error('LazyStream does not support tool rounds');
117
- }
118
- cancel() {
119
- this.started = true;
120
- }
121
- async *[Symbol.asyncIterator]() {
122
- this.onStart?.();
123
- this.started = true;
124
- yield { type: 'complete', response: createMockResponse([], 'refusal') };
125
- }
126
- }
127
- async function createFramework(mem, refusalHandling, modules = []) {
128
- const dir = mkdtempSync(join(tmpdir(), 'af-primary-summary-fallback-'));
129
- const framework = await AgentFramework.create({
130
- storePath: join(dir, 'store.chronicle'),
131
- membrane: mem.asMembrane(),
132
- agents: [{
133
- name: 'assistant',
134
- model: 'test-model',
135
- systemPrompt: 'system',
136
- refusalHandling,
137
- }],
138
- modules,
139
- });
140
- const agent = framework.getAgent('assistant');
141
- return { dir, framework, agent };
142
- }
143
- function branchInfo(id = 'branch-a', generation = 1) {
144
- return {
145
- id,
146
- name: 'main',
147
- head: 1,
148
- generation,
149
- created: new Date('2026-07-17T00:00:00.000Z'),
150
- };
151
- }
152
- function fallbackState(framework) {
153
- const raw = framework.getStore().getStateJson(FALLBACK_STATE_ID);
154
- return raw && typeof raw === 'object'
155
- ? raw
156
- : { requests: [] };
157
- }
158
- function countMessagesContaining(agent, needle) {
159
- const messages = agent.getContextManager().getAllMessages();
160
- return messages.filter((message) => JSON.stringify(message.content).includes(needle)).length;
161
- }
162
- async function persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts) {
163
- const build = framework.buildPrimarySummaryRequestRecord.bind(framework);
164
- const persist = framework.persistPrimarySummaryRequestRecord.bind(framework);
165
- const record = build(agent, 'baseline-request', 'primary', baselineRequest, baselineArtifacts, branchInfo(baselineArtifacts.compileResult.primarySummaryProjection.branch.id, baselineArtifacts.compileResult.primarySummaryProjection.branch.generation), 'end_turn', true, 0, 30, 'success');
166
- await persist(record);
167
- }
168
- async function waitForStream(framework, agentName) {
169
- const handle = framework.activeStreams.get(agentName);
170
- if (handle)
171
- await handle;
172
- }
173
- describe('primary summary refusal fallback', () => {
174
- it('feature disabled preserves the existing one-call refusal path', async () => {
175
- const membrane = new ScriptedMembrane([[refusalResponse()]]);
176
- const { dir, framework, agent } = await createFramework(membrane);
177
- try {
178
- const preparedRequest = request('system', 'hello');
179
- const preparedArtifacts = artifacts('disabled', ['L1-1']);
180
- agent.prepareActivationRequest = async () => ({ request: preparedRequest, artifacts: preparedArtifacts });
181
- agent.getCurrentBranchGeneration =
182
- () => branchInfo();
183
- await framework.startAgentStream(agent, {
184
- agentName: 'assistant',
185
- reason: 'test',
186
- source: 'test',
187
- timestamp: Date.now(),
188
- });
189
- await waitForStream(framework, 'assistant');
190
- assert.equal(membrane.calls.length, 1);
191
- assert.equal(framework.pendingRequests.length, 0);
192
- assert.deepEqual(fallbackState(framework).requests, []);
193
- }
194
- finally {
195
- await framework.stop();
196
- rmSync(dir, { recursive: true, force: true });
197
- }
198
- });
199
- it('zero-output refusal with one newly admitted summary triggers exactly one expanded retry', async () => {
200
- const membrane = new ScriptedMembrane([
201
- [refusalResponse()],
202
- [[createMockResponse([{ type: 'text', text: 'Recovered' }])][0]],
203
- ]);
204
- const { dir, framework, agent } = await createFramework(membrane, {
205
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
206
- });
207
- try {
208
- const baselineRequest = request('system', 'baseline');
209
- const baselineArtifacts = artifacts('shared', ['L1-1']);
210
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
211
- const currentRequest = request('system', 'latest input');
212
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2']);
213
- const retryRequest = request('system', 'expanded raw source');
214
- const retryArtifacts = {
215
- ...currentArtifacts,
216
- compileResult: {
217
- ...currentArtifacts.compileResult,
218
- primarySummaryProjection: {
219
- ...currentArtifacts.compileResult.primarySummaryProjection,
220
- selectedSummaries: currentArtifacts.compileResult.primarySummaryProjection.selectedSummaries.map((selection) => ({
221
- ...selection,
222
- renderedAs: selection.identity.id === 'L1-2' ? 'raw_expansion' : selection.renderedAs,
223
- })),
224
- },
225
- },
226
- };
227
- const quarantineCalls = [];
228
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
229
- agent.buildPrimarySummaryRawExpansionRequest = (_artifacts, summaries) => {
230
- assert.deepEqual(summaries.map((summary) => summary.id), ['L1-2']);
231
- return { request: retryRequest, artifacts: retryArtifacts };
232
- };
233
- agent.getCurrentBranchGeneration =
234
- () => branchInfo();
235
- const cm = agent.getContextManager();
236
- cm.quarantinePrimarySummaryForPrimaryLane = async (_contract, summaries) => {
237
- quarantineCalls.push(summaries);
238
- };
239
- const start = framework.startAgentStream.bind(framework);
240
- const trigger = {
241
- agentName: 'assistant',
242
- reason: 'test',
243
- source: 'test',
244
- timestamp: Date.now(),
245
- };
246
- await start(agent, trigger);
247
- await waitForStream(framework, 'assistant');
248
- const queuedRetry = framework.pendingRequests.shift();
249
- assert.ok(queuedRetry, 'a single fallback retry should be queued');
250
- assert.equal(membrane.calls.length, 1);
251
- await start(agent, queuedRetry);
252
- await waitForStream(framework, 'assistant');
253
- assert.equal(membrane.calls.length, 2);
254
- assert.equal(membrane.calls[1].messages[0].content[0].type, 'text');
255
- assert.equal(membrane.calls[1].messages[0].content[0].text, 'expanded raw source');
256
- assert.equal(quarantineCalls.length, 1);
257
- const records = fallbackState(framework).requests;
258
- const original = records.find((record) => record.requestId !== 'baseline-request');
259
- assert.equal(original.fallbackStatus, 'success');
260
- assert.equal(records.filter((record) => record.dispatchKind === 'primary_summary_fallback_retry').length, 1);
261
- }
262
- finally {
263
- await framework.stop();
264
- rmSync(dir, { recursive: true, force: true });
265
- }
266
- });
267
- it('persists initial refusal partial text once, holds, and never retries', async () => {
268
- const refusalText = 'initial refusal partial output';
269
- const membrane = new ScriptedMembrane([[refusalResponse([{ type: 'text', text: refusalText }])]]);
270
- const { dir, framework, agent } = await createFramework(membrane, {
271
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
272
- });
273
- try {
274
- const currentRequest = request('system', 'latest input');
275
- const currentArtifacts = artifacts('shared', ['L1-1']);
276
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
277
- agent.getCurrentBranchGeneration =
278
- () => branchInfo();
279
- await framework.startAgentStream(agent, {
280
- agentName: 'assistant',
281
- reason: 'test',
282
- source: 'test',
283
- timestamp: Date.now(),
284
- });
285
- await waitForStream(framework, 'assistant');
286
- assert.equal(membrane.calls.length, 1);
287
- assert.equal(framework.pendingRequests.length, 0);
288
- assert.equal(countMessagesContaining(agent, refusalText), 1);
289
- const record = fallbackState(framework).requests[0];
290
- assert.equal(record.finalStatus, 'held');
291
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
292
- }
293
- finally {
294
- await framework.stop();
295
- rmSync(dir, { recursive: true, force: true });
296
- }
297
- });
298
- it('persists retry refusal partial text once, holds, and stops at two provider calls', async () => {
299
- const refusalText = 'retry refusal partial output';
300
- const membrane = new ScriptedMembrane([
301
- [refusalResponse()],
302
- [refusalResponse([{ type: 'text', text: refusalText }])],
303
- ]);
304
- const { dir, framework, agent } = await createFramework(membrane, {
305
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
306
- });
307
- try {
308
- const baselineRequest = request('system', 'baseline');
309
- const baselineArtifacts = artifacts('shared', ['L1-1']);
310
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
311
- const currentRequest = request('system', 'latest input');
312
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2']);
313
- const retryRequest = request('system', 'expanded raw source');
314
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
315
- agent.buildPrimarySummaryRawExpansionRequest = () => ({ request: retryRequest, artifacts: currentArtifacts });
316
- agent.getCurrentBranchGeneration =
317
- () => branchInfo();
318
- const start = framework.startAgentStream.bind(framework);
319
- await start(agent, {
320
- agentName: 'assistant',
321
- reason: 'test',
322
- source: 'test',
323
- timestamp: Date.now(),
324
- });
325
- await waitForStream(framework, 'assistant');
326
- const queuedRetry = framework.pendingRequests.shift();
327
- assert.ok(queuedRetry);
328
- await start(agent, queuedRetry);
329
- await waitForStream(framework, 'assistant');
330
- assert.equal(membrane.calls.length, 2);
331
- assert.equal(framework.pendingRequests.length, 0);
332
- assert.equal(countMessagesContaining(agent, refusalText), 1);
333
- const records = fallbackState(framework).requests;
334
- const original = records.find((record) => record.requestId !== 'baseline-request' && record.dispatchKind === 'primary');
335
- const retry = records.find((record) => record.dispatchKind === 'primary_summary_fallback_retry');
336
- assert.equal(original.fallbackStatus, 'held');
337
- assert.equal(original.fallbackHeldReason, 'primary_summary_fallback_retry_partial_output');
338
- assert.equal(retry.finalStatus, 'held');
339
- assert.equal(retry.fallbackHeldReason, 'primary_summary_fallback_retry_partial_output');
340
- }
341
- finally {
342
- await framework.stop();
343
- rmSync(dir, { recursive: true, force: true });
344
- }
345
- });
346
- it('holds instead of retrying when no compatible healthy baseline exists', async () => {
347
- const membrane = new ScriptedMembrane([[refusalResponse()]]);
348
- const { dir, framework, agent } = await createFramework(membrane, {
349
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
350
- });
351
- try {
352
- const currentRequest = request('system', 'latest input');
353
- const currentArtifacts = artifacts('shared', ['L1-1']);
354
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
355
- agent.getCurrentBranchGeneration =
356
- () => branchInfo();
357
- await framework.startAgentStream(agent, {
358
- agentName: 'assistant',
359
- reason: 'test',
360
- source: 'test',
361
- timestamp: Date.now(),
362
- });
363
- await waitForStream(framework, 'assistant');
364
- assert.equal(membrane.calls.length, 1);
365
- assert.equal(framework.pendingRequests.length, 0);
366
- const held = fallbackState(framework).requests[0];
367
- assert.equal(held.finalStatus, 'held');
368
- assert.equal(held.fallbackHeldReason, 'primary_summary_refusal_no_compatible_healthy_baseline');
369
- assert.deepEqual(framework.healthSnapshot().primarySummaryFallback, { requests: 1, pendingDispatches: 0, unresolvedIntents: 0, held: 1 });
370
- }
371
- finally {
372
- await framework.stop();
373
- rmSync(dir, { recursive: true, force: true });
374
- }
375
- });
376
- it('holds instead of retrying when no newly admitted summaries exist', async () => {
377
- const membrane = new ScriptedMembrane([[refusalResponse()]]);
378
- const { dir, framework, agent } = await createFramework(membrane, {
379
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
380
- });
381
- try {
382
- const baselineRequest = request('system', 'baseline');
383
- const baselineArtifacts = artifacts('shared', ['L1-1']);
384
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
385
- const currentRequest = request('system', 'latest input');
386
- const currentArtifacts = artifacts('shared', ['L1-1']);
387
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
388
- agent.getCurrentBranchGeneration =
389
- () => branchInfo();
390
- await framework.startAgentStream(agent, {
391
- agentName: 'assistant',
392
- reason: 'test',
393
- source: 'test',
394
- timestamp: Date.now(),
395
- });
396
- await waitForStream(framework, 'assistant');
397
- const record = fallbackState(framework).requests.find((entry) => entry.requestId !== 'baseline-request');
398
- assert.equal(record.finalStatus, 'held');
399
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_no_newly_admitted_candidates');
400
- assert.equal(framework.pendingRequests.length, 0);
401
- }
402
- finally {
403
- await framework.stop();
404
- rmSync(dir, { recursive: true, force: true });
405
- }
406
- });
407
- it('holds instead of retrying when newly admitted summaries exceed the configured maximum', async () => {
408
- const membrane = new ScriptedMembrane([[refusalResponse()]]);
409
- const { dir, framework, agent } = await createFramework(membrane, {
410
- primarySummaryFallback: { enabled: true, maxNewSummaries: 1, requestBudgetTokens: 4096 },
411
- });
412
- try {
413
- const baselineRequest = request('system', 'baseline');
414
- const baselineArtifacts = artifacts('shared', ['L1-1']);
415
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
416
- const currentRequest = request('system', 'latest input');
417
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2', 'L1-3']);
418
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
419
- agent.getCurrentBranchGeneration =
420
- () => branchInfo();
421
- await framework.startAgentStream(agent, {
422
- agentName: 'assistant',
423
- reason: 'test',
424
- source: 'test',
425
- timestamp: Date.now(),
426
- });
427
- await waitForStream(framework, 'assistant');
428
- const record = fallbackState(framework).requests.find((entry) => entry.requestId !== 'baseline-request');
429
- assert.equal(record.finalStatus, 'held');
430
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_too_many_candidates');
431
- assert.equal(framework.pendingRequests.length, 0);
432
- }
433
- finally {
434
- await framework.stop();
435
- rmSync(dir, { recursive: true, force: true });
436
- }
437
- });
438
- it('holds instead of retrying for partial text output', async () => {
439
- const membrane = new ScriptedMembrane([[refusalResponse([{ type: 'text', text: 'partial' }])]]);
440
- const { dir, framework, agent } = await createFramework(membrane, {
441
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
442
- });
443
- try {
444
- const baselineRequest = request('system', 'baseline');
445
- const baselineArtifacts = artifacts('shared', ['L1-1']);
446
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
447
- const currentRequest = request('system', 'latest input');
448
- currentRequest.tools = [{
449
- name: 'echo-module--echo',
450
- description: 'Echoes the input',
451
- inputSchema: {
452
- type: 'object',
453
- properties: {
454
- message: { type: 'string' },
455
- },
456
- required: ['message'],
457
- },
458
- }];
459
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2']);
460
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
461
- agent.getCurrentBranchGeneration =
462
- () => branchInfo();
463
- await framework.startAgentStream(agent, {
464
- agentName: 'assistant',
465
- reason: 'test',
466
- source: 'test',
467
- timestamp: Date.now(),
468
- });
469
- await waitForStream(framework, 'assistant');
470
- const record = fallbackState(framework).requests.find((entry) => entry.requestId !== 'baseline-request');
471
- assert.equal(record.finalStatus, 'held');
472
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
473
- assert.equal(framework.pendingRequests.length, 0);
474
- }
475
- finally {
476
- await framework.stop();
477
- rmSync(dir, { recursive: true, force: true });
478
- }
479
- });
480
- it('holds instead of retrying for refusal content that includes a tool block', async () => {
481
- const membrane = new ScriptedMembrane([[refusalResponse([toolUseBlock('tool-partial', 'partial tool')])]]);
482
- const { dir, framework, agent } = await createFramework(membrane, {
483
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
484
- });
485
- try {
486
- const baselineRequest = request('system', 'baseline');
487
- const baselineArtifacts = artifacts('shared', ['L1-1']);
488
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
489
- const currentRequest = request('system', 'latest input');
490
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2']);
491
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
492
- agent.getCurrentBranchGeneration =
493
- () => branchInfo();
494
- await framework.startAgentStream(agent, {
495
- agentName: 'assistant',
496
- reason: 'test',
497
- source: 'test',
498
- timestamp: Date.now(),
499
- });
500
- await waitForStream(framework, 'assistant');
501
- const record = fallbackState(framework).requests.find((entry) => entry.requestId !== 'baseline-request');
502
- assert.equal(record.finalStatus, 'held');
503
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_partial_output');
504
- assert.equal(framework.pendingRequests.length, 0);
505
- }
506
- finally {
507
- await framework.stop();
508
- rmSync(dir, { recursive: true, force: true });
509
- }
510
- });
511
- it('treats same generation on a different branch id as incompatible', async () => {
512
- const membrane = new ScriptedMembrane([[refusalResponse()]]);
513
- const { dir, framework, agent } = await createFramework(membrane, {
514
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
515
- });
516
- try {
517
- const baselineRequest = request('system', 'baseline');
518
- const baselineArtifacts = artifacts('shared', ['L1-1'], 'branch-b');
519
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
520
- const currentRequest = request('system', 'latest input');
521
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2'], 'branch-a');
522
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
523
- agent.getCurrentBranchGeneration =
524
- () => branchInfo('branch-a');
525
- await framework.startAgentStream(agent, {
526
- agentName: 'assistant',
527
- reason: 'test',
528
- source: 'test',
529
- timestamp: Date.now(),
530
- });
531
- await waitForStream(framework, 'assistant');
532
- const record = fallbackState(framework).requests.find((entry) => entry.requestId !== 'baseline-request');
533
- assert.equal(record.finalStatus, 'held');
534
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_no_compatible_healthy_baseline');
535
- }
536
- finally {
537
- await framework.stop();
538
- rmSync(dir, { recursive: true, force: true });
539
- }
540
- });
541
- it('treats system or tool contract changes as incompatible', async () => {
542
- const membrane = new ScriptedMembrane([[refusalResponse()]]);
543
- const { dir, framework, agent } = await createFramework(membrane, {
544
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
545
- });
546
- try {
547
- const baselineRequest = request('system', 'baseline');
548
- const baselineArtifacts = artifacts('shared', ['L1-1']);
549
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
550
- const currentRequest = request('system', 'latest input');
551
- currentRequest.tools = [{
552
- name: 'echo',
553
- description: 'Echoes',
554
- inputSchema: { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] },
555
- }];
556
- const currentArtifacts = artifacts('changed', ['L1-1', 'L1-2']);
557
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
558
- agent.getCurrentBranchGeneration =
559
- () => branchInfo();
560
- await framework.startAgentStream(agent, {
561
- agentName: 'assistant',
562
- reason: 'test',
563
- source: 'test',
564
- timestamp: Date.now(),
565
- });
566
- await waitForStream(framework, 'assistant');
567
- const record = fallbackState(framework).requests.find((entry) => entry.requestId !== 'baseline-request');
568
- assert.equal(record.finalStatus, 'held');
569
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_no_compatible_healthy_baseline');
570
- }
571
- finally {
572
- await framework.stop();
573
- rmSync(dir, { recursive: true, force: true });
574
- }
575
- });
576
- it('holds instead of dispatching an over-budget expanded retry', async () => {
577
- const membrane = new ScriptedMembrane([[refusalResponse()]]);
578
- const { dir, framework, agent } = await createFramework(membrane, {
579
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 64 },
580
- });
581
- try {
582
- const baselineRequest = request('system', 'baseline');
583
- const baselineArtifacts = artifacts('shared', ['L1-1']);
584
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
585
- const currentRequest = request('system', 'latest input');
586
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2']);
587
- const retryRequest = request('system', 'expanded raw source '.repeat(200));
588
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
589
- agent.buildPrimarySummaryRawExpansionRequest = () => ({ request: retryRequest, artifacts: currentArtifacts });
590
- agent.getCurrentBranchGeneration =
591
- () => branchInfo();
592
- await framework.startAgentStream(agent, {
593
- agentName: 'assistant',
594
- reason: 'test',
595
- source: 'test',
596
- timestamp: Date.now(),
597
- });
598
- await waitForStream(framework, 'assistant');
599
- assert.equal(membrane.calls.length, 1);
600
- const record = fallbackState(framework).requests.find((entry) => entry.requestId !== 'baseline-request');
601
- assert.equal(record.finalStatus, 'held');
602
- assert.equal(record.fallbackHeldReason, 'primary_summary_refusal_retry_over_budget');
603
- }
604
- finally {
605
- await framework.stop();
606
- rmSync(dir, { recursive: true, force: true });
607
- }
608
- });
609
- it('restart reconciliation holds unresolved pending intents instead of retrying', async () => {
610
- const dir = mkdtempSync(join(tmpdir(), 'af-primary-summary-fallback-restart-'));
611
- const storePath = join(dir, 'store.chronicle');
612
- const firstMembrane = new ScriptedMembrane([]);
613
- const first = await AgentFramework.create({
614
- storePath,
615
- membrane: firstMembrane.asMembrane(),
616
- agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'system' }],
617
- modules: [],
618
- });
619
- try {
620
- first.getStore().setStateJson(FALLBACK_STATE_ID, {
621
- requests: [{
622
- requestId: 'stuck-request',
623
- agentName: 'assistant',
624
- namespace: 'default',
625
- timestamp: Date.now(),
626
- dispatchKind: 'primary',
627
- branch: { id: 'branch-a', name: 'main', generation: 1 },
628
- projection: projection('branch-a', 1, ['L1-1']),
629
- requestInputBoundTokens: 100,
630
- requestCompleteBoundTokens: 356,
631
- providerInputTokens: 40,
632
- systemHash: 'system-shared',
633
- modelConfigHash: 'model-shared',
634
- toolContractHash: 'tools-shared',
635
- stopReason: 'refusal',
636
- visibleAssistantOutput: false,
637
- executedToolCalls: 0,
638
- finalStatus: 'refusal',
639
- fallbackIntent: {
640
- intentId: 'intent-1',
641
- createdAt: Date.now(),
642
- baselineRequestId: 'baseline-request',
643
- candidateSummaries: [identity('L1-2')],
644
- retryRequestHash: 'retry-hash',
645
- retryCompleteBoundTokens: 512,
646
- requestBudgetTokens: 4096,
647
- },
648
- fallbackStatus: 'pending',
649
- }],
650
- });
651
- }
652
- finally {
653
- await first.stop();
654
- }
655
- const secondMembrane = new ScriptedMembrane([]);
656
- const second = await AgentFramework.create({
657
- storePath,
658
- membrane: secondMembrane.asMembrane(),
659
- agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'system' }],
660
- modules: [],
661
- });
662
- try {
663
- const record = fallbackState(second).requests[0];
664
- assert.equal(record.fallbackStatus, 'held');
665
- assert.equal(record.fallbackHeldReason, 'primary_summary_fallback_unresolved_on_restart');
666
- assert.equal(secondMembrane.calls.length, 0);
667
- }
668
- finally {
669
- await second.stop();
670
- rmSync(dir, { recursive: true, force: true });
671
- }
672
- });
673
- it('holds after a fallback retry refuses and never dispatches a second retry', async () => {
674
- const membrane = new ScriptedMembrane([
675
- [refusalResponse()],
676
- [refusalResponse()],
677
- ]);
678
- const { dir, framework, agent } = await createFramework(membrane, {
679
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
680
- });
681
- try {
682
- const baselineRequest = request('system', 'baseline');
683
- const baselineArtifacts = artifacts('shared', ['L1-1']);
684
- await persistHealthyBaseline(framework, agent, baselineRequest, baselineArtifacts);
685
- const currentRequest = request('system', 'latest input');
686
- const currentArtifacts = artifacts('shared', ['L1-1', 'L1-2']);
687
- const retryRequest = request('system', 'expanded raw source');
688
- agent.prepareActivationRequest = async () => ({ request: currentRequest, artifacts: currentArtifacts });
689
- agent.buildPrimarySummaryRawExpansionRequest = () => ({ request: retryRequest, artifacts: currentArtifacts });
690
- agent.getCurrentBranchGeneration =
691
- () => branchInfo();
692
- const start = framework.startAgentStream.bind(framework);
693
- const trigger = {
694
- agentName: 'assistant',
695
- reason: 'test',
696
- source: 'test',
697
- timestamp: Date.now(),
698
- };
699
- await start(agent, trigger);
700
- await waitForStream(framework, 'assistant');
701
- const queuedRetry = framework.pendingRequests.shift();
702
- assert.ok(queuedRetry);
703
- await start(agent, queuedRetry);
704
- await waitForStream(framework, 'assistant');
705
- assert.equal(membrane.calls.length, 2);
706
- assert.equal(framework.pendingRequests.length, 0);
707
- const records = fallbackState(framework).requests;
708
- const original = records.find((record) => record.requestId !== 'baseline-request' && record.dispatchKind === 'primary');
709
- const retry = records.find((record) => record.dispatchKind === 'primary_summary_fallback_retry');
710
- assert.equal(original.fallbackStatus, 'held');
711
- assert.equal(original.fallbackHeldReason, 'primary_summary_fallback_retry_refusal');
712
- assert.equal(retry.finalStatus, 'held');
713
- assert.equal(retry.fallbackHeldReason, 'primary_summary_fallback_retry_refusal');
714
- }
715
- finally {
716
- await framework.stop();
717
- rmSync(dir, { recursive: true, force: true });
718
- }
719
- });
720
- it('writes the pending request record before a lazy stream is iterated', async () => {
721
- const lazy = new LazyStream();
722
- const membrane = new ScriptedMembrane([lazy]);
723
- const { dir, framework, agent } = await createFramework(membrane, {
724
- primarySummaryFallback: { enabled: true, maxNewSummaries: 4, requestBudgetTokens: 4096 },
725
- });
726
- try {
727
- const preparedRequest = request('system', 'hello');
728
- const preparedArtifacts = artifacts('shared', ['L1-1']);
729
- let sawPendingBeforeIteration = false;
730
- lazy.onStart = () => {
731
- const stored = fallbackState(framework).requests;
732
- sawPendingBeforeIteration = stored.length === 1 && stored[0].finalStatus === 'pending';
733
- };
734
- agent.prepareActivationRequest = async () => ({ request: preparedRequest, artifacts: preparedArtifacts });
735
- agent.getCurrentBranchGeneration =
736
- () => branchInfo();
737
- await framework.startAgentStream(agent, {
738
- agentName: 'assistant',
739
- reason: 'test',
740
- source: 'test',
741
- timestamp: Date.now(),
742
- });
743
- await waitForStream(framework, 'assistant');
744
- assert.equal(lazy.started, true);
745
- assert.equal(sawPendingBeforeIteration, true);
746
- }
747
- finally {
748
- await framework.stop();
749
- rmSync(dir, { recursive: true, force: true });
750
- }
751
- });
752
- });
753
- //# sourceMappingURL=primary-summary-refusal-fallback.test.js.map