newmark-agent 0.5.14 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversation-utility-host.bundle.cjs +4565 -2998
- package/dist/conversation-utility-host.js +1 -1
- package/dist/core/agent.d.ts +108 -16
- package/dist/core/agent.js +698 -195
- package/dist/core/agentKernel/agent.d.ts +1 -0
- package/dist/core/agentKernel/agent.js +9 -0
- package/dist/core/agentKernelDiagnostics.d.ts +4 -6
- package/dist/core/agentKernelDiagnostics.js +12 -9
- package/dist/core/agentKernelRunner.d.ts +5 -0
- package/dist/core/agentKernelRunner.js +250 -107
- package/dist/core/autoRouter.js +17 -27
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.js +7 -1
- package/dist/core/conversationCommandState.d.ts +17 -0
- package/dist/core/conversationCommandState.js +140 -0
- package/dist/core/conversationKernel.d.ts +25 -4
- package/dist/core/conversationKernel.js +360 -89
- package/dist/core/conversationListEvent.d.ts +6 -0
- package/dist/core/conversationListEvent.js +14 -0
- package/dist/core/electronUtilityAgentClient.d.ts +4 -1
- package/dist/core/electronUtilityAgentClient.js +4 -4
- package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
- package/dist/core/electronUtilityRuntimePool.js +11 -5
- package/dist/core/installUpdate.js +41 -29
- package/dist/core/modelResponseHealth.d.ts +17 -0
- package/dist/core/modelResponseHealth.js +193 -0
- package/dist/core/providerUsageAccounting.d.ts +47 -0
- package/dist/core/providerUsageAccounting.js +71 -0
- package/dist/core/requestContextEstimate.d.ts +18 -0
- package/dist/core/requestContextEstimate.js +54 -0
- package/dist/core/subagent.d.ts +89 -5
- package/dist/core/subagent.js +264 -41
- package/dist/core/subagentCommunication.d.ts +43 -0
- package/dist/core/subagentCommunication.js +167 -0
- package/dist/core/types.d.ts +34 -1
- package/dist/core/utilityAgentProtocol.d.ts +4 -0
- package/dist/core/workEventCoalescer.js +4 -1
- package/dist/core/wslAgentClient.d.ts +18 -5
- package/dist/core/wslAgentClient.js +79 -27
- package/dist/core/wslAgentProtocol.d.ts +7 -0
- package/dist/core/wslAgentRuntimePool.d.ts +8 -2
- package/dist/core/wslAgentRuntimePool.js +19 -6
- package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
- package/dist/core/wslRuntimeProcessTree.js +104 -0
- package/dist/llm/provider.d.ts +5 -12
- package/dist/llm/provider.js +150 -269
- package/dist/main.js +582 -356
- package/dist/preload.js +3 -3
- package/dist/providers/chat-completions.adapter.d.ts +2 -0
- package/dist/providers/chat-completions.adapter.js +84 -36
- package/dist/providers/provider-adapter.d.ts +2 -0
- package/dist/providers/provider-events.d.ts +23 -14
- package/dist/providers/provider-events.js +148 -43
- package/dist/providers/provider-headers.d.ts +7 -3
- package/dist/providers/provider-headers.js +109 -39
- package/dist/providers/provider-request-compat.d.ts +6 -0
- package/dist/providers/provider-request-compat.js +37 -0
- package/dist/providers/responses.adapter.d.ts +2 -0
- package/dist/providers/responses.adapter.js +192 -126
- package/dist/server.d.ts +9 -2
- package/dist/server.js +100 -24
- package/dist/tools/index.js +17 -1
- package/dist/ui/index.html +2515 -693
- package/dist/ui/lucide-sprite.svg +7 -0
- package/dist/ui/startup.html +6 -6
- package/dist/wsl-agent-host.bundle.cjs +4575 -3006
- package/dist/wsl-agent-host.js +9 -4
- package/package.json +23 -4
package/dist/core/agent.js
CHANGED
|
@@ -35,9 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.Agent = exports.ROOT_AGENT_ACTOR_ID = void 0;
|
|
37
37
|
exports.normalizeIntelligenceTier = normalizeIntelligenceTier;
|
|
38
|
+
const modelResponseHealth_1 = require("./modelResponseHealth");
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
40
41
|
const crypto = __importStar(require("crypto"));
|
|
42
|
+
const promises_1 = require("node:timers/promises");
|
|
43
|
+
const providerUsageAccounting_1 = require("./providerUsageAccounting");
|
|
44
|
+
const requestContextEstimate_1 = require("./requestContextEstimate");
|
|
41
45
|
const imageInspect_1 = require("./imageInspect");
|
|
42
46
|
const conversationAttachments_1 = require("./conversationAttachments");
|
|
43
47
|
const displayImages_1 = require("./displayImages");
|
|
@@ -48,6 +52,7 @@ const index_1 = require("../tools/index");
|
|
|
48
52
|
const workspace_1 = require("./workspace");
|
|
49
53
|
const ssh_1 = require("./ssh");
|
|
50
54
|
const subagent_1 = require("./subagent");
|
|
55
|
+
const subagentCommunication_1 = require("./subagentCommunication");
|
|
51
56
|
const compat_1 = require("./compat");
|
|
52
57
|
const skills_1 = require("./skills");
|
|
53
58
|
const flow_1 = require("./flow");
|
|
@@ -222,6 +227,8 @@ class Agent {
|
|
|
222
227
|
lastCompression = null;
|
|
223
228
|
providerUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
224
229
|
lastProviderUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
230
|
+
providerUsageAccounting = (0, providerUsageAccounting_1.createAccounting)();
|
|
231
|
+
lastRequestContext = null;
|
|
225
232
|
compressionCache = [];
|
|
226
233
|
pendingHistoryRemovals = [];
|
|
227
234
|
branchMailbox = [];
|
|
@@ -264,15 +271,25 @@ class Agent {
|
|
|
264
271
|
activeAgentKernelRuntime = null;
|
|
265
272
|
modelSwitchCompressionPromise = null;
|
|
266
273
|
activePeerAgents = new Map();
|
|
274
|
+
// Bounded caller-owned peer slots retain protocol compatibility learning
|
|
275
|
+
// between mailbox jobs. They never retain the child Agent or go to disk.
|
|
276
|
+
peerProviderCaches = new Map();
|
|
267
277
|
awaitingAgentKernelRuntime = false;
|
|
268
278
|
pendingAgentKernelQueue = [];
|
|
269
279
|
linkedPlanAccess;
|
|
270
280
|
subagentContextPersist;
|
|
281
|
+
subagentPersistedHistory = [];
|
|
282
|
+
subagentPersistedCompression = null;
|
|
271
283
|
agentKernelUserMessageStartSubscribers = [];
|
|
272
284
|
rootInboxWakeSubscribers = [];
|
|
285
|
+
directRootInboxQueuedIds = new Set();
|
|
286
|
+
directRootWakePending = false;
|
|
287
|
+
rootInboxRetiredSubscribers = [];
|
|
273
288
|
activeWorkRunId = '';
|
|
274
289
|
loadedWorkspaceConversationKey = '';
|
|
275
290
|
managedWorkRunIds = new Set();
|
|
291
|
+
/** Public deltas since the last completed response, retained only until its boundary. */
|
|
292
|
+
pendingPublicWorkText = new Map();
|
|
276
293
|
finalizingWorkRunId = '';
|
|
277
294
|
agentRunService = null;
|
|
278
295
|
agentRunByWorkRunId = new Map();
|
|
@@ -308,6 +325,7 @@ class Agent {
|
|
|
308
325
|
recentChecks: [],
|
|
309
326
|
};
|
|
310
327
|
rootInboxListener = (message) => this.deliverRootInboxMessage(message);
|
|
328
|
+
boundSubagentManagerKey = '';
|
|
311
329
|
agentOnly;
|
|
312
330
|
runtimeActorId;
|
|
313
331
|
runtimeLifecycleRole;
|
|
@@ -430,6 +448,12 @@ class Agent {
|
|
|
430
448
|
this.toolDefinitionCache.clear();
|
|
431
449
|
this.status = 'idle';
|
|
432
450
|
}
|
|
451
|
+
/** Selecting the composer mode does not start a Goal or reset a running Flow. */
|
|
452
|
+
selectConversationMode(mode) {
|
|
453
|
+
this.mode = ['build', 'plan', 'chat', 'goal', 'flow'].includes(mode) ? mode : 'build';
|
|
454
|
+
this.invalidateSystemPrompt();
|
|
455
|
+
this.saveWorkspaceConversationState(true);
|
|
456
|
+
}
|
|
433
457
|
currentConversationFlowSelection() {
|
|
434
458
|
return this.flow?.name
|
|
435
459
|
? { name: this.flow.name, pc: Math.max(0, Math.floor(Number(this.flowPc) || 0)) }
|
|
@@ -916,13 +940,12 @@ class Agent {
|
|
|
916
940
|
return this.modelSelectionValue();
|
|
917
941
|
}
|
|
918
942
|
defaultModelCandidate() {
|
|
919
|
-
const rejected = new Set(['unavailable', 'auth_error', 'invalid_config']);
|
|
920
943
|
return this.config.allModels()
|
|
921
944
|
.map((model, index) => {
|
|
922
945
|
const validationStatus = effectiveModelValidationStatus(model);
|
|
923
946
|
const evaluationStatus = validationStatus === 'degraded' ? 'degraded' : String(model.evaluation?.status || '').toLowerCase();
|
|
924
947
|
const level = String(model.validation?.level || '').toLowerCase();
|
|
925
|
-
if (model.enabled === false
|
|
948
|
+
if (model.enabled === false) {
|
|
926
949
|
return null;
|
|
927
950
|
}
|
|
928
951
|
let score = 0;
|
|
@@ -1036,18 +1059,20 @@ class Agent {
|
|
|
1036
1059
|
this.routeAttemptStartedAt = Date.now();
|
|
1037
1060
|
}
|
|
1038
1061
|
async waitForPlannedRouteRetry(explicitDelayMs) {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1062
|
+
// Keep the original Build owner through the entire backoff. Stopping a
|
|
1063
|
+
// healthy request between attempts must not wait out a 60-second timer.
|
|
1064
|
+
const signal = this.activeProcessSignal();
|
|
1065
|
+
throwIfAgentAborted(signal);
|
|
1045
1066
|
const waitBudgetMs = Math.max(0, Math.min(15_000, this.lastRouteDecision?.retryBudgetMs ?? 5_000));
|
|
1046
|
-
const delay =
|
|
1047
|
-
|
|
1067
|
+
const delay = explicitDelayMs === undefined
|
|
1068
|
+
? Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs))
|
|
1069
|
+
: Math.max(0, explicitDelayMs);
|
|
1070
|
+
if (explicitDelayMs === undefined)
|
|
1071
|
+
this.lastRouteRetryDelayMs = 0;
|
|
1048
1072
|
if (!delay)
|
|
1049
1073
|
return;
|
|
1050
|
-
await
|
|
1074
|
+
await (0, promises_1.setTimeout)(delay, undefined, { signal });
|
|
1075
|
+
throwIfAgentAborted(signal);
|
|
1051
1076
|
}
|
|
1052
1077
|
recordRouteSuccess(latencyMs, throughput) {
|
|
1053
1078
|
const deployment = this.activeDeployment();
|
|
@@ -1395,8 +1420,8 @@ class Agent {
|
|
|
1395
1420
|
const reference = parsed.image;
|
|
1396
1421
|
const hydrated = this.hydrateDisplayImage(reference);
|
|
1397
1422
|
const model = this.activeModelConfig();
|
|
1398
|
-
const provider =
|
|
1399
|
-
if (!provider || !
|
|
1423
|
+
const provider = this.engineModel();
|
|
1424
|
+
if (!provider || !hydrated?.dataUrl || !hydrated.sha256)
|
|
1400
1425
|
return raw;
|
|
1401
1426
|
throwIfAgentAborted(signal);
|
|
1402
1427
|
const deployment = this.activeDeployment();
|
|
@@ -1411,7 +1436,7 @@ class Agent {
|
|
|
1411
1436
|
const instruction = useChinese
|
|
1412
1437
|
? `请观察图片并生成一个准确、具体的中文短标题,描述图片实际内容。只输出标题,不要加“图片”“示意图”“标题”等前缀,不要使用 Markdown,最多60个汉字。${captionHint ? `调用方提示:${captionHint}` : ''}`
|
|
1413
1438
|
: `Inspect the image and return one accurate, concrete short title describing its actual visual content. Output only the title, with no "image", "diagram", or "title" prefix, no Markdown, and at most 100 characters.${captionHint ? ` Caller hint: ${captionHint}` : ''}`;
|
|
1414
|
-
descriptionPromise = this.withTimeout(
|
|
1439
|
+
descriptionPromise = this.withTimeout(this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: [
|
|
1415
1440
|
{ type: 'text', text: instruction },
|
|
1416
1441
|
{ type: 'image_url', image_url: { url: hydrated.dataUrl } },
|
|
1417
1442
|
] }], useChinese ? '你是视觉图片标题生成器,只返回忠实、简洁的标题。' : 'You generate faithful concise titles for visual images. Return only the title.', 0, 120, signal), 30000)
|
|
@@ -1535,9 +1560,16 @@ class Agent {
|
|
|
1535
1560
|
mode: String(event.mode || this.modeName()),
|
|
1536
1561
|
model: String(event.model || this.model),
|
|
1537
1562
|
timestamp: String(event.timestamp || this.nowLabel()),
|
|
1563
|
+
toolCallId: isToolEvent && event.toolCallId ? String(event.toolCallId) : undefined,
|
|
1538
1564
|
toolName,
|
|
1539
1565
|
toolArgs: type === 'tool_call' && event.toolArgs ? this.visibleToolArgs(event.toolArgs) : undefined,
|
|
1540
1566
|
queue: isToolEvent ? undefined : event.queue,
|
|
1567
|
+
queueItems: isToolEvent ? undefined : event.queueItems?.map(item => ({
|
|
1568
|
+
id: item.id, text: item.text, queueMode: item.queueMode, requestedMode: item.requestedMode,
|
|
1569
|
+
images: item.images?.map(image => ({ ...image })),
|
|
1570
|
+
goalObjective: item.goalObjective, runId: item.runId, createdAt: item.createdAt,
|
|
1571
|
+
})),
|
|
1572
|
+
queuePaused: isToolEvent ? undefined : event.queuePaused,
|
|
1541
1573
|
workspaceId: target.workspaceId,
|
|
1542
1574
|
workspaceKey: event.workspaceKey,
|
|
1543
1575
|
runtimeKey: String(raw.runtimeKey || (0, conversationTarget_1.conversationRuntimeKey)(target)),
|
|
@@ -2250,6 +2282,9 @@ class Agent {
|
|
|
2250
2282
|
const run = this.workRuns.find(item => item.runId === String(runId || ''));
|
|
2251
2283
|
if (!run)
|
|
2252
2284
|
return false;
|
|
2285
|
+
if (status !== 'completed' && (run.status === 'running' || run.status === status || (run.status === 'interrupted' && status === 'force_interrupted'))) {
|
|
2286
|
+
this.persistInterruptedPublicWorkText(run);
|
|
2287
|
+
}
|
|
2253
2288
|
this.syncAgentRunTerminal(run.runId, status, endedAt);
|
|
2254
2289
|
this.flushPendingHistoryRemovals();
|
|
2255
2290
|
if (run.status !== 'running') {
|
|
@@ -2346,11 +2381,23 @@ class Agent {
|
|
|
2346
2381
|
conversationId: run.target.conversationId,
|
|
2347
2382
|
});
|
|
2348
2383
|
}
|
|
2384
|
+
persistInterruptedPublicWorkText(run) {
|
|
2385
|
+
const chunks = this.pendingPublicWorkText.get(run.runId);
|
|
2386
|
+
this.pendingPublicWorkText.delete(run.runId);
|
|
2387
|
+
const content = chunks?.join('');
|
|
2388
|
+
if (!content?.trim())
|
|
2389
|
+
return;
|
|
2390
|
+
// This is an incomplete public reply, not a successful final answer or a
|
|
2391
|
+
// model-history message. A single durable boundary replaces live deltas.
|
|
2392
|
+
this.emitWorkEvent({ type: 'response', content, runId: run.runId, conversationId: run.target.conversationId });
|
|
2393
|
+
}
|
|
2349
2394
|
emitWorkEvent(input) {
|
|
2350
2395
|
if (input.type === 'start' && !this.workRuns.some(run => run.runId === this.activeWorkRunId && run.status === 'running')) {
|
|
2351
2396
|
this.beginConversationWorkRun(input.runId || crypto.randomUUID(), this.currentConversationTarget(input.conversationId));
|
|
2352
2397
|
}
|
|
2353
2398
|
const activeRun = this.workRuns.find(run => run.runId === (input.runId || this.activeWorkRunId));
|
|
2399
|
+
if (activeRun && input.type === 'error')
|
|
2400
|
+
this.persistInterruptedPublicWorkText(activeRun);
|
|
2354
2401
|
const managedTurnBoundary = input.type === 'done'
|
|
2355
2402
|
&& !!activeRun
|
|
2356
2403
|
&& this.managedWorkRunIds.has(activeRun.runId)
|
|
@@ -2374,9 +2421,16 @@ class Agent {
|
|
|
2374
2421
|
mode: input.mode || this.modeName(),
|
|
2375
2422
|
model: input.model || this.model,
|
|
2376
2423
|
timestamp: input.timestamp || this.nowLabel(),
|
|
2424
|
+
toolCallId: isToolEvent ? input.toolCallId : undefined,
|
|
2377
2425
|
toolName,
|
|
2378
2426
|
toolArgs: publishedType === 'tool_call' && input.toolArgs ? this.visibleToolArgs(input.toolArgs) : undefined,
|
|
2379
2427
|
queue: isToolEvent ? undefined : input.queue,
|
|
2428
|
+
queueItems: isToolEvent ? undefined : input.queueItems?.map(item => ({
|
|
2429
|
+
id: item.id, text: item.text, queueMode: item.queueMode, requestedMode: item.requestedMode,
|
|
2430
|
+
images: item.images?.map(image => ({ ...image })),
|
|
2431
|
+
goalObjective: item.goalObjective, runId: item.runId, createdAt: item.createdAt,
|
|
2432
|
+
})),
|
|
2433
|
+
queuePaused: isToolEvent ? undefined : input.queuePaused,
|
|
2380
2434
|
workspaceId: input.workspaceId || activeRun?.target.workspaceId || this.currentConversationTarget(input.conversationId).workspaceId,
|
|
2381
2435
|
workspaceKey: input.workspaceKey,
|
|
2382
2436
|
runtimeKey: input.runtimeKey || activeRun?.runtimeKey,
|
|
@@ -2389,6 +2443,14 @@ class Agent {
|
|
|
2389
2443
|
fallback: input.fallback,
|
|
2390
2444
|
};
|
|
2391
2445
|
if (activeRun && this.isPersistablePublicWorkEvent(event)) {
|
|
2446
|
+
if (event.type === 'text' && activeRun.status === 'running') {
|
|
2447
|
+
const chunks = this.pendingPublicWorkText.get(activeRun.runId) || [];
|
|
2448
|
+
chunks.push(event.content);
|
|
2449
|
+
this.pendingPublicWorkText.set(activeRun.runId, chunks);
|
|
2450
|
+
}
|
|
2451
|
+
else if (event.type === 'response' || event.type === 'final_response' || event.type === 'done' || event.type === 'error') {
|
|
2452
|
+
this.pendingPublicWorkText.delete(activeRun.runId);
|
|
2453
|
+
}
|
|
2392
2454
|
activeRun.sequence = Number(sequence || activeRun.sequence + 1);
|
|
2393
2455
|
// Streaming text is delivered live, while the complete sanitized API
|
|
2394
2456
|
// response is persisted once at message_end. This avoids saving hundreds
|
|
@@ -2474,8 +2536,10 @@ class Agent {
|
|
|
2474
2536
|
attachAgentKernelRuntime(runtime) {
|
|
2475
2537
|
this.activeAgentKernelRuntime = runtime;
|
|
2476
2538
|
this.awaitingAgentKernelRuntime = false;
|
|
2477
|
-
if (!runtime)
|
|
2539
|
+
if (!runtime) {
|
|
2540
|
+
this.directRootInboxQueuedIds.clear();
|
|
2478
2541
|
return;
|
|
2542
|
+
}
|
|
2479
2543
|
// A user stop can arrive while the Native Kernel is still being loaded or
|
|
2480
2544
|
// assembling its first context. In that handoff window
|
|
2481
2545
|
// abortActiveKernelRun() can only abort the outer process signal; make a
|
|
@@ -2495,11 +2559,52 @@ class Agent {
|
|
|
2495
2559
|
this.agentKernelUserMessageStartSubscribers = this.agentKernelUserMessageStartSubscribers.filter(sub => sub !== fn);
|
|
2496
2560
|
};
|
|
2497
2561
|
}
|
|
2498
|
-
subscribeRootInboxWake(fn) {
|
|
2562
|
+
subscribeRootInboxWake(fn, retired) {
|
|
2499
2563
|
this.rootInboxWakeSubscribers.push(fn);
|
|
2564
|
+
if (retired)
|
|
2565
|
+
this.rootInboxRetiredSubscribers.push(retired);
|
|
2500
2566
|
return () => {
|
|
2501
2567
|
this.rootInboxWakeSubscribers = this.rootInboxWakeSubscribers.filter(sub => sub !== fn);
|
|
2568
|
+
if (retired)
|
|
2569
|
+
this.rootInboxRetiredSubscribers = this.rootInboxRetiredSubscribers.filter(sub => sub !== retired);
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
/** Called only after receipt-bearing tool history has been saved, or on cold replay. */
|
|
2573
|
+
acknowledgeSubagentSettlementReceipts() {
|
|
2574
|
+
if (this.isSubagentRuntime)
|
|
2575
|
+
return [];
|
|
2576
|
+
const receipts = this.history.flatMap(message => {
|
|
2577
|
+
const receipt = message.subagent_settlement_receipt;
|
|
2578
|
+
return message.role === 'tool' && ['subagent_result', 'subagent_read'].includes(String(message.name || ''))
|
|
2579
|
+
&& receipt && typeof receipt.peerId === 'string' && Number.isSafeInteger(receipt.revision) && receipt.revision > 0
|
|
2580
|
+
? [receipt] : [];
|
|
2581
|
+
});
|
|
2582
|
+
if (!receipts.length)
|
|
2583
|
+
return [];
|
|
2584
|
+
const ids = this.subagents.acknowledgeSettlementResults(receipts);
|
|
2585
|
+
if (!ids.length)
|
|
2586
|
+
return [];
|
|
2587
|
+
const retired = new Set(ids);
|
|
2588
|
+
const matches = (content, hidden, clientMessageId) => {
|
|
2589
|
+
if (!hidden || clientMessageId || typeof content !== 'string')
|
|
2590
|
+
return false;
|
|
2591
|
+
const id = content.match(/^\[Root subagent inbox id=([0-9a-f-]{36})\b/i)?.[1];
|
|
2592
|
+
return !!id && retired.has(id);
|
|
2502
2593
|
};
|
|
2594
|
+
this.pendingAgentKernelQueue = this.pendingAgentKernelQueue.filter(item => item.queueMode !== 'followUp'
|
|
2595
|
+
|| !matches(item.content, item.hiddenUserInput === true, item.clientMessageId));
|
|
2596
|
+
this.activeAgentKernelRuntime?.removeQueuedMessages?.((raw, mode) => {
|
|
2597
|
+
const message = raw;
|
|
2598
|
+
return mode === 'followUp' && matches(message.content, message.hiddenUserInput === true, message.clientMessageId);
|
|
2599
|
+
});
|
|
2600
|
+
const before = this.continuations.length;
|
|
2601
|
+
this.continuations = this.continuations.filter(item => item.queueMode !== 'followUp'
|
|
2602
|
+
|| !matches(item.content, item.hiddenUserInput === true, item.clientMessageId));
|
|
2603
|
+
if (this.continuations.length !== before)
|
|
2604
|
+
this.saveWorkspaceConversationState(true);
|
|
2605
|
+
for (const subscriber of this.rootInboxRetiredSubscribers)
|
|
2606
|
+
subscriber(ids);
|
|
2607
|
+
return ids;
|
|
2503
2608
|
}
|
|
2504
2609
|
notifyAgentKernelUserMessageStart(content, clientMessageId) {
|
|
2505
2610
|
const text = String(content || '');
|
|
@@ -2592,7 +2697,7 @@ class Agent {
|
|
|
2592
2697
|
}
|
|
2593
2698
|
consumeConversationContinuation(match) {
|
|
2594
2699
|
const index = this.continuations.findIndex(item => match.clientMessageId
|
|
2595
|
-
? item.clientMessageId === match.clientMessageId
|
|
2700
|
+
? item.clientMessageId === match.clientMessageId && item.queueMode === match.queueMode
|
|
2596
2701
|
: item.queueMode === match.queueMode && item.content === match.content);
|
|
2597
2702
|
if (index < 0)
|
|
2598
2703
|
return false;
|
|
@@ -2636,6 +2741,9 @@ class Agent {
|
|
|
2636
2741
|
attachments: attachments.length ? attachments : undefined,
|
|
2637
2742
|
hiddenUserInput: raw.hiddenUserInput === true,
|
|
2638
2743
|
createdAt: String(raw.createdAt || new Date().toISOString()),
|
|
2744
|
+
visibleUserInput: raw.visibleUserInput,
|
|
2745
|
+
visibleMode: raw.visibleMode,
|
|
2746
|
+
goalObjective: raw.goalObjective,
|
|
2639
2747
|
});
|
|
2640
2748
|
}
|
|
2641
2749
|
return Array.from(deduped.values()).slice(-100);
|
|
@@ -2730,9 +2838,9 @@ class Agent {
|
|
|
2730
2838
|
this.conversationStateDirty.delete(file);
|
|
2731
2839
|
this.writeStoredConversationStateNow(pending.state, pending.ws);
|
|
2732
2840
|
}
|
|
2733
|
-
getStoredFlowSuspension(conversationId = this.activeConversationId) {
|
|
2734
|
-
const stored = this.readStoredConversationState();
|
|
2735
|
-
const stateKey = this.
|
|
2841
|
+
getStoredFlowSuspension(conversationId = this.activeConversationId, ws = this.workspace.current) {
|
|
2842
|
+
const stored = this.readStoredConversationState(ws);
|
|
2843
|
+
const stateKey = this.workspaceConversationStateKeyFor(conversationId, ws);
|
|
2736
2844
|
if (stateKey && stored.flowSuspensions && stored.flowSuspensions[stateKey])
|
|
2737
2845
|
return stored.flowSuspensions[stateKey];
|
|
2738
2846
|
const legacy = stored.flowSuspension || null;
|
|
@@ -2741,8 +2849,8 @@ class Agent {
|
|
|
2741
2849
|
const legacyTargetsThis = !legacy.target || (legacy.target.conversationId === conversationId);
|
|
2742
2850
|
return legacyTargetsThis ? legacy : null;
|
|
2743
2851
|
}
|
|
2744
|
-
saveStoredFlowSuspension(suspension, conversationId = this.activeConversationId) {
|
|
2745
|
-
const stateKey = this.
|
|
2852
|
+
saveStoredFlowSuspension(suspension, conversationId = this.activeConversationId, ws = this.workspace.current) {
|
|
2853
|
+
const stateKey = this.workspaceConversationStateKeyFor(conversationId, ws);
|
|
2746
2854
|
if (!stateKey)
|
|
2747
2855
|
return;
|
|
2748
2856
|
// Use the lock-aware latest-state mutator directly. Passing a partial
|
|
@@ -2750,7 +2858,7 @@ class Agent {
|
|
|
2750
2858
|
// merge the deleted key back from the latest disk snapshot, making Stop,
|
|
2751
2859
|
// archive, and new-work handoff appear to clear a suspension in memory
|
|
2752
2860
|
// while it immediately reappears after reload.
|
|
2753
|
-
this.mutateStoredConversationState(
|
|
2861
|
+
this.mutateStoredConversationState(ws, latest => {
|
|
2754
2862
|
const flowSuspensions = { ...(latest.flowSuspensions || {}) };
|
|
2755
2863
|
if (suspension) {
|
|
2756
2864
|
flowSuspensions[stateKey] = { ...suspension, updatedAt: suspension.updatedAt || new Date().toISOString() };
|
|
@@ -2763,8 +2871,8 @@ class Agent {
|
|
|
2763
2871
|
return next;
|
|
2764
2872
|
});
|
|
2765
2873
|
}
|
|
2766
|
-
clearStoredFlowSuspension(conversationId = this.activeConversationId) {
|
|
2767
|
-
this.saveStoredFlowSuspension(null, conversationId);
|
|
2874
|
+
clearStoredFlowSuspension(conversationId = this.activeConversationId, ws = this.workspace.current) {
|
|
2875
|
+
this.saveStoredFlowSuspension(null, conversationId, ws);
|
|
2768
2876
|
}
|
|
2769
2877
|
getStoredConversationDraft(conversationId = this.activeConversationId) {
|
|
2770
2878
|
const key = this.workspaceConversationStateKey(conversationId);
|
|
@@ -3070,8 +3178,10 @@ class Agent {
|
|
|
3070
3178
|
if (this.isSubagentRuntime)
|
|
3071
3179
|
return;
|
|
3072
3180
|
const clean = this.safeConversationId(conversationId || 'default');
|
|
3073
|
-
this.
|
|
3074
|
-
this.
|
|
3181
|
+
const managerKey = this.subagentManagerKey(clean);
|
|
3182
|
+
if (this.boundSubagentManagerKey !== managerKey)
|
|
3183
|
+
this.subagents.releaseOwnerBinding(this.rootInboxListener);
|
|
3184
|
+
this.subagents = (0, subagent_1.sharedSubagentManager)(managerKey, {
|
|
3075
3185
|
conversationId: clean,
|
|
3076
3186
|
rootAgentId: state?.rootAgentId || this.runtimeActorId,
|
|
3077
3187
|
concurrency: this.subagentConcurrencyLimit(),
|
|
@@ -3086,6 +3196,22 @@ class Agent {
|
|
|
3086
3196
|
this.saveWorkspaceConversationState();
|
|
3087
3197
|
},
|
|
3088
3198
|
});
|
|
3199
|
+
this.boundSubagentManagerKey = managerKey;
|
|
3200
|
+
}
|
|
3201
|
+
/** Release an idle facade; shared peer state remains available to its next owner. */
|
|
3202
|
+
releaseConversationRuntimeBindings() {
|
|
3203
|
+
if (this.activeProcessSignal() || (this.subagents.ownsExecutionBinding(this.rootInboxListener) && this.subagents.hasPendingWork())) {
|
|
3204
|
+
throw new Error('Cannot release a conversation owner with running or queued work');
|
|
3205
|
+
}
|
|
3206
|
+
this.flushWorkspaceConversationState();
|
|
3207
|
+
this.subagents.releaseOwnerBinding(this.rootInboxListener);
|
|
3208
|
+
}
|
|
3209
|
+
assertPeerOwnerCanSwitch(conversationId, workspaceId = this.workspace.current?.id) {
|
|
3210
|
+
const changesTarget = this.safeConversationId(conversationId || 'default') !== this.activeConversationId
|
|
3211
|
+
|| workspaceId !== this.workspace.current?.id;
|
|
3212
|
+
if (changesTarget && this.subagents.ownsExecutionBinding(this.rootInboxListener) && this.subagents.hasPendingWork()) {
|
|
3213
|
+
throw new Error('Cannot switch an execution owner with running or queued subagents');
|
|
3214
|
+
}
|
|
3089
3215
|
}
|
|
3090
3216
|
persistSubagentState(conversationId, subagentState) {
|
|
3091
3217
|
if (this.isSubagentRuntime)
|
|
@@ -3109,16 +3235,55 @@ class Agent {
|
|
|
3109
3235
|
return child.queueActiveKernelMessage(`${marker}\n${message.body}`, 'steer', undefined, undefined, undefined, true);
|
|
3110
3236
|
}
|
|
3111
3237
|
deliverRootInboxMessage(message) {
|
|
3238
|
+
if (this.acknowledgeSubagentSettlementReceipts().includes(message.id))
|
|
3239
|
+
return true;
|
|
3112
3240
|
const marker = `[Root subagent inbox id=${message.id} ${message.kind} from ${message.fromAgentId}]`;
|
|
3113
3241
|
const prompt = `${marker}\n${message.body}\n\nReview this persisted peer result and summarize or continue the parent task as needed.`;
|
|
3242
|
+
// Stay in the already active kernel so hosted mailbox delivery shares the
|
|
3243
|
+
// same initialized system/tools instead of creating one Build per message.
|
|
3244
|
+
if (this.directRootInboxQueuedIds.has(message.id))
|
|
3245
|
+
return true;
|
|
3246
|
+
if (this.queueActiveKernelMessage(prompt, 'followUp', undefined, undefined, undefined, true)) {
|
|
3247
|
+
this.directRootInboxQueuedIds.add(message.id);
|
|
3248
|
+
return true;
|
|
3249
|
+
}
|
|
3114
3250
|
for (const sub of this.rootInboxWakeSubscribers) {
|
|
3115
3251
|
try {
|
|
3116
|
-
if (sub(prompt))
|
|
3252
|
+
if (sub(prompt, { wakeup: message.wakeup !== false }))
|
|
3117
3253
|
return true;
|
|
3118
3254
|
}
|
|
3119
3255
|
catch { /* ignore subscriber errors */ }
|
|
3120
3256
|
}
|
|
3121
|
-
|
|
3257
|
+
if (message.wakeup !== false && !this.rootInboxWakeSubscribers.length && !this.activeProcessSignal()
|
|
3258
|
+
&& !this.subagents.isSchedulingPaused() && this.subagents.ownsExecutionBinding(this.rootInboxListener)
|
|
3259
|
+
&& !this.directRootWakePending) {
|
|
3260
|
+
this.directRootWakePending = true;
|
|
3261
|
+
queueMicrotask(() => {
|
|
3262
|
+
this.directRootWakePending = false;
|
|
3263
|
+
if (this.rootInboxWakeSubscribers.length || this.activeProcessSignal() || this.subagents.isSchedulingPaused()
|
|
3264
|
+
|| !this.subagents.ownsExecutionBinding(this.rootInboxListener))
|
|
3265
|
+
return;
|
|
3266
|
+
const next = this.subagents.readRootInbox().find(item => item.wakeup !== false);
|
|
3267
|
+
if (!next)
|
|
3268
|
+
return;
|
|
3269
|
+
const text = `[Root subagent inbox id=${next.id} ${next.kind} from ${next.fromAgentId}]\n${next.body}`;
|
|
3270
|
+
void this.process({ text, hiddenUserInput: true }).catch(error => {
|
|
3271
|
+
this.recordWorkStatus(`Subagent mailbox wake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3272
|
+
});
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
return false;
|
|
3276
|
+
}
|
|
3277
|
+
/** A normal activation consumes queued mail without changing its static request prefix. */
|
|
3278
|
+
flushDirectRootInbox() {
|
|
3279
|
+
if (this.isSubagentRuntime || this.rootInboxWakeSubscribers.length)
|
|
3280
|
+
return;
|
|
3281
|
+
for (const message of this.subagents.readRootInbox()) {
|
|
3282
|
+
const marker = `[Root subagent inbox id=${message.id} `;
|
|
3283
|
+
if (this.history.some(item => item.role === 'user' && String(item.content || '').startsWith(marker)))
|
|
3284
|
+
continue;
|
|
3285
|
+
this.deliverRootInboxMessage(message);
|
|
3286
|
+
}
|
|
3122
3287
|
}
|
|
3123
3288
|
deliverPeerSettlement(record) {
|
|
3124
3289
|
const target = record.createdByAgentId === record.id ? this.subagents.rootAgentId : record.createdByAgentId;
|
|
@@ -3129,7 +3294,7 @@ class Agent {
|
|
|
3129
3294
|
if (delivery.ok)
|
|
3130
3295
|
return;
|
|
3131
3296
|
}
|
|
3132
|
-
this.subagents.sendRootMessage(record.id, body, 'result');
|
|
3297
|
+
this.subagents.sendRootMessage(record.id, body, 'result', record.settlementRevision);
|
|
3133
3298
|
}
|
|
3134
3299
|
recordsForState(state) {
|
|
3135
3300
|
if (!state)
|
|
@@ -3187,7 +3352,8 @@ class Agent {
|
|
|
3187
3352
|
// windows on demand ("load earlier messages").
|
|
3188
3353
|
const windowSize = Math.max(1, Math.floor(Number(options.window) || 0) || 200);
|
|
3189
3354
|
const totalMessages = fullChatMessages.length;
|
|
3190
|
-
const before =
|
|
3355
|
+
const before = options.before == null ? totalMessages
|
|
3356
|
+
: Math.min(totalMessages, Math.max(0, Math.floor(Number(options.before) || 0)));
|
|
3191
3357
|
const windowStart = Math.max(0, before - windowSize);
|
|
3192
3358
|
const chatMessages = fullChatMessages.slice(windowStart, before);
|
|
3193
3359
|
const workRuns = this.normalizeWorkRuns(isActiveConversation && viewingRuntimeNode ? this.workRuns : (viewedNode?.workRuns || persisted?.workRuns || memory?.workRuns));
|
|
@@ -3267,7 +3433,7 @@ class Agent {
|
|
|
3267
3433
|
branchIndexDirectory: { ...(tree.nodeIndex || {}) },
|
|
3268
3434
|
};
|
|
3269
3435
|
}
|
|
3270
|
-
ensureConversationSnapshot(conversationId = this.activeConversationId) {
|
|
3436
|
+
ensureConversationSnapshot(conversationId = this.activeConversationId, options = {}) {
|
|
3271
3437
|
const clean = this.safeConversationId(conversationId || 'default');
|
|
3272
3438
|
const stateKey = this.workspaceConversationStateKey(clean);
|
|
3273
3439
|
if (stateKey) {
|
|
@@ -3296,7 +3462,7 @@ class Agent {
|
|
|
3296
3462
|
this.writeStoredConversationState(stored);
|
|
3297
3463
|
}
|
|
3298
3464
|
}
|
|
3299
|
-
return this.getConversationSnapshot(clean);
|
|
3465
|
+
return this.getConversationSnapshot(clean, options);
|
|
3300
3466
|
}
|
|
3301
3467
|
rewindConversation(conversationId, messageIndex) {
|
|
3302
3468
|
const clean = this.safeConversationId(conversationId || 'default');
|
|
@@ -3732,24 +3898,71 @@ class Agent {
|
|
|
3732
3898
|
'No preamble, no explanation, no Markdown, no quotes, no trailing punctuation.',
|
|
3733
3899
|
].join('\n');
|
|
3734
3900
|
const prompt = `First user input:\n${String(firstUserInput || '').slice(0, 4000)}\n\nConversation title (a few words):`;
|
|
3735
|
-
const controller = new AbortController();
|
|
3736
|
-
const timer = setTimeout(() => controller.abort(new Error('conversation rename timed out')), 15000);
|
|
3737
3901
|
try {
|
|
3738
3902
|
if (signal?.aborted)
|
|
3739
3903
|
return '';
|
|
3740
3904
|
const { temperature, reasoningEffort } = provider.intelligenceConfig(intelligence);
|
|
3741
|
-
|
|
3905
|
+
// The title request uses the same transport policy and cancellation owner
|
|
3906
|
+
// as the formal response. A healthy slow model must not be turned into
|
|
3907
|
+
// five failed requests by an unrelated 15-second title deadline.
|
|
3908
|
+
// Reasoning tokens share the completion budget. A 64-token cap can
|
|
3909
|
+
// truncate even a short title and block every first conversation request.
|
|
3910
|
+
const generated = await this.chatWithConversationUsage(provider, modelName, [{ role: 'user', content: prompt }], system, temperature, 2048, signal, reasoningEffort);
|
|
3911
|
+
if (signal?.aborted)
|
|
3912
|
+
return '';
|
|
3742
3913
|
const title = this.normalizeConversationRenameTitle(generated);
|
|
3743
3914
|
const source = String(firstUserInput || '').replace(/\s+/g, ' ').trim();
|
|
3744
3915
|
return title && title !== source ? title : '';
|
|
3745
3916
|
}
|
|
3746
|
-
catch {
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
clearTimeout(timer);
|
|
3917
|
+
catch (error) {
|
|
3918
|
+
if (signal?.aborted)
|
|
3919
|
+
return '';
|
|
3920
|
+
throw error;
|
|
3751
3921
|
}
|
|
3752
3922
|
}
|
|
3923
|
+
conversationTitleFailureCause(error, provider) {
|
|
3924
|
+
const message = error instanceof Error ? error.message : '';
|
|
3925
|
+
const status = message.match(/^\[LLM Error:\s*([1-5]\d{2})\]/)?.[1];
|
|
3926
|
+
if (status) {
|
|
3927
|
+
// Extract only a short machine code. Never display the provider's raw
|
|
3928
|
+
// message/body, request URL, headers or echoed credentials in this gate.
|
|
3929
|
+
let code = '';
|
|
3930
|
+
try {
|
|
3931
|
+
const json = JSON.parse(message.slice(message.indexOf('{')));
|
|
3932
|
+
const candidate = typeof json.error?.code === 'string' ? json.error.code : '';
|
|
3933
|
+
if (/^[a-z][a-z0-9_.-]{0,63}$/i.test(candidate)
|
|
3934
|
+
&& !(provider.apiKey && candidate.includes(provider.apiKey))
|
|
3935
|
+
&& !(provider.baseUrl && candidate.includes(provider.baseUrl)))
|
|
3936
|
+
code = candidate;
|
|
3937
|
+
}
|
|
3938
|
+
catch { /* Non-JSON errors still retain their safe HTTP status. */ }
|
|
3939
|
+
const reason = code === 'get_channel_failed' ? 'no provider channel available' : {
|
|
3940
|
+
'400': 'request rejected', '401': 'authentication failed', '402': 'payment required',
|
|
3941
|
+
'403': 'access denied', '404': 'model or endpoint unavailable', '408': 'request timed out',
|
|
3942
|
+
'429': 'rate limited', '500': 'provider internal error', '502': 'provider gateway error',
|
|
3943
|
+
'503': 'service unavailable', '504': 'provider gateway timed out',
|
|
3944
|
+
}[status] || 'provider request failed';
|
|
3945
|
+
return `HTTP ${status}${code ? ` (${code})` : ''}: ${reason}`;
|
|
3946
|
+
}
|
|
3947
|
+
if (error instanceof Error && error.name === 'TimeoutError')
|
|
3948
|
+
return 'provider request timed out';
|
|
3949
|
+
if (error instanceof SyntaxError)
|
|
3950
|
+
return 'provider returned invalid JSON';
|
|
3951
|
+
return 'provider request failed';
|
|
3952
|
+
}
|
|
3953
|
+
async waitForConversationTitleRetry(delayMs, signal) {
|
|
3954
|
+
if (signal?.aborted || delayMs <= 0)
|
|
3955
|
+
return;
|
|
3956
|
+
await new Promise(resolve => {
|
|
3957
|
+
const finish = () => {
|
|
3958
|
+
clearTimeout(timer);
|
|
3959
|
+
signal?.removeEventListener('abort', finish);
|
|
3960
|
+
resolve();
|
|
3961
|
+
};
|
|
3962
|
+
const timer = setTimeout(finish, delayMs);
|
|
3963
|
+
signal?.addEventListener('abort', finish, { once: true });
|
|
3964
|
+
});
|
|
3965
|
+
}
|
|
3753
3966
|
/** Legacy completion hook retained as a no-op; naming is pre-response only. */
|
|
3754
3967
|
maybeAutoRenameConversationFromRun(_run) {
|
|
3755
3968
|
// Compatibility no-op. Automatic naming is a hard gate on the first
|
|
@@ -3765,19 +3978,33 @@ class Agent {
|
|
|
3765
3978
|
const retryDelaysMs = [0, 1000, 2000, 4000, 8000];
|
|
3766
3979
|
if (!messageId || !firstUserInput.trim())
|
|
3767
3980
|
return false;
|
|
3981
|
+
// Failure state belongs to this title attempt sequence, not the Agent or
|
|
3982
|
+
// another conversation. A later empty title replaces an earlier HTTP error.
|
|
3983
|
+
let lastFailure = 'provider returned an empty or unusable title';
|
|
3984
|
+
const requestTitle = async () => {
|
|
3985
|
+
lastFailure = 'provider returned an empty or unusable title';
|
|
3986
|
+
try {
|
|
3987
|
+
return await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal);
|
|
3988
|
+
}
|
|
3989
|
+
catch (error) {
|
|
3990
|
+
lastFailure = this.conversationTitleFailureCause(error, provider);
|
|
3991
|
+
return '';
|
|
3992
|
+
}
|
|
3993
|
+
};
|
|
3994
|
+
const exhaustedFailure = () => new Error(`Conversation title generation failed; ${lastFailure}. The first Agent request was not started. Retry the first input.`);
|
|
3768
3995
|
if (!stateKey) {
|
|
3769
3996
|
// Pure Agent/CLI mode has no workspace conversation file to rename, but
|
|
3770
3997
|
// still uses the title request as the required first model-availability
|
|
3771
3998
|
// probe before starting the formal response.
|
|
3772
3999
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3773
|
-
if (await
|
|
4000
|
+
if (await requestTitle())
|
|
3774
4001
|
return true;
|
|
3775
4002
|
if (signal?.aborted)
|
|
3776
4003
|
return false;
|
|
3777
4004
|
if (attempt < maxAttempts - 1)
|
|
3778
|
-
await
|
|
4005
|
+
await this.waitForConversationTitleRetry(retryDelaysMs[attempt], signal);
|
|
3779
4006
|
}
|
|
3780
|
-
|
|
4007
|
+
throw exhaustedFailure();
|
|
3781
4008
|
}
|
|
3782
4009
|
const stored = this.readStoredConversationState(workspace);
|
|
3783
4010
|
const entry = stored.conversations?.[stateKey];
|
|
@@ -3794,7 +4021,7 @@ class Agent {
|
|
|
3794
4021
|
if (memory)
|
|
3795
4022
|
memory.titleRequestMessageId = messageId;
|
|
3796
4023
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3797
|
-
const title = await
|
|
4024
|
+
const title = await requestTitle();
|
|
3798
4025
|
if (title) {
|
|
3799
4026
|
const latest = this.readStoredConversationState(workspace);
|
|
3800
4027
|
const current = latest.conversations?.[stateKey];
|
|
@@ -3812,9 +4039,9 @@ class Agent {
|
|
|
3812
4039
|
if (signal?.aborted)
|
|
3813
4040
|
return false;
|
|
3814
4041
|
if (attempt < maxAttempts - 1)
|
|
3815
|
-
await
|
|
4042
|
+
await this.waitForConversationTitleRetry(retryDelaysMs[attempt], signal);
|
|
3816
4043
|
}
|
|
3817
|
-
|
|
4044
|
+
throw exhaustedFailure();
|
|
3818
4045
|
}
|
|
3819
4046
|
firstPersistedUserForTitleGate() {
|
|
3820
4047
|
const firstUser = this.chatMessages.find(message => message.role === 'user');
|
|
@@ -4008,8 +4235,16 @@ class Agent {
|
|
|
4008
4235
|
};
|
|
4009
4236
|
}
|
|
4010
4237
|
saveWorkspaceConversationState(flush = true) {
|
|
4011
|
-
if (this.isSubagentRuntime)
|
|
4238
|
+
if (this.isSubagentRuntime) {
|
|
4239
|
+
if (this.subagentContextPersist && (this.history.length !== this.subagentPersistedHistory.length
|
|
4240
|
+
|| this.lastCompression !== this.subagentPersistedCompression
|
|
4241
|
+
|| this.history.some((message, index) => message !== this.subagentPersistedHistory[index]))) {
|
|
4242
|
+
this.subagentContextPersist(this.history, this.lastCompression);
|
|
4243
|
+
this.subagentPersistedHistory = this.history.slice();
|
|
4244
|
+
this.subagentPersistedCompression = this.lastCompression;
|
|
4245
|
+
}
|
|
4012
4246
|
return;
|
|
4247
|
+
}
|
|
4013
4248
|
const key = this.workspaceConversationKey();
|
|
4014
4249
|
if (!key)
|
|
4015
4250
|
return;
|
|
@@ -4018,6 +4253,7 @@ class Agent {
|
|
|
4018
4253
|
this.workspaceConversations.set(key, {
|
|
4019
4254
|
chatMessages: [...this.chatMessages],
|
|
4020
4255
|
history: [...this.history],
|
|
4256
|
+
providerUsage: this.conversationProviderUsage(),
|
|
4021
4257
|
compressionCache: [...this.compressionCache],
|
|
4022
4258
|
branchMailbox: [...this.branchMailbox],
|
|
4023
4259
|
branchCommunication: this.branchCommunicationEnabled,
|
|
@@ -4053,6 +4289,7 @@ class Agent {
|
|
|
4053
4289
|
title,
|
|
4054
4290
|
chatMessages: [...this.chatMessages],
|
|
4055
4291
|
history: [...this.history],
|
|
4292
|
+
providerUsage: this.conversationProviderUsage(),
|
|
4056
4293
|
compressionCache: [...this.compressionCache],
|
|
4057
4294
|
branchMailbox: [...this.branchMailbox],
|
|
4058
4295
|
branchCommunication: this.branchCommunicationEnabled,
|
|
@@ -4085,6 +4322,8 @@ class Agent {
|
|
|
4085
4322
|
}
|
|
4086
4323
|
loadWorkspaceConversationState() {
|
|
4087
4324
|
this.managedWorkRunIds.clear();
|
|
4325
|
+
this.pendingPublicWorkText.clear();
|
|
4326
|
+
this.restoreProviderUsage();
|
|
4088
4327
|
const key = this.workspaceConversationKey();
|
|
4089
4328
|
this.loadedWorkspaceConversationKey = key || '';
|
|
4090
4329
|
if (!key) {
|
|
@@ -4109,6 +4348,7 @@ class Agent {
|
|
|
4109
4348
|
}
|
|
4110
4349
|
const saved = this.workspaceConversations.get(key);
|
|
4111
4350
|
if (saved) {
|
|
4351
|
+
this.restoreProviderUsage(saved.providerUsage);
|
|
4112
4352
|
this.history = [...saved.history];
|
|
4113
4353
|
this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
4114
4354
|
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
@@ -4136,6 +4376,7 @@ class Agent {
|
|
|
4136
4376
|
const stored = this.readStoredConversationState();
|
|
4137
4377
|
const stateKey = this.workspaceConversationStateKey();
|
|
4138
4378
|
const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
|
|
4379
|
+
this.restoreProviderUsage(persisted?.providerUsage);
|
|
4139
4380
|
const repairedHistory = this.repairDanglingToolCalls(persisted?.history ? [...persisted.history] : []);
|
|
4140
4381
|
this.history = repairedHistory.messages;
|
|
4141
4382
|
this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
@@ -4182,6 +4423,7 @@ class Agent {
|
|
|
4182
4423
|
this.workspaceConversations.set(key, {
|
|
4183
4424
|
chatMessages: [...this.chatMessages],
|
|
4184
4425
|
history: [...this.history],
|
|
4426
|
+
providerUsage: this.conversationProviderUsage(),
|
|
4185
4427
|
compressionCache: [...this.compressionCache],
|
|
4186
4428
|
branchMailbox: [...this.branchMailbox],
|
|
4187
4429
|
branchCommunication: this.branchCommunicationEnabled,
|
|
@@ -4232,10 +4474,12 @@ class Agent {
|
|
|
4232
4474
|
return ws;
|
|
4233
4475
|
}
|
|
4234
4476
|
selectWorkspace(id) {
|
|
4477
|
+
this.assertPeerOwnerCanSwitch(this.activeConversationId, id);
|
|
4235
4478
|
this.saveWorkspaceConversationState();
|
|
4236
4479
|
return this.applyWorkspaceContext(this.workspace.select(id));
|
|
4237
4480
|
}
|
|
4238
4481
|
selectWorkspaceFromStorage(id) {
|
|
4482
|
+
this.assertPeerOwnerCanSwitch(this.activeConversationId, id);
|
|
4239
4483
|
const selected = this.workspace.select(id);
|
|
4240
4484
|
if (selected)
|
|
4241
4485
|
this.config.loadWorkspaceConfig(selected.path);
|
|
@@ -4273,6 +4517,7 @@ class Agent {
|
|
|
4273
4517
|
}
|
|
4274
4518
|
setConversation(id) {
|
|
4275
4519
|
const clean = this.safeConversationId(id || 'default');
|
|
4520
|
+
this.assertPeerOwnerCanSwitch(clean);
|
|
4276
4521
|
// Conversation runners may bind a target workspace directly before their
|
|
4277
4522
|
// first setConversation(). Do not save state loaded for another workspace
|
|
4278
4523
|
// under the new workspace key during that hand-off.
|
|
@@ -4285,6 +4530,7 @@ class Agent {
|
|
|
4285
4530
|
return this.activeConversationId;
|
|
4286
4531
|
}
|
|
4287
4532
|
setConversationFromStorage(id) {
|
|
4533
|
+
this.assertPeerOwnerCanSwitch(id);
|
|
4288
4534
|
this.activeConversationId = this.safeConversationId(id || 'default');
|
|
4289
4535
|
const key = this.workspaceConversationKey();
|
|
4290
4536
|
if (key)
|
|
@@ -4294,6 +4540,8 @@ class Agent {
|
|
|
4294
4540
|
}
|
|
4295
4541
|
persistActiveConversationSelection(id, ws = this.workspace.current) {
|
|
4296
4542
|
const clean = this.safeConversationId(id || 'default');
|
|
4543
|
+
if (ws && this.workspace.current && path.resolve(ws.path) === path.resolve(this.workspace.current.path))
|
|
4544
|
+
this.assertPeerOwnerCanSwitch(clean);
|
|
4297
4545
|
this.mutateStoredConversationState(ws, latest => ({
|
|
4298
4546
|
version: 3,
|
|
4299
4547
|
activeConversationId: clean,
|
|
@@ -4319,6 +4567,9 @@ class Agent {
|
|
|
4319
4567
|
}
|
|
4320
4568
|
abortActiveKernelRun(reason = 'unspecified') {
|
|
4321
4569
|
let aborted = false;
|
|
4570
|
+
if (!this.isSubagentRuntime && (reason === 'user_stop' || reason === 'force_stop')) {
|
|
4571
|
+
this.subagents.broadcastStop(this.runtimeActorId, this.currentWorkRunId() || this.routeTransactionId || 'inactive', reason === 'force_stop');
|
|
4572
|
+
}
|
|
4322
4573
|
this.subagents.pauseScheduling();
|
|
4323
4574
|
if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
|
|
4324
4575
|
const abortError = new Error('Agent run aborted');
|
|
@@ -4339,6 +4590,7 @@ class Agent {
|
|
|
4339
4590
|
this.pendingAgentKernelQueue = [];
|
|
4340
4591
|
return aborted;
|
|
4341
4592
|
}
|
|
4593
|
+
hasRunningSubagents() { return this.activePeerAgents.size > 0; }
|
|
4342
4594
|
activeProcessSignal() {
|
|
4343
4595
|
return this.activeProcessAbortController?.signal;
|
|
4344
4596
|
}
|
|
@@ -4575,7 +4827,7 @@ class Agent {
|
|
|
4575
4827
|
].join('\n');
|
|
4576
4828
|
const maxTokens = Math.max(1024, Math.min(8192, Math.ceil(content.length / 4)) + 512);
|
|
4577
4829
|
const { temperature } = provider.intelligenceConfig('low');
|
|
4578
|
-
const generated = await this.withTimeout(
|
|
4830
|
+
const generated = await this.withTimeout(this.chatWithConversationUsage(provider, modelName, [{ role: 'user', content: prompt }], system, temperature, maxTokens, signal), 120000);
|
|
4579
4831
|
const summary = String(generated || '').trim();
|
|
4580
4832
|
if (!summary || /^\[LLM Error(?::|\])/i.test(summary)) {
|
|
4581
4833
|
return {
|
|
@@ -5423,6 +5675,11 @@ class Agent {
|
|
|
5423
5675
|
const stored = this.readStoredConversationState(ws);
|
|
5424
5676
|
stored.conversations = stored.conversations || {};
|
|
5425
5677
|
const previous = stateKey ? stored.conversations[stateKey] : undefined;
|
|
5678
|
+
const providerUsage = source.conversationProviderUsage ? source.conversationProviderUsage() : source.providerUsageTotals ? {
|
|
5679
|
+
version: 1,
|
|
5680
|
+
totals: this.normalizeProviderUsageCounters(source.providerUsageTotals),
|
|
5681
|
+
last: this.normalizeProviderUsageCounters(source.lastProviderUsage),
|
|
5682
|
+
} : previous?.providerUsage;
|
|
5426
5683
|
const sourceTitleGate = source.getConversationTitleGateState?.() || source.titleGateState;
|
|
5427
5684
|
const titleRequestMessageId = sourceTitleGate?.titleRequestMessageId === undefined
|
|
5428
5685
|
? String(previous?.titleRequestMessageId || '')
|
|
@@ -5434,6 +5691,7 @@ class Agent {
|
|
|
5434
5691
|
this.workspaceConversations.set(key, {
|
|
5435
5692
|
chatMessages: normalizedChatMessages,
|
|
5436
5693
|
history: [...source.history],
|
|
5694
|
+
providerUsage,
|
|
5437
5695
|
plan,
|
|
5438
5696
|
linkedPlan,
|
|
5439
5697
|
subagentState,
|
|
@@ -5459,6 +5717,7 @@ class Agent {
|
|
|
5459
5717
|
title,
|
|
5460
5718
|
chatMessages: normalizedChatMessages,
|
|
5461
5719
|
history: [...source.history],
|
|
5720
|
+
providerUsage,
|
|
5462
5721
|
plan,
|
|
5463
5722
|
linkedPlan,
|
|
5464
5723
|
subagentState,
|
|
@@ -5481,6 +5740,7 @@ class Agent {
|
|
|
5481
5740
|
if (this.safeConversationId(this.activeConversationId || 'default') === clean) {
|
|
5482
5741
|
this.chatMessages = normalizedChatMessages;
|
|
5483
5742
|
this.history = [...source.history];
|
|
5743
|
+
this.restoreProviderUsage(providerUsage);
|
|
5484
5744
|
this.conversationPlan = plan;
|
|
5485
5745
|
this.linkedPlan = linkedPlan;
|
|
5486
5746
|
if (source.subagents)
|
|
@@ -5728,22 +5988,143 @@ class Agent {
|
|
|
5728
5988
|
buildBlockTokens: estimate(buildBlockAsciiChars, buildBlockNonAsciiChars, buildBlockStructuralChars, true),
|
|
5729
5989
|
};
|
|
5730
5990
|
}
|
|
5731
|
-
|
|
5732
|
-
const bounded = (value) =>
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5991
|
+
normalizeProviderUsageCounters(input) {
|
|
5992
|
+
const bounded = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
5993
|
+
? Math.max(0, Math.floor(value)) : 0;
|
|
5994
|
+
return { input: bounded(input?.input), output: bounded(input?.output), cacheRead: bounded(input?.cacheRead), cacheWrite: bounded(input?.cacheWrite) };
|
|
5995
|
+
}
|
|
5996
|
+
conversationProviderUsage() {
|
|
5997
|
+
return {
|
|
5998
|
+
version: 1, totals: { ...this.providerUsageTotals }, last: { ...this.lastProviderUsage },
|
|
5999
|
+
accounting: { ...this.providerUsageAccounting },
|
|
6000
|
+
requestContext: this.lastRequestContext ? { ...this.lastRequestContext } : null,
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
6003
|
+
normalizedConversationUsage(saved) {
|
|
6004
|
+
const totals = this.normalizeProviderUsageCounters(saved?.version === 1 ? saved.totals : undefined);
|
|
6005
|
+
const accounting = (0, providerUsageAccounting_1.createAccounting)(saved?.version === 1 ? totals : undefined);
|
|
6006
|
+
if (saved?.version === 1 && saved.accounting) {
|
|
6007
|
+
for (const key of ['requests', 'inputReportedRequests', 'outputReportedRequests', 'cacheReportedRequests', 'cacheEligibleInputTokens', 'cacheReadTokens']) {
|
|
6008
|
+
const value = saved.accounting[key];
|
|
6009
|
+
accounting[key] = typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
6010
|
+
}
|
|
6011
|
+
accounting.hasLegacyTotals = saved.accounting.hasLegacyTotals === true;
|
|
6012
|
+
accounting.lastInputTokens = typeof saved.accounting.lastInputTokens === 'number' && Number.isFinite(saved.accounting.lastInputTokens)
|
|
6013
|
+
? Math.max(0, Math.floor(saved.accounting.lastInputTokens)) : null;
|
|
6014
|
+
}
|
|
6015
|
+
let requestContext = null;
|
|
6016
|
+
const context = saved?.version === 1 ? saved.requestContext : null;
|
|
6017
|
+
if (context && typeof context.requestId === 'string' && typeof context.runId === 'string' && typeof context.at === 'string') {
|
|
6018
|
+
requestContext = { requestId: context.requestId, runId: context.runId, model: String(context.model || ''), at: context.at,
|
|
6019
|
+
estimatedTokens: 0, longHistoryTokens: 0, buildBlockTokens: 0, systemPromptTokens: 0, toolSchemaTokens: 0, messageCount: 0, hasImages: context.hasImages === true };
|
|
6020
|
+
for (const key of ['estimatedTokens', 'longHistoryTokens', 'buildBlockTokens', 'systemPromptTokens', 'toolSchemaTokens', 'messageCount', 'inputTokens', 'cacheReadTokens']) {
|
|
6021
|
+
const value = context[key];
|
|
6022
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
6023
|
+
requestContext[key] = Math.max(0, Math.floor(value));
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
return { version: 1, totals, last: this.normalizeProviderUsageCounters(saved?.version === 1 ? saved.last : undefined), accounting, requestContext };
|
|
6027
|
+
}
|
|
6028
|
+
restoreProviderUsage(saved) {
|
|
6029
|
+
// Legacy counters remain spend evidence, but cannot prove reporting coverage.
|
|
6030
|
+
const usage = this.normalizedConversationUsage(saved);
|
|
6031
|
+
this.providerUsageTotals = usage.totals;
|
|
6032
|
+
this.lastProviderUsage = usage.last;
|
|
6033
|
+
this.providerUsageAccounting = usage.accounting;
|
|
6034
|
+
this.lastRequestContext = usage.requestContext || null;
|
|
6035
|
+
}
|
|
6036
|
+
mutateRequestUsage(request, mutate) {
|
|
6037
|
+
const activeKey = this.workspaceConversationStateKey();
|
|
6038
|
+
if (request.stateKey === activeKey && request.conversationId === this.activeConversationId) {
|
|
6039
|
+
const usage = { version: 1, totals: this.providerUsageTotals, last: this.lastProviderUsage,
|
|
6040
|
+
accounting: this.providerUsageAccounting, requestContext: this.lastRequestContext };
|
|
6041
|
+
mutate(usage);
|
|
6042
|
+
this.lastProviderUsage = usage.last;
|
|
6043
|
+
this.lastRequestContext = usage.requestContext || null;
|
|
6044
|
+
this.saveWorkspaceConversationState(false);
|
|
6045
|
+
return;
|
|
6046
|
+
}
|
|
6047
|
+
// A late auxiliary response belongs to the captured target, not whichever
|
|
6048
|
+
// conversation the user selected while it was running. Do not revive deletion.
|
|
6049
|
+
if (!request.workspace || !request.stateKey)
|
|
6050
|
+
return;
|
|
6051
|
+
this.mutateStoredConversationState(request.workspace, stored => {
|
|
6052
|
+
const entry = stored.conversations?.[request.stateKey];
|
|
6053
|
+
if (!entry)
|
|
6054
|
+
return stored;
|
|
6055
|
+
const usage = this.normalizedConversationUsage(entry.providerUsage);
|
|
6056
|
+
mutate(usage);
|
|
6057
|
+
entry.providerUsage = usage;
|
|
6058
|
+
entry.updatedAt = new Date(Math.max(Date.now(), (Date.parse(entry.updatedAt || '') || 0) + 1)).toISOString();
|
|
6059
|
+
// A different active conversation can already have a debounced snapshot
|
|
6060
|
+
// of this same store. Update only this entry's usage in that snapshot so
|
|
6061
|
+
// its later flush cannot roll back the late measurement or discard B.
|
|
6062
|
+
const storePath = this.workspaceConversationStorePath(request.workspace);
|
|
6063
|
+
const dirtyEntry = storePath ? this.conversationStateDirty.get(storePath)?.state.conversations?.[request.stateKey] : undefined;
|
|
6064
|
+
if (dirtyEntry)
|
|
6065
|
+
dirtyEntry.providerUsage = structuredClone(usage);
|
|
6066
|
+
const memoryKey = `${request.workspace.isInternal ? 'internal' : 'external'}:${path.resolve(request.workspace.path)}::conversation:${request.conversationId}`;
|
|
6067
|
+
const memory = this.workspaceConversations.get(memoryKey);
|
|
6068
|
+
if (memory) {
|
|
6069
|
+
memory.providerUsage = structuredClone(usage);
|
|
6070
|
+
memory.updatedAt = entry.updatedAt;
|
|
6071
|
+
}
|
|
6072
|
+
return stored;
|
|
6073
|
+
});
|
|
6074
|
+
}
|
|
6075
|
+
beginProviderUsageRequest() {
|
|
6076
|
+
const request = {
|
|
6077
|
+
id: crypto.randomUUID(), conversationId: this.activeConversationId,
|
|
6078
|
+
workspace: this.workspace.current ? { ...this.workspace.current } : null,
|
|
6079
|
+
stateKey: this.workspaceConversationStateKey(),
|
|
6080
|
+
accounting: (0, providerUsageAccounting_1.beginAccountingRequest)((0, providerUsageAccounting_1.createAccounting)()),
|
|
5738
6081
|
};
|
|
5739
|
-
this.
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
6082
|
+
this.mutateRequestUsage(request, usage => { request.accounting = (0, providerUsageAccounting_1.beginAccountingRequest)(usage.accounting); });
|
|
6083
|
+
return request;
|
|
6084
|
+
}
|
|
6085
|
+
recordRequestContext(request, messages, system, tools, model) {
|
|
6086
|
+
const context = {
|
|
6087
|
+
...(0, requestContextEstimate_1.estimateSubmittedContext)(messages, system, tools, this.compressionBuildBlockStart(messages)),
|
|
6088
|
+
requestId: request.id, runId: this.currentWorkRunId(), model, at: new Date().toISOString(),
|
|
6089
|
+
};
|
|
6090
|
+
this.mutateRequestUsage(request, usage => { usage.requestContext = context; });
|
|
6091
|
+
}
|
|
6092
|
+
recordProviderUsage(input, request = this.beginProviderUsageRequest()) {
|
|
6093
|
+
this.mutateRequestUsage(request, usage => {
|
|
6094
|
+
(0, providerUsageAccounting_1.applyAccountingUsage)(usage.accounting, usage.totals, request.accounting, input);
|
|
6095
|
+
usage.last = { ...request.accounting.usage };
|
|
6096
|
+
if (usage.requestContext?.requestId === request.id) {
|
|
6097
|
+
if (request.accounting.reported.input)
|
|
6098
|
+
usage.requestContext.inputTokens = request.accounting.usage.input;
|
|
6099
|
+
if (request.accounting.reported.cacheRead)
|
|
6100
|
+
usage.requestContext.cacheReadTokens = request.accounting.usage.cacheRead;
|
|
6101
|
+
}
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
async chatWithConversationUsage(provider, ...args) {
|
|
6105
|
+
// Bind before awaiting. The callback runs for successful responses even if
|
|
6106
|
+
// usage is absent; failed HTTP/model-probe requests are not invented spend.
|
|
6107
|
+
const workspace = this.workspace.current ? { ...this.workspace.current } : null;
|
|
6108
|
+
const conversationId = this.activeConversationId;
|
|
6109
|
+
const stateKey = this.workspaceConversationStateKey();
|
|
6110
|
+
let request;
|
|
6111
|
+
const callback = args[7];
|
|
6112
|
+
args[7] = input => {
|
|
6113
|
+
if (!request) {
|
|
6114
|
+
request = { id: crypto.randomUUID(), workspace, conversationId, stateKey, accounting: (0, providerUsageAccounting_1.beginAccountingRequest)((0, providerUsageAccounting_1.createAccounting)()) };
|
|
6115
|
+
this.mutateRequestUsage(request, usage => { request.accounting = (0, providerUsageAccounting_1.beginAccountingRequest)(usage.accounting); });
|
|
6116
|
+
}
|
|
6117
|
+
this.recordProviderUsage(input, request);
|
|
6118
|
+
callback?.(input);
|
|
6119
|
+
};
|
|
6120
|
+
return provider.chat(...args);
|
|
5744
6121
|
}
|
|
5745
6122
|
contextWindow(modelName = this.model) {
|
|
5746
|
-
const
|
|
6123
|
+
const activeRequest = this.activeAgentKernelRuntime && this.lastRequestContext?.runId === this.currentWorkRunId()
|
|
6124
|
+
? this.lastRequestContext : null;
|
|
6125
|
+
const historyEstimate = activeRequest ? null
|
|
6126
|
+
: (0, requestContextEstimate_1.estimateSubmittedContext)(this.history, '', [], this.compressionBuildBlockStart(this.history));
|
|
6127
|
+
const estimatedTokens = activeRequest?.estimatedTokens ?? historyEstimate.estimatedTokens;
|
|
5747
6128
|
// Display and compression must share one window resolution. Both resolve
|
|
5748
6129
|
// the auto branch through the active (routed) deployment so the UI ring and
|
|
5749
6130
|
// the compaction trigger stay on the same maxTokens even after an Auto
|
|
@@ -5753,14 +6134,15 @@ class Agent {
|
|
|
5753
6134
|
const maxTokens = Math.max(1, Number(model?.max_tokens || 0) || 128000);
|
|
5754
6135
|
const ratio = estimatedTokens / maxTokens;
|
|
5755
6136
|
const budget = this.compressionBudget(this.history, modelName);
|
|
6137
|
+
const cache = (0, providerUsageAccounting_1.ratioSummary)(this.providerUsageAccounting);
|
|
5756
6138
|
return {
|
|
5757
6139
|
estimatedTokens,
|
|
5758
6140
|
maxTokens,
|
|
5759
6141
|
ratio,
|
|
5760
6142
|
warning: ratio >= 1 ? 'over_limit' : ratio >= 0.85 ? 'near_limit' : 'ok',
|
|
5761
6143
|
model: modelName,
|
|
5762
|
-
buildBlockTokens:
|
|
5763
|
-
longHistoryTokens:
|
|
6144
|
+
buildBlockTokens: activeRequest?.buildBlockTokens ?? historyEstimate.buildBlockTokens,
|
|
6145
|
+
longHistoryTokens: activeRequest?.longHistoryTokens ?? historyEstimate.longHistoryTokens,
|
|
5764
6146
|
buildBlockTriggerTokens: budget.buildBlockTriggerTokens,
|
|
5765
6147
|
longHistoryTriggerTokens: budget.longHistoryTriggerTokens,
|
|
5766
6148
|
buildBlockRetentionTokens: budget.buildBlockRetentionTokens,
|
|
@@ -5775,9 +6157,20 @@ class Agent {
|
|
|
5775
6157
|
providerOutputTokens: this.providerUsageTotals.output,
|
|
5776
6158
|
providerCacheReadTokens: this.providerUsageTotals.cacheRead,
|
|
5777
6159
|
providerCacheWriteTokens: this.providerUsageTotals.cacheWrite,
|
|
5778
|
-
providerCacheReadRatio:
|
|
5779
|
-
|
|
5780
|
-
|
|
6160
|
+
providerCacheReadRatio: cache.totalRatio,
|
|
6161
|
+
providerKnownCacheReadRatio: cache.knownRatio,
|
|
6162
|
+
providerCacheEligibleInputTokens: cache.denominator,
|
|
6163
|
+
providerUsageRequests: this.providerUsageAccounting.requests,
|
|
6164
|
+
providerUsageInputReportedRequests: this.providerUsageAccounting.inputReportedRequests,
|
|
6165
|
+
providerUsageOutputReportedRequests: this.providerUsageAccounting.outputReportedRequests,
|
|
6166
|
+
providerUsageCacheReportedRequests: this.providerUsageAccounting.cacheReportedRequests,
|
|
6167
|
+
providerUsageHasLegacyTotals: this.providerUsageAccounting.hasLegacyTotals,
|
|
6168
|
+
providerLastInputTokens: this.providerUsageAccounting.lastInputTokens,
|
|
6169
|
+
requestContext: this.lastRequestContext ? { ...this.lastRequestContext } : null,
|
|
6170
|
+
contextEstimateSource: activeRequest ? 'active_request' : 'history',
|
|
6171
|
+
contextEstimateHasImages: activeRequest?.hasImages ?? historyEstimate.hasImages,
|
|
6172
|
+
systemPromptTokens: activeRequest?.systemPromptTokens ?? 0,
|
|
6173
|
+
toolSchemaTokens: activeRequest?.toolSchemaTokens ?? 0,
|
|
5781
6174
|
};
|
|
5782
6175
|
}
|
|
5783
6176
|
resolveWindowModel(modelName) {
|
|
@@ -5827,16 +6220,17 @@ class Agent {
|
|
|
5827
6220
|
}
|
|
5828
6221
|
compressionBuildBlockStart(messages) {
|
|
5829
6222
|
const activeRunId = this.currentWorkRunId();
|
|
6223
|
+
// No active Build means every retained message is historical, including
|
|
6224
|
+
// legacy assistant/tool tails whose old persistence omitted run metadata.
|
|
6225
|
+
if (!activeRunId)
|
|
6226
|
+
return messages.length;
|
|
5830
6227
|
if (activeRunId) {
|
|
5831
6228
|
const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
|
|
5832
6229
|
if (index >= 0)
|
|
5833
6230
|
return index;
|
|
5834
6231
|
}
|
|
5835
|
-
//
|
|
5836
|
-
//
|
|
5837
|
-
// - 存在带 run_id 的消息:boundary 取最后一个 run 的起点之后(该 run 及其
|
|
5838
|
-
// 之前的历史属于长期历史,其后无归属的消息属于当前未命名区块);
|
|
5839
|
-
// - 完全没有 run_id:全部历史都是长期历史(不存在当前 Build Block)。
|
|
6232
|
+
// Legacy active transcripts may lack the current run tag. Retain the
|
|
6233
|
+
// historical tagged prefix; ordinary new inputs now carry their run_id.
|
|
5840
6234
|
let lastRunBoundary = -1;
|
|
5841
6235
|
for (let index = 0; index < messages.length; index += 1) {
|
|
5842
6236
|
if (String(messages[index]?.run_id || messages[index]?.runId || ''))
|
|
@@ -6189,6 +6583,7 @@ class Agent {
|
|
|
6189
6583
|
title: this.titleFromMessages(messages, clean),
|
|
6190
6584
|
chatMessages: messages,
|
|
6191
6585
|
history: sourceHistory,
|
|
6586
|
+
providerUsage: memory?.providerUsage,
|
|
6192
6587
|
plan: memory?.plan,
|
|
6193
6588
|
linkedPlan: memory?.linkedPlan,
|
|
6194
6589
|
subagentState: memory?.subagentState,
|
|
@@ -6286,6 +6681,7 @@ class Agent {
|
|
|
6286
6681
|
title: this.titleFromMessages(messages, clean),
|
|
6287
6682
|
chatMessages: messages,
|
|
6288
6683
|
history: sourceHistory,
|
|
6684
|
+
providerUsage: memory?.providerUsage,
|
|
6289
6685
|
plan: memory?.plan,
|
|
6290
6686
|
linkedPlan: memory?.linkedPlan,
|
|
6291
6687
|
subagentState: memory?.subagentState,
|
|
@@ -6702,7 +7098,7 @@ class Agent {
|
|
|
6702
7098
|
preference: Number.isFinite(Number(routeMetadata.route_preference)) ? Number(routeMetadata.route_preference) : undefined,
|
|
6703
7099
|
fallbackOnly: !!model.fallback_only,
|
|
6704
7100
|
};
|
|
6705
|
-
})
|
|
7101
|
+
});
|
|
6706
7102
|
}
|
|
6707
7103
|
persistRouteDecision(decision) {
|
|
6708
7104
|
try {
|
|
@@ -6749,15 +7145,11 @@ class Agent {
|
|
|
6749
7145
|
catch { }
|
|
6750
7146
|
}
|
|
6751
7147
|
allModelNames() {
|
|
6752
|
-
const names = this.config.allModels().filter(m => {
|
|
6753
|
-
const status = String(m.evaluation?.status || 'unvalidated');
|
|
6754
|
-
const validationStatus = m.validation?.status;
|
|
6755
|
-
return status === 'available' || status === 'unvalidated' || validationStatus === 'verified' || validationStatus === 'degraded';
|
|
6756
|
-
}).map(m => {
|
|
7148
|
+
const names = this.config.allModels().filter(m => m.enabled !== false).map(m => {
|
|
6757
7149
|
const label = m.display || m.name;
|
|
6758
7150
|
return `${m.provider} / ${label}`;
|
|
6759
7151
|
});
|
|
6760
|
-
return this.config.autoSwitchEnabled() &&
|
|
7152
|
+
return this.config.autoSwitchEnabled() && names.length > 0 ? ['auto', ...names] : names;
|
|
6761
7153
|
}
|
|
6762
7154
|
async evaluateAndSwitch(task, override = undefined) {
|
|
6763
7155
|
if (!this.config.autoSwitchEnabled() || this.model !== 'auto')
|
|
@@ -6847,39 +7239,22 @@ class Agent {
|
|
|
6847
7239
|
return !!this.resolvedDeployment && before !== deploymentIdentity(this.resolvedDeployment);
|
|
6848
7240
|
}
|
|
6849
7241
|
shouldExposeToolInterface() {
|
|
6850
|
-
|
|
6851
|
-
// historical behavior. Auto can safely suppress schemas because its
|
|
6852
|
-
// eligible deployments have explicit Standard/Extended evidence.
|
|
6853
|
-
if (this.model !== 'auto')
|
|
6854
|
-
return true;
|
|
6855
|
-
const model = this.activeModelConfig();
|
|
6856
|
-
if (!model)
|
|
6857
|
-
return false;
|
|
6858
|
-
const validation = model.validation;
|
|
6859
|
-
if (validation?.level !== 'standard' && validation?.level !== 'extended')
|
|
6860
|
-
return true;
|
|
6861
|
-
return validation.capabilities?.tool_use === true || validation.capabilities?.tools === true;
|
|
7242
|
+
return true;
|
|
6862
7243
|
}
|
|
6863
7244
|
modelIsUnavailable(modelName) {
|
|
6864
7245
|
const model = modelName === this.model || modelName === 'auto'
|
|
6865
|
-
? this.activeModelConfig()
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
const validationStatus = effectiveModelValidationStatus(model);
|
|
6870
|
-
// `discovered` means unvalidated, not a failed endpoint. Explicit fixed
|
|
6871
|
-
// selections remain usable for backwards compatibility; only an executed
|
|
6872
|
-
// Basic/Standard/Extended validation may pre-emptively mark them bad.
|
|
6873
|
-
if (model.validation?.level !== 'discovered'
|
|
6874
|
-
&& (validationStatus === 'unavailable' || validationStatus === 'auth_error' || validationStatus === 'invalid_config'))
|
|
6875
|
-
return true;
|
|
6876
|
-
const status = validationStatus === 'degraded' ? 'degraded' : String(model?.evaluation?.status || '').toLowerCase();
|
|
6877
|
-
return status === 'unavailable' || status.startsWith('error');
|
|
7246
|
+
? this.activeModelConfig() : (parseDeploymentSelectionValue(modelName) ? this.config.findDeployment(parseDeploymentSelectionValue(modelName)) : this.config.findModel(modelName));
|
|
7247
|
+
// Only configuration/user switches can prevent a request. Response and
|
|
7248
|
+
// validation observations are labels, never permission to disable a model.
|
|
7249
|
+
return !model || model.enabled === false;
|
|
6878
7250
|
}
|
|
7251
|
+
failedDeploymentsThisRun = new Set();
|
|
6879
7252
|
switchToFallbackModel(errorText = 'transport failure') {
|
|
6880
7253
|
const fallbackEnabled = this.config.getBool('models', 'fallback_on_unavailable');
|
|
6881
7254
|
const observedFailure = (0, autoRouter_1.classifyRouteFailure)(errorText);
|
|
6882
7255
|
const observedDeployment = this.activeDeployment();
|
|
7256
|
+
if (observedDeployment)
|
|
7257
|
+
this.failedDeploymentsThisRun.add(deploymentIdentity(observedDeployment));
|
|
6883
7258
|
// Keep balance exhaustion scoped to the deployment that actually failed.
|
|
6884
7259
|
// Provider adapters normally record this before returning an error, but
|
|
6885
7260
|
// fallback callers are also a public recovery boundary and must not rely
|
|
@@ -6951,7 +7326,7 @@ class Agent {
|
|
|
6951
7326
|
|| deploymentIdentity(this.deploymentRef(m)) !== deploymentIdentity(currentDeployment));
|
|
6952
7327
|
if (!all.length)
|
|
6953
7328
|
return null;
|
|
6954
|
-
const usable = all.filter(m => !this.
|
|
7329
|
+
const usable = all.filter(m => !modelConfigIsUnavailable(m) && !this.failedDeploymentsThisRun.has(deploymentIdentity(this.deploymentRef(m))));
|
|
6955
7330
|
if (!usable.length)
|
|
6956
7331
|
return null;
|
|
6957
7332
|
const pref = this.config.autoSwitchPreference();
|
|
@@ -7183,8 +7558,8 @@ class Agent {
|
|
|
7183
7558
|
/**
|
|
7184
7559
|
* Final visual safety net: OCR each submitted image and ask a text-only
|
|
7185
7560
|
* request to conservatively repair the OCR. This is intentionally callable
|
|
7186
|
-
* only after a visual-input refusal
|
|
7187
|
-
*
|
|
7561
|
+
* only after a real visual-input refusal. Cached capabilities never bypass
|
|
7562
|
+
* the initial image request, and OCR does not disable later image attempts.
|
|
7188
7563
|
*/
|
|
7189
7564
|
async finalVisualFallback(errorText, signal) {
|
|
7190
7565
|
if (!/(?:vision|image|multimodal|image_url|input_image).*(?:not supported|unsupported|拒绝|不支持|failed|failure|invalid)|(?:not supported|unsupported|拒绝|不支持).*(?:vision|image|multimodal|image_url|input_image)/i.test(String(errorText || '')))
|
|
@@ -7192,11 +7567,6 @@ class Agent {
|
|
|
7192
7567
|
const current = this.activeModelConfig();
|
|
7193
7568
|
if (!current)
|
|
7194
7569
|
return null;
|
|
7195
|
-
const alternateVision = this.config.allModels().some(model => model.enabled !== false && model.provider_id === current.provider_id &&
|
|
7196
|
-
model.name !== current.name && !!model.vision && !!model.api_key && !!model.provider_url &&
|
|
7197
|
-
!['unavailable', 'auth_error', 'invalid_config'].includes(String(model.evaluation?.status || model.validation?.status || '').toLowerCase()));
|
|
7198
|
-
if (alternateVision)
|
|
7199
|
-
return null;
|
|
7200
7570
|
const latest = [...this.history].reverse().find(item => item?.role === 'user');
|
|
7201
7571
|
const parts = latest?.content && Array.isArray(latest.content) ? latest.content : [];
|
|
7202
7572
|
const images = parts.map(part => {
|
|
@@ -7212,41 +7582,91 @@ class Agent {
|
|
|
7212
7582
|
if (result.ok && result.text.trim())
|
|
7213
7583
|
ocr.push({ index: index + 1, text: result.text.slice(0, 50_000), confidence: result.confidence });
|
|
7214
7584
|
}
|
|
7215
|
-
catch {
|
|
7585
|
+
catch (error) {
|
|
7586
|
+
if (signal?.aborted)
|
|
7587
|
+
throw error;
|
|
7588
|
+
}
|
|
7216
7589
|
}
|
|
7590
|
+
if (signal?.aborted)
|
|
7591
|
+
throw signal.reason || new Error('Aborted');
|
|
7217
7592
|
if (!ocr.length)
|
|
7218
7593
|
return JSON.stringify({ ok: false, fallback: 'mini_ocr_llm', error: 'Local OCR returned no readable text; no visual content was fabricated.' });
|
|
7219
|
-
const task = typeof latest?.content === 'string' ? latest.content : '';
|
|
7594
|
+
const task = typeof latest?.content === 'string' ? latest.content : parts.filter(part => part.type === 'text').map(part => String(part.text || '')).join('\n');
|
|
7220
7595
|
const evidence = ocr.map(item => `Image ${item.index} (OCR confidence ${item.confidence.toFixed(1)}):\n${item.text}`).join('\n\n');
|
|
7221
7596
|
const prompt = `The provider rejected image input. Answer the user's task using only this approximate OCR evidence. Correct obvious character, spacing, and line-break errors only when supported by context. Preserve [uncertain] markers for ambiguity and never invent missing visual content.\nUser task:\n${task.slice(0, 12_000)}\nOCR evidence:\n${evidence}`;
|
|
7222
7597
|
let corrected = '';
|
|
7223
7598
|
try {
|
|
7224
7599
|
const provider = this.engineModel();
|
|
7225
7600
|
if (provider)
|
|
7226
|
-
corrected = String(await
|
|
7601
|
+
corrected = String(await this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: prompt }], 'You are a text-only OCR correction assistant. Be conservative and explicit about uncertainty.', 0.05, 3000, signal) || '').trim();
|
|
7602
|
+
}
|
|
7603
|
+
catch (error) {
|
|
7604
|
+
if (signal?.aborted)
|
|
7605
|
+
throw error;
|
|
7227
7606
|
}
|
|
7228
|
-
catch { }
|
|
7229
7607
|
return JSON.stringify({
|
|
7230
7608
|
ok: !!(corrected || ocr.length),
|
|
7231
7609
|
fallback: 'mini_ocr_llm',
|
|
7232
7610
|
approximate: true,
|
|
7233
|
-
warning: '视觉输入被拒绝;以下内容来自本地 OCR,并经文本模型保守校正,可能不完整。',
|
|
7611
|
+
warning: corrected ? '视觉输入被拒绝;以下内容来自本地 OCR,并经文本模型保守校正,可能不完整。' : '视觉输入被拒绝,文本校正请求也未成功;以下为未经校正的本地 OCR 结果,可能不完整。',
|
|
7234
7612
|
raw_ocr: ocr,
|
|
7235
7613
|
corrected: corrected || ocr.map(item => item.text).join('\n\n'),
|
|
7236
7614
|
uncertainty: corrected ? 'preserved' : 'raw_ocr_only',
|
|
7237
7615
|
}, null, 2);
|
|
7238
7616
|
}
|
|
7239
|
-
engineModel() {
|
|
7617
|
+
engineModel(cache) {
|
|
7240
7618
|
if (this.forcedProvider) {
|
|
7241
7619
|
const active = this.activeDeployment();
|
|
7242
7620
|
if (!this.forcedProviderDeployment
|
|
7243
|
-
|| (active && deploymentIdentity(active) === this.forcedProviderDeployment))
|
|
7621
|
+
|| (active && deploymentIdentity(active) === this.forcedProviderDeployment)) {
|
|
7622
|
+
if (cache) {
|
|
7623
|
+
delete cache.key;
|
|
7624
|
+
delete cache.provider;
|
|
7625
|
+
}
|
|
7244
7626
|
return this.forcedProvider;
|
|
7627
|
+
}
|
|
7245
7628
|
}
|
|
7246
7629
|
const m = this.activeModelConfig();
|
|
7247
|
-
if (!m)
|
|
7630
|
+
if (!m) {
|
|
7631
|
+
if (cache) {
|
|
7632
|
+
delete cache.key;
|
|
7633
|
+
delete cache.provider;
|
|
7634
|
+
}
|
|
7248
7635
|
return null;
|
|
7249
|
-
|
|
7636
|
+
}
|
|
7637
|
+
return this.modelProvider(m, cache);
|
|
7638
|
+
}
|
|
7639
|
+
modelProvider(m, cache) {
|
|
7640
|
+
const apiMode = this.config.openAIApiMode();
|
|
7641
|
+
const adapters = this.config.contextFlag('provider_adapters_v2');
|
|
7642
|
+
const thinkingMaps = this.modelThinkingTierMaps(m);
|
|
7643
|
+
const proxy = this.providerProxyConfig();
|
|
7644
|
+
const create = () => (0, modelResponseHealth_1.observeModelResponses)(new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, apiMode, adapters, undefined, thinkingMaps, proxy), updates => (0, modelResponseHealth_1.recordModelResponseHealth)(this.config.rootPath, {
|
|
7645
|
+
providerId: m.provider_id, modelId: m.name, endpoint: m.provider_url, protocol: m.provider_protocol, credential: m.api_key,
|
|
7646
|
+
}, updates));
|
|
7647
|
+
if (!cache)
|
|
7648
|
+
return create();
|
|
7649
|
+
// Match the provider's configured-proxy/environment precedence. A changed
|
|
7650
|
+
// effective proxy must not reuse a dispatcher created for its predecessor.
|
|
7651
|
+
const effectiveProxyUrl = proxy.enabled === false ? '' : (proxy.url.trim()
|
|
7652
|
+
|| process.env.HTTPS_PROXY || process.env.HTTP_PROXY
|
|
7653
|
+
|| process.env.https_proxy || process.env.http_proxy || '');
|
|
7654
|
+
const orderedMaps = Object.keys(thinkingMaps || {}).sort().map(model => [
|
|
7655
|
+
model,
|
|
7656
|
+
Object.keys(thinkingMaps[model]).sort().map(tier => [tier, thinkingMaps[model][tier]]),
|
|
7657
|
+
]);
|
|
7658
|
+
const key = crypto.createHash('sha256').update(JSON.stringify([
|
|
7659
|
+
m.provider_id, m.name, m.logical_model_group_id || '',
|
|
7660
|
+
m.provider, m.provider_url, m.api_key, m.provider_protocol,
|
|
7661
|
+
apiMode, adapters, orderedMaps,
|
|
7662
|
+
proxy.enabled, proxy.url, proxy.auth, effectiveProxyUrl,
|
|
7663
|
+
])).digest('hex');
|
|
7664
|
+
if (cache.key === key && cache.provider)
|
|
7665
|
+
return cache.provider;
|
|
7666
|
+
const provider = create();
|
|
7667
|
+
cache.key = key;
|
|
7668
|
+
cache.provider = provider;
|
|
7669
|
+
return provider;
|
|
7250
7670
|
}
|
|
7251
7671
|
/**
|
|
7252
7672
|
* dev-0.4.3 模型原生思考强度档位映射表(模型名 → thinking_tier_map)。
|
|
@@ -7540,6 +7960,7 @@ class Agent {
|
|
|
7540
7960
|
return [{ type: 'text', text: '[Workspace required] Select or create a workspace before starting a conversation.' }];
|
|
7541
7961
|
}
|
|
7542
7962
|
if (this.processDepth === 0) {
|
|
7963
|
+
this.failedDeploymentsThisRun.clear();
|
|
7543
7964
|
this.processingConversationId = this.activeConversationId || 'default';
|
|
7544
7965
|
this.activeProcessAbortController = new AbortController();
|
|
7545
7966
|
this.subagents.resumeScheduling();
|
|
@@ -7558,27 +7979,6 @@ class Agent {
|
|
|
7558
7979
|
this.fileDiffs = [];
|
|
7559
7980
|
this.pendingOptions = [];
|
|
7560
7981
|
try {
|
|
7561
|
-
if (this.model === 'auto') {
|
|
7562
|
-
// Auto routing re-resolves each turn; drop a stale blocked deployment
|
|
7563
|
-
// so the router can pick an unblocked candidate instead of failing here.
|
|
7564
|
-
if (this.resolvedDeployment && this.isBalanceBlockedDeployment(this.resolvedDeployment)) {
|
|
7565
|
-
this.resolvedDeployment = null;
|
|
7566
|
-
this.lastRouteDecision = null;
|
|
7567
|
-
this.pendingAutoAttempts = [];
|
|
7568
|
-
}
|
|
7569
|
-
}
|
|
7570
|
-
else {
|
|
7571
|
-
const blockedMs = this.balanceBlockedMs();
|
|
7572
|
-
if (blockedMs > 0) {
|
|
7573
|
-
const waitSeconds = Math.max(1, Math.ceil(blockedMs / 1000));
|
|
7574
|
-
const deployment = this.activeDeployment();
|
|
7575
|
-
const label = deployment ? `${deployment.providerId}/${deployment.modelId}` : this.model;
|
|
7576
|
-
const message = `Provider balance exhausted (HTTP 402) for ${label}. Requests on this deployment are paused for ${waitSeconds}s; switch provider or model to continue immediately.`;
|
|
7577
|
-
this.status = 'error';
|
|
7578
|
-
this.emitWorkEvent({ type: 'error', content: message });
|
|
7579
|
-
throw new Error(message);
|
|
7580
|
-
}
|
|
7581
|
-
}
|
|
7582
7982
|
let text = typeof input === 'string' ? input : String(input.text || '');
|
|
7583
7983
|
const inputEnvelope = typeof input === 'string' ? null : input;
|
|
7584
7984
|
let hiddenUserInput = inputEnvelope?.hiddenUserInput === true;
|
|
@@ -7586,6 +7986,7 @@ class Agent {
|
|
|
7586
7986
|
if (!explicitFixedModel)
|
|
7587
7987
|
this.ensureUsableModelSelection();
|
|
7588
7988
|
let clientMessageId = String(inputEnvelope?.clientMessageId || '').trim();
|
|
7989
|
+
const userMessageId = String(inputEnvelope?.userMessageId || '').trim();
|
|
7589
7990
|
const inputRunId = String(inputEnvelope?.runId || this.activeWorkRunId || '').trim();
|
|
7590
7991
|
let rawImages = typeof input === 'string' ? [] : (Array.isArray(input.images) ? input.images : []);
|
|
7591
7992
|
// dev-0.4.3 Guide 批量续接:同一 Build block 内连续到达的多个 Guide
|
|
@@ -7674,16 +8075,6 @@ class Agent {
|
|
|
7674
8075
|
await this.evaluateAndSwitch(`${text}\n[image attachment]`, inputEnvelope?.routePolicy);
|
|
7675
8076
|
autoRouteEvaluated = true;
|
|
7676
8077
|
}
|
|
7677
|
-
const selectedModel = this.activeModelConfig();
|
|
7678
|
-
if (images.length && !selectedModel?.vision) {
|
|
7679
|
-
// Give the normal route planner first chance to select another
|
|
7680
|
-
// same-provider vision deployment. If none is available, the kernel
|
|
7681
|
-
// preflight invokes the final mini-OCR + text-only correction path.
|
|
7682
|
-
const hasSameProviderVision = selectedModel && this.config.allModels().some(model => model.enabled !== false && model.provider_id === selectedModel.provider_id &&
|
|
7683
|
-
model.name !== selectedModel.name && !!model.vision);
|
|
7684
|
-
if (hasSameProviderVision)
|
|
7685
|
-
this.switchToFallbackModel('vision input not supported by the selected model');
|
|
7686
|
-
}
|
|
7687
8078
|
const now = this.nowLabel();
|
|
7688
8079
|
const visibleUserInput = inputEnvelope?.visibleUserInput === undefined
|
|
7689
8080
|
? text
|
|
@@ -7692,7 +8083,7 @@ class Agent {
|
|
|
7692
8083
|
const historyContent = images.length
|
|
7693
8084
|
? [{ type: 'text', text }, ...images.map(image => ({ type: 'image_url', image_url: { url: image.dataUrl } }))]
|
|
7694
8085
|
: text;
|
|
7695
|
-
if (clientMessageId) {
|
|
8086
|
+
if (clientMessageId && !userMessageId) {
|
|
7696
8087
|
this.persistGuideMessage(clientMessageId, displayText, inputRunId, historyContent, attachments, String(inputEnvelope?.guideId || ''));
|
|
7697
8088
|
if (inputRunId) {
|
|
7698
8089
|
this.recordGuideReceipt({
|
|
@@ -7709,19 +8100,30 @@ class Agent {
|
|
|
7709
8100
|
}
|
|
7710
8101
|
}
|
|
7711
8102
|
else if (!hiddenUserInput) {
|
|
7712
|
-
const messageId = crypto.randomUUID();
|
|
7713
|
-
this.chatMessages.
|
|
8103
|
+
const messageId = userMessageId || crypto.randomUUID();
|
|
8104
|
+
const persistedUser = userMessageId ? this.chatMessages.find(message => message.role === 'user' && message.messageId === userMessageId) : undefined;
|
|
8105
|
+
const userMessage = {
|
|
7714
8106
|
messageId,
|
|
7715
8107
|
branchNodeId: this.currentBranchNodeId(),
|
|
7716
8108
|
role: 'user',
|
|
7717
8109
|
content: displayText,
|
|
7718
8110
|
mode: String(inputEnvelope?.visibleMode || this.modeName()),
|
|
7719
8111
|
model: this.model,
|
|
7720
|
-
timestamp: now,
|
|
8112
|
+
timestamp: persistedUser?.timestamp || now,
|
|
7721
8113
|
attachments: attachments.length ? attachments : undefined,
|
|
7722
8114
|
runId: this.currentWorkRunId() || undefined,
|
|
7723
|
-
}
|
|
7724
|
-
|
|
8115
|
+
};
|
|
8116
|
+
if (persistedUser)
|
|
8117
|
+
Object.assign(persistedUser, userMessage);
|
|
8118
|
+
else
|
|
8119
|
+
this.chatMessages.push(userMessage);
|
|
8120
|
+
const persistedHistory = userMessageId ? this.history.find(message => message.role === 'user' && message.user_message_id === userMessageId) : undefined;
|
|
8121
|
+
if (persistedHistory) {
|
|
8122
|
+
persistedHistory.content = historyContent;
|
|
8123
|
+
persistedHistory.run_id = this.currentWorkRunId() || undefined;
|
|
8124
|
+
}
|
|
8125
|
+
else
|
|
8126
|
+
this.history.push({ role: 'user', content: historyContent, run_id: this.currentWorkRunId() || undefined, ...(userMessageId ? { user_message_id: userMessageId } : {}) });
|
|
7725
8127
|
}
|
|
7726
8128
|
else {
|
|
7727
8129
|
this.history.push({
|
|
@@ -7734,10 +8136,16 @@ class Agent {
|
|
|
7734
8136
|
}
|
|
7735
8137
|
if (!hiddenUserInput)
|
|
7736
8138
|
this.recordWorkRunPrimaryPrompt(displayText);
|
|
8139
|
+
const submittedHistoryMessage = userMessageId
|
|
8140
|
+
? this.history.find(message => message.role === 'user' && message.user_message_id === userMessageId)
|
|
8141
|
+
: this.history.at(-1);
|
|
8142
|
+
const submittedChatMessage = userMessageId
|
|
8143
|
+
? this.chatMessages.find(message => message.role === 'user' && message.messageId === userMessageId)
|
|
8144
|
+
: [...this.chatMessages].reverse().find(message => message.role === 'user');
|
|
7737
8145
|
this.saveWorkspaceConversationState(true);
|
|
7738
8146
|
let firstResponseGateMessageId = '';
|
|
7739
8147
|
let firstResponseTitleInput = null;
|
|
7740
|
-
if (!hiddenUserInput && !clientMessageId && !this.firstAgentResponseStarted) {
|
|
8148
|
+
if (!hiddenUserInput && (!clientMessageId || !!userMessageId) && !this.firstAgentResponseStarted) {
|
|
7741
8149
|
const firstInput = this.firstPersistedUserForTitleGate();
|
|
7742
8150
|
if (!firstInput)
|
|
7743
8151
|
throw new Error('Conversation title generation failed; the first persisted user input is unavailable.');
|
|
@@ -7786,6 +8194,14 @@ class Agent {
|
|
|
7786
8194
|
// Agent provider request.
|
|
7787
8195
|
this.notifyAgentKernelUserMessageStart(text, clientMessageId || undefined);
|
|
7788
8196
|
this.emitWorkEvent({ type: 'start', content: 'Preparing request.' });
|
|
8197
|
+
// Standalone Agent runs acquire their run ID at start, after the title
|
|
8198
|
+
// gate. Bind the captured input now, without moving or bypassing that gate.
|
|
8199
|
+
const startedRunId = this.currentWorkRunId();
|
|
8200
|
+
if (startedRunId && submittedHistoryMessage?.role === 'user')
|
|
8201
|
+
submittedHistoryMessage.run_id = startedRunId;
|
|
8202
|
+
if (startedRunId && !hiddenUserInput && submittedChatMessage)
|
|
8203
|
+
submittedChatMessage.runId = startedRunId;
|
|
8204
|
+
this.saveWorkspaceConversationState(false);
|
|
7789
8205
|
// Use external opencode CLI engine
|
|
7790
8206
|
if (this.engine === 'opencode') {
|
|
7791
8207
|
if (images.length)
|
|
@@ -7996,6 +8412,8 @@ class Agent {
|
|
|
7996
8412
|
const accepted = await this.handleSubagentContinueEnvelope(args);
|
|
7997
8413
|
if (!accepted.ok || !accepted.data?.id)
|
|
7998
8414
|
return accepted.output;
|
|
8415
|
+
if (!['queued', 'working'].includes(accepted.data.status))
|
|
8416
|
+
return accepted.output;
|
|
7999
8417
|
const settled = await this.waitForSubagentSettlement(accepted.data.id);
|
|
8000
8418
|
return `${accepted.output}\n${settled?.result || settled?.error || ''}`.trim();
|
|
8001
8419
|
}
|
|
@@ -8005,22 +8423,36 @@ class Agent {
|
|
|
8005
8423
|
async handleSubagentContinueEnvelope(args) {
|
|
8006
8424
|
try {
|
|
8007
8425
|
const params = JSON.parse(args);
|
|
8008
|
-
|
|
8009
|
-
|
|
8426
|
+
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
8427
|
+
throw new Error('Arguments must be an object.');
|
|
8428
|
+
if (params.wakeup !== undefined && typeof params.wakeup !== 'boolean')
|
|
8429
|
+
throw new Error('wakeup must be a boolean.');
|
|
8430
|
+
if (params.kind !== undefined && !['directive', 'question', 'result', 'handoff'].includes(params.kind))
|
|
8431
|
+
throw new Error('Invalid message kind.');
|
|
8432
|
+
const name = String(params.id || params.name || '').trim();
|
|
8433
|
+
const selected = (0, subagentCommunication_1.selectSubagentCommunication)(this.history, params);
|
|
8434
|
+
const wakeup = params.wakeup === true;
|
|
8435
|
+
if (name === 'root' || name === this.subagents.rootAgentId) {
|
|
8436
|
+
const delivery = this.subagents.sendRootMessage(this.runtimeActorId, selected.body, params.kind || 'result', undefined, wakeup);
|
|
8437
|
+
return { ok: delivery.ok, output: delivery.ok
|
|
8438
|
+
? `[Root mailbox message persisted] ${delivery.message?.id} wakeup=${wakeup}`
|
|
8439
|
+
: `[Subagent] ${delivery.error}`, error: delivery.error,
|
|
8440
|
+
metadata: { kind: 'subagent-send', wakeup, selection: selected.selection } };
|
|
8441
|
+
}
|
|
8010
8442
|
const sa = this.subagents.get(name);
|
|
8011
8443
|
if (!sa)
|
|
8012
8444
|
return { ok: false, output: `[Subagent] Not found: ${name}`, error: `Not found: ${name}` };
|
|
8013
|
-
|
|
8014
|
-
return this.subagents.toToolResult(sa.id, '[Subagent] Prompt required.', false);
|
|
8015
|
-
sa.messages.push({ role: 'user', content: String(prompt), hidden_user_input: true });
|
|
8016
|
-
const delivery = this.subagents.sendMessage(this.runtimeActorId, sa.id, String(prompt), params.kind || 'directive', {
|
|
8445
|
+
const delivery = this.subagents.sendMessage(this.runtimeActorId, sa.id, selected.body, params.kind || 'directive', {
|
|
8017
8446
|
correlationId: params.correlation_id,
|
|
8018
8447
|
replyTo: params.reply_to,
|
|
8019
|
-
});
|
|
8020
|
-
|
|
8448
|
+
}, wakeup);
|
|
8449
|
+
const result = this.subagents.toToolResult(sa.id, delivery.ok ? `[Subagent message persisted] ${delivery.message?.id} wakeup=${wakeup}` : `[Subagent] ${delivery.error}`, delivery.ok);
|
|
8450
|
+
result.metadata = { ...result.metadata, wakeup, selection: selected.selection };
|
|
8451
|
+
return result;
|
|
8021
8452
|
}
|
|
8022
|
-
catch {
|
|
8023
|
-
|
|
8453
|
+
catch (error) {
|
|
8454
|
+
const detail = error instanceof Error ? error.message : 'Invalid continue arguments.';
|
|
8455
|
+
return { ok: false, output: `[Subagent] ${detail}`, error: detail };
|
|
8024
8456
|
}
|
|
8025
8457
|
}
|
|
8026
8458
|
handleSubagentResult(args) {
|
|
@@ -8038,7 +8470,11 @@ class Agent {
|
|
|
8038
8470
|
// assistant/user 消息即足以让主 Agent 判断上下文,完整历史按需走
|
|
8039
8471
|
// subagent_read 的 max_chars 分页读取。
|
|
8040
8472
|
const transcript = this.subagents.boundedResultTranscript(sa.id);
|
|
8041
|
-
|
|
8473
|
+
const result = this.subagents.toToolResult(sa.id, `get.subagent("${sa.name}", id="${sa.id}")\nStatus: ${sa.status}\nModel: ${sa.model}\nMode: ${sa.agentMode}\n\nResult:\n${sa.result || ''}\n\nRecent Conversation (bounded):\n${transcript}`, true);
|
|
8474
|
+
const receipt = this.subagentSettlementReceipt(sa.id);
|
|
8475
|
+
if (receipt)
|
|
8476
|
+
result.metadata = { ...result.metadata, settlementReceipt: receipt };
|
|
8477
|
+
return result;
|
|
8042
8478
|
}
|
|
8043
8479
|
catch {
|
|
8044
8480
|
return { ok: false, output: '[Subagent] Invalid result arguments.', error: 'Invalid result arguments.' };
|
|
@@ -8053,7 +8489,9 @@ class Agent {
|
|
|
8053
8489
|
const read = this.subagents.read(this.runtimeActorId, target, Number(params.max_chars || 16000));
|
|
8054
8490
|
if (!read.ok)
|
|
8055
8491
|
return { ok: false, output: `[Subagent] ${read.error}`, error: read.error, metadata: { kind: 'subagent-read' } };
|
|
8056
|
-
|
|
8492
|
+
const peer = this.subagents.get(target);
|
|
8493
|
+
const receipt = peer && read.snapshot?.peer.result === peer.result ? this.subagentSettlementReceipt(peer.id) : undefined;
|
|
8494
|
+
return { ok: true, output: JSON.stringify(read.snapshot, null, 2), data: read.snapshot, metadata: { kind: 'subagent-read', ...(receipt ? { settlementReceipt: receipt } : {}) } };
|
|
8057
8495
|
}
|
|
8058
8496
|
catch {
|
|
8059
8497
|
return { ok: false, output: '[Subagent] Invalid read arguments.', error: 'Invalid read arguments.' };
|
|
@@ -8065,7 +8503,7 @@ class Agent {
|
|
|
8065
8503
|
status = String(JSON.parse(args || '{}').status || '');
|
|
8066
8504
|
}
|
|
8067
8505
|
catch { }
|
|
8068
|
-
const subagents = this.subagents.
|
|
8506
|
+
const subagents = this.subagents.listSummaries(status);
|
|
8069
8507
|
return { ok: true, output: JSON.stringify({ conversationId: this.activeConversationId, subagents }, null, 2), metadata: { kind: 'subagent-list' } };
|
|
8070
8508
|
}
|
|
8071
8509
|
handleSubagentClose(args) {
|
|
@@ -8082,6 +8520,8 @@ class Agent {
|
|
|
8082
8520
|
if (actorId === this.subagents.rootAgentId)
|
|
8083
8521
|
this.activePeerAgents.get(sa.id)?.abortActiveKernelRun();
|
|
8084
8522
|
const closed = this.subagents.close(sa.id, actorId);
|
|
8523
|
+
if (closed)
|
|
8524
|
+
this.peerProviderCaches.delete(sa.id);
|
|
8085
8525
|
return this.subagents.toToolResult(sa.id, closed ? `[Subagent '${sa.name}' closed]` : '[Subagent] Close denied.', closed);
|
|
8086
8526
|
}
|
|
8087
8527
|
catch {
|
|
@@ -8395,7 +8835,7 @@ class Agent {
|
|
|
8395
8835
|
}, null, 2);
|
|
8396
8836
|
try {
|
|
8397
8837
|
const cfg = provider.intelligenceConfig(this.intelligence);
|
|
8398
|
-
const response = await this.withTimeout(
|
|
8838
|
+
const response = await this.withTimeout(this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: prompt }], system, Math.min(cfg.temperature, 0.2), Math.min(cfg.maxTokens, 3000), signal), 120000);
|
|
8399
8839
|
const parsed = this.extractMemoryLabJson(response);
|
|
8400
8840
|
if (!parsed)
|
|
8401
8841
|
return deterministic;
|
|
@@ -8443,7 +8883,7 @@ class Agent {
|
|
|
8443
8883
|
].join('\n');
|
|
8444
8884
|
try {
|
|
8445
8885
|
const cfg = provider.intelligenceConfig(this.intelligence);
|
|
8446
|
-
await this.withTimeout(
|
|
8886
|
+
await this.withTimeout(this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: JSON.stringify({ index: read.index }, null, 2) }], system, Math.min(cfg.temperature, 0.2), Math.min(cfg.maxTokens, 1200), signal), 120000);
|
|
8447
8887
|
}
|
|
8448
8888
|
catch {
|
|
8449
8889
|
throwIfAgentAborted(signal); /* deterministic reindex still runs */
|
|
@@ -8554,10 +8994,18 @@ class Agent {
|
|
|
8554
8994
|
}
|
|
8555
8995
|
const model = assignedModel?.name || (requestedModel === 'auto' ? this.activeModelName() : requestedModel);
|
|
8556
8996
|
const activeModel = this.activeModelConfig();
|
|
8557
|
-
const
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8997
|
+
const peerProviderCache = this.peerProviderCaches.get(id) || {};
|
|
8998
|
+
this.peerProviderCaches.delete(id);
|
|
8999
|
+
this.peerProviderCaches.set(id, peerProviderCache);
|
|
9000
|
+
for (const retainedId of this.peerProviderCaches.keys()) {
|
|
9001
|
+
if (this.peerProviderCaches.size <= 32)
|
|
9002
|
+
break;
|
|
9003
|
+
if (!this.activePeerAgents.has(retainedId) && retainedId !== id)
|
|
9004
|
+
this.peerProviderCaches.delete(retainedId);
|
|
9005
|
+
}
|
|
9006
|
+
const assignedProvider = assignedModel && (assignedModel.provider_id !== activeModel?.provider_id || assignedModel.name !== activeModel?.name)
|
|
9007
|
+
? this.modelProvider(assignedModel, peerProviderCache)
|
|
9008
|
+
: this.engineModel(peerProviderCache);
|
|
8561
9009
|
if (!assignedProvider || !model) {
|
|
8562
9010
|
throw new Error('No LLM configured. Add provider in Settings > Models.');
|
|
8563
9011
|
}
|
|
@@ -8601,7 +9049,7 @@ class Agent {
|
|
|
8601
9049
|
child.config.set('models', 'auto_switch', false);
|
|
8602
9050
|
child.config.set('skills', 'auto_download', 'disabled');
|
|
8603
9051
|
child.subagents = this.subagents;
|
|
8604
|
-
child.subagentContextPersist = (history, compression) => this.subagents.replaceContext(sa.id, history, compression);
|
|
9052
|
+
child.subagentContextPersist = (history, compression, inputCommitted) => this.subagents.replaceContext(sa.id, history, compression, inputCommitted);
|
|
8605
9053
|
const requestedFlowName = String(flowName || sa.flowName || '').trim();
|
|
8606
9054
|
if (sa.agentMode === 'flow' && requestedFlowName) {
|
|
8607
9055
|
const flowDir = path.join(this.rootPath, 'Flow');
|
|
@@ -8617,6 +9065,7 @@ class Agent {
|
|
|
8617
9065
|
&& message.content.startsWith("Peer agent '")));
|
|
8618
9066
|
const latestPersisted = persistedMessages.at(-1);
|
|
8619
9067
|
if (latestPersisted?.role === 'user'
|
|
9068
|
+
&& !(reason === 'resume' && prompt === subagent_1.SUBAGENT_RECOVERY_PROMPT)
|
|
8620
9069
|
&& (reason === 'spawn' || reason === 'resume' || prompt.includes(latestPersisted.content))) {
|
|
8621
9070
|
persistedMessages.pop();
|
|
8622
9071
|
}
|
|
@@ -8644,6 +9093,18 @@ class Agent {
|
|
|
8644
9093
|
entry.vision_image_path = message.vision_image_path;
|
|
8645
9094
|
return entry;
|
|
8646
9095
|
});
|
|
9096
|
+
const compression = sa.metadata?.contextCompression;
|
|
9097
|
+
if (compression && typeof compression.at === 'string' && typeof compression.summary === 'string') {
|
|
9098
|
+
child.lastCompression = {
|
|
9099
|
+
at: compression.at, summary: compression.summary,
|
|
9100
|
+
originalMessages: Number(compression.originalMessages) || 0,
|
|
9101
|
+
compressedMessages: Number(compression.compressedMessages) || 0,
|
|
9102
|
+
originalChars: Number(compression.originalChars) || 0,
|
|
9103
|
+
compressedChars: Number(compression.compressedChars) || compression.summary.length,
|
|
9104
|
+
compressedTokens: Number(compression.compressedTokens) || 0,
|
|
9105
|
+
model: String(compression.model || model), fallback: !!compression.fallback,
|
|
9106
|
+
};
|
|
9107
|
+
}
|
|
8647
9108
|
child.subscribeAgentKernelUserMessageStart(content => {
|
|
8648
9109
|
const match = String(content || '').match(/^\[Peer mailbox id=([0-9a-f-]{36})\b/i);
|
|
8649
9110
|
if (match)
|
|
@@ -8694,6 +9155,8 @@ class Agent {
|
|
|
8694
9155
|
if (unrelayPeerEvents)
|
|
8695
9156
|
unrelayPeerEvents();
|
|
8696
9157
|
this.activePeerAgents.delete(sa.id);
|
|
9158
|
+
if (this.subagents.get(sa.id)?.status === 'closed')
|
|
9159
|
+
this.peerProviderCaches.delete(sa.id);
|
|
8697
9160
|
}
|
|
8698
9161
|
}
|
|
8699
9162
|
persistPeerTranscript(peerId, child) {
|
|
@@ -8710,9 +9173,7 @@ class Agent {
|
|
|
8710
9173
|
}
|
|
8711
9174
|
subagentToolDefinitions(defs) {
|
|
8712
9175
|
const modelCapabilities = this.activeModelConfig();
|
|
8713
|
-
const visionFiltered =
|
|
8714
|
-
? defs
|
|
8715
|
-
: defs.filter((tool) => tool.function?.name !== 'image_inspect');
|
|
9176
|
+
const visionFiltered = defs;
|
|
8716
9177
|
const withImageGeneration = modelCapabilities?.image_output
|
|
8717
9178
|
? [...visionFiltered, {
|
|
8718
9179
|
type: 'function',
|
|
@@ -8756,8 +9217,6 @@ class Agent {
|
|
|
8756
9217
|
}
|
|
8757
9218
|
}
|
|
8758
9219
|
async handleImageInspect(args) {
|
|
8759
|
-
if (!this.activeModelConfig()?.vision)
|
|
8760
|
-
return '[Image inspect unavailable] The selected model has not passed vision validation.';
|
|
8761
9220
|
let input = {};
|
|
8762
9221
|
try {
|
|
8763
9222
|
input = JSON.parse(args);
|
|
@@ -9151,7 +9610,7 @@ class Agent {
|
|
|
9151
9610
|
const modelName = String(compressionModel || this.activeModelName()).trim();
|
|
9152
9611
|
if (!modelName)
|
|
9153
9612
|
return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
|
|
9154
|
-
const generated = await this.withTimeout(
|
|
9613
|
+
const generated = await this.withTimeout(this.chatWithConversationUsage(provider, modelName, [...prefixMessages, { role: 'user', content: prompt }], system, temperature, budget.summaryTokens, signal), 120000);
|
|
9155
9614
|
const generatedText = String(generated || '').trim();
|
|
9156
9615
|
if (!generatedText || /^\[LLM Error(?::|\])/i.test(generatedText) || /^LLM Error:/i.test(generatedText)) {
|
|
9157
9616
|
return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
|
|
@@ -9472,12 +9931,18 @@ class Agent {
|
|
|
9472
9931
|
].join('\n'));
|
|
9473
9932
|
}
|
|
9474
9933
|
if (this.intelligence === 'ultra') {
|
|
9475
|
-
parts.push([
|
|
9934
|
+
parts.push(this.isSubagentRuntime ? [
|
|
9935
|
+
'[Ultra Intelligence – Specialist Role]',
|
|
9936
|
+
'Own the delegated task and complete its implementation or investigation yourself. Preserve the assigned file and responsibility boundaries. You may delegate a concrete independent subtask only when it saves time while you continue useful work; never pass your whole assignment to another peer or create a chain of coordinators.',
|
|
9937
|
+
'Reuse an existing peer for follow-up work. Send concise, addressed mailbox messages only for new evidence, dependencies, blockers, handoffs or actionable corrections; avoid repeated status checks and unchanged progress messages. Include exact references and verification evidence in your result.',
|
|
9938
|
+
].join('\n') : [
|
|
9476
9939
|
'[Ultra Intelligence – Orchestrator Role]',
|
|
9477
|
-
'You are the lead orchestrator.
|
|
9478
|
-
'
|
|
9940
|
+
'You are the lead orchestrator. Decompose complex work into concrete independent sub-tasks and create specialized peers with `SubAgent`. Give each peer a clear objective, file or responsibility boundary, dependencies, deliverable and acceptance check. Retain useful work locally while peers investigate, implement or review; sequence dependent work and prevent overlapping edits. Merge evidence, resolve conflicts and own the final verification.',
|
|
9941
|
+
'Use task_create only for the conversation checklist; it never creates a SubAgent. Use the available shared concurrency when independent work exists, but do not create peers merely to fill slots. Reuse existing peers for follow-up work. Prefer concise addressed mailbox updates for new evidence, dependencies, blockers and corrections; avoid redundant broadcasts, repeated status checks and unchanged progress messages.',
|
|
9479
9942
|
].join('\n'));
|
|
9480
9943
|
}
|
|
9944
|
+
if (this.shouldExposeToolInterface())
|
|
9945
|
+
parts.push('Peer communication: subagent_send defaults to wakeup=false and the last visible text-history entry. An active receiver processes either wakeup value normally; use wakeup=true when an inactive peer or root must resume work. Choose concise sender-written summaries or explicit ranges of text/tool history, and forward only evidence relevant to the recipient. Use id="root" for the main agent. Do not retransmit unchanged history or rewrite earlier context to deliver a message.');
|
|
9481
9946
|
parts.push(this.buildModePrompt());
|
|
9482
9947
|
const value = this.contextV2.orchestrator.assemble({
|
|
9483
9948
|
// Keep the complete base prompt in one stable section. The linked_plan
|
|
@@ -9535,6 +10000,47 @@ class Agent {
|
|
|
9535
10000
|
currentUserInput: '',
|
|
9536
10001
|
});
|
|
9537
10002
|
}
|
|
10003
|
+
subagentSettlementReceipt(id) {
|
|
10004
|
+
const peer = this.subagents.get(id);
|
|
10005
|
+
return !this.isSubagentRuntime && peer && typeof peer.result === 'string'
|
|
10006
|
+
&& Number.isSafeInteger(peer.settlementRevision) && Number(peer.settlementRevision) > 0
|
|
10007
|
+
? { peerId: peer.id, revision: peer.settlementRevision } : undefined;
|
|
10008
|
+
}
|
|
10009
|
+
/** Peer jobs share a durable request prefix; credentials only enter its hash. */
|
|
10010
|
+
peerRequestCacheIdentity(systemPrompt, catalog) {
|
|
10011
|
+
if (!this.isSubagentRuntime)
|
|
10012
|
+
return undefined;
|
|
10013
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
10014
|
+
systemPrompt, catalog, model: this.activeModelConfig(),
|
|
10015
|
+
apiMode: this.config.openAIApiMode(), adapters: this.config.contextFlag('provider_adapters_v2'),
|
|
10016
|
+
proxy: this.providerProxyConfig(), intelligence: this.intelligence,
|
|
10017
|
+
compression: this.lastCompression?.at || '',
|
|
10018
|
+
})).digest('hex');
|
|
10019
|
+
}
|
|
10020
|
+
readPeerRequestCache(identity) {
|
|
10021
|
+
if (!identity)
|
|
10022
|
+
return undefined;
|
|
10023
|
+
const saved = this.subagents.get(this.runtimeActorId)?.metadata?.requestCache;
|
|
10024
|
+
if (saved?.version !== 1 || saved.identity !== identity || typeof saved.taskFocus !== 'string'
|
|
10025
|
+
|| !Array.isArray(saved.initialTools) || !saved.initialTools.every(name => typeof name === 'string')
|
|
10026
|
+
|| !Array.isArray(saved.provisionedTools) || !saved.provisionedTools.every(name => typeof name === 'string'))
|
|
10027
|
+
return undefined;
|
|
10028
|
+
return { ...saved, initialTools: saved.initialTools.slice(), provisionedTools: saved.provisionedTools.slice() };
|
|
10029
|
+
}
|
|
10030
|
+
persistPeerRequestCache(cache) {
|
|
10031
|
+
if (!this.isSubagentRuntime)
|
|
10032
|
+
return;
|
|
10033
|
+
const previous = this.subagents.get(this.runtimeActorId)?.metadata?.requestCache;
|
|
10034
|
+
if (JSON.stringify(previous) !== JSON.stringify(cache)) {
|
|
10035
|
+
this.subagents.patchMetadata(this.runtimeActorId, { requestCache: cache });
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
checkpointPeerInput() {
|
|
10039
|
+
// Called at the first real provider boundary, after the input has entered
|
|
10040
|
+
// working history. Commit context and job progress in one durable write.
|
|
10041
|
+
if (this.isSubagentRuntime)
|
|
10042
|
+
this.subagentContextPersist?.(this.history, this.lastCompression, true);
|
|
10043
|
+
}
|
|
9538
10044
|
cachedToolDefinitions() {
|
|
9539
10045
|
const identity = JSON.stringify({
|
|
9540
10046
|
mode: this.mode,
|
|
@@ -10008,7 +10514,9 @@ function routeProviderFingerprint(provider) {
|
|
|
10008
10514
|
});
|
|
10009
10515
|
}
|
|
10010
10516
|
function modelConfigurationFingerprint(model) {
|
|
10011
|
-
const { validation, evaluation, _previous_name, previous_name, ...configuration } = model;
|
|
10517
|
+
const { validation, evaluation, response_health, enabled, _previous_name, previous_name, ...configuration } = model;
|
|
10518
|
+
void response_health;
|
|
10519
|
+
void enabled;
|
|
10012
10520
|
void validation;
|
|
10013
10521
|
void evaluation;
|
|
10014
10522
|
void _previous_name;
|
|
@@ -10050,12 +10558,7 @@ function resetEditedModelValidationEvidence(incomingProviders, existingProviders
|
|
|
10050
10558
|
}
|
|
10051
10559
|
}
|
|
10052
10560
|
function modelConfigIsUnavailable(model) {
|
|
10053
|
-
|
|
10054
|
-
if (model.validation?.level !== 'discovered'
|
|
10055
|
-
&& (validationStatus === 'unavailable' || validationStatus === 'auth_error' || validationStatus === 'invalid_config'))
|
|
10056
|
-
return true;
|
|
10057
|
-
const evaluationStatus = validationStatus === 'degraded' ? 'degraded' : String(model.evaluation?.status || '').toLowerCase();
|
|
10058
|
-
return evaluationStatus === 'unavailable' || evaluationStatus.startsWith('error');
|
|
10561
|
+
return model.enabled === false;
|
|
10059
10562
|
}
|
|
10060
10563
|
function parseDeploymentSelectionValue(value) {
|
|
10061
10564
|
const marker = String(value || '').trim();
|