@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,6 +1,5 @@
1
1
  import { join } from 'node:path';
2
2
  import { appendFileSync, mkdirSync } from 'node:fs';
3
- import { createHash } from 'node:crypto';
4
3
  import { JsStore } from '@animalabs/chronicle';
5
4
  import { MembraneError } from '@animalabs/membrane';
6
5
  import { ContextManager, PassthroughStrategy } from '@animalabs/context-manager';
@@ -18,7 +17,26 @@ import { ConversationRouter } from './mcpl/conversation-router.js';
18
17
  import { safeSlice } from './safe-slice.js';
19
18
  import { toolResultDataToHistoryString } from './tool-result-history.js';
20
19
  import { splitProseSegments } from './prose-segments.js';
21
- import { canonicalJsonStringify } from './canonical-json.js';
20
+ /** Detect a supported image media type from magic bytes (the model API
21
+ * rejects mislabeled media types, so trust bytes over extensions).
22
+ * Returns undefined for non-image content. */
23
+ function sniffImageMediaType(data) {
24
+ if (data.length >= 4 && data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) {
25
+ return 'image/png';
26
+ }
27
+ if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) {
28
+ return 'image/jpeg';
29
+ }
30
+ if (data.length >= 3 && data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46) {
31
+ return 'image/gif';
32
+ }
33
+ if (data.length >= 12 &&
34
+ data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 &&
35
+ data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50) {
36
+ return 'image/webp';
37
+ }
38
+ return undefined;
39
+ }
22
40
  /**
23
41
  * Tools whose presence suppresses auto-routing of the surrounding prose,
24
42
  * for two distinct reasons:
@@ -51,12 +69,6 @@ const FRAMEWORK_STATE_ID = 'framework/state';
51
69
  const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
52
70
  const INFERENCE_LOG_ID = 'framework/inference-log';
53
71
  const PROCESS_LOG_ID = 'framework/process-log';
54
- const PRIMARY_SUMMARY_FALLBACK_STATE_ID = 'framework/primary-summary-fallback';
55
- const PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY = 'primarySummarySettlement';
56
- const PRIMARY_SUMMARY_SETTLEMENT_VERSION = 1;
57
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH = 1024;
58
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH = 256;
59
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH = 256;
60
72
  const TURN_CHECKPOINTS_ID = 'framework/turn-checkpoints'; // legacy single-map layout, read-only fallback
61
73
  const TURN_CHECKPOINTS_TREE_ID = 'framework/turn-checkpoints/tree';
62
74
  /** Maximum number of turn checkpoints to keep per agent. */
@@ -197,8 +209,6 @@ export class AgentFramework {
197
209
  processLoggingPersist;
198
210
  processLoggingBroadcast;
199
211
  activeStreams = new Map();
200
- activeStreamContexts = new Map();
201
- primarySummaryStateLock = Promise.resolve();
202
212
  /** Per-agent output locus pinned for the CURRENT logical turn (see
203
213
  * resolveTurnLocus in driveStream). Lives here — not in driveStream
204
214
  * locals — so the pin survives a context-budget stream restart, which
@@ -424,12 +434,6 @@ export class AgentFramework {
424
434
  // Already registered
425
435
  }
426
436
  }
427
- try {
428
- store.registerState({ id: PRIMARY_SUMMARY_FALLBACK_STATE_ID, strategy: 'snapshot' });
429
- }
430
- catch {
431
- // Already registered
432
- }
433
437
  const discordAwarenessOutboxPath = config.discordAwarenessOutboxPath
434
438
  ?? (config.storePath ? defaultDiscordAwarenessOutboxPath(config.storePath) : undefined);
435
439
  const discordAwarenessOutbox = discordAwarenessOutboxPath
@@ -453,13 +457,12 @@ export class AgentFramework {
453
457
  throw new DiscordAwarenessAccountingError('startup reconciliation', error);
454
458
  }
455
459
  }
460
+ // Restore persisted usage data (if any) from prior session
461
+ framework.restoreUsageState();
456
462
  // Create agents
457
463
  for (const agentConfig of config.agents) {
458
464
  await framework.createAgent(agentConfig);
459
465
  }
460
- // Restore persisted usage data (if any) from prior session
461
- framework.restoreUsageState();
462
- await framework.reconcilePrimarySummaryFallbackStateOnStartup();
463
466
  // Finish any branch-local suppression interrupted after Chronicle switched
464
467
  // branches. This runs before modules, MCPL connections, or inbound traffic.
465
468
  await framework.resumePreparedDiscordSuppressions();
@@ -680,7 +683,7 @@ export class AgentFramework {
680
683
  const queued = [...this.agents.values()].flatMap((agent) => {
681
684
  const cm = agent.getContextManager();
682
685
  const tools = this.getToolsForAgent(agent.name).filter((tool) => agent.canUseTool(tool.name));
683
- cm.setToolDefinitions?.(tools);
686
+ cm.setToolDefinitions(tools);
684
687
  if (cm.isReady())
685
688
  return [];
686
689
  const pending = cm.getPendingWork()?.description;
@@ -868,7 +871,9 @@ export class AgentFramework {
868
871
  ...channelTools,
869
872
  ...gateTools,
870
873
  this.buildAgentSettingsTool(),
871
- ...(this.getWorkspaceModule() ? [AgentFramework.SAVE_IMAGE_TOOL] : []),
874
+ ...(this.getWorkspaceModule()
875
+ ? [AgentFramework.SAVE_IMAGE_TOOL, AgentFramework.READ_IMAGE_TOOL]
876
+ : []),
872
877
  ];
873
878
  }
874
879
  /** The registered workspace module, if any (used by save_image). */
@@ -1251,1091 +1256,6 @@ export class AgentFramework {
1251
1256
  });
1252
1257
  return { promise, resolve, reject };
1253
1258
  }
1254
- hashJson(value) {
1255
- return createHash('sha256').update(canonicalJsonStringify(value), 'utf8').digest('hex');
1256
- }
1257
- estimateStringTokens(text, overhead = 0) {
1258
- return Math.ceil(Buffer.byteLength(text, 'utf8') / 4) + overhead;
1259
- }
1260
- estimateUnknownTokens(value, overhead = 0) {
1261
- return this.estimateStringTokens(JSON.stringify(value) ?? 'null', overhead);
1262
- }
1263
- estimateRequestInputBoundTokens(request) {
1264
- let total = 96;
1265
- total += this.estimateStringTokens(request.system ?? '', 48);
1266
- for (const message of request.messages) {
1267
- total += 24;
1268
- total += this.estimateStringTokens(message.participant, 8);
1269
- if (message.cacheBreakpoint)
1270
- total += 8;
1271
- for (const block of message.content) {
1272
- switch (block.type) {
1273
- case 'text':
1274
- total += this.estimateStringTokens(block.text, 24);
1275
- break;
1276
- case 'thinking':
1277
- total += this.estimateStringTokens(block.thinking, 32);
1278
- total += block.signature ? this.estimateStringTokens(block.signature, 8) : 0;
1279
- break;
1280
- case 'redacted_thinking':
1281
- total += this.estimateUnknownTokens(block, 32);
1282
- break;
1283
- default:
1284
- total += this.estimateUnknownTokens(block, 32);
1285
- break;
1286
- }
1287
- }
1288
- }
1289
- if (request.tools && request.tools.length > 0) {
1290
- total += 32;
1291
- for (const tool of request.tools) {
1292
- total += this.estimateUnknownTokens({
1293
- name: tool.name,
1294
- description: tool.description,
1295
- inputSchema: tool.inputSchema,
1296
- }, 48);
1297
- }
1298
- }
1299
- total += this.estimateUnknownTokens(request.config, 48);
1300
- if (request.providerParams !== undefined) {
1301
- total += this.estimateUnknownTokens(request.providerParams, 32);
1302
- }
1303
- total += request.promptCaching ? 16 : 0;
1304
- total += request.cacheTtl ? this.estimateStringTokens(request.cacheTtl, 8) : 0;
1305
- total += request.assistantParticipant
1306
- ? this.estimateStringTokens(request.assistantParticipant, 8)
1307
- : 0;
1308
- return total;
1309
- }
1310
- estimateRequestCompleteBoundTokens(request) {
1311
- const reserve = typeof request.config.maxTokens === 'number' && Number.isFinite(request.config.maxTokens)
1312
- ? request.config.maxTokens
1313
- : 0;
1314
- return this.estimateRequestInputBoundTokens(request) + reserve + 96;
1315
- }
1316
- primarySummaryAdmissionBoundTokens(originalRequestInputTokens, expandedRequest, originalProviderInputTokens) {
1317
- const expandedInput = this.estimateRequestInputBoundTokens(expandedRequest);
1318
- const expandedComplete = this.estimateRequestCompleteBoundTokens(expandedRequest);
1319
- if (originalProviderInputTokens === undefined)
1320
- return expandedComplete;
1321
- const reserve = typeof expandedRequest.config.maxTokens === 'number' && Number.isFinite(expandedRequest.config.maxTokens)
1322
- ? expandedRequest.config.maxTokens
1323
- : 0;
1324
- const positiveDelta = Math.max(0, expandedInput - originalRequestInputTokens);
1325
- return Math.max(expandedComplete, originalProviderInputTokens + positiveDelta + reserve + 96);
1326
- }
1327
- readPrimarySummaryFallbackState() {
1328
- const raw = this.store
1329
- ?.getStateJson?.(PRIMARY_SUMMARY_FALLBACK_STATE_ID);
1330
- if (!raw || typeof raw !== 'object')
1331
- return { requests: [] };
1332
- const requests = Array.isArray(raw.requests)
1333
- ? (raw.requests)
1334
- : [];
1335
- return { requests };
1336
- }
1337
- writePrimarySummaryFallbackState(state) {
1338
- this.store.setStateJson(PRIMARY_SUMMARY_FALLBACK_STATE_ID, state);
1339
- }
1340
- async withPrimarySummaryStateLock(work) {
1341
- const previous = this.primarySummaryStateLock;
1342
- let release;
1343
- this.primarySummaryStateLock = new Promise((resolve) => { release = resolve; });
1344
- await previous;
1345
- try {
1346
- return await work();
1347
- }
1348
- finally {
1349
- release();
1350
- }
1351
- }
1352
- async reconcilePrimarySummaryFallbackStateOnStartup() {
1353
- const alerts = [];
1354
- const unsettledRequests = (await this.withPrimarySummaryStateLock(() => this.readPrimarySummaryFallbackState().requests
1355
- .filter((record) => record.finalStatus === 'pending'
1356
- || record.finalStatus === 'refusal'
1357
- || record.fallbackStatus === 'pending')
1358
- .map((record) => structuredClone(record))));
1359
- const resolvedRequestIds = new Set();
1360
- for (const record of unsettledRequests) {
1361
- const result = await this.finalizePrimarySummarySettlementOnStartup(record);
1362
- if (result === 'finalized') {
1363
- resolvedRequestIds.add(record.requestId);
1364
- continue;
1365
- }
1366
- if (result && typeof result === 'object' && 'invalidReason' in result) {
1367
- resolvedRequestIds.add(record.requestId);
1368
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason: result.invalidReason });
1369
- await this.markPrimarySummaryRequestRestartFailure(record, result.invalidReason);
1370
- }
1371
- }
1372
- await this.withPrimarySummaryStateLock(() => {
1373
- const state = this.readPrimarySummaryFallbackState();
1374
- let changed = false;
1375
- state.requests = state.requests.map((record) => {
1376
- if (resolvedRequestIds.has(record.requestId))
1377
- return record;
1378
- if (record.dispatchKind === 'primary_summary_fallback_retry' && (record.finalStatus === 'pending' || record.finalStatus === 'refusal')) {
1379
- changed = true;
1380
- const reason = 'primary_summary_fallback_unresolved_on_restart';
1381
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1382
- return {
1383
- ...record,
1384
- finalStatus: 'held',
1385
- fallbackHeldReason: reason,
1386
- };
1387
- }
1388
- if (record.fallbackIntent && record.fallbackStatus === 'pending') {
1389
- changed = true;
1390
- const reason = 'primary_summary_fallback_unresolved_on_restart';
1391
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1392
- return {
1393
- ...record,
1394
- fallbackStatus: 'held',
1395
- fallbackHeldReason: reason,
1396
- };
1397
- }
1398
- if (record.finalStatus === 'refusal') {
1399
- changed = true;
1400
- const reason = 'primary_summary_request_unresolved_on_restart';
1401
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1402
- return {
1403
- ...record,
1404
- finalStatus: 'held',
1405
- fallbackStatus: 'held',
1406
- fallbackHeldReason: reason,
1407
- };
1408
- }
1409
- if (record.finalStatus === 'pending') {
1410
- changed = true;
1411
- const reason = 'primary_summary_request_unresolved_on_restart';
1412
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1413
- return {
1414
- ...record,
1415
- finalStatus: 'timeout_unknown',
1416
- fallbackHeldReason: reason,
1417
- };
1418
- }
1419
- return record;
1420
- });
1421
- if (changed)
1422
- this.writePrimarySummaryFallbackState({ requests: state.requests.slice(-256) });
1423
- });
1424
- for (const alert of alerts) {
1425
- this.opsAlert('refusal-held', alert.agentName, alert.reason, {
1426
- data: { requestId: alert.requestId },
1427
- });
1428
- }
1429
- }
1430
- primarySummaryIdentityKey(identity) {
1431
- return [
1432
- identity.id,
1433
- identity.contentHash,
1434
- identity.carrierHash,
1435
- identity.sourceLeafHash,
1436
- ].join('\u0000');
1437
- }
1438
- primarySummaryProjectionKeys(projection) {
1439
- return projection?.selectedSummaries.map((selection) => this.primarySummaryIdentityKey(selection.identity)) ?? [];
1440
- }
1441
- primarySummaryFallbackConfig(agent) {
1442
- const raw = agent.refusalHandling?.primarySummaryFallback;
1443
- const fallbackBudgetBase = agent.contextBudgetTokens ?? 100_000;
1444
- return {
1445
- enabled: raw?.enabled === true,
1446
- maxNewSummaries: typeof raw?.maxNewSummaries === 'number' && Number.isSafeInteger(raw.maxNewSummaries)
1447
- ? Math.max(0, raw.maxNewSummaries)
1448
- : 4,
1449
- requestBudgetTokens: typeof raw?.requestBudgetTokens === 'number' && Number.isFinite(raw.requestBudgetTokens) && raw.requestBudgetTokens > 0
1450
- ? raw.requestBudgetTokens
1451
- : fallbackBudgetBase + agent.maxTokens,
1452
- };
1453
- }
1454
- latestCompatibleHealthyPrimaryRequest(current, state) {
1455
- for (let i = state.requests.length - 1; i >= 0; i--) {
1456
- const candidate = state.requests[i];
1457
- if (candidate.requestId === current.requestId)
1458
- continue;
1459
- if (candidate.agentName !== current.agentName)
1460
- continue;
1461
- if (candidate.namespace !== current.namespace)
1462
- continue;
1463
- if (candidate.branch.id !== current.branch.id)
1464
- continue;
1465
- if (candidate.branch.generation !== current.branch.generation)
1466
- continue;
1467
- if (candidate.systemHash !== current.systemHash)
1468
- continue;
1469
- if (candidate.modelConfigHash !== current.modelConfigHash)
1470
- continue;
1471
- if (candidate.toolContractHash !== current.toolContractHash)
1472
- continue;
1473
- if (candidate.dispatchKind !== 'primary')
1474
- continue;
1475
- if (candidate.fallbackIntent || candidate.fallbackStatus)
1476
- continue;
1477
- if (candidate.finalStatus !== 'success')
1478
- continue;
1479
- return candidate;
1480
- }
1481
- return null;
1482
- }
1483
- async persistPrimarySummaryRequestRecord(record) {
1484
- await this.withPrimarySummaryStateLock(() => {
1485
- const state = this.readPrimarySummaryFallbackState();
1486
- const requests = state.requests.filter((entry) => entry.requestId !== record.requestId);
1487
- requests.push(record);
1488
- this.writePrimarySummaryFallbackState({ requests: requests.slice(-256) });
1489
- });
1490
- }
1491
- async updatePrimarySummaryRequestRecord(requestId, update) {
1492
- return this.withPrimarySummaryStateLock(() => {
1493
- const state = this.readPrimarySummaryFallbackState();
1494
- const index = state.requests.findIndex((entry) => entry.requestId === requestId);
1495
- if (index < 0)
1496
- return null;
1497
- state.requests[index] = update(state.requests[index]);
1498
- this.writePrimarySummaryFallbackState({ requests: state.requests.slice(-256) });
1499
- return state.requests[index];
1500
- });
1501
- }
1502
- async getPrimarySummaryRequestRecord(requestId) {
1503
- return this.withPrimarySummaryStateLock(() => {
1504
- const state = this.readPrimarySummaryFallbackState();
1505
- return state.requests.find((entry) => entry.requestId === requestId) ?? null;
1506
- });
1507
- }
1508
- primarySummaryContractsMatch(left, right) {
1509
- return left.systemHash === right.systemHash
1510
- && left.modelConfigHash === right.modelConfigHash
1511
- && left.toolContractHash === right.toolContractHash;
1512
- }
1513
- primarySummaryFallbackOutputPhaseHash(blocks) {
1514
- return this.hashJson(blocks);
1515
- }
1516
- primarySummaryFallbackBlockTypes(blocks) {
1517
- return blocks.map((block) => block.type);
1518
- }
1519
- static NON_EXECUTED_TOOL_RESULT_TEXT = '[tool not executed]';
1520
- buildNonExecutedToolResults(blocks) {
1521
- const seenToolUseIds = new Set();
1522
- return blocks
1523
- .filter((block) => block.type === 'tool_use')
1524
- .filter((block) => {
1525
- if (seenToolUseIds.has(block.id))
1526
- return false;
1527
- seenToolUseIds.add(block.id);
1528
- return true;
1529
- })
1530
- .map((block) => ({
1531
- type: 'tool_result',
1532
- toolUseId: block.id,
1533
- content: AgentFramework.NON_EXECUTED_TOOL_RESULT_TEXT,
1534
- isError: true,
1535
- }));
1536
- }
1537
- buildPrimarySummarySettlementId(record, outcome) {
1538
- return `${record.dispatchKind}:${outcome}:terminal:v${PRIMARY_SUMMARY_SETTLEMENT_VERSION}`;
1539
- }
1540
- buildPrimarySummarySettlementMetadata(record, options) {
1541
- const settlementId = options.settlementId ?? this.buildPrimarySummarySettlementId(record, options.outcome);
1542
- return {
1543
- [PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY]: {
1544
- version: PRIMARY_SUMMARY_SETTLEMENT_VERSION,
1545
- requestId: record.requestId,
1546
- settlementId,
1547
- agentName: record.agentName,
1548
- dispatchKind: record.dispatchKind,
1549
- outcome: options.outcome,
1550
- ...(options.stopReason ? { stopReason: options.stopReason } : {}),
1551
- ...(options.holdReason ? { holdReason: options.holdReason } : {}),
1552
- ...(options.providerInputTokens !== undefined
1553
- ? { providerInputTokens: options.providerInputTokens }
1554
- : {}),
1555
- visibleAssistantOutput: options.visibleAssistantOutput,
1556
- executedToolCalls: options.executedToolCalls,
1557
- branch: {
1558
- id: record.branch.id,
1559
- name: record.branch.name,
1560
- generation: record.branch.generation,
1561
- },
1562
- role: options.role,
1563
- kind: options.kind,
1564
- entryIndex: options.entryIndex,
1565
- entryCount: options.entryCount,
1566
- },
1567
- };
1568
- }
1569
- isBoundedNonEmptyString(value, maxLength) {
1570
- return typeof value === 'string' && value.length > 0 && value.length <= maxLength;
1571
- }
1572
- isSafeNonNegativeInteger(value) {
1573
- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
1574
- }
1575
- primarySummarySettlementRequestId(message) {
1576
- const raw = message.metadata?.[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY];
1577
- if (!raw || typeof raw !== 'object')
1578
- return null;
1579
- const requestId = raw.requestId;
1580
- return typeof requestId === 'string' ? requestId : null;
1581
- }
1582
- parsePrimarySummarySettlementMetadata(message) {
1583
- const raw = message.metadata?.[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY];
1584
- if (!raw || typeof raw !== 'object')
1585
- return null;
1586
- const candidate = raw;
1587
- if (candidate.version !== PRIMARY_SUMMARY_SETTLEMENT_VERSION ||
1588
- !this.isBoundedNonEmptyString(candidate.requestId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
1589
- !this.isBoundedNonEmptyString(candidate.settlementId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
1590
- !this.isBoundedNonEmptyString(candidate.agentName, PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH) ||
1591
- (candidate.dispatchKind !== 'primary' && candidate.dispatchKind !== 'primary_summary_fallback_retry') ||
1592
- (candidate.outcome !== 'success' && candidate.outcome !== 'held') ||
1593
- typeof candidate.visibleAssistantOutput !== 'boolean' ||
1594
- !this.isSafeNonNegativeInteger(candidate.executedToolCalls) ||
1595
- !this.isSafeNonNegativeInteger(candidate.entryIndex) ||
1596
- !this.isSafeNonNegativeInteger(candidate.entryCount) ||
1597
- (candidate.role !== 'assistant' && candidate.role !== 'user') ||
1598
- (candidate.kind !== 'assistant_output' && candidate.kind !== 'generated_tool_result') ||
1599
- !candidate.branch ||
1600
- !this.isBoundedNonEmptyString(candidate.branch.id, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
1601
- !this.isBoundedNonEmptyString(candidate.branch.name, PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH) ||
1602
- !this.isSafeNonNegativeInteger(candidate.branch.generation) ||
1603
- candidate.entryCount < 1 ||
1604
- candidate.entryIndex >= candidate.entryCount ||
1605
- (candidate.stopReason !== undefined
1606
- && !this.isBoundedNonEmptyString(candidate.stopReason, PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH)) ||
1607
- (candidate.holdReason !== undefined
1608
- && !this.isBoundedNonEmptyString(candidate.holdReason, PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH)) ||
1609
- (candidate.providerInputTokens !== undefined
1610
- && !this.isSafeNonNegativeInteger(candidate.providerInputTokens)) ||
1611
- (candidate.outcome === 'held' && candidate.holdReason === undefined) ||
1612
- (candidate.outcome === 'success' && candidate.holdReason !== undefined)) {
1613
- return { invalidReason: 'primary_summary_settlement_metadata_invalid_on_restart' };
1614
- }
1615
- return candidate;
1616
- }
1617
- primarySummarySettlementMetadataConsistent(left, right) {
1618
- return left.version === right.version
1619
- && left.requestId === right.requestId
1620
- && left.settlementId === right.settlementId
1621
- && left.agentName === right.agentName
1622
- && left.dispatchKind === right.dispatchKind
1623
- && left.outcome === right.outcome
1624
- && left.stopReason === right.stopReason
1625
- && left.holdReason === right.holdReason
1626
- && left.providerInputTokens === right.providerInputTokens
1627
- && left.visibleAssistantOutput === right.visibleAssistantOutput
1628
- && left.executedToolCalls === right.executedToolCalls
1629
- && left.branch.id === right.branch.id
1630
- && left.branch.name === right.branch.name
1631
- && left.branch.generation === right.branch.generation
1632
- && left.entryCount === right.entryCount;
1633
- }
1634
- primarySummaryShardEnvelopeConsistent(participant, left, candidateParticipant, right) {
1635
- return participant === candidateParticipant
1636
- && this.primarySummarySettlementMetadataConsistent(left, right)
1637
- && left.role === right.role
1638
- && left.kind === right.kind
1639
- && left.entryIndex === right.entryIndex;
1640
- }
1641
- primarySummaryLogicalSettlementEntries(agent, requestId) {
1642
- const allMessages = agent.getContextManager().getAllMessages()
1643
- .slice()
1644
- .sort((left, right) => left.sequence - right.sequence);
1645
- const entries = [];
1646
- for (let index = 0; index < allMessages.length;) {
1647
- const message = allMessages[index];
1648
- if (this.primarySummarySettlementRequestId(message) !== requestId) {
1649
- index++;
1650
- continue;
1651
- }
1652
- const parsed = this.parsePrimarySummarySettlementMetadata(message);
1653
- if (!parsed) {
1654
- index++;
1655
- continue;
1656
- }
1657
- if ('invalidReason' in parsed)
1658
- return parsed;
1659
- if (message.bodyGroupId === undefined) {
1660
- if (message.shardIndex !== undefined) {
1661
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1662
- }
1663
- entries.push({
1664
- messages: [message],
1665
- settlement: parsed,
1666
- participant: message.participant,
1667
- content: message.content,
1668
- });
1669
- index++;
1670
- continue;
1671
- }
1672
- const groupId = message.bodyGroupId;
1673
- if (!this.isBoundedNonEmptyString(groupId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH)) {
1674
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1675
- }
1676
- if (index > 0 && allMessages[index - 1]?.bodyGroupId === groupId) {
1677
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1678
- }
1679
- const groupMessages = [];
1680
- while (index < allMessages.length && allMessages[index].bodyGroupId === groupId) {
1681
- groupMessages.push(allMessages[index]);
1682
- index++;
1683
- }
1684
- if (allMessages.slice(index).some((candidate) => candidate.bodyGroupId === groupId)) {
1685
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1686
- }
1687
- const orderedGroupMessages = [];
1688
- let expectedShardIndex = 0;
1689
- for (const groupMessage of groupMessages) {
1690
- if (this.primarySummarySettlementRequestId(groupMessage) !== requestId) {
1691
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1692
- }
1693
- const groupParsed = this.parsePrimarySummarySettlementMetadata(groupMessage);
1694
- if (!groupParsed) {
1695
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1696
- }
1697
- if ('invalidReason' in groupParsed)
1698
- return groupParsed;
1699
- if (!this.primarySummaryShardEnvelopeConsistent(message.participant, parsed, groupMessage.participant, groupParsed)) {
1700
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1701
- }
1702
- if (groupMessage.shardIndex !== expectedShardIndex) {
1703
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
1704
- }
1705
- orderedGroupMessages.push(groupMessage);
1706
- expectedShardIndex++;
1707
- }
1708
- entries.push({
1709
- messages: orderedGroupMessages,
1710
- settlement: parsed,
1711
- participant: message.participant,
1712
- content: orderedGroupMessages.flatMap((groupMessage) => groupMessage.content),
1713
- bodyGroupId: groupId,
1714
- });
1715
- }
1716
- return entries;
1717
- }
1718
- primarySummaryContentMatches(left, right) {
1719
- return this.primarySummaryFallbackOutputPhaseHash(left) === this.primarySummaryFallbackOutputPhaseHash(right);
1720
- }
1721
- async persistPrimarySummaryChronicleEntries(agent, entries) {
1722
- const messageIds = [];
1723
- for (const entry of entries) {
1724
- if (entry.content.length === 0)
1725
- continue;
1726
- messageIds.push(agent.getContextManager().addMessage(entry.participant, entry.content, entry.metadata));
1727
- }
1728
- return messageIds;
1729
- }
1730
- async markPrimarySummaryRequestRestartFailure(record, reason) {
1731
- if (record.dispatchKind === 'primary_summary_fallback_retry') {
1732
- const parent = await this.findPrimarySummaryRetryParent(record.requestId);
1733
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
1734
- ...entry,
1735
- finalStatus: 'held',
1736
- fallbackHeldReason: reason,
1737
- }));
1738
- if (parent) {
1739
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => ({
1740
- ...entry,
1741
- finalStatus: 'held',
1742
- fallbackStatus: 'held',
1743
- fallbackHeldReason: reason,
1744
- }));
1745
- }
1746
- return;
1747
- }
1748
- await this.updatePrimarySummaryRequestRecord(record.requestId, (current) => ({
1749
- ...current,
1750
- finalStatus: 'held',
1751
- fallbackStatus: 'held',
1752
- fallbackHeldReason: reason,
1753
- }));
1754
- }
1755
- async persistPrimarySummaryHeldOutput(agent, requestId, holdReason, options) {
1756
- const record = await this.getPrimarySummaryRequestRecord(requestId);
1757
- if (!record) {
1758
- throw new Error(`Primary summary request '${requestId}' disappeared before held-output persistence`);
1759
- }
1760
- const placeholderBlocks = this.buildNonExecutedToolResults(options.assistantBlocks);
1761
- const entryCount = placeholderBlocks.length > 0 ? 2 : 1;
1762
- const assistantMetadata = this.buildPrimarySummarySettlementMetadata(record, {
1763
- outcome: 'held',
1764
- stopReason: options.stopReason,
1765
- holdReason,
1766
- providerInputTokens: options.providerInputTokens,
1767
- visibleAssistantOutput: options.visibleAssistantOutput,
1768
- executedToolCalls: options.executedToolCalls,
1769
- entryIndex: 0,
1770
- entryCount,
1771
- role: 'assistant',
1772
- kind: 'assistant_output',
1773
- });
1774
- const settlementId = assistantMetadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY].settlementId;
1775
- const placeholderMetadata = placeholderBlocks.length > 0
1776
- ? this.buildPrimarySummarySettlementMetadata(record, {
1777
- outcome: 'held',
1778
- settlementId,
1779
- stopReason: options.stopReason,
1780
- holdReason,
1781
- providerInputTokens: options.providerInputTokens,
1782
- visibleAssistantOutput: options.visibleAssistantOutput,
1783
- executedToolCalls: options.executedToolCalls,
1784
- entryIndex: 1,
1785
- entryCount,
1786
- role: 'user',
1787
- kind: 'generated_tool_result',
1788
- })
1789
- : undefined;
1790
- const messageIds = await this.persistPrimarySummaryChronicleEntries(agent, [
1791
- { participant: agent.name, content: options.assistantBlocks, metadata: assistantMetadata },
1792
- ...(placeholderBlocks.length > 0 && placeholderMetadata
1793
- ? [{ participant: 'user', content: placeholderBlocks, metadata: placeholderMetadata }]
1794
- : []),
1795
- ]);
1796
- if (options.retryDispatch && options.retryPhase && options.assistantBlocks.length > 0) {
1797
- await this.recordPrimarySummaryRetryOutput(options.retryDispatch.originalRequestId, options.retryDispatch.intentId, {
1798
- phase: options.retryPhase,
1799
- contentHash: this.primarySummaryFallbackOutputPhaseHash(options.assistantBlocks),
1800
- messageId: messageIds[0] ?? '',
1801
- blockTypes: this.primarySummaryFallbackBlockTypes(options.assistantBlocks),
1802
- persistedAt: Date.now(),
1803
- });
1804
- }
1805
- if (options.retryDispatch) {
1806
- await this.holdPrimarySummaryRetryFamily(agent, requestId, options.retryDispatch, holdReason, options.holdData);
1807
- return;
1808
- }
1809
- await this.holdPrimarySummaryRequest(agent, requestId, holdReason, options.holdData);
1810
- }
1811
- async findPrimarySummaryRetryParent(retryRequestId) {
1812
- return this.withPrimarySummaryStateLock(() => {
1813
- const state = this.readPrimarySummaryFallbackState();
1814
- for (const record of state.requests) {
1815
- const intent = record.fallbackIntent;
1816
- if (intent?.retryRequestId === retryRequestId) {
1817
- return { originalRequestId: record.requestId, intentId: intent.intentId };
1818
- }
1819
- }
1820
- return null;
1821
- });
1822
- }
1823
- async finalizePrimarySummarySettlementOnStartup(record) {
1824
- const agent = this.agents.get(record.agentName);
1825
- if (!agent)
1826
- return false;
1827
- const settlementEntries = this.primarySummaryLogicalSettlementEntries(agent, record.requestId);
1828
- if ('invalidReason' in settlementEntries)
1829
- return settlementEntries;
1830
- if (settlementEntries.length === 0) {
1831
- return false;
1832
- }
1833
- const currentBranch = agent.getCurrentBranchGeneration();
1834
- const assistantEntry = settlementEntries[0];
1835
- const settlement = assistantEntry.settlement;
1836
- if (assistantEntry.participant !== record.agentName ||
1837
- settlement.agentName !== record.agentName ||
1838
- settlement.dispatchKind !== record.dispatchKind ||
1839
- settlement.entryIndex !== 0 ||
1840
- settlement.role !== 'assistant' ||
1841
- settlement.kind !== 'assistant_output') {
1842
- return { invalidReason: 'primary_summary_settlement_invalid_assistant_marker_on_restart' };
1843
- }
1844
- if (!currentBranch) {
1845
- return { invalidReason: 'primary_summary_settlement_branch_unknown_on_restart' };
1846
- }
1847
- if (settlement.branch.id !== record.branch.id ||
1848
- settlement.branch.name !== record.branch.name ||
1849
- settlement.branch.generation !== record.branch.generation ||
1850
- settlement.branch.id !== currentBranch.id ||
1851
- settlement.branch.name !== currentBranch.name ||
1852
- settlement.branch.generation !== currentBranch.generation) {
1853
- return { invalidReason: 'primary_summary_settlement_branch_mismatch_on_restart' };
1854
- }
1855
- if (settlement.entryCount < 1 || settlement.entryCount > 2) {
1856
- return { invalidReason: 'primary_summary_settlement_entry_count_invalid_on_restart' };
1857
- }
1858
- const entriesByIndex = new Map();
1859
- for (const entry of settlementEntries) {
1860
- if (!this.primarySummarySettlementMetadataConsistent(settlement, entry.settlement)) {
1861
- return { invalidReason: 'primary_summary_settlement_marker_conflict_on_restart' };
1862
- }
1863
- if (entriesByIndex.has(entry.settlement.entryIndex)) {
1864
- return { invalidReason: 'primary_summary_settlement_duplicate_entry_on_restart' };
1865
- }
1866
- entriesByIndex.set(entry.settlement.entryIndex, entry);
1867
- }
1868
- const placeholderEntry = entriesByIndex.get(1);
1869
- if (placeholderEntry
1870
- && (placeholderEntry.participant !== 'user'
1871
- || placeholderEntry.settlement.role !== 'user'
1872
- || placeholderEntry.settlement.kind !== 'generated_tool_result')) {
1873
- return { invalidReason: 'primary_summary_settlement_placeholder_marker_invalid_on_restart' };
1874
- }
1875
- if ([...entriesByIndex.keys()].some((index) => index !== 0 && index !== 1)) {
1876
- return { invalidReason: 'primary_summary_settlement_out_of_order_entries_on_restart' };
1877
- }
1878
- const expectedPlaceholder = settlement.outcome === 'held'
1879
- ? this.buildNonExecutedToolResults(assistantEntry.content)
1880
- : [];
1881
- if (settlement.outcome === 'success') {
1882
- if (settlement.entryCount !== 1 || placeholderEntry) {
1883
- return { invalidReason: 'primary_summary_success_settlement_shape_invalid_on_restart' };
1884
- }
1885
- }
1886
- else {
1887
- const expectedEntryCount = expectedPlaceholder.length > 0 ? 2 : 1;
1888
- if (settlement.entryCount !== expectedEntryCount) {
1889
- return { invalidReason: 'primary_summary_held_settlement_shape_invalid_on_restart' };
1890
- }
1891
- if (placeholderEntry) {
1892
- if (!this.primarySummaryContentMatches(placeholderEntry.content, expectedPlaceholder)) {
1893
- return { invalidReason: 'primary_summary_settlement_placeholder_content_mismatch_on_restart' };
1894
- }
1895
- }
1896
- else if (expectedPlaceholder.length > 0) {
1897
- const placeholderMetadata = this.buildPrimarySummarySettlementMetadata(record, {
1898
- outcome: 'held',
1899
- settlementId: settlement.settlementId,
1900
- stopReason: settlement.stopReason,
1901
- holdReason: settlement.holdReason,
1902
- providerInputTokens: settlement.providerInputTokens,
1903
- visibleAssistantOutput: settlement.visibleAssistantOutput,
1904
- executedToolCalls: settlement.executedToolCalls,
1905
- entryIndex: 1,
1906
- entryCount: settlement.entryCount,
1907
- role: 'user',
1908
- kind: 'generated_tool_result',
1909
- });
1910
- await this.persistPrimarySummaryChronicleEntries(agent, [
1911
- { participant: 'user', content: expectedPlaceholder, metadata: placeholderMetadata },
1912
- ]);
1913
- }
1914
- }
1915
- if (record.dispatchKind === 'primary_summary_fallback_retry') {
1916
- const parent = await this.findPrimarySummaryRetryParent(record.requestId);
1917
- if (!parent)
1918
- return { invalidReason: 'primary_summary_fallback_parent_missing_on_restart' };
1919
- if (settlement.outcome === 'success') {
1920
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
1921
- ...entry,
1922
- stopReason: settlement.stopReason ?? entry.stopReason,
1923
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
1924
- visibleAssistantOutput: settlement.visibleAssistantOutput,
1925
- executedToolCalls: settlement.executedToolCalls,
1926
- finalStatus: 'success',
1927
- fallbackHeldReason: undefined,
1928
- }));
1929
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => {
1930
- const intent = entry.fallbackIntent;
1931
- return {
1932
- ...entry,
1933
- finalStatus: 'success',
1934
- fallbackStatus: 'success',
1935
- fallbackHeldReason: undefined,
1936
- fallbackIntent: intent && intent.intentId === parent.intentId
1937
- ? { ...intent, finalizedAt: intent.finalizedAt ?? Date.now() }
1938
- : intent,
1939
- };
1940
- });
1941
- return 'finalized';
1942
- }
1943
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
1944
- ...entry,
1945
- stopReason: settlement.stopReason ?? entry.stopReason,
1946
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
1947
- visibleAssistantOutput: settlement.visibleAssistantOutput,
1948
- executedToolCalls: settlement.executedToolCalls,
1949
- finalStatus: 'held',
1950
- fallbackHeldReason: settlement.holdReason,
1951
- }));
1952
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => ({
1953
- ...entry,
1954
- finalStatus: 'held',
1955
- fallbackStatus: 'held',
1956
- fallbackHeldReason: settlement.holdReason,
1957
- }));
1958
- return 'finalized';
1959
- }
1960
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
1961
- ...entry,
1962
- stopReason: settlement.stopReason ?? entry.stopReason,
1963
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
1964
- visibleAssistantOutput: settlement.visibleAssistantOutput,
1965
- executedToolCalls: settlement.executedToolCalls,
1966
- finalStatus: settlement.outcome === 'success' ? 'success' : 'held',
1967
- fallbackStatus: settlement.outcome === 'success' ? entry.fallbackStatus : 'held',
1968
- fallbackHeldReason: settlement.outcome === 'held' ? settlement.holdReason : undefined,
1969
- }));
1970
- return 'finalized';
1971
- }
1972
- summarizePrimarySummaryRetryRequest(request) {
1973
- const blockCounts = new Map();
1974
- for (const message of request.messages) {
1975
- for (const block of message.content) {
1976
- blockCounts.set(block.type, (blockCounts.get(block.type) ?? 0) + 1);
1977
- }
1978
- }
1979
- return {
1980
- kind: 'primary_summary_fallback_retry',
1981
- requestHash: this.hashJson(request),
1982
- systemHash: this.hashJson(request.system ?? ''),
1983
- messageCount: request.messages.length,
1984
- toolCount: request.tools?.length ?? 0,
1985
- participantCount: [...new Set(request.messages.map((message) => message.participant))].length,
1986
- blockCounts: Object.fromEntries([...blockCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
1987
- promptCaching: request.promptCaching === true,
1988
- cacheTtl: request.cacheTtl,
1989
- assistantParticipant: request.assistantParticipant,
1990
- requestCompleteBoundTokens: this.estimateRequestCompleteBoundTokens(request),
1991
- };
1992
- }
1993
- summarizePrimarySummaryRetryResponse(response) {
1994
- const blockCounts = new Map();
1995
- for (const block of response.content) {
1996
- blockCounts.set(block.type, (blockCounts.get(block.type) ?? 0) + 1);
1997
- }
1998
- return {
1999
- kind: 'primary_summary_fallback_retry',
2000
- stopReason: response.stopReason,
2001
- rawAssistantTextHash: response.rawAssistantText
2002
- ? this.hashJson(response.rawAssistantText)
2003
- : undefined,
2004
- blockCounts: Object.fromEntries([...blockCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
2005
- toolCallIds: response.toolCalls.map((call) => call.id),
2006
- usage: response.usage
2007
- ? {
2008
- inputTokens: response.usage.inputTokens,
2009
- outputTokens: response.usage.outputTokens,
2010
- }
2011
- : undefined,
2012
- };
2013
- }
2014
- async claimPrimarySummaryRetryIntent(originalRequestId, intentId, retryRequestId) {
2015
- let outcome = 'missing';
2016
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2017
- const intent = record.fallbackIntent;
2018
- if (!intent || intent.intentId !== intentId || record.fallbackStatus !== 'pending') {
2019
- return record;
2020
- }
2021
- if (intent.retryRequestId && intent.retryRequestId !== retryRequestId) {
2022
- outcome = 'duplicate';
2023
- return record;
2024
- }
2025
- outcome = 'claimed';
2026
- return {
2027
- ...record,
2028
- fallbackIntent: {
2029
- ...intent,
2030
- retryRequestId,
2031
- dispatchedAt: intent.dispatchedAt ?? Date.now(),
2032
- },
2033
- };
2034
- });
2035
- return outcome;
2036
- }
2037
- async validatePrimarySummaryRetryMutation(agent, requestId, retryDispatch, compiledRequest, artifacts, phase) {
2038
- const branch = agent.getCurrentBranchGeneration();
2039
- if (!branch || branch.id !== retryDispatch.branch.id || branch.generation !== retryDispatch.branch.generation) {
2040
- return {
2041
- ok: false,
2042
- reason: `primary_summary_fallback_branch_changed_${phase}`,
2043
- data: {
2044
- expectedBranchId: retryDispatch.branch.id,
2045
- expectedGeneration: retryDispatch.branch.generation,
2046
- actualBranchId: branch?.id,
2047
- actualGeneration: branch?.generation,
2048
- },
2049
- };
2050
- }
2051
- const requestHash = this.hashJson(compiledRequest);
2052
- if (requestHash !== retryDispatch.expectedRetryRequestHash) {
2053
- return {
2054
- ok: false,
2055
- reason: `primary_summary_fallback_request_changed_${phase}`,
2056
- data: {
2057
- expectedRetryRequestHash: retryDispatch.expectedRetryRequestHash,
2058
- actualRetryRequestHash: requestHash,
2059
- },
2060
- };
2061
- }
2062
- if (this.primarySummaryProjectionKeys(artifacts.compileResult.primarySummaryProjection ?? null).join('\u0000') !==
2063
- retryDispatch.originalProjectionKeys.join('\u0000')) {
2064
- return {
2065
- ok: false,
2066
- reason: `primary_summary_fallback_projection_changed_${phase}`,
2067
- data: {
2068
- expectedProjectionKeys: retryDispatch.originalProjectionKeys,
2069
- actualProjectionKeys: this.primarySummaryProjectionKeys(artifacts.compileResult.primarySummaryProjection ?? null),
2070
- },
2071
- };
2072
- }
2073
- if (!this.primarySummaryContractsMatch(artifacts.contract, retryDispatch.originalArtifacts.contract)) {
2074
- return {
2075
- ok: false,
2076
- reason: `primary_summary_fallback_contract_changed_${phase}`,
2077
- data: {
2078
- expectedContract: retryDispatch.originalArtifacts.contract,
2079
- actualContract: artifacts.contract,
2080
- },
2081
- };
2082
- }
2083
- const original = await this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId);
2084
- if (!original?.fallbackIntent ||
2085
- original.fallbackIntent.intentId !== retryDispatch.intentId ||
2086
- original.fallbackIntent.retryRequestId !== requestId ||
2087
- original.fallbackStatus !== 'pending') {
2088
- return {
2089
- ok: false,
2090
- reason: `primary_summary_fallback_intent_changed_${phase}`,
2091
- data: {
2092
- expectedIntentId: retryDispatch.intentId,
2093
- actualIntentId: original?.fallbackIntent?.intentId,
2094
- retryRequestId: original?.fallbackIntent?.retryRequestId,
2095
- fallbackStatus: original?.fallbackStatus,
2096
- },
2097
- };
2098
- }
2099
- return { ok: true };
2100
- }
2101
- async recordPrimarySummaryRetryQuarantine(originalRequestId, intentId) {
2102
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2103
- const intent = record.fallbackIntent;
2104
- if (!intent || intent.intentId !== intentId || intent.quarantinePersistedAt)
2105
- return record;
2106
- return {
2107
- ...record,
2108
- fallbackIntent: {
2109
- ...intent,
2110
- quarantinePersistedAt: Date.now(),
2111
- },
2112
- };
2113
- });
2114
- }
2115
- async recordPrimarySummaryRetryOutput(originalRequestId, intentId, output) {
2116
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2117
- const intent = record.fallbackIntent;
2118
- if (!intent || intent.intentId !== intentId)
2119
- return record;
2120
- const existing = intent.outputs?.find((entry) => entry.phase === output.phase);
2121
- if (existing &&
2122
- existing.contentHash === output.contentHash &&
2123
- existing.messageId === output.messageId) {
2124
- return record;
2125
- }
2126
- if (existing) {
2127
- throw new Error(`Primary summary fallback output phase '${output.phase}' changed after persistence`);
2128
- }
2129
- return {
2130
- ...record,
2131
- fallbackIntent: {
2132
- ...intent,
2133
- outputs: [...(intent.outputs ?? []), output],
2134
- },
2135
- };
2136
- });
2137
- }
2138
- async recordPrimarySummaryRetryToolDispatch(originalRequestId, intentId, call) {
2139
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2140
- const intent = record.fallbackIntent;
2141
- if (!intent || intent.intentId !== intentId)
2142
- return record;
2143
- const inputHash = this.hashJson(call.input);
2144
- const existing = intent.toolDispatches?.find((entry) => entry.callId === call.id);
2145
- if (existing?.inputHash === inputHash)
2146
- return record;
2147
- if (existing) {
2148
- throw new Error(`Primary summary fallback tool dispatch '${call.id}' changed after persistence`);
2149
- }
2150
- return {
2151
- ...record,
2152
- fallbackIntent: {
2153
- ...intent,
2154
- toolDispatches: [
2155
- ...(intent.toolDispatches ?? []),
2156
- {
2157
- callId: call.id,
2158
- inputHash,
2159
- recordedAt: Date.now(),
2160
- },
2161
- ],
2162
- },
2163
- };
2164
- });
2165
- }
2166
- async resolveContextBudgetRestartDispatch(requestId, retryDispatch) {
2167
- const retryIdentity = retryDispatch
2168
- ? {
2169
- requestId,
2170
- dispatchKind: 'primary_summary_fallback_retry',
2171
- originalRequestId: retryDispatch.originalRequestId,
2172
- intentId: retryDispatch.intentId,
2173
- }
2174
- : null;
2175
- const record = await this.getPrimarySummaryRequestRecord(requestId);
2176
- if (record?.dispatchKind === 'primary_summary_fallback_retry' && retryIdentity) {
2177
- return retryIdentity;
2178
- }
2179
- if (retryIdentity) {
2180
- const original = await this.getPrimarySummaryRequestRecord(retryIdentity.originalRequestId);
2181
- if (original?.fallbackIntent?.intentId === retryIdentity.intentId
2182
- && original.fallbackIntent.retryRequestId === requestId) {
2183
- return retryIdentity;
2184
- }
2185
- }
2186
- return {
2187
- requestId,
2188
- dispatchKind: 'primary',
2189
- };
2190
- }
2191
- contextBudgetRestartMatchesIdentity(request, identity) {
2192
- if (request.reason !== 'context_budget_restart')
2193
- return false;
2194
- const restart = request.contextBudgetRestart;
2195
- if (!restart)
2196
- return false;
2197
- if (restart.dispatchKind !== identity.dispatchKind)
2198
- return false;
2199
- if (restart.requestId !== identity.requestId)
2200
- return false;
2201
- if (identity.dispatchKind !== 'primary_summary_fallback_retry')
2202
- return true;
2203
- return restart.originalRequestId === identity.originalRequestId
2204
- && restart.intentId === identity.intentId;
2205
- }
2206
- clearQueuedContextBudgetRestart(identity) {
2207
- const before = this.pendingRequests.length;
2208
- this.pendingRequests = this.pendingRequests.filter((request) => !this.contextBudgetRestartMatchesIdentity(request, identity));
2209
- return before - this.pendingRequests.length;
2210
- }
2211
- async markPrimarySummaryRetrySuccess(requestId, retryDispatch, stopReason, providerInputTokens, visibleAssistantOutput, executedToolCalls) {
2212
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2213
- ...record,
2214
- stopReason,
2215
- providerInputTokens,
2216
- visibleAssistantOutput,
2217
- executedToolCalls,
2218
- finalStatus: 'success',
2219
- fallbackHeldReason: undefined,
2220
- }));
2221
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => {
2222
- const intent = record.fallbackIntent;
2223
- return {
2224
- ...record,
2225
- finalStatus: 'success',
2226
- fallbackStatus: 'success',
2227
- fallbackHeldReason: undefined,
2228
- fallbackIntent: intent && intent.intentId === retryDispatch.intentId
2229
- ? { ...intent, finalizedAt: Date.now() }
2230
- : intent,
2231
- };
2232
- });
2233
- }
2234
- async holdPrimarySummaryRetryFamily(agent, requestId, retryDispatch, reason, data, options) {
2235
- const clearedQueuedContextBudgetRestarts = this.clearQueuedContextBudgetRestart({
2236
- requestId,
2237
- dispatchKind: 'primary_summary_fallback_retry',
2238
- originalRequestId: retryDispatch.originalRequestId,
2239
- intentId: retryDispatch.intentId,
2240
- });
2241
- const holdData = clearedQueuedContextBudgetRestarts > 0
2242
- ? { ...(data ?? {}), clearedQueuedContextBudgetRestarts }
2243
- : data;
2244
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2245
- ...record,
2246
- ...(options?.stopReason ? { stopReason: options.stopReason } : {}),
2247
- finalStatus: 'held',
2248
- fallbackHeldReason: reason,
2249
- }));
2250
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => ({
2251
- ...record,
2252
- finalStatus: 'held',
2253
- fallbackStatus: 'held',
2254
- fallbackHeldReason: reason,
2255
- }));
2256
- this.opsAlert('refusal-held', agent.name, reason, { data: holdData });
2257
- if (options?.emitChronicleMarker === false)
2258
- return;
2259
- try {
2260
- agent.getContextManager().addMessage('user', [{
2261
- type: 'text',
2262
- text: `[refusal-held] Your previous turn was refused by the provider and ` +
2263
- `is being held for operator review rather than silently retried. ` +
2264
- `Automatic raw-expansion recovery was either unsafe or unsuccessful.`,
2265
- }], { system: true, kind: 'refusal-held', reason });
2266
- }
2267
- catch (error) {
2268
- console.error('[refusal-held] could not record chronicle marker:', error);
2269
- }
2270
- }
2271
- async primarySummaryRetryAlreadySucceeded(requestId, retryDispatch) {
2272
- const [retryRecord, originalRecord] = await Promise.all([
2273
- this.getPrimarySummaryRequestRecord(requestId),
2274
- this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId),
2275
- ]);
2276
- return retryRecord?.finalStatus === 'success'
2277
- || originalRecord?.fallbackStatus === 'success'
2278
- || originalRecord?.finalStatus === 'success';
2279
- }
2280
- async primarySummaryRetryFamilyAlreadySettled(requestId, retryDispatch) {
2281
- const [retryRecord, originalRecord] = await Promise.all([
2282
- this.getPrimarySummaryRequestRecord(requestId),
2283
- this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId),
2284
- ]);
2285
- return retryRecord?.finalStatus === 'held'
2286
- || retryRecord?.finalStatus === 'success'
2287
- || originalRecord?.fallbackStatus === 'held'
2288
- || originalRecord?.fallbackStatus === 'success'
2289
- || originalRecord?.finalStatus === 'held'
2290
- || originalRecord?.finalStatus === 'success';
2291
- }
2292
- async holdPrimarySummaryRequest(agent, requestId, reason, data) {
2293
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2294
- ...record,
2295
- finalStatus: 'held',
2296
- fallbackStatus: 'held',
2297
- fallbackHeldReason: reason,
2298
- }));
2299
- this.opsAlert('refusal-held', agent.name, reason, { data });
2300
- try {
2301
- agent.getContextManager().addMessage('user', [{
2302
- type: 'text',
2303
- text: `[refusal-held] Your previous turn was refused by the provider and ` +
2304
- `is being held for operator review rather than silently retried. ` +
2305
- `Automatic raw-expansion recovery was either unsafe or unsuccessful.`,
2306
- }], { system: true, kind: 'refusal-held', reason });
2307
- }
2308
- catch (error) {
2309
- console.error('[refusal-held] could not record chronicle marker:', error);
2310
- }
2311
- }
2312
- hasVisibleAssistantOutput(blocks) {
2313
- return blocks.some((block) => block.type !== 'thinking' && block.type !== 'redacted_thinking');
2314
- }
2315
- buildPrimarySummaryRequestRecord(agent, requestId, dispatchKind, request, artifacts, branch, stopReason, visibleAssistantOutput, executedToolCalls, providerInputTokens, finalStatus) {
2316
- const projection = artifacts?.compileResult.primarySummaryProjection ?? null;
2317
- return {
2318
- requestId,
2319
- agentName: agent.name,
2320
- namespace: projection?.namespace ?? `agents/${agent.name}`,
2321
- timestamp: Date.now(),
2322
- dispatchKind,
2323
- branch: branch
2324
- ? { id: branch.id, name: branch.name, generation: branch.generation }
2325
- : { id: 'unknown', name: 'unknown', generation: -1 },
2326
- projection,
2327
- requestInputBoundTokens: this.estimateRequestInputBoundTokens(request),
2328
- requestCompleteBoundTokens: this.estimateRequestCompleteBoundTokens(request),
2329
- providerInputTokens,
2330
- systemHash: artifacts?.contract.systemHash ?? this.hashJson(request.system ?? ''),
2331
- modelConfigHash: artifacts?.contract.modelConfigHash ?? this.hashJson({ request: request.config }),
2332
- toolContractHash: artifacts?.contract.toolContractHash ?? this.hashJson(request.tools ?? []),
2333
- stopReason,
2334
- visibleAssistantOutput,
2335
- executedToolCalls,
2336
- finalStatus,
2337
- };
2338
- }
2339
1259
  /** Liveness ping for an active ephemeral run (no-op otherwise). */
2340
1260
  touchEphemeralRun(agentName, inferenceStarted = false) {
2341
1261
  const run = this.ephemeralRuns.get(agentName);
@@ -2635,6 +1555,27 @@ export class AgentFramework {
2635
1555
  required: ['path'],
2636
1556
  },
2637
1557
  };
1558
+ /** Synthesized read_image tool — present when a workspace module is
1559
+ * registered. Returns the image as a native image block in the tool
1560
+ * result, so the agent SEES it in the live turn (history keeps a compact
1561
+ * placeholder via the standard tool-result serializer). */
1562
+ static READ_IMAGE_TOOL = {
1563
+ name: 'read_image',
1564
+ description: 'View an image file from a workspace mount. The image is returned into ' +
1565
+ 'your context so you can actually see it (e.g. after save_recent_image, ' +
1566
+ 'or for images placed in the workspace by other means). Path is ' +
1567
+ 'mount-prefixed, e.g. "project/photos/cat.png". Supports png/jpeg/gif/webp.',
1568
+ inputSchema: {
1569
+ type: 'object',
1570
+ properties: {
1571
+ path: {
1572
+ type: 'string',
1573
+ description: 'Mount-prefixed image path, e.g. "project/photos/name.png".',
1574
+ },
1575
+ },
1576
+ required: ['path'],
1577
+ },
1578
+ };
2638
1579
  static AGENT_SETTINGS_TOOL = {
2639
1580
  name: 'agent_settings',
2640
1581
  description: 'Read or change your hot runtime settings. This intentionally exposes only ' +
@@ -3684,7 +2625,6 @@ export class AgentFramework {
3684
2625
  // Cast to AgentState to bypass TypeScript's control flow narrowing
3685
2626
  const currentState = agent.state;
3686
2627
  if (currentState.status === 'ready') {
3687
- const activeStreamContext = this.activeStreamContexts.get(agent.name);
3688
2628
  // Flush pending assistant blocks (tool_use + preamble text) to context
3689
2629
  const pendingBlocks = this.pendingAssistantBlocks.get(agent.name);
3690
2630
  if (pendingBlocks) {
@@ -3795,34 +2735,6 @@ export class AgentFramework {
3795
2735
  // a terminal failure — rejecting an ephemeral's promise mid-run
3796
2736
  // and emitting a spurious inference:exhausted (same race shape as
3797
2737
  // endTurn above).
3798
- const requestId = activeStreamContext?.requestId;
3799
- const primarySummaryRetry = activeStreamContext?.trigger?.primarySummaryFallbackRetry;
3800
- const compiledRequest = activeStreamContext?.compiledRequest;
3801
- const artifacts = activeStreamContext?.artifacts;
3802
- const restartDispatch = await this.resolveContextBudgetRestartDispatch(requestId ?? `${agent.name}:context_budget_restart`, primarySummaryRetry);
3803
- if (restartDispatch.dispatchKind === 'primary_summary_fallback_retry'
3804
- && requestId
3805
- && primarySummaryRetry
3806
- && compiledRequest
3807
- && artifacts) {
3808
- const validation = await this.validatePrimarySummaryRetryMutation(agent, requestId, primarySummaryRetry, compiledRequest, artifacts, 'before_context_budget_restart_hold');
3809
- if (!validation.ok) {
3810
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, validation.reason, validation.data, { emitChronicleMarker: false, stopReason: 'abort' });
3811
- }
3812
- else {
3813
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, 'primary_summary_fallback_context_budget_restart_required', {
3814
- inputTokens: agent.lastStreamInputTokens,
3815
- budget: agent.maxStreamTokens,
3816
- originalRequestId: primarySummaryRetry.originalRequestId,
3817
- intentId: primarySummaryRetry.intentId,
3818
- }, { emitChronicleMarker: false, stopReason: 'abort' });
3819
- }
3820
- this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
3821
- currentState.stream?.cancel();
3822
- agent.reset();
3823
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
3824
- return;
3825
- }
3826
2738
  if (currentState.stream) {
3827
2739
  this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
3828
2740
  }
@@ -3839,8 +2751,6 @@ export class AgentFramework {
3839
2751
  reason: 'context_budget_restart',
3840
2752
  source: 'framework',
3841
2753
  timestamp: Date.now(),
3842
- channelId: activeStreamContext?.trigger?.channelId,
3843
- contextBudgetRestart: restartDispatch,
3844
2754
  });
3845
2755
  }
3846
2756
  else if (currentState.stream) {
@@ -4255,17 +3165,29 @@ export class AgentFramework {
4255
3165
  ? event.origin.messageId
4256
3166
  : event.eventId;
4257
3167
  const maxBackscroll = descriptor?.capabilities?.history?.maxMessages ?? 0;
3168
+ // Resolve human-readable names from the event origin wherever the
3169
+ // surface provided them — a mind reading this should see WHO addressed
3170
+ // it WHERE, not a wall of snowflake ids. Raw ids remain only inside the
3171
+ // tool-argument instructions, where they are the literal values to pass.
3172
+ const origin = (event.origin ?? {});
3173
+ const authorName = typeof origin.authorName === 'string' && origin.authorName ? origin.authorName : 'Someone';
3174
+ const channelLabel = triggerChannel.label ? `#${triggerChannel.label}` : `"${triggerChannel.channelId}"`;
3175
+ const place = typeof origin.guildName === 'string' && origin.guildName
3176
+ ? `${channelLabel} in "${origin.guildName}"`
3177
+ : channelLabel;
4258
3178
  content.push({
4259
3179
  type: 'text',
4260
- text: `\n[Channel invitation] You were addressed in closed channel ` +
4261
- `"${triggerChannel.label ?? triggerChannel.channelId}" (${triggerChannel.channelId}, server ${event.serverId}). ` +
4262
- `You received this one message without subscribing. ` +
3180
+ text: `\n[Channel invitation] ${authorName} addressed you in ${place} — ` +
3181
+ `a channel you haven't joined. You're seeing this one message; you won't receive more from it unless you join. ` +
3182
+ `Your options:\n` +
3183
+ `1. Reply without joining — simply write your reply as normal text this turn; it will be delivered to ${channelLabel}.\n` +
3184
+ `2. Join the channel — call channel_open with channelId "${triggerChannel.channelId}" and serverId "${event.serverId}"` +
4263
3185
  (maxBackscroll > 0
4264
- ? `To subscribe, call channel_open with channelId, serverId "${event.serverId}", backscroll (0-${maxBackscroll}), and beforeMessageId "${messageId}". `
4265
- : `To subscribe, call channel_open with this channelId and serverId "${event.serverId}". `) +
4266
- `You may reply once without subscribing. To remain closed, call channel_decline ` +
4267
- `with channelId, serverId "${event.serverId}", and messageId "${messageId}"; ` +
4268
- `optionally set acknowledge to a surface value such as 👀.`,
3186
+ ? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId "${messageId}".\n`
3187
+ : `.\n`) +
3188
+ `3. Stay out — call channel_decline with channelId "${triggerChannel.channelId}", serverId "${event.serverId}", ` +
3189
+ `and messageId "${messageId}" (optionally set acknowledge to an emoji like 👀 so ${authorName} isn't left hanging).\n` +
3190
+ `Doing nothing is also fine.`,
4269
3191
  });
4270
3192
  metadata.channelInvitation = true;
4271
3193
  metadata.channelOpen = false;
@@ -4338,8 +3260,7 @@ export class AgentFramework {
4338
3260
  // Clear pending (we'll re-add if inference doesn't run)
4339
3261
  this.pendingRequests = [];
4340
3262
  // Check each agent
4341
- for (const [agentName, queuedRequests] of requestsByAgent) {
4342
- let requests = queuedRequests;
3263
+ for (const [agentName, requests] of requestsByAgent) {
4343
3264
  const agent = this.agents.get(agentName);
4344
3265
  if (!agent) {
4345
3266
  // Agent not found — request is orphaned. Emit warning and drop.
@@ -4378,15 +3299,6 @@ export class AgentFramework {
4378
3299
  this.pendingRequests.push(...requests);
4379
3300
  continue;
4380
3301
  }
4381
- if (this.discordAwarenessBarrier) {
4382
- this.pendingRequests.push(...requests);
4383
- continue;
4384
- }
4385
- requests = requests.filter((request) => request.reason !== 'context_budget_restart'
4386
- || request.contextBudgetRestart?.dispatchKind !== 'primary_summary_fallback_retry');
4387
- if (requests.length === 0) {
4388
- continue;
4389
- }
4390
3302
  // Check policy
4391
3303
  if (!this.inferencePolicy.shouldInfer(agentName, requests, state)) {
4392
3304
  // Loud drop: a queued request that dies here is otherwise invisible —
@@ -4397,13 +3309,7 @@ export class AgentFramework {
4397
3309
  continue;
4398
3310
  }
4399
3311
  // Start streaming inference (non-blocking — driveStream runs in background)
4400
- const trigger = requests.find((request) => request.primarySummaryFallbackRetry) ?? requests[0];
4401
- if (trigger.primarySummaryFallbackRetry) {
4402
- for (const request of requests) {
4403
- if (request !== trigger)
4404
- this.pendingRequests.push(request);
4405
- }
4406
- }
3312
+ const trigger = requests[0];
4407
3313
  // Route this turn's auto-published speech to the channel that triggered
4408
3314
  // it (item-3 redux). A batched wake may carry several triggering channels
4409
3315
  // (messages arrived in >1 channel while the agent was busy/idle) — reply
@@ -4418,7 +3324,7 @@ export class AgentFramework {
4418
3324
  }
4419
3325
  async startAgentStream(agent, trigger, attempt = 0) {
4420
3326
  // Record turn checkpoint before inference (only on first attempt, not retries)
4421
- if (attempt === 0 && trigger?.reason !== 'primary-summary-fallback-retry') {
3327
+ if (attempt === 0) {
4422
3328
  this.recordTurnCheckpoint(agent.name);
4423
3329
  this.redoStacks.delete(agent.name); // new work invalidates redo
4424
3330
  }
@@ -4438,180 +3344,46 @@ export class AgentFramework {
4438
3344
  this.emitTrace({ type: 'inference:started', agentName: agent.name });
4439
3345
  this.eventGate?.onInferenceStarted(agent.name);
4440
3346
  this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), startedAt: Date.now() });
4441
- const requestId = `${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4442
3347
  try {
4443
3348
  const requestSnapshot = this.captureInferenceToolSnapshot(agent);
4444
3349
  const allTools = this.getToolsForAgent(agent.name, requestSnapshot);
4445
3350
  const tools = allTools.filter((t) => agent.canUseTool(t.name));
4446
- const fallbackConfig = this.primarySummaryFallbackConfig(agent);
4447
- const retryDispatch = trigger?.primarySummaryFallbackRetry;
4448
- let compiledRequest;
4449
- let artifacts;
4450
- let branch = agent.getCurrentBranchGeneration();
4451
- if (retryDispatch) {
4452
- if (!fallbackConfig.enabled) {
4453
- await this.holdPrimarySummaryRequest(agent, retryDispatch.originalRequestId, 'primary_summary_fallback_disabled_before_retry');
4454
- agent.reset();
4455
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4456
- this.eventGate?.onInferenceEnded(agent.name);
4457
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
4458
- return;
4459
- }
4460
- const claim = await this.claimPrimarySummaryRetryIntent(retryDispatch.originalRequestId, retryDispatch.intentId, requestId);
4461
- if (claim !== 'claimed') {
4462
- await this.holdPrimarySummaryRequest(agent, retryDispatch.originalRequestId, claim === 'duplicate'
4463
- ? 'primary_summary_fallback_duplicate_retry_blocked'
4464
- : 'primary_summary_fallback_retry_intent_missing', { intentId: retryDispatch.intentId, retryRequestId: requestId });
4465
- agent.reset();
4466
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4467
- this.eventGate?.onInferenceEnded(agent.name);
4468
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
4469
- return;
4470
- }
4471
- if (!branch ||
4472
- branch.id !== retryDispatch.branch.id ||
4473
- branch.generation !== retryDispatch.branch.generation) {
4474
- await this.holdPrimarySummaryRequest(agent, retryDispatch.originalRequestId, 'primary_summary_fallback_branch_changed_before_retry', {
4475
- expectedBranchId: retryDispatch.branch.id,
4476
- expectedGeneration: retryDispatch.branch.generation,
4477
- actualBranchId: branch?.id,
4478
- actualGeneration: branch?.generation,
4479
- });
4480
- agent.reset();
4481
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4482
- this.eventGate?.onInferenceEnded(agent.name);
4483
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
4484
- return;
4485
- }
4486
- const rebuilt = agent.buildPrimarySummaryRawExpansionRequest(retryDispatch.originalArtifacts, retryDispatch.candidateSummaries, tools);
4487
- compiledRequest = rebuilt.request;
4488
- artifacts = rebuilt.artifacts;
4489
- const rebuiltHash = this.hashJson(compiledRequest);
4490
- if (rebuiltHash !== retryDispatch.expectedRetryRequestHash ||
4491
- this.primarySummaryProjectionKeys(artifacts.compileResult.primarySummaryProjection ?? null).join('\u0000') !==
4492
- retryDispatch.originalProjectionKeys.join('\u0000')) {
4493
- await this.holdPrimarySummaryRequest(agent, retryDispatch.originalRequestId, 'primary_summary_fallback_revalidation_failed_before_retry', {
4494
- expectedRetryRequestHash: retryDispatch.expectedRetryRequestHash,
4495
- actualRetryRequestHash: rebuiltHash,
4496
- });
4497
- agent.reset();
4498
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4499
- this.eventGate?.onInferenceEnded(agent.name);
4500
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
4501
- return;
4502
- }
4503
- const retryRecord = this.buildPrimarySummaryRequestRecord(agent, requestId, 'primary_summary_fallback_retry', compiledRequest, artifacts, branch, undefined, false, 0, undefined, 'pending');
4504
- await this.persistPrimarySummaryRequestRecord(retryRecord);
4505
- const admittedTokens = this.primarySummaryAdmissionBoundTokens(retryDispatch.originalRequestInputBoundTokens, compiledRequest, retryDispatch.originalProviderInputTokens);
4506
- if (admittedTokens > retryDispatch.requestBudgetTokens) {
4507
- await this.holdPrimarySummaryRequest(agent, requestId, 'primary_summary_fallback_retry_over_budget', {
4508
- admittedTokens,
4509
- requestBudgetTokens: retryDispatch.requestBudgetTokens,
4510
- retryRequestHash: rebuiltHash,
4511
- });
4512
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => ({
4513
- ...record,
4514
- fallbackStatus: 'held',
4515
- fallbackHeldReason: 'primary_summary_fallback_retry_over_budget',
4516
- }));
4517
- agent.reset();
4518
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4519
- this.eventGate?.onInferenceEnded(agent.name);
4520
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
4521
- return;
3351
+ // Gather context from modules (pull-based) and MCPL hooks (push-based)
3352
+ // Both produce ContextInjection[] that get merged before inference.
3353
+ let injections;
3354
+ // Module gatherContext (fail-open, per-module timeout — the module's
3355
+ // contextTimeoutMs, else the registry default). Injections are
3356
+ // channel-scoped via scopeInjectionsForAgent, matching the MCPL hook
3357
+ // injections below, so conversation forks don't see cross-channel
3358
+ // content.
3359
+ try {
3360
+ const moduleInjections = this.scopeInjectionsForAgent(agent.name, await this.moduleRegistry.gatherContext(agent.name));
3361
+ if (moduleInjections.length > 0) {
3362
+ injections = moduleInjections;
4522
3363
  }
4523
3364
  }
4524
- else {
4525
- // Gather context from modules (pull-based) and MCPL hooks (push-based)
4526
- // Both produce ContextInjection[] that get merged before inference.
4527
- let injections;
4528
- // Module gatherContext (fail-open, per-module timeout — the module's
4529
- // contextTimeoutMs, else the registry default). Injections are
4530
- // channel-scoped via scopeInjectionsForAgent, matching the MCPL hook
4531
- // injections below, so conversation forks don't see cross-channel
4532
- // content.
3365
+ catch (error) {
3366
+ console.error('Module gatherContext error:', error);
3367
+ }
3368
+ // MCPL beforeInference hooks (fail-open)
3369
+ if (this.hookOrchestrator) {
4533
3370
  try {
4534
- const moduleInjections = this.scopeInjectionsForAgent(agent.name, await this.moduleRegistry.gatherContext(agent.name));
4535
- if (moduleInjections.length > 0) {
4536
- injections = moduleInjections;
3371
+ const hookParams = this.buildBeforeInferenceParams(agent, trigger);
3372
+ const hookInjections = this.scopeInjectionsForAgent(agent.name, await this.hookOrchestrator.beforeInference(hookParams));
3373
+ if (hookInjections.length > 0) {
3374
+ injections = injections ? [...injections, ...hookInjections] : hookInjections;
4537
3375
  }
4538
3376
  }
4539
3377
  catch (error) {
4540
- console.error('Module gatherContext error:', error);
4541
- }
4542
- // MCPL beforeInference hooks (fail-open)
4543
- if (this.hookOrchestrator) {
4544
- try {
4545
- const hookParams = this.buildBeforeInferenceParams(agent, trigger);
4546
- const hookInjections = this.scopeInjectionsForAgent(agent.name, await this.hookOrchestrator.beforeInference(hookParams));
4547
- if (hookInjections.length > 0) {
4548
- injections = injections ? [...injections, ...hookInjections] : hookInjections;
4549
- }
4550
- }
4551
- catch (error) {
4552
- console.error('beforeInference hook error:', error);
4553
- }
4554
- }
4555
- const prepared = await agent.prepareActivationRequest(tools, injections);
4556
- compiledRequest = prepared.request;
4557
- artifacts = prepared.artifacts;
4558
- branch = agent.getCurrentBranchGeneration();
4559
- if (fallbackConfig.enabled) {
4560
- const record = this.buildPrimarySummaryRequestRecord(agent, requestId, 'primary', compiledRequest, artifacts, branch, undefined, false, 0, undefined, 'pending');
4561
- await this.persistPrimarySummaryRequestRecord(record);
4562
- const expandedByQuarantine = artifacts.compileResult.primarySummaryProjection?.selectedSummaries.some((selection) => selection.renderedAs === 'raw_expansion') ?? false;
4563
- if (expandedByQuarantine) {
4564
- const admittedTokens = this.estimateRequestCompleteBoundTokens(compiledRequest);
4565
- if (admittedTokens > fallbackConfig.requestBudgetTokens) {
4566
- await this.holdPrimarySummaryRequest(agent, requestId, 'primary_summary_quarantine_over_budget', {
4567
- admittedTokens,
4568
- requestBudgetTokens: fallbackConfig.requestBudgetTokens,
4569
- });
4570
- agent.reset();
4571
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4572
- this.eventGate?.onInferenceEnded(agent.name);
4573
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
4574
- return;
4575
- }
4576
- }
3378
+ console.error('beforeInference hook error:', error);
4577
3379
  }
4578
3380
  }
4579
- const { stream } = agent.startStreamFromRequest(compiledRequest, artifacts);
4580
- this.activeStreamContexts.set(agent.name, {
4581
- requestId,
4582
- trigger,
4583
- compiledRequest,
4584
- artifacts,
4585
- });
4586
- const handle = this.driveStream(agent, stream, requestSnapshot, requestId, trigger, attempt, compiledRequest, artifacts);
3381
+ const { stream, request: compiledRequest } = await agent.startStreamWithInjections(tools, injections);
3382
+ const handle = this.driveStream(agent, stream, requestSnapshot, trigger, attempt, compiledRequest);
4587
3383
  this.activeStreams.set(agent.name, handle);
4588
3384
  }
4589
3385
  catch (error) {
4590
3386
  const err = error instanceof Error ? error : new Error(String(error));
4591
- if (trigger?.primarySummaryFallbackRetry) {
4592
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
4593
- ...record,
4594
- finalStatus: 'held',
4595
- fallbackHeldReason: err.message,
4596
- }));
4597
- await this.updatePrimarySummaryRequestRecord(trigger.primarySummaryFallbackRetry.originalRequestId, (record) => ({
4598
- ...record,
4599
- finalStatus: 'held',
4600
- fallbackStatus: 'held',
4601
- fallbackHeldReason: err.message,
4602
- }));
4603
- this.opsAlert('refusal-held', agent.name, err.message, {
4604
- data: { requestId, originalRequestId: trigger.primarySummaryFallbackRetry.originalRequestId },
4605
- });
4606
- agent.reset();
4607
- this.settleAgent(agent.name, {
4608
- stopReason: 'completed',
4609
- speech: '',
4610
- });
4611
- this.eventGate?.onInferenceEnded(agent.name);
4612
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
4613
- return;
4614
- }
4615
3387
  this.emitTrace({
4616
3388
  type: 'inference:failed',
4617
3389
  agentName: agent.name,
@@ -4650,17 +3422,11 @@ export class AgentFramework {
4650
3422
  }
4651
3423
  }
4652
3424
  }
4653
- async driveStream(agent, stream, requestSnapshot, requestId, trigger, attempt = 0, compiledRequest, artifacts) {
3425
+ async driveStream(agent, stream, requestSnapshot, trigger, attempt = 0, compiledRequest) {
4654
3426
  const startTime = Date.now();
3427
+ const requestId = `${agent.name}-${startTime}-${Math.random().toString(36).slice(2, 8)}`;
4655
3428
  const myStreamId = agent.streamId;
4656
3429
  let hadToolCalls = false;
4657
- let executedToolCalls = 0;
4658
- const primarySummaryConfig = this.primarySummaryFallbackConfig(agent);
4659
- const primarySummaryRetry = trigger?.primarySummaryFallbackRetry;
4660
- let primarySummaryQuarantinePersisted = false;
4661
- const fallbackRetryRequestForLog = primarySummaryRetry && compiledRequest
4662
- ? this.summarizePrimarySummaryRetryRequest(compiledRequest)
4663
- : compiledRequest ?? { note: 'streaming request' };
4664
3430
  // ---- Present-while-acting turn state ---------------------------------
4665
3431
  // Output locus for the current CONVERSATIONAL ROUND: resolved lazily and
4666
3432
  // kept stable across tool-only rounds. A channel message injected at a
@@ -4668,7 +3434,7 @@ export class AgentFramework {
4668
3434
  // single provider inference can now contain several conversations. The
4669
3435
  // pin lives in `turnLocusPins` so it survives a context-budget restart. A
4670
3436
  // null resolution is not pinned.
4671
- if (trigger?.reason !== 'context_budget_restart' && trigger?.reason !== 'primary-summary-fallback-retry') {
3437
+ if (trigger?.reason !== 'context_budget_restart') {
4672
3438
  this.turnLocusPins.delete(agent.name);
4673
3439
  this.midTurnRoutingResets.delete(agent.name);
4674
3440
  }
@@ -4736,61 +3502,6 @@ export class AgentFramework {
4736
3502
  }
4737
3503
  }
4738
3504
  };
4739
- const persistPrimarySummaryRetryQuarantine = async () => {
4740
- if (!primarySummaryRetry || primarySummaryQuarantinePersisted || !artifacts)
4741
- return true;
4742
- const validation = await this.validatePrimarySummaryRetryMutation(agent, requestId, primarySummaryRetry, compiledRequest, artifacts, 'before_quarantine');
4743
- if (!validation.ok) {
4744
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, validation.reason, validation.data, { emitChronicleMarker: false });
4745
- stream.cancel();
4746
- agent.reset();
4747
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4748
- return false;
4749
- }
4750
- try {
4751
- const quarantinePrimarySummaryForPrimaryLane = agent.getContextManager().quarantinePrimarySummaryForPrimaryLane;
4752
- if (typeof quarantinePrimarySummaryForPrimaryLane !== 'function') {
4753
- throw new Error('Primary summary quarantine is unavailable in the active context strategy');
4754
- }
4755
- await quarantinePrimarySummaryForPrimaryLane.call(agent.getContextManager(), artifacts.contract, primarySummaryRetry.candidateSummaries);
4756
- primarySummaryQuarantinePersisted = true;
4757
- await this.recordPrimarySummaryRetryQuarantine(primarySummaryRetry.originalRequestId, primarySummaryRetry.intentId);
4758
- return true;
4759
- }
4760
- catch (error) {
4761
- const reason = 'primary_summary_fallback_quarantine_failed';
4762
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, reason, {
4763
- error: error instanceof Error ? error.message : String(error),
4764
- });
4765
- stream.cancel();
4766
- agent.reset();
4767
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4768
- return false;
4769
- }
4770
- };
4771
- const persistPrimarySummaryRetryOutput = async (phase, blocks, participant = agent.name, metadata) => {
4772
- if (!primarySummaryRetry || !artifacts)
4773
- return true;
4774
- if (blocks.length === 0)
4775
- return true;
4776
- const validation = await this.validatePrimarySummaryRetryMutation(agent, requestId, primarySummaryRetry, compiledRequest, artifacts, `before_${phase}`);
4777
- if (!validation.ok) {
4778
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, validation.reason, validation.data, { emitChronicleMarker: false });
4779
- stream.cancel();
4780
- agent.reset();
4781
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4782
- return false;
4783
- }
4784
- const messageId = agent.getContextManager().addMessage(participant, blocks, metadata);
4785
- await this.recordPrimarySummaryRetryOutput(primarySummaryRetry.originalRequestId, primarySummaryRetry.intentId, {
4786
- phase,
4787
- contentHash: this.primarySummaryFallbackOutputPhaseHash(blocks),
4788
- messageId,
4789
- blockTypes: this.primarySummaryFallbackBlockTypes(blocks),
4790
- persistedAt: Date.now(),
4791
- });
4792
- return true;
4793
- };
4794
3505
  try {
4795
3506
  for await (const event of stream) {
4796
3507
  this.touchEphemeralRun(agent.name, true);
@@ -4818,6 +3529,7 @@ export class AgentFramework {
4818
3529
  case 'tool-calls': {
4819
3530
  adoptInjectedRound();
4820
3531
  hadToolCalls = true;
3532
+ this.recordEphemeralToolCalls(agent.name, event.calls.length);
4821
3533
  this.emitTrace({
4822
3534
  type: 'inference:tool_calls_yielded',
4823
3535
  agentName: agent.name,
@@ -4851,30 +3563,7 @@ export class AgentFramework {
4851
3563
  // Truncated responses yield 'complete' with stop_reason 'max_tokens' directly.
4852
3564
  // So all calls here have valid input — {} is legitimate for parameterless tools.
4853
3565
  agent.enterWaitingForTools(event.calls, stream);
4854
- if (!(await persistPrimarySummaryRetryQuarantine())) {
4855
- return;
4856
- }
4857
- if (primarySummaryRetry) {
4858
- const phase = `tool_round_${event.context.depth ?? 0}`;
4859
- if (!(await persistPrimarySummaryRetryOutput(phase, assistantBlocks))) {
4860
- return;
4861
- }
4862
- this.pendingAssistantBlocks.delete(agent.name);
4863
- }
4864
- executedToolCalls += event.calls.length;
4865
- this.recordEphemeralToolCalls(agent.name, event.calls.length);
4866
3566
  for (const call of event.calls) {
4867
- if (primarySummaryRetry) {
4868
- const validation = await this.validatePrimarySummaryRetryMutation(agent, requestId, primarySummaryRetry, compiledRequest, artifacts, `before_tool_dispatch_${call.id}`);
4869
- if (!validation.ok) {
4870
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, validation.reason, validation.data, { emitChronicleMarker: false });
4871
- stream.cancel();
4872
- agent.reset();
4873
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4874
- return;
4875
- }
4876
- await this.recordPrimarySummaryRetryToolDispatch(primarySummaryRetry.originalRequestId, primarySummaryRetry.intentId, call);
4877
- }
4878
3567
  this.dispatchToolCall(agent.name, call);
4879
3568
  }
4880
3569
  // Speak-while-acting: route THIS round's prose to the locus NOW
@@ -4956,167 +3645,6 @@ export class AgentFramework {
4956
3645
  agent.getContextManager().addMessage('user', toolResultContent);
4957
3646
  }
4958
3647
  }
4959
- const lastToolIdx = response.content.reduce((last, b, i) => b.type === 'tool_use' || b.type === 'tool_result' ? i : last, -1);
4960
- const terminalContent = lastToolIdx >= 0
4961
- ? response.content.slice(lastToolIdx + 1)
4962
- : response.content;
4963
- const heldAssistantContent = response.content;
4964
- const visibleAssistantOutput = this.hasVisibleAssistantOutput(response.content);
4965
- const providerInputTokens = response.usage?.inputTokens ??
4966
- response.details?.usage?.inputTokens;
4967
- if (primarySummaryConfig.enabled && compiledRequest && artifacts && response.stopReason === 'refusal') {
4968
- const stopDetails = response.raw?.response?.stop_details;
4969
- const category = stopDetails?.category ?? 'unknown';
4970
- console.error(`[inference-refusal] agent=${agent.name} category=${category}` +
4971
- (stopDetails?.explanation ? ` explanation=${stopDetails.explanation}` : ''));
4972
- this.noteRefusal(agent.name, category, response.details?.usage
4973
- ? {
4974
- input: response.details.usage.inputTokens,
4975
- output: response.details.usage.outputTokens,
4976
- }
4977
- : undefined);
4978
- const currentRecord = await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
4979
- ...record,
4980
- stopReason: response.stopReason,
4981
- providerInputTokens,
4982
- visibleAssistantOutput,
4983
- executedToolCalls,
4984
- finalStatus: 'refusal',
4985
- }));
4986
- const holdReasonData = {
4987
- category,
4988
- visibleAssistantOutput,
4989
- executedToolCalls,
4990
- candidateLimit: primarySummaryConfig.maxNewSummaries,
4991
- };
4992
- if (primarySummaryRetry) {
4993
- const reason = visibleAssistantOutput
4994
- ? 'primary_summary_fallback_retry_partial_output'
4995
- : executedToolCalls > 0
4996
- ? 'primary_summary_fallback_retry_tool_already_executed'
4997
- : 'primary_summary_fallback_retry_refusal';
4998
- if (visibleAssistantOutput || executedToolCalls > 0) {
4999
- await this.persistPrimarySummaryHeldOutput(agent, requestId, reason, {
5000
- stopReason: response.stopReason,
5001
- providerInputTokens,
5002
- visibleAssistantOutput,
5003
- executedToolCalls,
5004
- assistantBlocks: heldAssistantContent,
5005
- retryDispatch: primarySummaryRetry,
5006
- retryPhase: 'refusal_visible_output',
5007
- holdData: holdReasonData,
5008
- });
5009
- }
5010
- else {
5011
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, reason, holdReasonData);
5012
- }
5013
- }
5014
- else if (visibleAssistantOutput || executedToolCalls > 0 || !currentRecord?.projection) {
5015
- const reason = visibleAssistantOutput
5016
- ? 'primary_summary_refusal_partial_output'
5017
- : executedToolCalls > 0
5018
- ? 'primary_summary_refusal_tool_already_executed'
5019
- : 'primary_summary_refusal_no_projection';
5020
- if (visibleAssistantOutput || executedToolCalls > 0) {
5021
- await this.persistPrimarySummaryHeldOutput(agent, requestId, reason, {
5022
- stopReason: response.stopReason,
5023
- providerInputTokens,
5024
- visibleAssistantOutput,
5025
- executedToolCalls,
5026
- assistantBlocks: heldAssistantContent,
5027
- holdData: holdReasonData,
5028
- });
5029
- }
5030
- else {
5031
- await this.holdPrimarySummaryRequest(agent, requestId, reason, holdReasonData);
5032
- }
5033
- }
5034
- else {
5035
- const state = this.readPrimarySummaryFallbackState();
5036
- const baseline = this.latestCompatibleHealthyPrimaryRequest(currentRecord, state);
5037
- if (!baseline?.projection) {
5038
- await this.holdPrimarySummaryRequest(agent, requestId, 'primary_summary_refusal_no_compatible_healthy_baseline', holdReasonData);
5039
- }
5040
- else {
5041
- const baselineKeys = new Set(this.primarySummaryProjectionKeys(baseline.projection));
5042
- const candidates = currentRecord.projection.selectedSummaries
5043
- .filter((selection) => !baselineKeys.has(this.primarySummaryIdentityKey(selection.identity)))
5044
- .map((selection) => selection.identity);
5045
- if (candidates.length === 0) {
5046
- await this.holdPrimarySummaryRequest(agent, requestId, 'primary_summary_refusal_no_newly_admitted_candidates', holdReasonData);
5047
- }
5048
- else if (candidates.length > primarySummaryConfig.maxNewSummaries) {
5049
- await this.holdPrimarySummaryRequest(agent, requestId, 'primary_summary_refusal_too_many_candidates', { ...holdReasonData, candidateCount: candidates.length });
5050
- }
5051
- else {
5052
- try {
5053
- const retry = agent.buildPrimarySummaryRawExpansionRequest(artifacts, candidates, compiledRequest.tools ?? []);
5054
- const retryRequestHash = this.hashJson(retry.request);
5055
- const admittedTokens = this.primarySummaryAdmissionBoundTokens(currentRecord.requestInputBoundTokens, retry.request, providerInputTokens);
5056
- if (admittedTokens > primarySummaryConfig.requestBudgetTokens) {
5057
- await this.holdPrimarySummaryRequest(agent, requestId, 'primary_summary_refusal_retry_over_budget', {
5058
- ...holdReasonData,
5059
- admittedTokens,
5060
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
5061
- });
5062
- }
5063
- else {
5064
- const intentId = `${requestId}:fallback:${Math.random().toString(36).slice(2, 8)}`;
5065
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
5066
- ...record,
5067
- fallbackIntent: {
5068
- intentId,
5069
- createdAt: Date.now(),
5070
- baselineRequestId: baseline.requestId,
5071
- candidateSummaries: candidates,
5072
- retryRequestHash,
5073
- retryCompleteBoundTokens: admittedTokens,
5074
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
5075
- },
5076
- fallbackStatus: 'pending',
5077
- }));
5078
- this.pendingRequests.push({
5079
- agentName: agent.name,
5080
- reason: 'primary-summary-fallback-retry',
5081
- source: 'framework',
5082
- timestamp: Date.now(),
5083
- channelId: trigger?.channelId,
5084
- primarySummaryFallbackRetry: {
5085
- originalRequestId: requestId,
5086
- baselineRequestId: baseline.requestId,
5087
- intentId,
5088
- branch: {
5089
- id: currentRecord.branch.id,
5090
- generation: currentRecord.branch.generation,
5091
- },
5092
- candidateSummaries: candidates,
5093
- originalArtifacts: artifacts,
5094
- originalProjectionKeys: this.primarySummaryProjectionKeys(currentRecord.projection),
5095
- expectedRetryRequestHash: retryRequestHash,
5096
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
5097
- originalRequestInputBoundTokens: currentRecord.requestInputBoundTokens,
5098
- originalProviderInputTokens: providerInputTokens,
5099
- },
5100
- });
5101
- }
5102
- }
5103
- catch (error) {
5104
- await this.holdPrimarySummaryRequest(agent, requestId, 'primary_summary_refusal_retry_build_failed', {
5105
- ...holdReasonData,
5106
- error: error instanceof Error ? error.message : String(error),
5107
- });
5108
- }
5109
- }
5110
- }
5111
- }
5112
- agent.reset();
5113
- this.eventGate?.onInferenceEnded(agent.name);
5114
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5115
- continue;
5116
- }
5117
- if (!(await persistPrimarySummaryRetryQuarantine())) {
5118
- return;
5119
- }
5120
3648
  // Add assistant response to context.
5121
3649
  // If we had tool calls, each round's blocks (thinking + text +
5122
3650
  // tool_use) were already stored as pendingAssistantBlocks and
@@ -5125,55 +3653,17 @@ export class AgentFramework {
5125
3653
  // double-store earlier rounds' text/thinking AND detach signed
5126
3654
  // thinking blocks from their tool_use turn (the API requires
5127
3655
  // thinking to precede tool_use in the same assistant turn).
3656
+ const lastToolIdx = response.content.reduce((last, b, i) => b.type === 'tool_use' || b.type === 'tool_result' ? i : last, -1);
3657
+ const terminalContent = lastToolIdx >= 0
3658
+ ? response.content.slice(lastToolIdx + 1)
3659
+ : response.content;
5128
3660
  if (lastToolIdx >= 0) {
5129
3661
  if (terminalContent.length > 0) {
5130
- if (primarySummaryRetry) {
5131
- const retryRecord = await this.getPrimarySummaryRequestRecord(requestId);
5132
- const terminalMetadata = retryRecord
5133
- ? this.buildPrimarySummarySettlementMetadata(retryRecord, {
5134
- outcome: 'success',
5135
- stopReason: response.stopReason,
5136
- providerInputTokens,
5137
- visibleAssistantOutput,
5138
- executedToolCalls,
5139
- entryIndex: 0,
5140
- entryCount: 1,
5141
- role: 'assistant',
5142
- kind: 'assistant_output',
5143
- })
5144
- : undefined;
5145
- if (!(await persistPrimarySummaryRetryOutput('terminal', terminalContent, agent.name, terminalMetadata))) {
5146
- return;
5147
- }
5148
- }
5149
- else {
5150
- agent.addAssistantResponse(terminalContent);
5151
- }
3662
+ agent.addAssistantResponse(terminalContent);
5152
3663
  }
5153
3664
  }
5154
3665
  else {
5155
- if (primarySummaryRetry) {
5156
- const retryRecord = await this.getPrimarySummaryRequestRecord(requestId);
5157
- const terminalMetadata = retryRecord && terminalContent.length > 0
5158
- ? this.buildPrimarySummarySettlementMetadata(retryRecord, {
5159
- outcome: 'success',
5160
- stopReason: response.stopReason,
5161
- providerInputTokens,
5162
- visibleAssistantOutput,
5163
- executedToolCalls,
5164
- entryIndex: 0,
5165
- entryCount: 1,
5166
- role: 'assistant',
5167
- kind: 'assistant_output',
5168
- })
5169
- : undefined;
5170
- if (!(await persistPrimarySummaryRetryOutput('terminal', terminalContent, agent.name, terminalMetadata))) {
5171
- return;
5172
- }
5173
- }
5174
- else {
5175
- agent.addAssistantResponse(terminalContent);
5176
- }
3666
+ agent.addAssistantResponse(terminalContent);
5177
3667
  }
5178
3668
  // Run afterInference hooks (no-op if no MCPL servers)
5179
3669
  if (this.hookOrchestrator) {
@@ -5228,28 +3718,6 @@ export class AgentFramework {
5228
3718
  cacheRead: du.cacheReadTokens,
5229
3719
  }
5230
3720
  : undefined;
5231
- if (primarySummaryRetry && artifacts) {
5232
- const validation = await this.validatePrimarySummaryRetryMutation(agent, requestId, primarySummaryRetry, compiledRequest, artifacts, 'before_success_markers');
5233
- if (!validation.ok) {
5234
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, validation.reason, validation.data, { emitChronicleMarker: false });
5235
- return;
5236
- }
5237
- }
5238
- if (primarySummaryConfig.enabled) {
5239
- if (primarySummaryRetry) {
5240
- await this.markPrimarySummaryRetrySuccess(requestId, primarySummaryRetry, response.stopReason, providerInputTokens, visibleAssistantOutput, executedToolCalls);
5241
- }
5242
- else {
5243
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
5244
- ...record,
5245
- stopReason: response.stopReason,
5246
- providerInputTokens,
5247
- visibleAssistantOutput,
5248
- executedToolCalls,
5249
- finalStatus: 'success',
5250
- }));
5251
- }
5252
- }
5253
3721
  // Reset agent state before emitting inference:completed. Traces are
5254
3722
  // observability-only, but external synchronous listeners should
5255
3723
  // still see the terminal state at the terminal trace boundary.
@@ -5285,10 +3753,8 @@ export class AgentFramework {
5285
3753
  agentName: agent.name,
5286
3754
  requestId,
5287
3755
  success: true,
5288
- request: fallbackRetryRequestForLog,
5289
- response: primarySummaryRetry
5290
- ? this.summarizePrimarySummaryRetryResponse(response)
5291
- : response.raw ?? { note: 'streaming response' },
3756
+ request: compiledRequest ?? { note: 'streaming request' },
3757
+ response: response.raw ?? { note: 'streaming response' },
5292
3758
  durationMs,
5293
3759
  tokenUsage,
5294
3760
  stopReason: response.stopReason,
@@ -5481,22 +3947,12 @@ export class AgentFramework {
5481
3947
  requestId,
5482
3948
  success: false,
5483
3949
  error: err.message,
5484
- request: fallbackRetryRequestForLog,
3950
+ request: compiledRequest ?? { note: 'streaming request failed' },
5485
3951
  durationMs,
5486
3952
  });
5487
3953
  // Only reset + retry if this is still the active stream
5488
3954
  if (agent.streamId !== myStreamId)
5489
3955
  break;
5490
- if (primarySummaryRetry) {
5491
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, 'primary_summary_fallback_retry_stream_error', { error: err.message });
5492
- agent.reset();
5493
- this.eventGate?.onInferenceEnded(agent.name);
5494
- this.settleAgent(agent.name, {
5495
- stopReason: 'completed',
5496
- speech: '',
5497
- });
5498
- return;
5499
- }
5500
3956
  agent.reset();
5501
3957
  const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
5502
3958
  if (action.retry) {
@@ -5535,11 +3991,6 @@ export class AgentFramework {
5535
3991
  // reject an ephemeral's promise mid-run and bump the failure
5536
3992
  // streak). Gate release + stream teardown happen in `finally`.
5537
3993
  if (this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`)) {
5538
- if (primarySummaryRetry) {
5539
- if (!(await this.primarySummaryRetryFamilyAlreadySettled(requestId, primarySummaryRetry))) {
5540
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, 'primary_summary_fallback_retry_cancelled_before_success', { frameworkCancelled: true });
5541
- }
5542
- }
5543
3994
  this.eventGate?.onInferenceEnded(agent.name);
5544
3995
  return;
5545
3996
  }
@@ -5548,33 +3999,6 @@ export class AgentFramework {
5548
3999
  // may have already started a new stream, bumping streamId)
5549
4000
  if (agent.streamId === myStreamId) {
5550
4001
  const durationMs = Date.now() - startTime;
5551
- if (primarySummaryRetry) {
5552
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, `stream_aborted:${reason}`, { reason }, { stopReason: 'abort' });
5553
- agent.reset();
5554
- this.eventGate?.onInferenceEnded(agent.name);
5555
- this.logInference({
5556
- timestamp: startTime,
5557
- agentName: agent.name,
5558
- requestId,
5559
- success: false,
5560
- error: `Stream aborted: ${reason}`,
5561
- request: fallbackRetryRequestForLog,
5562
- durationMs,
5563
- });
5564
- this.settleAgent(agent.name, {
5565
- stopReason: 'completed',
5566
- speech: '',
5567
- });
5568
- return;
5569
- }
5570
- if (primarySummaryConfig.enabled) {
5571
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
5572
- ...record,
5573
- stopReason: 'abort',
5574
- finalStatus: 'error',
5575
- fallbackHeldReason: `stream_aborted:${reason}`,
5576
- }));
5577
- }
5578
4002
  agent.reset();
5579
4003
  this.settleAgent(agent.name, {
5580
4004
  stopReason: 'exhausted',
@@ -5598,7 +4022,7 @@ export class AgentFramework {
5598
4022
  requestId,
5599
4023
  success: false,
5600
4024
  error: `Stream aborted: ${reason}`,
5601
- request: fallbackRetryRequestForLog,
4025
+ request: compiledRequest ?? { note: 'streaming request aborted' },
5602
4026
  durationMs,
5603
4027
  });
5604
4028
  this.eventGate?.onInferenceEnded(agent.name);
@@ -5644,58 +4068,12 @@ export class AgentFramework {
5644
4068
  // inference:exhausted so ephemeral agent promises can settle.
5645
4069
  const err = error instanceof Error ? error : new Error(String(error));
5646
4070
  const durationMs = Date.now() - startTime;
5647
- if (primarySummaryRetry) {
5648
- if (await this.primarySummaryRetryAlreadySucceeded(requestId, primarySummaryRetry)) {
5649
- this.logInference({
5650
- timestamp: startTime,
5651
- agentName: agent.name,
5652
- requestId,
5653
- success: true,
5654
- request: fallbackRetryRequestForLog,
5655
- response: { kind: 'primary_summary_fallback_retry', postSuccessError: err.message },
5656
- durationMs,
5657
- });
5658
- agent.reset();
5659
- this.eventGate?.onInferenceEnded(agent.name);
5660
- this.settleAgent(agent.name, {
5661
- stopReason: 'completed',
5662
- speech: '',
5663
- });
5664
- return;
5665
- }
5666
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, 'primary_summary_fallback_retry_stream_threw', { error: err.message });
5667
- }
5668
- else if (primarySummaryConfig.enabled) {
5669
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
5670
- ...record,
5671
- finalStatus: 'error',
5672
- fallbackHeldReason: err.message,
5673
- }));
5674
- }
5675
4071
  this.emitTrace({
5676
4072
  type: 'inference:failed',
5677
4073
  agentName: agent.name,
5678
4074
  error: err.message,
5679
4075
  stack: err.stack,
5680
4076
  });
5681
- if (primarySummaryRetry) {
5682
- this.logInference({
5683
- timestamp: startTime,
5684
- agentName: agent.name,
5685
- requestId,
5686
- success: false,
5687
- error: `Stream threw: ${err.message}`,
5688
- request: fallbackRetryRequestForLog,
5689
- durationMs,
5690
- });
5691
- agent.reset();
5692
- this.eventGate?.onInferenceEnded(agent.name);
5693
- this.settleAgent(agent.name, {
5694
- stopReason: 'completed',
5695
- speech: '',
5696
- });
5697
- return;
5698
- }
5699
4077
  this.settleAgent(agent.name, {
5700
4078
  stopReason: 'exhausted',
5701
4079
  speech: '',
@@ -5716,7 +4094,7 @@ export class AgentFramework {
5716
4094
  requestId,
5717
4095
  success: false,
5718
4096
  error: `Stream threw: ${err.message}`,
5719
- request: fallbackRetryRequestForLog,
4097
+ request: compiledRequest ?? { note: 'streaming request threw' },
5720
4098
  durationMs,
5721
4099
  });
5722
4100
  agent.reset();
@@ -5737,7 +4115,6 @@ export class AgentFramework {
5737
4115
  this.channelRegistry?.stopTyping();
5738
4116
  this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
5739
4117
  this.activeStreams.delete(agent.name);
5740
- this.activeStreamContexts.delete(agent.name);
5741
4118
  this.pendingAssistantBlocks.delete(agent.name);
5742
4119
  // A conversation fork whose TTL closure turn just finished is done for
5743
4120
  // good — dispose it so the agent map doesn't grow monotonically.
@@ -5992,6 +4369,10 @@ export class AgentFramework {
5992
4369
  this.dispatchSaveImageToolCall(agentName, enrichedCall);
5993
4370
  return;
5994
4371
  }
4372
+ if (enrichedCall.name === 'read_image') {
4373
+ this.dispatchReadImageToolCall(agentName, enrichedCall);
4374
+ return;
4375
+ }
5995
4376
  // Route gate_status tool
5996
4377
  if (enrichedCall.name === 'gate_status' && this.eventGate) {
5997
4378
  this.dispatchGateToolCall(agentName, enrichedCall);
@@ -6224,22 +4605,12 @@ export class AgentFramework {
6224
4605
  * work, per-agent status + last inference activity. Cheap and read-only.
6225
4606
  */
6226
4607
  healthSnapshot() {
6227
- const primarySummary = this.readPrimarySummaryFallbackState();
6228
4608
  return {
6229
4609
  at: new Date().toISOString(),
6230
4610
  uptimeSec: Math.round(process.uptime()),
6231
4611
  gate: this.eventGate?.inferenceDiagnostics() ?? null,
6232
4612
  pendingRequests: this.pendingRequests.length,
6233
4613
  activeStreams: [...this.activeStreams.keys()],
6234
- primarySummaryFallback: {
6235
- requests: primarySummary.requests.length,
6236
- pendingDispatches: primarySummary.requests.filter((record) => record.finalStatus === 'pending').length,
6237
- unresolvedIntents: primarySummary.requests.filter((record) => record.fallbackStatus === 'pending').length,
6238
- held: primarySummary.requests.filter((record) => record.finalStatus === 'held' ||
6239
- record.finalStatus === 'timeout_unknown' ||
6240
- record.fallbackStatus === 'held' ||
6241
- record.fallbackStatus === 'timeout_unknown').length,
6242
- },
6243
4614
  agents: [...this.agents.entries()].map(([name, agent]) => ({
6244
4615
  name,
6245
4616
  status: agent.state.status,
@@ -7504,6 +5875,19 @@ export class AgentFramework {
7504
5875
  .then((result) => {
7505
5876
  const durationMs = Date.now() - startTime;
7506
5877
  this.emitTrace({ type: 'tool:completed', module: 'channels', tool: call.name, callId: call.id, durationMs });
5878
+ // A successful channel_open plants the agent's feet in the opened
5879
+ // channel: pin it as this turn's reply locus so plain-text speech
5880
+ // written right after opening routes THERE. Found live 2026-07-21
5881
+ // (Aria bring-up): open → plain-text reply → "no locus (no
5882
+ // home/active channel, defaultPublishChannel null)" on a turn with
5883
+ // no channel trigger (post-restart continuation). Next turn start
5884
+ // re-resolves the locus as usual (startAgentStream set/delete).
5885
+ if (call.name === 'channel_open' && result?.success) {
5886
+ const opened = result.data?.channelId
5887
+ ?? call.input?.channelId;
5888
+ if (opened)
5889
+ this.activeTriggerChannels.set(agentName, opened);
5890
+ }
7507
5891
  this.pushEvent({
7508
5892
  type: 'tool-result',
7509
5893
  callId: call.id,
@@ -7650,6 +6034,72 @@ export class AgentFramework {
7650
6034
  endTurn: true,
7651
6035
  });
7652
6036
  }
6037
+ /**
6038
+ * Handle the synthesized `read_image` tool: read image bytes from a
6039
+ * workspace mount and return them as a native MCP-shaped content array
6040
+ * (`[{type:'text'…},{type:'image', data, mimeType}]`). The live-turn
6041
+ * converter (tryNativeToolResultContent → membrane tool_result image
6042
+ * blocks) delivers the actual image to the model; history storage keeps
6043
+ * the standard compact `[image: …]` placeholder.
6044
+ */
6045
+ dispatchReadImageToolCall(agentName, call) {
6046
+ this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
6047
+ const finish = (result) => {
6048
+ this.emitTrace({
6049
+ type: result.isError ? 'tool:failed' : 'tool:completed',
6050
+ module: 'workspace',
6051
+ tool: call.name,
6052
+ callId: call.id,
6053
+ durationMs: 0,
6054
+ ...(result.isError ? { error: result.error } : {}),
6055
+ });
6056
+ this.pushEvent({ type: 'tool-result', callId: call.id, agentName, moduleName: 'workspace', result });
6057
+ };
6058
+ void (async () => {
6059
+ try {
6060
+ const workspace = this.getWorkspaceModule();
6061
+ if (!workspace || typeof workspace.readBinary !== 'function') {
6062
+ throw new Error('read_image requires a workspace module');
6063
+ }
6064
+ const input = (call.input ?? {});
6065
+ if (typeof input.path !== 'string' || input.path.length === 0) {
6066
+ throw new Error('read_image: `path` (mount-prefixed) is required');
6067
+ }
6068
+ const read = await workspace.readBinary(input.path);
6069
+ if ('error' in read)
6070
+ throw new Error(read.error);
6071
+ const mediaType = sniffImageMediaType(read.data);
6072
+ if (!mediaType) {
6073
+ throw new Error(`read_image: "${input.path}" does not look like a supported image ` +
6074
+ '(png/jpeg/gif/webp — checked by magic bytes, not extension)');
6075
+ }
6076
+ // Anthropic rejects images >5MB (and >8000px); guard the hard byte
6077
+ // limit here — resizing is out of scope without an image library.
6078
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
6079
+ if (read.data.byteLength > MAX_IMAGE_BYTES) {
6080
+ throw new Error(`read_image: "${input.path}" is ${read.data.byteLength} bytes — ` +
6081
+ `over the ${MAX_IMAGE_BYTES}-byte model limit`);
6082
+ }
6083
+ finish({
6084
+ success: true,
6085
+ data: [
6086
+ {
6087
+ type: 'text',
6088
+ text: `${input.path} (${mediaType}, ${Math.ceil(read.data.byteLength / 1024)} KB):`,
6089
+ },
6090
+ { type: 'image', data: read.data.toString('base64'), mimeType: mediaType },
6091
+ ],
6092
+ });
6093
+ }
6094
+ catch (error) {
6095
+ finish({
6096
+ success: false,
6097
+ error: error instanceof Error ? error.message : String(error),
6098
+ isError: true,
6099
+ });
6100
+ }
6101
+ })();
6102
+ }
7653
6103
  /**
7654
6104
  * Handle the synthesized `save_recent_image` tool: locate the requested
7655
6105
  * image blocks in the calling agent's context (blobs re-inlined, counted