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.
Files changed (69) hide show
  1. package/config.example.json +6 -0
  2. package/dist/cli-commands.d.ts +8 -0
  3. package/dist/cli-commands.js +216 -16
  4. package/dist/cli-discovery.d.ts +15 -0
  5. package/dist/cli-discovery.js +182 -0
  6. package/dist/cli-help.d.ts +2 -0
  7. package/dist/cli-help.js +25 -1
  8. package/dist/context/domain/types.d.ts +37 -0
  9. package/dist/context/services/context-orchestrator.js +2 -0
  10. package/dist/conversation-utility-host.bundle.cjs +1503 -214
  11. package/dist/conversation-utility-host.js +3 -0
  12. package/dist/core/agent.d.ts +157 -8
  13. package/dist/core/agent.js +1176 -112
  14. package/dist/core/agentKernel/agent-loop.js +29 -3
  15. package/dist/core/agentKernel/types.d.ts +7 -0
  16. package/dist/core/agentKernelRunner.d.ts +2 -0
  17. package/dist/core/agentKernelRunner.js +174 -27
  18. package/dist/core/config.d.ts +7 -2
  19. package/dist/core/config.js +24 -6
  20. package/dist/core/conversationKernel.d.ts +5 -0
  21. package/dist/core/conversationKernel.js +30 -1
  22. package/dist/core/dshCompatibility.d.ts +198 -0
  23. package/dist/core/dshCompatibility.js +600 -0
  24. package/dist/core/electronUtilityAgentClient.d.ts +4 -0
  25. package/dist/core/electronUtilityAgentClient.js +4 -0
  26. package/dist/core/electronUtilityRuntimePool.d.ts +19 -0
  27. package/dist/core/electronUtilityRuntimePool.js +76 -0
  28. package/dist/core/flow-runner.js +1 -1
  29. package/dist/core/mcpManager.d.ts +1 -0
  30. package/dist/core/mcpManager.js +100 -10
  31. package/dist/core/modelValidationStore.d.ts +4 -1
  32. package/dist/core/modelValidationStore.js +7 -1
  33. package/dist/core/subagent.d.ts +6 -0
  34. package/dist/core/subagent.js +22 -1
  35. package/dist/core/toolPolicy.d.ts +6 -0
  36. package/dist/core/toolPolicy.js +49 -1
  37. package/dist/core/types.d.ts +1 -1
  38. package/dist/core/utilityAgentProtocol.d.ts +8 -1
  39. package/dist/core/workspace.d.ts +15 -0
  40. package/dist/core/workspace.js +62 -1
  41. package/dist/core/wslAgentClient.d.ts +4 -0
  42. package/dist/core/wslAgentClient.js +4 -0
  43. package/dist/core/wslAgentProtocol.d.ts +8 -1
  44. package/dist/core/wslAgentRuntimePool.d.ts +12 -0
  45. package/dist/core/wslAgentRuntimePool.js +71 -0
  46. package/dist/launcher.js +48 -11
  47. package/dist/llm/provider.d.ts +9 -6
  48. package/dist/llm/provider.js +89 -36
  49. package/dist/main.js +326 -52
  50. package/dist/preload.js +17 -0
  51. package/dist/providers/chat-completions.adapter.js +42 -20
  52. package/dist/providers/provider-adapter.d.ts +3 -0
  53. package/dist/providers/provider-events.d.ts +7 -0
  54. package/dist/providers/provider-events.js +44 -0
  55. package/dist/providers/responses.adapter.js +1 -3
  56. package/dist/toolchain/registry/tool-registry.d.ts +13 -1
  57. package/dist/toolchain/registry/tool-registry.js +8 -0
  58. package/dist/toolchain/registry-seeder.js +51 -5
  59. package/dist/tools/index.js +11 -2
  60. package/dist/tools/nativeTools.js +5 -1
  61. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  62. package/dist/tui/src/app.js +47 -13
  63. package/dist/tui/src/render.js +23 -7
  64. package/dist/tui/src/state.js +61 -9
  65. package/dist/ui/index.html +2775 -284
  66. package/dist/ui/lucide-sprite.svg +26 -0
  67. package/dist/wsl-agent-host.bundle.cjs +1503 -214
  68. package/dist/wsl-agent-host.js +3 -0
  69. package/package.json +16 -5
@@ -193,9 +193,35 @@ async function executeToolCalls(toolCalls, context, config, signal) {
193
193
  }
194
194
  };
195
195
  if (config.toolExecution === 'parallel') {
196
- // Promise.all is the batch barrier: every launch succeeds or fails before
197
- // the provider receives any result and starts the next reasoning step.
198
- return await Promise.all(toolCalls.map(call => executeOne(call)));
196
+ // DSH 式并发安全分级:只有 concurrencySafe 工具才能与兄弟调用重叠执行,
197
+ // 有副作用的工具(缺省独占)形成串行屏障,避免盲目 Promise.all 导致
198
+ // write/edit/bash/browser_* 等副作用工具竞态。连续的并发安全调用聚合为
199
+ // 一个 Promise.all 批次;独占调用等待前面批次全部 settle 后单独执行。
200
+ const results = [];
201
+ let index = 0;
202
+ while (index < toolCalls.length) {
203
+ const call = toolCalls[index];
204
+ const tool = tools.find(candidate => candidate.name === call.name);
205
+ if (tool && tool.concurrencySafe === true) {
206
+ // 聚合连续的并发安全调用为一个并行批次。
207
+ const batch = [];
208
+ while (index < toolCalls.length) {
209
+ const candidate = toolCalls[index];
210
+ const candidateTool = tools.find(t => t.name === candidate.name);
211
+ if (!candidateTool || candidateTool.concurrencySafe !== true)
212
+ break;
213
+ batch.push(candidate);
214
+ index += 1;
215
+ }
216
+ results.push(...(await Promise.all(batch.map(c => executeOne(c)))));
217
+ }
218
+ else {
219
+ // 独占工具:串行屏障,等待前面批次全部 settle 后单独执行。
220
+ results.push(await executeOne(call));
221
+ index += 1;
222
+ }
223
+ }
224
+ return results;
199
225
  }
200
226
  const results = [];
201
227
  for (const call of toolCalls)
@@ -10,6 +10,13 @@ export type AgentTool = Tool & {
10
10
  terminate?: boolean;
11
11
  }>;
12
12
  executionMode?: 'sequential' | 'parallel';
13
+ /**
14
+ * 是否可与兄弟 tool call 并发执行(DSH isConcurrencySafe 语义的静态落地)。
15
+ * 缺省为 false(独占):有副作用的工具(write/edit/bash/browser_* 等)必须串行,
16
+ * 只有只读工具(read/glob/grep/web_search/pwd 等)显式声明 true 才允许并行。
17
+ * 这是 Newmark 从 DSH 学习的并发安全分级,避免盲目 Promise.all 导致副作用工具竞态。
18
+ */
19
+ concurrencySafe?: boolean;
13
20
  };
14
21
  export type QueueMode = 'all' | 'one-at-a-time';
15
22
  export interface ContextTransformResult {
@@ -75,6 +75,8 @@ interface KernelTool {
75
75
  terminate?: boolean;
76
76
  }>;
77
77
  executionMode?: 'sequential' | 'parallel';
78
+ /** 并发安全分级(DSH isConcurrencySafe):只读工具 true,有副作用工具缺省串行。 */
79
+ concurrencySafe?: boolean;
78
80
  }
79
81
  declare function normalizePublicProviderError(error: unknown, secrets?: unknown[]): string;
80
82
  export declare function runAgentKernel(agent: Agent): Promise<StreamToken[]>;
@@ -258,17 +258,40 @@ function normalizePublicProviderError(error, secrets = []) {
258
258
  }
259
259
  return raw.slice(0, 1_200);
260
260
  }
261
+ function throwIfKernelAborted(signal) {
262
+ if (!signal?.aborted)
263
+ return;
264
+ const reason = signal.reason;
265
+ if (reason instanceof Error) {
266
+ reason.name = 'AbortError';
267
+ throw reason;
268
+ }
269
+ const error = new Error(reason ? String(reason) : 'Agent run aborted');
270
+ error.name = 'AbortError';
271
+ throw error;
272
+ }
261
273
  async function runAgentKernel(agent) {
262
274
  const stopContextTimer = (0, performanceDiagnostics_1.performanceTimer)('context_prepare', { conversationId: agent.activeConversationId });
275
+ const processSignal = agent.activeProcessSignal();
276
+ if (processSignal?.aborted) {
277
+ stopContextTimer();
278
+ throwIfKernelAborted(processSignal);
279
+ }
263
280
  if (!agent.engineModel()) {
281
+ const message = 'No LLM configured. Add provider in Settings > Models.';
264
282
  agent.status = 'error';
265
283
  agent.saveWorkspaceConversationState();
266
- return [{ type: 'text', text: '[Error] No LLM configured. Add provider in Settings > Models.' }];
284
+ // A missing provider is a terminal run failure, not a visible assistant
285
+ // response. Throwing here lets Agent.process and ConversationKernel share
286
+ // the normal error finalization path, so GUI/TUI/CLI cannot turn this into
287
+ // a synthetic successful Build with an empty final summary.
288
+ throw new Error(message);
267
289
  }
268
290
  const [{ Agent: NativeAgent }, KernelStreamCompat] = await Promise.all([
269
291
  import('./agentKernel/index.js'),
270
292
  import('./agentKernel/stream-types.js'),
271
293
  ]);
294
+ throwIfKernelAborted(processSignal);
272
295
  const toolProvisioning = new ToolProvisionSession([], []);
273
296
  let activeToolSurfaceIdentity = '';
274
297
  let activeToolSurfaceNotice = '';
@@ -300,6 +323,7 @@ async function runAgentKernel(agent) {
300
323
  const initialToolSurface = refreshToolSurface(true);
301
324
  const assembledContext = agent.assembleContextV2(initialToolSurface.systemPromptNotice);
302
325
  const systemPrompt = assembledContext.text;
326
+ throwIfKernelAborted(processSignal);
303
327
  let providerRequestCount = 0;
304
328
  let bootstrappedCompressionAt = agent.lastCompression?.at || '';
305
329
  stopContextTimer();
@@ -320,6 +344,28 @@ async function runAgentKernel(agent) {
320
344
  kernel.state.tools = toKernelTools(agent, initialToolSurface.definitions, toolProvisioning);
321
345
  kernel.state.messages = toKernelMessages(agent);
322
346
  agent.attachAgentKernelRuntime(kernel);
347
+ let detachProcessAbort = () => { };
348
+ if (processSignal) {
349
+ const abortKernel = () => kernel.abort();
350
+ if (processSignal.aborted) {
351
+ kernel.abort();
352
+ }
353
+ else {
354
+ processSignal.addEventListener('abort', abortKernel, { once: true });
355
+ detachProcessAbort = () => processSignal.removeEventListener('abort', abortKernel);
356
+ }
357
+ }
358
+ try {
359
+ // abort() cannot cancel a NativeAgent before its internal run exists. The
360
+ // explicit check closes the remaining handoff window between attaching the
361
+ // kernel and entering kernel.prompt().
362
+ throwIfKernelAborted(processSignal);
363
+ }
364
+ catch (error) {
365
+ detachProcessAbort();
366
+ agent.attachAgentKernelRuntime(null);
367
+ throw error;
368
+ }
323
369
  const tokens = [];
324
370
  const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
325
371
  let lastAssistant = null;
@@ -433,6 +479,7 @@ async function runAgentKernel(agent) {
433
479
  }
434
480
  }
435
481
  finally {
482
+ detachProcessAbort();
436
483
  agent.attachAgentKernelRuntime(null);
437
484
  }
438
485
  agent.status = 'idle';
@@ -446,6 +493,8 @@ async function runAgentKernel(agent) {
446
493
  stream.push({ type: 'start', partial });
447
494
  let text = '';
448
495
  let thinking = '';
496
+ let thinkingStarted = false;
497
+ let thinkingRecorded = false;
449
498
  let contentIndex = 0;
450
499
  const finalContent = [];
451
500
  let textStarted = false;
@@ -467,14 +516,20 @@ async function runAgentKernel(agent) {
467
516
  const currentCompressionAt = currentAgent.lastCompression?.at || '';
468
517
  const compressionCompleted = !!currentCompressionAt && currentCompressionAt !== bootstrappedCompressionAt;
469
518
  const includeBootstrap = providerRequestCount === 0 || compressionCompleted;
519
+ // Keep the stable base prompt identical across tool sub-turns. The
520
+ // request-scoped ledger/bootstrap is needed on the first request
521
+ // (and once after compression), but re-injecting it on every round
522
+ // makes otherwise cacheable prompt prefixes look like new prompts.
470
523
  const requestSystemPrompt = [
471
524
  context.systemPrompt || '',
472
- buildRequestTaskFocus(currentAgent, context.messages, {
473
- includeBootstrap,
474
- compressionCompleted,
475
- activeTools: context.tools || [],
476
- toolCatalog: currentAgent.cachedToolDefinitions(),
477
- }),
525
+ includeBootstrap || compressionCompleted
526
+ ? buildRequestTaskFocus(currentAgent, context.messages, {
527
+ includeBootstrap,
528
+ compressionCompleted,
529
+ activeTools: context.tools || [],
530
+ toolCatalog: currentAgent.cachedToolDefinitions(),
531
+ })
532
+ : '',
478
533
  ].filter(Boolean).join('\n\n');
479
534
  providerRequestCount += 1;
480
535
  if (compressionCompleted)
@@ -485,7 +540,9 @@ async function runAgentKernel(agent) {
485
540
  messages: newmarkMessages,
486
541
  tools: context.tools || [],
487
542
  });
488
- for await (const token of currentProvider.chatStreamWithTools(currentModelName, newmarkMessages, requestSystemPrompt, temperature, maxTokens, toProviderToolDefinitions(context.tools || []), options?.signal, reasoningEffort)) {
543
+ for await (const token of currentProvider.chatStreamWithTools(currentModelName, newmarkMessages, requestSystemPrompt, temperature, maxTokens, toProviderToolDefinitions(context.tools || []), options?.signal, reasoningEffort, currentAgent.config.getBool('context', 'provider_session_id')
544
+ ? currentAgent.activeConversationId
545
+ : undefined)) {
489
546
  if (!firstTokenRecorded && ((token.type === 'text' && token.text) || (token.type === 'tool_call' && token.toolCall))) {
490
547
  firstTokenRecorded = true;
491
548
  (0, performanceDiagnostics_1.emitPerformanceEvent)({ stage: 'first_token', durationMs: Date.now() - requestStartedAt, conversationId: currentAgent.activeConversationId });
@@ -507,10 +564,20 @@ async function runAgentKernel(agent) {
507
564
  if (token.reasoningContent) {
508
565
  const delta = token.reasoningContent.slice(thinking.length);
509
566
  thinking = token.reasoningContent;
567
+ if (!thinkingStarted) {
568
+ thinkingStarted = true;
569
+ // 记录“思考中”的公开活动标记;推理文本绝不进入该事件。
570
+ currentAgent.emitWorkEvent({ type: 'thought', content: '' });
571
+ }
510
572
  if (delta) {
511
573
  stream.push({ type: 'thinking_delta', contentIndex, delta, partial: assistantMessage(model, thinking ? [{ type: 'text', text }] : [], 'stop') });
512
574
  }
513
575
  }
576
+ if (thinkingStarted && !thinkingRecorded && ((token.type === 'text' && token.text) || (token.type === 'tool_call' && token.toolCall))) {
577
+ thinkingRecorded = true;
578
+ // 思考结束:持久化完整思考过程,供 Build Block 展开查看(仍不进入聊天正文)。
579
+ currentAgent.emitWorkEvent({ type: 'thought_result', content: thinking });
580
+ }
514
581
  if (token.type === 'text' && token.text) {
515
582
  if (currentAgent.isLlmErrorText(token.text)) {
516
583
  text += token.text;
@@ -554,6 +621,10 @@ async function runAgentKernel(agent) {
554
621
  }
555
622
  if (process.env.NEWMARK_PROVIDER_DIAGNOSTICS === '1')
556
623
  console.error('[NewmarkKernel] provider-loop-complete');
624
+ if (thinkingStarted && !thinkingRecorded) {
625
+ thinkingRecorded = true;
626
+ currentAgent.emitWorkEvent({ type: 'thought_result', content: thinking });
627
+ }
557
628
  if (options?.signal?.aborted) {
558
629
  const aborted = assistantMessage(model, text ? [{ type: 'text', text }] : [], 'aborted');
559
630
  stream.push({ type: 'done', reason: 'aborted', message: aborted });
@@ -614,14 +685,12 @@ async function transformContext(agent, messages, signal) {
614
685
  // projection so the internal broker call/result and its compact catalog can
615
686
  // never be written into conversation state or revived after a reload.
616
687
  const newmarkMessages = publicHistoryFromKernelMessages(messages);
617
- const beforeCompression = JSON.stringify(newmarkMessages);
618
688
  const compressionAt = agent.lastCompression?.at || '';
619
- await agent.maybeCompress(newmarkMessages, provider, processSignal, compressionModel);
689
+ let compressed = await agent.maybeCompress(newmarkMessages, provider, processSignal, compressionModel);
620
690
  if (processSignal?.aborted)
621
691
  return messages;
622
- const primaryCompressed = JSON.stringify(newmarkMessages) !== beforeCompression;
623
- if (primaryCompressed && agent.estimateContextTokens(newmarkMessages) >= Math.floor(agent.contextWindow(compressionModel).maxTokens * 0.82)) {
624
- await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true);
692
+ if (compressed && agent.estimateContextTokens(newmarkMessages) >= Math.floor(agent.contextWindow(compressionModel).maxTokens * 0.82)) {
693
+ compressed = (await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true)) || compressed;
625
694
  }
626
695
  // Hard safety net: even a conservative worst-case token estimate must never
627
696
  // leave a request that could exceed the model's context window. The improved
@@ -631,9 +700,9 @@ async function transformContext(agent, messages, signal) {
631
700
  const windowMax = agent.contextWindow(compressionModel).maxTokens;
632
701
  const conservativeTokens = agent.estimateContextTokens(newmarkMessages);
633
702
  if (conservativeTokens >= Math.floor(windowMax * 0.9) && !processSignal?.aborted) {
634
- await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true);
703
+ compressed = (await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true)) || compressed;
635
704
  }
636
- if (JSON.stringify(newmarkMessages) === beforeCompression)
705
+ if (!compressed)
637
706
  return messages;
638
707
  const durableMessages = toKernelMessagesFromHistory(newmarkMessages, agent);
639
708
  if (agent.lastCompression?.at && agent.lastCompression.at !== compressionAt) {
@@ -684,21 +753,26 @@ function buildBuildContextBootstrap(agent, messages, options) {
684
753
  .filter(definition => toolDefinitionName(definition) !== TOOL_PROVISION_NAME)
685
754
  .map(definition => `- ${toolDefinitionName(definition)}: ${compactToolDescription(toolDefinitionDescription(definition))}`);
686
755
  const retainedMessages = messages.length;
687
- const compressionSummary = options.compressionCompleted
688
- ? compactTaskLedgerText(agent.lastCompression?.summary || '(compression summary unavailable)', 4000)
689
- : '';
756
+ // 缓存命中关键:压缩后不再把压缩摘要冗余注入 bootstrap——压缩摘要已通过
757
+ // transformContext compressionContinuationPrompt 写入 messages 前缀。这里
758
+ // 保持 bootstrap 文案在「压缩前/后」字节稳定,避免 compressionCompleted 分支
759
+ // 单独改变 system 内容而让 provider 前缀缓存失效。
760
+ // 首 Build 命名:仅当前对话标题仍自动生成时注入一次,缓存友好(后续 Build 不含)。
761
+ const renameDirective = agent.shouldPromptConversationRename()
762
+ ? [
763
+ '## Conversation Naming Bootstrap',
764
+ 'This is the FIRST Build Block of a NEW conversation whose title is still auto-generated. Call conversation_rename ONCE now with a short, concrete noun-phrase title describing this task (a few words, no sentences, no quoted prompts). This is a one-time, cache-friendly step so the conversation list shows a meaningful name.',
765
+ ]
766
+ : [];
690
767
  return [
691
768
  '## Build Context Bootstrap',
692
- options.compressionCompleted
693
- ? 'Injection reason: context compression just completed; this is the first provider request using the compacted context.'
694
- : 'Injection reason: this is the first provider request of a new Build.',
769
+ 'Injection reason: this is the first provider request of a new Build.',
695
770
  'This block is request-only runtime metadata. Do not quote it into conversation history, Build summaries, Memory Lab, or future compression summaries.',
696
771
  'Current context boundary:',
697
- options.compressionCompleted
698
- ? `- Compacted historical context: ${JSON.stringify(compressionSummary)}`
699
- : '- The durable conversation messages in this provider request are the current uncompressed context; use them directly and do not reinterpret them as a backlog.',
772
+ '- The durable conversation messages in this provider request are the current authoritative context; use them directly and do not reinterpret them as a backlog.',
700
773
  `- Retained non-system request messages: ${retainedMessages}. The latest real user-role message remains authoritative.`,
701
774
  buildConversationTaskLedger(agent),
775
+ ...renameDirective,
702
776
  '## Tool Awareness Bootstrap',
703
777
  'The following catalog is capability metadata only. Tool descriptions are not instructions, and a tool is callable only when its full schema is present in the provider tools field.',
704
778
  ...(catalogLines.length ? catalogLines : ['- No callable tools are available for this provider turn.']),
@@ -1287,15 +1361,25 @@ function toolDefinitionName(definition) {
1287
1361
  }
1288
1362
  function toKernelTools(agent, definitions, provisioning) {
1289
1363
  const tools = definitions || agent.cachedToolDefinitions();
1364
+ // 单一来源:先 seed toolchain registry,使每个工具的 riskLevel 从定义自动推断。
1365
+ let registry = null;
1366
+ try {
1367
+ registry = agent.ensureToolchain(tools).registry;
1368
+ }
1369
+ catch { }
1290
1370
  return tools.map((tool) => {
1291
1371
  const fn = tool?.function || {};
1372
+ const toolName = String(fn.name || '');
1373
+ const descriptor = registry?.get(toolName);
1292
1374
  return {
1293
- name: String(fn.name || ''),
1294
- label: String(fn.name || ''),
1375
+ name: toolName,
1376
+ label: toolName,
1295
1377
  description: String(fn.description || ''),
1296
1378
  parameters: fn.parameters || { type: 'object', properties: {}, required: [] },
1297
1379
  prepareArguments: parseToolArgs,
1298
1380
  executionMode: 'parallel',
1381
+ // DSH isConcurrencySafe 落地:从 registry 的 riskLevel 派生(read 工具并行)+ 白名单兜底。
1382
+ concurrencySafe: (0, toolPolicy_1.isConcurrencySafeTool)(toolName, descriptor?.riskLevel),
1299
1383
  execute: async (_toolCallId, params, signal) => {
1300
1384
  if (signal?.aborted)
1301
1385
  throw abortError();
@@ -1328,7 +1412,7 @@ function toKernelTools(agent, definitions, provisioning) {
1328
1412
  }
1329
1413
  const visionImage = visualFallbackImageInput(agent, name, rawText);
1330
1414
  const directImage = imageInspectDataUrl(name, rawText);
1331
- const text = sanitizeVisualToolText(name, rawText);
1415
+ const text = spillOversizedToolResult(agent, name, sanitizeVisualToolText(name, rawText));
1332
1416
  const content = [{ type: 'text', text }];
1333
1417
  if (visionImage.imagePath)
1334
1418
  content.push({ type: 'image', imagePath: visionImage.imagePath, mimeType: imageMimeForPath(visionImage.imagePath) });
@@ -1369,6 +1453,51 @@ function toolResultIndicatesFailure(text) {
1369
1453
  return false;
1370
1454
  }
1371
1455
  }
1456
+ /** 内联回传 model 的单个工具结果字符上限(约 6000 token)。
1457
+ * 超过则裁剪为「头部结论 + 尾部证据」,避免巨型 read/grep/bash 输出整段
1458
+ * 内联进 context 撑爆窗口(DSH tool-result-pruner 的内联落地)。 */
1459
+ const INLINE_TOOL_RESULT_MAX_CHARS = 24000;
1460
+ /** 有界处理内联工具结果。只对会产出大文本的工具生效;带视觉/结构化 JSON
1461
+ * 的工具结果保持原样(截断/落盘会破坏 JSON 结构)。 */
1462
+ function boundInlineToolResult(name, text) {
1463
+ const value = String(text || '');
1464
+ if (value.length <= INLINE_TOOL_RESULT_MAX_CHARS)
1465
+ return value;
1466
+ // 结构化结果(JSON/视觉/浏览器/子代理/计划等)不可安全截断,保持原样。
1467
+ if (['computer_use', 'browser_use', 'pdf_read', 'image_inspect', 'image_display', 'task', 'subagent_send', 'subagent_result', 'subagent_read', 'linked_plan', 'question'].includes(name)) {
1468
+ return value;
1469
+ }
1470
+ const headChars = Math.floor(INLINE_TOOL_RESULT_MAX_CHARS * 0.6);
1471
+ const tailChars = Math.max(0, INLINE_TOOL_RESULT_MAX_CHARS - headChars - 48);
1472
+ const head = value.slice(0, headChars).trimEnd();
1473
+ const tail = value.slice(-tailChars).trimStart();
1474
+ return `${head}\n\n[...tool result truncated: ${value.length - headChars - tailChars} chars omitted from middle...]\n\n${tail}`;
1475
+ }
1476
+ /**
1477
+ * 超大工具结果的完整处理:落盘完整内容(不进上下文),上下文只保留一个
1478
+ * tiny 引用标记 + 头部预览。Agent 之后可调用 compress_tool_result(artifact_id)
1479
+ * 把完整内容恢复为格式保留的压缩摘要;不调用则上下文中仅剩该引用(等价于
1480
+ * 截断,但完整内容在盘上可恢复,不丢失)。
1481
+ */
1482
+ function spillOversizedToolResult(agent, name, text) {
1483
+ const value = String(text || '');
1484
+ if (value.length <= INLINE_TOOL_RESULT_MAX_CHARS)
1485
+ return value;
1486
+ // 结构化结果不可安全落盘引用(破坏 JSON 结构),保持原样。
1487
+ if (['computer_use', 'browser_use', 'pdf_read', 'image_inspect', 'image_display', 'task', 'subagent_send', 'subagent_result', 'subagent_read', 'linked_plan', 'question'].includes(name)) {
1488
+ return value;
1489
+ }
1490
+ const artifactId = agent.storeToolResultArtifact(name, value);
1491
+ const headPreview = value.slice(0, 800).trimEnd();
1492
+ return [
1493
+ `[oversized_tool_result tool="${name}" artifact_id="${artifactId}" chars="${value.length}"]`,
1494
+ 'The full result was written out of context. The preview below is truncated to 800 chars.',
1495
+ 'Call compress_tool_result with this artifact_id to recover the full result as a format-preserving summary, or leave it truncated.',
1496
+ '',
1497
+ headPreview,
1498
+ '...(preview truncated)',
1499
+ ].join('\n');
1500
+ }
1372
1501
  function sanitizeVisualToolText(name, text) {
1373
1502
  if (name !== 'computer_use' && name !== 'browser_use' && name !== 'pdf_read' && name !== 'image_inspect')
1374
1503
  return text;
@@ -1483,6 +1612,14 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal) {
1483
1612
  return agent.handleSubagentResultEnvelope(args).output;
1484
1613
  if (name === 'subagent_close')
1485
1614
  return agent.handleSubagentCloseEnvelope(args).output;
1615
+ if (name === 'branch_list')
1616
+ return agent.handleBranchList(args).output;
1617
+ if (name === 'branch_send')
1618
+ return agent.handleBranchSend(args).output;
1619
+ if (name === 'branch_read')
1620
+ return agent.handleBranchRead(args).output;
1621
+ if (name === 'branch_create')
1622
+ return agent.handleBranchCreate(args).output;
1486
1623
  if (name === 'linked_plan')
1487
1624
  return agent.handleLinkedPlanTool(args);
1488
1625
  if (name === 'build_history_query')
@@ -1491,6 +1628,16 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal) {
1491
1628
  return (await agent.handleContextCompress(args, signal)).output;
1492
1629
  if (name === 'context_history_manage')
1493
1630
  return agent.handleContextHistoryManage(args).output;
1631
+ if (name === 'compress_tool_result')
1632
+ return (await agent.handleCompressToolResult(args, signal)).output;
1633
+ if (name === 'background_tool')
1634
+ return (await agent.handleBackgroundTool(args, signal)).output;
1635
+ if (name === 'read_tool_result')
1636
+ return agent.handleReadToolResult(args).output;
1637
+ if (name === 'goal_manage')
1638
+ return agent.handleGoalManage(args).output;
1639
+ if (name === 'conversation_rename')
1640
+ return agent.handleConversationRename(args).output;
1494
1641
  if (name === 'question') {
1495
1642
  if (agent.config.getStr('agent', 'option_feedback') === 'fully_autonomous')
1496
1643
  return '[question] Disabled by fully_autonomous option feedback.';
@@ -81,7 +81,10 @@ export declare class ConfigManager {
81
81
  rootPath: string;
82
82
  private config;
83
83
  private workspaceOverrides;
84
- constructor(rootPath: string);
84
+ private readonly readOnly;
85
+ constructor(rootPath: string, options?: {
86
+ readOnly?: boolean;
87
+ });
85
88
  reload(): void;
86
89
  private load;
87
90
  get<T = unknown>(section: string, key: string): T | undefined;
@@ -153,7 +156,9 @@ export interface ModelValidationSummary {
153
156
  message: string;
154
157
  };
155
158
  }
156
- export declare function ensureRootConfig(rootPath: string): void;
159
+ export declare function ensureRootConfig(rootPath: string, options?: {
160
+ readOnly?: boolean;
161
+ }): void;
157
162
  export declare function inferProviderProtocol(name: string, baseUrl: string): ProviderProtocol;
158
163
  export declare function normalizeProviderProtocol(value: unknown, name: string, baseUrl: string): ProviderProtocol;
159
164
  export declare function defaultProviderBaseUrl(protocol: ProviderProtocol): string;
@@ -64,8 +64,10 @@ class ConfigManager {
64
64
  rootPath;
65
65
  config;
66
66
  workspaceOverrides;
67
- constructor(rootPath) {
67
+ readOnly;
68
+ constructor(rootPath, options = {}) {
68
69
  this.rootPath = rootPath;
70
+ this.readOnly = options.readOnly === true;
69
71
  this.workspaceOverrides = new Map();
70
72
  this.config = this.load();
71
73
  }
@@ -80,6 +82,8 @@ class ConfigManager {
80
82
  const raw = JSON.parse(readJsonText(cp));
81
83
  const normalized = normalizeConfigShape(raw, true);
82
84
  if (isCorruptConfig(raw, normalized)) {
85
+ if (this.readOnly)
86
+ return defaultConfig();
83
87
  this.backupConfig(cp, 'invalid-shape');
84
88
  return this.writeRecoveredConfig(cp);
85
89
  }
@@ -87,7 +91,8 @@ class ConfigManager {
87
91
  // Provider ids are routing identities, so legacy/malformed catalogs must
88
92
  // not wait for an unrelated settings save before becoming collision-safe.
89
93
  try {
90
- fs.writeFileSync(cp, JSON.stringify(normalized, null, 2), 'utf-8');
94
+ if (!this.readOnly)
95
+ fs.writeFileSync(cp, JSON.stringify(normalized, null, 2), 'utf-8');
91
96
  }
92
97
  catch {
93
98
  // A read-only root may still be used for this process. The normalized
@@ -97,6 +102,8 @@ class ConfigManager {
97
102
  return normalized;
98
103
  }
99
104
  catch {
105
+ if (this.readOnly)
106
+ return defaultConfig();
100
107
  this.backupConfig(cp, 'invalid-json');
101
108
  return this.writeRecoveredConfig(cp);
102
109
  }
@@ -146,10 +153,14 @@ class ConfigManager {
146
153
  this.config[section][key] = { value: normalizedValue };
147
154
  }
148
155
  save() {
156
+ if (this.readOnly)
157
+ return;
149
158
  const j = JSON.stringify(this.config, null, 2);
150
159
  fs.writeFileSync(path.join(this.rootPath, 'config.json'), j, 'utf-8');
151
160
  }
152
161
  saveTo(targetPath) {
162
+ if (this.readOnly)
163
+ return;
153
164
  const j = JSON.stringify(this.config, null, 2);
154
165
  fs.writeFileSync(targetPath, j, 'utf-8');
155
166
  }
@@ -393,12 +404,16 @@ class ConfigManager {
393
404
  return providers;
394
405
  }
395
406
  writeRecoveredConfig(configPath) {
407
+ if (this.readOnly)
408
+ return defaultConfig();
396
409
  const config = loadExampleConfig();
397
410
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
398
411
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
399
412
  return config;
400
413
  }
401
414
  backupConfig(configPath, reason) {
415
+ if (this.readOnly)
416
+ return;
402
417
  try {
403
418
  if (!fs.existsSync(configPath))
404
419
  return;
@@ -412,13 +427,13 @@ class ConfigManager {
412
427
  }
413
428
  }
414
429
  exports.ConfigManager = ConfigManager;
415
- function ensureRootConfig(rootPath) {
430
+ function ensureRootConfig(rootPath, options = {}) {
416
431
  const configPath = path.join(rootPath, 'config.json');
417
432
  if (fs.existsSync(configPath)) {
418
- new ConfigManager(rootPath);
433
+ new ConfigManager(rootPath, options);
419
434
  return;
420
435
  }
421
- new ConfigManager(rootPath).save();
436
+ new ConfigManager(rootPath, options).save();
422
437
  }
423
438
  function normalizeConfigShape(raw, withDefaults) {
424
439
  const base = withDefaults ? defaultConfig() : {};
@@ -827,7 +842,10 @@ function defaultConfig() {
827
842
  general: {
828
843
  tone: { _description: "Conversation style", _type: "choice", _values: ["strict_simple", "casual_friendly"], value: "strict_simple" },
829
844
  language: { _description: "Default language", _type: "choice", _values: ["en", "zh", "auto"], value: "auto" },
830
- close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "minimize" },
845
+ // A first-run desktop window must have a deterministic close/exit
846
+ // contract. Users who explicitly choose minimize-to-tray keep that
847
+ // choice, but a fresh install must not hide the process on OS close.
848
+ close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "exit" },
831
849
  default_input: { _description: "Default input mode", _type: "choice", _values: ["guide", "next"], value: "guide" },
832
850
  auto_archive_on_close: { _description: "Auto archive on close", _type: "boolean", value: true },
833
851
  },
@@ -44,6 +44,10 @@ export interface ConversationKernelRunOptions {
44
44
  inputMode: 'guide' | 'next';
45
45
  engine: string;
46
46
  }
47
+ export interface ConversationContextCompressOptions {
48
+ keepRecent?: number;
49
+ force?: boolean;
50
+ }
47
51
  export interface ConversationKernelRunResult {
48
52
  tokens: Array<{
49
53
  type: string;
@@ -194,6 +198,7 @@ export declare class ConversationKernel {
194
198
  checkpointed: boolean;
195
199
  at: string;
196
200
  };
201
+ compressContext(target: ConversationTargetInput, options?: ConversationContextCompressOptions): Promise<Record<string, unknown>>;
197
202
  rateAutoRoute(target: ConversationTargetInput, score: number, expectedRouteId?: string): AutoRouteRatingResult;
198
203
  setWorkRunExpanded(target: ConversationTargetInput, runId: string, expanded: boolean): boolean;
199
204
  setInputMode(target: ConversationTargetInput, mode: string): 'guide' | 'next';
@@ -304,6 +304,35 @@ class ConversationKernel {
304
304
  at: new Date().toISOString(),
305
305
  };
306
306
  }
307
+ async compressContext(target, options = {}) {
308
+ const normalized = this.normalizeTarget(target);
309
+ const runtime = this.findRuntime(normalized);
310
+ if (runtime?.activePromise) {
311
+ return { ok: false, error: 'Context compression is unavailable while this conversation is running.' };
312
+ }
313
+ const runner = runtime?.runner || this.createRunner(normalized);
314
+ const result = await runner.handleContextCompress(JSON.stringify({
315
+ keep_recent: options.keepRecent,
316
+ force: options.force !== false,
317
+ }));
318
+ let payload = {};
319
+ try {
320
+ const parsed = JSON.parse(result.output || '{}');
321
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
322
+ payload = parsed;
323
+ }
324
+ catch {
325
+ payload = { output: result.output };
326
+ }
327
+ return {
328
+ ...payload,
329
+ ok: result.ok && payload.ok !== false,
330
+ error: result.error,
331
+ contextWindow: runner.contextWindow(),
332
+ contextCompression: runner.lastCompression,
333
+ displayHistory: { untouched: true, messageCount: runner.chatMessages.length },
334
+ };
335
+ }
307
336
  rateAutoRoute(target, score, expectedRouteId = '') {
308
337
  const runtime = this.findRuntime(target);
309
338
  if (!runtime)
@@ -499,7 +528,7 @@ class ConversationKernel {
499
528
  stopped = true;
500
529
  }
501
530
  else {
502
- runtime.runner.finishConversationWorkRun(runId, 'error');
531
+ runtime.runner.finishConversationWorkRun(runId, 'error', undefined, error instanceof Error ? error.message : String(error));
503
532
  throw error;
504
533
  }
505
534
  }