@animalabs/agent-framework 0.6.7 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/src/agent.d.ts +5 -34
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +62 -137
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +14 -56
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +192 -1742
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/modules/workspace/index.d.ts +10 -0
  10. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  11. package/dist/src/modules/workspace/index.js +48 -2
  12. package/dist/src/modules/workspace/index.js.map +1 -1
  13. package/dist/src/modules/workspace/types.d.ts +2 -0
  14. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  15. package/dist/src/modules/workspace/types.js.map +1 -1
  16. package/dist/src/modules/workspace/watcher.d.ts +17 -0
  17. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  18. package/dist/src/modules/workspace/watcher.js +143 -7
  19. package/dist/src/modules/workspace/watcher.js.map +1 -1
  20. package/dist/src/types/agent.d.ts +15 -12
  21. package/dist/src/types/agent.d.ts.map +1 -1
  22. package/dist/test/helpers/mock-membrane.js +1 -1
  23. package/dist/test/helpers/mock-membrane.js.map +1 -1
  24. package/dist/test/mcpl-awareness-barrier.test.js +1 -228
  25. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  26. package/dist/test/refusal-single-call.test.d.ts +2 -0
  27. package/dist/test/refusal-single-call.test.d.ts.map +1 -0
  28. package/dist/test/refusal-single-call.test.js +177 -0
  29. package/dist/test/refusal-single-call.test.js.map +1 -0
  30. package/dist/test/trunk-channel-routing.test.js +5 -1
  31. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  32. package/dist/test/workspace-watcher.test.js +161 -9
  33. package/dist/test/workspace-watcher.test.js.map +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +4 -4
  36. package/src/agent.ts +67 -226
  37. package/src/framework.ts +226 -2394
  38. package/src/modules/workspace/index.ts +47 -2
  39. package/src/modules/workspace/types.ts +2 -0
  40. package/src/modules/workspace/watcher.ts +160 -7
  41. package/src/types/agent.ts +17 -12
  42. package/dist/src/canonical-json.d.ts +0 -2
  43. package/dist/src/canonical-json.d.ts.map +0 -1
  44. package/dist/src/canonical-json.js +0 -23
  45. package/dist/src/canonical-json.js.map +0 -1
  46. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +0 -2
  47. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +0 -1
  48. package/dist/test/primary-summary-refusal-fallback-integration.test.js +0 -1955
  49. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +0 -1
  50. package/dist/test/primary-summary-refusal-fallback.test.d.ts +0 -2
  51. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +0 -1
  52. package/dist/test/primary-summary-refusal-fallback.test.js +0 -753
  53. package/dist/test/primary-summary-refusal-fallback.test.js.map +0 -1
  54. package/src/canonical-json.ts +0 -22
package/src/framework.ts CHANGED
@@ -1,19 +1,14 @@
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
- import type { Membrane, ContentBlock, NormalizedRequest, NormalizedResponse, YieldingStream, ToolResult as MembraneToolResult, ToolResultContentBlock } from '@animalabs/membrane';
4
+ import type { Membrane, ContentBlock, NormalizedRequest, YieldingStream, ToolResult as MembraneToolResult, ToolResultContentBlock } from '@animalabs/membrane';
6
5
  import { MembraneError } from '@animalabs/membrane';
7
6
  import { ContextManager, PassthroughStrategy } from '@animalabs/context-manager';
8
7
  import type {
9
- BranchGenerationInfo,
10
8
  MessageId,
11
9
  MessageMetadata,
12
10
  MessageQuery,
13
11
  MessageQueryResult,
14
- PrimarySummaryContract,
15
- PrimarySummaryIdentity,
16
- PrimarySummaryProjection,
17
12
  StoredMessage,
18
13
  } from '@animalabs/context-manager';
19
14
  import type {
@@ -56,7 +51,7 @@ import type {
56
51
  SameRoundThinkTextPolicy,
57
52
  } from './types/index.js';
58
53
  import { ProcessQueueImpl } from './queue.js';
59
- import { Agent, type ActivationCompileArtifacts } from './agent.js';
54
+ import { Agent } from './agent.js';
60
55
  import { ModuleRegistry, isStateExistsError } from './module-registry.js';
61
56
  import { McplServerRegistry } from './mcpl/server-registry.js';
62
57
  import { FeatureSetManager } from './mcpl/feature-set-manager.js';
@@ -70,7 +65,29 @@ import { safeSlice } from './safe-slice.js';
70
65
  import type { WorkspaceModule } from './modules/workspace/index.js';
71
66
  import { toolResultDataToHistoryString } from './tool-result-history.js';
72
67
  import { splitProseSegments } from './prose-segments.js';
73
- import { canonicalJsonStringify } from './canonical-json.js';
68
+
69
+ /** Detect a supported image media type from magic bytes (the model API
70
+ * rejects mislabeled media types, so trust bytes over extensions).
71
+ * Returns undefined for non-image content. */
72
+ function sniffImageMediaType(data: Buffer): string | undefined {
73
+ if (data.length >= 4 && data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) {
74
+ return 'image/png';
75
+ }
76
+ if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) {
77
+ return 'image/jpeg';
78
+ }
79
+ if (data.length >= 3 && data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46) {
80
+ return 'image/gif';
81
+ }
82
+ if (
83
+ data.length >= 12 &&
84
+ data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 &&
85
+ data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50
86
+ ) {
87
+ return 'image/webp';
88
+ }
89
+ return undefined;
90
+ }
74
91
 
75
92
  /**
76
93
  * Tools whose presence suppresses auto-routing of the surrounding prose,
@@ -127,12 +144,6 @@ const FRAMEWORK_STATE_ID = 'framework/state';
127
144
  const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
128
145
  const INFERENCE_LOG_ID = 'framework/inference-log';
129
146
  const PROCESS_LOG_ID = 'framework/process-log';
130
- const PRIMARY_SUMMARY_FALLBACK_STATE_ID = 'framework/primary-summary-fallback';
131
- const PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY = 'primarySummarySettlement';
132
- const PRIMARY_SUMMARY_SETTLEMENT_VERSION = 1;
133
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH = 1024;
134
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH = 256;
135
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH = 256;
136
147
  const TURN_CHECKPOINTS_ID = 'framework/turn-checkpoints'; // legacy single-map layout, read-only fallback
137
148
  const TURN_CHECKPOINTS_TREE_ID = 'framework/turn-checkpoints/tree';
138
149
 
@@ -187,128 +198,6 @@ interface InferenceToolSnapshot {
187
198
  sameRoundThinkTextPolicy: SameRoundThinkTextPolicy;
188
199
  }
189
200
 
190
- type PrimarySummaryFallbackStatus =
191
- | 'success'
192
- | 'refusal'
193
- | 'held'
194
- | 'pending'
195
- | 'error'
196
- | 'timeout_unknown';
197
-
198
- interface PrimarySummaryFallbackRetryDispatch {
199
- originalRequestId: string;
200
- baselineRequestId: string;
201
- intentId: string;
202
- branch: { id: string; generation: number };
203
- candidateSummaries: PrimarySummaryIdentity[];
204
- originalArtifacts: ActivationCompileArtifacts;
205
- originalProjectionKeys: string[];
206
- expectedRetryRequestHash: string;
207
- requestBudgetTokens: number;
208
- originalRequestInputBoundTokens: number;
209
- originalProviderInputTokens?: number;
210
- }
211
-
212
- interface PrimarySummaryFallbackIntentRecord {
213
- intentId: string;
214
- createdAt: number;
215
- baselineRequestId: string;
216
- candidateSummaries: PrimarySummaryIdentity[];
217
- retryRequestHash: string;
218
- retryCompleteBoundTokens: number;
219
- requestBudgetTokens: number;
220
- retryRequestId?: string;
221
- dispatchedAt?: number;
222
- quarantinePersistedAt?: number;
223
- outputs?: PrimarySummaryFallbackOutputRecord[];
224
- toolDispatches?: PrimarySummaryFallbackToolDispatchRecord[];
225
- finalizedAt?: number;
226
- }
227
-
228
- interface PrimarySummaryFallbackOutputRecord {
229
- phase: string;
230
- contentHash: string;
231
- messageId: string;
232
- blockTypes: string[];
233
- persistedAt: number;
234
- }
235
-
236
- type PrimarySummarySettlementOutcome = 'success' | 'held';
237
- type PrimarySummarySettlementRole = 'assistant' | 'user';
238
- type PrimarySummarySettlementKind = 'assistant_output' | 'generated_tool_result';
239
-
240
- interface PrimarySummarySettlementMetadata {
241
- version: number;
242
- requestId: string;
243
- settlementId: string;
244
- agentName: string;
245
- dispatchKind: PrimarySummaryRequestRecord['dispatchKind'];
246
- outcome: PrimarySummarySettlementOutcome;
247
- stopReason?: string;
248
- holdReason?: string;
249
- providerInputTokens?: number;
250
- visibleAssistantOutput: boolean;
251
- executedToolCalls: number;
252
- branch: { id: string; name: string; generation: number };
253
- role: PrimarySummarySettlementRole;
254
- kind: PrimarySummarySettlementKind;
255
- entryIndex: number;
256
- entryCount: number;
257
- }
258
-
259
- interface PrimarySummaryLogicalSettlementEntry {
260
- messages: StoredMessage[];
261
- settlement: PrimarySummarySettlementMetadata;
262
- participant: string;
263
- content: ContentBlock[];
264
- bodyGroupId?: string;
265
- }
266
-
267
- interface PrimarySummaryFallbackToolDispatchRecord {
268
- callId: string;
269
- inputHash: string;
270
- recordedAt: number;
271
- }
272
-
273
- interface PrimarySummaryRequestRecord {
274
- requestId: string;
275
- agentName: string;
276
- namespace: string;
277
- timestamp: number;
278
- dispatchKind: 'primary' | 'primary_summary_fallback_retry';
279
- branch: { id: string; name: string; generation: number };
280
- projection: PrimarySummaryProjection | null;
281
- requestInputBoundTokens: number;
282
- requestCompleteBoundTokens: number;
283
- providerInputTokens?: number;
284
- systemHash: string;
285
- modelConfigHash: string;
286
- toolContractHash: string;
287
- stopReason?: string;
288
- visibleAssistantOutput: boolean;
289
- executedToolCalls: number;
290
- finalStatus: PrimarySummaryFallbackStatus;
291
- fallbackIntent?: PrimarySummaryFallbackIntentRecord;
292
- fallbackStatus?: PrimarySummaryFallbackStatus;
293
- fallbackHeldReason?: string;
294
- }
295
-
296
- interface PrimarySummaryFallbackState {
297
- requests: PrimarySummaryRequestRecord[];
298
- }
299
-
300
- interface ContextBudgetRestartDispatch {
301
- requestId: string;
302
- dispatchKind: 'primary' | 'primary_summary_fallback_retry';
303
- originalRequestId?: string;
304
- intentId?: string;
305
- }
306
-
307
- interface PendingInferenceRequest extends InferenceRequest {
308
- primarySummaryFallbackRetry?: PrimarySummaryFallbackRetryDispatch;
309
- contextBudgetRestart?: ContextBudgetRestartDispatch;
310
- }
311
-
312
201
  /**
313
202
  * Default inference policy - infer if any request exists for the agent.
314
203
  */
@@ -456,13 +345,6 @@ interface EphemeralRun {
456
345
  toolCallsCount: number;
457
346
  }
458
347
 
459
- interface ActiveStreamContext {
460
- requestId: string;
461
- trigger?: PendingInferenceRequest;
462
- compiledRequest?: NormalizedRequest;
463
- artifacts?: ActivationCompileArtifacts;
464
- }
465
-
466
348
  /** Cap an API error message for inline use in a rewind marker: keep enough to
467
349
  * identify the failure class without pasting a wall of provider JSON into the
468
350
  * agent's context. */
@@ -479,7 +361,7 @@ export class AgentFramework {
479
361
  private moduleRegistry: ModuleRegistry;
480
362
  private inferencePolicy: InferencePolicy;
481
363
  private errorPolicy: ErrorPolicy;
482
- private pendingRequests: PendingInferenceRequest[] = [];
364
+ private pendingRequests: InferenceRequest[] = [];
483
365
  /**
484
366
  * Per-agent channel that triggered the agent's CURRENT inference turn, if any
485
367
  * (item-3 redux). Read by the ChannelRegistry's `activeChannelResolver` to
@@ -508,8 +390,6 @@ export class AgentFramework {
508
390
  private processLoggingPersist: boolean;
509
391
  private processLoggingBroadcast: boolean;
510
392
  private activeStreams: Map<string, Promise<void>> = new Map();
511
- private activeStreamContexts: Map<string, ActiveStreamContext> = new Map();
512
- private primarySummaryStateLock: Promise<void> = Promise.resolve();
513
393
 
514
394
  /** Per-agent output locus pinned for the CURRENT logical turn (see
515
395
  * resolveTurnLocus in driveStream). Lives here — not in driveStream
@@ -765,12 +645,6 @@ export class AgentFramework {
765
645
  }
766
646
  }
767
647
 
768
- try {
769
- store.registerState({ id: PRIMARY_SUMMARY_FALLBACK_STATE_ID, strategy: 'snapshot' });
770
- } catch {
771
- // Already registered
772
- }
773
-
774
648
  const discordAwarenessOutboxPath = config.discordAwarenessOutboxPath
775
649
  ?? (config.storePath ? defaultDiscordAwarenessOutboxPath(config.storePath) : undefined);
776
650
  const discordAwarenessOutbox = discordAwarenessOutboxPath
@@ -815,15 +689,14 @@ export class AgentFramework {
815
689
  }
816
690
  }
817
691
 
692
+ // Restore persisted usage data (if any) from prior session
693
+ framework.restoreUsageState();
694
+
818
695
  // Create agents
819
696
  for (const agentConfig of config.agents) {
820
697
  await framework.createAgent(agentConfig);
821
698
  }
822
699
 
823
- // Restore persisted usage data (if any) from prior session
824
- framework.restoreUsageState();
825
- await framework.reconcilePrimarySummaryFallbackStateOnStartup();
826
-
827
700
  // Finish any branch-local suppression interrupted after Chronicle switched
828
701
  // branches. This runs before modules, MCPL connections, or inbound traffic.
829
702
  await framework.resumePreparedDiscordSuppressions();
@@ -1080,7 +953,7 @@ export class AgentFramework {
1080
953
  const queued = [...this.agents.values()].flatMap((agent) => {
1081
954
  const cm = agent.getContextManager();
1082
955
  const tools = this.getToolsForAgent(agent.name).filter((tool) => agent.canUseTool(tool.name));
1083
- cm.setToolDefinitions?.(tools);
956
+ cm.setToolDefinitions(tools);
1084
957
  if (cm.isReady()) return [];
1085
958
  const pending = cm.getPendingWork()?.description;
1086
959
  const progress = this.contextProgress(cm);
@@ -1284,7 +1157,9 @@ export class AgentFramework {
1284
1157
  ...channelTools,
1285
1158
  ...gateTools,
1286
1159
  this.buildAgentSettingsTool(),
1287
- ...(this.getWorkspaceModule() ? [AgentFramework.SAVE_IMAGE_TOOL] : []),
1160
+ ...(this.getWorkspaceModule()
1161
+ ? [AgentFramework.SAVE_IMAGE_TOOL, AgentFramework.READ_IMAGE_TOOL]
1162
+ : []),
1288
1163
  ];
1289
1164
  }
1290
1165
 
@@ -1741,1322 +1616,6 @@ export class AgentFramework {
1741
1616
  return { promise, resolve, reject };
1742
1617
  }
1743
1618
 
1744
- private hashJson(value: unknown): string {
1745
- return createHash('sha256').update(canonicalJsonStringify(value), 'utf8').digest('hex');
1746
- }
1747
-
1748
- private estimateStringTokens(text: string, overhead = 0): number {
1749
- return Math.ceil(Buffer.byteLength(text, 'utf8') / 4) + overhead;
1750
- }
1751
-
1752
- private estimateUnknownTokens(value: unknown, overhead = 0): number {
1753
- return this.estimateStringTokens(JSON.stringify(value) ?? 'null', overhead);
1754
- }
1755
-
1756
- private estimateRequestInputBoundTokens(request: NormalizedRequest): number {
1757
- let total = 96;
1758
- total += this.estimateStringTokens(request.system ?? '', 48);
1759
- for (const message of request.messages) {
1760
- total += 24;
1761
- total += this.estimateStringTokens(message.participant, 8);
1762
- if ((message as { cacheBreakpoint?: boolean }).cacheBreakpoint) total += 8;
1763
- for (const block of message.content) {
1764
- switch (block.type) {
1765
- case 'text':
1766
- total += this.estimateStringTokens(block.text, 24);
1767
- break;
1768
- case 'thinking':
1769
- total += this.estimateStringTokens(block.thinking, 32);
1770
- total += block.signature ? this.estimateStringTokens(block.signature, 8) : 0;
1771
- break;
1772
- case 'redacted_thinking':
1773
- total += this.estimateUnknownTokens(block, 32);
1774
- break;
1775
- default:
1776
- total += this.estimateUnknownTokens(block, 32);
1777
- break;
1778
- }
1779
- }
1780
- }
1781
- if (request.tools && request.tools.length > 0) {
1782
- total += 32;
1783
- for (const tool of request.tools) {
1784
- total += this.estimateUnknownTokens({
1785
- name: tool.name,
1786
- description: tool.description,
1787
- inputSchema: tool.inputSchema,
1788
- }, 48);
1789
- }
1790
- }
1791
- total += this.estimateUnknownTokens(request.config, 48);
1792
- if (request.providerParams !== undefined) {
1793
- total += this.estimateUnknownTokens(request.providerParams, 32);
1794
- }
1795
- total += request.promptCaching ? 16 : 0;
1796
- total += request.cacheTtl ? this.estimateStringTokens(request.cacheTtl, 8) : 0;
1797
- total += request.assistantParticipant
1798
- ? this.estimateStringTokens(request.assistantParticipant, 8)
1799
- : 0;
1800
- return total;
1801
- }
1802
-
1803
- private estimateRequestCompleteBoundTokens(request: NormalizedRequest): number {
1804
- const reserve = typeof request.config.maxTokens === 'number' && Number.isFinite(request.config.maxTokens)
1805
- ? request.config.maxTokens
1806
- : 0;
1807
- return this.estimateRequestInputBoundTokens(request) + reserve + 96;
1808
- }
1809
-
1810
- private primarySummaryAdmissionBoundTokens(
1811
- originalRequestInputTokens: number,
1812
- expandedRequest: NormalizedRequest,
1813
- originalProviderInputTokens: number | undefined,
1814
- ): number {
1815
- const expandedInput = this.estimateRequestInputBoundTokens(expandedRequest);
1816
- const expandedComplete = this.estimateRequestCompleteBoundTokens(expandedRequest);
1817
- if (originalProviderInputTokens === undefined) return expandedComplete;
1818
- const reserve = typeof expandedRequest.config.maxTokens === 'number' && Number.isFinite(expandedRequest.config.maxTokens)
1819
- ? expandedRequest.config.maxTokens
1820
- : 0;
1821
- const positiveDelta = Math.max(0, expandedInput - originalRequestInputTokens);
1822
- return Math.max(
1823
- expandedComplete,
1824
- originalProviderInputTokens + positiveDelta + reserve + 96,
1825
- );
1826
- }
1827
-
1828
- private readPrimarySummaryFallbackState(): PrimarySummaryFallbackState {
1829
- const raw = (this.store as { getStateJson?: (id: string) => unknown } | undefined)
1830
- ?.getStateJson?.(PRIMARY_SUMMARY_FALLBACK_STATE_ID);
1831
- if (!raw || typeof raw !== 'object') return { requests: [] };
1832
- const requests = Array.isArray((raw as { requests?: unknown[] }).requests)
1833
- ? ((raw as { requests: PrimarySummaryRequestRecord[] }).requests)
1834
- : [];
1835
- return { requests };
1836
- }
1837
-
1838
- private writePrimarySummaryFallbackState(state: PrimarySummaryFallbackState): void {
1839
- this.store.setStateJson(PRIMARY_SUMMARY_FALLBACK_STATE_ID, state);
1840
- }
1841
-
1842
- private async withPrimarySummaryStateLock<T>(work: () => Promise<T> | T): Promise<T> {
1843
- const previous = this.primarySummaryStateLock;
1844
- let release!: () => void;
1845
- this.primarySummaryStateLock = new Promise<void>((resolve) => { release = resolve; });
1846
- await previous;
1847
- try {
1848
- return await work();
1849
- } finally {
1850
- release();
1851
- }
1852
- }
1853
-
1854
- private async reconcilePrimarySummaryFallbackStateOnStartup(): Promise<void> {
1855
- const alerts: Array<{ agentName: string; requestId: string; reason: string }> = [];
1856
- const unsettledRequests = (await this.withPrimarySummaryStateLock(() =>
1857
- this.readPrimarySummaryFallbackState().requests
1858
- .filter((record) =>
1859
- record.finalStatus === 'pending'
1860
- || record.finalStatus === 'refusal'
1861
- || record.fallbackStatus === 'pending',
1862
- )
1863
- .map((record) => structuredClone(record)),
1864
- )) as PrimarySummaryRequestRecord[];
1865
-
1866
- const resolvedRequestIds = new Set<string>();
1867
- for (const record of unsettledRequests) {
1868
- const result = await this.finalizePrimarySummarySettlementOnStartup(record);
1869
- if (result === 'finalized') {
1870
- resolvedRequestIds.add(record.requestId);
1871
- continue;
1872
- }
1873
- if (result && typeof result === 'object' && 'invalidReason' in result) {
1874
- resolvedRequestIds.add(record.requestId);
1875
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason: result.invalidReason });
1876
- await this.markPrimarySummaryRequestRestartFailure(record, result.invalidReason);
1877
- }
1878
- }
1879
-
1880
- await this.withPrimarySummaryStateLock(() => {
1881
- const state = this.readPrimarySummaryFallbackState();
1882
- let changed = false;
1883
- state.requests = state.requests.map((record) => {
1884
- if (resolvedRequestIds.has(record.requestId)) return record;
1885
- if (record.dispatchKind === 'primary_summary_fallback_retry' && (
1886
- record.finalStatus === 'pending' || record.finalStatus === 'refusal'
1887
- )) {
1888
- changed = true;
1889
- const reason = 'primary_summary_fallback_unresolved_on_restart';
1890
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1891
- return {
1892
- ...record,
1893
- finalStatus: 'held',
1894
- fallbackHeldReason: reason,
1895
- };
1896
- }
1897
- if (record.fallbackIntent && record.fallbackStatus === 'pending') {
1898
- changed = true;
1899
- const reason = 'primary_summary_fallback_unresolved_on_restart';
1900
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1901
- return {
1902
- ...record,
1903
- fallbackStatus: 'held',
1904
- fallbackHeldReason: reason,
1905
- };
1906
- }
1907
- if (record.finalStatus === 'refusal') {
1908
- changed = true;
1909
- const reason = 'primary_summary_request_unresolved_on_restart';
1910
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1911
- return {
1912
- ...record,
1913
- finalStatus: 'held',
1914
- fallbackStatus: 'held',
1915
- fallbackHeldReason: reason,
1916
- };
1917
- }
1918
- if (record.finalStatus === 'pending') {
1919
- changed = true;
1920
- const reason = 'primary_summary_request_unresolved_on_restart';
1921
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1922
- return {
1923
- ...record,
1924
- finalStatus: 'timeout_unknown',
1925
- fallbackHeldReason: reason,
1926
- };
1927
- }
1928
- return record;
1929
- });
1930
- if (changed) this.writePrimarySummaryFallbackState({ requests: state.requests.slice(-256) });
1931
- });
1932
- for (const alert of alerts) {
1933
- this.opsAlert('refusal-held', alert.agentName, alert.reason, {
1934
- data: { requestId: alert.requestId },
1935
- });
1936
- }
1937
- }
1938
-
1939
- private primarySummaryIdentityKey(identity: PrimarySummaryIdentity): string {
1940
- return [
1941
- identity.id,
1942
- identity.contentHash,
1943
- identity.carrierHash,
1944
- identity.sourceLeafHash,
1945
- ].join('\u0000');
1946
- }
1947
-
1948
- private primarySummaryProjectionKeys(projection: PrimarySummaryProjection | null): string[] {
1949
- return projection?.selectedSummaries.map((selection) =>
1950
- this.primarySummaryIdentityKey(selection.identity),
1951
- ) ?? [];
1952
- }
1953
-
1954
- private primarySummaryFallbackConfig(agent: Agent): {
1955
- enabled: boolean;
1956
- maxNewSummaries: number;
1957
- requestBudgetTokens: number;
1958
- } {
1959
- const raw = (agent.refusalHandling as {
1960
- primarySummaryFallback?: {
1961
- enabled?: boolean;
1962
- maxNewSummaries?: number;
1963
- requestBudgetTokens?: number;
1964
- };
1965
- } | undefined)?.primarySummaryFallback;
1966
- const fallbackBudgetBase = agent.contextBudgetTokens ?? 100_000;
1967
- return {
1968
- enabled: raw?.enabled === true,
1969
- maxNewSummaries:
1970
- typeof raw?.maxNewSummaries === 'number' && Number.isSafeInteger(raw.maxNewSummaries)
1971
- ? Math.max(0, raw.maxNewSummaries)
1972
- : 4,
1973
- requestBudgetTokens:
1974
- typeof raw?.requestBudgetTokens === 'number' && Number.isFinite(raw.requestBudgetTokens) && raw.requestBudgetTokens > 0
1975
- ? raw.requestBudgetTokens
1976
- : fallbackBudgetBase + agent.maxTokens,
1977
- };
1978
- }
1979
-
1980
- private latestCompatibleHealthyPrimaryRequest(
1981
- current: PrimarySummaryRequestRecord,
1982
- state: PrimarySummaryFallbackState,
1983
- ): PrimarySummaryRequestRecord | null {
1984
- for (let i = state.requests.length - 1; i >= 0; i--) {
1985
- const candidate = state.requests[i]!;
1986
- if (candidate.requestId === current.requestId) continue;
1987
- if (candidate.agentName !== current.agentName) continue;
1988
- if (candidate.namespace !== current.namespace) continue;
1989
- if (candidate.branch.id !== current.branch.id) continue;
1990
- if (candidate.branch.generation !== current.branch.generation) continue;
1991
- if (candidate.systemHash !== current.systemHash) continue;
1992
- if (candidate.modelConfigHash !== current.modelConfigHash) continue;
1993
- if (candidate.toolContractHash !== current.toolContractHash) continue;
1994
- if (candidate.dispatchKind !== 'primary') continue;
1995
- if (candidate.fallbackIntent || candidate.fallbackStatus) continue;
1996
- if (candidate.finalStatus !== 'success') continue;
1997
- return candidate;
1998
- }
1999
- return null;
2000
- }
2001
-
2002
- private async persistPrimarySummaryRequestRecord(record: PrimarySummaryRequestRecord): Promise<void> {
2003
- await this.withPrimarySummaryStateLock(() => {
2004
- const state = this.readPrimarySummaryFallbackState();
2005
- const requests = state.requests.filter((entry) => entry.requestId !== record.requestId);
2006
- requests.push(record);
2007
- this.writePrimarySummaryFallbackState({ requests: requests.slice(-256) });
2008
- });
2009
- }
2010
-
2011
- private async updatePrimarySummaryRequestRecord(
2012
- requestId: string,
2013
- update: (record: PrimarySummaryRequestRecord) => PrimarySummaryRequestRecord,
2014
- ): Promise<PrimarySummaryRequestRecord | null> {
2015
- return this.withPrimarySummaryStateLock(() => {
2016
- const state = this.readPrimarySummaryFallbackState();
2017
- const index = state.requests.findIndex((entry) => entry.requestId === requestId);
2018
- if (index < 0) return null;
2019
- state.requests[index] = update(state.requests[index]!);
2020
- this.writePrimarySummaryFallbackState({ requests: state.requests.slice(-256) });
2021
- return state.requests[index]!;
2022
- });
2023
- }
2024
-
2025
- private async getPrimarySummaryRequestRecord(requestId: string): Promise<PrimarySummaryRequestRecord | null> {
2026
- return this.withPrimarySummaryStateLock(() => {
2027
- const state = this.readPrimarySummaryFallbackState();
2028
- return state.requests.find((entry) => entry.requestId === requestId) ?? null;
2029
- });
2030
- }
2031
-
2032
- private primarySummaryContractsMatch(
2033
- left: PrimarySummaryContract & { systemHash: string },
2034
- right: PrimarySummaryContract & { systemHash: string },
2035
- ): boolean {
2036
- return left.systemHash === right.systemHash
2037
- && left.modelConfigHash === right.modelConfigHash
2038
- && left.toolContractHash === right.toolContractHash;
2039
- }
2040
-
2041
- private primarySummaryFallbackOutputPhaseHash(blocks: ReadonlyArray<ContentBlock>): string {
2042
- return this.hashJson(blocks);
2043
- }
2044
-
2045
- private primarySummaryFallbackBlockTypes(blocks: ReadonlyArray<ContentBlock>): string[] {
2046
- return blocks.map((block) => block.type);
2047
- }
2048
-
2049
- private static readonly NON_EXECUTED_TOOL_RESULT_TEXT = '[tool not executed]';
2050
-
2051
- private buildNonExecutedToolResults(
2052
- blocks: ReadonlyArray<ContentBlock>,
2053
- ): ContentBlock[] {
2054
- const seenToolUseIds = new Set<string>();
2055
- return blocks
2056
- .filter((block): block is ContentBlock & { type: 'tool_use'; id: string } => block.type === 'tool_use')
2057
- .filter((block) => {
2058
- if (seenToolUseIds.has(block.id)) return false;
2059
- seenToolUseIds.add(block.id);
2060
- return true;
2061
- })
2062
- .map((block) => ({
2063
- type: 'tool_result' as const,
2064
- toolUseId: block.id,
2065
- content: AgentFramework.NON_EXECUTED_TOOL_RESULT_TEXT,
2066
- isError: true,
2067
- }));
2068
- }
2069
-
2070
- private buildPrimarySummarySettlementId(
2071
- record: Pick<PrimarySummaryRequestRecord, 'dispatchKind'>,
2072
- outcome: PrimarySummarySettlementOutcome,
2073
- ): string {
2074
- return `${record.dispatchKind}:${outcome}:terminal:v${PRIMARY_SUMMARY_SETTLEMENT_VERSION}`;
2075
- }
2076
-
2077
- private buildPrimarySummarySettlementMetadata(
2078
- record: PrimarySummaryRequestRecord,
2079
- options: {
2080
- outcome: PrimarySummarySettlementOutcome;
2081
- settlementId?: string;
2082
- stopReason?: string;
2083
- holdReason?: string;
2084
- providerInputTokens?: number;
2085
- visibleAssistantOutput: boolean;
2086
- executedToolCalls: number;
2087
- entryIndex: number;
2088
- entryCount: number;
2089
- role: PrimarySummarySettlementRole;
2090
- kind: PrimarySummarySettlementKind;
2091
- },
2092
- ): MessageMetadata {
2093
- const settlementId = options.settlementId ?? this.buildPrimarySummarySettlementId(record, options.outcome);
2094
- return {
2095
- [PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY]: {
2096
- version: PRIMARY_SUMMARY_SETTLEMENT_VERSION,
2097
- requestId: record.requestId,
2098
- settlementId,
2099
- agentName: record.agentName,
2100
- dispatchKind: record.dispatchKind,
2101
- outcome: options.outcome,
2102
- ...(options.stopReason ? { stopReason: options.stopReason } : {}),
2103
- ...(options.holdReason ? { holdReason: options.holdReason } : {}),
2104
- ...(options.providerInputTokens !== undefined
2105
- ? { providerInputTokens: options.providerInputTokens }
2106
- : {}),
2107
- visibleAssistantOutput: options.visibleAssistantOutput,
2108
- executedToolCalls: options.executedToolCalls,
2109
- branch: {
2110
- id: record.branch.id,
2111
- name: record.branch.name,
2112
- generation: record.branch.generation,
2113
- },
2114
- role: options.role,
2115
- kind: options.kind,
2116
- entryIndex: options.entryIndex,
2117
- entryCount: options.entryCount,
2118
- } satisfies PrimarySummarySettlementMetadata,
2119
- };
2120
- }
2121
-
2122
- private isBoundedNonEmptyString(value: unknown, maxLength: number): value is string {
2123
- return typeof value === 'string' && value.length > 0 && value.length <= maxLength;
2124
- }
2125
-
2126
- private isSafeNonNegativeInteger(value: unknown): value is number {
2127
- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
2128
- }
2129
-
2130
- private primarySummarySettlementRequestId(message: StoredMessage): string | null {
2131
- const raw = message.metadata?.[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY];
2132
- if (!raw || typeof raw !== 'object') return null;
2133
- const requestId = (raw as { requestId?: unknown }).requestId;
2134
- return typeof requestId === 'string' ? requestId : null;
2135
- }
2136
-
2137
- private parsePrimarySummarySettlementMetadata(
2138
- message: StoredMessage,
2139
- ): PrimarySummarySettlementMetadata | { invalidReason: string } | null {
2140
- const raw = message.metadata?.[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY];
2141
- if (!raw || typeof raw !== 'object') return null;
2142
- const candidate = raw as Partial<PrimarySummarySettlementMetadata>;
2143
- if (
2144
- candidate.version !== PRIMARY_SUMMARY_SETTLEMENT_VERSION ||
2145
- !this.isBoundedNonEmptyString(candidate.requestId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
2146
- !this.isBoundedNonEmptyString(candidate.settlementId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
2147
- !this.isBoundedNonEmptyString(candidate.agentName, PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH) ||
2148
- (candidate.dispatchKind !== 'primary' && candidate.dispatchKind !== 'primary_summary_fallback_retry') ||
2149
- (candidate.outcome !== 'success' && candidate.outcome !== 'held') ||
2150
- typeof candidate.visibleAssistantOutput !== 'boolean' ||
2151
- !this.isSafeNonNegativeInteger(candidate.executedToolCalls) ||
2152
- !this.isSafeNonNegativeInteger(candidate.entryIndex) ||
2153
- !this.isSafeNonNegativeInteger(candidate.entryCount) ||
2154
- (candidate.role !== 'assistant' && candidate.role !== 'user') ||
2155
- (candidate.kind !== 'assistant_output' && candidate.kind !== 'generated_tool_result') ||
2156
- !candidate.branch ||
2157
- !this.isBoundedNonEmptyString(candidate.branch.id, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
2158
- !this.isBoundedNonEmptyString(candidate.branch.name, PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH) ||
2159
- !this.isSafeNonNegativeInteger(candidate.branch.generation) ||
2160
- candidate.entryCount < 1 ||
2161
- candidate.entryIndex >= candidate.entryCount ||
2162
- (candidate.stopReason !== undefined
2163
- && !this.isBoundedNonEmptyString(candidate.stopReason, PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH)) ||
2164
- (candidate.holdReason !== undefined
2165
- && !this.isBoundedNonEmptyString(candidate.holdReason, PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH)) ||
2166
- (candidate.providerInputTokens !== undefined
2167
- && !this.isSafeNonNegativeInteger(candidate.providerInputTokens)) ||
2168
- (candidate.outcome === 'held' && candidate.holdReason === undefined) ||
2169
- (candidate.outcome === 'success' && candidate.holdReason !== undefined)
2170
- ) {
2171
- return { invalidReason: 'primary_summary_settlement_metadata_invalid_on_restart' };
2172
- }
2173
- return candidate as PrimarySummarySettlementMetadata;
2174
- }
2175
-
2176
- private primarySummarySettlementMetadataConsistent(
2177
- left: PrimarySummarySettlementMetadata,
2178
- right: PrimarySummarySettlementMetadata,
2179
- ): boolean {
2180
- return left.version === right.version
2181
- && left.requestId === right.requestId
2182
- && left.settlementId === right.settlementId
2183
- && left.agentName === right.agentName
2184
- && left.dispatchKind === right.dispatchKind
2185
- && left.outcome === right.outcome
2186
- && left.stopReason === right.stopReason
2187
- && left.holdReason === right.holdReason
2188
- && left.providerInputTokens === right.providerInputTokens
2189
- && left.visibleAssistantOutput === right.visibleAssistantOutput
2190
- && left.executedToolCalls === right.executedToolCalls
2191
- && left.branch.id === right.branch.id
2192
- && left.branch.name === right.branch.name
2193
- && left.branch.generation === right.branch.generation
2194
- && left.entryCount === right.entryCount;
2195
- }
2196
-
2197
- private primarySummaryShardEnvelopeConsistent(
2198
- participant: string,
2199
- left: PrimarySummarySettlementMetadata,
2200
- candidateParticipant: string,
2201
- right: PrimarySummarySettlementMetadata,
2202
- ): boolean {
2203
- return participant === candidateParticipant
2204
- && this.primarySummarySettlementMetadataConsistent(left, right)
2205
- && left.role === right.role
2206
- && left.kind === right.kind
2207
- && left.entryIndex === right.entryIndex;
2208
- }
2209
-
2210
- private primarySummaryLogicalSettlementEntries(
2211
- agent: Agent,
2212
- requestId: string,
2213
- ): PrimarySummaryLogicalSettlementEntry[] | { invalidReason: string } {
2214
- const allMessages = agent.getContextManager().getAllMessages()
2215
- .slice()
2216
- .sort((left, right) => left.sequence - right.sequence);
2217
- const entries: PrimarySummaryLogicalSettlementEntry[] = [];
2218
- for (let index = 0; index < allMessages.length;) {
2219
- const message = allMessages[index]!;
2220
- if (this.primarySummarySettlementRequestId(message) !== requestId) {
2221
- index++;
2222
- continue;
2223
- }
2224
- const parsed = this.parsePrimarySummarySettlementMetadata(message);
2225
- if (!parsed) {
2226
- index++;
2227
- continue;
2228
- }
2229
- if ('invalidReason' in parsed) return parsed;
2230
-
2231
- if (message.bodyGroupId === undefined) {
2232
- if (message.shardIndex !== undefined) {
2233
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2234
- }
2235
- entries.push({
2236
- messages: [message],
2237
- settlement: parsed,
2238
- participant: message.participant,
2239
- content: message.content,
2240
- });
2241
- index++;
2242
- continue;
2243
- }
2244
-
2245
- const groupId = message.bodyGroupId;
2246
- if (!this.isBoundedNonEmptyString(groupId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH)) {
2247
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2248
- }
2249
- if (index > 0 && allMessages[index - 1]?.bodyGroupId === groupId) {
2250
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2251
- }
2252
-
2253
- const groupMessages: StoredMessage[] = [];
2254
- while (index < allMessages.length && allMessages[index]!.bodyGroupId === groupId) {
2255
- groupMessages.push(allMessages[index]!);
2256
- index++;
2257
- }
2258
- if (allMessages.slice(index).some((candidate) => candidate.bodyGroupId === groupId)) {
2259
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2260
- }
2261
-
2262
- const orderedGroupMessages: StoredMessage[] = [];
2263
- let expectedShardIndex = 0;
2264
- for (const groupMessage of groupMessages) {
2265
- if (this.primarySummarySettlementRequestId(groupMessage) !== requestId) {
2266
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2267
- }
2268
- const groupParsed = this.parsePrimarySummarySettlementMetadata(groupMessage);
2269
- if (!groupParsed) {
2270
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2271
- }
2272
- if ('invalidReason' in groupParsed) return groupParsed;
2273
- if (!this.primarySummaryShardEnvelopeConsistent(
2274
- message.participant,
2275
- parsed,
2276
- groupMessage.participant,
2277
- groupParsed,
2278
- )) {
2279
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2280
- }
2281
- if (groupMessage.shardIndex !== expectedShardIndex) {
2282
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2283
- }
2284
- orderedGroupMessages.push(groupMessage);
2285
- expectedShardIndex++;
2286
- }
2287
-
2288
- entries.push({
2289
- messages: orderedGroupMessages,
2290
- settlement: parsed,
2291
- participant: message.participant,
2292
- content: orderedGroupMessages.flatMap((groupMessage) => groupMessage.content),
2293
- bodyGroupId: groupId,
2294
- });
2295
- }
2296
- return entries;
2297
- }
2298
-
2299
- private primarySummaryContentMatches(
2300
- left: ReadonlyArray<ContentBlock>,
2301
- right: ReadonlyArray<ContentBlock>,
2302
- ): boolean {
2303
- return this.primarySummaryFallbackOutputPhaseHash(left) === this.primarySummaryFallbackOutputPhaseHash(right);
2304
- }
2305
-
2306
- private async persistPrimarySummaryChronicleEntries(
2307
- agent: Agent,
2308
- entries: Array<{ participant: string; content: ContentBlock[]; metadata?: MessageMetadata }>,
2309
- ): Promise<string[]> {
2310
- const messageIds: string[] = [];
2311
- for (const entry of entries) {
2312
- if (entry.content.length === 0) continue;
2313
- messageIds.push(agent.getContextManager().addMessage(entry.participant, entry.content, entry.metadata));
2314
- }
2315
- return messageIds;
2316
- }
2317
-
2318
- private async markPrimarySummaryRequestRestartFailure(
2319
- record: PrimarySummaryRequestRecord,
2320
- reason: string,
2321
- ): Promise<void> {
2322
- if (record.dispatchKind === 'primary_summary_fallback_retry') {
2323
- const parent = await this.findPrimarySummaryRetryParent(record.requestId);
2324
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2325
- ...entry,
2326
- finalStatus: 'held',
2327
- fallbackHeldReason: reason,
2328
- }));
2329
- if (parent) {
2330
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => ({
2331
- ...entry,
2332
- finalStatus: 'held',
2333
- fallbackStatus: 'held',
2334
- fallbackHeldReason: reason,
2335
- }));
2336
- }
2337
- return;
2338
- }
2339
- await this.updatePrimarySummaryRequestRecord(record.requestId, (current) => ({
2340
- ...current,
2341
- finalStatus: 'held',
2342
- fallbackStatus: 'held',
2343
- fallbackHeldReason: reason,
2344
- }));
2345
- }
2346
-
2347
- private async persistPrimarySummaryHeldOutput(
2348
- agent: Agent,
2349
- requestId: string,
2350
- holdReason: string,
2351
- options: {
2352
- stopReason?: string;
2353
- providerInputTokens?: number;
2354
- visibleAssistantOutput: boolean;
2355
- executedToolCalls: number;
2356
- assistantBlocks: ContentBlock[];
2357
- retryDispatch?: PrimarySummaryFallbackRetryDispatch;
2358
- retryPhase?: string;
2359
- holdData?: Record<string, unknown>;
2360
- },
2361
- ): Promise<void> {
2362
- const record = await this.getPrimarySummaryRequestRecord(requestId);
2363
- if (!record) {
2364
- throw new Error(`Primary summary request '${requestId}' disappeared before held-output persistence`);
2365
- }
2366
- const placeholderBlocks = this.buildNonExecutedToolResults(options.assistantBlocks);
2367
- const entryCount = placeholderBlocks.length > 0 ? 2 : 1;
2368
- const assistantMetadata = this.buildPrimarySummarySettlementMetadata(record, {
2369
- outcome: 'held',
2370
- stopReason: options.stopReason,
2371
- holdReason,
2372
- providerInputTokens: options.providerInputTokens,
2373
- visibleAssistantOutput: options.visibleAssistantOutput,
2374
- executedToolCalls: options.executedToolCalls,
2375
- entryIndex: 0,
2376
- entryCount,
2377
- role: 'assistant',
2378
- kind: 'assistant_output',
2379
- });
2380
- const settlementId =
2381
- (assistantMetadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY] as PrimarySummarySettlementMetadata).settlementId;
2382
- const placeholderMetadata = placeholderBlocks.length > 0
2383
- ? this.buildPrimarySummarySettlementMetadata(record, {
2384
- outcome: 'held',
2385
- settlementId,
2386
- stopReason: options.stopReason,
2387
- holdReason,
2388
- providerInputTokens: options.providerInputTokens,
2389
- visibleAssistantOutput: options.visibleAssistantOutput,
2390
- executedToolCalls: options.executedToolCalls,
2391
- entryIndex: 1,
2392
- entryCount,
2393
- role: 'user',
2394
- kind: 'generated_tool_result',
2395
- })
2396
- : undefined;
2397
- const messageIds = await this.persistPrimarySummaryChronicleEntries(agent, [
2398
- { participant: agent.name, content: options.assistantBlocks, metadata: assistantMetadata },
2399
- ...(placeholderBlocks.length > 0 && placeholderMetadata
2400
- ? [{ participant: 'user', content: placeholderBlocks, metadata: placeholderMetadata }]
2401
- : []),
2402
- ]);
2403
- if (options.retryDispatch && options.retryPhase && options.assistantBlocks.length > 0) {
2404
- await this.recordPrimarySummaryRetryOutput(
2405
- options.retryDispatch.originalRequestId,
2406
- options.retryDispatch.intentId,
2407
- {
2408
- phase: options.retryPhase,
2409
- contentHash: this.primarySummaryFallbackOutputPhaseHash(options.assistantBlocks),
2410
- messageId: messageIds[0] ?? '',
2411
- blockTypes: this.primarySummaryFallbackBlockTypes(options.assistantBlocks),
2412
- persistedAt: Date.now(),
2413
- },
2414
- );
2415
- }
2416
- if (options.retryDispatch) {
2417
- await this.holdPrimarySummaryRetryFamily(
2418
- agent,
2419
- requestId,
2420
- options.retryDispatch,
2421
- holdReason,
2422
- options.holdData,
2423
- );
2424
- return;
2425
- }
2426
- await this.holdPrimarySummaryRequest(agent, requestId, holdReason, options.holdData);
2427
- }
2428
-
2429
- private async findPrimarySummaryRetryParent(
2430
- retryRequestId: string,
2431
- ): Promise<{ originalRequestId: string; intentId: string } | null> {
2432
- return this.withPrimarySummaryStateLock(() => {
2433
- const state = this.readPrimarySummaryFallbackState();
2434
- for (const record of state.requests) {
2435
- const intent = record.fallbackIntent;
2436
- if (intent?.retryRequestId === retryRequestId) {
2437
- return { originalRequestId: record.requestId, intentId: intent.intentId };
2438
- }
2439
- }
2440
- return null;
2441
- });
2442
- }
2443
-
2444
- private async finalizePrimarySummarySettlementOnStartup(
2445
- record: PrimarySummaryRequestRecord,
2446
- ): Promise<'finalized' | { invalidReason: string } | false> {
2447
- const agent = this.agents.get(record.agentName);
2448
- if (!agent) return false;
2449
- const settlementEntries = this.primarySummaryLogicalSettlementEntries(agent, record.requestId);
2450
- if ('invalidReason' in settlementEntries) return settlementEntries;
2451
- if (settlementEntries.length === 0) {
2452
- return false;
2453
- }
2454
- const currentBranch = agent.getCurrentBranchGeneration();
2455
- const assistantEntry = settlementEntries[0]!;
2456
- const settlement = assistantEntry.settlement;
2457
- if (
2458
- assistantEntry.participant !== record.agentName ||
2459
- settlement.agentName !== record.agentName ||
2460
- settlement.dispatchKind !== record.dispatchKind ||
2461
- settlement.entryIndex !== 0 ||
2462
- settlement.role !== 'assistant' ||
2463
- settlement.kind !== 'assistant_output'
2464
- ) {
2465
- return { invalidReason: 'primary_summary_settlement_invalid_assistant_marker_on_restart' };
2466
- }
2467
- if (!currentBranch) {
2468
- return { invalidReason: 'primary_summary_settlement_branch_unknown_on_restart' };
2469
- }
2470
- if (
2471
- settlement.branch.id !== record.branch.id ||
2472
- settlement.branch.name !== record.branch.name ||
2473
- settlement.branch.generation !== record.branch.generation ||
2474
- settlement.branch.id !== currentBranch.id ||
2475
- settlement.branch.name !== currentBranch.name ||
2476
- settlement.branch.generation !== currentBranch.generation
2477
- ) {
2478
- return { invalidReason: 'primary_summary_settlement_branch_mismatch_on_restart' };
2479
- }
2480
- if (settlement.entryCount < 1 || settlement.entryCount > 2) {
2481
- return { invalidReason: 'primary_summary_settlement_entry_count_invalid_on_restart' };
2482
- }
2483
- const entriesByIndex = new Map<number, PrimarySummaryLogicalSettlementEntry>();
2484
- for (const entry of settlementEntries) {
2485
- if (!this.primarySummarySettlementMetadataConsistent(settlement, entry.settlement)) {
2486
- return { invalidReason: 'primary_summary_settlement_marker_conflict_on_restart' };
2487
- }
2488
- if (entriesByIndex.has(entry.settlement.entryIndex)) {
2489
- return { invalidReason: 'primary_summary_settlement_duplicate_entry_on_restart' };
2490
- }
2491
- entriesByIndex.set(entry.settlement.entryIndex, entry);
2492
- }
2493
- const placeholderEntry = entriesByIndex.get(1);
2494
- if (
2495
- placeholderEntry
2496
- && (
2497
- placeholderEntry.participant !== 'user'
2498
- || placeholderEntry.settlement.role !== 'user'
2499
- || placeholderEntry.settlement.kind !== 'generated_tool_result'
2500
- )
2501
- ) {
2502
- return { invalidReason: 'primary_summary_settlement_placeholder_marker_invalid_on_restart' };
2503
- }
2504
- if ([...entriesByIndex.keys()].some((index) => index !== 0 && index !== 1)) {
2505
- return { invalidReason: 'primary_summary_settlement_out_of_order_entries_on_restart' };
2506
- }
2507
- const expectedPlaceholder = settlement.outcome === 'held'
2508
- ? this.buildNonExecutedToolResults(assistantEntry.content)
2509
- : [];
2510
- if (settlement.outcome === 'success') {
2511
- if (settlement.entryCount !== 1 || placeholderEntry) {
2512
- return { invalidReason: 'primary_summary_success_settlement_shape_invalid_on_restart' };
2513
- }
2514
- } else {
2515
- const expectedEntryCount = expectedPlaceholder.length > 0 ? 2 : 1;
2516
- if (settlement.entryCount !== expectedEntryCount) {
2517
- return { invalidReason: 'primary_summary_held_settlement_shape_invalid_on_restart' };
2518
- }
2519
- if (placeholderEntry) {
2520
- if (!this.primarySummaryContentMatches(placeholderEntry.content, expectedPlaceholder)) {
2521
- return { invalidReason: 'primary_summary_settlement_placeholder_content_mismatch_on_restart' };
2522
- }
2523
- } else if (expectedPlaceholder.length > 0) {
2524
- const placeholderMetadata = this.buildPrimarySummarySettlementMetadata(record, {
2525
- outcome: 'held',
2526
- settlementId: settlement.settlementId,
2527
- stopReason: settlement.stopReason,
2528
- holdReason: settlement.holdReason,
2529
- providerInputTokens: settlement.providerInputTokens,
2530
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2531
- executedToolCalls: settlement.executedToolCalls,
2532
- entryIndex: 1,
2533
- entryCount: settlement.entryCount,
2534
- role: 'user',
2535
- kind: 'generated_tool_result',
2536
- });
2537
- await this.persistPrimarySummaryChronicleEntries(agent, [
2538
- { participant: 'user', content: expectedPlaceholder, metadata: placeholderMetadata },
2539
- ]);
2540
- }
2541
- }
2542
-
2543
- if (record.dispatchKind === 'primary_summary_fallback_retry') {
2544
- const parent = await this.findPrimarySummaryRetryParent(record.requestId);
2545
- if (!parent) return { invalidReason: 'primary_summary_fallback_parent_missing_on_restart' };
2546
- if (settlement.outcome === 'success') {
2547
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2548
- ...entry,
2549
- stopReason: settlement.stopReason ?? entry.stopReason,
2550
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
2551
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2552
- executedToolCalls: settlement.executedToolCalls,
2553
- finalStatus: 'success',
2554
- fallbackHeldReason: undefined,
2555
- }));
2556
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => {
2557
- const intent = entry.fallbackIntent;
2558
- return {
2559
- ...entry,
2560
- finalStatus: 'success',
2561
- fallbackStatus: 'success',
2562
- fallbackHeldReason: undefined,
2563
- fallbackIntent: intent && intent.intentId === parent.intentId
2564
- ? { ...intent, finalizedAt: intent.finalizedAt ?? Date.now() }
2565
- : intent,
2566
- };
2567
- });
2568
- return 'finalized';
2569
- }
2570
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2571
- ...entry,
2572
- stopReason: settlement.stopReason ?? entry.stopReason,
2573
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
2574
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2575
- executedToolCalls: settlement.executedToolCalls,
2576
- finalStatus: 'held',
2577
- fallbackHeldReason: settlement.holdReason,
2578
- }));
2579
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => ({
2580
- ...entry,
2581
- finalStatus: 'held',
2582
- fallbackStatus: 'held',
2583
- fallbackHeldReason: settlement.holdReason,
2584
- }));
2585
- return 'finalized';
2586
- }
2587
-
2588
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2589
- ...entry,
2590
- stopReason: settlement.stopReason ?? entry.stopReason,
2591
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
2592
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2593
- executedToolCalls: settlement.executedToolCalls,
2594
- finalStatus: settlement.outcome === 'success' ? 'success' : 'held',
2595
- fallbackStatus: settlement.outcome === 'success' ? entry.fallbackStatus : 'held',
2596
- fallbackHeldReason: settlement.outcome === 'held' ? settlement.holdReason : undefined,
2597
- }));
2598
- return 'finalized';
2599
- }
2600
-
2601
- private summarizePrimarySummaryRetryRequest(request: NormalizedRequest): Record<string, unknown> {
2602
- const blockCounts = new Map<string, number>();
2603
- for (const message of request.messages) {
2604
- for (const block of message.content) {
2605
- blockCounts.set(block.type, (blockCounts.get(block.type) ?? 0) + 1);
2606
- }
2607
- }
2608
- return {
2609
- kind: 'primary_summary_fallback_retry',
2610
- requestHash: this.hashJson(request),
2611
- systemHash: this.hashJson(request.system ?? ''),
2612
- messageCount: request.messages.length,
2613
- toolCount: request.tools?.length ?? 0,
2614
- participantCount: [...new Set(request.messages.map((message) => message.participant))].length,
2615
- blockCounts: Object.fromEntries([...blockCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
2616
- promptCaching: request.promptCaching === true,
2617
- cacheTtl: request.cacheTtl,
2618
- assistantParticipant: request.assistantParticipant,
2619
- requestCompleteBoundTokens: this.estimateRequestCompleteBoundTokens(request),
2620
- };
2621
- }
2622
-
2623
- private summarizePrimarySummaryRetryResponse(response: NormalizedResponse): Record<string, unknown> {
2624
- const blockCounts = new Map<string, number>();
2625
- for (const block of response.content) {
2626
- blockCounts.set(block.type, (blockCounts.get(block.type) ?? 0) + 1);
2627
- }
2628
- return {
2629
- kind: 'primary_summary_fallback_retry',
2630
- stopReason: response.stopReason,
2631
- rawAssistantTextHash: response.rawAssistantText
2632
- ? this.hashJson(response.rawAssistantText)
2633
- : undefined,
2634
- blockCounts: Object.fromEntries([...blockCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
2635
- toolCallIds: response.toolCalls.map((call: { id: string }) => call.id),
2636
- usage: response.usage
2637
- ? {
2638
- inputTokens: response.usage.inputTokens,
2639
- outputTokens: response.usage.outputTokens,
2640
- }
2641
- : undefined,
2642
- };
2643
- }
2644
-
2645
- private async claimPrimarySummaryRetryIntent(
2646
- originalRequestId: string,
2647
- intentId: string,
2648
- retryRequestId: string,
2649
- ): Promise<'claimed' | 'duplicate' | 'missing'> {
2650
- let outcome: 'claimed' | 'duplicate' | 'missing' = 'missing';
2651
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2652
- const intent = record.fallbackIntent;
2653
- if (!intent || intent.intentId !== intentId || record.fallbackStatus !== 'pending') {
2654
- return record;
2655
- }
2656
- if (intent.retryRequestId && intent.retryRequestId !== retryRequestId) {
2657
- outcome = 'duplicate';
2658
- return record;
2659
- }
2660
- outcome = 'claimed';
2661
- return {
2662
- ...record,
2663
- fallbackIntent: {
2664
- ...intent,
2665
- retryRequestId,
2666
- dispatchedAt: intent.dispatchedAt ?? Date.now(),
2667
- },
2668
- };
2669
- });
2670
- return outcome;
2671
- }
2672
-
2673
- private async validatePrimarySummaryRetryMutation(
2674
- agent: Agent,
2675
- requestId: string,
2676
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2677
- compiledRequest: NormalizedRequest,
2678
- artifacts: ActivationCompileArtifacts,
2679
- phase: string,
2680
- ): Promise<{ ok: true } | { ok: false; reason: string; data: Record<string, unknown> }> {
2681
- const branch = agent.getCurrentBranchGeneration();
2682
- if (!branch || branch.id !== retryDispatch.branch.id || branch.generation !== retryDispatch.branch.generation) {
2683
- return {
2684
- ok: false,
2685
- reason: `primary_summary_fallback_branch_changed_${phase}`,
2686
- data: {
2687
- expectedBranchId: retryDispatch.branch.id,
2688
- expectedGeneration: retryDispatch.branch.generation,
2689
- actualBranchId: branch?.id,
2690
- actualGeneration: branch?.generation,
2691
- },
2692
- };
2693
- }
2694
- const requestHash = this.hashJson(compiledRequest);
2695
- if (requestHash !== retryDispatch.expectedRetryRequestHash) {
2696
- return {
2697
- ok: false,
2698
- reason: `primary_summary_fallback_request_changed_${phase}`,
2699
- data: {
2700
- expectedRetryRequestHash: retryDispatch.expectedRetryRequestHash,
2701
- actualRetryRequestHash: requestHash,
2702
- },
2703
- };
2704
- }
2705
- if (
2706
- this.primarySummaryProjectionKeys(artifacts.compileResult.primarySummaryProjection ?? null).join('\u0000') !==
2707
- retryDispatch.originalProjectionKeys.join('\u0000')
2708
- ) {
2709
- return {
2710
- ok: false,
2711
- reason: `primary_summary_fallback_projection_changed_${phase}`,
2712
- data: {
2713
- expectedProjectionKeys: retryDispatch.originalProjectionKeys,
2714
- actualProjectionKeys: this.primarySummaryProjectionKeys(
2715
- artifacts.compileResult.primarySummaryProjection ?? null,
2716
- ),
2717
- },
2718
- };
2719
- }
2720
- if (!this.primarySummaryContractsMatch(artifacts.contract, retryDispatch.originalArtifacts.contract)) {
2721
- return {
2722
- ok: false,
2723
- reason: `primary_summary_fallback_contract_changed_${phase}`,
2724
- data: {
2725
- expectedContract: retryDispatch.originalArtifacts.contract,
2726
- actualContract: artifacts.contract,
2727
- },
2728
- };
2729
- }
2730
- const original = await this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId);
2731
- if (
2732
- !original?.fallbackIntent ||
2733
- original.fallbackIntent.intentId !== retryDispatch.intentId ||
2734
- original.fallbackIntent.retryRequestId !== requestId ||
2735
- original.fallbackStatus !== 'pending'
2736
- ) {
2737
- return {
2738
- ok: false,
2739
- reason: `primary_summary_fallback_intent_changed_${phase}`,
2740
- data: {
2741
- expectedIntentId: retryDispatch.intentId,
2742
- actualIntentId: original?.fallbackIntent?.intentId,
2743
- retryRequestId: original?.fallbackIntent?.retryRequestId,
2744
- fallbackStatus: original?.fallbackStatus,
2745
- },
2746
- };
2747
- }
2748
- return { ok: true };
2749
- }
2750
-
2751
- private async recordPrimarySummaryRetryQuarantine(
2752
- originalRequestId: string,
2753
- intentId: string,
2754
- ): Promise<void> {
2755
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2756
- const intent = record.fallbackIntent;
2757
- if (!intent || intent.intentId !== intentId || intent.quarantinePersistedAt) return record;
2758
- return {
2759
- ...record,
2760
- fallbackIntent: {
2761
- ...intent,
2762
- quarantinePersistedAt: Date.now(),
2763
- },
2764
- };
2765
- });
2766
- }
2767
-
2768
- private async recordPrimarySummaryRetryOutput(
2769
- originalRequestId: string,
2770
- intentId: string,
2771
- output: PrimarySummaryFallbackOutputRecord,
2772
- ): Promise<void> {
2773
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2774
- const intent = record.fallbackIntent;
2775
- if (!intent || intent.intentId !== intentId) return record;
2776
- const existing = intent.outputs?.find((entry) => entry.phase === output.phase);
2777
- if (
2778
- existing &&
2779
- existing.contentHash === output.contentHash &&
2780
- existing.messageId === output.messageId
2781
- ) {
2782
- return record;
2783
- }
2784
- if (existing) {
2785
- throw new Error(`Primary summary fallback output phase '${output.phase}' changed after persistence`);
2786
- }
2787
- return {
2788
- ...record,
2789
- fallbackIntent: {
2790
- ...intent,
2791
- outputs: [...(intent.outputs ?? []), output],
2792
- },
2793
- };
2794
- });
2795
- }
2796
-
2797
- private async recordPrimarySummaryRetryToolDispatch(
2798
- originalRequestId: string,
2799
- intentId: string,
2800
- call: ToolCall,
2801
- ): Promise<void> {
2802
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2803
- const intent = record.fallbackIntent;
2804
- if (!intent || intent.intentId !== intentId) return record;
2805
- const inputHash = this.hashJson(call.input);
2806
- const existing = intent.toolDispatches?.find((entry) => entry.callId === call.id);
2807
- if (existing?.inputHash === inputHash) return record;
2808
- if (existing) {
2809
- throw new Error(`Primary summary fallback tool dispatch '${call.id}' changed after persistence`);
2810
- }
2811
- return {
2812
- ...record,
2813
- fallbackIntent: {
2814
- ...intent,
2815
- toolDispatches: [
2816
- ...(intent.toolDispatches ?? []),
2817
- {
2818
- callId: call.id,
2819
- inputHash,
2820
- recordedAt: Date.now(),
2821
- },
2822
- ],
2823
- },
2824
- };
2825
- });
2826
- }
2827
-
2828
- private async resolveContextBudgetRestartDispatch(
2829
- requestId: string,
2830
- retryDispatch?: PrimarySummaryFallbackRetryDispatch,
2831
- ): Promise<ContextBudgetRestartDispatch> {
2832
- const retryIdentity = retryDispatch
2833
- ? {
2834
- requestId,
2835
- dispatchKind: 'primary_summary_fallback_retry' as const,
2836
- originalRequestId: retryDispatch.originalRequestId,
2837
- intentId: retryDispatch.intentId,
2838
- }
2839
- : null;
2840
- const record = await this.getPrimarySummaryRequestRecord(requestId);
2841
- if (record?.dispatchKind === 'primary_summary_fallback_retry' && retryIdentity) {
2842
- return retryIdentity;
2843
- }
2844
- if (retryIdentity) {
2845
- const original = await this.getPrimarySummaryRequestRecord(retryIdentity.originalRequestId);
2846
- if (
2847
- original?.fallbackIntent?.intentId === retryIdentity.intentId
2848
- && original.fallbackIntent.retryRequestId === requestId
2849
- ) {
2850
- return retryIdentity;
2851
- }
2852
- }
2853
- return {
2854
- requestId,
2855
- dispatchKind: 'primary',
2856
- };
2857
- }
2858
-
2859
- private contextBudgetRestartMatchesIdentity(
2860
- request: PendingInferenceRequest,
2861
- identity: ContextBudgetRestartDispatch,
2862
- ): boolean {
2863
- if (request.reason !== 'context_budget_restart') return false;
2864
- const restart = request.contextBudgetRestart;
2865
- if (!restart) return false;
2866
- if (restart.dispatchKind !== identity.dispatchKind) return false;
2867
- if (restart.requestId !== identity.requestId) return false;
2868
- if (identity.dispatchKind !== 'primary_summary_fallback_retry') return true;
2869
- return restart.originalRequestId === identity.originalRequestId
2870
- && restart.intentId === identity.intentId;
2871
- }
2872
-
2873
- private clearQueuedContextBudgetRestart(identity: ContextBudgetRestartDispatch): number {
2874
- const before = this.pendingRequests.length;
2875
- this.pendingRequests = this.pendingRequests.filter(
2876
- (request) => !this.contextBudgetRestartMatchesIdentity(request, identity),
2877
- );
2878
- return before - this.pendingRequests.length;
2879
- }
2880
-
2881
- private async markPrimarySummaryRetrySuccess(
2882
- requestId: string,
2883
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2884
- stopReason: string | undefined,
2885
- providerInputTokens: number | undefined,
2886
- visibleAssistantOutput: boolean,
2887
- executedToolCalls: number,
2888
- ): Promise<void> {
2889
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2890
- ...record,
2891
- stopReason,
2892
- providerInputTokens,
2893
- visibleAssistantOutput,
2894
- executedToolCalls,
2895
- finalStatus: 'success',
2896
- fallbackHeldReason: undefined,
2897
- }));
2898
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => {
2899
- const intent = record.fallbackIntent;
2900
- return {
2901
- ...record,
2902
- finalStatus: 'success',
2903
- fallbackStatus: 'success',
2904
- fallbackHeldReason: undefined,
2905
- fallbackIntent: intent && intent.intentId === retryDispatch.intentId
2906
- ? { ...intent, finalizedAt: Date.now() }
2907
- : intent,
2908
- };
2909
- });
2910
- }
2911
-
2912
- private async holdPrimarySummaryRetryFamily(
2913
- agent: Agent,
2914
- requestId: string,
2915
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2916
- reason: string,
2917
- data?: Record<string, unknown>,
2918
- options?: { emitChronicleMarker?: boolean; stopReason?: string },
2919
- ): Promise<void> {
2920
- const clearedQueuedContextBudgetRestarts = this.clearQueuedContextBudgetRestart({
2921
- requestId,
2922
- dispatchKind: 'primary_summary_fallback_retry',
2923
- originalRequestId: retryDispatch.originalRequestId,
2924
- intentId: retryDispatch.intentId,
2925
- });
2926
- const holdData = clearedQueuedContextBudgetRestarts > 0
2927
- ? { ...(data ?? {}), clearedQueuedContextBudgetRestarts }
2928
- : data;
2929
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2930
- ...record,
2931
- ...(options?.stopReason ? { stopReason: options.stopReason } : {}),
2932
- finalStatus: 'held',
2933
- fallbackHeldReason: reason,
2934
- }));
2935
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => ({
2936
- ...record,
2937
- finalStatus: 'held',
2938
- fallbackStatus: 'held',
2939
- fallbackHeldReason: reason,
2940
- }));
2941
- this.opsAlert('refusal-held', agent.name, reason, { data: holdData });
2942
- if (options?.emitChronicleMarker === false) return;
2943
- try {
2944
- agent.getContextManager().addMessage(
2945
- 'user',
2946
- [{
2947
- type: 'text',
2948
- text:
2949
- `[refusal-held] Your previous turn was refused by the provider and ` +
2950
- `is being held for operator review rather than silently retried. ` +
2951
- `Automatic raw-expansion recovery was either unsafe or unsuccessful.`,
2952
- }],
2953
- { system: true, kind: 'refusal-held', reason },
2954
- );
2955
- } catch (error) {
2956
- console.error('[refusal-held] could not record chronicle marker:', error);
2957
- }
2958
- }
2959
-
2960
- private async primarySummaryRetryAlreadySucceeded(
2961
- requestId: string,
2962
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2963
- ): Promise<boolean> {
2964
- const [retryRecord, originalRecord] = await Promise.all([
2965
- this.getPrimarySummaryRequestRecord(requestId),
2966
- this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId),
2967
- ]);
2968
- return retryRecord?.finalStatus === 'success'
2969
- || originalRecord?.fallbackStatus === 'success'
2970
- || originalRecord?.finalStatus === 'success';
2971
- }
2972
-
2973
- private async primarySummaryRetryFamilyAlreadySettled(
2974
- requestId: string,
2975
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2976
- ): Promise<boolean> {
2977
- const [retryRecord, originalRecord] = await Promise.all([
2978
- this.getPrimarySummaryRequestRecord(requestId),
2979
- this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId),
2980
- ]);
2981
- return retryRecord?.finalStatus === 'held'
2982
- || retryRecord?.finalStatus === 'success'
2983
- || originalRecord?.fallbackStatus === 'held'
2984
- || originalRecord?.fallbackStatus === 'success'
2985
- || originalRecord?.finalStatus === 'held'
2986
- || originalRecord?.finalStatus === 'success';
2987
- }
2988
-
2989
- private async holdPrimarySummaryRequest(
2990
- agent: Agent,
2991
- requestId: string,
2992
- reason: string,
2993
- data?: Record<string, unknown>,
2994
- ): Promise<void> {
2995
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2996
- ...record,
2997
- finalStatus: 'held',
2998
- fallbackStatus: 'held',
2999
- fallbackHeldReason: reason,
3000
- }));
3001
- this.opsAlert('refusal-held', agent.name, reason, { data });
3002
- try {
3003
- agent.getContextManager().addMessage(
3004
- 'user',
3005
- [{
3006
- type: 'text',
3007
- text:
3008
- `[refusal-held] Your previous turn was refused by the provider and ` +
3009
- `is being held for operator review rather than silently retried. ` +
3010
- `Automatic raw-expansion recovery was either unsafe or unsuccessful.`,
3011
- }],
3012
- { system: true, kind: 'refusal-held', reason },
3013
- );
3014
- } catch (error) {
3015
- console.error('[refusal-held] could not record chronicle marker:', error);
3016
- }
3017
- }
3018
-
3019
- private hasVisibleAssistantOutput(blocks: ReadonlyArray<ContentBlock>): boolean {
3020
- return blocks.some((block) => block.type !== 'thinking' && block.type !== 'redacted_thinking');
3021
- }
3022
-
3023
- private buildPrimarySummaryRequestRecord(
3024
- agent: Agent,
3025
- requestId: string,
3026
- dispatchKind: PrimarySummaryRequestRecord['dispatchKind'],
3027
- request: NormalizedRequest,
3028
- artifacts: ActivationCompileArtifacts | undefined,
3029
- branch: BranchGenerationInfo | null,
3030
- stopReason: string | undefined,
3031
- visibleAssistantOutput: boolean,
3032
- executedToolCalls: number,
3033
- providerInputTokens: number | undefined,
3034
- finalStatus: PrimarySummaryFallbackStatus,
3035
- ): PrimarySummaryRequestRecord {
3036
- const projection = artifacts?.compileResult.primarySummaryProjection ?? null;
3037
- return {
3038
- requestId,
3039
- agentName: agent.name,
3040
- namespace: projection?.namespace ?? `agents/${agent.name}`,
3041
- timestamp: Date.now(),
3042
- dispatchKind,
3043
- branch: branch
3044
- ? { id: branch.id, name: branch.name, generation: branch.generation }
3045
- : { id: 'unknown', name: 'unknown', generation: -1 },
3046
- projection,
3047
- requestInputBoundTokens: this.estimateRequestInputBoundTokens(request),
3048
- requestCompleteBoundTokens: this.estimateRequestCompleteBoundTokens(request),
3049
- providerInputTokens,
3050
- systemHash: artifacts?.contract.systemHash ?? this.hashJson(request.system ?? ''),
3051
- modelConfigHash: artifacts?.contract.modelConfigHash ?? this.hashJson({ request: request.config }),
3052
- toolContractHash: artifacts?.contract.toolContractHash ?? this.hashJson(request.tools ?? []),
3053
- stopReason,
3054
- visibleAssistantOutput,
3055
- executedToolCalls,
3056
- finalStatus,
3057
- };
3058
- }
3059
-
3060
1619
  /** Liveness ping for an active ephemeral run (no-op otherwise). */
3061
1620
  private touchEphemeralRun(agentName: string, inferenceStarted = false): void {
3062
1621
  const run = this.ephemeralRuns.get(agentName);
@@ -3381,6 +1940,29 @@ export class AgentFramework {
3381
1940
  },
3382
1941
  };
3383
1942
 
1943
+ /** Synthesized read_image tool — present when a workspace module is
1944
+ * registered. Returns the image as a native image block in the tool
1945
+ * result, so the agent SEES it in the live turn (history keeps a compact
1946
+ * placeholder via the standard tool-result serializer). */
1947
+ private static readonly READ_IMAGE_TOOL: import('./types/index.js').ToolDefinition = {
1948
+ name: 'read_image',
1949
+ description:
1950
+ 'View an image file from a workspace mount. The image is returned into ' +
1951
+ 'your context so you can actually see it (e.g. after save_recent_image, ' +
1952
+ 'or for images placed in the workspace by other means). Path is ' +
1953
+ 'mount-prefixed, e.g. "project/photos/cat.png". Supports png/jpeg/gif/webp.',
1954
+ inputSchema: {
1955
+ type: 'object',
1956
+ properties: {
1957
+ path: {
1958
+ type: 'string',
1959
+ description: 'Mount-prefixed image path, e.g. "project/photos/name.png".',
1960
+ },
1961
+ },
1962
+ required: ['path'],
1963
+ },
1964
+ };
1965
+
3384
1966
  private static readonly AGENT_SETTINGS_TOOL: import('./types/index.js').ToolDefinition = {
3385
1967
  name: 'agent_settings',
3386
1968
  description:
@@ -4603,7 +3185,6 @@ export class AgentFramework {
4603
3185
  // Cast to AgentState to bypass TypeScript's control flow narrowing
4604
3186
  const currentState = agent.state as AgentState;
4605
3187
  if (currentState.status === 'ready') {
4606
- const activeStreamContext = this.activeStreamContexts.get(agent.name);
4607
3188
  // Flush pending assistant blocks (tool_use + preamble text) to context
4608
3189
  const pendingBlocks = this.pendingAssistantBlocks.get(agent.name);
4609
3190
  if (pendingBlocks) {
@@ -4725,59 +3306,6 @@ export class AgentFramework {
4725
3306
  // a terminal failure — rejecting an ephemeral's promise mid-run
4726
3307
  // and emitting a spurious inference:exhausted (same race shape as
4727
3308
  // endTurn above).
4728
- const requestId = activeStreamContext?.requestId;
4729
- const primarySummaryRetry = activeStreamContext?.trigger?.primarySummaryFallbackRetry;
4730
- const compiledRequest = activeStreamContext?.compiledRequest;
4731
- const artifacts = activeStreamContext?.artifacts;
4732
- const restartDispatch = await this.resolveContextBudgetRestartDispatch(
4733
- requestId ?? `${agent.name}:context_budget_restart`,
4734
- primarySummaryRetry,
4735
- );
4736
- if (
4737
- restartDispatch.dispatchKind === 'primary_summary_fallback_retry'
4738
- && requestId
4739
- && primarySummaryRetry
4740
- && compiledRequest
4741
- && artifacts
4742
- ) {
4743
- const validation = await this.validatePrimarySummaryRetryMutation(
4744
- agent,
4745
- requestId,
4746
- primarySummaryRetry,
4747
- compiledRequest,
4748
- artifacts,
4749
- 'before_context_budget_restart_hold',
4750
- );
4751
- if (!validation.ok) {
4752
- await this.holdPrimarySummaryRetryFamily(
4753
- agent,
4754
- requestId,
4755
- primarySummaryRetry,
4756
- validation.reason,
4757
- validation.data,
4758
- { emitChronicleMarker: false, stopReason: 'abort' },
4759
- );
4760
- } else {
4761
- await this.holdPrimarySummaryRetryFamily(
4762
- agent,
4763
- requestId,
4764
- primarySummaryRetry,
4765
- 'primary_summary_fallback_context_budget_restart_required',
4766
- {
4767
- inputTokens: agent.lastStreamInputTokens,
4768
- budget: agent.maxStreamTokens,
4769
- originalRequestId: primarySummaryRetry.originalRequestId,
4770
- intentId: primarySummaryRetry.intentId,
4771
- },
4772
- { emitChronicleMarker: false, stopReason: 'abort' },
4773
- );
4774
- }
4775
- this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
4776
- currentState.stream?.cancel();
4777
- agent.reset();
4778
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4779
- return;
4780
- }
4781
3309
  if (currentState.stream) {
4782
3310
  this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
4783
3311
  }
@@ -4794,8 +3322,6 @@ export class AgentFramework {
4794
3322
  reason: 'context_budget_restart',
4795
3323
  source: 'framework',
4796
3324
  timestamp: Date.now(),
4797
- channelId: activeStreamContext?.trigger?.channelId,
4798
- contextBudgetRestart: restartDispatch,
4799
3325
  });
4800
3326
  } else if (currentState.stream) {
4801
3327
  // Streaming path: convert results and resume the stream.
@@ -5296,18 +3822,30 @@ export class AgentFramework {
5296
3822
  ? event.origin.messageId
5297
3823
  : event.eventId;
5298
3824
  const maxBackscroll = descriptor?.capabilities?.history?.maxMessages ?? 0;
3825
+ // Resolve human-readable names from the event origin wherever the
3826
+ // surface provided them — a mind reading this should see WHO addressed
3827
+ // it WHERE, not a wall of snowflake ids. Raw ids remain only inside the
3828
+ // tool-argument instructions, where they are the literal values to pass.
3829
+ const origin = (event.origin ?? {}) as Record<string, unknown>;
3830
+ const authorName = typeof origin.authorName === 'string' && origin.authorName ? origin.authorName : 'Someone';
3831
+ const channelLabel = triggerChannel.label ? `#${triggerChannel.label}` : `"${triggerChannel.channelId}"`;
3832
+ const place = typeof origin.guildName === 'string' && origin.guildName
3833
+ ? `${channelLabel} in "${origin.guildName}"`
3834
+ : channelLabel;
5299
3835
  content.push({
5300
3836
  type: 'text',
5301
3837
  text:
5302
- `\n[Channel invitation] You were addressed in closed channel ` +
5303
- `"${triggerChannel.label ?? triggerChannel.channelId}" (${triggerChannel.channelId}, server ${event.serverId}). ` +
5304
- `You received this one message without subscribing. ` +
3838
+ `\n[Channel invitation] ${authorName} addressed you in ${place} — ` +
3839
+ `a channel you haven't joined. You're seeing this one message; you won't receive more from it unless you join. ` +
3840
+ `Your options:\n` +
3841
+ `1. Reply without joining — simply write your reply as normal text this turn; it will be delivered to ${channelLabel}.\n` +
3842
+ `2. Join the channel — call channel_open with channelId "${triggerChannel.channelId}" and serverId "${event.serverId}"` +
5305
3843
  (maxBackscroll > 0
5306
- ? `To subscribe, call channel_open with channelId, serverId "${event.serverId}", backscroll (0-${maxBackscroll}), and beforeMessageId "${messageId}". `
5307
- : `To subscribe, call channel_open with this channelId and serverId "${event.serverId}". `) +
5308
- `You may reply once without subscribing. To remain closed, call channel_decline ` +
5309
- `with channelId, serverId "${event.serverId}", and messageId "${messageId}"; ` +
5310
- `optionally set acknowledge to a surface value such as 👀.`,
3844
+ ? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId "${messageId}".\n`
3845
+ : `.\n`) +
3846
+ `3. Stay out — call channel_decline with channelId "${triggerChannel.channelId}", serverId "${event.serverId}", ` +
3847
+ `and messageId "${messageId}" (optionally set acknowledge to an emoji like 👀 so ${authorName} isn't left hanging).\n` +
3848
+ `Doing nothing is also fine.`,
5311
3849
  });
5312
3850
  metadata.channelInvitation = true;
5313
3851
  metadata.channelOpen = false;
@@ -5382,7 +3920,7 @@ export class AgentFramework {
5382
3920
  const state = this.createFrameworkState();
5383
3921
 
5384
3922
  // Group requests by agent
5385
- const requestsByAgent = new Map<string, PendingInferenceRequest[]>();
3923
+ const requestsByAgent = new Map<string, InferenceRequest[]>();
5386
3924
  for (const req of this.pendingRequests) {
5387
3925
  const existing = requestsByAgent.get(req.agentName) ?? [];
5388
3926
  existing.push(req);
@@ -5393,8 +3931,7 @@ export class AgentFramework {
5393
3931
  this.pendingRequests = [];
5394
3932
 
5395
3933
  // Check each agent
5396
- for (const [agentName, queuedRequests] of requestsByAgent) {
5397
- let requests = queuedRequests;
3934
+ for (const [agentName, requests] of requestsByAgent) {
5398
3935
  const agent = this.agents.get(agentName);
5399
3936
  if (!agent) {
5400
3937
  // Agent not found — request is orphaned. Emit warning and drop.
@@ -5439,19 +3976,6 @@ export class AgentFramework {
5439
3976
  continue;
5440
3977
  }
5441
3978
 
5442
- if (this.discordAwarenessBarrier) {
5443
- this.pendingRequests.push(...requests);
5444
- continue;
5445
- }
5446
-
5447
- requests = requests.filter((request) =>
5448
- request.reason !== 'context_budget_restart'
5449
- || request.contextBudgetRestart?.dispatchKind !== 'primary_summary_fallback_retry'
5450
- );
5451
- if (requests.length === 0) {
5452
- continue;
5453
- }
5454
-
5455
3979
  // Check policy
5456
3980
  if (!this.inferencePolicy.shouldInfer(agentName, requests, state)) {
5457
3981
  // Loud drop: a queued request that dies here is otherwise invisible —
@@ -5465,12 +3989,7 @@ export class AgentFramework {
5465
3989
  }
5466
3990
 
5467
3991
  // Start streaming inference (non-blocking — driveStream runs in background)
5468
- const trigger = requests.find((request) => request.primarySummaryFallbackRetry) ?? requests[0]!;
5469
- if (trigger.primarySummaryFallbackRetry) {
5470
- for (const request of requests) {
5471
- if (request !== trigger) this.pendingRequests.push(request);
5472
- }
5473
- }
3992
+ const trigger = requests[0];
5474
3993
  // Route this turn's auto-published speech to the channel that triggered
5475
3994
  // it (item-3 redux). A batched wake may carry several triggering channels
5476
3995
  // (messages arrived in >1 channel while the agent was busy/idle) — reply
@@ -5487,9 +4006,9 @@ export class AgentFramework {
5487
4006
  }
5488
4007
  }
5489
4008
 
5490
- private async startAgentStream(agent: Agent, trigger?: PendingInferenceRequest, attempt = 0): Promise<void> {
4009
+ private async startAgentStream(agent: Agent, trigger?: InferenceRequest, attempt = 0): Promise<void> {
5491
4010
  // Record turn checkpoint before inference (only on first attempt, not retries)
5492
- if (attempt === 0 && trigger?.reason !== 'primary-summary-fallback-retry') {
4011
+ if (attempt === 0) {
5493
4012
  this.recordTurnCheckpoint(agent.name);
5494
4013
  this.redoStacks.delete(agent.name); // new work invalidates redo
5495
4014
  }
@@ -5503,283 +4022,69 @@ export class AgentFramework {
5503
4022
  if (trigger?.channelId) {
5504
4023
  this.activeTriggerChannels.set(agent.name, trigger.channelId);
5505
4024
  } else {
5506
- this.activeTriggerChannels.delete(agent.name);
5507
- }
5508
-
5509
- this.touchEphemeralRun(agent.name, true);
5510
- this.emitTrace({ type: 'inference:started', agentName: agent.name });
5511
- this.eventGate?.onInferenceStarted(agent.name);
5512
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), startedAt: Date.now() });
5513
- const requestId = `${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5514
-
5515
- try {
5516
- const requestSnapshot = this.captureInferenceToolSnapshot(agent);
5517
- const allTools = this.getToolsForAgent(agent.name, requestSnapshot);
5518
- const tools = allTools.filter((t) => agent.canUseTool(t.name));
5519
- const fallbackConfig = this.primarySummaryFallbackConfig(agent);
5520
- const retryDispatch = trigger?.primarySummaryFallbackRetry;
5521
- let compiledRequest: NormalizedRequest;
5522
- let artifacts: ActivationCompileArtifacts;
5523
- let branch = agent.getCurrentBranchGeneration();
5524
-
5525
- if (retryDispatch) {
5526
- if (!fallbackConfig.enabled) {
5527
- await this.holdPrimarySummaryRequest(
5528
- agent,
5529
- retryDispatch.originalRequestId,
5530
- 'primary_summary_fallback_disabled_before_retry',
5531
- );
5532
- agent.reset();
5533
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5534
- this.eventGate?.onInferenceEnded(agent.name);
5535
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5536
- return;
5537
- }
5538
-
5539
- const claim = await this.claimPrimarySummaryRetryIntent(
5540
- retryDispatch.originalRequestId,
5541
- retryDispatch.intentId,
5542
- requestId,
5543
- );
5544
- if (claim !== 'claimed') {
5545
- await this.holdPrimarySummaryRequest(
5546
- agent,
5547
- retryDispatch.originalRequestId,
5548
- claim === 'duplicate'
5549
- ? 'primary_summary_fallback_duplicate_retry_blocked'
5550
- : 'primary_summary_fallback_retry_intent_missing',
5551
- { intentId: retryDispatch.intentId, retryRequestId: requestId },
5552
- );
5553
- agent.reset();
5554
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5555
- this.eventGate?.onInferenceEnded(agent.name);
5556
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5557
- return;
5558
- }
5559
-
5560
- if (
5561
- !branch ||
5562
- branch.id !== retryDispatch.branch.id ||
5563
- branch.generation !== retryDispatch.branch.generation
5564
- ) {
5565
- await this.holdPrimarySummaryRequest(
5566
- agent,
5567
- retryDispatch.originalRequestId,
5568
- 'primary_summary_fallback_branch_changed_before_retry',
5569
- {
5570
- expectedBranchId: retryDispatch.branch.id,
5571
- expectedGeneration: retryDispatch.branch.generation,
5572
- actualBranchId: branch?.id,
5573
- actualGeneration: branch?.generation,
5574
- },
5575
- );
5576
- agent.reset();
5577
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5578
- this.eventGate?.onInferenceEnded(agent.name);
5579
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5580
- return;
5581
- }
4025
+ this.activeTriggerChannels.delete(agent.name);
4026
+ }
5582
4027
 
5583
- const rebuilt = agent.buildPrimarySummaryRawExpansionRequest(
5584
- retryDispatch.originalArtifacts,
5585
- retryDispatch.candidateSummaries,
5586
- tools,
5587
- );
5588
- compiledRequest = rebuilt.request;
5589
- artifacts = rebuilt.artifacts;
4028
+ this.touchEphemeralRun(agent.name, true);
4029
+ this.emitTrace({ type: 'inference:started', agentName: agent.name });
4030
+ this.eventGate?.onInferenceStarted(agent.name);
4031
+ this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), startedAt: Date.now() });
5590
4032
 
5591
- const rebuiltHash = this.hashJson(compiledRequest);
5592
- if (
5593
- rebuiltHash !== retryDispatch.expectedRetryRequestHash ||
5594
- this.primarySummaryProjectionKeys(artifacts.compileResult.primarySummaryProjection ?? null).join('\u0000') !==
5595
- retryDispatch.originalProjectionKeys.join('\u0000')
5596
- ) {
5597
- await this.holdPrimarySummaryRequest(
5598
- agent,
5599
- retryDispatch.originalRequestId,
5600
- 'primary_summary_fallback_revalidation_failed_before_retry',
5601
- {
5602
- expectedRetryRequestHash: retryDispatch.expectedRetryRequestHash,
5603
- actualRetryRequestHash: rebuiltHash,
5604
- },
5605
- );
5606
- agent.reset();
5607
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5608
- this.eventGate?.onInferenceEnded(agent.name);
5609
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5610
- return;
5611
- }
4033
+ try {
4034
+ const requestSnapshot = this.captureInferenceToolSnapshot(agent);
4035
+ const allTools = this.getToolsForAgent(agent.name, requestSnapshot);
4036
+ const tools = allTools.filter((t) => agent.canUseTool(t.name));
5612
4037
 
5613
- const retryRecord = this.buildPrimarySummaryRequestRecord(
5614
- agent,
5615
- requestId,
5616
- 'primary_summary_fallback_retry',
5617
- compiledRequest,
5618
- artifacts,
5619
- branch,
5620
- undefined,
5621
- false,
5622
- 0,
5623
- undefined,
5624
- 'pending',
5625
- );
5626
- await this.persistPrimarySummaryRequestRecord(retryRecord);
4038
+ // Gather context from modules (pull-based) and MCPL hooks (push-based)
4039
+ // Both produce ContextInjection[] that get merged before inference.
4040
+ let injections: ContextInjection[] | undefined;
5627
4041
 
5628
- const admittedTokens = this.primarySummaryAdmissionBoundTokens(
5629
- retryDispatch.originalRequestInputBoundTokens,
5630
- compiledRequest,
5631
- retryDispatch.originalProviderInputTokens,
4042
+ // Module gatherContext (fail-open, per-module timeout — the module's
4043
+ // contextTimeoutMs, else the registry default). Injections are
4044
+ // channel-scoped via scopeInjectionsForAgent, matching the MCPL hook
4045
+ // injections below, so conversation forks don't see cross-channel
4046
+ // content.
4047
+ try {
4048
+ const moduleInjections = this.scopeInjectionsForAgent(
4049
+ agent.name,
4050
+ await this.moduleRegistry.gatherContext(agent.name),
5632
4051
  );
5633
- if (admittedTokens > retryDispatch.requestBudgetTokens) {
5634
- await this.holdPrimarySummaryRequest(
5635
- agent,
5636
- requestId,
5637
- 'primary_summary_fallback_retry_over_budget',
5638
- {
5639
- admittedTokens,
5640
- requestBudgetTokens: retryDispatch.requestBudgetTokens,
5641
- retryRequestHash: rebuiltHash,
5642
- },
5643
- );
5644
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => ({
5645
- ...record,
5646
- fallbackStatus: 'held',
5647
- fallbackHeldReason: 'primary_summary_fallback_retry_over_budget',
5648
- }));
5649
- agent.reset();
5650
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5651
- this.eventGate?.onInferenceEnded(agent.name);
5652
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5653
- return;
4052
+ if (moduleInjections.length > 0) {
4053
+ injections = moduleInjections;
5654
4054
  }
5655
- } else {
5656
- // Gather context from modules (pull-based) and MCPL hooks (push-based)
5657
- // Both produce ContextInjection[] that get merged before inference.
5658
- let injections: ContextInjection[] | undefined;
5659
-
5660
- // Module gatherContext (fail-open, per-module timeout — the module's
5661
- // contextTimeoutMs, else the registry default). Injections are
5662
- // channel-scoped via scopeInjectionsForAgent, matching the MCPL hook
5663
- // injections below, so conversation forks don't see cross-channel
5664
- // content.
4055
+ } catch (error) {
4056
+ console.error('Module gatherContext error:', error);
4057
+ }
4058
+
4059
+ // MCPL beforeInference hooks (fail-open)
4060
+ if (this.hookOrchestrator) {
5665
4061
  try {
5666
- const moduleInjections = this.scopeInjectionsForAgent(
4062
+ const hookParams = this.buildBeforeInferenceParams(agent, trigger);
4063
+ const hookInjections = this.scopeInjectionsForAgent(
5667
4064
  agent.name,
5668
- await this.moduleRegistry.gatherContext(agent.name),
4065
+ await this.hookOrchestrator.beforeInference(hookParams),
5669
4066
  );
5670
- if (moduleInjections.length > 0) {
5671
- injections = moduleInjections;
4067
+ if (hookInjections.length > 0) {
4068
+ injections = injections ? [...injections, ...hookInjections] : hookInjections;
5672
4069
  }
5673
4070
  } catch (error) {
5674
- console.error('Module gatherContext error:', error);
5675
- }
5676
-
5677
- // MCPL beforeInference hooks (fail-open)
5678
- if (this.hookOrchestrator) {
5679
- try {
5680
- const hookParams = this.buildBeforeInferenceParams(agent, trigger);
5681
- const hookInjections = this.scopeInjectionsForAgent(
5682
- agent.name,
5683
- await this.hookOrchestrator.beforeInference(hookParams),
5684
- );
5685
- if (hookInjections.length > 0) {
5686
- injections = injections ? [...injections, ...hookInjections] : hookInjections;
5687
- }
5688
- } catch (error) {
5689
- console.error('beforeInference hook error:', error);
5690
- }
5691
- }
5692
-
5693
- const prepared = await agent.prepareActivationRequest(tools, injections);
5694
- compiledRequest = prepared.request;
5695
- artifacts = prepared.artifacts;
5696
- branch = agent.getCurrentBranchGeneration();
5697
-
5698
- if (fallbackConfig.enabled) {
5699
- const record = this.buildPrimarySummaryRequestRecord(
5700
- agent,
5701
- requestId,
5702
- 'primary',
5703
- compiledRequest,
5704
- artifacts,
5705
- branch,
5706
- undefined,
5707
- false,
5708
- 0,
5709
- undefined,
5710
- 'pending',
5711
- );
5712
- await this.persistPrimarySummaryRequestRecord(record);
5713
- const expandedByQuarantine =
5714
- artifacts.compileResult.primarySummaryProjection?.selectedSummaries.some(
5715
- (selection) => selection.renderedAs === 'raw_expansion',
5716
- ) ?? false;
5717
- if (expandedByQuarantine) {
5718
- const admittedTokens = this.estimateRequestCompleteBoundTokens(compiledRequest);
5719
- if (admittedTokens > fallbackConfig.requestBudgetTokens) {
5720
- await this.holdPrimarySummaryRequest(
5721
- agent,
5722
- requestId,
5723
- 'primary_summary_quarantine_over_budget',
5724
- {
5725
- admittedTokens,
5726
- requestBudgetTokens: fallbackConfig.requestBudgetTokens,
5727
- },
5728
- );
5729
- agent.reset();
5730
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5731
- this.eventGate?.onInferenceEnded(agent.name);
5732
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5733
- return;
5734
- }
5735
- }
4071
+ console.error('beforeInference hook error:', error);
5736
4072
  }
5737
4073
  }
5738
4074
 
5739
- const { stream } = agent.startStreamFromRequest(compiledRequest, artifacts);
5740
- this.activeStreamContexts.set(agent.name, {
5741
- requestId,
5742
- trigger,
5743
- compiledRequest,
5744
- artifacts,
5745
- });
4075
+ const { stream, request: compiledRequest } = await agent.startStreamWithInjections(tools, injections);
4076
+
5746
4077
  const handle = this.driveStream(
5747
4078
  agent,
5748
4079
  stream,
5749
4080
  requestSnapshot,
5750
- requestId,
5751
4081
  trigger,
5752
4082
  attempt,
5753
4083
  compiledRequest,
5754
- artifacts,
5755
4084
  );
5756
4085
  this.activeStreams.set(agent.name, handle);
5757
4086
  } catch (error) {
5758
4087
  const err = error instanceof Error ? error : new Error(String(error));
5759
- if (trigger?.primarySummaryFallbackRetry) {
5760
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
5761
- ...record,
5762
- finalStatus: 'held',
5763
- fallbackHeldReason: err.message,
5764
- }));
5765
- await this.updatePrimarySummaryRequestRecord(trigger.primarySummaryFallbackRetry.originalRequestId, (record) => ({
5766
- ...record,
5767
- finalStatus: 'held',
5768
- fallbackStatus: 'held',
5769
- fallbackHeldReason: err.message,
5770
- }));
5771
- this.opsAlert('refusal-held', agent.name, err.message, {
5772
- data: { requestId, originalRequestId: trigger.primarySummaryFallbackRetry.originalRequestId },
5773
- });
5774
- agent.reset();
5775
- this.settleAgent(agent.name, {
5776
- stopReason: 'completed',
5777
- speech: '',
5778
- });
5779
- this.eventGate?.onInferenceEnded(agent.name);
5780
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5781
- return;
5782
- }
5783
4088
  this.emitTrace({
5784
4089
  type: 'inference:failed',
5785
4090
  agentName: agent.name,
@@ -5823,23 +4128,14 @@ export class AgentFramework {
5823
4128
  agent: Agent,
5824
4129
  stream: YieldingStream,
5825
4130
  requestSnapshot: InferenceToolSnapshot,
5826
- requestId: string,
5827
- trigger?: PendingInferenceRequest,
4131
+ trigger?: InferenceRequest,
5828
4132
  attempt = 0,
5829
- compiledRequest?: NormalizedRequest,
5830
- artifacts?: ActivationCompileArtifacts,
4133
+ compiledRequest?: NormalizedRequest
5831
4134
  ): Promise<void> {
5832
4135
  const startTime = Date.now();
4136
+ const requestId = `${agent.name}-${startTime}-${Math.random().toString(36).slice(2, 8)}`;
5833
4137
  const myStreamId = agent.streamId;
5834
4138
  let hadToolCalls = false;
5835
- let executedToolCalls = 0;
5836
- const primarySummaryConfig = this.primarySummaryFallbackConfig(agent);
5837
- const primarySummaryRetry = trigger?.primarySummaryFallbackRetry;
5838
- let primarySummaryQuarantinePersisted = false;
5839
- const fallbackRetryRequestForLog =
5840
- primarySummaryRetry && compiledRequest
5841
- ? this.summarizePrimarySummaryRetryRequest(compiledRequest)
5842
- : compiledRequest ?? { note: 'streaming request' };
5843
4139
 
5844
4140
  // ---- Present-while-acting turn state ---------------------------------
5845
4141
  // Output locus for the current CONVERSATIONAL ROUND: resolved lazily and
@@ -5848,7 +4144,7 @@ export class AgentFramework {
5848
4144
  // single provider inference can now contain several conversations. The
5849
4145
  // pin lives in `turnLocusPins` so it survives a context-budget restart. A
5850
4146
  // null resolution is not pinned.
5851
- if (trigger?.reason !== 'context_budget_restart' && trigger?.reason !== 'primary-summary-fallback-retry') {
4147
+ if (trigger?.reason !== 'context_budget_restart') {
5852
4148
  this.turnLocusPins.delete(agent.name);
5853
4149
  this.midTurnRoutingResets.delete(agent.name);
5854
4150
  }
@@ -5920,104 +4216,6 @@ export class AgentFramework {
5920
4216
  }
5921
4217
  };
5922
4218
 
5923
- const persistPrimarySummaryRetryQuarantine = async (): Promise<boolean> => {
5924
- if (!primarySummaryRetry || primarySummaryQuarantinePersisted || !artifacts) return true;
5925
- const validation = await this.validatePrimarySummaryRetryMutation(
5926
- agent,
5927
- requestId,
5928
- primarySummaryRetry,
5929
- compiledRequest!,
5930
- artifacts,
5931
- 'before_quarantine',
5932
- );
5933
- if (!validation.ok) {
5934
- await this.holdPrimarySummaryRetryFamily(
5935
- agent,
5936
- requestId,
5937
- primarySummaryRetry,
5938
- validation.reason,
5939
- validation.data,
5940
- { emitChronicleMarker: false },
5941
- );
5942
- stream.cancel();
5943
- agent.reset();
5944
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5945
- return false;
5946
- }
5947
- try {
5948
- const quarantinePrimarySummaryForPrimaryLane =
5949
- agent.getContextManager().quarantinePrimarySummaryForPrimaryLane;
5950
- if (typeof quarantinePrimarySummaryForPrimaryLane !== 'function') {
5951
- throw new Error('Primary summary quarantine is unavailable in the active context strategy');
5952
- }
5953
- await quarantinePrimarySummaryForPrimaryLane.call(
5954
- agent.getContextManager(),
5955
- artifacts.contract,
5956
- primarySummaryRetry.candidateSummaries,
5957
- );
5958
- primarySummaryQuarantinePersisted = true;
5959
- await this.recordPrimarySummaryRetryQuarantine(
5960
- primarySummaryRetry.originalRequestId,
5961
- primarySummaryRetry.intentId,
5962
- );
5963
- return true;
5964
- } catch (error) {
5965
- const reason = 'primary_summary_fallback_quarantine_failed';
5966
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, reason, {
5967
- error: error instanceof Error ? error.message : String(error),
5968
- });
5969
- stream.cancel();
5970
- agent.reset();
5971
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5972
- return false;
5973
- }
5974
- };
5975
-
5976
- const persistPrimarySummaryRetryOutput = async (
5977
- phase: string,
5978
- blocks: ContentBlock[],
5979
- participant = agent.name,
5980
- metadata?: MessageMetadata,
5981
- ): Promise<boolean> => {
5982
- if (!primarySummaryRetry || !artifacts) return true;
5983
- if (blocks.length === 0) return true;
5984
- const validation = await this.validatePrimarySummaryRetryMutation(
5985
- agent,
5986
- requestId,
5987
- primarySummaryRetry,
5988
- compiledRequest!,
5989
- artifacts,
5990
- `before_${phase}`,
5991
- );
5992
- if (!validation.ok) {
5993
- await this.holdPrimarySummaryRetryFamily(
5994
- agent,
5995
- requestId,
5996
- primarySummaryRetry,
5997
- validation.reason,
5998
- validation.data,
5999
- { emitChronicleMarker: false },
6000
- );
6001
- stream.cancel();
6002
- agent.reset();
6003
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
6004
- return false;
6005
- }
6006
- const messageId = agent.getContextManager().addMessage(participant, blocks, metadata);
6007
- await this.recordPrimarySummaryRetryOutput(
6008
- primarySummaryRetry.originalRequestId,
6009
- primarySummaryRetry.intentId,
6010
- {
6011
- phase,
6012
- contentHash: this.primarySummaryFallbackOutputPhaseHash(blocks),
6013
- messageId,
6014
- blockTypes: this.primarySummaryFallbackBlockTypes(blocks),
6015
- persistedAt: Date.now(),
6016
- },
6017
- );
6018
- return true;
6019
- };
6020
-
6021
4219
  try {
6022
4220
  for await (const event of stream) {
6023
4221
  this.touchEphemeralRun(agent.name, true);
@@ -6047,6 +4245,7 @@ export class AgentFramework {
6047
4245
  case 'tool-calls': {
6048
4246
  adoptInjectedRound();
6049
4247
  hadToolCalls = true;
4248
+ this.recordEphemeralToolCalls(agent.name, event.calls.length);
6050
4249
  this.emitTrace({
6051
4250
  type: 'inference:tool_calls_yielded',
6052
4251
  agentName: agent.name,
@@ -6085,51 +4284,7 @@ export class AgentFramework {
6085
4284
 
6086
4285
  agent.enterWaitingForTools(event.calls, stream);
6087
4286
 
6088
- if (!(await persistPrimarySummaryRetryQuarantine())) {
6089
- return;
6090
- }
6091
-
6092
- if (primarySummaryRetry) {
6093
- const phase = `tool_round_${event.context.depth ?? 0}`;
6094
- if (!(await persistPrimarySummaryRetryOutput(phase, assistantBlocks))) {
6095
- return;
6096
- }
6097
- this.pendingAssistantBlocks.delete(agent.name);
6098
- }
6099
-
6100
- executedToolCalls += event.calls.length;
6101
- this.recordEphemeralToolCalls(agent.name, event.calls.length);
6102
-
6103
4287
  for (const call of event.calls) {
6104
- if (primarySummaryRetry) {
6105
- const validation = await this.validatePrimarySummaryRetryMutation(
6106
- agent,
6107
- requestId,
6108
- primarySummaryRetry,
6109
- compiledRequest!,
6110
- artifacts!,
6111
- `before_tool_dispatch_${call.id}`,
6112
- );
6113
- if (!validation.ok) {
6114
- await this.holdPrimarySummaryRetryFamily(
6115
- agent,
6116
- requestId,
6117
- primarySummaryRetry,
6118
- validation.reason,
6119
- validation.data,
6120
- { emitChronicleMarker: false },
6121
- );
6122
- stream.cancel();
6123
- agent.reset();
6124
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
6125
- return;
6126
- }
6127
- await this.recordPrimarySummaryRetryToolDispatch(
6128
- primarySummaryRetry.originalRequestId,
6129
- primarySummaryRetry.intentId,
6130
- call,
6131
- );
6132
- }
6133
4288
  this.dispatchToolCall(agent.name, call);
6134
4289
  }
6135
4290
 
@@ -6222,213 +4377,6 @@ export class AgentFramework {
6222
4377
  }
6223
4378
  }
6224
4379
 
6225
- const lastToolIdx = response.content.reduce(
6226
- (last: number, b: ContentBlock, i: number) =>
6227
- b.type === 'tool_use' || b.type === 'tool_result' ? i : last,
6228
- -1
6229
- );
6230
- const terminalContent = lastToolIdx >= 0
6231
- ? response.content.slice(lastToolIdx + 1)
6232
- : response.content;
6233
- const heldAssistantContent = response.content;
6234
- const visibleAssistantOutput = this.hasVisibleAssistantOutput(response.content);
6235
- const providerInputTokens =
6236
- response.usage?.inputTokens ??
6237
- response.details?.usage?.inputTokens;
6238
-
6239
- if (primarySummaryConfig.enabled && compiledRequest && artifacts && response.stopReason === 'refusal') {
6240
- const stopDetails = (response.raw?.response as {
6241
- stop_details?: { category?: string; explanation?: string };
6242
- } | undefined)?.stop_details;
6243
- const category = stopDetails?.category ?? 'unknown';
6244
- console.error(
6245
- `[inference-refusal] agent=${agent.name} category=${category}` +
6246
- (stopDetails?.explanation ? ` explanation=${stopDetails.explanation}` : ''),
6247
- );
6248
- this.noteRefusal(agent.name, category, response.details?.usage
6249
- ? {
6250
- input: response.details.usage.inputTokens,
6251
- output: response.details.usage.outputTokens,
6252
- }
6253
- : undefined);
6254
-
6255
- const currentRecord = await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6256
- ...record,
6257
- stopReason: response.stopReason,
6258
- providerInputTokens,
6259
- visibleAssistantOutput,
6260
- executedToolCalls,
6261
- finalStatus: 'refusal',
6262
- }));
6263
-
6264
- const holdReasonData = {
6265
- category,
6266
- visibleAssistantOutput,
6267
- executedToolCalls,
6268
- candidateLimit: primarySummaryConfig.maxNewSummaries,
6269
- };
6270
-
6271
- if (primarySummaryRetry) {
6272
- const reason = visibleAssistantOutput
6273
- ? 'primary_summary_fallback_retry_partial_output'
6274
- : executedToolCalls > 0
6275
- ? 'primary_summary_fallback_retry_tool_already_executed'
6276
- : 'primary_summary_fallback_retry_refusal';
6277
- if (visibleAssistantOutput || executedToolCalls > 0) {
6278
- await this.persistPrimarySummaryHeldOutput(agent, requestId, reason, {
6279
- stopReason: response.stopReason,
6280
- providerInputTokens,
6281
- visibleAssistantOutput,
6282
- executedToolCalls,
6283
- assistantBlocks: heldAssistantContent,
6284
- retryDispatch: primarySummaryRetry,
6285
- retryPhase: 'refusal_visible_output',
6286
- holdData: holdReasonData,
6287
- });
6288
- } else {
6289
- await this.holdPrimarySummaryRetryFamily(
6290
- agent,
6291
- requestId,
6292
- primarySummaryRetry,
6293
- reason,
6294
- holdReasonData,
6295
- );
6296
- }
6297
- } else if (visibleAssistantOutput || executedToolCalls > 0 || !currentRecord?.projection) {
6298
- const reason = visibleAssistantOutput
6299
- ? 'primary_summary_refusal_partial_output'
6300
- : executedToolCalls > 0
6301
- ? 'primary_summary_refusal_tool_already_executed'
6302
- : 'primary_summary_refusal_no_projection';
6303
- if (visibleAssistantOutput || executedToolCalls > 0) {
6304
- await this.persistPrimarySummaryHeldOutput(agent, requestId, reason, {
6305
- stopReason: response.stopReason,
6306
- providerInputTokens,
6307
- visibleAssistantOutput,
6308
- executedToolCalls,
6309
- assistantBlocks: heldAssistantContent,
6310
- holdData: holdReasonData,
6311
- });
6312
- } else {
6313
- await this.holdPrimarySummaryRequest(agent, requestId, reason, holdReasonData);
6314
- }
6315
- } else {
6316
- const state = this.readPrimarySummaryFallbackState();
6317
- const baseline = this.latestCompatibleHealthyPrimaryRequest(currentRecord, state);
6318
- if (!baseline?.projection) {
6319
- await this.holdPrimarySummaryRequest(
6320
- agent,
6321
- requestId,
6322
- 'primary_summary_refusal_no_compatible_healthy_baseline',
6323
- holdReasonData,
6324
- );
6325
- } else {
6326
- const baselineKeys = new Set(this.primarySummaryProjectionKeys(baseline.projection));
6327
- const candidates = currentRecord.projection.selectedSummaries
6328
- .filter((selection) => !baselineKeys.has(this.primarySummaryIdentityKey(selection.identity)))
6329
- .map((selection) => selection.identity);
6330
- if (candidates.length === 0) {
6331
- await this.holdPrimarySummaryRequest(
6332
- agent,
6333
- requestId,
6334
- 'primary_summary_refusal_no_newly_admitted_candidates',
6335
- holdReasonData,
6336
- );
6337
- } else if (candidates.length > primarySummaryConfig.maxNewSummaries) {
6338
- await this.holdPrimarySummaryRequest(
6339
- agent,
6340
- requestId,
6341
- 'primary_summary_refusal_too_many_candidates',
6342
- { ...holdReasonData, candidateCount: candidates.length },
6343
- );
6344
- } else {
6345
- try {
6346
- const retry = agent.buildPrimarySummaryRawExpansionRequest(
6347
- artifacts,
6348
- candidates,
6349
- compiledRequest.tools ?? [],
6350
- );
6351
- const retryRequestHash = this.hashJson(retry.request);
6352
- const admittedTokens = this.primarySummaryAdmissionBoundTokens(
6353
- currentRecord.requestInputBoundTokens,
6354
- retry.request,
6355
- providerInputTokens,
6356
- );
6357
- if (admittedTokens > primarySummaryConfig.requestBudgetTokens) {
6358
- await this.holdPrimarySummaryRequest(
6359
- agent,
6360
- requestId,
6361
- 'primary_summary_refusal_retry_over_budget',
6362
- {
6363
- ...holdReasonData,
6364
- admittedTokens,
6365
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
6366
- },
6367
- );
6368
- } else {
6369
- const intentId = `${requestId}:fallback:${Math.random().toString(36).slice(2, 8)}`;
6370
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6371
- ...record,
6372
- fallbackIntent: {
6373
- intentId,
6374
- createdAt: Date.now(),
6375
- baselineRequestId: baseline.requestId,
6376
- candidateSummaries: candidates,
6377
- retryRequestHash,
6378
- retryCompleteBoundTokens: admittedTokens,
6379
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
6380
- },
6381
- fallbackStatus: 'pending',
6382
- }));
6383
- this.pendingRequests.push({
6384
- agentName: agent.name,
6385
- reason: 'primary-summary-fallback-retry',
6386
- source: 'framework',
6387
- timestamp: Date.now(),
6388
- channelId: trigger?.channelId,
6389
- primarySummaryFallbackRetry: {
6390
- originalRequestId: requestId,
6391
- baselineRequestId: baseline.requestId,
6392
- intentId,
6393
- branch: {
6394
- id: currentRecord.branch.id,
6395
- generation: currentRecord.branch.generation,
6396
- },
6397
- candidateSummaries: candidates,
6398
- originalArtifacts: artifacts,
6399
- originalProjectionKeys: this.primarySummaryProjectionKeys(currentRecord.projection),
6400
- expectedRetryRequestHash: retryRequestHash,
6401
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
6402
- originalRequestInputBoundTokens: currentRecord.requestInputBoundTokens,
6403
- originalProviderInputTokens: providerInputTokens,
6404
- },
6405
- });
6406
- }
6407
- } catch (error) {
6408
- await this.holdPrimarySummaryRequest(
6409
- agent,
6410
- requestId,
6411
- 'primary_summary_refusal_retry_build_failed',
6412
- {
6413
- ...holdReasonData,
6414
- error: error instanceof Error ? error.message : String(error),
6415
- },
6416
- );
6417
- }
6418
- }
6419
- }
6420
- }
6421
-
6422
- agent.reset();
6423
- this.eventGate?.onInferenceEnded(agent.name);
6424
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
6425
- continue;
6426
- }
6427
-
6428
- if (!(await persistPrimarySummaryRetryQuarantine())) {
6429
- return;
6430
- }
6431
-
6432
4380
  // Add assistant response to context.
6433
4381
  // If we had tool calls, each round's blocks (thinking + text +
6434
4382
  // tool_use) were already stored as pendingAssistantBlocks and
@@ -6437,52 +4385,20 @@ export class AgentFramework {
6437
4385
  // double-store earlier rounds' text/thinking AND detach signed
6438
4386
  // thinking blocks from their tool_use turn (the API requires
6439
4387
  // thinking to precede tool_use in the same assistant turn).
4388
+ const lastToolIdx = response.content.reduce(
4389
+ (last: number, b: ContentBlock, i: number) =>
4390
+ b.type === 'tool_use' || b.type === 'tool_result' ? i : last,
4391
+ -1
4392
+ );
4393
+ const terminalContent = lastToolIdx >= 0
4394
+ ? response.content.slice(lastToolIdx + 1)
4395
+ : response.content;
6440
4396
  if (lastToolIdx >= 0) {
6441
4397
  if (terminalContent.length > 0) {
6442
- if (primarySummaryRetry) {
6443
- const retryRecord = await this.getPrimarySummaryRequestRecord(requestId);
6444
- const terminalMetadata = retryRecord
6445
- ? this.buildPrimarySummarySettlementMetadata(retryRecord, {
6446
- outcome: 'success',
6447
- stopReason: response.stopReason,
6448
- providerInputTokens,
6449
- visibleAssistantOutput,
6450
- executedToolCalls,
6451
- entryIndex: 0,
6452
- entryCount: 1,
6453
- role: 'assistant',
6454
- kind: 'assistant_output',
6455
- })
6456
- : undefined;
6457
- if (!(await persistPrimarySummaryRetryOutput('terminal', terminalContent, agent.name, terminalMetadata))) {
6458
- return;
6459
- }
6460
- } else {
6461
- agent.addAssistantResponse(terminalContent);
6462
- }
6463
- }
6464
- } else {
6465
- if (primarySummaryRetry) {
6466
- const retryRecord = await this.getPrimarySummaryRequestRecord(requestId);
6467
- const terminalMetadata = retryRecord && terminalContent.length > 0
6468
- ? this.buildPrimarySummarySettlementMetadata(retryRecord, {
6469
- outcome: 'success',
6470
- stopReason: response.stopReason,
6471
- providerInputTokens,
6472
- visibleAssistantOutput,
6473
- executedToolCalls,
6474
- entryIndex: 0,
6475
- entryCount: 1,
6476
- role: 'assistant',
6477
- kind: 'assistant_output',
6478
- })
6479
- : undefined;
6480
- if (!(await persistPrimarySummaryRetryOutput('terminal', terminalContent, agent.name, terminalMetadata))) {
6481
- return;
6482
- }
6483
- } else {
6484
4398
  agent.addAssistantResponse(terminalContent);
6485
4399
  }
4400
+ } else {
4401
+ agent.addAssistantResponse(terminalContent);
6486
4402
  }
6487
4403
 
6488
4404
  // Run afterInference hooks (no-op if no MCPL servers)
@@ -6544,50 +4460,6 @@ export class AgentFramework {
6544
4460
  }
6545
4461
  : undefined;
6546
4462
 
6547
- if (primarySummaryRetry && artifacts) {
6548
- const validation = await this.validatePrimarySummaryRetryMutation(
6549
- agent,
6550
- requestId,
6551
- primarySummaryRetry,
6552
- compiledRequest!,
6553
- artifacts,
6554
- 'before_success_markers',
6555
- );
6556
- if (!validation.ok) {
6557
- await this.holdPrimarySummaryRetryFamily(
6558
- agent,
6559
- requestId,
6560
- primarySummaryRetry,
6561
- validation.reason,
6562
- validation.data,
6563
- { emitChronicleMarker: false },
6564
- );
6565
- return;
6566
- }
6567
- }
6568
-
6569
- if (primarySummaryConfig.enabled) {
6570
- if (primarySummaryRetry) {
6571
- await this.markPrimarySummaryRetrySuccess(
6572
- requestId,
6573
- primarySummaryRetry,
6574
- response.stopReason,
6575
- providerInputTokens,
6576
- visibleAssistantOutput,
6577
- executedToolCalls,
6578
- );
6579
- } else {
6580
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6581
- ...record,
6582
- stopReason: response.stopReason,
6583
- providerInputTokens,
6584
- visibleAssistantOutput,
6585
- executedToolCalls,
6586
- finalStatus: 'success',
6587
- }));
6588
- }
6589
- }
6590
-
6591
4463
  // Reset agent state before emitting inference:completed. Traces are
6592
4464
  // observability-only, but external synchronous listeners should
6593
4465
  // still see the terminal state at the terminal trace boundary.
@@ -6626,10 +4498,8 @@ export class AgentFramework {
6626
4498
  agentName: agent.name,
6627
4499
  requestId,
6628
4500
  success: true,
6629
- request: fallbackRetryRequestForLog,
6630
- response: primarySummaryRetry
6631
- ? this.summarizePrimarySummaryRetryResponse(response)
6632
- : response.raw ?? { note: 'streaming response' },
4501
+ request: compiledRequest ?? { note: 'streaming request' },
4502
+ response: response.raw ?? { note: 'streaming response' },
6633
4503
  durationMs,
6634
4504
  tokenUsage,
6635
4505
  stopReason: response.stopReason,
@@ -6851,30 +4721,13 @@ export class AgentFramework {
6851
4721
  requestId,
6852
4722
  success: false,
6853
4723
  error: err.message,
6854
- request: fallbackRetryRequestForLog,
4724
+ request: compiledRequest ?? { note: 'streaming request failed' },
6855
4725
  durationMs,
6856
4726
  });
6857
4727
 
6858
4728
  // Only reset + retry if this is still the active stream
6859
4729
  if (agent.streamId !== myStreamId) break;
6860
4730
 
6861
- if (primarySummaryRetry) {
6862
- await this.holdPrimarySummaryRetryFamily(
6863
- agent,
6864
- requestId,
6865
- primarySummaryRetry,
6866
- 'primary_summary_fallback_retry_stream_error',
6867
- { error: err.message },
6868
- );
6869
- agent.reset();
6870
- this.eventGate?.onInferenceEnded(agent.name);
6871
- this.settleAgent(agent.name, {
6872
- stopReason: 'completed',
6873
- speech: '',
6874
- });
6875
- return;
6876
- }
6877
-
6878
4731
  agent.reset();
6879
4732
 
6880
4733
  const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
@@ -6914,17 +4767,6 @@ export class AgentFramework {
6914
4767
  // reject an ephemeral's promise mid-run and bump the failure
6915
4768
  // streak). Gate release + stream teardown happen in `finally`.
6916
4769
  if (this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`)) {
6917
- if (primarySummaryRetry) {
6918
- if (!(await this.primarySummaryRetryFamilyAlreadySettled(requestId, primarySummaryRetry))) {
6919
- await this.holdPrimarySummaryRetryFamily(
6920
- agent,
6921
- requestId,
6922
- primarySummaryRetry,
6923
- 'primary_summary_fallback_retry_cancelled_before_success',
6924
- { frameworkCancelled: true },
6925
- );
6926
- }
6927
- }
6928
4770
  this.eventGate?.onInferenceEnded(agent.name);
6929
4771
  return;
6930
4772
  }
@@ -6933,40 +4775,6 @@ export class AgentFramework {
6933
4775
  // may have already started a new stream, bumping streamId)
6934
4776
  if (agent.streamId === myStreamId) {
6935
4777
  const durationMs = Date.now() - startTime;
6936
- if (primarySummaryRetry) {
6937
- await this.holdPrimarySummaryRetryFamily(
6938
- agent,
6939
- requestId,
6940
- primarySummaryRetry,
6941
- `stream_aborted:${reason}`,
6942
- { reason },
6943
- { stopReason: 'abort' },
6944
- );
6945
- agent.reset();
6946
- this.eventGate?.onInferenceEnded(agent.name);
6947
- this.logInference({
6948
- timestamp: startTime,
6949
- agentName: agent.name,
6950
- requestId,
6951
- success: false,
6952
- error: `Stream aborted: ${reason}`,
6953
- request: fallbackRetryRequestForLog,
6954
- durationMs,
6955
- });
6956
- this.settleAgent(agent.name, {
6957
- stopReason: 'completed',
6958
- speech: '',
6959
- });
6960
- return;
6961
- }
6962
- if (primarySummaryConfig.enabled) {
6963
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6964
- ...record,
6965
- stopReason: 'abort',
6966
- finalStatus: 'error',
6967
- fallbackHeldReason: `stream_aborted:${reason}`,
6968
- }));
6969
- }
6970
4778
  agent.reset();
6971
4779
  this.settleAgent(agent.name, {
6972
4780
  stopReason: 'exhausted',
@@ -6990,7 +4798,7 @@ export class AgentFramework {
6990
4798
  requestId,
6991
4799
  success: false,
6992
4800
  error: `Stream aborted: ${reason}`,
6993
- request: fallbackRetryRequestForLog,
4801
+ request: compiledRequest ?? { note: 'streaming request aborted' },
6994
4802
  durationMs,
6995
4803
  });
6996
4804
  this.eventGate?.onInferenceEnded(agent.name);
@@ -7042,63 +4850,12 @@ export class AgentFramework {
7042
4850
  // inference:exhausted so ephemeral agent promises can settle.
7043
4851
  const err = error instanceof Error ? error : new Error(String(error));
7044
4852
  const durationMs = Date.now() - startTime;
7045
- if (primarySummaryRetry) {
7046
- if (await this.primarySummaryRetryAlreadySucceeded(requestId, primarySummaryRetry)) {
7047
- this.logInference({
7048
- timestamp: startTime,
7049
- agentName: agent.name,
7050
- requestId,
7051
- success: true,
7052
- request: fallbackRetryRequestForLog,
7053
- response: { kind: 'primary_summary_fallback_retry', postSuccessError: err.message },
7054
- durationMs,
7055
- });
7056
- agent.reset();
7057
- this.eventGate?.onInferenceEnded(agent.name);
7058
- this.settleAgent(agent.name, {
7059
- stopReason: 'completed',
7060
- speech: '',
7061
- });
7062
- return;
7063
- }
7064
- await this.holdPrimarySummaryRetryFamily(
7065
- agent,
7066
- requestId,
7067
- primarySummaryRetry,
7068
- 'primary_summary_fallback_retry_stream_threw',
7069
- { error: err.message },
7070
- );
7071
- } else if (primarySummaryConfig.enabled) {
7072
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
7073
- ...record,
7074
- finalStatus: 'error',
7075
- fallbackHeldReason: err.message,
7076
- }));
7077
- }
7078
4853
  this.emitTrace({
7079
4854
  type: 'inference:failed',
7080
4855
  agentName: agent.name,
7081
4856
  error: err.message,
7082
4857
  stack: err.stack,
7083
4858
  });
7084
- if (primarySummaryRetry) {
7085
- this.logInference({
7086
- timestamp: startTime,
7087
- agentName: agent.name,
7088
- requestId,
7089
- success: false,
7090
- error: `Stream threw: ${err.message}`,
7091
- request: fallbackRetryRequestForLog,
7092
- durationMs,
7093
- });
7094
- agent.reset();
7095
- this.eventGate?.onInferenceEnded(agent.name);
7096
- this.settleAgent(agent.name, {
7097
- stopReason: 'completed',
7098
- speech: '',
7099
- });
7100
- return;
7101
- }
7102
4859
  this.settleAgent(agent.name, {
7103
4860
  stopReason: 'exhausted',
7104
4861
  speech: '',
@@ -7119,7 +4876,7 @@ export class AgentFramework {
7119
4876
  requestId,
7120
4877
  success: false,
7121
4878
  error: `Stream threw: ${err.message}`,
7122
- request: fallbackRetryRequestForLog,
4879
+ request: compiledRequest ?? { note: 'streaming request threw' },
7123
4880
  durationMs,
7124
4881
  });
7125
4882
  agent.reset();
@@ -7140,7 +4897,6 @@ export class AgentFramework {
7140
4897
  this.channelRegistry?.stopTyping();
7141
4898
  this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
7142
4899
  this.activeStreams.delete(agent.name);
7143
- this.activeStreamContexts.delete(agent.name);
7144
4900
  this.pendingAssistantBlocks.delete(agent.name);
7145
4901
 
7146
4902
  // A conversation fork whose TTL closure turn just finished is done for
@@ -7415,6 +5171,11 @@ export class AgentFramework {
7415
5171
  return;
7416
5172
  }
7417
5173
 
5174
+ if (enrichedCall.name === 'read_image') {
5175
+ this.dispatchReadImageToolCall(agentName, enrichedCall);
5176
+ return;
5177
+ }
5178
+
7418
5179
  // Route gate_status tool
7419
5180
  if (enrichedCall.name === 'gate_status' && this.eventGate) {
7420
5181
  this.dispatchGateToolCall(agentName, enrichedCall);
@@ -7678,23 +5439,12 @@ export class AgentFramework {
7678
5439
  * work, per-agent status + last inference activity. Cheap and read-only.
7679
5440
  */
7680
5441
  healthSnapshot(): Record<string, unknown> {
7681
- const primarySummary = this.readPrimarySummaryFallbackState();
7682
5442
  return {
7683
5443
  at: new Date().toISOString(),
7684
5444
  uptimeSec: Math.round(process.uptime()),
7685
5445
  gate: this.eventGate?.inferenceDiagnostics() ?? null,
7686
5446
  pendingRequests: this.pendingRequests.length,
7687
5447
  activeStreams: [...this.activeStreams.keys()],
7688
- primarySummaryFallback: {
7689
- requests: primarySummary.requests.length,
7690
- pendingDispatches: primarySummary.requests.filter((record) => record.finalStatus === 'pending').length,
7691
- unresolvedIntents: primarySummary.requests.filter((record) => record.fallbackStatus === 'pending').length,
7692
- held: primarySummary.requests.filter((record) =>
7693
- record.finalStatus === 'held' ||
7694
- record.finalStatus === 'timeout_unknown' ||
7695
- record.fallbackStatus === 'held' ||
7696
- record.fallbackStatus === 'timeout_unknown').length,
7697
- },
7698
5448
  agents: [...this.agents.entries()].map(([name, agent]) => ({
7699
5449
  name,
7700
5450
  status: agent.state.status,
@@ -9249,6 +6999,19 @@ export class AgentFramework {
9249
6999
  .then((result) => {
9250
7000
  const durationMs = Date.now() - startTime;
9251
7001
  this.emitTrace({ type: 'tool:completed', module: 'channels', tool: call.name, callId: call.id, durationMs });
7002
+ // A successful channel_open plants the agent's feet in the opened
7003
+ // channel: pin it as this turn's reply locus so plain-text speech
7004
+ // written right after opening routes THERE. Found live 2026-07-21
7005
+ // (Aria bring-up): open → plain-text reply → "no locus (no
7006
+ // home/active channel, defaultPublishChannel null)" on a turn with
7007
+ // no channel trigger (post-restart continuation). Next turn start
7008
+ // re-resolves the locus as usual (startAgentStream set/delete).
7009
+ if (call.name === 'channel_open' && result?.success) {
7010
+ const opened =
7011
+ (result.data as { channelId?: string } | undefined)?.channelId
7012
+ ?? (call.input as { channelId?: string } | undefined)?.channelId;
7013
+ if (opened) this.activeTriggerChannels.set(agentName, opened);
7014
+ }
9252
7015
  this.pushEvent({
9253
7016
  type: 'tool-result',
9254
7017
  callId: call.id,
@@ -9411,6 +7174,75 @@ export class AgentFramework {
9411
7174
  });
9412
7175
  }
9413
7176
 
7177
+ /**
7178
+ * Handle the synthesized `read_image` tool: read image bytes from a
7179
+ * workspace mount and return them as a native MCP-shaped content array
7180
+ * (`[{type:'text'…},{type:'image', data, mimeType}]`). The live-turn
7181
+ * converter (tryNativeToolResultContent → membrane tool_result image
7182
+ * blocks) delivers the actual image to the model; history storage keeps
7183
+ * the standard compact `[image: …]` placeholder.
7184
+ */
7185
+ private dispatchReadImageToolCall(agentName: string, call: ToolCall): void {
7186
+ this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
7187
+ const finish = (result: ToolResult): void => {
7188
+ this.emitTrace({
7189
+ type: result.isError ? 'tool:failed' : 'tool:completed',
7190
+ module: 'workspace',
7191
+ tool: call.name,
7192
+ callId: call.id,
7193
+ durationMs: 0,
7194
+ ...(result.isError ? { error: result.error } : {}),
7195
+ });
7196
+ this.pushEvent({ type: 'tool-result', callId: call.id, agentName, moduleName: 'workspace', result });
7197
+ };
7198
+ void (async (): Promise<void> => {
7199
+ try {
7200
+ const workspace = this.getWorkspaceModule();
7201
+ if (!workspace || typeof workspace.readBinary !== 'function') {
7202
+ throw new Error('read_image requires a workspace module');
7203
+ }
7204
+ const input = (call.input ?? {}) as { path?: unknown };
7205
+ if (typeof input.path !== 'string' || input.path.length === 0) {
7206
+ throw new Error('read_image: `path` (mount-prefixed) is required');
7207
+ }
7208
+ const read = await workspace.readBinary(input.path);
7209
+ if ('error' in read) throw new Error(read.error);
7210
+ const mediaType = sniffImageMediaType(read.data);
7211
+ if (!mediaType) {
7212
+ throw new Error(
7213
+ `read_image: "${input.path}" does not look like a supported image ` +
7214
+ '(png/jpeg/gif/webp — checked by magic bytes, not extension)',
7215
+ );
7216
+ }
7217
+ // Anthropic rejects images >5MB (and >8000px); guard the hard byte
7218
+ // limit here — resizing is out of scope without an image library.
7219
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
7220
+ if (read.data.byteLength > MAX_IMAGE_BYTES) {
7221
+ throw new Error(
7222
+ `read_image: "${input.path}" is ${read.data.byteLength} bytes — ` +
7223
+ `over the ${MAX_IMAGE_BYTES}-byte model limit`,
7224
+ );
7225
+ }
7226
+ finish({
7227
+ success: true,
7228
+ data: [
7229
+ {
7230
+ type: 'text',
7231
+ text: `${input.path} (${mediaType}, ${Math.ceil(read.data.byteLength / 1024)} KB):`,
7232
+ },
7233
+ { type: 'image', data: read.data.toString('base64'), mimeType: mediaType },
7234
+ ],
7235
+ });
7236
+ } catch (error) {
7237
+ finish({
7238
+ success: false,
7239
+ error: error instanceof Error ? error.message : String(error),
7240
+ isError: true,
7241
+ });
7242
+ }
7243
+ })();
7244
+ }
7245
+
9414
7246
  /**
9415
7247
  * Handle the synthesized `save_recent_image` tool: locate the requested
9416
7248
  * image blocks in the calling agent's context (blobs re-inlined, counted