newmark-agent 0.3.11 → 0.4.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/config.example.json +6 -0
- package/dist/cli-commands.d.ts +8 -0
- package/dist/cli-commands.js +216 -16
- package/dist/cli-discovery.d.ts +15 -0
- package/dist/cli-discovery.js +182 -0
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.js +25 -1
- package/dist/context/domain/types.d.ts +37 -0
- package/dist/context/services/context-orchestrator.js +2 -0
- package/dist/conversation-utility-host.bundle.cjs +1503 -214
- package/dist/conversation-utility-host.js +3 -0
- package/dist/core/agent.d.ts +157 -8
- package/dist/core/agent.js +1176 -112
- package/dist/core/agentKernel/agent-loop.js +29 -3
- package/dist/core/agentKernel/types.d.ts +7 -0
- package/dist/core/agentKernelRunner.d.ts +2 -0
- package/dist/core/agentKernelRunner.js +174 -27
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.js +24 -6
- package/dist/core/conversationKernel.d.ts +5 -0
- package/dist/core/conversationKernel.js +30 -1
- package/dist/core/dshCompatibility.d.ts +198 -0
- package/dist/core/dshCompatibility.js +600 -0
- package/dist/core/electronUtilityAgentClient.d.ts +4 -0
- package/dist/core/electronUtilityAgentClient.js +4 -0
- package/dist/core/electronUtilityRuntimePool.d.ts +19 -0
- package/dist/core/electronUtilityRuntimePool.js +76 -0
- package/dist/core/flow-runner.js +1 -1
- package/dist/core/mcpManager.d.ts +1 -0
- package/dist/core/mcpManager.js +100 -10
- package/dist/core/modelValidationStore.d.ts +4 -1
- package/dist/core/modelValidationStore.js +7 -1
- package/dist/core/subagent.d.ts +6 -0
- package/dist/core/subagent.js +22 -1
- package/dist/core/toolPolicy.d.ts +6 -0
- package/dist/core/toolPolicy.js +49 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/utilityAgentProtocol.d.ts +8 -1
- package/dist/core/workspace.d.ts +15 -0
- package/dist/core/workspace.js +62 -1
- package/dist/core/wslAgentClient.d.ts +4 -0
- package/dist/core/wslAgentClient.js +4 -0
- package/dist/core/wslAgentProtocol.d.ts +8 -1
- package/dist/core/wslAgentRuntimePool.d.ts +12 -0
- package/dist/core/wslAgentRuntimePool.js +71 -0
- package/dist/launcher.js +48 -11
- package/dist/llm/provider.d.ts +9 -6
- package/dist/llm/provider.js +89 -36
- package/dist/main.js +326 -52
- package/dist/preload.js +17 -0
- package/dist/providers/chat-completions.adapter.js +42 -20
- package/dist/providers/provider-adapter.d.ts +3 -0
- package/dist/providers/provider-events.d.ts +7 -0
- package/dist/providers/provider-events.js +44 -0
- package/dist/providers/responses.adapter.js +1 -3
- package/dist/toolchain/registry/tool-registry.d.ts +13 -1
- package/dist/toolchain/registry/tool-registry.js +8 -0
- package/dist/toolchain/registry-seeder.js +51 -5
- package/dist/tools/index.js +11 -2
- package/dist/tools/nativeTools.js +5 -1
- package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
- package/dist/tui/src/app.js +47 -13
- package/dist/tui/src/render.js +23 -7
- package/dist/tui/src/state.js +61 -9
- package/dist/ui/index.html +2775 -284
- package/dist/ui/lucide-sprite.svg +26 -0
- package/dist/wsl-agent-host.bundle.cjs +1503 -214
- package/dist/wsl-agent-host.js +3 -0
- package/package.json +16 -5
package/dist/core/agent.js
CHANGED
|
@@ -69,6 +69,19 @@ const performanceDiagnostics_1 = require("./performanceDiagnostics");
|
|
|
69
69
|
const compressionHistoryArchive_1 = require("./compressionHistoryArchive");
|
|
70
70
|
const runtimeLifecycle_1 = require("./runtimeLifecycle");
|
|
71
71
|
exports.ROOT_AGENT_ACTOR_ID = '00000000-0000-4000-8000-000000000001';
|
|
72
|
+
// Inline completion is an interactive surface: a stale suggestion is worse
|
|
73
|
+
// than no suggestion, and a request that occupies the provider for tens of
|
|
74
|
+
// seconds makes every subsequent keystroke feel broken. Keep its budget
|
|
75
|
+
// separate from normal conversation requests.
|
|
76
|
+
const EDITOR_COMPLETION_BEFORE_CONTEXT_CHARS = 3200;
|
|
77
|
+
const EDITOR_COMPLETION_AFTER_CONTEXT_CHARS = 800;
|
|
78
|
+
const EDITOR_COMPLETION_MAX_TOKENS = 96;
|
|
79
|
+
const EDITOR_COMPLETION_MAX_TEXT_CHARS = 1200;
|
|
80
|
+
const EDITOR_COMPLETION_TIMEOUT_MS = 6500;
|
|
81
|
+
/** 压缩摘要调用前,被省略段中单个工具结果(tool/function 角色)的字符数上限;
|
|
82
|
+
* 超过则裁剪为「头部结论 + 尾部证据」,避免巨型 read/grep/terminal 输出整段
|
|
83
|
+
* 重放给摘要模型(DSH toolResultPruner 的 Newmark 落地)。 */
|
|
84
|
+
const TOOL_RESULT_PRUNE_CHARS = 8000;
|
|
72
85
|
function normalizeIntelligenceTier(value) {
|
|
73
86
|
const tier = String(value || '').trim().toLowerCase();
|
|
74
87
|
return tier === 'low' || tier === 'high' || tier === 'xhigh' || tier === 'max' || tier === 'ultra' ? tier : 'medium';
|
|
@@ -126,6 +139,7 @@ let CORE_SYSTEM_PROMPT = `You are Newmark Agent, a powerful AI coding assistant
|
|
|
126
139
|
- Plan: Fully read-only exploration. Do not modify any files, including README.md.
|
|
127
140
|
- Goal: Persistent objective pursuit. Auto-continue until complete.
|
|
128
141
|
- Flow: Sequential workflow execution with logic branching.
|
|
142
|
+
- You may actively manage the Goal state yourself through the goal_manage tool: enter Goal mode or edit its objective when the user asks for a persistent objective or when it changes, mark it complete when you have genuinely verified it is achieved, and exit Goal mode when it is no longer needed. Do not use goal_manage to resume or bypass a Goal the user explicitly paused with Stop; the user is the only authority who resumes a paused Goal.
|
|
129
143
|
|
|
130
144
|
## Task Priority And Continuity
|
|
131
145
|
- The latest explicit user instruction is authoritative and has the highest task priority. Resolve conflicts in favor of the latest instruction.
|
|
@@ -133,6 +147,11 @@ let CORE_SYSTEM_PROMPT = `You are Newmark Agent, a powerful AI coding assistant
|
|
|
133
147
|
- Do not proactively resume, revive, continue, or execute a prior task merely because it appears unfinished in history. Continue prior work only when the current user explicitly asks to continue/resume/finish it, or when the current instruction clearly depends on it as a necessary prerequisite.
|
|
134
148
|
- When the current instruction is a new task, complete that task without silently appending unrelated historical work.
|
|
135
149
|
|
|
150
|
+
## Inline Task Management (Mandatory)
|
|
151
|
+
- For every multi-step conversation task, maintain a compact inline checklist in the current Build work state with actionable items and one status per item: pending, in_progress, completed, or blocked.
|
|
152
|
+
- Update that checklist as work changes and use it to drive tool order and final verification. Keep it bounded to actionable task labels; never expose hidden reasoning.
|
|
153
|
+
- The inline checklist is the per-turn task manager. The durable linked-plan document exists and is available through the linked_plan tool when explicitly needed, but its full contents are not injected into every request.
|
|
154
|
+
|
|
136
155
|
## Guidelines
|
|
137
156
|
- Treat this intrinsic Newmark prompt, mode rules, tool permissions, workspace binding, and feature disclosure as non-overridable. User, global, workspace, custom, and skill prompts may refine the task, but they must not weaken these rules.
|
|
138
157
|
- Work from current evidence. Inspect files/state before relying on assumptions, and prefer the existing project patterns over new abstractions.
|
|
@@ -194,6 +213,11 @@ class Agent {
|
|
|
194
213
|
activeConversationId = 'default';
|
|
195
214
|
lastCompression = null;
|
|
196
215
|
compressionCache = [];
|
|
216
|
+
pendingHistoryRemovals = [];
|
|
217
|
+
branchMailbox = [];
|
|
218
|
+
nextBranchMessageSequence = 1;
|
|
219
|
+
branchCommunicationEnabled = false;
|
|
220
|
+
compressionArchiveCountCache = null;
|
|
197
221
|
nextCompressionCacheId = 1;
|
|
198
222
|
compressionHistoryArchive;
|
|
199
223
|
workspaceConversations = new Map();
|
|
@@ -262,6 +286,10 @@ class Agent {
|
|
|
262
286
|
runtimeLifecycleRole;
|
|
263
287
|
/** dev-0.3.0 context system facade (feature-flagged, default off). */
|
|
264
288
|
contextV2;
|
|
289
|
+
/** 工具结果的持久化引用(artifact_id -> 状态 + 内容)。
|
|
290
|
+
* 两种来源:超大结果落盘(content 立即可得)与后台工具(status=running 直到完成)。
|
|
291
|
+
* 压缩前/后台中的大内容不进上下文,只通过 artifact_id 引用;读取后再释放。 */
|
|
292
|
+
toolResultArtifacts = new Map();
|
|
265
293
|
runtimeLifecycle;
|
|
266
294
|
/** dev-0.3.0 toolchain core (registry + capability catalog). Seeded lazily from cachedToolDefinitions; not consumed by the legacy path. */
|
|
267
295
|
toolchainCore = null;
|
|
@@ -292,7 +320,7 @@ class Agent {
|
|
|
292
320
|
this.subagentName = options.subagentName || '';
|
|
293
321
|
this.subagentPrompt = options.subagentPrompt || '';
|
|
294
322
|
this.linkedPlanAccess = options.linkedPlanAccess;
|
|
295
|
-
this.config = new config_1.ConfigManager(rootPath);
|
|
323
|
+
this.config = new config_1.ConfigManager(rootPath, { readOnly: options.readOnlyConfig === true });
|
|
296
324
|
this.compressionHistoryArchive = new compressionHistoryArchive_1.CompressionHistoryArchive(rootPath);
|
|
297
325
|
this.contextV2 = new agent_context_manager_1.AgentContextManager(rootPath, this.config);
|
|
298
326
|
this.agentRunService = this.config.contextFlag('agent_runtime_v2')
|
|
@@ -479,6 +507,8 @@ class Agent {
|
|
|
479
507
|
const raw = entry.tree;
|
|
480
508
|
if (raw && [1, 2].includes(Number(raw.version)) && raw.nodes && raw.nodes[raw.activeNodeId]) {
|
|
481
509
|
raw.version = 2;
|
|
510
|
+
if (!Array.isArray(raw.runningNodeIds) || !raw.runningNodeIds.length)
|
|
511
|
+
raw.runningNodeIds = [raw.activeNodeId];
|
|
482
512
|
this.coalesceConversationBranchGroups(raw);
|
|
483
513
|
this.rebuildConversationTreeIndex(raw);
|
|
484
514
|
entry.activeBranchId = raw.activeNodeId;
|
|
@@ -499,6 +529,7 @@ class Agent {
|
|
|
499
529
|
rootNodeId: source.id,
|
|
500
530
|
activeNodeId,
|
|
501
531
|
activeGroupId: groupId,
|
|
532
|
+
runningNodeIds: [activeNodeId],
|
|
502
533
|
nodes,
|
|
503
534
|
branchGroups: {
|
|
504
535
|
[groupId]: {
|
|
@@ -580,13 +611,24 @@ class Agent {
|
|
|
580
611
|
treePath(tree, nodeId) {
|
|
581
612
|
return tree && tree.nodes[nodeId] ? this.treeAncestry(tree, nodeId).reverse() : [];
|
|
582
613
|
}
|
|
614
|
+
/** 确定性消息 ID:基于角色+内容+索引的 sha256,保证旧数据缺失 messageId 时补生成稳定、
|
|
615
|
+
* 不漂移,且跨分支共享 fork 前缀消息得到一致 ID。 */
|
|
616
|
+
deterministicMessageId(message, index) {
|
|
617
|
+
const seed = `${index}:${String(message.role || '')}:${String(message.content === undefined ? '' : (typeof message.content === 'string' ? message.content : JSON.stringify(message.content)))}`;
|
|
618
|
+
return `m-${crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16)}`;
|
|
619
|
+
}
|
|
620
|
+
/** 确定性 Guide ID:基于消息 ID + 索引,保证补生成稳定唯一。 */
|
|
621
|
+
deterministicGuideId(message, index) {
|
|
622
|
+
const base = String(message.messageId || this.deterministicMessageId(message, index));
|
|
623
|
+
return `g-${crypto.createHash('sha256').update(`${index}:${base}`).digest('hex').slice(0, 16)}`;
|
|
624
|
+
}
|
|
583
625
|
rebuildConversationTreeIndex(tree) {
|
|
584
626
|
const childIds = new Map();
|
|
585
627
|
for (const node of Object.values(tree.nodes)) {
|
|
586
|
-
node.chatMessages = (node.chatMessages || []).map(message => ({
|
|
628
|
+
node.chatMessages = (node.chatMessages || []).map((message, messageIndex) => ({
|
|
587
629
|
...message,
|
|
588
|
-
messageId: String(message.messageId || '') ||
|
|
589
|
-
guideId: message.clientMessageId ? (String(message.guideId || '') ||
|
|
630
|
+
messageId: String(message.messageId || '') || this.deterministicMessageId(message, messageIndex),
|
|
631
|
+
guideId: message.clientMessageId ? (String(message.guideId || '') || this.deterministicGuideId(message, messageIndex)) : undefined,
|
|
590
632
|
branchNodeId: node.id,
|
|
591
633
|
}));
|
|
592
634
|
node.workRuns = this.normalizeWorkRuns(node.workRuns).map(run => ({
|
|
@@ -763,7 +805,12 @@ class Agent {
|
|
|
763
805
|
}
|
|
764
806
|
const previousAuto = this.model === 'auto' ? this.resolvedDeployment : null;
|
|
765
807
|
const qualified = parseDeploymentSelectionValue(requested);
|
|
766
|
-
const
|
|
808
|
+
const legacyQualified = requested.includes('/')
|
|
809
|
+
? this.config.allModels().filter(model => `${model.provider_id}/${model.name}` === requested || `${model.provider}/${model.name}` === requested)
|
|
810
|
+
: [];
|
|
811
|
+
const current = qualified
|
|
812
|
+
? this.config.findDeployment(qualified)
|
|
813
|
+
: (legacyQualified.length === 1 ? legacyQualified[0] : (requested ? this.config.findModel(requested) : undefined));
|
|
767
814
|
this.model = current?.name || requested;
|
|
768
815
|
this.fixedDeployment = current ? this.deploymentRef(current) : qualified;
|
|
769
816
|
this.resolvedDeployment = null;
|
|
@@ -1237,7 +1284,7 @@ class Agent {
|
|
|
1237
1284
|
}
|
|
1238
1285
|
isPersistablePublicWorkEvent(event) {
|
|
1239
1286
|
const type = String(event.type || '').toLowerCase();
|
|
1240
|
-
const publicTypes = new Set(['start', 'text', 'response', 'final_response', 'tool_call', 'tool_result', 'status', 'done', 'error', 'queue_update', 'guide']);
|
|
1287
|
+
const publicTypes = new Set(['start', 'text', 'response', 'final_response', 'tool_call', 'tool_result', 'thought', 'thought_result', 'status', 'done', 'error', 'queue_update', 'guide']);
|
|
1241
1288
|
if (!publicTypes.has(type))
|
|
1242
1289
|
return false;
|
|
1243
1290
|
// Tool implementation details are never public. They are dropped before
|
|
@@ -1943,6 +1990,35 @@ class Agent {
|
|
|
1943
1990
|
this.saveWorkspaceConversationState();
|
|
1944
1991
|
return true;
|
|
1945
1992
|
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Close any running Build ledger entries owned by an explicitly interrupted
|
|
1995
|
+
* lifecycle before a Flow is resumed or a conversation is archived.
|
|
1996
|
+
*
|
|
1997
|
+
* The normal Flow runner guard must continue to reject a genuinely
|
|
1998
|
+
* concurrent Build. This method is deliberately explicit and target-scoped:
|
|
1999
|
+
* callers use it only after the owning Flow has been stopped/paused or when
|
|
2000
|
+
* archive has won the lifecycle race. Without this boundary, an isolated
|
|
2001
|
+
* Agent created during resume can legitimately reload the previous snapshot
|
|
2002
|
+
* while its runtime owner is still this Electron process and the guard would
|
|
2003
|
+
* mistake that stale ledger entry for an active Build.
|
|
2004
|
+
*/
|
|
2005
|
+
interruptRunningConversationWorkRuns(target = this.currentConversationTarget(), status = 'interrupted') {
|
|
2006
|
+
const workspaceId = String(target.workspaceId || '');
|
|
2007
|
+
const conversationId = this.safeConversationId(target.conversationId || this.activeConversationId || 'default');
|
|
2008
|
+
const running = this.workRuns
|
|
2009
|
+
.filter(run => run.status === 'running'
|
|
2010
|
+
&& String(run.target.workspaceId || '') === workspaceId
|
|
2011
|
+
&& this.safeConversationId(run.target.conversationId || 'default') === conversationId)
|
|
2012
|
+
.map(run => run.runId);
|
|
2013
|
+
let changed = 0;
|
|
2014
|
+
for (const runId of running) {
|
|
2015
|
+
if (this.finishConversationWorkRun(runId, status))
|
|
2016
|
+
changed += 1;
|
|
2017
|
+
}
|
|
2018
|
+
if (changed)
|
|
2019
|
+
this.flushWorkspaceConversationState();
|
|
2020
|
+
return changed;
|
|
2021
|
+
}
|
|
1946
2022
|
recordGuideReceipt(input) {
|
|
1947
2023
|
const receipt = this.normalizeGuideReceipt(input);
|
|
1948
2024
|
let run = this.workRuns.find(item => item.runId === receipt.runId);
|
|
@@ -1984,9 +2060,11 @@ class Agent {
|
|
|
1984
2060
|
const userHistory = (Array.isArray(history) ? history : []).filter(message => message?.role === 'user');
|
|
1985
2061
|
const consumedUserHistory = new Set();
|
|
1986
2062
|
let nextUserHistoryIndex = 0;
|
|
1987
|
-
return (Array.isArray(messages) ? messages : []).map(message => {
|
|
1988
|
-
|
|
1989
|
-
|
|
2063
|
+
return (Array.isArray(messages) ? messages : []).map((message, messageIndex) => {
|
|
2064
|
+
// 确定性补生成:缺失 messageId/guideId 的旧数据用内容 hash 补,避免每次归一化随机
|
|
2065
|
+
// UUID 导致冷重载 ID 漂移(持久化 ID 唯一性与稳定性)。
|
|
2066
|
+
const messageId = String(message?.messageId || '').trim() || this.deterministicMessageId(message, messageIndex);
|
|
2067
|
+
const guideId = message?.clientMessageId ? (String(message.guideId || '').trim() || this.deterministicGuideId(message, messageIndex)) : undefined;
|
|
1990
2068
|
const identified = { ...message, messageId, guideId, branchNodeId: String(message?.branchNodeId || '') || this.currentBranchNodeId() };
|
|
1991
2069
|
if (!message || message.role !== 'user')
|
|
1992
2070
|
return identified;
|
|
@@ -2062,11 +2140,12 @@ class Agent {
|
|
|
2062
2140
|
this.saveWorkspaceConversationState(true);
|
|
2063
2141
|
return true;
|
|
2064
2142
|
}
|
|
2065
|
-
finishConversationWorkRun(runId, status, endedAt = this.nowIso()) {
|
|
2143
|
+
finishConversationWorkRun(runId, status, endedAt = this.nowIso(), errorMessage = '') {
|
|
2066
2144
|
const run = this.workRuns.find(item => item.runId === String(runId || ''));
|
|
2067
2145
|
if (!run)
|
|
2068
2146
|
return false;
|
|
2069
2147
|
this.syncAgentRunTerminal(run.runId, status, endedAt);
|
|
2148
|
+
this.flushPendingHistoryRemovals();
|
|
2070
2149
|
if (run.status !== 'running') {
|
|
2071
2150
|
if (run.status !== 'interrupted' || status !== 'force_interrupted') {
|
|
2072
2151
|
if (run.status !== status)
|
|
@@ -2110,7 +2189,9 @@ class Agent {
|
|
|
2110
2189
|
this.enforceGoalTerminalInvariant(status, goalAudit);
|
|
2111
2190
|
this.emitWorkEvent({
|
|
2112
2191
|
type: status === 'completed' ? 'done' : status === 'error' ? 'error' : 'status',
|
|
2113
|
-
content: status === '
|
|
2192
|
+
content: status === 'error'
|
|
2193
|
+
? (String(errorMessage || '').trim() || 'Agent run failed.')
|
|
2194
|
+
: status === 'force_interrupted' ? 'Force interrupted.' : status === 'interrupted' ? 'Interrupted.' : 'Response complete.',
|
|
2114
2195
|
status,
|
|
2115
2196
|
runId: run.runId,
|
|
2116
2197
|
conversationId: run.target.conversationId,
|
|
@@ -2211,6 +2292,7 @@ class Agent {
|
|
|
2211
2292
|
activeRun.endedAt = /^\d{4}-\d{2}-\d{2}T/.test(event.timestamp) ? event.timestamp : this.nowIso();
|
|
2212
2293
|
activeRun.expanded = true;
|
|
2213
2294
|
this.activeWorkRunId = '';
|
|
2295
|
+
this.flushPendingHistoryRemovals();
|
|
2214
2296
|
}
|
|
2215
2297
|
}
|
|
2216
2298
|
if (isToolEvent && process.env.NEWMARK_PROVIDER_DIAGNOSTICS === '1') {
|
|
@@ -2281,6 +2363,12 @@ class Agent {
|
|
|
2281
2363
|
this.awaitingAgentKernelRuntime = false;
|
|
2282
2364
|
if (!runtime)
|
|
2283
2365
|
return;
|
|
2366
|
+
// A user stop can arrive while the Native Kernel is still being loaded or
|
|
2367
|
+
// assembling its first context. In that handoff window
|
|
2368
|
+
// abortActiveKernelRun() can only abort the outer process signal; make a
|
|
2369
|
+
// runtime that attaches afterwards observe the already-aborted state too.
|
|
2370
|
+
if (this.activeProcessAbortController?.signal.aborted)
|
|
2371
|
+
runtime.abort?.();
|
|
2284
2372
|
const queued = this.pendingAgentKernelQueue.splice(0);
|
|
2285
2373
|
for (const item of queued) {
|
|
2286
2374
|
const accepted = this.forwardAgentKernelQueueMessage(item.content, item.queueMode, item.clientMessageId, item.runId, item.images, item.hiddenUserInput);
|
|
@@ -2544,16 +2632,23 @@ class Agent {
|
|
|
2544
2632
|
const stateKey = this.workspaceConversationStateKey(conversationId);
|
|
2545
2633
|
if (!stateKey)
|
|
2546
2634
|
return;
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2635
|
+
// Use the lock-aware latest-state mutator directly. Passing a partial
|
|
2636
|
+
// flowSuspensions object through writeStoredConversationStateNow() would
|
|
2637
|
+
// merge the deleted key back from the latest disk snapshot, making Stop,
|
|
2638
|
+
// archive, and new-work handoff appear to clear a suspension in memory
|
|
2639
|
+
// while it immediately reappears after reload.
|
|
2640
|
+
this.mutateStoredConversationState(this.workspace.current, latest => {
|
|
2641
|
+
const flowSuspensions = { ...(latest.flowSuspensions || {}) };
|
|
2642
|
+
if (suspension) {
|
|
2643
|
+
flowSuspensions[stateKey] = { ...suspension, updatedAt: suspension.updatedAt || new Date().toISOString() };
|
|
2644
|
+
}
|
|
2645
|
+
else {
|
|
2646
|
+
delete flowSuspensions[stateKey];
|
|
2647
|
+
}
|
|
2648
|
+
const next = { ...latest, flowSuspensions };
|
|
2649
|
+
delete next.flowSuspension;
|
|
2650
|
+
return next;
|
|
2651
|
+
});
|
|
2557
2652
|
}
|
|
2558
2653
|
clearStoredFlowSuspension(conversationId = this.activeConversationId) {
|
|
2559
2654
|
this.saveStoredFlowSuspension(null, conversationId);
|
|
@@ -2770,6 +2865,7 @@ class Agent {
|
|
|
2770
2865
|
pinned: !!value.pinned,
|
|
2771
2866
|
pinnedAt: value.pinnedAt || '',
|
|
2772
2867
|
order: Number(value.order || 0),
|
|
2868
|
+
branchCommunication: !!value.branchCommunication,
|
|
2773
2869
|
});
|
|
2774
2870
|
}
|
|
2775
2871
|
rows.sort((a, b) => {
|
|
@@ -3091,7 +3187,7 @@ class Agent {
|
|
|
3091
3187
|
if (!tree) {
|
|
3092
3188
|
const originalId = String(entry.rootBranchNodeId || '') || crypto.randomUUID();
|
|
3093
3189
|
const original = this.treeNodeFromEntry(originalId, null, requestedIndex, '', entry);
|
|
3094
|
-
tree = { version: 2, rootNodeId: originalId, activeNodeId: originalId, activeGroupId: '', nodes: { [originalId]: original }, branchGroups: {}, nodeIndex: {}, pathIndex: {} };
|
|
3190
|
+
tree = { version: 2, rootNodeId: originalId, activeNodeId: originalId, activeGroupId: '', runningNodeIds: [originalId], nodes: { [originalId]: original }, branchGroups: {}, nodeIndex: {}, pathIndex: {} };
|
|
3095
3191
|
entry.tree = tree;
|
|
3096
3192
|
entry.rootBranchNodeId = originalId;
|
|
3097
3193
|
}
|
|
@@ -3199,6 +3295,16 @@ class Agent {
|
|
|
3199
3295
|
nodeIds: [parentNodeId, branchId],
|
|
3200
3296
|
};
|
|
3201
3297
|
}
|
|
3298
|
+
if (this.branchCommunicationEnabled) {
|
|
3299
|
+
tree.runningNodeIds = tree.runningNodeIds || [];
|
|
3300
|
+
if (parentNodeId && !tree.runningNodeIds.includes(parentNodeId))
|
|
3301
|
+
tree.runningNodeIds.push(parentNodeId);
|
|
3302
|
+
if (!tree.runningNodeIds.includes(branchId))
|
|
3303
|
+
tree.runningNodeIds.push(branchId);
|
|
3304
|
+
}
|
|
3305
|
+
else {
|
|
3306
|
+
tree.runningNodeIds = [branchId];
|
|
3307
|
+
}
|
|
3202
3308
|
tree.activeNodeId = branchId;
|
|
3203
3309
|
tree.activeGroupId = groupId;
|
|
3204
3310
|
this.rebuildConversationTreeIndex(tree);
|
|
@@ -3216,6 +3322,14 @@ class Agent {
|
|
|
3216
3322
|
this.setConversationFromStorage(clean);
|
|
3217
3323
|
return this.getConversationSnapshot(clean);
|
|
3218
3324
|
}
|
|
3325
|
+
setBranchCommunication(enabled) {
|
|
3326
|
+
this.branchCommunicationEnabled = !!enabled;
|
|
3327
|
+
this.saveWorkspaceConversationState(true);
|
|
3328
|
+
return this.branchCommunicationEnabled;
|
|
3329
|
+
}
|
|
3330
|
+
isBranchCommunicationEnabled() {
|
|
3331
|
+
return this.branchCommunicationEnabled;
|
|
3332
|
+
}
|
|
3219
3333
|
switchConversationBranch(conversationId, branchId, branchGroupId = '') {
|
|
3220
3334
|
const clean = this.safeConversationId(conversationId || 'default');
|
|
3221
3335
|
this.saveWorkspaceConversationState(true);
|
|
@@ -3234,6 +3348,18 @@ class Agent {
|
|
|
3234
3348
|
const group = requestedGroup?.nodeIds.includes(branch.id)
|
|
3235
3349
|
? requestedGroup
|
|
3236
3350
|
: Object.values(tree.branchGroups).find(item => item.nodeIds.includes(branch.id) && item.nodeIds.includes(priorActiveNodeId));
|
|
3351
|
+
// 分支交流模式:移除“运行分支唯一性”——更换运行分支变为“新增运行分支”,
|
|
3352
|
+
// 旧分支保持运行,目标分支加入运行集合;其余交互逻辑不变。
|
|
3353
|
+
if (this.branchCommunicationEnabled) {
|
|
3354
|
+
tree.runningNodeIds = tree.runningNodeIds || [];
|
|
3355
|
+
for (const runningId of [priorActiveNodeId, branch.id]) {
|
|
3356
|
+
if (runningId && !tree.runningNodeIds.includes(runningId))
|
|
3357
|
+
tree.runningNodeIds.push(runningId);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
else {
|
|
3361
|
+
tree.runningNodeIds = [branch.id];
|
|
3362
|
+
}
|
|
3237
3363
|
tree.activeNodeId = branch.id;
|
|
3238
3364
|
if (group)
|
|
3239
3365
|
tree.activeGroupId = group.id;
|
|
@@ -3285,6 +3411,24 @@ class Agent {
|
|
|
3285
3411
|
this.writeStoredConversationState(stored);
|
|
3286
3412
|
return true;
|
|
3287
3413
|
}
|
|
3414
|
+
/**
|
|
3415
|
+
* 首 Build 命名提示判定:仅当 (1) 当前对话尚无历史 Build(排除当前 run)且
|
|
3416
|
+
* (2) 其持久化 title 仍是自动生成(含为空)时返回 true。满足条件时运行时会
|
|
3417
|
+
* 在首个 provider request 的 bootstrap 注入一次性命名指令,让 Agent 调用
|
|
3418
|
+
* conversation_rename 自行命名。判定本身只读存储、无副作用,保缓存稳定。
|
|
3419
|
+
*/
|
|
3420
|
+
shouldPromptConversationRename() {
|
|
3421
|
+
if (this.conversationBuildHistory(1).length > 0)
|
|
3422
|
+
return false;
|
|
3423
|
+
const conversationId = this.activeConversationId || 'default';
|
|
3424
|
+
const stateKey = this.workspaceConversationStateKey(conversationId);
|
|
3425
|
+
if (!stateKey)
|
|
3426
|
+
return false;
|
|
3427
|
+
const entry = this.readStoredConversationState().conversations?.[stateKey];
|
|
3428
|
+
const priorTitle = entry?.title;
|
|
3429
|
+
const messages = entry?.chatMessages || this.chatMessages;
|
|
3430
|
+
return this.isGeneratedConversationTitle(priorTitle, conversationId, messages);
|
|
3431
|
+
}
|
|
3288
3432
|
reorderConversations(ids) {
|
|
3289
3433
|
const prefix = this.workspaceConversationPrefix() || '';
|
|
3290
3434
|
const normalized = Array.from(new Set((Array.isArray(ids) ? ids : []).map(id => this.safeConversationId(id)).filter(Boolean)));
|
|
@@ -3389,6 +3533,8 @@ class Agent {
|
|
|
3389
3533
|
chatMessages: [...this.chatMessages],
|
|
3390
3534
|
history: [...this.history],
|
|
3391
3535
|
compressionCache: [...this.compressionCache],
|
|
3536
|
+
branchMailbox: [...this.branchMailbox],
|
|
3537
|
+
branchCommunication: this.branchCommunicationEnabled,
|
|
3392
3538
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3393
3539
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3394
3540
|
subagentState: this.subagents.serialize(),
|
|
@@ -3422,6 +3568,8 @@ class Agent {
|
|
|
3422
3568
|
chatMessages: [...this.chatMessages],
|
|
3423
3569
|
history: [...this.history],
|
|
3424
3570
|
compressionCache: [...this.compressionCache],
|
|
3571
|
+
branchMailbox: [...this.branchMailbox],
|
|
3572
|
+
branchCommunication: this.branchCommunicationEnabled,
|
|
3425
3573
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3426
3574
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3427
3575
|
subagentState: this.subagents.serialize(),
|
|
@@ -3474,6 +3622,9 @@ class Agent {
|
|
|
3474
3622
|
this.history = [...saved.history];
|
|
3475
3623
|
this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
3476
3624
|
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
3625
|
+
this.branchMailbox = (saved.branchMailbox || []).map(message => ({ ...message }));
|
|
3626
|
+
this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map(message => Number(message.sequence) || 0)) + 1;
|
|
3627
|
+
this.branchCommunicationEnabled = !!saved.branchCommunication;
|
|
3477
3628
|
this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
|
|
3478
3629
|
this.conversationPlan = this.normalizeConversationPlan(saved.plan);
|
|
3479
3630
|
this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
|
|
@@ -3496,6 +3647,9 @@ class Agent {
|
|
|
3496
3647
|
this.history = persisted?.history ? [...persisted.history] : [];
|
|
3497
3648
|
this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
3498
3649
|
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
3650
|
+
this.branchMailbox = (persisted?.branchMailbox || []).map(message => ({ ...message }));
|
|
3651
|
+
this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map(message => Number(message.sequence) || 0)) + 1;
|
|
3652
|
+
this.branchCommunicationEnabled = !!persisted?.branchCommunication;
|
|
3499
3653
|
this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
|
|
3500
3654
|
this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
|
|
3501
3655
|
this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
|
|
@@ -3534,6 +3688,8 @@ class Agent {
|
|
|
3534
3688
|
chatMessages: [...this.chatMessages],
|
|
3535
3689
|
history: [...this.history],
|
|
3536
3690
|
compressionCache: [...this.compressionCache],
|
|
3691
|
+
branchMailbox: [...this.branchMailbox],
|
|
3692
|
+
branchCommunication: this.branchCommunicationEnabled,
|
|
3537
3693
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3538
3694
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3539
3695
|
subagentState: this.subagents.serialize(),
|
|
@@ -3595,6 +3751,28 @@ class Agent {
|
|
|
3595
3751
|
this.loadWorkspaceConversationState();
|
|
3596
3752
|
return selected;
|
|
3597
3753
|
}
|
|
3754
|
+
refreshWorkspaceRegistryFromStorage() {
|
|
3755
|
+
const before = JSON.stringify({
|
|
3756
|
+
internal: this.workspace.internal,
|
|
3757
|
+
external: this.workspace.external,
|
|
3758
|
+
current: this.workspace.current,
|
|
3759
|
+
});
|
|
3760
|
+
const selected = this.workspace.reloadFromStorage();
|
|
3761
|
+
const after = JSON.stringify({
|
|
3762
|
+
internal: this.workspace.internal,
|
|
3763
|
+
external: this.workspace.external,
|
|
3764
|
+
current: this.workspace.current,
|
|
3765
|
+
});
|
|
3766
|
+
if (before === after)
|
|
3767
|
+
return selected;
|
|
3768
|
+
if (selected)
|
|
3769
|
+
this.config.loadWorkspaceConfig(selected.path);
|
|
3770
|
+
else
|
|
3771
|
+
this.config.clearWorkspaceOverrides();
|
|
3772
|
+
this.workspaceConversations.clear();
|
|
3773
|
+
this.loadWorkspaceConversationState();
|
|
3774
|
+
return selected;
|
|
3775
|
+
}
|
|
3598
3776
|
setConversation(id) {
|
|
3599
3777
|
const clean = this.safeConversationId(id || 'default');
|
|
3600
3778
|
// Conversation runners may bind a target workspace directly before their
|
|
@@ -3718,6 +3896,9 @@ class Agent {
|
|
|
3718
3896
|
if (!run)
|
|
3719
3897
|
return JSON.stringify({ ok: false, error: 'Historical Build Block state is unavailable.' });
|
|
3720
3898
|
const maxEvents = Math.max(1, Math.min(200, Math.floor(Number(input.max_events || 80))));
|
|
3899
|
+
// 有界历史读取:每条 activity/guide content 截断到 2000 字符,避免无界历史
|
|
3900
|
+
// Build 的巨型 tool 输出整段内联进当前 Build,既省 token 也保后续缓存前缀稳定。
|
|
3901
|
+
const boundedActivityChars = Math.max(100, Math.min(4000, Math.floor(Number(input.max_chars || 2000))));
|
|
3721
3902
|
const publicEvents = run.events.filter(event => !['text', 'response', 'final_response'].includes(event.type));
|
|
3722
3903
|
const activities = publicEvents.slice(-maxEvents).map(event => ({
|
|
3723
3904
|
sequence: event.sequence,
|
|
@@ -3725,7 +3906,7 @@ class Agent {
|
|
|
3725
3906
|
timestamp: event.timestamp,
|
|
3726
3907
|
toolName: event.toolName,
|
|
3727
3908
|
status: event.status,
|
|
3728
|
-
content: this.sanitizePublicWorkContent(event.content || ''),
|
|
3909
|
+
content: this.sanitizePublicWorkContent(event.content || '').slice(0, boundedActivityChars),
|
|
3729
3910
|
}));
|
|
3730
3911
|
return JSON.stringify({
|
|
3731
3912
|
ok: true,
|
|
@@ -3736,7 +3917,7 @@ class Agent {
|
|
|
3736
3917
|
status: guide.status,
|
|
3737
3918
|
createdAt: guide.createdAt,
|
|
3738
3919
|
updatedAt: guide.updatedAt,
|
|
3739
|
-
content: this.sanitizePublicWorkContent(guide.content || ''),
|
|
3920
|
+
content: this.sanitizePublicWorkContent(guide.content || '').slice(0, boundedActivityChars),
|
|
3740
3921
|
})),
|
|
3741
3922
|
},
|
|
3742
3923
|
truncatedActivities: Math.max(0, publicEvents.length - activities.length),
|
|
@@ -3783,6 +3964,505 @@ class Agent {
|
|
|
3783
3964
|
this.config.set('context', 'keep_recent_messages', previousKeepLast);
|
|
3784
3965
|
}
|
|
3785
3966
|
}
|
|
3967
|
+
/**
|
|
3968
|
+
* 落盘一个超大工具结果,返回 artifact_id。完整内容不进上下文——上下文只保留
|
|
3969
|
+
* tiny 引用;compress_tool_result 按 id 读取后再压缩。落盘后状态即 done。
|
|
3970
|
+
*/
|
|
3971
|
+
storeToolResultArtifact(tool, content) {
|
|
3972
|
+
const id = crypto.randomUUID();
|
|
3973
|
+
this.toolResultArtifacts.set(id, { tool, content, status: 'done', createdAt: Date.now() });
|
|
3974
|
+
return id;
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* 注册一个后台工具任务,立即返回 background_id(status=running)。真实工具在
|
|
3978
|
+
* 后台执行,完成后由 finishToolResultArtifact 标记 done/error。后台结果持久化
|
|
3979
|
+
* 等待 read_tool_result 读取后再释放。
|
|
3980
|
+
*/
|
|
3981
|
+
beginBackgroundTool(tool) {
|
|
3982
|
+
const id = crypto.randomUUID();
|
|
3983
|
+
this.toolResultArtifacts.set(id, { tool, content: '', status: 'running', createdAt: Date.now() });
|
|
3984
|
+
return id;
|
|
3985
|
+
}
|
|
3986
|
+
/** 标记后台任务完成(写结果)或失败(写错误)。 */
|
|
3987
|
+
finishToolResultArtifact(id, content, error) {
|
|
3988
|
+
const artifact = this.toolResultArtifacts.get(id);
|
|
3989
|
+
if (!artifact)
|
|
3990
|
+
return;
|
|
3991
|
+
if (error) {
|
|
3992
|
+
artifact.status = 'error';
|
|
3993
|
+
artifact.error = error;
|
|
3994
|
+
}
|
|
3995
|
+
else {
|
|
3996
|
+
artifact.status = 'done';
|
|
3997
|
+
artifact.content = content;
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* 按 artifact_id 读取工具结果引用(compress_tool_result / read_tool_result 共用)。
|
|
4002
|
+
*/
|
|
4003
|
+
readToolResultArtifact(id) {
|
|
4004
|
+
return this.toolResultArtifacts.get(id) ?? null;
|
|
4005
|
+
}
|
|
4006
|
+
/**
|
|
4007
|
+
* 压缩一个极大的工具调用结果(保留格式),供 Agent 主动选用以替代硬截断。
|
|
4008
|
+
*
|
|
4009
|
+
* 入参为 artifact_id(而非完整 content),故压缩前的大内容不进入上下文。
|
|
4010
|
+
* 缓存命中隔离:压缩 LLM 调用使用独立 system + 单条 user 消息,与主对话
|
|
4011
|
+
* system/历史前缀不相交,不污染缓存命中。
|
|
4012
|
+
*/
|
|
4013
|
+
async handleCompressToolResult(args, signal) {
|
|
4014
|
+
let input = {};
|
|
4015
|
+
try {
|
|
4016
|
+
input = JSON.parse(args || '{}');
|
|
4017
|
+
}
|
|
4018
|
+
catch { }
|
|
4019
|
+
// 压缩前内容不入上下文:优先按 artifact_id 引用读取落盘内容;兼容同时传入
|
|
4020
|
+
// 小段 content 的场景(此时 content 较短,直接压缩即可)。
|
|
4021
|
+
const artifactId = String(input.artifact_id || '').trim();
|
|
4022
|
+
const inlineContent = typeof input.content === 'string' ? input.content : String(input.content ?? '');
|
|
4023
|
+
let content = '';
|
|
4024
|
+
let source = 'inline';
|
|
4025
|
+
if (artifactId) {
|
|
4026
|
+
const artifact = this.readToolResultArtifact(artifactId);
|
|
4027
|
+
if (!artifact)
|
|
4028
|
+
return { ok: false, output: '[compress_tool_result] Unknown or expired artifact_id.', error: 'Unknown artifact_id.' };
|
|
4029
|
+
if (artifact.status === 'running')
|
|
4030
|
+
return { ok: false, output: '[compress_tool_result] Tool result is still running in the background; read_tool_result first.', error: 'still-running.' };
|
|
4031
|
+
if (artifact.status === 'error')
|
|
4032
|
+
return { ok: false, output: '[compress_tool_result] Backgronud tool failed: ' + String(artifact.error || 'unknown error'), error: 'background-error.' };
|
|
4033
|
+
content = artifact.content;
|
|
4034
|
+
source = 'artifact';
|
|
4035
|
+
}
|
|
4036
|
+
else if (inlineContent.trim()) {
|
|
4037
|
+
content = inlineContent;
|
|
4038
|
+
}
|
|
4039
|
+
else {
|
|
4040
|
+
return { ok: false, output: '[compress_tool_result] artifact_id (or content) is required.', error: 'artifact_id is required.' };
|
|
4041
|
+
}
|
|
4042
|
+
const formatHint = String(input.format_hint || '').trim();
|
|
4043
|
+
const provider = this.engineModel();
|
|
4044
|
+
const modelName = this.activeModelName();
|
|
4045
|
+
if (!provider || !modelName) {
|
|
4046
|
+
// 无 provider 时回退为本地有界截断(保留头尾,诚实标注省略)。
|
|
4047
|
+
return {
|
|
4048
|
+
ok: true,
|
|
4049
|
+
output: JSON.stringify({
|
|
4050
|
+
ok: true,
|
|
4051
|
+
compressed: true,
|
|
4052
|
+
method: 'local-fallback',
|
|
4053
|
+
summary: this.pruneToolResultContent(content),
|
|
4054
|
+
originalChars: content.length,
|
|
4055
|
+
}, null, 2),
|
|
4056
|
+
metadata: { kind: 'compress-tool-result' },
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
4059
|
+
try {
|
|
4060
|
+
// 独立 system + 单条 user 消息:不共享主对话前缀缓存。
|
|
4061
|
+
const system = [
|
|
4062
|
+
'You are a tool-result compression engine.',
|
|
4063
|
+
'Compress ONE oversized tool result into a concise, format-preserving summary.',
|
|
4064
|
+
'Preserve the exact structure the original result carries: keep JSON objects/arrays valid, keep table columns/rows, keep code blocks, keep file paths, identifiers, numbers, error strings, and key-value pairs verbatim.',
|
|
4065
|
+
'Do not drop error messages, command outputs that matter for correctness, or any identifier the agent may need to continue.',
|
|
4066
|
+
'Return ONLY the compressed result, with no preamble, no Markdown fences, no "here is" phrasing.',
|
|
4067
|
+
].join('\n');
|
|
4068
|
+
const formatSuffix = formatHint ? `\n\nFormat to preserve: ${formatHint}` : '';
|
|
4069
|
+
const prompt = [
|
|
4070
|
+
'Original tool result (do not shorten meaningful structure; remove only redundant/boilerplate whitespace and trivially repeated noise):',
|
|
4071
|
+
'',
|
|
4072
|
+
content,
|
|
4073
|
+
formatSuffix,
|
|
4074
|
+
].join('\n');
|
|
4075
|
+
const maxTokens = Math.max(1024, Math.min(8192, Math.ceil(content.length / 4)) + 512);
|
|
4076
|
+
const { temperature } = provider.intelligenceConfig('low');
|
|
4077
|
+
const generated = await this.withTimeout(provider.chat(modelName, [{ role: 'user', content: prompt }], system, temperature, maxTokens, signal), 120000);
|
|
4078
|
+
const summary = String(generated || '').trim();
|
|
4079
|
+
if (!summary || /^\[LLM Error(?::|\])/i.test(summary)) {
|
|
4080
|
+
return {
|
|
4081
|
+
ok: true,
|
|
4082
|
+
output: JSON.stringify({ ok: true, compressed: true, method: 'local-fallback', summary: this.pruneToolResultContent(content), originalChars: content.length }, null, 2),
|
|
4083
|
+
metadata: { kind: 'compress-tool-result' },
|
|
4084
|
+
};
|
|
4085
|
+
}
|
|
4086
|
+
return {
|
|
4087
|
+
ok: true,
|
|
4088
|
+
output: JSON.stringify({
|
|
4089
|
+
ok: true,
|
|
4090
|
+
compressed: true,
|
|
4091
|
+
method: 'model-summary',
|
|
4092
|
+
model: modelName,
|
|
4093
|
+
summary,
|
|
4094
|
+
originalChars: content.length,
|
|
4095
|
+
compressedChars: summary.length,
|
|
4096
|
+
}, null, 2),
|
|
4097
|
+
metadata: { kind: 'compress-tool-result' },
|
|
4098
|
+
};
|
|
4099
|
+
}
|
|
4100
|
+
catch {
|
|
4101
|
+
return {
|
|
4102
|
+
ok: true,
|
|
4103
|
+
output: JSON.stringify({ ok: true, compressed: true, method: 'local-fallback', summary: this.pruneToolResultContent(content), originalChars: content.length }, null, 2),
|
|
4104
|
+
metadata: { kind: 'compress-tool-result' },
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* 工具后台化:把一个工具调用派发到后台运行,立即返回 background_id,不阻塞
|
|
4110
|
+
* 对话回合。真实工具在后台执行,完成后持久化到 toolResultArtifacts,
|
|
4111
|
+
* read_tool_result 按 background_id 读取后再释放。
|
|
4112
|
+
*
|
|
4113
|
+
* 缓存命中优化:后台化工具只返回 tiny 的 background_id(不进大结果到上下文),
|
|
4114
|
+
* 真实结果按需读取,避免大结果撑爆上下文、破坏前缀缓存。
|
|
4115
|
+
*/
|
|
4116
|
+
async handleBackgroundTool(args, signal) {
|
|
4117
|
+
let input = {};
|
|
4118
|
+
try {
|
|
4119
|
+
input = JSON.parse(args || '{}');
|
|
4120
|
+
}
|
|
4121
|
+
catch { }
|
|
4122
|
+
const tool = String(input.tool || '').trim();
|
|
4123
|
+
if (!tool)
|
|
4124
|
+
return { ok: false, output: '[background_tool] tool is required.', error: 'tool is required.' };
|
|
4125
|
+
if (tool === 'background_tool' || tool === 'read_tool_result' || tool === 'compress_tool_result' || tool === 'goal_manage' || tool === 'conversation_rename') {
|
|
4126
|
+
return { ok: false, output: '[background_tool] cannot background the control tools (background_tool/read_tool_result/compress_tool_result/goal_manage/conversation_rename).', error: 'control-tool-unsupported.' };
|
|
4127
|
+
}
|
|
4128
|
+
// 禁止后台化子代理/编排类工具——它们有独立的生命周期管理,后台化会破坏其
|
|
4129
|
+
// mailbox/abort/persistence 语义。
|
|
4130
|
+
if (/^(task|subagent_|flow_|context_|question|skill)/.test(tool)) {
|
|
4131
|
+
return { ok: false, output: '[background_tool] orchestration/flow tools cannot be backgrounded.', error: 'orchestration-unsupported.' };
|
|
4132
|
+
}
|
|
4133
|
+
const toolArgs = input.args;
|
|
4134
|
+
const argStr = typeof toolArgs === 'string' ? toolArgs : (toolArgs === undefined ? '{}' : JSON.stringify(toolArgs));
|
|
4135
|
+
const backgroundId = this.beginBackgroundTool(tool);
|
|
4136
|
+
const wsDir = this.workspace.current?.path || this.rootPath;
|
|
4137
|
+
// 后台执行:不 await,完成/失败后回写 artifact。
|
|
4138
|
+
void this.tools.execute(tool, argStr, wsDir, {
|
|
4139
|
+
mode: this.mode,
|
|
4140
|
+
workspacePath: wsDir,
|
|
4141
|
+
conversationId: this.activeConversationId || 'default',
|
|
4142
|
+
actorId: this.runtimeActorId,
|
|
4143
|
+
workspaceId: this.workspace.current?.id || '',
|
|
4144
|
+
backend: process.env.NEWMARK_WSL_DISTRO ? 'wsl' : (process.platform === 'win32' ? 'windows' : process.platform),
|
|
4145
|
+
signal,
|
|
4146
|
+
}).then((content) => {
|
|
4147
|
+
this.finishToolResultArtifact(backgroundId, content);
|
|
4148
|
+
}).catch((error) => {
|
|
4149
|
+
this.finishToolResultArtifact(backgroundId, '', error instanceof Error ? error.message : String(error));
|
|
4150
|
+
});
|
|
4151
|
+
return {
|
|
4152
|
+
ok: true,
|
|
4153
|
+
output: JSON.stringify({ ok: true, background_id: backgroundId, tool, status: 'running', createdAt: new Date().toISOString() }, null, 2),
|
|
4154
|
+
metadata: { kind: 'background-tool' },
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* 读取后台工具结果:done 时返回结果(按需释放),running 时返回状态,error
|
|
4159
|
+
* 时返回错误。与 compress_tool_result 共享 toolResultArtifacts。
|
|
4160
|
+
*/
|
|
4161
|
+
handleReadToolResult(args) {
|
|
4162
|
+
let input = {};
|
|
4163
|
+
try {
|
|
4164
|
+
input = JSON.parse(args || '{}');
|
|
4165
|
+
}
|
|
4166
|
+
catch { }
|
|
4167
|
+
const id = String(input.background_id || input.artifact_id || '').trim();
|
|
4168
|
+
if (!id)
|
|
4169
|
+
return { ok: false, output: '[read_tool_result] background_id is required.', error: 'background_id is required.' };
|
|
4170
|
+
const artifact = this.readToolResultArtifact(id);
|
|
4171
|
+
if (!artifact)
|
|
4172
|
+
return { ok: false, output: '[read_tool_result] Unknown or already-released background_id.', error: 'unknown-background-id.' };
|
|
4173
|
+
const release = Boolean(input.release);
|
|
4174
|
+
const result = {
|
|
4175
|
+
ok: true,
|
|
4176
|
+
background_id: id,
|
|
4177
|
+
tool: artifact.tool,
|
|
4178
|
+
status: artifact.status,
|
|
4179
|
+
createdAt: artifact.createdAt ? new Date(artifact.createdAt).toISOString() : '',
|
|
4180
|
+
};
|
|
4181
|
+
if (artifact.status === 'running') {
|
|
4182
|
+
result.running = true;
|
|
4183
|
+
}
|
|
4184
|
+
else if (artifact.status === 'error') {
|
|
4185
|
+
result.error = artifact.error || 'background tool failed';
|
|
4186
|
+
}
|
|
4187
|
+
else {
|
|
4188
|
+
result.content = artifact.content;
|
|
4189
|
+
// 释放:读取后从持久化删除,避免重复读取占用内存。
|
|
4190
|
+
if (release)
|
|
4191
|
+
this.toolResultArtifacts.delete(id);
|
|
4192
|
+
}
|
|
4193
|
+
return { ok: true, output: JSON.stringify(result, null, 2), metadata: { kind: 'read-tool-result' } };
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Agent 主动管理 Goal 状态:进入 / 编辑 objective / 标记完成 / 退出。
|
|
4197
|
+
* 兼容原有 Goal 机制:enter/update 复用 updateGoal(记录 change、mode=goal、
|
|
4198
|
+
* 尊重已暂停状态),complete 复用 markGoalComplete(verified + clearGoal),
|
|
4199
|
+
* exit 复用 clearGoal(回 build 不声称完成)。不破坏「用户 Stop 暂停」边界:
|
|
4200
|
+
* 本工具不提供 pause/resume,避免 Agent 绕过用户的显式暂停。
|
|
4201
|
+
*/
|
|
4202
|
+
handleGoalManage(args) {
|
|
4203
|
+
let input = {};
|
|
4204
|
+
try {
|
|
4205
|
+
input = JSON.parse(args || '{}');
|
|
4206
|
+
}
|
|
4207
|
+
catch { }
|
|
4208
|
+
const action = String(input.action || '').trim();
|
|
4209
|
+
const objective = String(input.objective || '').replace(/\s+/g, ' ').trim();
|
|
4210
|
+
const reason = String(input.reason || '').trim();
|
|
4211
|
+
const hadGoal = !!this.goal;
|
|
4212
|
+
const priorObjective = this.goal?.objective || '';
|
|
4213
|
+
if (!['enter', 'update', 'complete', 'exit'].includes(action)) {
|
|
4214
|
+
return { ok: false, output: '[goal_manage] action is required (enter|update|complete|exit).', error: 'action is required.' };
|
|
4215
|
+
}
|
|
4216
|
+
if ((action === 'enter' || action === 'update') && !objective) {
|
|
4217
|
+
return { ok: false, output: '[goal_manage] objective is required for enter/update.', error: 'objective is required.' };
|
|
4218
|
+
}
|
|
4219
|
+
if (action === 'enter' || action === 'update') {
|
|
4220
|
+
this.updateGoal(objective);
|
|
4221
|
+
const entered = !hadGoal && action === 'enter';
|
|
4222
|
+
return {
|
|
4223
|
+
ok: true,
|
|
4224
|
+
output: JSON.stringify({
|
|
4225
|
+
ok: true,
|
|
4226
|
+
action,
|
|
4227
|
+
enteredGoal: entered,
|
|
4228
|
+
objective: this.goal?.objective || objective,
|
|
4229
|
+
mode: this.mode,
|
|
4230
|
+
paused: this.goal?.paused || false,
|
|
4231
|
+
goalRounds: this.goal?.goalRounds || 0,
|
|
4232
|
+
...(reason ? { reason } : {}),
|
|
4233
|
+
}, null, 2),
|
|
4234
|
+
metadata: { kind: 'goal-manage' },
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
if (action === 'complete') {
|
|
4238
|
+
if (!this.goal)
|
|
4239
|
+
return { ok: true, output: JSON.stringify({ ok: true, action, completed: false, note: 'No active Goal to complete.' }, null, 2), metadata: { kind: 'goal-manage' } };
|
|
4240
|
+
this.markGoalComplete();
|
|
4241
|
+
return {
|
|
4242
|
+
ok: true,
|
|
4243
|
+
output: JSON.stringify({ ok: true, action, completed: true, priorObjective, mode: this.mode, goal: null }, null, 2),
|
|
4244
|
+
metadata: { kind: 'goal-manage' },
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
// action === 'exit'
|
|
4248
|
+
if (!this.goal)
|
|
4249
|
+
return { ok: true, output: JSON.stringify({ ok: true, action, cleared: false, note: 'No active Goal to exit.' }, null, 2), metadata: { kind: 'goal-manage' } };
|
|
4250
|
+
this.clearGoal();
|
|
4251
|
+
return {
|
|
4252
|
+
ok: true,
|
|
4253
|
+
output: JSON.stringify({ ok: true, action, cleared: true, priorObjective, mode: this.mode, goal: null }, null, 2),
|
|
4254
|
+
metadata: { kind: 'goal-manage' },
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Agent 自行命名当前对话。首 Build Block 上运行时通过 bootstrap 提示(见
|
|
4259
|
+
* agentKernelRunner.buildBuildContextBootstrap)请求 Agent 调用一次;这里复用
|
|
4260
|
+
* 已有的 renameConversation 持久化路径。返回简短结果以保缓存友好。
|
|
4261
|
+
*/
|
|
4262
|
+
handleConversationRename(args) {
|
|
4263
|
+
let input = {};
|
|
4264
|
+
try {
|
|
4265
|
+
input = JSON.parse(args || '{}');
|
|
4266
|
+
}
|
|
4267
|
+
catch { }
|
|
4268
|
+
const title = String(input.title || '').replace(/\s+/g, ' ').trim();
|
|
4269
|
+
if (!title)
|
|
4270
|
+
return { ok: false, output: '[conversation_rename] title is required.', error: 'title is required.' };
|
|
4271
|
+
const conversationId = this.activeConversationId || 'default';
|
|
4272
|
+
const ok = this.renameConversation(conversationId, title);
|
|
4273
|
+
if (!ok)
|
|
4274
|
+
return { ok: false, output: '[conversation_rename] could not rename conversation (no state key or empty title).', error: 'rename failed.' };
|
|
4275
|
+
return {
|
|
4276
|
+
ok: true,
|
|
4277
|
+
output: JSON.stringify({ ok: true, conversationId, title: title.slice(0, 80) }, null, 2),
|
|
4278
|
+
metadata: { kind: 'conversation-rename' },
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
conversationTree() {
|
|
4282
|
+
const stateKey = this.workspaceConversationStateKey();
|
|
4283
|
+
const stored = this.readStoredConversationState();
|
|
4284
|
+
const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : undefined;
|
|
4285
|
+
return persisted ? this.normalizeConversationTree(persisted) : null;
|
|
4286
|
+
}
|
|
4287
|
+
currentRuntimeBranchId() {
|
|
4288
|
+
return String(this.conversationTree()?.activeNodeId || '');
|
|
4289
|
+
}
|
|
4290
|
+
handleBranchList(args) {
|
|
4291
|
+
try {
|
|
4292
|
+
const params = JSON.parse(args || '{}');
|
|
4293
|
+
if (!this.branchCommunicationEnabled) {
|
|
4294
|
+
return { ok: false, output: '[branch_list] Branch communication is not enabled for this conversation. Enable "允许分支交流" at conversation creation time.', error: 'branch communication disabled.' };
|
|
4295
|
+
}
|
|
4296
|
+
const tree = this.conversationTree();
|
|
4297
|
+
const nodes = tree?.nodes || {};
|
|
4298
|
+
const activeNodeId = String(tree?.activeNodeId || '');
|
|
4299
|
+
const branches = Object.values(nodes).map(node => {
|
|
4300
|
+
const inbound = this.branchMailbox.filter(m => m.toBranchId === node.id);
|
|
4301
|
+
const outbound = this.branchMailbox.filter(m => m.fromBranchId === node.id);
|
|
4302
|
+
return {
|
|
4303
|
+
id: node.id,
|
|
4304
|
+
parentId: node.parentId,
|
|
4305
|
+
active: node.id === activeNodeId,
|
|
4306
|
+
sourceMessageIndex: node.sourceMessageIndex,
|
|
4307
|
+
sourceText: String(node.sourceText || '').slice(0, 160),
|
|
4308
|
+
chatMessages: node.chatMessages.length,
|
|
4309
|
+
history: node.history.length,
|
|
4310
|
+
workRuns: node.workRuns.length,
|
|
4311
|
+
runningWorkRuns: node.workRuns.filter(run => run.status === 'running').length,
|
|
4312
|
+
mailbox: { inbound: inbound.length, unread: inbound.filter(m => !m.readAt).length, outbound: outbound.length },
|
|
4313
|
+
};
|
|
4314
|
+
});
|
|
4315
|
+
return {
|
|
4316
|
+
ok: true,
|
|
4317
|
+
output: JSON.stringify({ ok: true, conversationId: this.activeConversationId, branchCommunication: true, activeBranchId: activeNodeId, branchCount: branches.length, branches }, null, 2),
|
|
4318
|
+
metadata: { kind: 'branch-list' },
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4321
|
+
catch {
|
|
4322
|
+
return { ok: false, output: '[branch_list] Invalid arguments.', error: 'Invalid arguments.' };
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
handleBranchSend(args) {
|
|
4326
|
+
try {
|
|
4327
|
+
const params = JSON.parse(args || '{}');
|
|
4328
|
+
if (!this.branchCommunicationEnabled)
|
|
4329
|
+
return { ok: false, output: '[branch_send] Branch communication is not enabled for this conversation.', error: 'branch communication disabled.' };
|
|
4330
|
+
const toBranchId = String(params.to_branch || params.toBranchId || params.branch || '').trim();
|
|
4331
|
+
const body = String(params.message || params.body || '').trim();
|
|
4332
|
+
const kind = String(params.kind || 'message').trim();
|
|
4333
|
+
if (!toBranchId)
|
|
4334
|
+
return { ok: false, output: '[branch_send] to_branch is required.', error: 'to_branch is required.' };
|
|
4335
|
+
if (!body)
|
|
4336
|
+
return { ok: false, output: '[branch_send] message is required.', error: 'message is required.' };
|
|
4337
|
+
const tree = this.conversationTree();
|
|
4338
|
+
const target = tree?.nodes[toBranchId];
|
|
4339
|
+
if (!target)
|
|
4340
|
+
return { ok: false, output: '[branch_send] Branch not found: ' + toBranchId, error: 'Branch not found: ' + toBranchId };
|
|
4341
|
+
const fromBranchId = this.currentRuntimeBranchId();
|
|
4342
|
+
if (!fromBranchId)
|
|
4343
|
+
return { ok: false, output: '[branch_send] Could not determine the current runtime branch.', error: 'runtime branch unknown.' };
|
|
4344
|
+
if (fromBranchId === toBranchId)
|
|
4345
|
+
return { ok: false, output: '[branch_send] A branch cannot message itself.', error: 'self-message forbidden.' };
|
|
4346
|
+
const message = {
|
|
4347
|
+
id: crypto.randomUUID(),
|
|
4348
|
+
conversationId: this.activeConversationId || 'default',
|
|
4349
|
+
sequence: this.nextBranchMessageSequence++,
|
|
4350
|
+
fromBranchId,
|
|
4351
|
+
toBranchId,
|
|
4352
|
+
kind: kind === 'directive' ? 'directive' : kind === 'result' ? 'result' : 'message',
|
|
4353
|
+
body: body.slice(0, 32000),
|
|
4354
|
+
correlationId: params.correlation_id ? String(params.correlation_id) : undefined,
|
|
4355
|
+
replyTo: params.reply_to ? String(params.reply_to) : undefined,
|
|
4356
|
+
createdAt: new Date().toISOString(),
|
|
4357
|
+
};
|
|
4358
|
+
this.branchMailbox.push(message);
|
|
4359
|
+
this.saveWorkspaceConversationState(true);
|
|
4360
|
+
return {
|
|
4361
|
+
ok: true,
|
|
4362
|
+
output: JSON.stringify({ ok: true, message: { id: message.id, fromBranchId, toBranchId, kind: message.kind, sequence: message.sequence } }, null, 2),
|
|
4363
|
+
metadata: { kind: 'branch-send' },
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
catch {
|
|
4367
|
+
return { ok: false, output: '[branch_send] Invalid arguments.', error: 'Invalid arguments.' };
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
handleBranchRead(args) {
|
|
4371
|
+
try {
|
|
4372
|
+
const params = JSON.parse(args || '{}');
|
|
4373
|
+
if (!this.branchCommunicationEnabled)
|
|
4374
|
+
return { ok: false, output: '[branch_read] Branch communication is not enabled for this conversation.', error: 'branch communication disabled.' };
|
|
4375
|
+
const branchId = String(params.branch || params.branch_id || params.id || '').trim();
|
|
4376
|
+
if (!branchId)
|
|
4377
|
+
return { ok: false, output: '[branch_read] branch is required.', error: 'branch is required.' };
|
|
4378
|
+
const tree = this.conversationTree();
|
|
4379
|
+
const node = tree?.nodes[branchId];
|
|
4380
|
+
if (!node)
|
|
4381
|
+
return { ok: false, output: '[branch_read] Branch not found: ' + branchId, error: 'Branch not found: ' + branchId };
|
|
4382
|
+
const fromBranchId = this.currentRuntimeBranchId();
|
|
4383
|
+
const maxChars = Math.max(100, Math.min(16000, Math.floor(Number(params.max_chars || 8000))));
|
|
4384
|
+
const inbound = this.branchMailbox
|
|
4385
|
+
.filter(m => m.toBranchId === fromBranchId && m.fromBranchId === branchId)
|
|
4386
|
+
.sort((a, b) => a.sequence - b.sequence)
|
|
4387
|
+
.map(m => ({ id: m.id, sequence: m.sequence, kind: m.kind, body: m.body, createdAt: m.createdAt, read: !!m.readAt }));
|
|
4388
|
+
for (const m of inbound) {
|
|
4389
|
+
const stored = this.branchMailbox.find(x => x.id === m.id);
|
|
4390
|
+
if (stored && !stored.readAt)
|
|
4391
|
+
stored.readAt = new Date().toISOString();
|
|
4392
|
+
}
|
|
4393
|
+
if (inbound.length)
|
|
4394
|
+
this.saveWorkspaceConversationState(true);
|
|
4395
|
+
const activity = node.workRuns.slice(-10).map(run => {
|
|
4396
|
+
const finalEvent = [...run.events].reverse().find(event => event.type === 'final_response');
|
|
4397
|
+
return {
|
|
4398
|
+
runId: run.runId,
|
|
4399
|
+
status: run.status,
|
|
4400
|
+
startedAt: run.startedAt,
|
|
4401
|
+
endedAt: run.endedAt,
|
|
4402
|
+
finalResult: finalEvent ? String(finalEvent.content || '').slice(0, maxChars) : '',
|
|
4403
|
+
recentEvents: run.events.slice(-6).map(event => '[' + event.type + '] ' + String(event.content || '').slice(0, 240)),
|
|
4404
|
+
};
|
|
4405
|
+
});
|
|
4406
|
+
return {
|
|
4407
|
+
ok: true,
|
|
4408
|
+
output: JSON.stringify({
|
|
4409
|
+
ok: true,
|
|
4410
|
+
branch: {
|
|
4411
|
+
id: node.id,
|
|
4412
|
+
parentId: node.parentId,
|
|
4413
|
+
sourceMessageIndex: node.sourceMessageIndex,
|
|
4414
|
+
sourceText: String(node.sourceText || '').slice(0, 240),
|
|
4415
|
+
chatMessages: node.chatMessages.length,
|
|
4416
|
+
history: node.history.length,
|
|
4417
|
+
},
|
|
4418
|
+
inbound,
|
|
4419
|
+
activity,
|
|
4420
|
+
}, null, 2),
|
|
4421
|
+
metadata: { kind: 'branch-read' },
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4424
|
+
catch {
|
|
4425
|
+
return { ok: false, output: '[branch_read] Invalid arguments.', error: 'Invalid arguments.' };
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
handleBranchCreate(args) {
|
|
4429
|
+
try {
|
|
4430
|
+
const params = JSON.parse(args || '{}');
|
|
4431
|
+
if (!this.branchCommunicationEnabled)
|
|
4432
|
+
return { ok: false, output: '[branch_create] Branch communication is not enabled for this conversation. Enable "允许分支交流" at conversation creation time.', error: 'branch communication disabled.' };
|
|
4433
|
+
const messageIndex = Math.floor(Number(params.message_index ?? params.messageIndex ?? params.index));
|
|
4434
|
+
const prompt = String(params.prompt || params.message || params.text || '').trim();
|
|
4435
|
+
if (!Number.isFinite(messageIndex) || messageIndex < 0)
|
|
4436
|
+
return { ok: false, output: '[branch_create] message_index is required (0-based index of a user message in the conversation history, i.e. the historical block position).', error: 'message_index is required.' };
|
|
4437
|
+
if (!prompt)
|
|
4438
|
+
return { ok: false, output: '[branch_create] prompt is required (the new branch initial instruction).', error: 'prompt is required.' };
|
|
4439
|
+
const locator = {};
|
|
4440
|
+
if (params.message_id)
|
|
4441
|
+
locator.messageId = String(params.message_id);
|
|
4442
|
+
if (params.guide_id)
|
|
4443
|
+
locator.guideId = String(params.guide_id);
|
|
4444
|
+
if (params.client_message_id)
|
|
4445
|
+
locator.clientMessageId = String(params.client_message_id);
|
|
4446
|
+
if (params.run_id)
|
|
4447
|
+
locator.runId = String(params.run_id);
|
|
4448
|
+
const snapshot = this.branchConversation(this.activeConversationId || 'default', messageIndex, prompt, locator);
|
|
4449
|
+
return {
|
|
4450
|
+
ok: true,
|
|
4451
|
+
output: JSON.stringify({
|
|
4452
|
+
ok: true,
|
|
4453
|
+
branchId: snapshot.activeBranchId,
|
|
4454
|
+
runtimeBranchId: snapshot.runtimeBranchId,
|
|
4455
|
+
messageIndex,
|
|
4456
|
+
prompt: prompt.slice(0, 240),
|
|
4457
|
+
branches: snapshot.branches,
|
|
4458
|
+
}, null, 2),
|
|
4459
|
+
metadata: { kind: 'branch-create' },
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
catch (e) {
|
|
4463
|
+
return { ok: false, output: '[branch_create] ' + (e instanceof Error ? e.message : String(e)), error: e instanceof Error ? e.message : String(e) };
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
3786
4466
|
handleContextHistoryManage(args) {
|
|
3787
4467
|
let input = {};
|
|
3788
4468
|
try {
|
|
@@ -3827,16 +4507,23 @@ class Agent {
|
|
|
3827
4507
|
error: 'remove position is in the protected context zone.',
|
|
3828
4508
|
};
|
|
3829
4509
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
4510
|
+
// 缓存优化:卸载只针对长期历史,且不立即 splice——当前 Build Block 内
|
|
4511
|
+
// 保持 history 不变以复用 provider 前缀缓存,Block 结束后才对后续 Block 生效。
|
|
4512
|
+
const target = this.history[position];
|
|
4513
|
+
const fingerprint = this.historyRecordFingerprint(target);
|
|
4514
|
+
if (!this.pendingHistoryRemovals.some(item => item.fingerprint === fingerprint && item.position === position)) {
|
|
4515
|
+
this.pendingHistoryRemovals.push({ position, fingerprint });
|
|
4516
|
+
}
|
|
3832
4517
|
return {
|
|
3833
4518
|
ok: true,
|
|
3834
4519
|
output: JSON.stringify({
|
|
3835
4520
|
ok: true,
|
|
3836
4521
|
action: 'remove',
|
|
3837
4522
|
removedPosition: position,
|
|
3838
|
-
removedRole: String(
|
|
4523
|
+
removedRole: String(target?.role || ''),
|
|
4524
|
+
deferred: true,
|
|
3839
4525
|
remaining: this.history.length,
|
|
4526
|
+
effectiveAt: 'after the current Build Block ends; applies to subsequent Blocks only',
|
|
3840
4527
|
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3841
4528
|
}, null, 2),
|
|
3842
4529
|
metadata: { kind: 'context-history-remove' },
|
|
@@ -4035,9 +4722,18 @@ class Agent {
|
|
|
4035
4722
|
maxTokens,
|
|
4036
4723
|
triggerTokens: budget.triggerTokens,
|
|
4037
4724
|
targetTokens: budget.targetTokens,
|
|
4725
|
+
buildBlockTokens: budget.buildBlockTokens,
|
|
4726
|
+
longHistoryTokens: budget.longHistoryTokens,
|
|
4727
|
+
buildBlockTriggerTokens: budget.buildBlockTriggerTokens,
|
|
4728
|
+
longHistoryTriggerTokens: budget.longHistoryTriggerTokens,
|
|
4729
|
+
buildBlockRetentionTokens: budget.buildBlockRetentionTokens,
|
|
4730
|
+
longHistoryRetentionTokens: budget.longHistoryRetentionTokens,
|
|
4731
|
+
buildBlockUsagePercent: maxTokens > 0 ? Math.round((budget.buildBlockTokens / maxTokens) * 1000) / 10 : 0,
|
|
4732
|
+
longHistoryUsagePercent: maxTokens > 0 ? Math.round((budget.longHistoryTokens / maxTokens) * 1000) / 10 : 0,
|
|
4038
4733
|
summaryTokens: budget.summaryTokens,
|
|
4039
4734
|
usagePercent: maxTokens > 0 ? Math.round((estimatedTokens / maxTokens) * 1000) / 10 : 0,
|
|
4040
|
-
thresholdReached: budget.
|
|
4735
|
+
thresholdReached: budget.buildBlockTokens >= budget.buildBlockTriggerTokens
|
|
4736
|
+
|| budget.longHistoryTokens >= budget.longHistoryTriggerTokens,
|
|
4041
4737
|
keepRecentMessages: this.config.getNum('context', 'keep_recent_messages') || 10,
|
|
4042
4738
|
lastCompression: this.lastCompression ? {
|
|
4043
4739
|
at: this.lastCompression.at,
|
|
@@ -4066,6 +4762,11 @@ class Agent {
|
|
|
4066
4762
|
lastUserMessageIndex: lastUserIndex,
|
|
4067
4763
|
protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0,
|
|
4068
4764
|
},
|
|
4765
|
+
pendingRemovals: {
|
|
4766
|
+
count: this.pendingHistoryRemovals.length,
|
|
4767
|
+
positions: this.pendingHistoryRemovals.map(item => item.position),
|
|
4768
|
+
effectiveAt: 'after the current Build Block ends; applies to subsequent Blocks only',
|
|
4769
|
+
},
|
|
4069
4770
|
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
4070
4771
|
}, null, 2),
|
|
4071
4772
|
metadata: { kind: 'context-history-status' },
|
|
@@ -4340,10 +5041,21 @@ class Agent {
|
|
|
4340
5041
|
return names.find(n => n.includes(this.model)) || this.model;
|
|
4341
5042
|
}
|
|
4342
5043
|
estimateContextTokens(messages = this.history) {
|
|
5044
|
+
return this.estimateContextTokenComponents(messages, 0).estimatedTokens;
|
|
5045
|
+
}
|
|
5046
|
+
estimateContextTokenComponents(messages, buildBlockStart) {
|
|
4343
5047
|
let asciiChars = 0;
|
|
4344
5048
|
let nonAsciiChars = 0;
|
|
4345
5049
|
let structuralChars = 0;
|
|
4346
|
-
|
|
5050
|
+
let longHistoryAsciiChars = 0;
|
|
5051
|
+
let longHistoryNonAsciiChars = 0;
|
|
5052
|
+
let longHistoryStructuralChars = 0;
|
|
5053
|
+
let buildBlockAsciiChars = 0;
|
|
5054
|
+
let buildBlockNonAsciiChars = 0;
|
|
5055
|
+
let buildBlockStructuralChars = 0;
|
|
5056
|
+
const boundary = Math.max(0, Math.min(messages.length, Math.floor(buildBlockStart)));
|
|
5057
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
5058
|
+
const m = messages[index];
|
|
4347
5059
|
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
|
|
4348
5060
|
const toolCalls = Array.isArray(m.tool_calls) ? JSON.stringify(m.tool_calls) : '';
|
|
4349
5061
|
const text = `${content}${toolCalls}`;
|
|
@@ -4359,14 +5071,31 @@ class Agent {
|
|
|
4359
5071
|
// plain prose (~4 chars/token). Charge structural bytes separately so
|
|
4360
5072
|
// large SubAgent transcripts, tool catalogs, and escaped payloads cannot
|
|
4361
5073
|
// hide behind the prose heuristic and slip past the compression trigger.
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
5074
|
+
const structural = (typeof m.content === 'object' && m.content ? Math.max(0, content.length) : 0)
|
|
5075
|
+
+ (toolCalls ? Math.max(0, toolCalls.length) : 0);
|
|
5076
|
+
structuralChars += structural;
|
|
5077
|
+
if (index < boundary) {
|
|
5078
|
+
longHistoryAsciiChars += Math.max(0, text.length - nonAscii);
|
|
5079
|
+
longHistoryNonAsciiChars += nonAscii;
|
|
5080
|
+
longHistoryStructuralChars += structural;
|
|
5081
|
+
}
|
|
5082
|
+
else {
|
|
5083
|
+
buildBlockAsciiChars += Math.max(0, text.length - nonAscii);
|
|
5084
|
+
buildBlockNonAsciiChars += nonAscii;
|
|
5085
|
+
buildBlockStructuralChars += structural;
|
|
5086
|
+
}
|
|
4366
5087
|
}
|
|
4367
5088
|
// Prose: ~4 ASCII chars/token. Count non-ASCII chars at 1 token each and
|
|
4368
5089
|
// fold structural overhead in on top.
|
|
4369
|
-
|
|
5090
|
+
const estimate = (ascii, nonAscii, structural, emptyIsZero = false) => {
|
|
5091
|
+
const raw = ascii / 4 + nonAscii + structural / 6;
|
|
5092
|
+
return emptyIsZero && raw <= 0 ? 0 : Math.max(1, Math.ceil(raw));
|
|
5093
|
+
};
|
|
5094
|
+
return {
|
|
5095
|
+
estimatedTokens: estimate(asciiChars, nonAsciiChars, structuralChars),
|
|
5096
|
+
longHistoryTokens: estimate(longHistoryAsciiChars, longHistoryNonAsciiChars, longHistoryStructuralChars, true),
|
|
5097
|
+
buildBlockTokens: estimate(buildBlockAsciiChars, buildBlockNonAsciiChars, buildBlockStructuralChars, true),
|
|
5098
|
+
};
|
|
4370
5099
|
}
|
|
4371
5100
|
contextWindow(modelName = this.model) {
|
|
4372
5101
|
const estimatedTokens = this.estimateContextTokens();
|
|
@@ -4378,12 +5107,24 @@ class Agent {
|
|
|
4378
5107
|
const model = this.resolveWindowModel(modelName);
|
|
4379
5108
|
const maxTokens = Math.max(1, Number(model?.max_tokens || 0) || 128000);
|
|
4380
5109
|
const ratio = estimatedTokens / maxTokens;
|
|
5110
|
+
const budget = this.compressionBudget(this.history, modelName);
|
|
4381
5111
|
return {
|
|
4382
5112
|
estimatedTokens,
|
|
4383
5113
|
maxTokens,
|
|
4384
5114
|
ratio,
|
|
4385
5115
|
warning: ratio >= 1 ? 'over_limit' : ratio >= 0.85 ? 'near_limit' : 'ok',
|
|
4386
5116
|
model: modelName,
|
|
5117
|
+
buildBlockTokens: budget.buildBlockTokens,
|
|
5118
|
+
longHistoryTokens: budget.longHistoryTokens,
|
|
5119
|
+
buildBlockTriggerTokens: budget.buildBlockTriggerTokens,
|
|
5120
|
+
longHistoryTriggerTokens: budget.longHistoryTriggerTokens,
|
|
5121
|
+
buildBlockRetentionTokens: budget.buildBlockRetentionTokens,
|
|
5122
|
+
longHistoryRetentionTokens: budget.longHistoryRetentionTokens,
|
|
5123
|
+
thresholdReached: budget.buildBlockTokens >= budget.buildBlockTriggerTokens
|
|
5124
|
+
|| budget.longHistoryTokens >= budget.longHistoryTriggerTokens,
|
|
5125
|
+
compressionEnabled: this.config.getBool('context', 'auto_compress'),
|
|
5126
|
+
cacheEntries: this.compressionCache.length,
|
|
5127
|
+
archiveEntries: this.compressionArchiveEntryCount(),
|
|
4387
5128
|
};
|
|
4388
5129
|
}
|
|
4389
5130
|
resolveWindowModel(modelName) {
|
|
@@ -4395,16 +5136,37 @@ class Agent {
|
|
|
4395
5136
|
const model = this.resolveWindowModel(modelName);
|
|
4396
5137
|
return Math.max(1, Number(model?.max_tokens || 0) || 128000);
|
|
4397
5138
|
}
|
|
4398
|
-
compressionBudget(messages) {
|
|
4399
|
-
const maxTokens = this.contextMaxTokens();
|
|
5139
|
+
compressionBudget(messages, modelName = this.model) {
|
|
5140
|
+
const maxTokens = this.contextMaxTokens(modelName);
|
|
5141
|
+
const buildBlockStart = this.compressionBuildBlockStart(messages);
|
|
5142
|
+
const estimates = this.estimateContextTokenComponents(messages, buildBlockStart);
|
|
5143
|
+
const buildBlockTriggerTokens = Math.max(128, Math.floor(maxTokens * 0.70));
|
|
5144
|
+
const longHistoryTriggerTokens = Math.max(128, Math.floor(maxTokens * 0.20));
|
|
5145
|
+
const longHistoryRetentionTokens = longHistoryTriggerTokens;
|
|
4400
5146
|
return {
|
|
4401
|
-
estimatedTokens:
|
|
5147
|
+
estimatedTokens: estimates.estimatedTokens,
|
|
4402
5148
|
maxTokens,
|
|
4403
|
-
|
|
4404
|
-
|
|
5149
|
+
// Keep the legacy names for status consumers and older integrations:
|
|
5150
|
+
// triggerTokens is the active Build-block threshold and targetTokens is
|
|
5151
|
+
// the long-history summary budget.
|
|
5152
|
+
triggerTokens: buildBlockTriggerTokens,
|
|
5153
|
+
targetTokens: longHistoryRetentionTokens,
|
|
4405
5154
|
summaryTokens: Math.max(96, Math.min(1600, Math.floor(maxTokens * 0.12))),
|
|
5155
|
+
buildBlockTokens: estimates.buildBlockTokens,
|
|
5156
|
+
longHistoryTokens: estimates.longHistoryTokens,
|
|
5157
|
+
buildBlockTriggerTokens,
|
|
5158
|
+
longHistoryTriggerTokens,
|
|
5159
|
+
buildBlockRetentionTokens: buildBlockTriggerTokens,
|
|
5160
|
+
longHistoryRetentionTokens,
|
|
4406
5161
|
};
|
|
4407
5162
|
}
|
|
5163
|
+
compressionBuildBlockStart(messages) {
|
|
5164
|
+
const activeRunId = this.currentWorkRunId();
|
|
5165
|
+
if (!activeRunId)
|
|
5166
|
+
return 0;
|
|
5167
|
+
const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
|
|
5168
|
+
return index >= 0 ? index : 0;
|
|
5169
|
+
}
|
|
4408
5170
|
recentContextSuffix(messages, maxMessages, tokenBudget) {
|
|
4409
5171
|
if (!messages.length)
|
|
4410
5172
|
return [];
|
|
@@ -4663,29 +5425,52 @@ class Agent {
|
|
|
4663
5425
|
this.saveWorkspaceConversationState(true);
|
|
4664
5426
|
return { text, hiddenUserInput: true, goalContinuation: true };
|
|
4665
5427
|
}
|
|
4666
|
-
|
|
5428
|
+
buildSessionArchive(messages, mode, model, archiveDir) {
|
|
4667
5429
|
const stamp = new Date().toISOString().replace(/[:.]/g, '').replace('T', '_').replace('Z', '');
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
const
|
|
4672
|
-
let
|
|
4673
|
-
|
|
4674
|
-
|
|
5430
|
+
// Millisecond-only names collide when a user clicks several archive
|
|
5431
|
+
// buttons in one event-loop turn. Keep the readable timestamp and add a
|
|
5432
|
+
// cryptographic suffix so every request owns an independent file.
|
|
5433
|
+
const filename = `session_${stamp}_${crypto.randomUUID().slice(0, 8)}.md`;
|
|
5434
|
+
let markdown = `# Newmark Session — ${stamp}\n\n`;
|
|
5435
|
+
markdown += `**Mode**: ${mode}\n**Model**: ${model}\n`;
|
|
5436
|
+
markdown += `**Messages**: ${messages.length}\n\n---\n\n`;
|
|
4675
5437
|
if (this.goal)
|
|
4676
|
-
|
|
5438
|
+
markdown += `**Goal**: ${this.goal.objective}\n\n`;
|
|
4677
5439
|
for (const msg of messages) {
|
|
4678
|
-
|
|
5440
|
+
markdown += `**[${msg.role}] ${msg.timestamp}**\n\n${msg.content}\n\n`;
|
|
4679
5441
|
for (const attachment of (0, conversationAttachments_1.hydrateConversationImageAttachments)(this.rootPath, msg.attachments)) {
|
|
4680
5442
|
const archived = (0, conversationAttachments_1.archiveConversationImageAttachment)(this.rootPath, archiveDir, attachment);
|
|
4681
5443
|
if (!archived)
|
|
4682
5444
|
continue;
|
|
4683
5445
|
const alt = archived.name.replace(/[\]\r\n]/g, ' ').trim() || 'Submitted image';
|
|
4684
|
-
|
|
5446
|
+
markdown += `\n\n`;
|
|
4685
5447
|
}
|
|
4686
5448
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
5449
|
+
return { filename, markdown };
|
|
5450
|
+
}
|
|
5451
|
+
writeSessionArchive(messages, mode, model) {
|
|
5452
|
+
const archiveDir = this.archiveDir();
|
|
5453
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
5454
|
+
const archive = this.buildSessionArchive(messages, mode, model, archiveDir);
|
|
5455
|
+
fs.writeFileSync(path.join(archiveDir, archive.filename), archive.markdown, 'utf-8');
|
|
5456
|
+
return archive.filename;
|
|
5457
|
+
}
|
|
5458
|
+
async writeSessionArchiveAsync(messages, mode, model, archiveDir = this.archiveDir()) {
|
|
5459
|
+
const archive = this.buildSessionArchive(messages, mode, model, archiveDir);
|
|
5460
|
+
await fs.promises.mkdir(archiveDir, { recursive: true });
|
|
5461
|
+
const outPath = path.join(archiveDir, archive.filename);
|
|
5462
|
+
const tempPath = `${outPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
5463
|
+
try {
|
|
5464
|
+
await fs.promises.writeFile(tempPath, archive.markdown, 'utf-8');
|
|
5465
|
+
await fs.promises.rename(tempPath, outPath);
|
|
5466
|
+
}
|
|
5467
|
+
finally {
|
|
5468
|
+
try {
|
|
5469
|
+
await fs.promises.unlink(tempPath);
|
|
5470
|
+
}
|
|
5471
|
+
catch { }
|
|
5472
|
+
}
|
|
5473
|
+
return archive.filename;
|
|
4689
5474
|
}
|
|
4690
5475
|
archiveSession() {
|
|
4691
5476
|
return this.writeSessionArchive(this.chatMessages, this.modeName(), this.model);
|
|
@@ -4759,6 +5544,111 @@ class Agent {
|
|
|
4759
5544
|
}
|
|
4760
5545
|
return filename;
|
|
4761
5546
|
}
|
|
5547
|
+
/**
|
|
5548
|
+
* Non-blocking archive writer used by the desktop IPC path. The conversation
|
|
5549
|
+
* state merge remains synchronous and lock-protected, but the potentially
|
|
5550
|
+
* large markdown payload and manifest use promise-based filesystem I/O so
|
|
5551
|
+
* independent workspaces can archive in parallel without freezing Electron.
|
|
5552
|
+
*/
|
|
5553
|
+
async archiveConversationAsync(conversationId) {
|
|
5554
|
+
// Archive payloads intentionally start in parallel. The final state
|
|
5555
|
+
// mutation below operates on the latest locked disk snapshot, so no
|
|
5556
|
+
// JavaScript queue is needed for independent targets in one workspace.
|
|
5557
|
+
return await this.archiveConversationAsyncUnlocked(conversationId);
|
|
5558
|
+
}
|
|
5559
|
+
async archiveConversationAsyncUnlocked(conversationId) {
|
|
5560
|
+
const ws = this.workspace.current;
|
|
5561
|
+
if (!ws)
|
|
5562
|
+
return null;
|
|
5563
|
+
const clean = this.safeConversationId(conversationId || 'default');
|
|
5564
|
+
const stateKey = this.workspaceConversationStateKey(clean);
|
|
5565
|
+
if (!stateKey)
|
|
5566
|
+
return null;
|
|
5567
|
+
const memoryKey = `${ws.isInternal ? 'internal' : 'external'}:${path.resolve(ws.path)}::conversation:${clean}`;
|
|
5568
|
+
const archiveDir = path.join(ws.path, 'archive');
|
|
5569
|
+
const workspacePrefix = this.workspaceConversationPrefix() || '';
|
|
5570
|
+
const archiveMode = this.modeName();
|
|
5571
|
+
const archiveModel = this.model;
|
|
5572
|
+
// readStoredConversationState returns a cache object. Clone it before any
|
|
5573
|
+
// asynchronous gap so concurrent archive requests cannot mutate one
|
|
5574
|
+
// another's source snapshot.
|
|
5575
|
+
const cachedStored = this.readStoredConversationState(ws);
|
|
5576
|
+
const stored = JSON.parse(JSON.stringify(cachedStored || {}));
|
|
5577
|
+
const persisted = stored.conversations?.[stateKey];
|
|
5578
|
+
if (persisted)
|
|
5579
|
+
this.normalizeConversationTree(persisted);
|
|
5580
|
+
const memory = this.workspaceConversations.get(memoryKey);
|
|
5581
|
+
const persistedMessagesAvailable = persisted?.chatMessages !== undefined;
|
|
5582
|
+
const sourceMessages = persisted?.chatMessages ?? memory?.chatMessages ?? [];
|
|
5583
|
+
const sourceHistory = persistedMessagesAvailable
|
|
5584
|
+
? (persisted?.history ?? [])
|
|
5585
|
+
: (memory?.history ?? persisted?.history ?? []);
|
|
5586
|
+
const messages = this.normalizeConversationChatMessages(sourceMessages, sourceHistory);
|
|
5587
|
+
const filename = await this.writeSessionArchiveAsync(messages, archiveMode, archiveModel, archiveDir);
|
|
5588
|
+
const archiveEntry = persisted ? JSON.parse(JSON.stringify(persisted)) : {
|
|
5589
|
+
title: this.titleFromMessages(messages, clean),
|
|
5590
|
+
chatMessages: messages,
|
|
5591
|
+
history: sourceHistory,
|
|
5592
|
+
plan: memory?.plan,
|
|
5593
|
+
linkedPlan: memory?.linkedPlan,
|
|
5594
|
+
subagentState: memory?.subagentState,
|
|
5595
|
+
workRuns: memory?.workRuns,
|
|
5596
|
+
continuations: memory?.continuations,
|
|
5597
|
+
updatedAt: new Date().toISOString(),
|
|
5598
|
+
};
|
|
5599
|
+
const manifest = {
|
|
5600
|
+
version: 2,
|
|
5601
|
+
kind: 'newmark-conversation-archive',
|
|
5602
|
+
archivedAt: new Date().toISOString(),
|
|
5603
|
+
conversationId: clean,
|
|
5604
|
+
workspaceId: ws.id,
|
|
5605
|
+
workspaceName: ws.name,
|
|
5606
|
+
workspacePath: ws.path,
|
|
5607
|
+
workspaceInternal: ws.isInternal,
|
|
5608
|
+
statePrefix: workspacePrefix,
|
|
5609
|
+
entry: this.conversationEntryForDisk(archiveEntry),
|
|
5610
|
+
};
|
|
5611
|
+
const manifestPath = this.archiveManifestPath(path.join(archiveDir, filename));
|
|
5612
|
+
const manifestTempPath = `${manifestPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
5613
|
+
try {
|
|
5614
|
+
await fs.promises.writeFile(manifestTempPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
5615
|
+
await fs.promises.rename(manifestTempPath, manifestPath);
|
|
5616
|
+
}
|
|
5617
|
+
finally {
|
|
5618
|
+
try {
|
|
5619
|
+
await fs.promises.unlink(manifestTempPath);
|
|
5620
|
+
}
|
|
5621
|
+
catch { }
|
|
5622
|
+
}
|
|
5623
|
+
this.finalizeAsyncConversationArchive(clean, stateKey, memoryKey, ws);
|
|
5624
|
+
return filename;
|
|
5625
|
+
}
|
|
5626
|
+
finalizeAsyncConversationArchive(clean, stateKey, memoryKey, ws) {
|
|
5627
|
+
let nextActiveId = '';
|
|
5628
|
+
this.mutateStoredConversationState(ws, latest => {
|
|
5629
|
+
latest.conversations = latest.conversations || {};
|
|
5630
|
+
delete latest.conversations[stateKey];
|
|
5631
|
+
// Derive the target prefix from the captured state key rather than the
|
|
5632
|
+
// Agent's possibly changed foreground workspace.
|
|
5633
|
+
const prefix = stateKey.slice(0, Math.max(0, stateKey.length - clean.length - 1)) + '-';
|
|
5634
|
+
const remaining = Object.keys(latest.conversations)
|
|
5635
|
+
.filter(key => !prefix || key.startsWith(prefix))
|
|
5636
|
+
.map(key => key.slice(prefix.length))
|
|
5637
|
+
.filter(Boolean);
|
|
5638
|
+
const currentActiveId = this.safeConversationId(latest.activeConversationId || this.activeConversationId || 'default');
|
|
5639
|
+
if (clean === currentActiveId)
|
|
5640
|
+
latest.activeConversationId = remaining[0] || 'default';
|
|
5641
|
+
nextActiveId = latest.activeConversationId || remaining[0] || 'default';
|
|
5642
|
+
return latest;
|
|
5643
|
+
});
|
|
5644
|
+
this.workspaceConversations.delete(memoryKey);
|
|
5645
|
+
const duplicateMemoryKey = `${ws.isInternal ? 'internal' : 'external'}:${path.resolve(ws.path)}::conversation:${clean}`;
|
|
5646
|
+
this.workspaceConversations.delete(duplicateMemoryKey);
|
|
5647
|
+
if (clean === this.safeConversationId(this.activeConversationId || 'default')) {
|
|
5648
|
+
this.activeConversationId = nextActiveId || 'default';
|
|
5649
|
+
this.loadWorkspaceConversationState();
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
4762
5652
|
listStoredConversationIds(stored) {
|
|
4763
5653
|
const prefix = `${this.workspaceConversationPrefix() || ''}-`;
|
|
4764
5654
|
return Object.keys(stored.conversations || {})
|
|
@@ -5393,10 +6283,10 @@ class Agent {
|
|
|
5393
6283
|
const provider = this.config.findProvider(providerId);
|
|
5394
6284
|
return all.filter(m => m.provider_id === (provider?.id || providerId));
|
|
5395
6285
|
}
|
|
5396
|
-
async validateModels(selectedNames) {
|
|
6286
|
+
async validateModels(selectedNames, options = {}) {
|
|
5397
6287
|
if (this.modelValidationPromise)
|
|
5398
6288
|
return this.modelValidationPromise;
|
|
5399
|
-
const validation = this.runModelValidation(selectedNames);
|
|
6289
|
+
const validation = this.runModelValidation(selectedNames, options.persist !== false);
|
|
5400
6290
|
this.modelValidationPromise = validation;
|
|
5401
6291
|
try {
|
|
5402
6292
|
return await validation;
|
|
@@ -5416,7 +6306,7 @@ class Agent {
|
|
|
5416
6306
|
recentChecks: this.modelValidationProgress.recentChecks.map(item => ({ ...item })),
|
|
5417
6307
|
};
|
|
5418
6308
|
}
|
|
5419
|
-
async runModelValidation(selectedNames) {
|
|
6309
|
+
async runModelValidation(selectedNames, persist = true) {
|
|
5420
6310
|
const selectedModels = this.config.modelsForSelections(selectedNames);
|
|
5421
6311
|
if (!selectedModels.length) {
|
|
5422
6312
|
this.modelValidationProgress = {
|
|
@@ -5427,7 +6317,10 @@ class Agent {
|
|
|
5427
6317
|
}
|
|
5428
6318
|
const results = [];
|
|
5429
6319
|
const catalogByProvider = new Map();
|
|
5430
|
-
|
|
6320
|
+
// Read-only CLI validation may reuse already persisted, redacted evidence
|
|
6321
|
+
// without rewriting it. This keeps the no-mutation contract while making
|
|
6322
|
+
// repeated release/user checks local when the seven-day record is fresh.
|
|
6323
|
+
const cache = new modelValidationStore_1.FileModelValidationCache(this.rootPath, { readOnly: !persist });
|
|
5431
6324
|
const checksPerModel = 11;
|
|
5432
6325
|
let currentModel = '';
|
|
5433
6326
|
let currentModelChecks = 0;
|
|
@@ -5568,7 +6461,8 @@ class Agent {
|
|
|
5568
6461
|
completedModels: this.modelValidationProgress.completedModels + 1,
|
|
5569
6462
|
};
|
|
5570
6463
|
}
|
|
5571
|
-
|
|
6464
|
+
if (persist)
|
|
6465
|
+
this.config.save();
|
|
5572
6466
|
this.modelValidationProgress = {
|
|
5573
6467
|
...this.modelValidationProgress,
|
|
5574
6468
|
running: false,
|
|
@@ -5591,23 +6485,86 @@ class Agent {
|
|
|
5591
6485
|
return new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'));
|
|
5592
6486
|
}
|
|
5593
6487
|
async editorModelRequest(input, signal) {
|
|
5594
|
-
const models = this.config.allModels().filter(model =>
|
|
6488
|
+
const models = this.config.allModels().filter(model => {
|
|
6489
|
+
if (model.enabled === false)
|
|
6490
|
+
return false;
|
|
6491
|
+
if (!String(model.api_key || '').trim() || !String(model.provider_url || '').trim())
|
|
6492
|
+
return false;
|
|
6493
|
+
const statuses = [model.evaluation?.status, model.validation?.status]
|
|
6494
|
+
.map(status => String(status || '').trim().toLowerCase())
|
|
6495
|
+
.filter(Boolean);
|
|
6496
|
+
if (statuses.some(status => status === 'auth_error' || status === 'invalid_config' || status.startsWith('error')))
|
|
6497
|
+
return false;
|
|
6498
|
+
const hasPositiveEvidence = statuses.some(status => status === 'available'
|
|
6499
|
+
|| status === 'verified'
|
|
6500
|
+
|| status === 'degraded'
|
|
6501
|
+
|| status === 'rate_limited');
|
|
6502
|
+
return !statuses.length || hasPositiveEvidence;
|
|
6503
|
+
});
|
|
5595
6504
|
const current = this.activeModelConfig();
|
|
5596
6505
|
const copilot = input.preferCopilot ? models.find(model => model.provider_protocol === 'github_models' && model.enabled !== false) : undefined;
|
|
5597
6506
|
const selected = copilot || (current && models.find(model => model.provider_id === current.provider_id && model.name === current.name)) || models.find(model => (model.validation?.level === 'standard' || model.validation?.level === 'extended') &&
|
|
5598
|
-
(model.validation
|
|
6507
|
+
(model.validation?.status === 'verified' || model.validation?.status === 'degraded')) || models.find(model => model.evaluation?.status === 'available') || models[0];
|
|
5599
6508
|
if (!selected?.api_key || !selected.provider_url)
|
|
5600
6509
|
return { ok: false, text: '', error: 'No available editor prediction model.' };
|
|
5601
|
-
const provider =
|
|
6510
|
+
const provider = input.completion
|
|
6511
|
+
? new provider_1.LLMProvider(selected.provider, selected.provider_url, selected.api_key, selected.provider_protocol, 'chat_stream', this.config.contextFlag('provider_adapters_v2'), EDITOR_COMPLETION_TIMEOUT_MS)
|
|
6512
|
+
: new provider_1.LLMProvider(selected.provider, selected.provider_url, selected.api_key, selected.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'));
|
|
5602
6513
|
const language = path.extname(String(input.path || '')).replace(/^\./, '') || 'text';
|
|
5603
6514
|
const system = input.completion
|
|
5604
6515
|
? 'You are an inline code completion engine. Return only the exact text to insert at the cursor. Do not use Markdown fences or explanations.'
|
|
5605
6516
|
: 'You are Newmark Editor Agent. Give concise, actionable code guidance grounded in the supplied file and selection. Do not claim changes were applied.';
|
|
6517
|
+
const before = String(input.before || '').slice(-EDITOR_COMPLETION_BEFORE_CONTEXT_CHARS);
|
|
6518
|
+
const after = String(input.after || '').slice(0, EDITOR_COMPLETION_AFTER_CONTEXT_CHARS);
|
|
5606
6519
|
const prompt = input.completion
|
|
5607
|
-
? `Language: ${language}\nFile: ${input.path || ''}\
|
|
6520
|
+
? `Language: ${language}\nFile: ${input.path || ''}\nCode before cursor:\n${before}\nCode after cursor:\n${after}\nReturn only the shortest useful continuation.`
|
|
5608
6521
|
: `File: ${input.path || ''}\nInstruction: ${input.instruction || 'Review the current code and suggest the next useful change.'}\nSelection:\n${String(input.selection || '').slice(0, 8000)}\nFile content:\n${String(input.content || '').slice(0, 18000)}`;
|
|
5609
6522
|
try {
|
|
5610
|
-
const
|
|
6523
|
+
const messages = [{ role: 'user', content: prompt }];
|
|
6524
|
+
let rawText = '';
|
|
6525
|
+
const canStreamCompletion = !!input.completion
|
|
6526
|
+
&& typeof input.onTextDelta === 'function'
|
|
6527
|
+
&& (selected.provider_protocol !== 'openai' || this.config.contextFlag('provider_adapters_v2'));
|
|
6528
|
+
if (canStreamCompletion) {
|
|
6529
|
+
const streamed = [];
|
|
6530
|
+
let streamFailure = null;
|
|
6531
|
+
try {
|
|
6532
|
+
for await (const token of provider.chatStreamWithTools(selected.name, messages, system, 0.05, EDITOR_COMPLETION_MAX_TOKENS, [], signal)) {
|
|
6533
|
+
if (token.type !== 'text' || !token.text)
|
|
6534
|
+
continue;
|
|
6535
|
+
const delta = String(token.text);
|
|
6536
|
+
if (/^\[(?:LLM )?Error\b/i.test(delta)) {
|
|
6537
|
+
streamFailure = new Error(delta);
|
|
6538
|
+
continue;
|
|
6539
|
+
}
|
|
6540
|
+
streamed.push(delta);
|
|
6541
|
+
input.onTextDelta?.(delta);
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
catch (error) {
|
|
6545
|
+
if (signal?.aborted)
|
|
6546
|
+
throw error;
|
|
6547
|
+
streamFailure = error instanceof Error ? error : new Error(String(error));
|
|
6548
|
+
}
|
|
6549
|
+
if (streamFailure) {
|
|
6550
|
+
// A provider may accept the streaming request but reject its SSE
|
|
6551
|
+
// mode. Retry once through the already-supported bounded chat path;
|
|
6552
|
+
// this keeps older gateways working without hiding a partial stream
|
|
6553
|
+
// behind a false success.
|
|
6554
|
+
rawText = await provider.chat(selected.name, messages, system, 0.05, EDITOR_COMPLETION_MAX_TOKENS, signal);
|
|
6555
|
+
}
|
|
6556
|
+
else {
|
|
6557
|
+
rawText = streamed.join('');
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
else {
|
|
6561
|
+
rawText = await provider.chat(selected.name, messages, system, 0.05, input.completion ? EDITOR_COMPLETION_MAX_TOKENS : 1800, signal);
|
|
6562
|
+
}
|
|
6563
|
+
rawText = rawText
|
|
6564
|
+
.replace(/^```[\w-]*\s*|\s*```$/g, '');
|
|
6565
|
+
// Preserve indentation for real code, but treat whitespace-only model
|
|
6566
|
+
// responses as an actual empty suggestion instead of a visible ghost.
|
|
6567
|
+
const text = rawText.trim() ? rawText.slice(0, input.completion ? EDITOR_COMPLETION_MAX_TEXT_CHARS : rawText.length) : '';
|
|
5611
6568
|
return { ok: !!text, text, model: selected.name, provider: selected.provider };
|
|
5612
6569
|
}
|
|
5613
6570
|
catch (error) {
|
|
@@ -5851,7 +6808,14 @@ class Agent {
|
|
|
5851
6808
|
const text = typeof input === 'string' ? input : String(input.text || '');
|
|
5852
6809
|
const inputEnvelope = typeof input === 'string' ? null : input;
|
|
5853
6810
|
const hiddenUserInput = inputEnvelope?.hiddenUserInput === true;
|
|
5854
|
-
|
|
6811
|
+
// An empty selection may be repaired to the configured default, but an
|
|
6812
|
+
// explicitly requested fixed model must remain visible to the
|
|
6813
|
+
// fail-closed availability check below. Otherwise a typo such as
|
|
6814
|
+
// provider-that-does-not-exist/missing-model silently becomes the first
|
|
6815
|
+
// configured fixture/default deployment.
|
|
6816
|
+
const explicitFixedModel = this.model !== '' && this.model !== 'auto';
|
|
6817
|
+
if (!explicitFixedModel)
|
|
6818
|
+
this.ensureUsableModelSelection();
|
|
5855
6819
|
const clientMessageId = String(inputEnvelope?.clientMessageId || '').trim();
|
|
5856
6820
|
const inputRunId = String(inputEnvelope?.runId || this.activeWorkRunId || '').trim();
|
|
5857
6821
|
const rawImages = typeof input === 'string' ? [] : (Array.isArray(input.images) ? input.images : []);
|
|
@@ -5949,7 +6913,16 @@ class Agent {
|
|
|
5949
6913
|
await this.evaluateAndSwitch(displayText, inputEnvelope?.routePolicy);
|
|
5950
6914
|
}
|
|
5951
6915
|
if (this.model && this.modelIsUnavailable(this.model)) {
|
|
6916
|
+
const requestedModel = this.model;
|
|
5952
6917
|
this.switchToFallbackModel();
|
|
6918
|
+
if (this.modelIsUnavailable(this.model)) {
|
|
6919
|
+
const message = `[Error] Model '${requestedModel || 'unknown'}' is unavailable or not configured. Select a configured model or enable a valid provider before sending.`;
|
|
6920
|
+
this.status = 'error';
|
|
6921
|
+
// Do not return an error token as if it were a successful assistant
|
|
6922
|
+
// response. The normal catch/finalizer path must publish an error
|
|
6923
|
+
// terminal event and keep the work run out of completed state.
|
|
6924
|
+
throw new Error(message);
|
|
6925
|
+
}
|
|
5953
6926
|
}
|
|
5954
6927
|
// Use external opencode CLI engine
|
|
5955
6928
|
if (this.engine === 'opencode') {
|
|
@@ -6166,8 +7139,12 @@ class Agent {
|
|
|
6166
7139
|
const sa = this.subagents.get(name);
|
|
6167
7140
|
if (!sa)
|
|
6168
7141
|
return { ok: false, output: `[Subagent] Not found: ${name}`, error: `Not found: ${name}` };
|
|
6169
|
-
|
|
6170
|
-
|
|
7142
|
+
// 上下文回归优化:不再把完整 transcript(含所有中间 tool call/result 洪水)
|
|
7143
|
+
// 注入主 Agent 上下文。结果优先,transcript 只保留有界尾部——最近几条
|
|
7144
|
+
// assistant/user 消息即足以让主 Agent 判断上下文,完整历史按需走
|
|
7145
|
+
// subagent_read 的 max_chars 分页读取。
|
|
7146
|
+
const transcript = this.subagents.boundedResultTranscript(sa.id);
|
|
7147
|
+
return 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);
|
|
6171
7148
|
}
|
|
6172
7149
|
catch {
|
|
6173
7150
|
return { ok: false, output: '[Subagent] Invalid result arguments.', error: 'Invalid result arguments.' };
|
|
@@ -6765,7 +7742,7 @@ class Agent {
|
|
|
6765
7742
|
: '';
|
|
6766
7743
|
const delegatedPrompt = [
|
|
6767
7744
|
continuation,
|
|
6768
|
-
requestedFlowName ? `[Workflow requested: ${requestedFlowName}
|
|
7745
|
+
requestedFlowName ? `[Workflow requested: ${requestedFlowName}]` : '',
|
|
6769
7746
|
child.goal ? `[Goal objective: ${child.goal.objective}]` : '',
|
|
6770
7747
|
`Workspace: ${workspacePath}`,
|
|
6771
7748
|
prompt,
|
|
@@ -7059,35 +8036,45 @@ class Agent {
|
|
|
7059
8036
|
}
|
|
7060
8037
|
async maybeCompress(msgs, provider, signal, compressionModel, force = false) {
|
|
7061
8038
|
if (signal?.aborted)
|
|
7062
|
-
return;
|
|
8039
|
+
return false;
|
|
7063
8040
|
if (!this.config.getBool('context', 'auto_compress'))
|
|
7064
|
-
return;
|
|
8041
|
+
return false;
|
|
7065
8042
|
const total = msgs.reduce((sum, m) => sum + (typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content || '').length), 0);
|
|
7066
8043
|
const budget = this.compressionBudget(msgs);
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
if (!
|
|
8044
|
+
const thresholdReached = budget.buildBlockTokens >= budget.buildBlockTriggerTokens
|
|
8045
|
+
|| budget.longHistoryTokens >= budget.longHistoryTriggerTokens;
|
|
8046
|
+
if (!thresholdReached && !force)
|
|
8047
|
+
return false;
|
|
8048
|
+
const priorSummary = String(this.lastCompression?.summary || '').trim();
|
|
8049
|
+
const priorSummaryMarker = priorSummary.slice(0, 240);
|
|
8050
|
+
const priorSummaryPresent = !!priorSummaryMarker
|
|
8051
|
+
&& msgs.some(message => String(message.content || '').includes(priorSummaryMarker));
|
|
8052
|
+
if (!force && this.lastCompression && priorSummaryPresent) {
|
|
7070
8053
|
const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
|
|
7071
8054
|
const baselineTokens = Math.max(0, Number(this.lastCompression.compressedTokens || 0));
|
|
7072
8055
|
const charGrowth = baselineChars ? Math.max(0, total - baselineChars) : Number.POSITIVE_INFINITY;
|
|
7073
8056
|
const tokenGrowth = baselineTokens ? Math.max(0, budget.estimatedTokens - baselineTokens) : Number.POSITIVE_INFINITY;
|
|
7074
8057
|
const minCharGrowth = Math.max(12_000, Math.floor(baselineChars * 0.25));
|
|
7075
|
-
const minTokenGrowth = Math.max(1_024, Math.floor(budget.
|
|
8058
|
+
const minTokenGrowth = Math.max(1_024, Math.floor(budget.buildBlockTriggerTokens * 0.2));
|
|
7076
8059
|
if (charGrowth < minCharGrowth && tokenGrowth < minTokenGrowth)
|
|
7077
|
-
return;
|
|
8060
|
+
return false;
|
|
7078
8061
|
}
|
|
7079
8062
|
const originalMessageCount = msgs.length;
|
|
7080
8063
|
const configuredKeepLast = this.config.getNum('context', 'keep_recent_messages') || 10;
|
|
7081
8064
|
if (msgs.length <= 1)
|
|
7082
|
-
return;
|
|
8065
|
+
return false;
|
|
7083
8066
|
// Reserve room for the one-time post-compression continuation anchor so
|
|
7084
8067
|
// adding it cannot push a near-limit request back over the target budget.
|
|
7085
8068
|
const continuationAnchorTokens = this.estimateContextTokens([this.postCompressionContinuationMessage()]);
|
|
7086
|
-
|
|
8069
|
+
// The current Build keeps its own 70% working window. The omitted prefix
|
|
8070
|
+
// is long-term history and is summarized with its separate 20% budget.
|
|
8071
|
+
// This prevents a completed Build from consuming the same small recent
|
|
8072
|
+
// window as an active Build and avoids back-to-back compaction cycles.
|
|
8073
|
+
const recentBudget = Math.max(64, budget.buildBlockRetentionTokens - budget.summaryTokens - continuationAnchorTokens);
|
|
7087
8074
|
const recent = this.recentContextSuffix(msgs, configuredKeepLast, recentBudget);
|
|
7088
8075
|
const recentStart = Math.max(0, msgs.length - recent.length);
|
|
7089
8076
|
if (recentStart <= 0)
|
|
7090
|
-
return;
|
|
8077
|
+
return false;
|
|
7091
8078
|
// The first history item is usually the first user task, not foundational
|
|
7092
8079
|
// context. Keeping it forever makes an old task more salient after every
|
|
7093
8080
|
// compaction. Foundational rules are rebuilt by buildSystemPrompt(); the
|
|
@@ -7096,7 +8083,7 @@ class Agent {
|
|
|
7096
8083
|
const currentInstruction = this.latestUserHistoryText(recent);
|
|
7097
8084
|
const compression = await this.buildCompressionSummary(middle, total, budget, provider, signal, compressionModel || this.activeModelName(), currentInstruction);
|
|
7098
8085
|
if (signal?.aborted)
|
|
7099
|
-
return;
|
|
8086
|
+
return false;
|
|
7100
8087
|
const compressed = [{
|
|
7101
8088
|
role: 'system',
|
|
7102
8089
|
content: compression.summary,
|
|
@@ -7123,6 +8110,7 @@ class Agent {
|
|
|
7123
8110
|
};
|
|
7124
8111
|
this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
|
|
7125
8112
|
this.persistCompressedHistory(compression.summary, recent.length, msgs);
|
|
8113
|
+
return true;
|
|
7126
8114
|
}
|
|
7127
8115
|
async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = '') {
|
|
7128
8116
|
const workspacePath = this.workspace.current?.path || this.rootPath;
|
|
@@ -7154,19 +8142,48 @@ class Agent {
|
|
|
7154
8142
|
return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
|
|
7155
8143
|
try {
|
|
7156
8144
|
const { temperature } = provider.intelligenceConfig('low');
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
8145
|
+
// DSH 式前缀缓存命中:复用主对话的稳定 base prompt 作为 system,并把被压缩
|
|
8146
|
+
// 的省略段消息原样保留为前缀,仅追加一条压缩指令 user 消息。这样压缩摘要
|
|
8147
|
+
// 调用与主对话最近一次请求共享相同的 system 前缀和历史消息前缀,命中
|
|
8148
|
+
// provider 的 warm prefix cache(KV cache),避免摘要调用反复重新计价整个前缀。
|
|
8149
|
+
const system = this.buildSystemPrompt();
|
|
8150
|
+
// DSH 式 tool-result pruning:压缩摘要调用前先裁剪大工具结果,避免把
|
|
8151
|
+
// 巨型 read/grep/terminal 输出整段重放给摘要模型(既省 token,也减少
|
|
8152
|
+
// 前缀缓存被单条超大结果打断)。头部保留判断所需的结论性内容,尾部保留
|
|
8153
|
+
// 路径/错误等收尾证据。
|
|
8154
|
+
const prunedPrefixMessages = middle.map((message) => {
|
|
8155
|
+
const record = message;
|
|
8156
|
+
const role = String(record.role || '');
|
|
8157
|
+
const isToolResult = role === 'tool' || role === 'function';
|
|
8158
|
+
const content = record.content;
|
|
8159
|
+
if (isToolResult && typeof content === 'string' && content.length > TOOL_RESULT_PRUNE_CHARS) {
|
|
8160
|
+
return {
|
|
8161
|
+
...message,
|
|
8162
|
+
content: this.pruneToolResultContent(content),
|
|
8163
|
+
};
|
|
8164
|
+
}
|
|
8165
|
+
return message;
|
|
8166
|
+
});
|
|
8167
|
+
// 原样复用 pruned middle 消息作前缀,但把历史图片降级为占位文本:
|
|
8168
|
+
// 图片对摘要无益,且会破坏前缀缓存(主对话前缀中不含图片字节)。
|
|
8169
|
+
const prefixMessages = prunedPrefixMessages.map((message) => {
|
|
8170
|
+
if (!Array.isArray(message.content))
|
|
8171
|
+
return { ...message };
|
|
8172
|
+
const parts = message.content.map(part => (part?.type === 'image_url'
|
|
8173
|
+
? { type: 'text', text: '[Historical image attachment omitted after context compression.]' }
|
|
8174
|
+
: { ...part }));
|
|
8175
|
+
return { ...message, content: parts };
|
|
8176
|
+
});
|
|
8177
|
+
const prompt = [
|
|
8178
|
+
'Compress the following conversation segment into a structured checkpoint for this coding assistant.',
|
|
8179
|
+
'The omitted transcript below is the conversation ABOVE this instruction; the latest retained user instruction is OUTSIDE the segment and remains authoritative.',
|
|
8180
|
+
'',
|
|
7160
8181
|
'Classify task state instead of treating every historical user request as still active.',
|
|
7161
8182
|
'Preserve an older task as active or unfinished only when the transcript or explicit tracker shows concrete unfinished work and it remains relevant to the latest instruction or a required dependency.',
|
|
7162
8183
|
'Within Active Or Unfinished Work, order every retained historical task from newest to oldest. The newest unfinished task must be completed before the next-newest task.',
|
|
7163
8184
|
'Completed, superseded, abandoned, and unrelated tasks belong under Completed Or Background Work and must not be revived as the current objective.',
|
|
7164
8185
|
'Preserve concrete facts, current workspace, mode, model, tool results, files changed, decisions, errors, constraints, and user preferences.',
|
|
7165
8186
|
'Do not invent completion. Mark uncertainty explicitly.',
|
|
7166
|
-
'Return concise Markdown with these stable headings: Active Or Unfinished Work; Completed Or Background Work; Decisions And Constraints; Tool And Verification Evidence; Relevant Files.',
|
|
7167
|
-
].join('\n');
|
|
7168
|
-
const prompt = [
|
|
7169
|
-
'Compress the following conversation segment.',
|
|
7170
8187
|
'',
|
|
7171
8188
|
'Required metadata to preserve:',
|
|
7172
8189
|
meta,
|
|
@@ -7174,16 +8191,16 @@ class Agent {
|
|
|
7174
8191
|
`Original message count in omitted segment: ${middle.length}`,
|
|
7175
8192
|
`Original total message chars before compression: ${totalChars}`,
|
|
7176
8193
|
'',
|
|
7177
|
-
'Latest retained user instruction (authoritative and not part of the omitted
|
|
8194
|
+
'Latest retained user instruction (authoritative and not part of the omitted segment):',
|
|
7178
8195
|
currentInstruction || '(No retained user text was available; preserve uncertainty and do not promote old tasks without evidence.)',
|
|
7179
8196
|
'',
|
|
7180
|
-
'
|
|
7181
|
-
|
|
8197
|
+
'Return ONLY concise Markdown with these stable headings:',
|
|
8198
|
+
'Active Or Unfinished Work; Completed Or Background Work; Decisions And Constraints; Tool And Verification Evidence; Relevant Files.',
|
|
7182
8199
|
].join('\n');
|
|
7183
8200
|
const modelName = String(compressionModel || this.activeModelName()).trim();
|
|
7184
8201
|
if (!modelName)
|
|
7185
8202
|
return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
|
|
7186
|
-
const generated = await this.withTimeout(provider.chat(modelName, [{ role: 'user', content: prompt }], system, temperature, budget.summaryTokens, signal), 120000);
|
|
8203
|
+
const generated = await this.withTimeout(provider.chat(modelName, [...prefixMessages, { role: 'user', content: prompt }], system, temperature, budget.summaryTokens, signal), 120000);
|
|
7187
8204
|
const generatedText = String(generated || '').trim();
|
|
7188
8205
|
if (!generatedText || /^\[LLM Error(?::|\])/i.test(generatedText) || /^LLM Error:/i.test(generatedText)) {
|
|
7189
8206
|
return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
|
|
@@ -7231,6 +8248,16 @@ class Agent {
|
|
|
7231
8248
|
}
|
|
7232
8249
|
return '';
|
|
7233
8250
|
}
|
|
8251
|
+
/** 裁剪超长工具结果:保留头部结论性内容 + 尾部证据(路径/错误/收尾),
|
|
8252
|
+
* 中间用占位标记省略。与 DSH toolResultPruner 的语义一致。 */
|
|
8253
|
+
pruneToolResultContent(content) {
|
|
8254
|
+
const text = String(content || '');
|
|
8255
|
+
const headChars = Math.floor(TOOL_RESULT_PRUNE_CHARS * 0.6);
|
|
8256
|
+
const tailChars = Math.max(0, TOOL_RESULT_PRUNE_CHARS - headChars - 48);
|
|
8257
|
+
const head = text.slice(0, headChars).trimEnd();
|
|
8258
|
+
const tail = text.slice(-tailChars).trimStart();
|
|
8259
|
+
return `${head}\n\n[...tool result pruned ${text.length - headChars - tailChars} chars...]\n\n${tail}`;
|
|
8260
|
+
}
|
|
7234
8261
|
compressionHistoryContent(content) {
|
|
7235
8262
|
if (!Array.isArray(content))
|
|
7236
8263
|
return String(content || '');
|
|
@@ -7296,6 +8323,17 @@ class Agent {
|
|
|
7296
8323
|
return [];
|
|
7297
8324
|
}
|
|
7298
8325
|
}
|
|
8326
|
+
compressionArchiveEntryCount() {
|
|
8327
|
+
const scopeKey = this.compressionArchiveScopeKey();
|
|
8328
|
+
if (!scopeKey)
|
|
8329
|
+
return 0;
|
|
8330
|
+
if (this.compressionArchiveCountCache?.scopeKey === scopeKey)
|
|
8331
|
+
return this.compressionArchiveCountCache.count;
|
|
8332
|
+
const hotIds = new Set(this.compressionCache.map(entry => entry.id));
|
|
8333
|
+
const count = this.compressionHistoryArchive.activeEntries(scopeKey).filter(entry => !hotIds.has(entry.id)).length;
|
|
8334
|
+
this.compressionArchiveCountCache = { scopeKey, count };
|
|
8335
|
+
return count;
|
|
8336
|
+
}
|
|
7299
8337
|
archiveColdCompressionEntries(entries) {
|
|
7300
8338
|
const scopeKey = this.compressionArchiveScopeKey();
|
|
7301
8339
|
if (!scopeKey)
|
|
@@ -7318,6 +8356,7 @@ class Agent {
|
|
|
7318
8356
|
return;
|
|
7319
8357
|
try {
|
|
7320
8358
|
this.compressionHistoryArchive.markRestored(scopeKey, id);
|
|
8359
|
+
this.compressionArchiveCountCache = null;
|
|
7321
8360
|
}
|
|
7322
8361
|
catch {
|
|
7323
8362
|
// The restored context is already authoritative; archive bookkeeping is best-effort.
|
|
@@ -7355,6 +8394,7 @@ class Agent {
|
|
|
7355
8394
|
const failed = this.archiveColdCompressionEntries(evicted);
|
|
7356
8395
|
this.compressionCache = [...failed, ...this.compressionCache.slice(this.compressionCache.length - maxEntries)];
|
|
7357
8396
|
}
|
|
8397
|
+
this.compressionArchiveCountCache = null;
|
|
7358
8398
|
this.saveWorkspaceConversationState(true);
|
|
7359
8399
|
}
|
|
7360
8400
|
contextHistoryProtectedStartIndex() {
|
|
@@ -7367,6 +8407,34 @@ class Agent {
|
|
|
7367
8407
|
candidates.push(lastUserIndex);
|
|
7368
8408
|
return candidates.length ? Math.min(...candidates) : -1;
|
|
7369
8409
|
}
|
|
8410
|
+
historyRecordFingerprint(record) {
|
|
8411
|
+
if (!record)
|
|
8412
|
+
return '';
|
|
8413
|
+
return `${String(record.role || '')}\u0000${JSON.stringify(record.content ?? '')}`;
|
|
8414
|
+
}
|
|
8415
|
+
flushPendingHistoryRemovals() {
|
|
8416
|
+
if (!this.pendingHistoryRemovals.length)
|
|
8417
|
+
return;
|
|
8418
|
+
const pending = this.pendingHistoryRemovals;
|
|
8419
|
+
this.pendingHistoryRemovals = [];
|
|
8420
|
+
// 按 position 从大到小处理,避免多次 splice 造成索引偏移。
|
|
8421
|
+
const ordered = pending.slice().sort((a, b) => b.position - a.position);
|
|
8422
|
+
for (const item of ordered) {
|
|
8423
|
+
const atPosition = this.history[item.position];
|
|
8424
|
+
if (atPosition && this.historyRecordFingerprint(atPosition) === item.fingerprint) {
|
|
8425
|
+
this.history.splice(item.position, 1);
|
|
8426
|
+
continue;
|
|
8427
|
+
}
|
|
8428
|
+
// 位置已被压缩/折叠改变时,用内容指纹兜底移除首个匹配项。
|
|
8429
|
+
for (let i = this.history.length - 1; i >= 0; i -= 1) {
|
|
8430
|
+
if (this.historyRecordFingerprint(this.history[i]) === item.fingerprint) {
|
|
8431
|
+
this.history.splice(i, 1);
|
|
8432
|
+
break;
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
this.saveWorkspaceConversationState(true);
|
|
8437
|
+
}
|
|
7370
8438
|
contextHistoryProtectedZone() {
|
|
7371
8439
|
const start = this.contextHistoryProtectedStartIndex();
|
|
7372
8440
|
const zone = new Set();
|
|
@@ -7387,9 +8455,6 @@ class Agent {
|
|
|
7387
8455
|
buildSystemPrompt() {
|
|
7388
8456
|
const cwd = this.workspace.current?.path || this.rootPath;
|
|
7389
8457
|
const enabledSkills = this.skills.active();
|
|
7390
|
-
const currentSkillTask = this.latestUserHistoryText(this.history);
|
|
7391
|
-
const relevantSkills = this.skills.search(currentSkillTask, 8);
|
|
7392
|
-
const linkedPlan = this.getLinkedPlan();
|
|
7393
8458
|
const globalPromptPath = path.join(this.rootPath, 'agent.md');
|
|
7394
8459
|
const globalPrompt = normalizeInjectedPrompt(fs.existsSync(globalPromptPath) ? fs.readFileSync(globalPromptPath, 'utf-8') : '');
|
|
7395
8460
|
const workspacePrompt = normalizeInjectedPrompt(this.workspace.currentAgentPrompt());
|
|
@@ -7398,7 +8463,6 @@ class Agent {
|
|
|
7398
8463
|
mode: this.mode,
|
|
7399
8464
|
conversationId: this.activeConversationId,
|
|
7400
8465
|
subagent: this.isSubagentRuntime ? [this.subagentName, this.subagentPrompt] : null,
|
|
7401
|
-
linkedPlanRevision: linkedPlan.revision,
|
|
7402
8466
|
goal: this.goal ? [this.goal.objective, this.goal.paused] : null,
|
|
7403
8467
|
promptMode: this.config.getStr('workspace', 'prompt_mode'),
|
|
7404
8468
|
customPrompt: this.config.getStr('agent', 'custom_prompt'),
|
|
@@ -7407,8 +8471,7 @@ class Agent {
|
|
|
7407
8471
|
optionFeedback: this.config.getStr('agent', 'option_feedback'),
|
|
7408
8472
|
model: this.model,
|
|
7409
8473
|
intelligence: this.intelligence,
|
|
7410
|
-
skills: enabledSkills.map(skill => [skill.name, skill.description]),
|
|
7411
|
-
relevantSkills: relevantSkills.map(skill => [skill.name, skill.description]),
|
|
8474
|
+
skills: enabledSkills.slice(0, 8).map(skill => [skill.name, skill.description]),
|
|
7412
8475
|
globalPrompt,
|
|
7413
8476
|
workspacePrompt,
|
|
7414
8477
|
});
|
|
@@ -7429,7 +8492,6 @@ class Agent {
|
|
|
7429
8492
|
parts.push(this.buildFeatureDisclosurePrompt());
|
|
7430
8493
|
if (this.mode === 'plan')
|
|
7431
8494
|
parts.push(`[Plan Tool Policy]\n${(0, toolPolicy_1.planModePolicyPrompt)()}`);
|
|
7432
|
-
parts.push(`[Linked Plan revision=${linkedPlan.revision}]\n${linkedPlan.markdown || '(empty)'}`);
|
|
7433
8495
|
const pm = this.config.getStr('workspace', 'prompt_mode') || 'both';
|
|
7434
8496
|
const injectedPrompts = new Set();
|
|
7435
8497
|
if ((pm === 'global_only' || pm === 'both') && globalPrompt) {
|
|
@@ -7448,7 +8510,7 @@ class Agent {
|
|
|
7448
8510
|
if (enabledSkills.length) {
|
|
7449
8511
|
parts.push([
|
|
7450
8512
|
'[Enabled Skills]',
|
|
7451
|
-
...
|
|
8513
|
+
...enabledSkills.slice(0, 8).map(s => `- ${s.name}: ${s.description || 'No description'}`),
|
|
7452
8514
|
'Use the skill tool with query when the matching skill is uncertain, then load exactly one skill by name. Skill bodies and paths are intentionally omitted until loaded. Disabled skills are intentionally omitted.',
|
|
7453
8515
|
].join('\n'));
|
|
7454
8516
|
}
|
|
@@ -7461,18 +8523,21 @@ class Agent {
|
|
|
7461
8523
|
}
|
|
7462
8524
|
parts.push(this.buildModePrompt());
|
|
7463
8525
|
const value = this.contextV2.orchestrator.assemble({
|
|
7464
|
-
|
|
7465
|
-
|
|
8526
|
+
// Keep the complete base prompt in one stable section. The linked_plan
|
|
8527
|
+
// section remains structurally present for Context V2 compatibility but
|
|
8528
|
+
// is intentionally empty: plan contents are retrieved through the tool.
|
|
8529
|
+
generalPrompt: parts.filter(Boolean).join('\n\n'),
|
|
8530
|
+
responseProtocol: '',
|
|
7466
8531
|
baseToolDefinitions: undefined,
|
|
7467
|
-
workspaceAgentProfile:
|
|
7468
|
-
agentRoleAndPermissions:
|
|
7469
|
-
capabilityBoundarySummary:
|
|
7470
|
-
activeToolsetManifest:
|
|
7471
|
-
buildBlockStartupInput:
|
|
7472
|
-
buildBlockMetadata:
|
|
7473
|
-
linkedPlan:
|
|
7474
|
-
activeTasks:
|
|
7475
|
-
currentWorkSet:
|
|
8532
|
+
workspaceAgentProfile: '',
|
|
8533
|
+
agentRoleAndPermissions: '',
|
|
8534
|
+
capabilityBoundarySummary: '',
|
|
8535
|
+
activeToolsetManifest: '',
|
|
8536
|
+
buildBlockStartupInput: '',
|
|
8537
|
+
buildBlockMetadata: '',
|
|
8538
|
+
linkedPlan: '',
|
|
8539
|
+
activeTasks: '',
|
|
8540
|
+
currentWorkSet: '',
|
|
7476
8541
|
branchLogSummary: '',
|
|
7477
8542
|
retrievedOldBlockSummary: '',
|
|
7478
8543
|
buildHistoryCheckpoint: '',
|
|
@@ -7487,11 +8552,9 @@ class Agent {
|
|
|
7487
8552
|
* dev-0.3.0: assemble the model-request system prompt through the Context
|
|
7488
8553
|
* Orchestrator, the single assembly point for every model request. No inline
|
|
7489
8554
|
* prompt concatenation remains in agent.ts: buildSystemPrompt() itself
|
|
7490
|
-
* routes its
|
|
7491
|
-
*
|
|
7492
|
-
*
|
|
7493
|
-
* semantics; for now the legacy sections occupy the first string slots in
|
|
7494
|
-
* their original order and empty sections are skipped.
|
|
8555
|
+
* routes its stable base prompt through the orchestrator, and this method
|
|
8556
|
+
* appends the tool surface notice. The linked-plan section is deliberately
|
|
8557
|
+
* empty here; linked-plan content is tool-retrieved on demand.
|
|
7495
8558
|
*/
|
|
7496
8559
|
assembleContextV2(toolSurfaceNotice) {
|
|
7497
8560
|
return this.contextV2.orchestrator.assemble({
|
|
@@ -7559,6 +8622,7 @@ class Agent {
|
|
|
7559
8622
|
'- Before memory_lab_update, inspect the target with memory_lab_query or memory_lab_read. For an existing component pass expectedUpdatedAt so concurrent/stale writes fail closed; preserve established tag parent paths.',
|
|
7560
8623
|
'- Use memory_lab_delete only for an explicit user request to forget/remove memory. Prior revisions are retained under Memory Lab/archive and mutation decisions are appended to policy.jsonl for replay.',
|
|
7561
8624
|
'- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. Use build_history_query only when the current user asks what specifically happened in one Build Block; querying history is read-only and never authorizes resuming that work.',
|
|
8625
|
+
'- Linked plan disclosure: a durable conversation-linked Markdown plan exists and can be inspected or updated with linked_plan when explicitly needed or required by Plan mode. Its full Markdown and revision are not injected into every model request.',
|
|
7562
8626
|
'- A memory_lab_update, memory_lab_delete, or memory_lab_reindex call is unfinished until its awaited tool result contains rebuildReceipt.completed=true. The completion receipt is represented by the tool activity inside the current Build block and should not be repeated as a separate completion message.',
|
|
7563
8627
|
`- Skills and subagents: skill searches enabled metadata and loads one SKILL.md body on demand; skill_download installs offline skill folders; task creates constrained subagents tracked in agent state.`,
|
|
7564
8628
|
`- Visible output contract: assistant replies are sanitized before display to remove hidden-reasoning markers. ${visibleOutputContract}`,
|