@animalabs/agent-framework 0.6.6 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/src/agent.d.ts +39 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +172 -56
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server.js +0 -0
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/canonical-json.d.ts +2 -0
- package/dist/src/canonical-json.d.ts.map +1 -0
- package/dist/src/canonical-json.js +23 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/framework.d.ts +147 -3
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +2719 -125
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +9 -2
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +3 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +29 -15
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +36 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +142 -28
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +12 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +661 -2
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +4 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +99 -0
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -0
- package/dist/src/recovery/discord-awareness-outbox.js +334 -0
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +45 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -0
- package/dist/src/recovery/offline-branch.js +362 -0
- package/dist/src/recovery/offline-branch.js.map +1 -0
- package/dist/src/recovery/recover-cli.d.ts +3 -0
- package/dist/src/recovery/recover-cli.d.ts.map +1 -0
- package/dist/src/recovery/recover-cli.js +129 -0
- package/dist/src/recovery/recover-cli.js.map +1 -0
- package/dist/src/types/agent.d.ts +22 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +31 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.js +200 -93
- package/dist/test/agent-runtime-settings.test.js.map +1 -1
- package/dist/test/busy-agent-scheduler.test.d.ts +2 -0
- package/dist/test/busy-agent-scheduler.test.d.ts.map +1 -0
- package/dist/test/busy-agent-scheduler.test.js +62 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts +2 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.js +47 -0
- package/dist/test/channel-lifecycle-provenance.test.js.map +1 -0
- package/dist/test/discord-awareness-outbox.test.d.ts +2 -0
- package/dist/test/discord-awareness-outbox.test.d.ts.map +1 -0
- package/dist/test/discord-awareness-outbox.test.js +248 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -0
- package/dist/test/helpers/mock-membrane.js +1 -1
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/maintenance-timer.test.js +100 -0
- package/dist/test/maintenance-timer.test.js.map +1 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +2 -0
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +845 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js +22 -3
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +15 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/offline-recovery-branch.test.d.ts +2 -0
- package/dist/test/offline-recovery-branch.test.d.ts.map +1 -0
- package/dist/test/offline-recovery-branch.test.js +226 -0
- package/dist/test/offline-recovery-branch.test.js.map +1 -0
- package/dist/test/present-while-acting.test.d.ts +3 -2
- package/dist/test/present-while-acting.test.d.ts.map +1 -1
- package/dist/test/present-while-acting.test.js +31 -3
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js +1955 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.js +753 -0
- package/dist/test/primary-summary-refusal-fallback.test.js.map +1 -0
- package/dist/test/same-round-think-policy.test.d.ts +2 -0
- package/dist/test/same-round-think-policy.test.d.ts.map +1 -0
- package/dist/test/same-round-think-policy.test.js +853 -0
- package/dist/test/same-round-think-policy.test.js.map +1 -0
- package/dist/test/workspace-read-image.test.d.ts +2 -0
- package/dist/test/workspace-read-image.test.d.ts.map +1 -0
- package/dist/test/workspace-read-image.test.js +518 -0
- package/dist/test/workspace-read-image.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -3
- package/src/agent.ts +265 -62
- package/src/api/server.ts +8 -2
- package/src/canonical-json.ts +22 -0
- package/src/framework.ts +3561 -145
- package/src/gate/event-gate.ts +15 -3
- package/src/index.ts +20 -0
- package/src/mcpl/channel-registry.ts +31 -16
- package/src/mcpl/server-connection.ts +163 -28
- package/src/modules/workspace/index.ts +759 -2
- package/src/modules/workspace/types.ts +5 -0
- package/src/recovery/discord-awareness-outbox.ts +459 -0
- package/src/recovery/offline-branch.ts +510 -0
- package/src/recovery/recover-cli.ts +130 -0
- package/src/types/agent.ts +26 -0
- package/src/types/framework.ts +17 -0
- package/src/types/module.ts +33 -0
- package/src/types/trace.ts +2 -0
package/src/framework.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { JsStore } from '@animalabs/chronicle';
|
|
4
|
-
import type { Membrane, ContentBlock, NormalizedRequest, YieldingStream, ToolResult as MembraneToolResult, ToolResultContentBlock } from '@animalabs/membrane';
|
|
5
|
+
import type { Membrane, ContentBlock, NormalizedRequest, NormalizedResponse, YieldingStream, ToolResult as MembraneToolResult, ToolResultContentBlock } from '@animalabs/membrane';
|
|
5
6
|
import { MembraneError } from '@animalabs/membrane';
|
|
6
7
|
import { ContextManager, PassthroughStrategy } from '@animalabs/context-manager';
|
|
7
8
|
import type {
|
|
9
|
+
BranchGenerationInfo,
|
|
8
10
|
MessageId,
|
|
9
11
|
MessageMetadata,
|
|
10
12
|
MessageQuery,
|
|
11
13
|
MessageQueryResult,
|
|
14
|
+
PrimarySummaryContract,
|
|
15
|
+
PrimarySummaryIdentity,
|
|
16
|
+
PrimarySummaryProjection,
|
|
12
17
|
StoredMessage,
|
|
13
18
|
} from '@animalabs/context-manager';
|
|
14
19
|
import type {
|
|
@@ -47,9 +52,11 @@ import type {
|
|
|
47
52
|
AgentRuntimeSettingsPatch,
|
|
48
53
|
AgentRuntimeSettingsOverrides,
|
|
49
54
|
AgentRuntimeSettingsSnapshot,
|
|
55
|
+
AgentSettingsExtension,
|
|
56
|
+
SameRoundThinkTextPolicy,
|
|
50
57
|
} from './types/index.js';
|
|
51
58
|
import { ProcessQueueImpl } from './queue.js';
|
|
52
|
-
import { Agent } from './agent.js';
|
|
59
|
+
import { Agent, type ActivationCompileArtifacts } from './agent.js';
|
|
53
60
|
import { ModuleRegistry, isStateExistsError } from './module-registry.js';
|
|
54
61
|
import { McplServerRegistry } from './mcpl/server-registry.js';
|
|
55
62
|
import { FeatureSetManager } from './mcpl/feature-set-manager.js';
|
|
@@ -60,8 +67,10 @@ import { InferenceRouter } from './mcpl/inference-router.js';
|
|
|
60
67
|
import { ChannelRegistry } from './mcpl/channel-registry.js';
|
|
61
68
|
import { ConversationRouter } from './mcpl/conversation-router.js';
|
|
62
69
|
import { safeSlice } from './safe-slice.js';
|
|
70
|
+
import type { WorkspaceModule } from './modules/workspace/index.js';
|
|
63
71
|
import { toolResultDataToHistoryString } from './tool-result-history.js';
|
|
64
72
|
import { splitProseSegments } from './prose-segments.js';
|
|
73
|
+
import { canonicalJsonStringify } from './canonical-json.js';
|
|
65
74
|
|
|
66
75
|
/**
|
|
67
76
|
* Tools whose presence suppresses auto-routing of the surrounding prose,
|
|
@@ -71,15 +80,14 @@ import { splitProseSegments } from './prose-segments.js';
|
|
|
71
80
|
* - explicit delivery tools (channel_publish / *send_message /
|
|
72
81
|
* *reply_message / *send_dm) — already sent the message, so routing the
|
|
73
82
|
* prose again would double-post.
|
|
74
|
-
* `think` is deliberately NOT here: it is silent *reasoning*,
|
|
75
|
-
*
|
|
83
|
+
* `think` is deliberately NOT here: it is silent *reasoning*, and same-round
|
|
84
|
+
* prose beside it is governed separately by same_round_think_text_policy.
|
|
76
85
|
*
|
|
77
|
-
* Scope:
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* double-post guard.
|
|
86
|
+
* Scope: an explicit delivery suppresses prose from that round onward until
|
|
87
|
+
* another external message is injected. The suppression prevents a
|
|
88
|
+
* `send_message` followed by "sent it" from double-posting, but a new message
|
|
89
|
+
* starts a new conversational round and must be answerable with plain prose.
|
|
90
|
+
* `skip_reply` ends the turn at the tool-result boundary.
|
|
83
91
|
*/
|
|
84
92
|
const SILENCING_TOOLS = new Set([
|
|
85
93
|
'skip_reply', 'channel_publish', 'send_message', 'reply_message', 'send_dm',
|
|
@@ -108,16 +116,31 @@ import type {
|
|
|
108
116
|
} from './mcpl/types.js';
|
|
109
117
|
import type { ContextInjection } from '@animalabs/context-manager';
|
|
110
118
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
119
|
+
import {
|
|
120
|
+
DEFAULT_DISCORD_AWARENESS_EMOJI,
|
|
121
|
+
DiscordAwarenessOutbox,
|
|
122
|
+
defaultDiscordAwarenessOutboxPath,
|
|
123
|
+
extractDiscordAwarenessRefs,
|
|
124
|
+
} from './recovery/discord-awareness-outbox.js';
|
|
111
125
|
|
|
112
126
|
const FRAMEWORK_STATE_ID = 'framework/state';
|
|
113
127
|
const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
|
|
114
128
|
const INFERENCE_LOG_ID = 'framework/inference-log';
|
|
115
129
|
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;
|
|
116
136
|
const TURN_CHECKPOINTS_ID = 'framework/turn-checkpoints'; // legacy single-map layout, read-only fallback
|
|
117
137
|
const TURN_CHECKPOINTS_TREE_ID = 'framework/turn-checkpoints/tree';
|
|
118
138
|
|
|
119
139
|
/** Maximum number of turn checkpoints to keep per agent. */
|
|
120
140
|
const MAX_TURN_CHECKPOINTS = 20;
|
|
141
|
+
const DEFAULT_DISCORD_AWARENESS_DEADLINE_MS = 10_000;
|
|
142
|
+
const MIN_DISCORD_AWARENESS_DEADLINE_MS = 50;
|
|
143
|
+
const MAX_DISCORD_AWARENESS_DEADLINE_MS = 60_000;
|
|
121
144
|
|
|
122
145
|
interface TurnCheckpoint {
|
|
123
146
|
agentName: string;
|
|
@@ -127,11 +150,165 @@ interface TurnCheckpoint {
|
|
|
127
150
|
timestamp: number;
|
|
128
151
|
}
|
|
129
152
|
|
|
153
|
+
type DiscordAwarenessDrainOutcome =
|
|
154
|
+
| { status: 'delivered'; delivered: number; failed: number }
|
|
155
|
+
| { status: 'unavailable'; accounted: number };
|
|
156
|
+
|
|
157
|
+
interface DiscordAwarenessBarrier {
|
|
158
|
+
generation: number;
|
|
159
|
+
requiresBarrier: boolean;
|
|
160
|
+
promise: Promise<DiscordAwarenessDrainOutcome>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeDiscordAwarenessDeadline(value: number | undefined): number {
|
|
164
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
165
|
+
return DEFAULT_DISCORD_AWARENESS_DEADLINE_MS;
|
|
166
|
+
}
|
|
167
|
+
return Math.max(
|
|
168
|
+
MIN_DISCORD_AWARENESS_DEADLINE_MS,
|
|
169
|
+
Math.min(MAX_DISCORD_AWARENESS_DEADLINE_MS, Math.floor(value)),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
class DiscordAwarenessAccountingError extends Error {
|
|
174
|
+
constructor(operation: string, cause: unknown) {
|
|
175
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
176
|
+
super(`Discord awareness accounting failed during ${operation}: ${detail}`, { cause });
|
|
177
|
+
this.name = 'DiscordAwarenessAccountingError';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
130
181
|
interface RedoEntry {
|
|
131
182
|
branchName: string;
|
|
132
183
|
checkpoint: TurnCheckpoint;
|
|
133
184
|
}
|
|
134
185
|
|
|
186
|
+
interface InferenceToolSnapshot {
|
|
187
|
+
sameRoundThinkTextPolicy: SameRoundThinkTextPolicy;
|
|
188
|
+
}
|
|
189
|
+
|
|
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
|
+
|
|
135
312
|
/**
|
|
136
313
|
* Default inference policy - infer if any request exists for the agent.
|
|
137
314
|
*/
|
|
@@ -145,6 +322,11 @@ class DefaultInferencePolicy implements InferencePolicy {
|
|
|
145
322
|
}
|
|
146
323
|
}
|
|
147
324
|
|
|
325
|
+
function isPermanentDiscordReactionFailure(message: string): boolean {
|
|
326
|
+
return /unknown message|unknown channel|missing access|missing permissions|missing permission|cannot access|channel .* not found|message .* not found/i
|
|
327
|
+
.test(message);
|
|
328
|
+
}
|
|
329
|
+
|
|
148
330
|
/**
|
|
149
331
|
* Default error policy - retry with exponential backoff.
|
|
150
332
|
*
|
|
@@ -274,6 +456,13 @@ interface EphemeralRun {
|
|
|
274
456
|
toolCallsCount: number;
|
|
275
457
|
}
|
|
276
458
|
|
|
459
|
+
interface ActiveStreamContext {
|
|
460
|
+
requestId: string;
|
|
461
|
+
trigger?: PendingInferenceRequest;
|
|
462
|
+
compiledRequest?: NormalizedRequest;
|
|
463
|
+
artifacts?: ActivationCompileArtifacts;
|
|
464
|
+
}
|
|
465
|
+
|
|
277
466
|
/** Cap an API error message for inline use in a rewind marker: keep enough to
|
|
278
467
|
* identify the failure class without pasting a wall of provider JSON into the
|
|
279
468
|
* agent's context. */
|
|
@@ -290,7 +479,7 @@ export class AgentFramework {
|
|
|
290
479
|
private moduleRegistry: ModuleRegistry;
|
|
291
480
|
private inferencePolicy: InferencePolicy;
|
|
292
481
|
private errorPolicy: ErrorPolicy;
|
|
293
|
-
private pendingRequests:
|
|
482
|
+
private pendingRequests: PendingInferenceRequest[] = [];
|
|
294
483
|
/**
|
|
295
484
|
* Per-agent channel that triggered the agent's CURRENT inference turn, if any
|
|
296
485
|
* (item-3 redux). Read by the ChannelRegistry's `activeChannelResolver` to
|
|
@@ -312,20 +501,30 @@ export class AgentFramework {
|
|
|
312
501
|
private maintenanceRunId = 0;
|
|
313
502
|
private currentMaintenanceRun: ContextMaintenanceRun | null = null;
|
|
314
503
|
private maintenanceHistory: ContextMaintenanceRun[] = [];
|
|
315
|
-
/** Last time we
|
|
504
|
+
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
316
505
|
private staleWarnAt = new Map<string, number>();
|
|
317
506
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
318
507
|
private lastInferenceAt = new Map<string, { startedAt?: number; endedAt?: number; failedAt?: number; lastError?: string }>();
|
|
319
508
|
private processLoggingPersist: boolean;
|
|
320
509
|
private processLoggingBroadcast: boolean;
|
|
321
510
|
private activeStreams: Map<string, Promise<void>> = new Map();
|
|
511
|
+
private activeStreamContexts: Map<string, ActiveStreamContext> = new Map();
|
|
512
|
+
private primarySummaryStateLock: Promise<void> = Promise.resolve();
|
|
322
513
|
|
|
323
514
|
/** Per-agent output locus pinned for the CURRENT logical turn (see
|
|
324
515
|
* resolveTurnLocus in driveStream). Lives here — not in driveStream
|
|
325
516
|
* locals — so the pin survives a context-budget stream restart, which
|
|
326
|
-
* continues the same logical turn in a fresh driveStream.
|
|
327
|
-
*
|
|
517
|
+
* continues the same logical turn in a fresh driveStream. A mid-turn
|
|
518
|
+
* injected channel message replaces the pin for the next conversational
|
|
519
|
+
* round; otherwise it remains stable. Cleared at the start of every
|
|
520
|
+
* non-restart turn. */
|
|
328
521
|
private turnLocusPins: Map<string, string> = new Map();
|
|
522
|
+
/** A tool boundary injected fresh external input into the live stream.
|
|
523
|
+
* Presence (including a null value) tells driveStream to clear sticky
|
|
524
|
+
* explicit-send suppression before handling the next model round. A
|
|
525
|
+
* string also moves that round's reply locus to the newest injected
|
|
526
|
+
* channel. */
|
|
527
|
+
private midTurnRoutingResets: Map<string, string | null> = new Map();
|
|
329
528
|
private pendingAssistantBlocks: Map<string, ContentBlock[]> = new Map();
|
|
330
529
|
/** Streams the FRAMEWORK cancelled for non-terminal reasons, keyed
|
|
331
530
|
* `${agentName}:${streamId}`: an endTurn tool result or a context-budget
|
|
@@ -439,6 +638,15 @@ export class AgentFramework {
|
|
|
439
638
|
* subsystem can be lazily initialized by connectMcplServer when the
|
|
440
639
|
* framework started with zero configured servers. */
|
|
441
640
|
private mcplInferenceRoutingConfig: import('./mcpl/types.js').InferenceRoutingPolicy | null = null;
|
|
641
|
+
/** Durable, non-Chronicle projection queue for messages removed by a branch. */
|
|
642
|
+
private discordAwarenessOutbox: DiscordAwarenessOutbox | null = null;
|
|
643
|
+
private discordAwarenessEmoji = DEFAULT_DISCORD_AWARENESS_EMOJI;
|
|
644
|
+
private discordAwarenessDeadlineMs = DEFAULT_DISCORD_AWARENESS_DEADLINE_MS;
|
|
645
|
+
/** Serialize per-server drains so reconnect and an online undo cannot race. */
|
|
646
|
+
private discordAwarenessDrains: Map<string, Promise<DiscordAwarenessDrainOutcome>> = new Map();
|
|
647
|
+
/** Framework-global inference gate; older generations cannot release it. */
|
|
648
|
+
private discordAwarenessBarrier: DiscordAwarenessBarrier | null = null;
|
|
649
|
+
private discordAwarenessBarrierGeneration = 0;
|
|
442
650
|
|
|
443
651
|
// EventGate (null when FrameworkConfig.gate is omitted)
|
|
444
652
|
private eventGate: EventGate | null = null;
|
|
@@ -459,6 +667,9 @@ export class AgentFramework {
|
|
|
459
667
|
processLoggingPersist: boolean,
|
|
460
668
|
processLoggingBroadcast: boolean,
|
|
461
669
|
timeZone: string,
|
|
670
|
+
discordAwarenessOutbox: DiscordAwarenessOutbox | null,
|
|
671
|
+
discordAwarenessEmoji: string,
|
|
672
|
+
discordAwarenessDeadlineMs: number,
|
|
462
673
|
) {
|
|
463
674
|
this.store = store;
|
|
464
675
|
this.ownsStore = ownsStore;
|
|
@@ -470,6 +681,9 @@ export class AgentFramework {
|
|
|
470
681
|
this.processLoggingPersist = processLoggingPersist;
|
|
471
682
|
this.processLoggingBroadcast = processLoggingBroadcast;
|
|
472
683
|
this.timeZone = timeZone;
|
|
684
|
+
this.discordAwarenessOutbox = discordAwarenessOutbox;
|
|
685
|
+
this.discordAwarenessEmoji = discordAwarenessEmoji;
|
|
686
|
+
this.discordAwarenessDeadlineMs = discordAwarenessDeadlineMs;
|
|
473
687
|
this.queue = new ProcessQueueImpl();
|
|
474
688
|
this.usageTracker = new UsageTracker({
|
|
475
689
|
emitTrace: (e: UsageUpdatedEvent) => this.emitTrace({ ...e }),
|
|
@@ -551,6 +765,18 @@ export class AgentFramework {
|
|
|
551
765
|
}
|
|
552
766
|
}
|
|
553
767
|
|
|
768
|
+
try {
|
|
769
|
+
store.registerState({ id: PRIMARY_SUMMARY_FALLBACK_STATE_ID, strategy: 'snapshot' });
|
|
770
|
+
} catch {
|
|
771
|
+
// Already registered
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const discordAwarenessOutboxPath = config.discordAwarenessOutboxPath
|
|
775
|
+
?? (config.storePath ? defaultDiscordAwarenessOutboxPath(config.storePath) : undefined);
|
|
776
|
+
const discordAwarenessOutbox = discordAwarenessOutboxPath
|
|
777
|
+
? new DiscordAwarenessOutbox(discordAwarenessOutboxPath)
|
|
778
|
+
: null;
|
|
779
|
+
|
|
554
780
|
const framework = new AgentFramework(
|
|
555
781
|
store,
|
|
556
782
|
ownsStore,
|
|
@@ -562,16 +788,46 @@ export class AgentFramework {
|
|
|
562
788
|
processLoggingPersist,
|
|
563
789
|
processLoggingBroadcast,
|
|
564
790
|
resolveTimeZone(config.timeZone),
|
|
791
|
+
discordAwarenessOutbox,
|
|
792
|
+
config.discordAwarenessEmoji ?? DEFAULT_DISCORD_AWARENESS_EMOJI,
|
|
793
|
+
normalizeDiscordAwarenessDeadline(config.discordAwarenessDeadlineMs),
|
|
565
794
|
);
|
|
566
795
|
|
|
567
|
-
//
|
|
568
|
-
|
|
796
|
+
// If an offline recovery process crashed after switching Chronicle but
|
|
797
|
+
// before committing its prepared marker batch, the active branch is the
|
|
798
|
+
// commit record. Promote it now; no quarantined content is read.
|
|
799
|
+
if (discordAwarenessOutbox) {
|
|
800
|
+
try {
|
|
801
|
+
const activated = discordAwarenessOutbox.activatePreparedForBranch(
|
|
802
|
+
store.currentBranch().name,
|
|
803
|
+
store.listBranches(),
|
|
804
|
+
);
|
|
805
|
+
if (activated > 0) {
|
|
806
|
+
console.error(
|
|
807
|
+
`[discord-awareness] recovered ${activated} prepared batch(es) for active branch ${store.currentBranch().name}`,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
} catch (error) {
|
|
811
|
+
// The branch may be safe, but reporting the framework ready while its
|
|
812
|
+
// durable awareness projection is unreadable creates a half-ready
|
|
813
|
+
// host whose data plane can never be released safely.
|
|
814
|
+
throw new DiscordAwarenessAccountingError('startup reconciliation', error);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
569
817
|
|
|
570
818
|
// Create agents
|
|
571
819
|
for (const agentConfig of config.agents) {
|
|
572
820
|
await framework.createAgent(agentConfig);
|
|
573
821
|
}
|
|
574
822
|
|
|
823
|
+
// Restore persisted usage data (if any) from prior session
|
|
824
|
+
framework.restoreUsageState();
|
|
825
|
+
await framework.reconcilePrimarySummaryFallbackStateOnStartup();
|
|
826
|
+
|
|
827
|
+
// Finish any branch-local suppression interrupted after Chronicle switched
|
|
828
|
+
// branches. This runs before modules, MCPL connections, or inbound traffic.
|
|
829
|
+
await framework.resumePreparedDiscordSuppressions();
|
|
830
|
+
|
|
575
831
|
// Add modules
|
|
576
832
|
for (const module of config.modules) {
|
|
577
833
|
await framework.addModule(module);
|
|
@@ -821,11 +1077,10 @@ export class AgentFramework {
|
|
|
821
1077
|
}
|
|
822
1078
|
|
|
823
1079
|
private async runQueuedMaintenance(): Promise<void> {
|
|
824
|
-
const allTools = this.getAllTools();
|
|
825
1080
|
const queued = [...this.agents.values()].flatMap((agent) => {
|
|
826
1081
|
const cm = agent.getContextManager();
|
|
827
|
-
const tools =
|
|
828
|
-
cm.setToolDefinitions(tools);
|
|
1082
|
+
const tools = this.getToolsForAgent(agent.name).filter((tool) => agent.canUseTool(tool.name));
|
|
1083
|
+
cm.setToolDefinitions?.(tools);
|
|
829
1084
|
if (cm.isReady()) return [];
|
|
830
1085
|
const pending = cm.getPendingWork()?.description;
|
|
831
1086
|
const progress = this.contextProgress(cm);
|
|
@@ -1028,10 +1283,138 @@ export class AgentFramework {
|
|
|
1028
1283
|
...this.mcplTools,
|
|
1029
1284
|
...channelTools,
|
|
1030
1285
|
...gateTools,
|
|
1031
|
-
|
|
1286
|
+
this.buildAgentSettingsTool(),
|
|
1287
|
+
...(this.getWorkspaceModule() ? [AgentFramework.SAVE_IMAGE_TOOL] : []),
|
|
1032
1288
|
];
|
|
1033
1289
|
}
|
|
1034
1290
|
|
|
1291
|
+
/** The registered workspace module, if any (used by save_image). */
|
|
1292
|
+
private getWorkspaceModule(): WorkspaceModule | undefined {
|
|
1293
|
+
const module = this.moduleRegistry.getAllModules().find((m) => m.name === 'workspace');
|
|
1294
|
+
return module && typeof (module as WorkspaceModule).writeBinary === 'function'
|
|
1295
|
+
? (module as WorkspaceModule)
|
|
1296
|
+
: undefined;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/** Core agent_settings keys — extension keys must not collide with these. */
|
|
1300
|
+
private static readonly AGENT_SETTINGS_CORE_KEYS = [
|
|
1301
|
+
'context_budget_tokens',
|
|
1302
|
+
'tail_tokens',
|
|
1303
|
+
'transition_pace_tokens',
|
|
1304
|
+
'same_round_think_text_policy',
|
|
1305
|
+
];
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Collect module-declared agent_settings extensions (Module.getAgentSettingsExtension),
|
|
1309
|
+
* keyed by owning module name. Extensions whose keys collide with the core
|
|
1310
|
+
* settings or an earlier extension are skipped loudly — silent shadowing
|
|
1311
|
+
* would make updates route to the wrong owner.
|
|
1312
|
+
*/
|
|
1313
|
+
private collectAgentSettingsExtensions(): Map<string, AgentSettingsExtension> {
|
|
1314
|
+
const result = new Map<string, AgentSettingsExtension>();
|
|
1315
|
+
const taken = new Set<string>(AgentFramework.AGENT_SETTINGS_CORE_KEYS);
|
|
1316
|
+
for (const module of this.moduleRegistry.getAllModules()) {
|
|
1317
|
+
const ext = module.getAgentSettingsExtension?.();
|
|
1318
|
+
if (!ext) continue;
|
|
1319
|
+
const collision = ext.keys.find((k) => taken.has(k));
|
|
1320
|
+
if (collision) {
|
|
1321
|
+
console.error(
|
|
1322
|
+
`[agent-settings] extension from module '${module.name}' skipped: key '${collision}' already taken`,
|
|
1323
|
+
);
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
ext.keys.forEach((k) => taken.add(k));
|
|
1327
|
+
result.set(module.name, ext);
|
|
1328
|
+
}
|
|
1329
|
+
return result;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
private captureInferenceToolSnapshot(agent: Agent): InferenceToolSnapshot {
|
|
1333
|
+
return {
|
|
1334
|
+
sameRoundThinkTextPolicy: agent.getEffectiveSameRoundThinkTextPolicy(),
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
private buildThinkTool(
|
|
1339
|
+
policy: SameRoundThinkTextPolicy,
|
|
1340
|
+
): import('./types/index.js').ToolDefinition {
|
|
1341
|
+
return {
|
|
1342
|
+
name: 'think',
|
|
1343
|
+
description:
|
|
1344
|
+
'Reason privately. The think({content}) argument stays in your own context and is NOT sent ' +
|
|
1345
|
+
'to channels or other surfaces. ' +
|
|
1346
|
+
(policy === 'private'
|
|
1347
|
+
? 'Your current same_round_think_text_policy is private, so ordinary text emitted in the SAME native assistant round as think() is withheld from channel routing. Later rounds without think() route normally.'
|
|
1348
|
+
: 'Your current same_round_think_text_policy is public, so ordinary text emitted in the SAME native assistant round as think() may still be routed publicly as your speech. Later rounds without think() route normally.'
|
|
1349
|
+
) +
|
|
1350
|
+
' Use think() to work things out privately. To inspect or change this policy, use agent_settings get/update on same_round_think_text_policy. To deliberately not reply this turn, call skip_reply instead.',
|
|
1351
|
+
inputSchema: {
|
|
1352
|
+
type: 'object' as const,
|
|
1353
|
+
properties: {
|
|
1354
|
+
content: {
|
|
1355
|
+
type: 'string',
|
|
1356
|
+
description: 'Your private thought / reasoning (optional; not sent anywhere).',
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
required: [],
|
|
1360
|
+
},
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** agent_settings tool definition with module extension fields merged in. */
|
|
1365
|
+
private buildAgentSettingsTool(): import('./types/index.js').ToolDefinition {
|
|
1366
|
+
const base = AgentFramework.AGENT_SETTINGS_TOOL;
|
|
1367
|
+
const extensions = this.collectAgentSettingsExtensions();
|
|
1368
|
+
if (extensions.size === 0) return base;
|
|
1369
|
+
const baseSchema = base.inputSchema as {
|
|
1370
|
+
type: string;
|
|
1371
|
+
properties: Record<string, unknown>;
|
|
1372
|
+
required?: string[];
|
|
1373
|
+
};
|
|
1374
|
+
const extProperties: Record<string, unknown> = {};
|
|
1375
|
+
const extKeys: string[] = [];
|
|
1376
|
+
for (const ext of extensions.values()) {
|
|
1377
|
+
Object.assign(extProperties, ext.properties);
|
|
1378
|
+
extKeys.push(...ext.keys);
|
|
1379
|
+
}
|
|
1380
|
+
const settingsProp = baseSchema.properties.settings as { items?: { enum?: string[] } };
|
|
1381
|
+
return {
|
|
1382
|
+
...base,
|
|
1383
|
+
description:
|
|
1384
|
+
base.description +
|
|
1385
|
+
` Additional host-managed settings also live here: ${extKeys.join(', ')}.`,
|
|
1386
|
+
inputSchema: {
|
|
1387
|
+
...baseSchema,
|
|
1388
|
+
properties: {
|
|
1389
|
+
...baseSchema.properties,
|
|
1390
|
+
...extProperties,
|
|
1391
|
+
settings: {
|
|
1392
|
+
...(baseSchema.properties.settings as Record<string, unknown>),
|
|
1393
|
+
items: {
|
|
1394
|
+
...(settingsProp.items as Record<string, unknown>),
|
|
1395
|
+
enum: [...(settingsProp.items?.enum ?? []), ...extKeys],
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
},
|
|
1399
|
+
} as unknown as import('./types/index.js').ToolDefinition['inputSchema'],
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
private getToolsForAgent(
|
|
1404
|
+
agentName: string,
|
|
1405
|
+
snapshot?: InferenceToolSnapshot,
|
|
1406
|
+
): import('./types/index.js').ToolDefinition[] {
|
|
1407
|
+
return this.getAllTools().map((tool) => {
|
|
1408
|
+
if (tool.name === 'think') {
|
|
1409
|
+
return this.buildThinkTool(
|
|
1410
|
+
snapshot?.sameRoundThinkTextPolicy
|
|
1411
|
+
?? this.getAgentRuntimeSettings(agentName).sameRoundThinkTextPolicy,
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
return tool;
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1035
1418
|
getAgentRuntimeSettings(agentName: string): AgentRuntimeSettingsSnapshot {
|
|
1036
1419
|
const agent = this.agents.get(agentName);
|
|
1037
1420
|
if (!agent) throw new Error(`Unknown agent: ${agentName}`);
|
|
@@ -1124,7 +1507,7 @@ export class AgentFramework {
|
|
|
1124
1507
|
throw new Error(`Agent not found: ${agentName}`);
|
|
1125
1508
|
}
|
|
1126
1509
|
|
|
1127
|
-
const tools = this.
|
|
1510
|
+
const tools = this.getToolsForAgent(agentName).filter((t) => agent.canUseTool(t.name));
|
|
1128
1511
|
|
|
1129
1512
|
// Default: no dynamic injection gathering → fully transparent (no
|
|
1130
1513
|
// inference, no Chronicle writes, no external RPC). Opt in explicitly.
|
|
@@ -1279,6 +1662,22 @@ export class AgentFramework {
|
|
|
1279
1662
|
}
|
|
1280
1663
|
}
|
|
1281
1664
|
|
|
1665
|
+
private validatePersistedAgentRuntimeSettings(
|
|
1666
|
+
agentName: string,
|
|
1667
|
+
overrides: AgentRuntimeSettingsOverrides,
|
|
1668
|
+
): AgentRuntimeSettingsOverrides {
|
|
1669
|
+
if (
|
|
1670
|
+
overrides.sameRoundThinkTextPolicy !== undefined &&
|
|
1671
|
+
overrides.sameRoundThinkTextPolicy !== 'public' &&
|
|
1672
|
+
overrides.sameRoundThinkTextPolicy !== 'private'
|
|
1673
|
+
) {
|
|
1674
|
+
throw new Error(
|
|
1675
|
+
`Invalid persisted sameRoundThinkTextPolicy for agent "${agentName}": ${JSON.stringify(overrides.sameRoundThinkTextPolicy)}`,
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
return overrides;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1282
1681
|
private persistAgentRuntimeSettings(
|
|
1283
1682
|
agentName: string,
|
|
1284
1683
|
overrides: AgentRuntimeSettingsOverrides,
|
|
@@ -1329,17 +1728,1333 @@ export class AgentFramework {
|
|
|
1329
1728
|
// Data persists in the store under the namespace for investigation.
|
|
1330
1729
|
};
|
|
1331
1730
|
|
|
1332
|
-
return { agent, contextManager, cleanup };
|
|
1731
|
+
return { agent, contextManager, cleanup };
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
private createDeferred<T>(): Deferred<T> {
|
|
1735
|
+
let resolve!: (value: T) => void;
|
|
1736
|
+
let reject!: (reason?: unknown) => void;
|
|
1737
|
+
const promise = new Promise<T>((res, rej) => {
|
|
1738
|
+
resolve = res;
|
|
1739
|
+
reject = rej;
|
|
1740
|
+
});
|
|
1741
|
+
return { promise, resolve, reject };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
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
|
+
});
|
|
1333
2910
|
}
|
|
1334
2911
|
|
|
1335
|
-
private
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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,
|
|
1341
2925
|
});
|
|
1342
|
-
|
|
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
|
+
};
|
|
1343
3058
|
}
|
|
1344
3059
|
|
|
1345
3060
|
/** Liveness ping for an active ephemeral run (no-op otherwise). */
|
|
@@ -1633,11 +3348,44 @@ export class AgentFramework {
|
|
|
1633
3348
|
inputSchema: { type: 'object' },
|
|
1634
3349
|
};
|
|
1635
3350
|
|
|
3351
|
+
/** Synthesized save_image tool — present when a workspace module is
|
|
3352
|
+
* registered. Lets the agent persist an image it has already seen in its
|
|
3353
|
+
* own context (Discord attachments arrive inlined as base64 and are
|
|
3354
|
+
* otherwise unreachable as files). */
|
|
3355
|
+
private static readonly SAVE_IMAGE_TOOL: import('./types/index.js').ToolDefinition = {
|
|
3356
|
+
name: 'save_recent_image',
|
|
3357
|
+
description:
|
|
3358
|
+
'Save one or more recent images from your own context to workspace files. ' +
|
|
3359
|
+
'Images are counted back from the most recent (index 0). A single image is ' +
|
|
3360
|
+
'written to `path` as given (e.g. "project/photos/cat.png"); when `count` > 1 ' +
|
|
3361
|
+
'the range index..index+count-1 is saved with numeric suffixes ' +
|
|
3362
|
+
'("cat-0.png", "cat-1.png", …; 0 = the newest of the range). Saved files are ' +
|
|
3363
|
+
'visible via workspace tools and the /files/ endpoint.',
|
|
3364
|
+
inputSchema: {
|
|
3365
|
+
type: 'object',
|
|
3366
|
+
properties: {
|
|
3367
|
+
path: {
|
|
3368
|
+
type: 'string',
|
|
3369
|
+
description: 'Mount-prefixed destination path, e.g. "project/photos/name.png".',
|
|
3370
|
+
},
|
|
3371
|
+
index: {
|
|
3372
|
+
type: 'number',
|
|
3373
|
+
description: 'Which image, counting back from the most recent (0 = most recent). Default 0.',
|
|
3374
|
+
},
|
|
3375
|
+
count: {
|
|
3376
|
+
type: 'number',
|
|
3377
|
+
description: 'How many images to save, starting at `index` and going further back. Default 1.',
|
|
3378
|
+
},
|
|
3379
|
+
},
|
|
3380
|
+
required: ['path'],
|
|
3381
|
+
},
|
|
3382
|
+
};
|
|
3383
|
+
|
|
1636
3384
|
private static readonly AGENT_SETTINGS_TOOL: import('./types/index.js').ToolDefinition = {
|
|
1637
3385
|
name: 'agent_settings',
|
|
1638
3386
|
description:
|
|
1639
3387
|
'Read or change your hot runtime settings. This intentionally exposes only ' +
|
|
1640
|
-
'context budget, recent raw tail size, and
|
|
3388
|
+
'context budget, recent raw tail size, transition pace, and same-round think text routing; model, prompts, ' +
|
|
1641
3389
|
'folding strategy, and other restart-bound configuration are not mutable here. ' +
|
|
1642
3390
|
'Lower context budgets converge gradually under the transition pace before ' +
|
|
1643
3391
|
'becoming the hard live limit; increases take effect immediately.',
|
|
@@ -1648,9 +3396,16 @@ export class AgentFramework {
|
|
|
1648
3396
|
context_budget_tokens: { type: 'number', description: 'Total input context budget, including the reserved response allowance.' },
|
|
1649
3397
|
tail_tokens: { type: 'number', description: 'Recent raw context retained verbatim.' },
|
|
1650
3398
|
transition_pace_tokens: { type: 'number', description: 'Maximum ordinary KV re-read/perturbation per compile while converging.' },
|
|
3399
|
+
same_round_think_text_policy: {
|
|
3400
|
+
type: 'string',
|
|
3401
|
+
enum: ['public', 'private'],
|
|
3402
|
+
description:
|
|
3403
|
+
'Routing policy for ordinary text emitted in the same native assistant round as think(). ' +
|
|
3404
|
+
"Omitted in the recipe preserves the compatibility carry-forward: public.",
|
|
3405
|
+
},
|
|
1651
3406
|
settings: {
|
|
1652
3407
|
type: 'array',
|
|
1653
|
-
items: { type: 'string', enum: ['context_budget_tokens', 'tail_tokens', 'transition_pace_tokens'] },
|
|
3408
|
+
items: { type: 'string', enum: ['context_budget_tokens', 'tail_tokens', 'transition_pace_tokens', 'same_round_think_text_policy'] },
|
|
1654
3409
|
description: 'For reset: settings to restore to recipe values. Omit to reset all.',
|
|
1655
3410
|
},
|
|
1656
3411
|
},
|
|
@@ -1943,6 +3698,9 @@ export class AgentFramework {
|
|
|
1943
3698
|
undone?: number;
|
|
1944
3699
|
requested?: number;
|
|
1945
3700
|
messagesRemoved?: number;
|
|
3701
|
+
/** Discord addresses removed by message-granular undo. The durable outbox
|
|
3702
|
+
* owns eventual delivery; this is also returned for immediate surfaces. */
|
|
3703
|
+
removedRefs?: Array<{ serverId: string; channelId: string; messageId: string }>;
|
|
1946
3704
|
hidden?: number;
|
|
1947
3705
|
/** For `hide`: the Discord (channelId, messageId) of each removed message
|
|
1948
3706
|
* that carried one — so the surface can mark them with a reaction. */
|
|
@@ -2089,8 +3847,26 @@ export class AgentFramework {
|
|
|
2089
3847
|
};
|
|
2090
3848
|
}
|
|
2091
3849
|
const target = allMessages[allMessages.length - 1 - n];
|
|
2092
|
-
const
|
|
3850
|
+
const discarded = allMessages.slice(allMessages.length - n);
|
|
3851
|
+
const removedRefs = extractDiscordAwarenessRefs(discarded);
|
|
3852
|
+
const sourceBranch = this.store.currentBranch().name;
|
|
3853
|
+
const targetBranch = `undo-msgs/${agentName}/${Date.now()}`;
|
|
3854
|
+
|
|
3855
|
+
// Prepare the external side effect before switching Chronicle. If the
|
|
3856
|
+
// process dies after the switch but before activate(), startup promotes
|
|
3857
|
+
// this batch by matching targetBranch to the active branch.
|
|
3858
|
+
const markerBatch = this.discordAwarenessOutbox?.prepare({
|
|
3859
|
+
agentName,
|
|
3860
|
+
sourceBranch,
|
|
3861
|
+
targetBranch,
|
|
3862
|
+
refs: removedRefs,
|
|
3863
|
+
emoji: this.discordAwarenessEmoji,
|
|
3864
|
+
}) ?? null;
|
|
3865
|
+
|
|
3866
|
+
const branchName = cm.branchAt(target.id, targetBranch);
|
|
2093
3867
|
await cm.switchBranch(branchName);
|
|
3868
|
+
if (markerBatch) this.discordAwarenessOutbox!.activate(markerBatch.id);
|
|
3869
|
+
await this.syncDiscordAwarenessMarkers();
|
|
2094
3870
|
|
|
2095
3871
|
// Materialize config files from the new branch (fire-and-forget; gate
|
|
2096
3872
|
// picks up via mtime) — mirrors undoLastTurn.
|
|
@@ -2109,6 +3885,7 @@ export class AgentFramework {
|
|
|
2109
3885
|
return {
|
|
2110
3886
|
ok: true,
|
|
2111
3887
|
messagesRemoved: n,
|
|
3888
|
+
removedRefs,
|
|
2112
3889
|
lastVisible: await this.lastVisiblePreview(agentName),
|
|
2113
3890
|
};
|
|
2114
3891
|
}
|
|
@@ -2138,6 +3915,8 @@ export class AgentFramework {
|
|
|
2138
3915
|
return { ok: true, undone: 0, requested, lastVisible: null };
|
|
2139
3916
|
}
|
|
2140
3917
|
|
|
3918
|
+
await this.syncDiscordAwarenessMarkers();
|
|
3919
|
+
|
|
2141
3920
|
return { ok: true, undone, requested, lastVisible: await this.lastVisiblePreview(agentName) };
|
|
2142
3921
|
}
|
|
2143
3922
|
|
|
@@ -2727,7 +4506,11 @@ export class AgentFramework {
|
|
|
2727
4506
|
|
|
2728
4507
|
const agent = new Agent(config, contextManager, this.membrane);
|
|
2729
4508
|
const restoredSettings = this.readAgentRuntimeSettings(config.name);
|
|
2730
|
-
if (restoredSettings)
|
|
4509
|
+
if (restoredSettings) {
|
|
4510
|
+
agent.restoreRuntimeSettings(
|
|
4511
|
+
this.validatePersistedAgentRuntimeSettings(config.name, restoredSettings),
|
|
4512
|
+
);
|
|
4513
|
+
}
|
|
2731
4514
|
this.agents.set(config.name, agent);
|
|
2732
4515
|
this.agentConfigs.set(config.name, config);
|
|
2733
4516
|
|
|
@@ -2763,12 +4546,21 @@ export class AgentFramework {
|
|
|
2763
4546
|
// Check for inference requests
|
|
2764
4547
|
await this.processInferenceRequests();
|
|
2765
4548
|
|
|
2766
|
-
// Yield to the event loop between iterations.
|
|
2767
|
-
//
|
|
2768
|
-
// (
|
|
2769
|
-
|
|
4549
|
+
// Yield to the event loop between iterations. A pending inference request
|
|
4550
|
+
// is not necessarily runnable: while its agent is streaming or waiting for
|
|
4551
|
+
// tools, processInferenceRequests() deliberately requeues it. Treating that
|
|
4552
|
+
// requeued request as "work made progress" creates a microtask-only polling
|
|
4553
|
+
// loop. If activeStreams bookkeeping is absent/stale at the same time, the
|
|
4554
|
+
// old code had no await at all and starved the tool-result and HTTP I/O that
|
|
4555
|
+
// could make the agent runnable again (the Sol outage, 2026-07-15).
|
|
4556
|
+
//
|
|
4557
|
+
// No queue event means no foreground progress, so always take the normal
|
|
4558
|
+
// polling backoff. After an event, retain the low-latency macrotask yield
|
|
4559
|
+
// while background work remains. Both paths give Bun/Node a real event-loop
|
|
4560
|
+
// turn; neither can recurse forever through already-resolved promises.
|
|
4561
|
+
if (!event) {
|
|
2770
4562
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2771
|
-
} else if (this.activeStreams.size > 0) {
|
|
4563
|
+
} else if (this.pendingRequests.length > 0 || this.activeStreams.size > 0) {
|
|
2772
4564
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2773
4565
|
}
|
|
2774
4566
|
}
|
|
@@ -2811,6 +4603,7 @@ export class AgentFramework {
|
|
|
2811
4603
|
// Cast to AgentState to bypass TypeScript's control flow narrowing
|
|
2812
4604
|
const currentState = agent.state as AgentState;
|
|
2813
4605
|
if (currentState.status === 'ready') {
|
|
4606
|
+
const activeStreamContext = this.activeStreamContexts.get(agent.name);
|
|
2814
4607
|
// Flush pending assistant blocks (tool_use + preamble text) to context
|
|
2815
4608
|
const pendingBlocks = this.pendingAssistantBlocks.get(agent.name);
|
|
2816
4609
|
if (pendingBlocks) {
|
|
@@ -2883,6 +4676,25 @@ export class AgentFramework {
|
|
|
2883
4676
|
}
|
|
2884
4677
|
}
|
|
2885
4678
|
|
|
4679
|
+
// A newly injected message begins a new conversational round inside
|
|
4680
|
+
// the same provider inference. Remember that boundary for
|
|
4681
|
+
// driveStream: an explicit send in the preceding round must not
|
|
4682
|
+
// silence the reply, and the newest channel-bearing injection is
|
|
4683
|
+
// now the reply locus. Map presence matters even without a channel
|
|
4684
|
+
// (CLI/module input): it still clears the prior send suppression.
|
|
4685
|
+
if (midTurnInjections.length > 0) {
|
|
4686
|
+
const injectedChannelId = midTurnInjections.reduce<string | null>(
|
|
4687
|
+
(latest, injection) => {
|
|
4688
|
+
const candidate = injection.metadata?.channelId;
|
|
4689
|
+
return typeof candidate === 'string' && candidate.length > 0
|
|
4690
|
+
? candidate
|
|
4691
|
+
: latest;
|
|
4692
|
+
},
|
|
4693
|
+
null,
|
|
4694
|
+
);
|
|
4695
|
+
this.midTurnRoutingResets.set(agent.name, injectedChannelId);
|
|
4696
|
+
}
|
|
4697
|
+
|
|
2886
4698
|
// Check if any tool result requested endTurn
|
|
2887
4699
|
const shouldEndTurn = currentState.toolResults.some(tc => tc.result.endTurn);
|
|
2888
4700
|
|
|
@@ -2913,6 +4725,59 @@ export class AgentFramework {
|
|
|
2913
4725
|
// a terminal failure — rejecting an ephemeral's promise mid-run
|
|
2914
4726
|
// and emitting a spurious inference:exhausted (same race shape as
|
|
2915
4727
|
// 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
|
+
}
|
|
2916
4781
|
if (currentState.stream) {
|
|
2917
4782
|
this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
|
|
2918
4783
|
}
|
|
@@ -2929,6 +4794,8 @@ export class AgentFramework {
|
|
|
2929
4794
|
reason: 'context_budget_restart',
|
|
2930
4795
|
source: 'framework',
|
|
2931
4796
|
timestamp: Date.now(),
|
|
4797
|
+
channelId: activeStreamContext?.trigger?.channelId,
|
|
4798
|
+
contextBudgetRestart: restartDispatch,
|
|
2932
4799
|
});
|
|
2933
4800
|
} else if (currentState.stream) {
|
|
2934
4801
|
// Streaming path: convert results and resume the stream.
|
|
@@ -3515,7 +5382,7 @@ export class AgentFramework {
|
|
|
3515
5382
|
const state = this.createFrameworkState();
|
|
3516
5383
|
|
|
3517
5384
|
// Group requests by agent
|
|
3518
|
-
const requestsByAgent = new Map<string,
|
|
5385
|
+
const requestsByAgent = new Map<string, PendingInferenceRequest[]>();
|
|
3519
5386
|
for (const req of this.pendingRequests) {
|
|
3520
5387
|
const existing = requestsByAgent.get(req.agentName) ?? [];
|
|
3521
5388
|
existing.push(req);
|
|
@@ -3526,7 +5393,8 @@ export class AgentFramework {
|
|
|
3526
5393
|
this.pendingRequests = [];
|
|
3527
5394
|
|
|
3528
5395
|
// Check each agent
|
|
3529
|
-
for (const [agentName,
|
|
5396
|
+
for (const [agentName, queuedRequests] of requestsByAgent) {
|
|
5397
|
+
let requests = queuedRequests;
|
|
3530
5398
|
const agent = this.agents.get(agentName);
|
|
3531
5399
|
if (!agent) {
|
|
3532
5400
|
// Agent not found — request is orphaned. Emit warning and drop.
|
|
@@ -3546,7 +5414,15 @@ export class AgentFramework {
|
|
|
3546
5414
|
if (agent.state.status === 'inferring' || agent.state.status === 'streaming' || agent.state.status === 'waiting_for_tools') {
|
|
3547
5415
|
// Re-queue requests, but warn if they've been pending too long
|
|
3548
5416
|
const oldest = Math.min(...requests.map(r => r.timestamp));
|
|
3549
|
-
if (
|
|
5417
|
+
if (
|
|
5418
|
+
now - oldest > STALE_REQUEST_MS &&
|
|
5419
|
+
(this.staleWarnAt.get(agentName) ?? 0) < now - 60_000
|
|
5420
|
+
) {
|
|
5421
|
+
// Trace and stderr share the throttle. The old code throttled only
|
|
5422
|
+
// stderr, so a long-running tool call generated one trace per poll
|
|
5423
|
+
// (up to 100/sec after the scheduler backoff), adding avoidable work
|
|
5424
|
+
// precisely while the agent was already under pressure.
|
|
5425
|
+
this.staleWarnAt.set(agentName, now);
|
|
3550
5426
|
this.emitTrace({
|
|
3551
5427
|
type: 'inference:request_stale',
|
|
3552
5428
|
agentName,
|
|
@@ -3554,19 +5430,28 @@ export class AgentFramework {
|
|
|
3554
5430
|
requestCount: requests.length,
|
|
3555
5431
|
oldestRequestAge: now - oldest,
|
|
3556
5432
|
});
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
`[inference-stale] agent=${agentName} busy (${agent.state.status}) — ` +
|
|
3562
|
-
`${requests.length} request(s) waiting ${Math.round((now - oldest) / 1000)}s`,
|
|
3563
|
-
);
|
|
3564
|
-
}
|
|
5433
|
+
console.error(
|
|
5434
|
+
`[inference-stale] agent=${agentName} busy (${agent.state.status}) — ` +
|
|
5435
|
+
`${requests.length} request(s) waiting ${Math.round((now - oldest) / 1000)}s`,
|
|
5436
|
+
);
|
|
3565
5437
|
}
|
|
3566
5438
|
this.pendingRequests.push(...requests);
|
|
3567
5439
|
continue;
|
|
3568
5440
|
}
|
|
3569
5441
|
|
|
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
|
+
|
|
3570
5455
|
// Check policy
|
|
3571
5456
|
if (!this.inferencePolicy.shouldInfer(agentName, requests, state)) {
|
|
3572
5457
|
// Loud drop: a queued request that dies here is otherwise invisible —
|
|
@@ -3580,7 +5465,12 @@ export class AgentFramework {
|
|
|
3580
5465
|
}
|
|
3581
5466
|
|
|
3582
5467
|
// Start streaming inference (non-blocking — driveStream runs in background)
|
|
3583
|
-
const trigger = requests[0]
|
|
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
|
+
}
|
|
3584
5474
|
// Route this turn's auto-published speech to the channel that triggered
|
|
3585
5475
|
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
3586
5476
|
// (messages arrived in >1 channel while the agent was busy/idle) — reply
|
|
@@ -3597,9 +5487,9 @@ export class AgentFramework {
|
|
|
3597
5487
|
}
|
|
3598
5488
|
}
|
|
3599
5489
|
|
|
3600
|
-
private async startAgentStream(agent: Agent, trigger?:
|
|
5490
|
+
private async startAgentStream(agent: Agent, trigger?: PendingInferenceRequest, attempt = 0): Promise<void> {
|
|
3601
5491
|
// Record turn checkpoint before inference (only on first attempt, not retries)
|
|
3602
|
-
if (attempt === 0) {
|
|
5492
|
+
if (attempt === 0 && trigger?.reason !== 'primary-summary-fallback-retry') {
|
|
3603
5493
|
this.recordTurnCheckpoint(agent.name);
|
|
3604
5494
|
this.redoStacks.delete(agent.name); // new work invalidates redo
|
|
3605
5495
|
}
|
|
@@ -3620,54 +5510,276 @@ export class AgentFramework {
|
|
|
3620
5510
|
this.emitTrace({ type: 'inference:started', agentName: agent.name });
|
|
3621
5511
|
this.eventGate?.onInferenceStarted(agent.name);
|
|
3622
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)}`;
|
|
3623
5514
|
|
|
3624
5515
|
try {
|
|
3625
|
-
const
|
|
5516
|
+
const requestSnapshot = this.captureInferenceToolSnapshot(agent);
|
|
5517
|
+
const allTools = this.getToolsForAgent(agent.name, requestSnapshot);
|
|
3626
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
|
+
}
|
|
3627
5538
|
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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
|
+
}
|
|
3631
5559
|
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
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
|
+
}
|
|
5582
|
+
|
|
5583
|
+
const rebuilt = agent.buildPrimarySummaryRawExpansionRequest(
|
|
5584
|
+
retryDispatch.originalArtifacts,
|
|
5585
|
+
retryDispatch.candidateSummaries,
|
|
5586
|
+
tools,
|
|
3641
5587
|
);
|
|
3642
|
-
|
|
3643
|
-
|
|
5588
|
+
compiledRequest = rebuilt.request;
|
|
5589
|
+
artifacts = rebuilt.artifacts;
|
|
5590
|
+
|
|
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;
|
|
3644
5611
|
}
|
|
3645
|
-
} catch (error) {
|
|
3646
|
-
console.error('Module gatherContext error:', error);
|
|
3647
|
-
}
|
|
3648
5612
|
|
|
3649
|
-
|
|
3650
|
-
|
|
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);
|
|
5627
|
+
|
|
5628
|
+
const admittedTokens = this.primarySummaryAdmissionBoundTokens(
|
|
5629
|
+
retryDispatch.originalRequestInputBoundTokens,
|
|
5630
|
+
compiledRequest,
|
|
5631
|
+
retryDispatch.originalProviderInputTokens,
|
|
5632
|
+
);
|
|
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;
|
|
5654
|
+
}
|
|
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.
|
|
3651
5665
|
try {
|
|
3652
|
-
const
|
|
3653
|
-
const hookInjections = this.scopeInjectionsForAgent(
|
|
5666
|
+
const moduleInjections = this.scopeInjectionsForAgent(
|
|
3654
5667
|
agent.name,
|
|
3655
|
-
await this.
|
|
5668
|
+
await this.moduleRegistry.gatherContext(agent.name),
|
|
3656
5669
|
);
|
|
3657
|
-
if (
|
|
3658
|
-
injections =
|
|
5670
|
+
if (moduleInjections.length > 0) {
|
|
5671
|
+
injections = moduleInjections;
|
|
3659
5672
|
}
|
|
3660
5673
|
} catch (error) {
|
|
3661
|
-
console.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
|
+
}
|
|
3662
5691
|
}
|
|
3663
|
-
}
|
|
3664
5692
|
|
|
3665
|
-
|
|
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
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
3666
5738
|
|
|
3667
|
-
const
|
|
5739
|
+
const { stream } = agent.startStreamFromRequest(compiledRequest, artifacts);
|
|
5740
|
+
this.activeStreamContexts.set(agent.name, {
|
|
5741
|
+
requestId,
|
|
5742
|
+
trigger,
|
|
5743
|
+
compiledRequest,
|
|
5744
|
+
artifacts,
|
|
5745
|
+
});
|
|
5746
|
+
const handle = this.driveStream(
|
|
5747
|
+
agent,
|
|
5748
|
+
stream,
|
|
5749
|
+
requestSnapshot,
|
|
5750
|
+
requestId,
|
|
5751
|
+
trigger,
|
|
5752
|
+
attempt,
|
|
5753
|
+
compiledRequest,
|
|
5754
|
+
artifacts,
|
|
5755
|
+
);
|
|
3668
5756
|
this.activeStreams.set(agent.name, handle);
|
|
3669
5757
|
} catch (error) {
|
|
3670
5758
|
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
|
+
}
|
|
3671
5783
|
this.emitTrace({
|
|
3672
5784
|
type: 'inference:failed',
|
|
3673
5785
|
agentName: agent.name,
|
|
@@ -3710,27 +5822,35 @@ export class AgentFramework {
|
|
|
3710
5822
|
private async driveStream(
|
|
3711
5823
|
agent: Agent,
|
|
3712
5824
|
stream: YieldingStream,
|
|
3713
|
-
|
|
5825
|
+
requestSnapshot: InferenceToolSnapshot,
|
|
5826
|
+
requestId: string,
|
|
5827
|
+
trigger?: PendingInferenceRequest,
|
|
3714
5828
|
attempt = 0,
|
|
3715
|
-
compiledRequest?: NormalizedRequest
|
|
5829
|
+
compiledRequest?: NormalizedRequest,
|
|
5830
|
+
artifacts?: ActivationCompileArtifacts,
|
|
3716
5831
|
): Promise<void> {
|
|
3717
5832
|
const startTime = Date.now();
|
|
3718
|
-
const requestId = `${agent.name}-${startTime}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3719
5833
|
const myStreamId = agent.streamId;
|
|
3720
5834
|
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' };
|
|
3721
5843
|
|
|
3722
5844
|
// ---- Present-while-acting turn state ---------------------------------
|
|
3723
|
-
// Output locus for
|
|
3724
|
-
//
|
|
3725
|
-
//
|
|
3726
|
-
//
|
|
3727
|
-
//
|
|
3728
|
-
//
|
|
3729
|
-
|
|
3730
|
-
// early (e.g. heartbeat-triggered) may still resolve a channel by the
|
|
3731
|
-
// time later prose routes.
|
|
3732
|
-
if (trigger?.reason !== 'context_budget_restart') {
|
|
5845
|
+
// Output locus for the current CONVERSATIONAL ROUND: resolved lazily and
|
|
5846
|
+
// kept stable across tool-only rounds. A channel message injected at a
|
|
5847
|
+
// tool boundary replaces the pin before the next model round, because a
|
|
5848
|
+
// single provider inference can now contain several conversations. The
|
|
5849
|
+
// pin lives in `turnLocusPins` so it survives a context-budget restart. A
|
|
5850
|
+
// null resolution is not pinned.
|
|
5851
|
+
if (trigger?.reason !== 'context_budget_restart' && trigger?.reason !== 'primary-summary-fallback-retry') {
|
|
3733
5852
|
this.turnLocusPins.delete(agent.name);
|
|
5853
|
+
this.midTurnRoutingResets.delete(agent.name);
|
|
3734
5854
|
}
|
|
3735
5855
|
const resolveTurnLocus = (): string | null => {
|
|
3736
5856
|
let locus = this.turnLocusPins.get(agent.name) ?? null;
|
|
@@ -3757,10 +5877,9 @@ export class AgentFramework {
|
|
|
3757
5877
|
.catch((err) => console.error('mid-turn speech routing failed:', err));
|
|
3758
5878
|
};
|
|
3759
5879
|
|
|
3760
|
-
// Sticky
|
|
3761
|
-
//
|
|
3762
|
-
//
|
|
3763
|
-
// rounds' prose was already delivered live and cannot be retracted.
|
|
5880
|
+
// Sticky explicit-send suppression: prose after send_message stays quiet
|
|
5881
|
+
// to prevent a redundant "sent it" postscript. Fresh injected input
|
|
5882
|
+
// clears it, because the following prose is a reply to a new message.
|
|
3764
5883
|
let turnSilenced = false;
|
|
3765
5884
|
|
|
3766
5885
|
// Live routing is only trusted when the membrane provides verbatim
|
|
@@ -3777,11 +5896,128 @@ export class AgentFramework {
|
|
|
3777
5896
|
// responding to, for the whole duration of this turn. Started here (paired
|
|
3778
5897
|
// with the finally below, so it can never leak) and refreshed on a 7s
|
|
3779
5898
|
// interval by the ChannelRegistry until stopped on any exit path.
|
|
3780
|
-
|
|
5899
|
+
let typingChannel = this.channelRegistry
|
|
3781
5900
|
? trigger?.channelId ?? this.channelRegistry.getDefaultPublishChannel()
|
|
3782
5901
|
: null;
|
|
3783
5902
|
if (typingChannel) this.channelRegistry!.startTyping(typingChannel);
|
|
3784
5903
|
|
|
5904
|
+
const adoptInjectedRound = (): void => {
|
|
5905
|
+
if (!this.midTurnRoutingResets.has(agent.name)) return;
|
|
5906
|
+
const nextLocus = this.midTurnRoutingResets.get(agent.name) ?? null;
|
|
5907
|
+
this.midTurnRoutingResets.delete(agent.name);
|
|
5908
|
+
|
|
5909
|
+
// A new human/agent message, not merely a tool result, means any earlier
|
|
5910
|
+
// explicit delivery has completed its conversational job.
|
|
5911
|
+
turnSilenced = false;
|
|
5912
|
+
|
|
5913
|
+
if (nextLocus) {
|
|
5914
|
+
this.turnLocusPins.set(agent.name, nextLocus);
|
|
5915
|
+
if (typingChannel !== nextLocus) {
|
|
5916
|
+
if (typingChannel) this.channelRegistry?.stopTyping(typingChannel);
|
|
5917
|
+
typingChannel = nextLocus;
|
|
5918
|
+
this.channelRegistry?.startTyping(nextLocus);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
};
|
|
5922
|
+
|
|
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
|
+
|
|
3785
6021
|
try {
|
|
3786
6022
|
for await (const event of stream) {
|
|
3787
6023
|
this.touchEphemeralRun(agent.name, true);
|
|
@@ -3809,8 +6045,8 @@ export class AgentFramework {
|
|
|
3809
6045
|
}
|
|
3810
6046
|
|
|
3811
6047
|
case 'tool-calls': {
|
|
6048
|
+
adoptInjectedRound();
|
|
3812
6049
|
hadToolCalls = true;
|
|
3813
|
-
this.recordEphemeralToolCalls(agent.name, event.calls.length);
|
|
3814
6050
|
this.emitTrace({
|
|
3815
6051
|
type: 'inference:tool_calls_yielded',
|
|
3816
6052
|
agentName: agent.name,
|
|
@@ -3849,7 +6085,51 @@ export class AgentFramework {
|
|
|
3849
6085
|
|
|
3850
6086
|
agent.enterWaitingForTools(event.calls, stream);
|
|
3851
6087
|
|
|
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
|
+
|
|
3852
6103
|
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
|
+
}
|
|
3853
6133
|
this.dispatchToolCall(agent.name, call);
|
|
3854
6134
|
}
|
|
3855
6135
|
|
|
@@ -3866,6 +6146,9 @@ export class AgentFramework {
|
|
|
3866
6146
|
// note above — the fallback preamble is cumulative in XML mode).
|
|
3867
6147
|
if (this.channelRegistry) {
|
|
3868
6148
|
const roundToolNames = event.calls.map((c) => c.name);
|
|
6149
|
+
const hasSameRoundPrivateThink =
|
|
6150
|
+
roundToolNames.includes('think') &&
|
|
6151
|
+
requestSnapshot.sameRoundThinkTextPolicy === 'private';
|
|
3869
6152
|
if (roundToolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n)))) {
|
|
3870
6153
|
turnSilenced = true;
|
|
3871
6154
|
}
|
|
@@ -3877,6 +6160,10 @@ export class AgentFramework {
|
|
|
3877
6160
|
console.error(
|
|
3878
6161
|
`[routing] ${agent.name}: mid-turn round [${roundToolNames.join(', ')}] -> prose NOT routed (turn silenced)`,
|
|
3879
6162
|
);
|
|
6163
|
+
} else if (hasSameRoundPrivateThink) {
|
|
6164
|
+
console.error(
|
|
6165
|
+
`[routing] ${agent.name}: mid-turn round [${roundToolNames.join(', ')}] -> prose NOT routed (same_round_think_text_policy=private)`,
|
|
6166
|
+
);
|
|
3880
6167
|
} else {
|
|
3881
6168
|
const locus = resolveTurnLocus();
|
|
3882
6169
|
console.error(
|
|
@@ -3894,6 +6181,7 @@ export class AgentFramework {
|
|
|
3894
6181
|
}
|
|
3895
6182
|
|
|
3896
6183
|
case 'complete': {
|
|
6184
|
+
adoptInjectedRound();
|
|
3897
6185
|
const durationMs = Date.now() - startTime;
|
|
3898
6186
|
const response = event.response;
|
|
3899
6187
|
|
|
@@ -3934,14 +6222,6 @@ export class AgentFramework {
|
|
|
3934
6222
|
}
|
|
3935
6223
|
}
|
|
3936
6224
|
|
|
3937
|
-
// Add assistant response to context.
|
|
3938
|
-
// If we had tool calls, each round's blocks (thinking + text +
|
|
3939
|
-
// tool_use) were already stored as pendingAssistantBlocks and
|
|
3940
|
-
// flushed when tool results arrived. Only store TRAILING content
|
|
3941
|
-
// — blocks after the last tool block. A type-based filter would
|
|
3942
|
-
// double-store earlier rounds' text/thinking AND detach signed
|
|
3943
|
-
// thinking blocks from their tool_use turn (the API requires
|
|
3944
|
-
// thinking to precede tool_use in the same assistant turn).
|
|
3945
6225
|
const lastToolIdx = response.content.reduce(
|
|
3946
6226
|
(last: number, b: ContentBlock, i: number) =>
|
|
3947
6227
|
b.type === 'tool_use' || b.type === 'tool_result' ? i : last,
|
|
@@ -3950,12 +6230,259 @@ export class AgentFramework {
|
|
|
3950
6230
|
const terminalContent = lastToolIdx >= 0
|
|
3951
6231
|
? response.content.slice(lastToolIdx + 1)
|
|
3952
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
|
+
// Add assistant response to context.
|
|
6433
|
+
// If we had tool calls, each round's blocks (thinking + text +
|
|
6434
|
+
// tool_use) were already stored as pendingAssistantBlocks and
|
|
6435
|
+
// flushed when tool results arrived. Only store TRAILING content
|
|
6436
|
+
// — blocks after the last tool block. A type-based filter would
|
|
6437
|
+
// double-store earlier rounds' text/thinking AND detach signed
|
|
6438
|
+
// thinking blocks from their tool_use turn (the API requires
|
|
6439
|
+
// thinking to precede tool_use in the same assistant turn).
|
|
3953
6440
|
if (lastToolIdx >= 0) {
|
|
3954
6441
|
if (terminalContent.length > 0) {
|
|
3955
|
-
|
|
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
|
+
}
|
|
3956
6463
|
}
|
|
3957
6464
|
} else {
|
|
3958
|
-
|
|
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
|
+
agent.addAssistantResponse(terminalContent);
|
|
6485
|
+
}
|
|
3959
6486
|
}
|
|
3960
6487
|
|
|
3961
6488
|
// Run afterInference hooks (no-op if no MCPL servers)
|
|
@@ -4017,6 +6544,50 @@ export class AgentFramework {
|
|
|
4017
6544
|
}
|
|
4018
6545
|
: undefined;
|
|
4019
6546
|
|
|
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
|
+
|
|
4020
6591
|
// Reset agent state before emitting inference:completed. Traces are
|
|
4021
6592
|
// observability-only, but external synchronous listeners should
|
|
4022
6593
|
// still see the terminal state at the terminal trace boundary.
|
|
@@ -4055,8 +6626,10 @@ export class AgentFramework {
|
|
|
4055
6626
|
agentName: agent.name,
|
|
4056
6627
|
requestId,
|
|
4057
6628
|
success: true,
|
|
4058
|
-
request:
|
|
4059
|
-
response:
|
|
6629
|
+
request: fallbackRetryRequestForLog,
|
|
6630
|
+
response: primarySummaryRetry
|
|
6631
|
+
? this.summarizePrimarySummaryRetryResponse(response)
|
|
6632
|
+
: response.raw ?? { note: 'streaming response' },
|
|
4060
6633
|
durationMs,
|
|
4061
6634
|
tokenUsage,
|
|
4062
6635
|
stopReason: response.stopReason,
|
|
@@ -4209,15 +6782,17 @@ export class AgentFramework {
|
|
|
4209
6782
|
// speech/thoughts split is left untouched (module/TUI rendering
|
|
4210
6783
|
// unaffected) — this only governs what reaches the channel.
|
|
4211
6784
|
//
|
|
4212
|
-
//
|
|
4213
|
-
//
|
|
4214
|
-
//
|
|
4215
|
-
//
|
|
6785
|
+
// Native round routing uses the live sticky flag, which a fresh
|
|
6786
|
+
// injected message can reset. The legacy/fallback path has no
|
|
6787
|
+
// reliable round boundaries, so retain its historical turn-wide
|
|
6788
|
+
// scan to avoid double-posting.
|
|
4216
6789
|
const toolNames = response.content
|
|
4217
6790
|
.filter((b) => b.type === 'tool_use')
|
|
4218
6791
|
.map((b) => (b as unknown as { name?: string }).name)
|
|
4219
6792
|
.filter((n): n is string => typeof n === 'string');
|
|
4220
|
-
const silenced =
|
|
6793
|
+
const silenced = liveProseRouting
|
|
6794
|
+
? turnSilenced
|
|
6795
|
+
: turnSilenced || toolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n)));
|
|
4221
6796
|
|
|
4222
6797
|
const segments = splitProseSegments(liveProseRouting ? terminalContent : response.content);
|
|
4223
6798
|
|
|
@@ -4276,13 +6851,30 @@ export class AgentFramework {
|
|
|
4276
6851
|
requestId,
|
|
4277
6852
|
success: false,
|
|
4278
6853
|
error: err.message,
|
|
4279
|
-
request:
|
|
6854
|
+
request: fallbackRetryRequestForLog,
|
|
4280
6855
|
durationMs,
|
|
4281
6856
|
});
|
|
4282
6857
|
|
|
4283
6858
|
// Only reset + retry if this is still the active stream
|
|
4284
6859
|
if (agent.streamId !== myStreamId) break;
|
|
4285
6860
|
|
|
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
|
+
|
|
4286
6878
|
agent.reset();
|
|
4287
6879
|
|
|
4288
6880
|
const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
@@ -4322,6 +6914,17 @@ export class AgentFramework {
|
|
|
4322
6914
|
// reject an ephemeral's promise mid-run and bump the failure
|
|
4323
6915
|
// streak). Gate release + stream teardown happen in `finally`.
|
|
4324
6916
|
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
|
+
}
|
|
4325
6928
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
4326
6929
|
return;
|
|
4327
6930
|
}
|
|
@@ -4330,6 +6933,40 @@ export class AgentFramework {
|
|
|
4330
6933
|
// may have already started a new stream, bumping streamId)
|
|
4331
6934
|
if (agent.streamId === myStreamId) {
|
|
4332
6935
|
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
|
+
}
|
|
4333
6970
|
agent.reset();
|
|
4334
6971
|
this.settleAgent(agent.name, {
|
|
4335
6972
|
stopReason: 'exhausted',
|
|
@@ -4353,7 +6990,7 @@ export class AgentFramework {
|
|
|
4353
6990
|
requestId,
|
|
4354
6991
|
success: false,
|
|
4355
6992
|
error: `Stream aborted: ${reason}`,
|
|
4356
|
-
request:
|
|
6993
|
+
request: fallbackRetryRequestForLog,
|
|
4357
6994
|
durationMs,
|
|
4358
6995
|
});
|
|
4359
6996
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
@@ -4405,12 +7042,63 @@ export class AgentFramework {
|
|
|
4405
7042
|
// inference:exhausted so ephemeral agent promises can settle.
|
|
4406
7043
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4407
7044
|
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
|
+
}
|
|
4408
7078
|
this.emitTrace({
|
|
4409
7079
|
type: 'inference:failed',
|
|
4410
7080
|
agentName: agent.name,
|
|
4411
7081
|
error: err.message,
|
|
4412
7082
|
stack: err.stack,
|
|
4413
7083
|
});
|
|
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
|
+
}
|
|
4414
7102
|
this.settleAgent(agent.name, {
|
|
4415
7103
|
stopReason: 'exhausted',
|
|
4416
7104
|
speech: '',
|
|
@@ -4431,7 +7119,7 @@ export class AgentFramework {
|
|
|
4431
7119
|
requestId,
|
|
4432
7120
|
success: false,
|
|
4433
7121
|
error: `Stream threw: ${err.message}`,
|
|
4434
|
-
request:
|
|
7122
|
+
request: fallbackRetryRequestForLog,
|
|
4435
7123
|
durationMs,
|
|
4436
7124
|
});
|
|
4437
7125
|
agent.reset();
|
|
@@ -4452,6 +7140,7 @@ export class AgentFramework {
|
|
|
4452
7140
|
this.channelRegistry?.stopTyping();
|
|
4453
7141
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
4454
7142
|
this.activeStreams.delete(agent.name);
|
|
7143
|
+
this.activeStreamContexts.delete(agent.name);
|
|
4455
7144
|
this.pendingAssistantBlocks.delete(agent.name);
|
|
4456
7145
|
|
|
4457
7146
|
// A conversation fork whose TTL closure turn just finished is done for
|
|
@@ -4589,6 +7278,51 @@ export class AgentFramework {
|
|
|
4589
7278
|
return undefined;
|
|
4590
7279
|
}
|
|
4591
7280
|
|
|
7281
|
+
private approximateDecodedBase64Bytes(base64: string): number {
|
|
7282
|
+
const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
|
|
7283
|
+
return Math.max(0, Math.floor(base64.length * 3 / 4) - padding);
|
|
7284
|
+
}
|
|
7285
|
+
|
|
7286
|
+
private sanitizeProcessLogValue(value: unknown, seen = new WeakMap<object, unknown>()): unknown {
|
|
7287
|
+
if (value && typeof value === 'object') {
|
|
7288
|
+
const seenValue = seen.get(value);
|
|
7289
|
+
if (seenValue) return seenValue;
|
|
7290
|
+
}
|
|
7291
|
+
if (Array.isArray(value)) {
|
|
7292
|
+
const clone: unknown[] = [];
|
|
7293
|
+
seen.set(value, clone);
|
|
7294
|
+
for (const item of value) {
|
|
7295
|
+
clone.push(this.sanitizeProcessLogValue(item, seen));
|
|
7296
|
+
}
|
|
7297
|
+
return clone;
|
|
7298
|
+
}
|
|
7299
|
+
if (!value || typeof value !== 'object') {
|
|
7300
|
+
return value;
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7303
|
+
const record = value as Record<string, unknown>;
|
|
7304
|
+
if (record.type === 'image' && typeof record.data === 'string' && typeof record.mimeType === 'string') {
|
|
7305
|
+
const clone: Record<string, unknown> = {};
|
|
7306
|
+
seen.set(value, clone);
|
|
7307
|
+
for (const [key, child] of Object.entries(record)) {
|
|
7308
|
+
if (key === 'data' || key === 'mimeType') continue;
|
|
7309
|
+
clone[key] = this.sanitizeProcessLogValue(child, seen);
|
|
7310
|
+
}
|
|
7311
|
+
clone.type = 'image';
|
|
7312
|
+
clone.mimeType = record.mimeType;
|
|
7313
|
+
clone.approxByteLength = this.approximateDecodedBase64Bytes(record.data);
|
|
7314
|
+
clone.redacted = true;
|
|
7315
|
+
return clone;
|
|
7316
|
+
}
|
|
7317
|
+
|
|
7318
|
+
const clone: Record<string, unknown> = {};
|
|
7319
|
+
seen.set(value, clone);
|
|
7320
|
+
for (const [key, child] of Object.entries(record)) {
|
|
7321
|
+
clone[key] = this.sanitizeProcessLogValue(child, seen);
|
|
7322
|
+
}
|
|
7323
|
+
return clone;
|
|
7324
|
+
}
|
|
7325
|
+
|
|
4592
7326
|
private logInference(entry: InferenceLogEntry): void {
|
|
4593
7327
|
// Store large request/response as blobs
|
|
4594
7328
|
const entryToStore = { ...entry };
|
|
@@ -4622,15 +7356,15 @@ export class AgentFramework {
|
|
|
4622
7356
|
private logProcessEvent(event: ProcessEvent, responses: ModuleProcessResponse[]): void {
|
|
4623
7357
|
const entry: ProcessLogEntry = {
|
|
4624
7358
|
timestamp: Date.now(),
|
|
4625
|
-
processEvent: event,
|
|
4626
|
-
responses,
|
|
7359
|
+
processEvent: this.sanitizeProcessLogValue(event) as ProcessEvent,
|
|
7360
|
+
responses: this.sanitizeProcessLogValue(responses) as ModuleProcessResponse[],
|
|
4627
7361
|
};
|
|
4628
7362
|
|
|
4629
7363
|
// Blob threshold: 10KB
|
|
4630
7364
|
const BLOB_THRESHOLD = 10000;
|
|
4631
7365
|
|
|
4632
7366
|
const entryToStore = { ...entry };
|
|
4633
|
-
const responsesJson = JSON.stringify(responses);
|
|
7367
|
+
const responsesJson = JSON.stringify(entry.responses);
|
|
4634
7368
|
if (responsesJson.length > BLOB_THRESHOLD) {
|
|
4635
7369
|
const blobId = this.store.storeBlob(Buffer.from(responsesJson), 'application/json');
|
|
4636
7370
|
entryToStore.responses = { blobId };
|
|
@@ -4676,6 +7410,11 @@ export class AgentFramework {
|
|
|
4676
7410
|
return;
|
|
4677
7411
|
}
|
|
4678
7412
|
|
|
7413
|
+
if (enrichedCall.name === 'save_recent_image') {
|
|
7414
|
+
this.dispatchSaveImageToolCall(agentName, enrichedCall);
|
|
7415
|
+
return;
|
|
7416
|
+
}
|
|
7417
|
+
|
|
4679
7418
|
// Route gate_status tool
|
|
4680
7419
|
if (enrichedCall.name === 'gate_status' && this.eventGate) {
|
|
4681
7420
|
this.dispatchGateToolCall(agentName, enrichedCall);
|
|
@@ -4939,12 +7678,23 @@ export class AgentFramework {
|
|
|
4939
7678
|
* work, per-agent status + last inference activity. Cheap and read-only.
|
|
4940
7679
|
*/
|
|
4941
7680
|
healthSnapshot(): Record<string, unknown> {
|
|
7681
|
+
const primarySummary = this.readPrimarySummaryFallbackState();
|
|
4942
7682
|
return {
|
|
4943
7683
|
at: new Date().toISOString(),
|
|
4944
7684
|
uptimeSec: Math.round(process.uptime()),
|
|
4945
7685
|
gate: this.eventGate?.inferenceDiagnostics() ?? null,
|
|
4946
7686
|
pendingRequests: this.pendingRequests.length,
|
|
4947
7687
|
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
|
+
},
|
|
4948
7698
|
agents: [...this.agents.entries()].map(([name, agent]) => ({
|
|
4949
7699
|
name,
|
|
4950
7700
|
status: agent.state.status,
|
|
@@ -4979,6 +7729,22 @@ export class AgentFramework {
|
|
|
4979
7729
|
* every ~15 min instead of flooding the channel on every occurrence.
|
|
4980
7730
|
* See connectome docs/observability.md.
|
|
4981
7731
|
*/
|
|
7732
|
+
/**
|
|
7733
|
+
* Public ops-alert entry point for host-wired alarms that originate
|
|
7734
|
+
* OUTSIDE the framework (e.g. context-manager's repeating
|
|
7735
|
+
* compression-quarantine klaxon, wired in the host at strategy
|
|
7736
|
+
* construction). Same sinks and cooldown as internal alerts:
|
|
7737
|
+
* failures.log + ops:alert trace + CONNECTOME_OPS_WEBHOOK.
|
|
7738
|
+
*/
|
|
7739
|
+
notifyOps(
|
|
7740
|
+
kind: string,
|
|
7741
|
+
agentName: string,
|
|
7742
|
+
message: string,
|
|
7743
|
+
data?: Record<string, unknown>,
|
|
7744
|
+
): void {
|
|
7745
|
+
this.opsAlert(kind, agentName, message, { data });
|
|
7746
|
+
}
|
|
7747
|
+
|
|
4982
7748
|
private opsAlert(
|
|
4983
7749
|
kind: string,
|
|
4984
7750
|
agentName: string,
|
|
@@ -5074,6 +7840,17 @@ export class AgentFramework {
|
|
|
5074
7840
|
return {};
|
|
5075
7841
|
}
|
|
5076
7842
|
|
|
7843
|
+
/**
|
|
7844
|
+
* Public entry to the ops-alert pipeline (failures.log + ops:alert trace +
|
|
7845
|
+
* throttled webhook) for host-level detectors OUTSIDE the framework's own
|
|
7846
|
+
* inference driver — e.g. the host's logging adapter catching refusals on
|
|
7847
|
+
* off-path calls (compression/summarizer, maintenance) that noteRefusal
|
|
7848
|
+
* never sees. Same per-(agent,kind) cooldown as everything else.
|
|
7849
|
+
*/
|
|
7850
|
+
notifyOpsAlert(kind: string, agentName: string, message: string, data?: Record<string, unknown>): void {
|
|
7851
|
+
this.opsAlert(kind, agentName, message, { data });
|
|
7852
|
+
}
|
|
7853
|
+
|
|
5077
7854
|
private noteInferenceExhausted(
|
|
5078
7855
|
agentName: string,
|
|
5079
7856
|
reason: string,
|
|
@@ -5355,8 +8132,16 @@ export class AgentFramework {
|
|
|
5355
8132
|
|
|
5356
8133
|
for (const config of serverConfigs) {
|
|
5357
8134
|
try {
|
|
5358
|
-
|
|
8135
|
+
// Stage every startup connection with both planes closed. This removes
|
|
8136
|
+
// server-order dependence: no early heartbeat/shell push can become
|
|
8137
|
+
// model-visible before a later Discord connection has joined the same
|
|
8138
|
+
// awareness generation.
|
|
8139
|
+
await this.connectMcplServerInternal(config, true);
|
|
5359
8140
|
} catch (error) {
|
|
8141
|
+
if (error instanceof DiscordAwarenessAccountingError) {
|
|
8142
|
+
await this.mcplServerRegistry.closeAll();
|
|
8143
|
+
throw error;
|
|
8144
|
+
}
|
|
5360
8145
|
// Fail-open: log and continue with remaining servers
|
|
5361
8146
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
5362
8147
|
console.error(`Failed to connect MCPL server "${config.id}":`, err.message);
|
|
@@ -5374,10 +8159,353 @@ export class AgentFramework {
|
|
|
5374
8159
|
}
|
|
5375
8160
|
}
|
|
5376
8161
|
|
|
8162
|
+
// Start the durable drain only after all startup connections are staged.
|
|
8163
|
+
// The awareness tools/call is written before control-plane registration is
|
|
8164
|
+
// flushed, preserving the server-side queue -> registration -> call order.
|
|
8165
|
+
const startupBarrier = this.installMcplDataPlaneGate();
|
|
8166
|
+
this.releaseMcplDataPlaneGate(startupBarrier);
|
|
8167
|
+
try {
|
|
8168
|
+
await startupBarrier.promise;
|
|
8169
|
+
this.completeMcplDataPlaneGate(startupBarrier);
|
|
8170
|
+
} catch (error) {
|
|
8171
|
+
await this.mcplServerRegistry.closeAll();
|
|
8172
|
+
throw error;
|
|
8173
|
+
}
|
|
8174
|
+
|
|
5377
8175
|
// Discover tools from all connected servers
|
|
5378
8176
|
await this.refreshMcplTools();
|
|
5379
8177
|
}
|
|
5380
8178
|
|
|
8179
|
+
/** Reconcile the durable ledger with Chronicle, then deliver every server's work. */
|
|
8180
|
+
async syncDiscordAwarenessMarkers(_onlyServerId?: string): Promise<void> {
|
|
8181
|
+
if (!this.discordAwarenessOutbox) return;
|
|
8182
|
+
// A targeted retry may discover pending work for other Discord servers
|
|
8183
|
+
// during reconciliation. Safety is global, so one generation accounts for
|
|
8184
|
+
// all pending operations before releasing any MCPL data plane.
|
|
8185
|
+
const barrier = this.installMcplDataPlaneGate();
|
|
8186
|
+
this.releaseMcplDataPlaneGate(barrier);
|
|
8187
|
+
try {
|
|
8188
|
+
await barrier.promise;
|
|
8189
|
+
this.completeMcplDataPlaneGate(barrier);
|
|
8190
|
+
} catch (error) {
|
|
8191
|
+
await this.failMcplDataPlaneGate(barrier, 'explicit synchronization', error);
|
|
8192
|
+
throw error;
|
|
8193
|
+
}
|
|
8194
|
+
}
|
|
8195
|
+
|
|
8196
|
+
private async resumePreparedDiscordSuppressions(): Promise<void> {
|
|
8197
|
+
if (!this.discordAwarenessOutbox) return;
|
|
8198
|
+
const batches = this.discordAwarenessOutbox.preparedSuppressionsForBranch(
|
|
8199
|
+
this.store.currentBranch().name,
|
|
8200
|
+
this.store.listBranches(),
|
|
8201
|
+
);
|
|
8202
|
+
for (const batch of batches) {
|
|
8203
|
+
const agent = this.agents.get(batch.agentName);
|
|
8204
|
+
if (!agent) {
|
|
8205
|
+
throw new Error(
|
|
8206
|
+
`Cannot resume Discord suppression ${batch.id}: agent ${batch.agentName} is not configured`,
|
|
8207
|
+
);
|
|
8208
|
+
}
|
|
8209
|
+
const cm = agent.getContextManager();
|
|
8210
|
+
for (const interval of [...(batch.suppressionIntervals ?? [])].reverse()) {
|
|
8211
|
+
const from = cm.getMessage(interval.fromId as MessageId);
|
|
8212
|
+
const to = cm.getMessage(interval.toId as MessageId);
|
|
8213
|
+
if (!from && !to) continue; // This interval committed before the crash.
|
|
8214
|
+
if (!from || !to) {
|
|
8215
|
+
throw new Error(
|
|
8216
|
+
`Cannot safely resume Discord suppression ${batch.id}: interval ` +
|
|
8217
|
+
`${interval.fromId}..${interval.toId} is only partially present`,
|
|
8218
|
+
);
|
|
8219
|
+
}
|
|
8220
|
+
if (interval.fromId === interval.toId) cm.removeMessage(from.id);
|
|
8221
|
+
else cm.removeMessages(from.id, to.id);
|
|
8222
|
+
}
|
|
8223
|
+
this.discordAwarenessOutbox.activate(batch.id);
|
|
8224
|
+
console.error(`[discord-awareness] resumed suppression batch ${batch.id}`);
|
|
8225
|
+
}
|
|
8226
|
+
}
|
|
8227
|
+
|
|
8228
|
+
/**
|
|
8229
|
+
* Deliver every due ref independently. Permanent Discord failures are kept
|
|
8230
|
+
* in the ledger for audit but do not block later operations; retryable
|
|
8231
|
+
* failures remain pending for the next reconnect/list-change attempt.
|
|
8232
|
+
*/
|
|
8233
|
+
private drainDiscordAwarenessOutbox(
|
|
8234
|
+
serverId: string,
|
|
8235
|
+
): Promise<DiscordAwarenessDrainOutcome> {
|
|
8236
|
+
if (!this.discordAwarenessOutbox) {
|
|
8237
|
+
return Promise.resolve({ status: 'delivered', delivered: 0, failed: 0 });
|
|
8238
|
+
}
|
|
8239
|
+
const existing = this.discordAwarenessDrains.get(serverId);
|
|
8240
|
+
if (existing) return existing;
|
|
8241
|
+
|
|
8242
|
+
const drain = (async () => {
|
|
8243
|
+
const connection = this.mcplServerRegistry?.getServer(serverId);
|
|
8244
|
+
if (!connection?.isConnected) {
|
|
8245
|
+
const operations = this.readDiscordAwarenessPending(serverId);
|
|
8246
|
+
for (const operation of operations) {
|
|
8247
|
+
this.writeDiscordAwarenessFailure(
|
|
8248
|
+
operation.batchId,
|
|
8249
|
+
operation.ref,
|
|
8250
|
+
operation.action,
|
|
8251
|
+
'Awareness delivery not attempted: MCPL connection unavailable',
|
|
8252
|
+
false,
|
|
8253
|
+
);
|
|
8254
|
+
}
|
|
8255
|
+
if (operations.length > 0) {
|
|
8256
|
+
console.error(
|
|
8257
|
+
`[discord-awareness] ${serverId}: connection unavailable; ` +
|
|
8258
|
+
`durably deferred=${operations.length}`,
|
|
8259
|
+
);
|
|
8260
|
+
}
|
|
8261
|
+
return { status: 'unavailable' as const, accounted: operations.length };
|
|
8262
|
+
}
|
|
8263
|
+
|
|
8264
|
+
let delivered = 0;
|
|
8265
|
+
let failed = 0;
|
|
8266
|
+
const attempted = new Set<string>();
|
|
8267
|
+
while (true) {
|
|
8268
|
+
const operations = this.readDiscordAwarenessPending(serverId).filter((operation) => {
|
|
8269
|
+
const key = `${operation.batchId}\0${operation.ref.channelId}\0${operation.ref.messageId}\0${operation.action}`;
|
|
8270
|
+
if (attempted.has(key)) return false;
|
|
8271
|
+
attempted.add(key);
|
|
8272
|
+
return true;
|
|
8273
|
+
});
|
|
8274
|
+
if (operations.length === 0) break;
|
|
8275
|
+
for (const operation of operations) {
|
|
8276
|
+
const ref = operation.ref;
|
|
8277
|
+
const channelId = ref.channelId.startsWith('discord:')
|
|
8278
|
+
? ref.channelId.split(':').at(-1)!
|
|
8279
|
+
: ref.channelId;
|
|
8280
|
+
let deliveryError: string | undefined;
|
|
8281
|
+
try {
|
|
8282
|
+
const tool = operation.action === 'add' ? 'add_reaction' : 'remove_reaction';
|
|
8283
|
+
const result = await connection.sendToolsCallWithDeadline(tool, {
|
|
8284
|
+
channelId,
|
|
8285
|
+
messageId: ref.messageId,
|
|
8286
|
+
emoji: operation.emoji,
|
|
8287
|
+
}, this.discordAwarenessDeadlineMs);
|
|
8288
|
+
if (result.isError) {
|
|
8289
|
+
deliveryError = result.content
|
|
8290
|
+
.map((content) => content.text ?? '')
|
|
8291
|
+
.filter(Boolean)
|
|
8292
|
+
.join('; ') || `Discord ${tool} returned an error`;
|
|
8293
|
+
}
|
|
8294
|
+
} catch (error) {
|
|
8295
|
+
deliveryError = error instanceof Error ? error.message : String(error);
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
if (deliveryError !== undefined) {
|
|
8299
|
+
const permanent = isPermanentDiscordReactionFailure(deliveryError);
|
|
8300
|
+
this.writeDiscordAwarenessFailure(
|
|
8301
|
+
operation.batchId,
|
|
8302
|
+
ref,
|
|
8303
|
+
operation.action,
|
|
8304
|
+
deliveryError,
|
|
8305
|
+
permanent,
|
|
8306
|
+
);
|
|
8307
|
+
failed++;
|
|
8308
|
+
console.error(
|
|
8309
|
+
`[discord-awareness] ${operation.action} failed for ${ref.channelId}/${ref.messageId}` +
|
|
8310
|
+
` (${permanent ? 'permanent' : 'retryable'}): ${deliveryError}`,
|
|
8311
|
+
);
|
|
8312
|
+
} else {
|
|
8313
|
+
this.writeDiscordAwarenessSuccess(operation.batchId, ref, operation.action);
|
|
8314
|
+
delivered++;
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
if (delivered > 0 || failed > 0) {
|
|
8319
|
+
console.error(
|
|
8320
|
+
`[discord-awareness] ${serverId}: delivered=${delivered} failed=${failed}`,
|
|
8321
|
+
);
|
|
8322
|
+
}
|
|
8323
|
+
return { status: 'delivered' as const, delivered, failed };
|
|
8324
|
+
})().finally(() => {
|
|
8325
|
+
this.discordAwarenessDrains.delete(serverId);
|
|
8326
|
+
});
|
|
8327
|
+
|
|
8328
|
+
this.discordAwarenessDrains.set(serverId, drain);
|
|
8329
|
+
return drain;
|
|
8330
|
+
}
|
|
8331
|
+
|
|
8332
|
+
private readDiscordAwarenessPending(serverId?: string) {
|
|
8333
|
+
try {
|
|
8334
|
+
return this.discordAwarenessOutbox!.pending(serverId);
|
|
8335
|
+
} catch (error) {
|
|
8336
|
+
throw new DiscordAwarenessAccountingError(
|
|
8337
|
+
`ledger read${serverId ? ` for ${serverId}` : ''}`,
|
|
8338
|
+
error,
|
|
8339
|
+
);
|
|
8340
|
+
}
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
private writeDiscordAwarenessSuccess(
|
|
8344
|
+
batchId: string,
|
|
8345
|
+
ref: import('./recovery/discord-awareness-outbox.js').DiscordAwarenessRef,
|
|
8346
|
+
action: import('./recovery/discord-awareness-outbox.js').DiscordAwarenessAction,
|
|
8347
|
+
): void {
|
|
8348
|
+
try {
|
|
8349
|
+
this.discordAwarenessOutbox!.recordSuccess(batchId, ref, action);
|
|
8350
|
+
} catch (error) {
|
|
8351
|
+
throw new DiscordAwarenessAccountingError('recordSuccess ledger write', error);
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
|
|
8355
|
+
private writeDiscordAwarenessFailure(
|
|
8356
|
+
batchId: string,
|
|
8357
|
+
ref: import('./recovery/discord-awareness-outbox.js').DiscordAwarenessRef,
|
|
8358
|
+
action: import('./recovery/discord-awareness-outbox.js').DiscordAwarenessAction,
|
|
8359
|
+
errorMessage: string,
|
|
8360
|
+
permanent: boolean,
|
|
8361
|
+
): void {
|
|
8362
|
+
try {
|
|
8363
|
+
this.discordAwarenessOutbox!.recordFailure(
|
|
8364
|
+
batchId,
|
|
8365
|
+
ref,
|
|
8366
|
+
action,
|
|
8367
|
+
errorMessage,
|
|
8368
|
+
permanent,
|
|
8369
|
+
);
|
|
8370
|
+
} catch (error) {
|
|
8371
|
+
throw new DiscordAwarenessAccountingError('recordFailure ledger write', error);
|
|
8372
|
+
}
|
|
8373
|
+
}
|
|
8374
|
+
|
|
8375
|
+
private beginDiscordAwarenessBarrier(): {
|
|
8376
|
+
requiresBarrier: boolean;
|
|
8377
|
+
promise: Promise<DiscordAwarenessDrainOutcome>;
|
|
8378
|
+
} {
|
|
8379
|
+
if (!this.discordAwarenessOutbox) {
|
|
8380
|
+
return {
|
|
8381
|
+
requiresBarrier: false,
|
|
8382
|
+
promise: Promise.resolve({ status: 'delivered', delivered: 0, failed: 0 }),
|
|
8383
|
+
};
|
|
8384
|
+
}
|
|
8385
|
+
try {
|
|
8386
|
+
this.discordAwarenessOutbox.reconcileForBranch(
|
|
8387
|
+
this.store.currentBranch().name,
|
|
8388
|
+
this.store.listBranches(),
|
|
8389
|
+
);
|
|
8390
|
+
} catch (error) {
|
|
8391
|
+
throw new DiscordAwarenessAccountingError('branch reconciliation', error);
|
|
8392
|
+
}
|
|
8393
|
+
const pending = this.readDiscordAwarenessPending();
|
|
8394
|
+
if (pending.length === 0) {
|
|
8395
|
+
return {
|
|
8396
|
+
requiresBarrier: false,
|
|
8397
|
+
promise: Promise.resolve({ status: 'delivered', delivered: 0, failed: 0 }),
|
|
8398
|
+
};
|
|
8399
|
+
}
|
|
8400
|
+
const serverIds = [...new Set(pending.map((operation) => operation.ref.serverId))];
|
|
8401
|
+
const promise = Promise.all(
|
|
8402
|
+
serverIds.map((serverId) => this.drainDiscordAwarenessOutbox(serverId)),
|
|
8403
|
+
).then((outcomes): DiscordAwarenessDrainOutcome => {
|
|
8404
|
+
if (outcomes.every((outcome) => outcome.status === 'unavailable')) {
|
|
8405
|
+
return {
|
|
8406
|
+
status: 'unavailable',
|
|
8407
|
+
accounted: outcomes.reduce(
|
|
8408
|
+
(count, outcome) => count + (outcome.status === 'unavailable' ? outcome.accounted : 0),
|
|
8409
|
+
0,
|
|
8410
|
+
),
|
|
8411
|
+
};
|
|
8412
|
+
}
|
|
8413
|
+
return {
|
|
8414
|
+
status: 'delivered',
|
|
8415
|
+
delivered: outcomes.reduce(
|
|
8416
|
+
(count, outcome) => count + (outcome.status === 'delivered' ? outcome.delivered : 0),
|
|
8417
|
+
0,
|
|
8418
|
+
),
|
|
8419
|
+
failed: outcomes.reduce(
|
|
8420
|
+
(count, outcome) => count + (outcome.status === 'delivered' ? outcome.failed : 0),
|
|
8421
|
+
0,
|
|
8422
|
+
),
|
|
8423
|
+
};
|
|
8424
|
+
});
|
|
8425
|
+
return { requiresBarrier: true, promise };
|
|
8426
|
+
}
|
|
8427
|
+
|
|
8428
|
+
/**
|
|
8429
|
+
* Install one framework-global Discord-awareness generation before releasing
|
|
8430
|
+
* any inference-bearing event. With pending work, every MCPL data plane is
|
|
8431
|
+
* paused while all control planes remain live. With no pending work, ready()
|
|
8432
|
+
* runs synchronously so request responders preserve their historical
|
|
8433
|
+
* same-stack behavior.
|
|
8434
|
+
*/
|
|
8435
|
+
private installMcplDataPlaneGate(): DiscordAwarenessBarrier {
|
|
8436
|
+
for (const connection of this.mcplServerRegistry?.getAllServers() ?? []) {
|
|
8437
|
+
connection.pauseDataPlane();
|
|
8438
|
+
}
|
|
8439
|
+
const generation = ++this.discordAwarenessBarrierGeneration;
|
|
8440
|
+
|
|
8441
|
+
let begun: ReturnType<AgentFramework['beginDiscordAwarenessBarrier']>;
|
|
8442
|
+
try {
|
|
8443
|
+
begun = this.beginDiscordAwarenessBarrier();
|
|
8444
|
+
} catch (error) {
|
|
8445
|
+
begun = {
|
|
8446
|
+
requiresBarrier: true,
|
|
8447
|
+
promise: Promise.reject(error),
|
|
8448
|
+
};
|
|
8449
|
+
}
|
|
8450
|
+
const barrier: DiscordAwarenessBarrier = { generation, ...begun };
|
|
8451
|
+
this.discordAwarenessBarrier = barrier;
|
|
8452
|
+
return barrier;
|
|
8453
|
+
}
|
|
8454
|
+
|
|
8455
|
+
private releaseMcplDataPlaneGate(
|
|
8456
|
+
barrier: DiscordAwarenessBarrier,
|
|
8457
|
+
): void {
|
|
8458
|
+
if (this.discordAwarenessBarrier !== barrier) return;
|
|
8459
|
+
if (barrier.requiresBarrier) {
|
|
8460
|
+
for (const connection of this.mcplServerRegistry?.getAllServers() ?? []) {
|
|
8461
|
+
connection.readyControlPlane();
|
|
8462
|
+
}
|
|
8463
|
+
return;
|
|
8464
|
+
}
|
|
8465
|
+
this.completeMcplDataPlaneGate(barrier);
|
|
8466
|
+
}
|
|
8467
|
+
|
|
8468
|
+
private completeMcplDataPlaneGate(
|
|
8469
|
+
barrier: DiscordAwarenessBarrier,
|
|
8470
|
+
): boolean {
|
|
8471
|
+
if (this.discordAwarenessBarrier !== barrier) return false;
|
|
8472
|
+
this.discordAwarenessBarrier = null;
|
|
8473
|
+
for (const connection of this.mcplServerRegistry?.getAllServers() ?? []) {
|
|
8474
|
+
// ready() can synchronously flush a nested list-change notification that
|
|
8475
|
+
// installs a newer global generation. Never let this older completion
|
|
8476
|
+
// release any remaining server behind that newer gate.
|
|
8477
|
+
if (this.discordAwarenessBarrier !== null) return false;
|
|
8478
|
+
connection.ready();
|
|
8479
|
+
}
|
|
8480
|
+
return this.discordAwarenessBarrier === null;
|
|
8481
|
+
}
|
|
8482
|
+
|
|
8483
|
+
private async failMcplDataPlaneGate(
|
|
8484
|
+
barrier: DiscordAwarenessBarrier,
|
|
8485
|
+
context: string,
|
|
8486
|
+
error: unknown,
|
|
8487
|
+
): Promise<void> {
|
|
8488
|
+
if (this.discordAwarenessBarrier !== barrier) return;
|
|
8489
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
8490
|
+
console.error(`[discord-awareness] ${context} failed globally:`, err.message);
|
|
8491
|
+
const connections = this.mcplServerRegistry?.getAllServers() ?? [];
|
|
8492
|
+
for (const connection of connections) {
|
|
8493
|
+
this.emitTrace({
|
|
8494
|
+
type: 'mcpl:server-error',
|
|
8495
|
+
serverId: connection.id,
|
|
8496
|
+
error: `Discord awareness accounting unhealthy: ${err.message}`,
|
|
8497
|
+
});
|
|
8498
|
+
}
|
|
8499
|
+
await Promise.all(connections.map(async (connection) => {
|
|
8500
|
+
await connection.reconnectAfterFailure().catch((closeError) => {
|
|
8501
|
+
console.error(
|
|
8502
|
+
`[discord-awareness] could not recycle unhealthy connection ${connection.id}:`,
|
|
8503
|
+
closeError instanceof Error ? closeError.message : closeError,
|
|
8504
|
+
);
|
|
8505
|
+
});
|
|
8506
|
+
}));
|
|
8507
|
+
}
|
|
8508
|
+
|
|
5381
8509
|
/**
|
|
5382
8510
|
* Connect a single MCPL server: register routing entries, open the
|
|
5383
8511
|
* connection, wire events, and initialize feature sets / scopes /
|
|
@@ -5386,6 +8514,7 @@ export class AgentFramework {
|
|
|
5386
8514
|
*/
|
|
5387
8515
|
private async connectMcplServerInternal(
|
|
5388
8516
|
config: import('./mcpl/types.js').McplServerConfig,
|
|
8517
|
+
deferAwareness = false,
|
|
5389
8518
|
): Promise<void> {
|
|
5390
8519
|
if (!this.mcplServerRegistry || !this.mcplHostCapabilities) {
|
|
5391
8520
|
throw new Error('MCPL subsystem is not initialized');
|
|
@@ -5408,14 +8537,31 @@ export class AgentFramework {
|
|
|
5408
8537
|
|
|
5409
8538
|
const connection = await this.mcplServerRegistry.addServer(config, this.mcplHostCapabilities);
|
|
5410
8539
|
|
|
5411
|
-
// Wire
|
|
5412
|
-
//
|
|
8540
|
+
// Wire listeners before either startup staging or the runtime global gate
|
|
8541
|
+
// releases control traffic needed for registration and marker service.
|
|
5413
8542
|
this.wireMcplEvents(connection);
|
|
5414
|
-
connection.ready();
|
|
5415
8543
|
|
|
5416
8544
|
// Initialize feature sets if server advertises MCPL capabilities
|
|
5417
8545
|
this.registerMcplServerFeatures(config, connection);
|
|
5418
8546
|
|
|
8547
|
+
if (deferAwareness) {
|
|
8548
|
+
this.emitTrace({ type: 'module:added', moduleName: `mcpl:${config.id}` });
|
|
8549
|
+
return;
|
|
8550
|
+
}
|
|
8551
|
+
|
|
8552
|
+
const awarenessBarrier = this.installMcplDataPlaneGate();
|
|
8553
|
+
this.releaseMcplDataPlaneGate(awarenessBarrier);
|
|
8554
|
+
try {
|
|
8555
|
+
await awarenessBarrier.promise;
|
|
8556
|
+
this.completeMcplDataPlaneGate(awarenessBarrier);
|
|
8557
|
+
} catch (error) {
|
|
8558
|
+
// Startup cannot fail open on a broken awareness ledger. Disable the
|
|
8559
|
+
// reconnecting stub/connection before propagating the distinct error to
|
|
8560
|
+
// initializeMcpl, which tears down any other servers and aborts create().
|
|
8561
|
+
await connection.close().catch(() => {});
|
|
8562
|
+
throw error;
|
|
8563
|
+
}
|
|
8564
|
+
|
|
5419
8565
|
this.emitTrace({ type: 'module:added', moduleName: `mcpl:${config.id}` });
|
|
5420
8566
|
}
|
|
5421
8567
|
|
|
@@ -5629,10 +8775,12 @@ export class AgentFramework {
|
|
|
5629
8775
|
});
|
|
5630
8776
|
|
|
5631
8777
|
// Handle push events (Step 6)
|
|
5632
|
-
connection.on('push-event', (
|
|
8778
|
+
connection.on('push-event', async (
|
|
5633
8779
|
params: PushEventParams,
|
|
5634
8780
|
responder?: { respond: (result: unknown) => void; respondError: (code: number, message: string) => void },
|
|
5635
8781
|
) => {
|
|
8782
|
+
const barrier = this.discordAwarenessBarrier;
|
|
8783
|
+
if (barrier) await barrier.promise;
|
|
5636
8784
|
this.pushHandler?.handlePushEvent(connection.id, params, responder as never);
|
|
5637
8785
|
});
|
|
5638
8786
|
|
|
@@ -5642,6 +8790,8 @@ export class AgentFramework {
|
|
|
5642
8790
|
responder?: { id: string | number; respond: (result: unknown) => void; respondError: (code: number, message: string) => void },
|
|
5643
8791
|
) => {
|
|
5644
8792
|
if (this.inferenceRouter && responder) {
|
|
8793
|
+
const barrier = this.discordAwarenessBarrier;
|
|
8794
|
+
if (barrier) await barrier.promise;
|
|
5645
8795
|
await this.inferenceRouter.handleInferenceRequest(connection.id, params, {
|
|
5646
8796
|
respond: responder.respond,
|
|
5647
8797
|
respondError: responder.respondError,
|
|
@@ -5664,10 +8814,12 @@ export class AgentFramework {
|
|
|
5664
8814
|
});
|
|
5665
8815
|
|
|
5666
8816
|
// Handle incoming channel messages (Step 7)
|
|
5667
|
-
connection.on('channels-incoming', (
|
|
8817
|
+
connection.on('channels-incoming', async (
|
|
5668
8818
|
params: ChannelsIncomingParams,
|
|
5669
8819
|
responder?: { respond: (result: unknown) => void },
|
|
5670
8820
|
) => {
|
|
8821
|
+
const barrier = this.discordAwarenessBarrier;
|
|
8822
|
+
if (barrier) await barrier.promise;
|
|
5671
8823
|
this.channelRegistry?.handleIncoming(connection.id, params, responder as never);
|
|
5672
8824
|
});
|
|
5673
8825
|
|
|
@@ -5678,6 +8830,8 @@ export class AgentFramework {
|
|
|
5678
8830
|
) => {
|
|
5679
8831
|
if (!responder) return;
|
|
5680
8832
|
try {
|
|
8833
|
+
const barrier = this.discordAwarenessBarrier;
|
|
8834
|
+
if (barrier) await barrier.promise;
|
|
5681
8835
|
const result = await this.handleHostCommand(connection.id, params ?? {});
|
|
5682
8836
|
responder.respond(result);
|
|
5683
8837
|
} catch (error) {
|
|
@@ -5688,7 +8842,19 @@ export class AgentFramework {
|
|
|
5688
8842
|
|
|
5689
8843
|
// Handle dynamic tool list changes (notifications/tools/list_changed)
|
|
5690
8844
|
connection.on('tools-list-changed', () => {
|
|
8845
|
+
// Pause before starting any async refresh. This listener runs
|
|
8846
|
+
// synchronously in the transport line callback, so a following inbound
|
|
8847
|
+
// data event cannot pass before the new barrier exists.
|
|
8848
|
+
const awarenessBarrier = this.installMcplDataPlaneGate();
|
|
8849
|
+
this.releaseMcplDataPlaneGate(awarenessBarrier);
|
|
5691
8850
|
this.handleToolsListChanged(connection.id);
|
|
8851
|
+
void awarenessBarrier.promise.then(() => {
|
|
8852
|
+
this.completeMcplDataPlaneGate(awarenessBarrier);
|
|
8853
|
+
}).catch((error) => this.failMcplDataPlaneGate(
|
|
8854
|
+
awarenessBarrier,
|
|
8855
|
+
'tools-list reconciliation',
|
|
8856
|
+
error,
|
|
8857
|
+
));
|
|
5692
8858
|
});
|
|
5693
8859
|
|
|
5694
8860
|
// Re-establish full server registration on reconnect. The 'close' handler
|
|
@@ -5699,6 +8865,10 @@ export class AgentFramework {
|
|
|
5699
8865
|
// preserved across the transient close are resumed by idempotent
|
|
5700
8866
|
// registration. Then refresh tools (server may have different tools).
|
|
5701
8867
|
connection.on('reconnect', (info?: { attempts?: number }) => {
|
|
8868
|
+
// Install the barrier synchronously so any inbound event emitted after
|
|
8869
|
+
// reconnect observes it before doing work that could wake an agent. The
|
|
8870
|
+
// connection paused its data plane before wiring the fresh transport.
|
|
8871
|
+
const awarenessBarrier = this.installMcplDataPlaneGate();
|
|
5702
8872
|
try {
|
|
5703
8873
|
const config = this.mcplServerConfigs.get(connection.id);
|
|
5704
8874
|
if (config) {
|
|
@@ -5710,15 +8880,26 @@ export class AgentFramework {
|
|
|
5710
8880
|
error instanceof Error ? error.message : error,
|
|
5711
8881
|
);
|
|
5712
8882
|
}
|
|
8883
|
+
this.releaseMcplDataPlaneGate(awarenessBarrier);
|
|
5713
8884
|
this.handleToolsListChanged(connection.id);
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
8885
|
+
void awarenessBarrier.promise.then(() => {
|
|
8886
|
+
if (
|
|
8887
|
+
awarenessBarrier.requiresBarrier
|
|
8888
|
+
&& !this.completeMcplDataPlaneGate(awarenessBarrier)
|
|
8889
|
+
) return;
|
|
8890
|
+
this.emitTrace({
|
|
8891
|
+
type: 'mcpl:server-reconnected',
|
|
8892
|
+
serverId: connection.id,
|
|
8893
|
+
attempts: info?.attempts ?? 0,
|
|
8894
|
+
});
|
|
8895
|
+
// Mirror the module:removed emitted on 'close' so module-lifecycle
|
|
8896
|
+
// consumers see the server come back, not just vanish.
|
|
8897
|
+
this.emitTrace({ type: 'module:added', moduleName: `mcpl:${connection.id}` });
|
|
8898
|
+
}).catch((error) => this.failMcplDataPlaneGate(
|
|
8899
|
+
awarenessBarrier,
|
|
8900
|
+
'reconnect reconciliation',
|
|
8901
|
+
error,
|
|
8902
|
+
));
|
|
5722
8903
|
});
|
|
5723
8904
|
|
|
5724
8905
|
// Surface connect/reconnect failures. Before these traces existed the
|
|
@@ -5760,6 +8941,7 @@ export class AgentFramework {
|
|
|
5760
8941
|
// divergence greppable instead of a silent drift into stale state.
|
|
5761
8942
|
connection.on('orphaned-response', (info: {
|
|
5762
8943
|
id: string | number;
|
|
8944
|
+
method?: string;
|
|
5763
8945
|
hadState: boolean;
|
|
5764
8946
|
hadCheckpoint: boolean;
|
|
5765
8947
|
}) => {
|
|
@@ -5767,6 +8949,7 @@ export class AgentFramework {
|
|
|
5767
8949
|
type: 'mcpl:orphaned-response',
|
|
5768
8950
|
serverId: connection.id,
|
|
5769
8951
|
responseId: info.id,
|
|
8952
|
+
method: info.method,
|
|
5770
8953
|
hadState: info.hadState,
|
|
5771
8954
|
hadCheckpoint: info.hadCheckpoint,
|
|
5772
8955
|
});
|
|
@@ -6228,6 +9411,153 @@ export class AgentFramework {
|
|
|
6228
9411
|
});
|
|
6229
9412
|
}
|
|
6230
9413
|
|
|
9414
|
+
/**
|
|
9415
|
+
* Handle the synthesized `save_recent_image` tool: locate the requested
|
|
9416
|
+
* image blocks in the calling agent's context (blobs re-inlined, counted
|
|
9417
|
+
* back from the most recent) and write their bytes to a workspace mount
|
|
9418
|
+
* via WorkspaceModule.writeBinary.
|
|
9419
|
+
*/
|
|
9420
|
+
private dispatchSaveImageToolCall(agentName: string, call: ToolCall): void {
|
|
9421
|
+
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
9422
|
+
const finish = (result: ToolResult): void => {
|
|
9423
|
+
this.emitTrace({
|
|
9424
|
+
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
9425
|
+
module: 'workspace',
|
|
9426
|
+
tool: call.name,
|
|
9427
|
+
callId: call.id,
|
|
9428
|
+
durationMs: 0,
|
|
9429
|
+
...(result.isError ? { error: result.error } : {}),
|
|
9430
|
+
});
|
|
9431
|
+
this.pushEvent({ type: 'tool-result', callId: call.id, agentName, moduleName: 'workspace', result });
|
|
9432
|
+
};
|
|
9433
|
+
void (async (): Promise<void> => {
|
|
9434
|
+
try {
|
|
9435
|
+
const workspace = this.getWorkspaceModule();
|
|
9436
|
+
if (!workspace) throw new Error('save_recent_image requires a workspace module');
|
|
9437
|
+
const agent = this.agents.get(agentName);
|
|
9438
|
+
if (!agent) throw new Error(`Unknown agent: ${agentName}`);
|
|
9439
|
+
const input = (call.input ?? {}) as { path?: unknown; index?: unknown; count?: unknown };
|
|
9440
|
+
if (typeof input.path !== 'string' || input.path.length === 0) {
|
|
9441
|
+
throw new Error('save_recent_image: `path` (mount-prefixed) is required');
|
|
9442
|
+
}
|
|
9443
|
+
const index = input.index === undefined ? 0 : Number(input.index);
|
|
9444
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
9445
|
+
throw new Error('save_recent_image: `index` must be a non-negative integer');
|
|
9446
|
+
}
|
|
9447
|
+
const count = input.count === undefined ? 1 : Number(input.count);
|
|
9448
|
+
const MAX_COUNT = 20;
|
|
9449
|
+
if (!Number.isInteger(count) || count < 1 || count > MAX_COUNT) {
|
|
9450
|
+
throw new Error(`save_recent_image: \`count\` must be an integer in 1..${MAX_COUNT}`);
|
|
9451
|
+
}
|
|
9452
|
+
const lastWanted = index + count - 1;
|
|
9453
|
+
|
|
9454
|
+
// Walk the message store tail-first in bounded windows, re-inlining
|
|
9455
|
+
// blob media, until we've collected the requested range. Scanning is
|
|
9456
|
+
// capped so a pathological index can't drag the whole store through
|
|
9457
|
+
// blob resolution.
|
|
9458
|
+
const MAX_SCAN = 500;
|
|
9459
|
+
const WINDOW = 25;
|
|
9460
|
+
const cm = agent.getContextManager();
|
|
9461
|
+
const total = cm.getMessageCount();
|
|
9462
|
+
let seen = 0;
|
|
9463
|
+
let scanned = 0;
|
|
9464
|
+
const found: Array<{
|
|
9465
|
+
rangeOffset: number;
|
|
9466
|
+
data: string;
|
|
9467
|
+
mediaType: string;
|
|
9468
|
+
messagesBack: number;
|
|
9469
|
+
}> = [];
|
|
9470
|
+
for (let end = total; end > 0 && scanned < MAX_SCAN && found.length < count; end -= WINDOW) {
|
|
9471
|
+
const start = Math.max(0, end - WINDOW);
|
|
9472
|
+
const { messages } = cm.getMessageWindow(start, end - start, { resolveBlobs: true });
|
|
9473
|
+
scanned += end - start;
|
|
9474
|
+
for (let i = messages.length - 1; i >= 0 && found.length < count; i--) {
|
|
9475
|
+
const content = messages[i]?.content;
|
|
9476
|
+
if (!Array.isArray(content)) continue;
|
|
9477
|
+
for (let b = content.length - 1; b >= 0 && found.length < count; b--) {
|
|
9478
|
+
const block = content[b] as {
|
|
9479
|
+
type?: string;
|
|
9480
|
+
source?: { type?: string; data?: string; mediaType?: string };
|
|
9481
|
+
};
|
|
9482
|
+
if (block?.type !== 'image') continue;
|
|
9483
|
+
if (seen >= index && seen <= lastWanted) {
|
|
9484
|
+
if (block.source?.type !== 'base64' || typeof block.source.data !== 'string') {
|
|
9485
|
+
throw new Error(
|
|
9486
|
+
`save_recent_image: image at index ${seen} is not stored inline (base64) — cannot save it`,
|
|
9487
|
+
);
|
|
9488
|
+
}
|
|
9489
|
+
found.push({
|
|
9490
|
+
rangeOffset: seen - index,
|
|
9491
|
+
data: block.source.data,
|
|
9492
|
+
mediaType: block.source.mediaType ?? 'image/png',
|
|
9493
|
+
messagesBack: total - (start + i),
|
|
9494
|
+
});
|
|
9495
|
+
}
|
|
9496
|
+
seen++;
|
|
9497
|
+
}
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
if (found.length === 0) {
|
|
9501
|
+
throw new Error(
|
|
9502
|
+
seen === 0
|
|
9503
|
+
? `save_recent_image: no images found in the most recent ${Math.min(scanned, MAX_SCAN)} messages`
|
|
9504
|
+
: `save_recent_image: only ${seen} image(s) found in the most recent ${Math.min(scanned, MAX_SCAN)} messages (asked for index ${index})`,
|
|
9505
|
+
);
|
|
9506
|
+
}
|
|
9507
|
+
|
|
9508
|
+
// Single image → path as given. Range → numeric suffix before the
|
|
9509
|
+
// extension ("cat.png" → "cat-0.png"), 0 = newest of the range.
|
|
9510
|
+
const pathFor = (rangeOffset: number): string => {
|
|
9511
|
+
if (count === 1) return input.path as string;
|
|
9512
|
+
const p = input.path as string;
|
|
9513
|
+
const dot = p.lastIndexOf('.');
|
|
9514
|
+
const slash = p.lastIndexOf('/');
|
|
9515
|
+
return dot > slash
|
|
9516
|
+
? `${p.slice(0, dot)}-${rangeOffset}${p.slice(dot)}`
|
|
9517
|
+
: `${p}-${rangeOffset}`;
|
|
9518
|
+
};
|
|
9519
|
+
const savedFiles: Array<Record<string, unknown>> = [];
|
|
9520
|
+
for (const image of found) {
|
|
9521
|
+
const destination = pathFor(image.rangeOffset);
|
|
9522
|
+
const bytes = Buffer.from(image.data, 'base64');
|
|
9523
|
+
const result = await workspace.writeBinary(destination, bytes, image.mediaType);
|
|
9524
|
+
if (!result.success) {
|
|
9525
|
+
finish({
|
|
9526
|
+
success: false,
|
|
9527
|
+
error:
|
|
9528
|
+
`save_recent_image: failed writing "${destination}": ${result.error}` +
|
|
9529
|
+
(savedFiles.length > 0
|
|
9530
|
+
? ` (already saved: ${savedFiles.map((f) => f.path).join(', ')})`
|
|
9531
|
+
: ''),
|
|
9532
|
+
isError: true,
|
|
9533
|
+
});
|
|
9534
|
+
return;
|
|
9535
|
+
}
|
|
9536
|
+
savedFiles.push({
|
|
9537
|
+
...(result.data as Record<string, unknown>),
|
|
9538
|
+
imageIndex: index + image.rangeOffset,
|
|
9539
|
+
messagesBack: image.messagesBack,
|
|
9540
|
+
});
|
|
9541
|
+
}
|
|
9542
|
+
finish({
|
|
9543
|
+
success: true,
|
|
9544
|
+
data: {
|
|
9545
|
+
saved: savedFiles,
|
|
9546
|
+
...(found.length < count
|
|
9547
|
+
? { note: `only ${found.length} of ${count} requested images exist in the scanned window` }
|
|
9548
|
+
: {}),
|
|
9549
|
+
},
|
|
9550
|
+
});
|
|
9551
|
+
} catch (error) {
|
|
9552
|
+
finish({
|
|
9553
|
+
success: false,
|
|
9554
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9555
|
+
isError: true,
|
|
9556
|
+
});
|
|
9557
|
+
}
|
|
9558
|
+
})();
|
|
9559
|
+
}
|
|
9560
|
+
|
|
6231
9561
|
private dispatchAgentSettingsToolCall(agentName: string, call: ToolCall): void {
|
|
6232
9562
|
this.emitTrace({
|
|
6233
9563
|
type: 'tool:started',
|
|
@@ -6243,11 +9573,22 @@ export class AgentFramework {
|
|
|
6243
9573
|
context_budget_tokens?: unknown;
|
|
6244
9574
|
tail_tokens?: unknown;
|
|
6245
9575
|
transition_pace_tokens?: unknown;
|
|
9576
|
+
same_round_think_text_policy?: unknown;
|
|
6246
9577
|
settings?: unknown;
|
|
9578
|
+
} & Record<string, unknown>;
|
|
9579
|
+
const extensions = this.collectAgentSettingsExtensions();
|
|
9580
|
+
/** Extension values merged flat alongside the core snapshot. */
|
|
9581
|
+
const extGet = (): Record<string, unknown> => {
|
|
9582
|
+
const out: Record<string, unknown> = {};
|
|
9583
|
+
for (const ext of extensions.values()) Object.assign(out, ext.get(agentName));
|
|
9584
|
+
return out;
|
|
6247
9585
|
};
|
|
6248
9586
|
switch (input.action) {
|
|
6249
9587
|
case 'get':
|
|
6250
|
-
result = {
|
|
9588
|
+
result = {
|
|
9589
|
+
success: true,
|
|
9590
|
+
data: { ...this.getAgentRuntimeSettings(agentName), ...extGet() },
|
|
9591
|
+
};
|
|
6251
9592
|
break;
|
|
6252
9593
|
case 'cancel':
|
|
6253
9594
|
result = { success: true, data: this.cancelAgentRuntimeSettingsTransition(agentName) };
|
|
@@ -6261,26 +9602,101 @@ export class AgentFramework {
|
|
|
6261
9602
|
if (input.transition_pace_tokens !== undefined) {
|
|
6262
9603
|
patch.transitionPaceTokens = Number(input.transition_pace_tokens);
|
|
6263
9604
|
}
|
|
6264
|
-
|
|
9605
|
+
if (input.same_round_think_text_policy !== undefined) {
|
|
9606
|
+
patch.sameRoundThinkTextPolicy = input.same_round_think_text_policy as AgentRuntimeSettingsPatch['sameRoundThinkTextPolicy'];
|
|
9607
|
+
}
|
|
9608
|
+
// Route extension-owned keys to their modules; apply the core patch
|
|
9609
|
+
// only when it touches core keys (an extension-only update must not
|
|
9610
|
+
// disturb a converging budget transition).
|
|
9611
|
+
const extResults: Record<string, unknown> = {};
|
|
9612
|
+
for (const ext of extensions.values()) {
|
|
9613
|
+
const slice: Record<string, unknown> = {};
|
|
9614
|
+
for (const key of ext.keys) {
|
|
9615
|
+
if (input[key] !== undefined) slice[key] = input[key];
|
|
9616
|
+
}
|
|
9617
|
+
if (Object.keys(slice).length > 0) {
|
|
9618
|
+
Object.assign(extResults, ext.update(agentName, slice));
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9621
|
+
const coreTouched = Object.keys(patch).length > 0;
|
|
9622
|
+
const core = coreTouched
|
|
9623
|
+
? this.updateAgentRuntimeSettings(agentName, patch)
|
|
9624
|
+
: this.getAgentRuntimeSettings(agentName);
|
|
9625
|
+
result = {
|
|
9626
|
+
success: true,
|
|
9627
|
+
data: {
|
|
9628
|
+
...core,
|
|
9629
|
+
...extGet(),
|
|
9630
|
+
...extResults,
|
|
9631
|
+
...(patch.sameRoundThinkTextPolicy !== undefined
|
|
9632
|
+
? {
|
|
9633
|
+
sameRoundThinkTextPolicyUpdateNote:
|
|
9634
|
+
'Stored now; applies to provider think routing and description beginning with the next inference.',
|
|
9635
|
+
}
|
|
9636
|
+
: {}),
|
|
9637
|
+
},
|
|
9638
|
+
};
|
|
6265
9639
|
break;
|
|
6266
9640
|
}
|
|
6267
9641
|
case 'reset': {
|
|
6268
9642
|
let keys: Array<keyof AgentRuntimeSettingsPatch> | undefined;
|
|
9643
|
+
const extResetKeys = new Map<AgentSettingsExtension, string[]>();
|
|
9644
|
+
const resetsAllSettings = input.settings === undefined;
|
|
6269
9645
|
if (input.settings !== undefined) {
|
|
6270
9646
|
if (!Array.isArray(input.settings)) throw new Error('reset `settings` must be an array');
|
|
6271
9647
|
const names: Record<string, keyof AgentRuntimeSettingsPatch> = {
|
|
6272
9648
|
context_budget_tokens: 'contextBudgetTokens',
|
|
6273
9649
|
tail_tokens: 'tailTokens',
|
|
6274
9650
|
transition_pace_tokens: 'transitionPaceTokens',
|
|
9651
|
+
same_round_think_text_policy: 'sameRoundThinkTextPolicy',
|
|
6275
9652
|
};
|
|
6276
|
-
keys =
|
|
6277
|
-
|
|
6278
|
-
|
|
9653
|
+
keys = [];
|
|
9654
|
+
for (const name of input.settings) {
|
|
9655
|
+
if (typeof name === 'string' && names[name]) {
|
|
9656
|
+
keys.push(names[name]);
|
|
9657
|
+
continue;
|
|
6279
9658
|
}
|
|
6280
|
-
|
|
6281
|
-
|
|
9659
|
+
const owner = [...extensions.values()].find(
|
|
9660
|
+
(ext) => typeof name === 'string' && ext.keys.includes(name),
|
|
9661
|
+
);
|
|
9662
|
+
if (!owner) throw new Error(`Unknown reset setting: ${String(name)}`);
|
|
9663
|
+
const list = extResetKeys.get(owner) ?? [];
|
|
9664
|
+
list.push(name as string);
|
|
9665
|
+
extResetKeys.set(owner, list);
|
|
9666
|
+
}
|
|
9667
|
+
if (keys.length === 0) keys = undefined;
|
|
9668
|
+
}
|
|
9669
|
+
const touchedSameRoundThinkTextPolicy =
|
|
9670
|
+
resetsAllSettings || keys?.includes('sameRoundThinkTextPolicy') === true;
|
|
9671
|
+
const extResults: Record<string, unknown> = {};
|
|
9672
|
+
if (input.settings === undefined) {
|
|
9673
|
+
// Reset-all covers extensions too.
|
|
9674
|
+
for (const ext of extensions.values()) {
|
|
9675
|
+
if (ext.reset) Object.assign(extResults, ext.reset(agentName));
|
|
9676
|
+
}
|
|
9677
|
+
} else {
|
|
9678
|
+
for (const [ext, list] of extResetKeys) {
|
|
9679
|
+
if (ext.reset) Object.assign(extResults, ext.reset(agentName, list));
|
|
9680
|
+
}
|
|
6282
9681
|
}
|
|
6283
|
-
|
|
9682
|
+
const coreTouched = input.settings === undefined || keys !== undefined;
|
|
9683
|
+
const core = coreTouched
|
|
9684
|
+
? this.resetAgentRuntimeSettings(agentName, keys)
|
|
9685
|
+
: this.getAgentRuntimeSettings(agentName);
|
|
9686
|
+
result = {
|
|
9687
|
+
success: true,
|
|
9688
|
+
data: {
|
|
9689
|
+
...core,
|
|
9690
|
+
...extGet(),
|
|
9691
|
+
...extResults,
|
|
9692
|
+
...(touchedSameRoundThinkTextPolicy
|
|
9693
|
+
? {
|
|
9694
|
+
sameRoundThinkTextPolicyUpdateNote:
|
|
9695
|
+
'Reset now; restored provider think routing and description apply beginning with the next inference.',
|
|
9696
|
+
}
|
|
9697
|
+
: {}),
|
|
9698
|
+
},
|
|
9699
|
+
};
|
|
6284
9700
|
break;
|
|
6285
9701
|
}
|
|
6286
9702
|
default:
|