newmark-agent 0.5.14 → 0.5.15
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 +3373 -1900
- package/dist/conversation-utility-host.js +1 -1
- package/dist/core/agent.d.ts +105 -14
- package/dist/core/agent.js +658 -104
- 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 +236 -85
- package/dist/core/autoRouter.js +16 -3
- 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 +347 -83
- 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/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 +32 -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 +148 -267
- 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 +2374 -616
- package/dist/ui/lucide-sprite.svg +7 -0
- package/dist/ui/startup.html +6 -6
- package/dist/wsl-agent-host.bundle.cjs +3381 -1906
- package/dist/wsl-agent-host.js +9 -4
- package/package.json +23 -4
package/dist/core/agent.js
CHANGED
|
@@ -38,6 +38,9 @@ exports.normalizeIntelligenceTier = normalizeIntelligenceTier;
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const promises_1 = require("node:timers/promises");
|
|
42
|
+
const providerUsageAccounting_1 = require("./providerUsageAccounting");
|
|
43
|
+
const requestContextEstimate_1 = require("./requestContextEstimate");
|
|
41
44
|
const imageInspect_1 = require("./imageInspect");
|
|
42
45
|
const conversationAttachments_1 = require("./conversationAttachments");
|
|
43
46
|
const displayImages_1 = require("./displayImages");
|
|
@@ -48,6 +51,7 @@ const index_1 = require("../tools/index");
|
|
|
48
51
|
const workspace_1 = require("./workspace");
|
|
49
52
|
const ssh_1 = require("./ssh");
|
|
50
53
|
const subagent_1 = require("./subagent");
|
|
54
|
+
const subagentCommunication_1 = require("./subagentCommunication");
|
|
51
55
|
const compat_1 = require("./compat");
|
|
52
56
|
const skills_1 = require("./skills");
|
|
53
57
|
const flow_1 = require("./flow");
|
|
@@ -222,6 +226,8 @@ class Agent {
|
|
|
222
226
|
lastCompression = null;
|
|
223
227
|
providerUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
224
228
|
lastProviderUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
229
|
+
providerUsageAccounting = (0, providerUsageAccounting_1.createAccounting)();
|
|
230
|
+
lastRequestContext = null;
|
|
225
231
|
compressionCache = [];
|
|
226
232
|
pendingHistoryRemovals = [];
|
|
227
233
|
branchMailbox = [];
|
|
@@ -264,15 +270,25 @@ class Agent {
|
|
|
264
270
|
activeAgentKernelRuntime = null;
|
|
265
271
|
modelSwitchCompressionPromise = null;
|
|
266
272
|
activePeerAgents = new Map();
|
|
273
|
+
// Bounded caller-owned peer slots retain protocol compatibility learning
|
|
274
|
+
// between mailbox jobs. They never retain the child Agent or go to disk.
|
|
275
|
+
peerProviderCaches = new Map();
|
|
267
276
|
awaitingAgentKernelRuntime = false;
|
|
268
277
|
pendingAgentKernelQueue = [];
|
|
269
278
|
linkedPlanAccess;
|
|
270
279
|
subagentContextPersist;
|
|
280
|
+
subagentPersistedHistory = [];
|
|
281
|
+
subagentPersistedCompression = null;
|
|
271
282
|
agentKernelUserMessageStartSubscribers = [];
|
|
272
283
|
rootInboxWakeSubscribers = [];
|
|
284
|
+
directRootInboxQueuedIds = new Set();
|
|
285
|
+
directRootWakePending = false;
|
|
286
|
+
rootInboxRetiredSubscribers = [];
|
|
273
287
|
activeWorkRunId = '';
|
|
274
288
|
loadedWorkspaceConversationKey = '';
|
|
275
289
|
managedWorkRunIds = new Set();
|
|
290
|
+
/** Public deltas since the last completed response, retained only until its boundary. */
|
|
291
|
+
pendingPublicWorkText = new Map();
|
|
276
292
|
finalizingWorkRunId = '';
|
|
277
293
|
agentRunService = null;
|
|
278
294
|
agentRunByWorkRunId = new Map();
|
|
@@ -308,6 +324,7 @@ class Agent {
|
|
|
308
324
|
recentChecks: [],
|
|
309
325
|
};
|
|
310
326
|
rootInboxListener = (message) => this.deliverRootInboxMessage(message);
|
|
327
|
+
boundSubagentManagerKey = '';
|
|
311
328
|
agentOnly;
|
|
312
329
|
runtimeActorId;
|
|
313
330
|
runtimeLifecycleRole;
|
|
@@ -430,6 +447,12 @@ class Agent {
|
|
|
430
447
|
this.toolDefinitionCache.clear();
|
|
431
448
|
this.status = 'idle';
|
|
432
449
|
}
|
|
450
|
+
/** Selecting the composer mode does not start a Goal or reset a running Flow. */
|
|
451
|
+
selectConversationMode(mode) {
|
|
452
|
+
this.mode = ['build', 'plan', 'chat', 'goal', 'flow'].includes(mode) ? mode : 'build';
|
|
453
|
+
this.invalidateSystemPrompt();
|
|
454
|
+
this.saveWorkspaceConversationState(true);
|
|
455
|
+
}
|
|
433
456
|
currentConversationFlowSelection() {
|
|
434
457
|
return this.flow?.name
|
|
435
458
|
? { name: this.flow.name, pc: Math.max(0, Math.floor(Number(this.flowPc) || 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();
|
|
@@ -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,12 +2559,53 @@ 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);
|
|
2502
2570
|
};
|
|
2503
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);
|
|
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;
|
|
2608
|
+
}
|
|
2504
2609
|
notifyAgentKernelUserMessageStart(content, clientMessageId) {
|
|
2505
2610
|
const text = String(content || '');
|
|
2506
2611
|
if (!text && !clientMessageId)
|
|
@@ -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,69 @@ 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
|
+
const generated = await this.chatWithConversationUsage(provider, modelName, [{ role: 'user', content: prompt }], system, temperature, 64, signal, reasoningEffort);
|
|
3909
|
+
if (signal?.aborted)
|
|
3910
|
+
return '';
|
|
3742
3911
|
const title = this.normalizeConversationRenameTitle(generated);
|
|
3743
3912
|
const source = String(firstUserInput || '').replace(/\s+/g, ' ').trim();
|
|
3744
3913
|
return title && title !== source ? title : '';
|
|
3745
3914
|
}
|
|
3746
|
-
catch {
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
clearTimeout(timer);
|
|
3915
|
+
catch (error) {
|
|
3916
|
+
if (signal?.aborted)
|
|
3917
|
+
return '';
|
|
3918
|
+
throw error;
|
|
3751
3919
|
}
|
|
3752
3920
|
}
|
|
3921
|
+
conversationTitleFailureCause(error, provider) {
|
|
3922
|
+
const message = error instanceof Error ? error.message : '';
|
|
3923
|
+
const status = message.match(/^\[LLM Error:\s*([1-5]\d{2})\]/)?.[1];
|
|
3924
|
+
if (status) {
|
|
3925
|
+
// Extract only a short machine code. Never display the provider's raw
|
|
3926
|
+
// message/body, request URL, headers or echoed credentials in this gate.
|
|
3927
|
+
let code = '';
|
|
3928
|
+
try {
|
|
3929
|
+
const json = JSON.parse(message.slice(message.indexOf('{')));
|
|
3930
|
+
const candidate = typeof json.error?.code === 'string' ? json.error.code : '';
|
|
3931
|
+
if (/^[a-z][a-z0-9_.-]{0,63}$/i.test(candidate)
|
|
3932
|
+
&& !(provider.apiKey && candidate.includes(provider.apiKey))
|
|
3933
|
+
&& !(provider.baseUrl && candidate.includes(provider.baseUrl)))
|
|
3934
|
+
code = candidate;
|
|
3935
|
+
}
|
|
3936
|
+
catch { /* Non-JSON errors still retain their safe HTTP status. */ }
|
|
3937
|
+
const reason = code === 'get_channel_failed' ? 'no provider channel available' : {
|
|
3938
|
+
'400': 'request rejected', '401': 'authentication failed', '402': 'payment required',
|
|
3939
|
+
'403': 'access denied', '404': 'model or endpoint unavailable', '408': 'request timed out',
|
|
3940
|
+
'429': 'rate limited', '500': 'provider internal error', '502': 'provider gateway error',
|
|
3941
|
+
'503': 'service unavailable', '504': 'provider gateway timed out',
|
|
3942
|
+
}[status] || 'provider request failed';
|
|
3943
|
+
return `HTTP ${status}${code ? ` (${code})` : ''}: ${reason}`;
|
|
3944
|
+
}
|
|
3945
|
+
if (error instanceof Error && error.name === 'TimeoutError')
|
|
3946
|
+
return 'provider request timed out';
|
|
3947
|
+
if (error instanceof SyntaxError)
|
|
3948
|
+
return 'provider returned invalid JSON';
|
|
3949
|
+
return 'provider request failed';
|
|
3950
|
+
}
|
|
3951
|
+
async waitForConversationTitleRetry(delayMs, signal) {
|
|
3952
|
+
if (signal?.aborted || delayMs <= 0)
|
|
3953
|
+
return;
|
|
3954
|
+
await new Promise(resolve => {
|
|
3955
|
+
const finish = () => {
|
|
3956
|
+
clearTimeout(timer);
|
|
3957
|
+
signal?.removeEventListener('abort', finish);
|
|
3958
|
+
resolve();
|
|
3959
|
+
};
|
|
3960
|
+
const timer = setTimeout(finish, delayMs);
|
|
3961
|
+
signal?.addEventListener('abort', finish, { once: true });
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3753
3964
|
/** Legacy completion hook retained as a no-op; naming is pre-response only. */
|
|
3754
3965
|
maybeAutoRenameConversationFromRun(_run) {
|
|
3755
3966
|
// Compatibility no-op. Automatic naming is a hard gate on the first
|
|
@@ -3765,19 +3976,33 @@ class Agent {
|
|
|
3765
3976
|
const retryDelaysMs = [0, 1000, 2000, 4000, 8000];
|
|
3766
3977
|
if (!messageId || !firstUserInput.trim())
|
|
3767
3978
|
return false;
|
|
3979
|
+
// Failure state belongs to this title attempt sequence, not the Agent or
|
|
3980
|
+
// another conversation. A later empty title replaces an earlier HTTP error.
|
|
3981
|
+
let lastFailure = 'provider returned an empty or unusable title';
|
|
3982
|
+
const requestTitle = async () => {
|
|
3983
|
+
lastFailure = 'provider returned an empty or unusable title';
|
|
3984
|
+
try {
|
|
3985
|
+
return await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal);
|
|
3986
|
+
}
|
|
3987
|
+
catch (error) {
|
|
3988
|
+
lastFailure = this.conversationTitleFailureCause(error, provider);
|
|
3989
|
+
return '';
|
|
3990
|
+
}
|
|
3991
|
+
};
|
|
3992
|
+
const exhaustedFailure = () => new Error(`Conversation title generation failed; ${lastFailure}. The first Agent request was not started. Retry the first input.`);
|
|
3768
3993
|
if (!stateKey) {
|
|
3769
3994
|
// Pure Agent/CLI mode has no workspace conversation file to rename, but
|
|
3770
3995
|
// still uses the title request as the required first model-availability
|
|
3771
3996
|
// probe before starting the formal response.
|
|
3772
3997
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3773
|
-
if (await
|
|
3998
|
+
if (await requestTitle())
|
|
3774
3999
|
return true;
|
|
3775
4000
|
if (signal?.aborted)
|
|
3776
4001
|
return false;
|
|
3777
4002
|
if (attempt < maxAttempts - 1)
|
|
3778
|
-
await
|
|
4003
|
+
await this.waitForConversationTitleRetry(retryDelaysMs[attempt], signal);
|
|
3779
4004
|
}
|
|
3780
|
-
|
|
4005
|
+
throw exhaustedFailure();
|
|
3781
4006
|
}
|
|
3782
4007
|
const stored = this.readStoredConversationState(workspace);
|
|
3783
4008
|
const entry = stored.conversations?.[stateKey];
|
|
@@ -3794,7 +4019,7 @@ class Agent {
|
|
|
3794
4019
|
if (memory)
|
|
3795
4020
|
memory.titleRequestMessageId = messageId;
|
|
3796
4021
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3797
|
-
const title = await
|
|
4022
|
+
const title = await requestTitle();
|
|
3798
4023
|
if (title) {
|
|
3799
4024
|
const latest = this.readStoredConversationState(workspace);
|
|
3800
4025
|
const current = latest.conversations?.[stateKey];
|
|
@@ -3812,9 +4037,9 @@ class Agent {
|
|
|
3812
4037
|
if (signal?.aborted)
|
|
3813
4038
|
return false;
|
|
3814
4039
|
if (attempt < maxAttempts - 1)
|
|
3815
|
-
await
|
|
4040
|
+
await this.waitForConversationTitleRetry(retryDelaysMs[attempt], signal);
|
|
3816
4041
|
}
|
|
3817
|
-
|
|
4042
|
+
throw exhaustedFailure();
|
|
3818
4043
|
}
|
|
3819
4044
|
firstPersistedUserForTitleGate() {
|
|
3820
4045
|
const firstUser = this.chatMessages.find(message => message.role === 'user');
|
|
@@ -4008,8 +4233,16 @@ class Agent {
|
|
|
4008
4233
|
};
|
|
4009
4234
|
}
|
|
4010
4235
|
saveWorkspaceConversationState(flush = true) {
|
|
4011
|
-
if (this.isSubagentRuntime)
|
|
4236
|
+
if (this.isSubagentRuntime) {
|
|
4237
|
+
if (this.subagentContextPersist && (this.history.length !== this.subagentPersistedHistory.length
|
|
4238
|
+
|| this.lastCompression !== this.subagentPersistedCompression
|
|
4239
|
+
|| this.history.some((message, index) => message !== this.subagentPersistedHistory[index]))) {
|
|
4240
|
+
this.subagentContextPersist(this.history, this.lastCompression);
|
|
4241
|
+
this.subagentPersistedHistory = this.history.slice();
|
|
4242
|
+
this.subagentPersistedCompression = this.lastCompression;
|
|
4243
|
+
}
|
|
4012
4244
|
return;
|
|
4245
|
+
}
|
|
4013
4246
|
const key = this.workspaceConversationKey();
|
|
4014
4247
|
if (!key)
|
|
4015
4248
|
return;
|
|
@@ -4018,6 +4251,7 @@ class Agent {
|
|
|
4018
4251
|
this.workspaceConversations.set(key, {
|
|
4019
4252
|
chatMessages: [...this.chatMessages],
|
|
4020
4253
|
history: [...this.history],
|
|
4254
|
+
providerUsage: this.conversationProviderUsage(),
|
|
4021
4255
|
compressionCache: [...this.compressionCache],
|
|
4022
4256
|
branchMailbox: [...this.branchMailbox],
|
|
4023
4257
|
branchCommunication: this.branchCommunicationEnabled,
|
|
@@ -4053,6 +4287,7 @@ class Agent {
|
|
|
4053
4287
|
title,
|
|
4054
4288
|
chatMessages: [...this.chatMessages],
|
|
4055
4289
|
history: [...this.history],
|
|
4290
|
+
providerUsage: this.conversationProviderUsage(),
|
|
4056
4291
|
compressionCache: [...this.compressionCache],
|
|
4057
4292
|
branchMailbox: [...this.branchMailbox],
|
|
4058
4293
|
branchCommunication: this.branchCommunicationEnabled,
|
|
@@ -4085,6 +4320,8 @@ class Agent {
|
|
|
4085
4320
|
}
|
|
4086
4321
|
loadWorkspaceConversationState() {
|
|
4087
4322
|
this.managedWorkRunIds.clear();
|
|
4323
|
+
this.pendingPublicWorkText.clear();
|
|
4324
|
+
this.restoreProviderUsage();
|
|
4088
4325
|
const key = this.workspaceConversationKey();
|
|
4089
4326
|
this.loadedWorkspaceConversationKey = key || '';
|
|
4090
4327
|
if (!key) {
|
|
@@ -4109,6 +4346,7 @@ class Agent {
|
|
|
4109
4346
|
}
|
|
4110
4347
|
const saved = this.workspaceConversations.get(key);
|
|
4111
4348
|
if (saved) {
|
|
4349
|
+
this.restoreProviderUsage(saved.providerUsage);
|
|
4112
4350
|
this.history = [...saved.history];
|
|
4113
4351
|
this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
4114
4352
|
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
@@ -4136,6 +4374,7 @@ class Agent {
|
|
|
4136
4374
|
const stored = this.readStoredConversationState();
|
|
4137
4375
|
const stateKey = this.workspaceConversationStateKey();
|
|
4138
4376
|
const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
|
|
4377
|
+
this.restoreProviderUsage(persisted?.providerUsage);
|
|
4139
4378
|
const repairedHistory = this.repairDanglingToolCalls(persisted?.history ? [...persisted.history] : []);
|
|
4140
4379
|
this.history = repairedHistory.messages;
|
|
4141
4380
|
this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
@@ -4182,6 +4421,7 @@ class Agent {
|
|
|
4182
4421
|
this.workspaceConversations.set(key, {
|
|
4183
4422
|
chatMessages: [...this.chatMessages],
|
|
4184
4423
|
history: [...this.history],
|
|
4424
|
+
providerUsage: this.conversationProviderUsage(),
|
|
4185
4425
|
compressionCache: [...this.compressionCache],
|
|
4186
4426
|
branchMailbox: [...this.branchMailbox],
|
|
4187
4427
|
branchCommunication: this.branchCommunicationEnabled,
|
|
@@ -4232,10 +4472,12 @@ class Agent {
|
|
|
4232
4472
|
return ws;
|
|
4233
4473
|
}
|
|
4234
4474
|
selectWorkspace(id) {
|
|
4475
|
+
this.assertPeerOwnerCanSwitch(this.activeConversationId, id);
|
|
4235
4476
|
this.saveWorkspaceConversationState();
|
|
4236
4477
|
return this.applyWorkspaceContext(this.workspace.select(id));
|
|
4237
4478
|
}
|
|
4238
4479
|
selectWorkspaceFromStorage(id) {
|
|
4480
|
+
this.assertPeerOwnerCanSwitch(this.activeConversationId, id);
|
|
4239
4481
|
const selected = this.workspace.select(id);
|
|
4240
4482
|
if (selected)
|
|
4241
4483
|
this.config.loadWorkspaceConfig(selected.path);
|
|
@@ -4273,6 +4515,7 @@ class Agent {
|
|
|
4273
4515
|
}
|
|
4274
4516
|
setConversation(id) {
|
|
4275
4517
|
const clean = this.safeConversationId(id || 'default');
|
|
4518
|
+
this.assertPeerOwnerCanSwitch(clean);
|
|
4276
4519
|
// Conversation runners may bind a target workspace directly before their
|
|
4277
4520
|
// first setConversation(). Do not save state loaded for another workspace
|
|
4278
4521
|
// under the new workspace key during that hand-off.
|
|
@@ -4285,6 +4528,7 @@ class Agent {
|
|
|
4285
4528
|
return this.activeConversationId;
|
|
4286
4529
|
}
|
|
4287
4530
|
setConversationFromStorage(id) {
|
|
4531
|
+
this.assertPeerOwnerCanSwitch(id);
|
|
4288
4532
|
this.activeConversationId = this.safeConversationId(id || 'default');
|
|
4289
4533
|
const key = this.workspaceConversationKey();
|
|
4290
4534
|
if (key)
|
|
@@ -4294,6 +4538,8 @@ class Agent {
|
|
|
4294
4538
|
}
|
|
4295
4539
|
persistActiveConversationSelection(id, ws = this.workspace.current) {
|
|
4296
4540
|
const clean = this.safeConversationId(id || 'default');
|
|
4541
|
+
if (ws && this.workspace.current && path.resolve(ws.path) === path.resolve(this.workspace.current.path))
|
|
4542
|
+
this.assertPeerOwnerCanSwitch(clean);
|
|
4297
4543
|
this.mutateStoredConversationState(ws, latest => ({
|
|
4298
4544
|
version: 3,
|
|
4299
4545
|
activeConversationId: clean,
|
|
@@ -4319,6 +4565,9 @@ class Agent {
|
|
|
4319
4565
|
}
|
|
4320
4566
|
abortActiveKernelRun(reason = 'unspecified') {
|
|
4321
4567
|
let aborted = false;
|
|
4568
|
+
if (!this.isSubagentRuntime && (reason === 'user_stop' || reason === 'force_stop')) {
|
|
4569
|
+
this.subagents.broadcastStop(this.runtimeActorId, this.currentWorkRunId() || this.routeTransactionId || 'inactive', reason === 'force_stop');
|
|
4570
|
+
}
|
|
4322
4571
|
this.subagents.pauseScheduling();
|
|
4323
4572
|
if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
|
|
4324
4573
|
const abortError = new Error('Agent run aborted');
|
|
@@ -4339,6 +4588,7 @@ class Agent {
|
|
|
4339
4588
|
this.pendingAgentKernelQueue = [];
|
|
4340
4589
|
return aborted;
|
|
4341
4590
|
}
|
|
4591
|
+
hasRunningSubagents() { return this.activePeerAgents.size > 0; }
|
|
4342
4592
|
activeProcessSignal() {
|
|
4343
4593
|
return this.activeProcessAbortController?.signal;
|
|
4344
4594
|
}
|
|
@@ -4575,7 +4825,7 @@ class Agent {
|
|
|
4575
4825
|
].join('\n');
|
|
4576
4826
|
const maxTokens = Math.max(1024, Math.min(8192, Math.ceil(content.length / 4)) + 512);
|
|
4577
4827
|
const { temperature } = provider.intelligenceConfig('low');
|
|
4578
|
-
const generated = await this.withTimeout(
|
|
4828
|
+
const generated = await this.withTimeout(this.chatWithConversationUsage(provider, modelName, [{ role: 'user', content: prompt }], system, temperature, maxTokens, signal), 120000);
|
|
4579
4829
|
const summary = String(generated || '').trim();
|
|
4580
4830
|
if (!summary || /^\[LLM Error(?::|\])/i.test(summary)) {
|
|
4581
4831
|
return {
|
|
@@ -5423,6 +5673,11 @@ class Agent {
|
|
|
5423
5673
|
const stored = this.readStoredConversationState(ws);
|
|
5424
5674
|
stored.conversations = stored.conversations || {};
|
|
5425
5675
|
const previous = stateKey ? stored.conversations[stateKey] : undefined;
|
|
5676
|
+
const providerUsage = source.conversationProviderUsage ? source.conversationProviderUsage() : source.providerUsageTotals ? {
|
|
5677
|
+
version: 1,
|
|
5678
|
+
totals: this.normalizeProviderUsageCounters(source.providerUsageTotals),
|
|
5679
|
+
last: this.normalizeProviderUsageCounters(source.lastProviderUsage),
|
|
5680
|
+
} : previous?.providerUsage;
|
|
5426
5681
|
const sourceTitleGate = source.getConversationTitleGateState?.() || source.titleGateState;
|
|
5427
5682
|
const titleRequestMessageId = sourceTitleGate?.titleRequestMessageId === undefined
|
|
5428
5683
|
? String(previous?.titleRequestMessageId || '')
|
|
@@ -5434,6 +5689,7 @@ class Agent {
|
|
|
5434
5689
|
this.workspaceConversations.set(key, {
|
|
5435
5690
|
chatMessages: normalizedChatMessages,
|
|
5436
5691
|
history: [...source.history],
|
|
5692
|
+
providerUsage,
|
|
5437
5693
|
plan,
|
|
5438
5694
|
linkedPlan,
|
|
5439
5695
|
subagentState,
|
|
@@ -5459,6 +5715,7 @@ class Agent {
|
|
|
5459
5715
|
title,
|
|
5460
5716
|
chatMessages: normalizedChatMessages,
|
|
5461
5717
|
history: [...source.history],
|
|
5718
|
+
providerUsage,
|
|
5462
5719
|
plan,
|
|
5463
5720
|
linkedPlan,
|
|
5464
5721
|
subagentState,
|
|
@@ -5481,6 +5738,7 @@ class Agent {
|
|
|
5481
5738
|
if (this.safeConversationId(this.activeConversationId || 'default') === clean) {
|
|
5482
5739
|
this.chatMessages = normalizedChatMessages;
|
|
5483
5740
|
this.history = [...source.history];
|
|
5741
|
+
this.restoreProviderUsage(providerUsage);
|
|
5484
5742
|
this.conversationPlan = plan;
|
|
5485
5743
|
this.linkedPlan = linkedPlan;
|
|
5486
5744
|
if (source.subagents)
|
|
@@ -5728,22 +5986,143 @@ class Agent {
|
|
|
5728
5986
|
buildBlockTokens: estimate(buildBlockAsciiChars, buildBlockNonAsciiChars, buildBlockStructuralChars, true),
|
|
5729
5987
|
};
|
|
5730
5988
|
}
|
|
5731
|
-
|
|
5732
|
-
const bounded = (value) =>
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5989
|
+
normalizeProviderUsageCounters(input) {
|
|
5990
|
+
const bounded = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
5991
|
+
? Math.max(0, Math.floor(value)) : 0;
|
|
5992
|
+
return { input: bounded(input?.input), output: bounded(input?.output), cacheRead: bounded(input?.cacheRead), cacheWrite: bounded(input?.cacheWrite) };
|
|
5993
|
+
}
|
|
5994
|
+
conversationProviderUsage() {
|
|
5995
|
+
return {
|
|
5996
|
+
version: 1, totals: { ...this.providerUsageTotals }, last: { ...this.lastProviderUsage },
|
|
5997
|
+
accounting: { ...this.providerUsageAccounting },
|
|
5998
|
+
requestContext: this.lastRequestContext ? { ...this.lastRequestContext } : null,
|
|
5999
|
+
};
|
|
6000
|
+
}
|
|
6001
|
+
normalizedConversationUsage(saved) {
|
|
6002
|
+
const totals = this.normalizeProviderUsageCounters(saved?.version === 1 ? saved.totals : undefined);
|
|
6003
|
+
const accounting = (0, providerUsageAccounting_1.createAccounting)(saved?.version === 1 ? totals : undefined);
|
|
6004
|
+
if (saved?.version === 1 && saved.accounting) {
|
|
6005
|
+
for (const key of ['requests', 'inputReportedRequests', 'outputReportedRequests', 'cacheReportedRequests', 'cacheEligibleInputTokens', 'cacheReadTokens']) {
|
|
6006
|
+
const value = saved.accounting[key];
|
|
6007
|
+
accounting[key] = typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
6008
|
+
}
|
|
6009
|
+
accounting.hasLegacyTotals = saved.accounting.hasLegacyTotals === true;
|
|
6010
|
+
accounting.lastInputTokens = typeof saved.accounting.lastInputTokens === 'number' && Number.isFinite(saved.accounting.lastInputTokens)
|
|
6011
|
+
? Math.max(0, Math.floor(saved.accounting.lastInputTokens)) : null;
|
|
6012
|
+
}
|
|
6013
|
+
let requestContext = null;
|
|
6014
|
+
const context = saved?.version === 1 ? saved.requestContext : null;
|
|
6015
|
+
if (context && typeof context.requestId === 'string' && typeof context.runId === 'string' && typeof context.at === 'string') {
|
|
6016
|
+
requestContext = { requestId: context.requestId, runId: context.runId, model: String(context.model || ''), at: context.at,
|
|
6017
|
+
estimatedTokens: 0, longHistoryTokens: 0, buildBlockTokens: 0, systemPromptTokens: 0, toolSchemaTokens: 0, messageCount: 0, hasImages: context.hasImages === true };
|
|
6018
|
+
for (const key of ['estimatedTokens', 'longHistoryTokens', 'buildBlockTokens', 'systemPromptTokens', 'toolSchemaTokens', 'messageCount', 'inputTokens', 'cacheReadTokens']) {
|
|
6019
|
+
const value = context[key];
|
|
6020
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
6021
|
+
requestContext[key] = Math.max(0, Math.floor(value));
|
|
6022
|
+
}
|
|
6023
|
+
}
|
|
6024
|
+
return { version: 1, totals, last: this.normalizeProviderUsageCounters(saved?.version === 1 ? saved.last : undefined), accounting, requestContext };
|
|
6025
|
+
}
|
|
6026
|
+
restoreProviderUsage(saved) {
|
|
6027
|
+
// Legacy counters remain spend evidence, but cannot prove reporting coverage.
|
|
6028
|
+
const usage = this.normalizedConversationUsage(saved);
|
|
6029
|
+
this.providerUsageTotals = usage.totals;
|
|
6030
|
+
this.lastProviderUsage = usage.last;
|
|
6031
|
+
this.providerUsageAccounting = usage.accounting;
|
|
6032
|
+
this.lastRequestContext = usage.requestContext || null;
|
|
6033
|
+
}
|
|
6034
|
+
mutateRequestUsage(request, mutate) {
|
|
6035
|
+
const activeKey = this.workspaceConversationStateKey();
|
|
6036
|
+
if (request.stateKey === activeKey && request.conversationId === this.activeConversationId) {
|
|
6037
|
+
const usage = { version: 1, totals: this.providerUsageTotals, last: this.lastProviderUsage,
|
|
6038
|
+
accounting: this.providerUsageAccounting, requestContext: this.lastRequestContext };
|
|
6039
|
+
mutate(usage);
|
|
6040
|
+
this.lastProviderUsage = usage.last;
|
|
6041
|
+
this.lastRequestContext = usage.requestContext || null;
|
|
6042
|
+
this.saveWorkspaceConversationState(false);
|
|
6043
|
+
return;
|
|
6044
|
+
}
|
|
6045
|
+
// A late auxiliary response belongs to the captured target, not whichever
|
|
6046
|
+
// conversation the user selected while it was running. Do not revive deletion.
|
|
6047
|
+
if (!request.workspace || !request.stateKey)
|
|
6048
|
+
return;
|
|
6049
|
+
this.mutateStoredConversationState(request.workspace, stored => {
|
|
6050
|
+
const entry = stored.conversations?.[request.stateKey];
|
|
6051
|
+
if (!entry)
|
|
6052
|
+
return stored;
|
|
6053
|
+
const usage = this.normalizedConversationUsage(entry.providerUsage);
|
|
6054
|
+
mutate(usage);
|
|
6055
|
+
entry.providerUsage = usage;
|
|
6056
|
+
entry.updatedAt = new Date(Math.max(Date.now(), (Date.parse(entry.updatedAt || '') || 0) + 1)).toISOString();
|
|
6057
|
+
// A different active conversation can already have a debounced snapshot
|
|
6058
|
+
// of this same store. Update only this entry's usage in that snapshot so
|
|
6059
|
+
// its later flush cannot roll back the late measurement or discard B.
|
|
6060
|
+
const storePath = this.workspaceConversationStorePath(request.workspace);
|
|
6061
|
+
const dirtyEntry = storePath ? this.conversationStateDirty.get(storePath)?.state.conversations?.[request.stateKey] : undefined;
|
|
6062
|
+
if (dirtyEntry)
|
|
6063
|
+
dirtyEntry.providerUsage = structuredClone(usage);
|
|
6064
|
+
const memoryKey = `${request.workspace.isInternal ? 'internal' : 'external'}:${path.resolve(request.workspace.path)}::conversation:${request.conversationId}`;
|
|
6065
|
+
const memory = this.workspaceConversations.get(memoryKey);
|
|
6066
|
+
if (memory) {
|
|
6067
|
+
memory.providerUsage = structuredClone(usage);
|
|
6068
|
+
memory.updatedAt = entry.updatedAt;
|
|
6069
|
+
}
|
|
6070
|
+
return stored;
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
beginProviderUsageRequest() {
|
|
6074
|
+
const request = {
|
|
6075
|
+
id: crypto.randomUUID(), conversationId: this.activeConversationId,
|
|
6076
|
+
workspace: this.workspace.current ? { ...this.workspace.current } : null,
|
|
6077
|
+
stateKey: this.workspaceConversationStateKey(),
|
|
6078
|
+
accounting: (0, providerUsageAccounting_1.beginAccountingRequest)((0, providerUsageAccounting_1.createAccounting)()),
|
|
6079
|
+
};
|
|
6080
|
+
this.mutateRequestUsage(request, usage => { request.accounting = (0, providerUsageAccounting_1.beginAccountingRequest)(usage.accounting); });
|
|
6081
|
+
return request;
|
|
6082
|
+
}
|
|
6083
|
+
recordRequestContext(request, messages, system, tools, model) {
|
|
6084
|
+
const context = {
|
|
6085
|
+
...(0, requestContextEstimate_1.estimateSubmittedContext)(messages, system, tools, this.compressionBuildBlockStart(messages)),
|
|
6086
|
+
requestId: request.id, runId: this.currentWorkRunId(), model, at: new Date().toISOString(),
|
|
6087
|
+
};
|
|
6088
|
+
this.mutateRequestUsage(request, usage => { usage.requestContext = context; });
|
|
6089
|
+
}
|
|
6090
|
+
recordProviderUsage(input, request = this.beginProviderUsageRequest()) {
|
|
6091
|
+
this.mutateRequestUsage(request, usage => {
|
|
6092
|
+
(0, providerUsageAccounting_1.applyAccountingUsage)(usage.accounting, usage.totals, request.accounting, input);
|
|
6093
|
+
usage.last = { ...request.accounting.usage };
|
|
6094
|
+
if (usage.requestContext?.requestId === request.id) {
|
|
6095
|
+
if (request.accounting.reported.input)
|
|
6096
|
+
usage.requestContext.inputTokens = request.accounting.usage.input;
|
|
6097
|
+
if (request.accounting.reported.cacheRead)
|
|
6098
|
+
usage.requestContext.cacheReadTokens = request.accounting.usage.cacheRead;
|
|
6099
|
+
}
|
|
6100
|
+
});
|
|
6101
|
+
}
|
|
6102
|
+
async chatWithConversationUsage(provider, ...args) {
|
|
6103
|
+
// Bind before awaiting. The callback runs for successful responses even if
|
|
6104
|
+
// usage is absent; failed HTTP/model-probe requests are not invented spend.
|
|
6105
|
+
const workspace = this.workspace.current ? { ...this.workspace.current } : null;
|
|
6106
|
+
const conversationId = this.activeConversationId;
|
|
6107
|
+
const stateKey = this.workspaceConversationStateKey();
|
|
6108
|
+
let request;
|
|
6109
|
+
const callback = args[7];
|
|
6110
|
+
args[7] = input => {
|
|
6111
|
+
if (!request) {
|
|
6112
|
+
request = { id: crypto.randomUUID(), workspace, conversationId, stateKey, accounting: (0, providerUsageAccounting_1.beginAccountingRequest)((0, providerUsageAccounting_1.createAccounting)()) };
|
|
6113
|
+
this.mutateRequestUsage(request, usage => { request.accounting = (0, providerUsageAccounting_1.beginAccountingRequest)(usage.accounting); });
|
|
6114
|
+
}
|
|
6115
|
+
this.recordProviderUsage(input, request);
|
|
6116
|
+
callback?.(input);
|
|
5738
6117
|
};
|
|
5739
|
-
|
|
5740
|
-
this.providerUsageTotals.input += usage.input;
|
|
5741
|
-
this.providerUsageTotals.output += usage.output;
|
|
5742
|
-
this.providerUsageTotals.cacheRead += usage.cacheRead;
|
|
5743
|
-
this.providerUsageTotals.cacheWrite += usage.cacheWrite;
|
|
6118
|
+
return provider.chat(...args);
|
|
5744
6119
|
}
|
|
5745
6120
|
contextWindow(modelName = this.model) {
|
|
5746
|
-
const
|
|
6121
|
+
const activeRequest = this.activeAgentKernelRuntime && this.lastRequestContext?.runId === this.currentWorkRunId()
|
|
6122
|
+
? this.lastRequestContext : null;
|
|
6123
|
+
const historyEstimate = activeRequest ? null
|
|
6124
|
+
: (0, requestContextEstimate_1.estimateSubmittedContext)(this.history, '', [], this.compressionBuildBlockStart(this.history));
|
|
6125
|
+
const estimatedTokens = activeRequest?.estimatedTokens ?? historyEstimate.estimatedTokens;
|
|
5747
6126
|
// Display and compression must share one window resolution. Both resolve
|
|
5748
6127
|
// the auto branch through the active (routed) deployment so the UI ring and
|
|
5749
6128
|
// the compaction trigger stay on the same maxTokens even after an Auto
|
|
@@ -5753,14 +6132,15 @@ class Agent {
|
|
|
5753
6132
|
const maxTokens = Math.max(1, Number(model?.max_tokens || 0) || 128000);
|
|
5754
6133
|
const ratio = estimatedTokens / maxTokens;
|
|
5755
6134
|
const budget = this.compressionBudget(this.history, modelName);
|
|
6135
|
+
const cache = (0, providerUsageAccounting_1.ratioSummary)(this.providerUsageAccounting);
|
|
5756
6136
|
return {
|
|
5757
6137
|
estimatedTokens,
|
|
5758
6138
|
maxTokens,
|
|
5759
6139
|
ratio,
|
|
5760
6140
|
warning: ratio >= 1 ? 'over_limit' : ratio >= 0.85 ? 'near_limit' : 'ok',
|
|
5761
6141
|
model: modelName,
|
|
5762
|
-
buildBlockTokens:
|
|
5763
|
-
longHistoryTokens:
|
|
6142
|
+
buildBlockTokens: activeRequest?.buildBlockTokens ?? historyEstimate.buildBlockTokens,
|
|
6143
|
+
longHistoryTokens: activeRequest?.longHistoryTokens ?? historyEstimate.longHistoryTokens,
|
|
5764
6144
|
buildBlockTriggerTokens: budget.buildBlockTriggerTokens,
|
|
5765
6145
|
longHistoryTriggerTokens: budget.longHistoryTriggerTokens,
|
|
5766
6146
|
buildBlockRetentionTokens: budget.buildBlockRetentionTokens,
|
|
@@ -5775,9 +6155,20 @@ class Agent {
|
|
|
5775
6155
|
providerOutputTokens: this.providerUsageTotals.output,
|
|
5776
6156
|
providerCacheReadTokens: this.providerUsageTotals.cacheRead,
|
|
5777
6157
|
providerCacheWriteTokens: this.providerUsageTotals.cacheWrite,
|
|
5778
|
-
providerCacheReadRatio:
|
|
5779
|
-
|
|
5780
|
-
|
|
6158
|
+
providerCacheReadRatio: cache.totalRatio,
|
|
6159
|
+
providerKnownCacheReadRatio: cache.knownRatio,
|
|
6160
|
+
providerCacheEligibleInputTokens: cache.denominator,
|
|
6161
|
+
providerUsageRequests: this.providerUsageAccounting.requests,
|
|
6162
|
+
providerUsageInputReportedRequests: this.providerUsageAccounting.inputReportedRequests,
|
|
6163
|
+
providerUsageOutputReportedRequests: this.providerUsageAccounting.outputReportedRequests,
|
|
6164
|
+
providerUsageCacheReportedRequests: this.providerUsageAccounting.cacheReportedRequests,
|
|
6165
|
+
providerUsageHasLegacyTotals: this.providerUsageAccounting.hasLegacyTotals,
|
|
6166
|
+
providerLastInputTokens: this.providerUsageAccounting.lastInputTokens,
|
|
6167
|
+
requestContext: this.lastRequestContext ? { ...this.lastRequestContext } : null,
|
|
6168
|
+
contextEstimateSource: activeRequest ? 'active_request' : 'history',
|
|
6169
|
+
contextEstimateHasImages: activeRequest?.hasImages ?? historyEstimate.hasImages,
|
|
6170
|
+
systemPromptTokens: activeRequest?.systemPromptTokens ?? 0,
|
|
6171
|
+
toolSchemaTokens: activeRequest?.toolSchemaTokens ?? 0,
|
|
5781
6172
|
};
|
|
5782
6173
|
}
|
|
5783
6174
|
resolveWindowModel(modelName) {
|
|
@@ -5827,16 +6218,17 @@ class Agent {
|
|
|
5827
6218
|
}
|
|
5828
6219
|
compressionBuildBlockStart(messages) {
|
|
5829
6220
|
const activeRunId = this.currentWorkRunId();
|
|
6221
|
+
// No active Build means every retained message is historical, including
|
|
6222
|
+
// legacy assistant/tool tails whose old persistence omitted run metadata.
|
|
6223
|
+
if (!activeRunId)
|
|
6224
|
+
return messages.length;
|
|
5830
6225
|
if (activeRunId) {
|
|
5831
6226
|
const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
|
|
5832
6227
|
if (index >= 0)
|
|
5833
6228
|
return index;
|
|
5834
6229
|
}
|
|
5835
|
-
//
|
|
5836
|
-
//
|
|
5837
|
-
// - 存在带 run_id 的消息:boundary 取最后一个 run 的起点之后(该 run 及其
|
|
5838
|
-
// 之前的历史属于长期历史,其后无归属的消息属于当前未命名区块);
|
|
5839
|
-
// - 完全没有 run_id:全部历史都是长期历史(不存在当前 Build Block)。
|
|
6230
|
+
// Legacy active transcripts may lack the current run tag. Retain the
|
|
6231
|
+
// historical tagged prefix; ordinary new inputs now carry their run_id.
|
|
5840
6232
|
let lastRunBoundary = -1;
|
|
5841
6233
|
for (let index = 0; index < messages.length; index += 1) {
|
|
5842
6234
|
if (String(messages[index]?.run_id || messages[index]?.runId || ''))
|
|
@@ -6189,6 +6581,7 @@ class Agent {
|
|
|
6189
6581
|
title: this.titleFromMessages(messages, clean),
|
|
6190
6582
|
chatMessages: messages,
|
|
6191
6583
|
history: sourceHistory,
|
|
6584
|
+
providerUsage: memory?.providerUsage,
|
|
6192
6585
|
plan: memory?.plan,
|
|
6193
6586
|
linkedPlan: memory?.linkedPlan,
|
|
6194
6587
|
subagentState: memory?.subagentState,
|
|
@@ -6286,6 +6679,7 @@ class Agent {
|
|
|
6286
6679
|
title: this.titleFromMessages(messages, clean),
|
|
6287
6680
|
chatMessages: messages,
|
|
6288
6681
|
history: sourceHistory,
|
|
6682
|
+
providerUsage: memory?.providerUsage,
|
|
6289
6683
|
plan: memory?.plan,
|
|
6290
6684
|
linkedPlan: memory?.linkedPlan,
|
|
6291
6685
|
subagentState: memory?.subagentState,
|
|
@@ -7223,7 +7617,7 @@ class Agent {
|
|
|
7223
7617
|
try {
|
|
7224
7618
|
const provider = this.engineModel();
|
|
7225
7619
|
if (provider)
|
|
7226
|
-
corrected = String(await
|
|
7620
|
+
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();
|
|
7227
7621
|
}
|
|
7228
7622
|
catch { }
|
|
7229
7623
|
return JSON.stringify({
|
|
@@ -7236,17 +7630,57 @@ class Agent {
|
|
|
7236
7630
|
uncertainty: corrected ? 'preserved' : 'raw_ocr_only',
|
|
7237
7631
|
}, null, 2);
|
|
7238
7632
|
}
|
|
7239
|
-
engineModel() {
|
|
7633
|
+
engineModel(cache) {
|
|
7240
7634
|
if (this.forcedProvider) {
|
|
7241
7635
|
const active = this.activeDeployment();
|
|
7242
7636
|
if (!this.forcedProviderDeployment
|
|
7243
|
-
|| (active && deploymentIdentity(active) === this.forcedProviderDeployment))
|
|
7637
|
+
|| (active && deploymentIdentity(active) === this.forcedProviderDeployment)) {
|
|
7638
|
+
if (cache) {
|
|
7639
|
+
delete cache.key;
|
|
7640
|
+
delete cache.provider;
|
|
7641
|
+
}
|
|
7244
7642
|
return this.forcedProvider;
|
|
7643
|
+
}
|
|
7245
7644
|
}
|
|
7246
7645
|
const m = this.activeModelConfig();
|
|
7247
|
-
if (!m)
|
|
7646
|
+
if (!m) {
|
|
7647
|
+
if (cache) {
|
|
7648
|
+
delete cache.key;
|
|
7649
|
+
delete cache.provider;
|
|
7650
|
+
}
|
|
7248
7651
|
return null;
|
|
7249
|
-
|
|
7652
|
+
}
|
|
7653
|
+
return this.modelProvider(m, cache);
|
|
7654
|
+
}
|
|
7655
|
+
modelProvider(m, cache) {
|
|
7656
|
+
const apiMode = this.config.openAIApiMode();
|
|
7657
|
+
const adapters = this.config.contextFlag('provider_adapters_v2');
|
|
7658
|
+
const thinkingMaps = this.modelThinkingTierMaps(m);
|
|
7659
|
+
const proxy = this.providerProxyConfig();
|
|
7660
|
+
const create = () => new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, apiMode, adapters, undefined, thinkingMaps, proxy);
|
|
7661
|
+
if (!cache)
|
|
7662
|
+
return create();
|
|
7663
|
+
// Match the provider's configured-proxy/environment precedence. A changed
|
|
7664
|
+
// effective proxy must not reuse a dispatcher created for its predecessor.
|
|
7665
|
+
const effectiveProxyUrl = proxy.enabled === false ? '' : (proxy.url.trim()
|
|
7666
|
+
|| process.env.HTTPS_PROXY || process.env.HTTP_PROXY
|
|
7667
|
+
|| process.env.https_proxy || process.env.http_proxy || '');
|
|
7668
|
+
const orderedMaps = Object.keys(thinkingMaps || {}).sort().map(model => [
|
|
7669
|
+
model,
|
|
7670
|
+
Object.keys(thinkingMaps[model]).sort().map(tier => [tier, thinkingMaps[model][tier]]),
|
|
7671
|
+
]);
|
|
7672
|
+
const key = crypto.createHash('sha256').update(JSON.stringify([
|
|
7673
|
+
m.provider_id, m.name, m.logical_model_group_id || '',
|
|
7674
|
+
m.provider, m.provider_url, m.api_key, m.provider_protocol,
|
|
7675
|
+
apiMode, adapters, orderedMaps,
|
|
7676
|
+
proxy.enabled, proxy.url, proxy.auth, effectiveProxyUrl,
|
|
7677
|
+
])).digest('hex');
|
|
7678
|
+
if (cache.key === key && cache.provider)
|
|
7679
|
+
return cache.provider;
|
|
7680
|
+
const provider = create();
|
|
7681
|
+
cache.key = key;
|
|
7682
|
+
cache.provider = provider;
|
|
7683
|
+
return provider;
|
|
7250
7684
|
}
|
|
7251
7685
|
/**
|
|
7252
7686
|
* dev-0.4.3 模型原生思考强度档位映射表(模型名 → thinking_tier_map)。
|
|
@@ -7586,6 +8020,7 @@ class Agent {
|
|
|
7586
8020
|
if (!explicitFixedModel)
|
|
7587
8021
|
this.ensureUsableModelSelection();
|
|
7588
8022
|
let clientMessageId = String(inputEnvelope?.clientMessageId || '').trim();
|
|
8023
|
+
const userMessageId = String(inputEnvelope?.userMessageId || '').trim();
|
|
7589
8024
|
const inputRunId = String(inputEnvelope?.runId || this.activeWorkRunId || '').trim();
|
|
7590
8025
|
let rawImages = typeof input === 'string' ? [] : (Array.isArray(input.images) ? input.images : []);
|
|
7591
8026
|
// dev-0.4.3 Guide 批量续接:同一 Build block 内连续到达的多个 Guide
|
|
@@ -7692,7 +8127,7 @@ class Agent {
|
|
|
7692
8127
|
const historyContent = images.length
|
|
7693
8128
|
? [{ type: 'text', text }, ...images.map(image => ({ type: 'image_url', image_url: { url: image.dataUrl } }))]
|
|
7694
8129
|
: text;
|
|
7695
|
-
if (clientMessageId) {
|
|
8130
|
+
if (clientMessageId && !userMessageId) {
|
|
7696
8131
|
this.persistGuideMessage(clientMessageId, displayText, inputRunId, historyContent, attachments, String(inputEnvelope?.guideId || ''));
|
|
7697
8132
|
if (inputRunId) {
|
|
7698
8133
|
this.recordGuideReceipt({
|
|
@@ -7709,19 +8144,30 @@ class Agent {
|
|
|
7709
8144
|
}
|
|
7710
8145
|
}
|
|
7711
8146
|
else if (!hiddenUserInput) {
|
|
7712
|
-
const messageId = crypto.randomUUID();
|
|
7713
|
-
this.chatMessages.
|
|
8147
|
+
const messageId = userMessageId || crypto.randomUUID();
|
|
8148
|
+
const persistedUser = userMessageId ? this.chatMessages.find(message => message.role === 'user' && message.messageId === userMessageId) : undefined;
|
|
8149
|
+
const userMessage = {
|
|
7714
8150
|
messageId,
|
|
7715
8151
|
branchNodeId: this.currentBranchNodeId(),
|
|
7716
8152
|
role: 'user',
|
|
7717
8153
|
content: displayText,
|
|
7718
8154
|
mode: String(inputEnvelope?.visibleMode || this.modeName()),
|
|
7719
8155
|
model: this.model,
|
|
7720
|
-
timestamp: now,
|
|
8156
|
+
timestamp: persistedUser?.timestamp || now,
|
|
7721
8157
|
attachments: attachments.length ? attachments : undefined,
|
|
7722
8158
|
runId: this.currentWorkRunId() || undefined,
|
|
7723
|
-
}
|
|
7724
|
-
|
|
8159
|
+
};
|
|
8160
|
+
if (persistedUser)
|
|
8161
|
+
Object.assign(persistedUser, userMessage);
|
|
8162
|
+
else
|
|
8163
|
+
this.chatMessages.push(userMessage);
|
|
8164
|
+
const persistedHistory = userMessageId ? this.history.find(message => message.role === 'user' && message.user_message_id === userMessageId) : undefined;
|
|
8165
|
+
if (persistedHistory) {
|
|
8166
|
+
persistedHistory.content = historyContent;
|
|
8167
|
+
persistedHistory.run_id = this.currentWorkRunId() || undefined;
|
|
8168
|
+
}
|
|
8169
|
+
else
|
|
8170
|
+
this.history.push({ role: 'user', content: historyContent, run_id: this.currentWorkRunId() || undefined, ...(userMessageId ? { user_message_id: userMessageId } : {}) });
|
|
7725
8171
|
}
|
|
7726
8172
|
else {
|
|
7727
8173
|
this.history.push({
|
|
@@ -7734,10 +8180,16 @@ class Agent {
|
|
|
7734
8180
|
}
|
|
7735
8181
|
if (!hiddenUserInput)
|
|
7736
8182
|
this.recordWorkRunPrimaryPrompt(displayText);
|
|
8183
|
+
const submittedHistoryMessage = userMessageId
|
|
8184
|
+
? this.history.find(message => message.role === 'user' && message.user_message_id === userMessageId)
|
|
8185
|
+
: this.history.at(-1);
|
|
8186
|
+
const submittedChatMessage = userMessageId
|
|
8187
|
+
? this.chatMessages.find(message => message.role === 'user' && message.messageId === userMessageId)
|
|
8188
|
+
: [...this.chatMessages].reverse().find(message => message.role === 'user');
|
|
7737
8189
|
this.saveWorkspaceConversationState(true);
|
|
7738
8190
|
let firstResponseGateMessageId = '';
|
|
7739
8191
|
let firstResponseTitleInput = null;
|
|
7740
|
-
if (!hiddenUserInput && !clientMessageId && !this.firstAgentResponseStarted) {
|
|
8192
|
+
if (!hiddenUserInput && (!clientMessageId || !!userMessageId) && !this.firstAgentResponseStarted) {
|
|
7741
8193
|
const firstInput = this.firstPersistedUserForTitleGate();
|
|
7742
8194
|
if (!firstInput)
|
|
7743
8195
|
throw new Error('Conversation title generation failed; the first persisted user input is unavailable.');
|
|
@@ -7786,6 +8238,14 @@ class Agent {
|
|
|
7786
8238
|
// Agent provider request.
|
|
7787
8239
|
this.notifyAgentKernelUserMessageStart(text, clientMessageId || undefined);
|
|
7788
8240
|
this.emitWorkEvent({ type: 'start', content: 'Preparing request.' });
|
|
8241
|
+
// Standalone Agent runs acquire their run ID at start, after the title
|
|
8242
|
+
// gate. Bind the captured input now, without moving or bypassing that gate.
|
|
8243
|
+
const startedRunId = this.currentWorkRunId();
|
|
8244
|
+
if (startedRunId && submittedHistoryMessage?.role === 'user')
|
|
8245
|
+
submittedHistoryMessage.run_id = startedRunId;
|
|
8246
|
+
if (startedRunId && !hiddenUserInput && submittedChatMessage)
|
|
8247
|
+
submittedChatMessage.runId = startedRunId;
|
|
8248
|
+
this.saveWorkspaceConversationState(false);
|
|
7789
8249
|
// Use external opencode CLI engine
|
|
7790
8250
|
if (this.engine === 'opencode') {
|
|
7791
8251
|
if (images.length)
|
|
@@ -7996,6 +8456,8 @@ class Agent {
|
|
|
7996
8456
|
const accepted = await this.handleSubagentContinueEnvelope(args);
|
|
7997
8457
|
if (!accepted.ok || !accepted.data?.id)
|
|
7998
8458
|
return accepted.output;
|
|
8459
|
+
if (!['queued', 'working'].includes(accepted.data.status))
|
|
8460
|
+
return accepted.output;
|
|
7999
8461
|
const settled = await this.waitForSubagentSettlement(accepted.data.id);
|
|
8000
8462
|
return `${accepted.output}\n${settled?.result || settled?.error || ''}`.trim();
|
|
8001
8463
|
}
|
|
@@ -8005,22 +8467,36 @@ class Agent {
|
|
|
8005
8467
|
async handleSubagentContinueEnvelope(args) {
|
|
8006
8468
|
try {
|
|
8007
8469
|
const params = JSON.parse(args);
|
|
8008
|
-
|
|
8009
|
-
|
|
8470
|
+
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
8471
|
+
throw new Error('Arguments must be an object.');
|
|
8472
|
+
if (params.wakeup !== undefined && typeof params.wakeup !== 'boolean')
|
|
8473
|
+
throw new Error('wakeup must be a boolean.');
|
|
8474
|
+
if (params.kind !== undefined && !['directive', 'question', 'result', 'handoff'].includes(params.kind))
|
|
8475
|
+
throw new Error('Invalid message kind.');
|
|
8476
|
+
const name = String(params.id || params.name || '').trim();
|
|
8477
|
+
const selected = (0, subagentCommunication_1.selectSubagentCommunication)(this.history, params);
|
|
8478
|
+
const wakeup = params.wakeup === true;
|
|
8479
|
+
if (name === 'root' || name === this.subagents.rootAgentId) {
|
|
8480
|
+
const delivery = this.subagents.sendRootMessage(this.runtimeActorId, selected.body, params.kind || 'result', undefined, wakeup);
|
|
8481
|
+
return { ok: delivery.ok, output: delivery.ok
|
|
8482
|
+
? `[Root mailbox message persisted] ${delivery.message?.id} wakeup=${wakeup}`
|
|
8483
|
+
: `[Subagent] ${delivery.error}`, error: delivery.error,
|
|
8484
|
+
metadata: { kind: 'subagent-send', wakeup, selection: selected.selection } };
|
|
8485
|
+
}
|
|
8010
8486
|
const sa = this.subagents.get(name);
|
|
8011
8487
|
if (!sa)
|
|
8012
8488
|
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', {
|
|
8489
|
+
const delivery = this.subagents.sendMessage(this.runtimeActorId, sa.id, selected.body, params.kind || 'directive', {
|
|
8017
8490
|
correlationId: params.correlation_id,
|
|
8018
8491
|
replyTo: params.reply_to,
|
|
8019
|
-
});
|
|
8020
|
-
|
|
8492
|
+
}, wakeup);
|
|
8493
|
+
const result = this.subagents.toToolResult(sa.id, delivery.ok ? `[Subagent message persisted] ${delivery.message?.id} wakeup=${wakeup}` : `[Subagent] ${delivery.error}`, delivery.ok);
|
|
8494
|
+
result.metadata = { ...result.metadata, wakeup, selection: selected.selection };
|
|
8495
|
+
return result;
|
|
8021
8496
|
}
|
|
8022
|
-
catch {
|
|
8023
|
-
|
|
8497
|
+
catch (error) {
|
|
8498
|
+
const detail = error instanceof Error ? error.message : 'Invalid continue arguments.';
|
|
8499
|
+
return { ok: false, output: `[Subagent] ${detail}`, error: detail };
|
|
8024
8500
|
}
|
|
8025
8501
|
}
|
|
8026
8502
|
handleSubagentResult(args) {
|
|
@@ -8038,7 +8514,11 @@ class Agent {
|
|
|
8038
8514
|
// assistant/user 消息即足以让主 Agent 判断上下文,完整历史按需走
|
|
8039
8515
|
// subagent_read 的 max_chars 分页读取。
|
|
8040
8516
|
const transcript = this.subagents.boundedResultTranscript(sa.id);
|
|
8041
|
-
|
|
8517
|
+
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);
|
|
8518
|
+
const receipt = this.subagentSettlementReceipt(sa.id);
|
|
8519
|
+
if (receipt)
|
|
8520
|
+
result.metadata = { ...result.metadata, settlementReceipt: receipt };
|
|
8521
|
+
return result;
|
|
8042
8522
|
}
|
|
8043
8523
|
catch {
|
|
8044
8524
|
return { ok: false, output: '[Subagent] Invalid result arguments.', error: 'Invalid result arguments.' };
|
|
@@ -8053,7 +8533,9 @@ class Agent {
|
|
|
8053
8533
|
const read = this.subagents.read(this.runtimeActorId, target, Number(params.max_chars || 16000));
|
|
8054
8534
|
if (!read.ok)
|
|
8055
8535
|
return { ok: false, output: `[Subagent] ${read.error}`, error: read.error, metadata: { kind: 'subagent-read' } };
|
|
8056
|
-
|
|
8536
|
+
const peer = this.subagents.get(target);
|
|
8537
|
+
const receipt = peer && read.snapshot?.peer.result === peer.result ? this.subagentSettlementReceipt(peer.id) : undefined;
|
|
8538
|
+
return { ok: true, output: JSON.stringify(read.snapshot, null, 2), data: read.snapshot, metadata: { kind: 'subagent-read', ...(receipt ? { settlementReceipt: receipt } : {}) } };
|
|
8057
8539
|
}
|
|
8058
8540
|
catch {
|
|
8059
8541
|
return { ok: false, output: '[Subagent] Invalid read arguments.', error: 'Invalid read arguments.' };
|
|
@@ -8065,7 +8547,7 @@ class Agent {
|
|
|
8065
8547
|
status = String(JSON.parse(args || '{}').status || '');
|
|
8066
8548
|
}
|
|
8067
8549
|
catch { }
|
|
8068
|
-
const subagents = this.subagents.
|
|
8550
|
+
const subagents = this.subagents.listSummaries(status);
|
|
8069
8551
|
return { ok: true, output: JSON.stringify({ conversationId: this.activeConversationId, subagents }, null, 2), metadata: { kind: 'subagent-list' } };
|
|
8070
8552
|
}
|
|
8071
8553
|
handleSubagentClose(args) {
|
|
@@ -8082,6 +8564,8 @@ class Agent {
|
|
|
8082
8564
|
if (actorId === this.subagents.rootAgentId)
|
|
8083
8565
|
this.activePeerAgents.get(sa.id)?.abortActiveKernelRun();
|
|
8084
8566
|
const closed = this.subagents.close(sa.id, actorId);
|
|
8567
|
+
if (closed)
|
|
8568
|
+
this.peerProviderCaches.delete(sa.id);
|
|
8085
8569
|
return this.subagents.toToolResult(sa.id, closed ? `[Subagent '${sa.name}' closed]` : '[Subagent] Close denied.', closed);
|
|
8086
8570
|
}
|
|
8087
8571
|
catch {
|
|
@@ -8395,7 +8879,7 @@ class Agent {
|
|
|
8395
8879
|
}, null, 2);
|
|
8396
8880
|
try {
|
|
8397
8881
|
const cfg = provider.intelligenceConfig(this.intelligence);
|
|
8398
|
-
const response = await this.withTimeout(
|
|
8882
|
+
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
8883
|
const parsed = this.extractMemoryLabJson(response);
|
|
8400
8884
|
if (!parsed)
|
|
8401
8885
|
return deterministic;
|
|
@@ -8443,7 +8927,7 @@ class Agent {
|
|
|
8443
8927
|
].join('\n');
|
|
8444
8928
|
try {
|
|
8445
8929
|
const cfg = provider.intelligenceConfig(this.intelligence);
|
|
8446
|
-
await this.withTimeout(
|
|
8930
|
+
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
8931
|
}
|
|
8448
8932
|
catch {
|
|
8449
8933
|
throwIfAgentAborted(signal); /* deterministic reindex still runs */
|
|
@@ -8554,10 +9038,18 @@ class Agent {
|
|
|
8554
9038
|
}
|
|
8555
9039
|
const model = assignedModel?.name || (requestedModel === 'auto' ? this.activeModelName() : requestedModel);
|
|
8556
9040
|
const activeModel = this.activeModelConfig();
|
|
8557
|
-
const
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
9041
|
+
const peerProviderCache = this.peerProviderCaches.get(id) || {};
|
|
9042
|
+
this.peerProviderCaches.delete(id);
|
|
9043
|
+
this.peerProviderCaches.set(id, peerProviderCache);
|
|
9044
|
+
for (const retainedId of this.peerProviderCaches.keys()) {
|
|
9045
|
+
if (this.peerProviderCaches.size <= 32)
|
|
9046
|
+
break;
|
|
9047
|
+
if (!this.activePeerAgents.has(retainedId) && retainedId !== id)
|
|
9048
|
+
this.peerProviderCaches.delete(retainedId);
|
|
9049
|
+
}
|
|
9050
|
+
const assignedProvider = assignedModel && (assignedModel.provider_id !== activeModel?.provider_id || assignedModel.name !== activeModel?.name)
|
|
9051
|
+
? this.modelProvider(assignedModel, peerProviderCache)
|
|
9052
|
+
: this.engineModel(peerProviderCache);
|
|
8561
9053
|
if (!assignedProvider || !model) {
|
|
8562
9054
|
throw new Error('No LLM configured. Add provider in Settings > Models.');
|
|
8563
9055
|
}
|
|
@@ -8601,7 +9093,7 @@ class Agent {
|
|
|
8601
9093
|
child.config.set('models', 'auto_switch', false);
|
|
8602
9094
|
child.config.set('skills', 'auto_download', 'disabled');
|
|
8603
9095
|
child.subagents = this.subagents;
|
|
8604
|
-
child.subagentContextPersist = (history, compression) => this.subagents.replaceContext(sa.id, history, compression);
|
|
9096
|
+
child.subagentContextPersist = (history, compression, inputCommitted) => this.subagents.replaceContext(sa.id, history, compression, inputCommitted);
|
|
8605
9097
|
const requestedFlowName = String(flowName || sa.flowName || '').trim();
|
|
8606
9098
|
if (sa.agentMode === 'flow' && requestedFlowName) {
|
|
8607
9099
|
const flowDir = path.join(this.rootPath, 'Flow');
|
|
@@ -8617,6 +9109,7 @@ class Agent {
|
|
|
8617
9109
|
&& message.content.startsWith("Peer agent '")));
|
|
8618
9110
|
const latestPersisted = persistedMessages.at(-1);
|
|
8619
9111
|
if (latestPersisted?.role === 'user'
|
|
9112
|
+
&& !(reason === 'resume' && prompt === subagent_1.SUBAGENT_RECOVERY_PROMPT)
|
|
8620
9113
|
&& (reason === 'spawn' || reason === 'resume' || prompt.includes(latestPersisted.content))) {
|
|
8621
9114
|
persistedMessages.pop();
|
|
8622
9115
|
}
|
|
@@ -8644,6 +9137,18 @@ class Agent {
|
|
|
8644
9137
|
entry.vision_image_path = message.vision_image_path;
|
|
8645
9138
|
return entry;
|
|
8646
9139
|
});
|
|
9140
|
+
const compression = sa.metadata?.contextCompression;
|
|
9141
|
+
if (compression && typeof compression.at === 'string' && typeof compression.summary === 'string') {
|
|
9142
|
+
child.lastCompression = {
|
|
9143
|
+
at: compression.at, summary: compression.summary,
|
|
9144
|
+
originalMessages: Number(compression.originalMessages) || 0,
|
|
9145
|
+
compressedMessages: Number(compression.compressedMessages) || 0,
|
|
9146
|
+
originalChars: Number(compression.originalChars) || 0,
|
|
9147
|
+
compressedChars: Number(compression.compressedChars) || compression.summary.length,
|
|
9148
|
+
compressedTokens: Number(compression.compressedTokens) || 0,
|
|
9149
|
+
model: String(compression.model || model), fallback: !!compression.fallback,
|
|
9150
|
+
};
|
|
9151
|
+
}
|
|
8647
9152
|
child.subscribeAgentKernelUserMessageStart(content => {
|
|
8648
9153
|
const match = String(content || '').match(/^\[Peer mailbox id=([0-9a-f-]{36})\b/i);
|
|
8649
9154
|
if (match)
|
|
@@ -8694,6 +9199,8 @@ class Agent {
|
|
|
8694
9199
|
if (unrelayPeerEvents)
|
|
8695
9200
|
unrelayPeerEvents();
|
|
8696
9201
|
this.activePeerAgents.delete(sa.id);
|
|
9202
|
+
if (this.subagents.get(sa.id)?.status === 'closed')
|
|
9203
|
+
this.peerProviderCaches.delete(sa.id);
|
|
8697
9204
|
}
|
|
8698
9205
|
}
|
|
8699
9206
|
persistPeerTranscript(peerId, child) {
|
|
@@ -9151,7 +9658,7 @@ class Agent {
|
|
|
9151
9658
|
const modelName = String(compressionModel || this.activeModelName()).trim();
|
|
9152
9659
|
if (!modelName)
|
|
9153
9660
|
return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
|
|
9154
|
-
const generated = await this.withTimeout(
|
|
9661
|
+
const generated = await this.withTimeout(this.chatWithConversationUsage(provider, modelName, [...prefixMessages, { role: 'user', content: prompt }], system, temperature, budget.summaryTokens, signal), 120000);
|
|
9155
9662
|
const generatedText = String(generated || '').trim();
|
|
9156
9663
|
if (!generatedText || /^\[LLM Error(?::|\])/i.test(generatedText) || /^LLM Error:/i.test(generatedText)) {
|
|
9157
9664
|
return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
|
|
@@ -9472,12 +9979,18 @@ class Agent {
|
|
|
9472
9979
|
].join('\n'));
|
|
9473
9980
|
}
|
|
9474
9981
|
if (this.intelligence === 'ultra') {
|
|
9475
|
-
parts.push([
|
|
9982
|
+
parts.push(this.isSubagentRuntime ? [
|
|
9983
|
+
'[Ultra Intelligence – Specialist Role]',
|
|
9984
|
+
'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.',
|
|
9985
|
+
'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.',
|
|
9986
|
+
].join('\n') : [
|
|
9476
9987
|
'[Ultra Intelligence – Orchestrator Role]',
|
|
9477
|
-
'You are the lead orchestrator.
|
|
9478
|
-
'
|
|
9988
|
+
'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.',
|
|
9989
|
+
'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
9990
|
].join('\n'));
|
|
9480
9991
|
}
|
|
9992
|
+
if (this.shouldExposeToolInterface())
|
|
9993
|
+
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
9994
|
parts.push(this.buildModePrompt());
|
|
9482
9995
|
const value = this.contextV2.orchestrator.assemble({
|
|
9483
9996
|
// Keep the complete base prompt in one stable section. The linked_plan
|
|
@@ -9535,6 +10048,47 @@ class Agent {
|
|
|
9535
10048
|
currentUserInput: '',
|
|
9536
10049
|
});
|
|
9537
10050
|
}
|
|
10051
|
+
subagentSettlementReceipt(id) {
|
|
10052
|
+
const peer = this.subagents.get(id);
|
|
10053
|
+
return !this.isSubagentRuntime && peer && typeof peer.result === 'string'
|
|
10054
|
+
&& Number.isSafeInteger(peer.settlementRevision) && Number(peer.settlementRevision) > 0
|
|
10055
|
+
? { peerId: peer.id, revision: peer.settlementRevision } : undefined;
|
|
10056
|
+
}
|
|
10057
|
+
/** Peer jobs share a durable request prefix; credentials only enter its hash. */
|
|
10058
|
+
peerRequestCacheIdentity(systemPrompt, catalog) {
|
|
10059
|
+
if (!this.isSubagentRuntime)
|
|
10060
|
+
return undefined;
|
|
10061
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
10062
|
+
systemPrompt, catalog, model: this.activeModelConfig(),
|
|
10063
|
+
apiMode: this.config.openAIApiMode(), adapters: this.config.contextFlag('provider_adapters_v2'),
|
|
10064
|
+
proxy: this.providerProxyConfig(), intelligence: this.intelligence,
|
|
10065
|
+
compression: this.lastCompression?.at || '',
|
|
10066
|
+
})).digest('hex');
|
|
10067
|
+
}
|
|
10068
|
+
readPeerRequestCache(identity) {
|
|
10069
|
+
if (!identity)
|
|
10070
|
+
return undefined;
|
|
10071
|
+
const saved = this.subagents.get(this.runtimeActorId)?.metadata?.requestCache;
|
|
10072
|
+
if (saved?.version !== 1 || saved.identity !== identity || typeof saved.taskFocus !== 'string'
|
|
10073
|
+
|| !Array.isArray(saved.initialTools) || !saved.initialTools.every(name => typeof name === 'string')
|
|
10074
|
+
|| !Array.isArray(saved.provisionedTools) || !saved.provisionedTools.every(name => typeof name === 'string'))
|
|
10075
|
+
return undefined;
|
|
10076
|
+
return { ...saved, initialTools: saved.initialTools.slice(), provisionedTools: saved.provisionedTools.slice() };
|
|
10077
|
+
}
|
|
10078
|
+
persistPeerRequestCache(cache) {
|
|
10079
|
+
if (!this.isSubagentRuntime)
|
|
10080
|
+
return;
|
|
10081
|
+
const previous = this.subagents.get(this.runtimeActorId)?.metadata?.requestCache;
|
|
10082
|
+
if (JSON.stringify(previous) !== JSON.stringify(cache)) {
|
|
10083
|
+
this.subagents.patchMetadata(this.runtimeActorId, { requestCache: cache });
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
checkpointPeerInput() {
|
|
10087
|
+
// Called at the first real provider boundary, after the input has entered
|
|
10088
|
+
// working history. Commit context and job progress in one durable write.
|
|
10089
|
+
if (this.isSubagentRuntime)
|
|
10090
|
+
this.subagentContextPersist?.(this.history, this.lastCompression, true);
|
|
10091
|
+
}
|
|
9538
10092
|
cachedToolDefinitions() {
|
|
9539
10093
|
const identity = JSON.stringify({
|
|
9540
10094
|
mode: this.mode,
|