@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.
- package/dist/src/agent.d.ts +5 -34
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +62 -137
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +14 -56
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +192 -1742
- package/dist/src/framework.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +10 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +48 -2
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +2 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +17 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +143 -7
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/types/agent.d.ts +15 -12
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/test/helpers/mock-membrane.js +1 -1
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/mcpl-awareness-barrier.test.js +1 -228
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/refusal-single-call.test.d.ts +2 -0
- package/dist/test/refusal-single-call.test.d.ts.map +1 -0
- package/dist/test/refusal-single-call.test.js +177 -0
- package/dist/test/refusal-single-call.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +5 -1
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/workspace-watcher.test.js +161 -9
- package/dist/test/workspace-watcher.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/agent.ts +67 -226
- package/src/framework.ts +226 -2394
- package/src/modules/workspace/index.ts +47 -2
- package/src/modules/workspace/types.ts +2 -0
- package/src/modules/workspace/watcher.ts +160 -7
- package/src/types/agent.ts +17 -12
- package/dist/src/canonical-json.d.ts +0 -2
- package/dist/src/canonical-json.d.ts.map +0 -1
- package/dist/src/canonical-json.js +0 -23
- package/dist/src/canonical-json.js.map +0 -1
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +0 -2
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +0 -1
- package/dist/test/primary-summary-refusal-fallback-integration.test.js +0 -1955
- package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +0 -1
- package/dist/test/primary-summary-refusal-fallback.test.d.ts +0 -2
- package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +0 -1
- package/dist/test/primary-summary-refusal-fallback.test.js +0 -753
- package/dist/test/primary-summary-refusal-fallback.test.js.map +0 -1
- package/src/canonical-json.ts +0 -22
package/dist/src/framework.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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()
|
|
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]
|
|
4261
|
-
`
|
|
4262
|
-
`
|
|
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
|
-
?
|
|
4265
|
-
: `
|
|
4266
|
-
`
|
|
4267
|
-
`
|
|
4268
|
-
`
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
4447
|
-
|
|
4448
|
-
let
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
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
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
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
|
|
4535
|
-
|
|
4536
|
-
|
|
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('
|
|
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.
|
|
4580
|
-
this.
|
|
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,
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
5289
|
-
response:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|