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
package/dist/preload.js CHANGED
@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('api', {
12
12
  sendMessage: (message, target) => ipcRenderer.invoke('agent:send', message, target),
13
13
  enqueueGuide: (envelope) => ipcRenderer.invoke('agent:enqueueGuide', envelope),
14
14
  checkpointConversation: (request) => ipcRenderer.invoke('agent:checkpointConversation', request),
15
+ compressContext: (request) => ipcRenderer.invoke('agent:compressContext', request),
15
16
  rateAutoRoute: (request) => ipcRenderer.invoke('agent:rateAutoRoute', request),
16
17
  stopConversation: (request) => ipcRenderer.invoke('agent:stopConversation', request),
17
18
  setWorkRunExpanded: (request) => ipcRenderer.invoke('agent:setWorkRunExpanded', request),
@@ -32,12 +33,16 @@ contextBridge.exposeInMainWorld('api', {
32
33
  getConversationPlan: (conversationId) => ipcRenderer.invoke('agent:getConversationPlan', conversationId),
33
34
  updateConversationPlan: (plan, conversationId) => ipcRenderer.invoke('agent:updateConversationPlan', plan, conversationId),
34
35
  setConversationPinned: (id, pinned) => ipcRenderer.invoke('agent:setConversationPinned', id, pinned),
36
+ setConversationBranchCommunication: (target, enabled) => ipcRenderer.invoke('agent:setConversationBranchCommunication', target, enabled),
35
37
  renameConversation: (id, title) => ipcRenderer.invoke('agent:renameConversation', id, title),
36
38
  reorderConversations: (ids) => ipcRenderer.invoke('agent:reorderConversations', ids),
37
39
  browserRegisterGuest: (guestContentsId, target) => ipcRenderer.invoke('browser:registerGuest', guestContentsId, target),
38
40
  onBrowserEnsureGuest: (callback) => {
39
41
  ipcRenderer.on('browser:ensureGuest', (_event, target) => callback(target));
40
42
  },
43
+ onKeyboardCommand: (callback) => {
44
+ ipcRenderer.on('keyboard:command', (_event, payload) => callback(payload));
45
+ },
41
46
  browserControl: (request) => ipcRenderer.invoke('browser:control', request),
42
47
  computerUseState: (target) => ipcRenderer.invoke('agent:computerUseState', target),
43
48
  setComputerUseEnabled: (target, enabled) => ipcRenderer.invoke('agent:setComputerUseEnabled', target, enabled),
@@ -72,6 +77,10 @@ contextBridge.exposeInMainWorld('api', {
72
77
  readWorkspacePrompt: () => ipcRenderer.invoke('workspace:readPrompt'),
73
78
  saveWorkspacePrompt: (content) => ipcRenderer.invoke('workspace:savePrompt', content),
74
79
  editorComplete: (request) => ipcRenderer.invoke('agent:editorComplete', request),
80
+ editorCompleteCancel: () => ipcRenderer.invoke('agent:editorCompleteCancel'),
81
+ onEditorCompletionDelta: (callback) => {
82
+ ipcRenderer.on('agent:editorCompletionDelta', (_event, payload) => callback(payload));
83
+ },
75
84
  editorAssist: (request) => ipcRenderer.invoke('agent:editorAssist', request),
76
85
  filePathForFile: (file) => {
77
86
  try {
@@ -85,6 +94,7 @@ contextBridge.exposeInMainWorld('api', {
85
94
  selectFolder: () => ipcRenderer.invoke('dialog:selectFolder'),
86
95
  executeBash: (cmd, shell, cwd) => ipcRenderer.invoke('agent:executeBash', cmd, shell, cwd),
87
96
  openExternal: (path) => ipcRenderer.invoke('agent:openExternal', path),
97
+ openWebUrl: (url) => ipcRenderer.invoke('app:openWebUrl', url),
88
98
  selectWorkspace: (id) => ipcRenderer.invoke('agent:selectWorkspace', id),
89
99
  createWorkspace: (name) => ipcRenderer.invoke('agent:createWorkspace', name),
90
100
  createExternalWorkspace: (name, dirPath) => ipcRenderer.invoke('agent:createExternalWorkspace', name, dirPath),
@@ -113,6 +123,7 @@ contextBridge.exposeInMainWorld('api', {
113
123
  removeSkill: (name) => ipcRenderer.invoke('skills:remove', name),
114
124
  refreshSkills: () => ipcRenderer.invoke('skills:refresh'),
115
125
  listMcpServers: () => ipcRenderer.invoke('mcp:list'),
126
+ discoverDshCompatibility: () => ipcRenderer.invoke('dsh:discover'),
116
127
  upsertMcpServer: (input) => ipcRenderer.invoke('mcp:upsert', input),
117
128
  setMcpServerEnabled: (id, enabled) => ipcRenderer.invoke('mcp:setEnabled', id, enabled),
118
129
  removeMcpServer: (id) => ipcRenderer.invoke('mcp:remove', id),
@@ -173,6 +184,12 @@ contextBridge.exposeInMainWorld('api', {
173
184
  onAgentWorkEvent: (callback) => {
174
185
  ipcRenderer.on('agent:workEvent', callback);
175
186
  },
187
+ onWorkspaceChanged: (callback) => {
188
+ ipcRenderer.on('workspace:changed', (_event, payload) => callback(payload));
189
+ },
190
+ removeWorkspaceChangedListener: () => {
191
+ ipcRenderer.removeAllListeners('workspace:changed');
192
+ },
176
193
  removeAgentWorkEventListener: () => {
177
194
  ipcRenderer.removeAllListeners('agent:workEvent');
178
195
  },
@@ -48,6 +48,10 @@ class ChatCompletionsAdapter {
48
48
  };
49
49
  if (request.reasoningEffort)
50
50
  body.reasoning_effort = request.reasoningEffort;
51
+ // 会话标识透传:仅当上层(支持 session_id 语义的 provider)显式填充时写进
52
+ // body,否则省略,避免严格 API 拒绝未知字段。
53
+ if (request.sessionId)
54
+ body.session_id = request.sessionId;
51
55
  const base = request.baseUrl.replace(/\/+$/, '');
52
56
  return {
53
57
  url: `${base}/chat/completions`,
@@ -90,17 +94,16 @@ class ChatCompletionsAdapter {
90
94
  }
91
95
  const decoder = new TextDecoder();
92
96
  let buffer = '';
93
- let currentToolCall = null;
97
+ const toolCalls = new Map();
98
+ const toolCallOrder = [];
99
+ let syntheticToolIndex = 0;
100
+ let lastToolIndex = 0;
94
101
  let contentPolicyBlocked = false;
95
102
  let emittedContent = false;
96
103
  let emittedTool = false;
97
104
  try {
98
105
  while (true) {
99
- if (signal.aborted)
100
- throw (0, provider_events_1.providerAbortError)(signal);
101
- const readPromise = reader.read();
102
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Stream read timeout')), 30000));
103
- const { done, value } = await Promise.race([readPromise, timeoutPromise]);
106
+ const { done, value } = await (0, provider_events_1.readProviderStreamChunk)(reader, signal);
104
107
  if (done)
105
108
  break;
106
109
  buffer += decoder.decode(value, { stream: true });
@@ -141,32 +144,51 @@ class ChatCompletionsAdapter {
141
144
  emittedContent = true;
142
145
  yield { type: 'text.delta', delta: textDelta };
143
146
  }
144
- const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
145
- for (const raw of toolCalls) {
147
+ const deltaToolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
148
+ for (const raw of deltaToolCalls) {
146
149
  const tc = raw;
147
150
  const fn = tc.function && typeof tc.function === 'object' ? tc.function : {};
148
- if (tc.id) {
149
- if (currentToolCall) {
150
- emittedTool = true;
151
- yield { type: 'tool_call.completed', id: currentToolCall.id, name: currentToolCall.name, arguments: currentToolCall.arguments };
152
- }
151
+ const rawIndex = Number(tc.index);
152
+ const index = Number.isInteger(rawIndex) && rawIndex >= 0
153
+ ? rawIndex
154
+ : (tc.id ? syntheticToolIndex++ : lastToolIndex);
155
+ lastToolIndex = index;
156
+ let currentToolCall = toolCalls.get(index);
157
+ if (!currentToolCall && tc.id) {
153
158
  currentToolCall = {
154
159
  id: String(tc.id || ''),
155
160
  name: (0, chat_messages_1.openAIToolName)(String(fn.name || '')),
156
- arguments: String(fn.arguments || ''),
161
+ argumentParts: [],
157
162
  };
163
+ toolCalls.set(index, currentToolCall);
164
+ toolCallOrder.push(index);
158
165
  yield { type: 'tool_call.started', id: currentToolCall.id, name: currentToolCall.name };
159
166
  }
160
- else if (fn.arguments && currentToolCall) {
161
- currentToolCall.arguments += String(fn.arguments);
162
- yield { type: 'tool_call.arguments.delta', id: currentToolCall.id, delta: String(fn.arguments) };
167
+ if (currentToolCall && fn.name && !currentToolCall.name)
168
+ currentToolCall.name = (0, chat_messages_1.openAIToolName)(String(fn.name));
169
+ if (currentToolCall && fn.arguments !== undefined && fn.arguments !== null) {
170
+ const argumentDelta = String(fn.arguments);
171
+ if (argumentDelta) {
172
+ currentToolCall.argumentParts.push(argumentDelta);
173
+ yield { type: 'tool_call.arguments.delta', id: currentToolCall.id, delta: argumentDelta };
174
+ }
163
175
  }
164
176
  }
165
177
  }
166
178
  }
167
- if (currentToolCall && currentToolCall.arguments) {
168
- emittedTool = true;
169
- yield { type: 'tool_call.completed', id: currentToolCall.id, name: currentToolCall.name, arguments: currentToolCall.arguments };
179
+ if (toolCallOrder.length) {
180
+ for (const index of toolCallOrder) {
181
+ const currentToolCall = toolCalls.get(index);
182
+ if (!currentToolCall)
183
+ continue;
184
+ emittedTool = true;
185
+ yield {
186
+ type: 'tool_call.completed',
187
+ id: currentToolCall.id,
188
+ name: currentToolCall.name,
189
+ arguments: currentToolCall.argumentParts.join(''),
190
+ };
191
+ }
170
192
  }
171
193
  else if (!emittedContent && !emittedTool && contentPolicyBlocked) {
172
194
  yield { type: 'response.failed', error: '[Error] Content policy refusal (content_filter).' };
@@ -55,6 +55,9 @@ export interface NormalizedAgentRequest {
55
55
  reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
56
56
  apiKey: string;
57
57
  baseUrl: string;
58
+ /** 可选的会话标识。仅当目标 provider 显式支持 session_id 语义的上下文缓存
59
+ * 时才由上层填充;adapter 在存在时透传,否则省略该字段(避免严格 API 拒绝未知字段)。 */
60
+ sessionId?: string;
58
61
  }
59
62
  export interface TokenEstimate {
60
63
  inputTokens: number;
@@ -12,6 +12,13 @@ export declare function defaultProviderTransport(request: SerializedProviderRequ
12
12
  * (`name === 'AbortError'`, preserves the abort reason when present).
13
13
  */
14
14
  export declare function providerAbortError(signal?: AbortSignal): Error;
15
+ export declare function providerStreamTimeoutError(timeoutMs: number): Error;
16
+ /**
17
+ * Read one SSE chunk with both user cancellation and an inactivity deadline.
18
+ * Cancelling the reader is important: rejecting the race alone leaves the
19
+ * provider socket alive and lets later requests accumulate behind it.
20
+ */
21
+ export declare function readProviderStreamChunk(reader: ReadableStreamDefaultReader<Uint8Array>, signal: AbortSignal, timeoutMs?: number): Promise<ReadableStreamReadResult<Uint8Array>>;
15
22
  export declare function parseProviderSse(raw: string): Array<{
16
23
  event?: string;
17
24
  data: string;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defaultProviderTransport = defaultProviderTransport;
4
4
  exports.providerAbortError = providerAbortError;
5
+ exports.providerStreamTimeoutError = providerStreamTimeoutError;
6
+ exports.readProviderStreamChunk = readProviderStreamChunk;
5
7
  exports.parseProviderSse = parseProviderSse;
6
8
  exports.isContentPolicyBlocked = isContentPolicyBlocked;
7
9
  exports.normalizeProviderUsage = normalizeProviderUsage;
@@ -34,6 +36,48 @@ function providerAbortError(signal) {
34
36
  error.name = 'AbortError';
35
37
  return error;
36
38
  }
39
+ function providerStreamTimeoutError(timeoutMs) {
40
+ const error = new Error('Stream read timeout');
41
+ error.name = 'TimeoutError';
42
+ error.message = `Stream read timeout after ${timeoutMs}ms`;
43
+ return error;
44
+ }
45
+ /**
46
+ * Read one SSE chunk with both user cancellation and an inactivity deadline.
47
+ * Cancelling the reader is important: rejecting the race alone leaves the
48
+ * provider socket alive and lets later requests accumulate behind it.
49
+ */
50
+ async function readProviderStreamChunk(reader, signal, timeoutMs = 30_000) {
51
+ if (signal.aborted)
52
+ throw providerAbortError(signal);
53
+ let timer;
54
+ let onAbort;
55
+ const abortPromise = new Promise((_, reject) => {
56
+ onAbort = () => reject(providerAbortError(signal));
57
+ signal.addEventListener('abort', onAbort, { once: true });
58
+ });
59
+ const timeoutPromise = new Promise((_, reject) => {
60
+ timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
61
+ });
62
+ try {
63
+ return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
64
+ }
65
+ catch (error) {
66
+ if (signal.aborted || (error instanceof Error && error.name === 'TimeoutError')) {
67
+ try {
68
+ await reader.cancel(error);
69
+ }
70
+ catch { }
71
+ }
72
+ throw error;
73
+ }
74
+ finally {
75
+ if (timer)
76
+ clearTimeout(timer);
77
+ if (onAbort)
78
+ signal.removeEventListener('abort', onAbort);
79
+ }
80
+ }
37
81
  function parseProviderSse(raw) {
38
82
  const events = [];
39
83
  for (const block of String(raw || '').replace(/\r\n/g, '\n').split(/\n\n+/)) {
@@ -132,9 +132,7 @@ class ResponsesAdapter {
132
132
  let streamError = '';
133
133
  try {
134
134
  while (true) {
135
- if (signal.aborted)
136
- throw (0, provider_events_1.providerAbortError)(signal);
137
- const { done, value } = await reader.read();
135
+ const { done, value } = await (0, provider_events_1.readProviderStreamChunk)(reader, signal);
138
136
  if (done)
139
137
  break;
140
138
  buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n');
@@ -1,4 +1,4 @@
1
- import { ToolDescriptor, ToolIdempotency, RiskLevel } from '../../context/domain/types';
1
+ import { ToolDescriptor, ToolIdempotency, RiskLevel, ToolExecuteFn, ToolConcurrencySafeFn, ToolRenderFn, ToolPresentationMetaFn, ToolFinalizeContentFn, ToolPresentCallFn, ToolPresentResultFn } from '../../context/domain/types';
2
2
  export interface ToolDescriptorInput {
3
3
  toolId: string;
4
4
  capabilityId: string;
@@ -15,6 +15,18 @@ export interface ToolDescriptorInput {
15
15
  supportedScopes?: string[];
16
16
  cacheGroup?: string;
17
17
  implementationHash?: string;
18
+ /** DSH ToolDefinition.execute:命令式功能实现引用(cordis 核功能承载)。 */
19
+ execute?: ToolExecuteFn;
20
+ /** DSH ToolDefinition.isConcurrencySafe:运行时并发分类。 */
21
+ isConcurrencySafe?: ToolConcurrencySafeFn;
22
+ /** DSH ToolOutputDefinition.render / presentationMeta。 */
23
+ render?: ToolRenderFn;
24
+ presentationMeta?: ToolPresentationMetaFn;
25
+ /** DSH ToolDefinition.finalizeContent / timeoutMs / presentCall / presentResult。 */
26
+ finalizeContent?: ToolFinalizeContentFn;
27
+ timeoutMs?: number;
28
+ presentCall?: ToolPresentCallFn;
29
+ presentResult?: ToolPresentResultFn;
18
30
  }
19
31
  /**
20
32
  * Tool Registry: the authoritative set of ToolDescriptors. Schemas are
@@ -28,6 +28,14 @@ class ToolRegistry {
28
28
  implementationHash: input.implementationHash,
29
29
  cacheGroup: input.cacheGroup || `${input.namespace}.${input.name}`,
30
30
  enabled: true,
31
+ execute: input.execute,
32
+ isConcurrencySafe: input.isConcurrencySafe,
33
+ render: input.render,
34
+ presentationMeta: input.presentationMeta,
35
+ finalizeContent: input.finalizeContent,
36
+ timeoutMs: input.timeoutMs,
37
+ presentCall: input.presentCall,
38
+ presentResult: input.presentResult,
31
39
  };
32
40
  this.tools.set(input.toolId, descriptor);
33
41
  return descriptor;
@@ -60,15 +60,28 @@ function inferRiskLevel(name, description, annotations) {
60
60
  return 'external';
61
61
  if (READ_TOOL_PATTERN.test(name))
62
62
  return 'read';
63
+ // DSH 读工具命名惯例(get_/list_/query_/inspect_/read_ 前缀):这些动词本质只读,
64
+ // 避免被启发式误判为 write(例如 DSH 的 get_goal / cordis_inspect_list)。
65
+ if (/^(get_|list_|query_|inspect_|read_)/.test(name) && !/_(create|update|set|write|delete|remove|run|execute|send|save|push|edit|toggle|define|stop|start)$/.test(name))
66
+ return 'read';
63
67
  return 'write';
64
68
  }
65
69
  function inferIdempotency(name) {
66
- if (/^(bash|computer_use|browser_use|run|exec|execute|task)$/.test(name))
70
+ // shell 命令工具每次执行都可能改变外部状态,本质非幂等(DSH pwsh/bash/terminal 等)。
71
+ if (/^(bash|pwsh|powershell|cmd|shell|terminal|computer_use|browser_use|run|exec|execute|task)$/.test(name))
67
72
  return 'non_idempotent';
68
73
  if (/^(write|edit|append|send|save|create|update|set|put|register|patch)/.test(name))
69
74
  return 'conditionally_idempotent';
70
75
  return undefined;
71
76
  }
77
+ /** 从真实 tool description 提取简洁 shortDescription(首句或截断),保 cordis 核可读。 */
78
+ function compactDescription(description, fallback) {
79
+ const clean = String(description || '').replace(/\s+/g, ' ').trim();
80
+ if (!clean)
81
+ return fallback;
82
+ const firstSentence = clean.split(/(?<=[.!?])\s+/)[0] || clean;
83
+ return firstSentence.slice(0, 120);
84
+ }
72
85
  function resolveDefinition(definition) {
73
86
  if (!definition || typeof definition !== 'object')
74
87
  return null;
@@ -82,11 +95,35 @@ function resolveDefinition(definition) {
82
95
  };
83
96
  }
84
97
  if (typeof record.name === 'string') {
98
+ // 兼容两种字段名:Newmark 的 SeededToolDefinition 用 inputSchema,
99
+ // DSH 的 ToolSchema 用 parameters(dsh-llm ToolSchema:{name, description, parameters})。
100
+ const rawParameters = record.inputSchema ?? record.parameters;
101
+ const rawExecute = record.execute;
102
+ const rawConcurrencySafe = record.isConcurrencySafe;
103
+ // DSH ToolOutputDefinition 是嵌套对象 {schema, render, presentationMeta};
104
+ // outputSchema 从 output.schema 提取。
105
+ const rawOutput = record.output;
106
+ const outputSchema = record.outputSchema ?? rawOutput?.schema;
107
+ const render = rawOutput?.render;
108
+ const presentationMeta = rawOutput?.presentationMeta;
109
+ const finalizeContent = record.finalizeContent;
110
+ const timeoutMs = record.timeoutMs;
111
+ const presentCall = record.presentCall;
112
+ const presentResult = record.presentResult;
85
113
  return {
86
114
  name: record.name,
87
115
  description: typeof record.description === 'string' ? record.description : '',
88
- parameters: record.inputSchema,
116
+ parameters: rawParameters,
117
+ outputSchema,
89
118
  annotations: record.annotations,
119
+ execute: typeof rawExecute === 'function' ? rawExecute : undefined,
120
+ isConcurrencySafe: typeof rawConcurrencySafe === 'function' ? rawConcurrencySafe : undefined,
121
+ render: typeof render === 'function' ? render : undefined,
122
+ presentationMeta: typeof presentationMeta === 'function' ? presentationMeta : undefined,
123
+ finalizeContent: typeof finalizeContent === 'function' ? finalizeContent : undefined,
124
+ timeoutMs: typeof timeoutMs === 'number' ? timeoutMs : undefined,
125
+ presentCall: typeof presentCall === 'function' ? presentCall : undefined,
126
+ presentResult: typeof presentResult === 'function' ? presentResult : undefined,
90
127
  };
91
128
  }
92
129
  return null;
@@ -135,7 +172,7 @@ function seedToolchainFromDefinitions(definitions, options) {
135
172
  if (riskLevel === 'destructive' || (riskLevel === 'external' && entry.input.riskLevel !== 'destructive')) {
136
173
  entry.input.riskLevel = riskLevel;
137
174
  }
138
- entry.resolved.push({ name: definition.name, riskLevel, parameters: definition.parameters });
175
+ entry.resolved.push({ ...definition, riskLevel, domain });
139
176
  }
140
177
  for (const [domain, entry] of byDomain) {
141
178
  const requiredPermissions = entry.input.riskLevel === 'destructive'
@@ -166,13 +203,22 @@ function seedToolchainFromDefinitions(definitions, options) {
166
203
  namespace,
167
204
  name: tool.name,
168
205
  version,
169
- shortDescription: tool.name,
170
- fullDescription: `${tool.name} (${domain})`,
206
+ shortDescription: compactDescription(tool.description, tool.name),
207
+ fullDescription: tool.description && tool.description.trim() ? tool.description : `${tool.name} (${domain})`,
171
208
  inputSchema: tool.parameters ?? { type: 'object', properties: {}, required: [] },
209
+ outputSchema: tool.outputSchema,
172
210
  riskLevel: tool.riskLevel,
173
211
  idempotency,
174
212
  requiredPermissions: required,
175
213
  implementationHash: (0, deterministic_1.sha256)(tool.name),
214
+ execute: tool.execute,
215
+ isConcurrencySafe: tool.isConcurrencySafe,
216
+ render: tool.render,
217
+ presentationMeta: tool.presentationMeta,
218
+ finalizeContent: tool.finalizeContent,
219
+ timeoutMs: tool.timeoutMs,
220
+ presentCall: tool.presentCall,
221
+ presentResult: tool.presentResult,
176
222
  };
177
223
  core.registry.register(input);
178
224
  toolIds.push(tool.name);
@@ -349,9 +349,9 @@ class ToolExecutor {
349
349
  t('subagent_result', 'Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
350
350
  t('subagent_close', 'Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
351
351
  t('linked_plan', 'Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.', { action: { type: 'string', enum: ['get', 'update'] }, markdown: { type: 'string' }, expected_revision: { type: 'number' } }, ['action']),
352
- t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' } }, []),
352
+ t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query. Every activity/guide content is bounded to max_chars (default 2000) to keep the read lean and cache-friendly.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' }, max_chars: { type: 'number', minimum: 100, maximum: 4000, description: 'Per-event/per-guide content character bound; defaults to 2000.' } }, []),
353
353
  t('context_compress', 'Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.', { keep_recent: { type: 'number', minimum: 2, maximum: 60, description: 'Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages.' }, force: { type: 'boolean', description: 'Compress even if the context is not yet over the automatic threshold. Defaults to false.' } }, []),
354
- t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. The hot cache stays bounded; evicted folded segments remain in a conversation-isolated append-only cold archive and are loaded only by explicit search/read/restore calls. Actions: list returns a bounded index of current context entries; remove deletes one current entry; summarize folds a contiguous current range; restore reinserts a folded segment when its summary marker is still present; search finds matching hot or archived segments; read returns one bounded segment without injecting the whole archive; status reports budgets, hot cache, cold archive, and the protected recent zone. The recent context tail and last user message are protected from remove/summarize unless dangerous is true.', {
354
+ t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. The hot cache stays bounded; evicted folded segments remain in a conversation-isolated append-only cold archive and are loaded only by explicit search/read/restore calls. Actions: list returns a bounded index of current context entries; remove declares one long-term entry for unload (see below); summarize folds a contiguous current range; restore reinserts a folded segment when its summary marker is still present; search finds matching hot or archived segments; read returns one bounded segment without injecting the whole archive; status reports budgets, hot cache, cold archive, the protected recent zone, and pending removals. The recent context tail and last user message are protected from remove/summarize unless dangerous is true. For cache-optimization, remove ONLY targets long-term history (never the protected recent tail or last user message) and does NOT unload immediately: the declared entry stays in context for the rest of the current Build Block so the provider prefix cache stays stable, then is physically removed when the Block ends — applying to subsequent Blocks only.', {
355
355
  action: { type: 'string', enum: ['list', 'remove', 'summarize', 'restore', 'search', 'read', 'status'], description: 'list current entries; remove one; summarize a range; restore by restore_id; search hot/cold folded segments; read one bounded folded segment; status report context budgets and storage.' },
356
356
  position: { type: 'number', minimum: 0, description: '0-based context entry index for remove, or the start of the range for summarize.' },
357
357
  to: { type: 'number', minimum: 0, description: '0-based inclusive end of the range for summarize. Defaults to position.' },
@@ -363,6 +363,15 @@ class ToolExecutor {
363
363
  max_chars: { type: 'number', minimum: 1000, maximum: 60000, description: 'Maximum message-content characters returned by read (default 12000).' },
364
364
  dangerous: { type: 'boolean', description: 'Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message.' },
365
365
  }, ['action']),
366
+ t('branch_list', 'List all branches in this conversation and their mailbox/activity status. Only available when the conversation was created with "允许分支交流" (allow branch communication) enabled. Returns each branch id, active flag, source message index, message/history/workRun counts, and unread mailbox counts so you can decide who to message or read next.', {}, []),
367
+ t('branch_send', 'Send a message to another branch in this conversation. Only available when branch communication is enabled. The message is persisted to the target branch mailbox and becomes visible to that branch on its next branch_read. Use this to coordinate work across concurrently running branches. A branch cannot message itself.', { to_branch: { type: 'string', description: 'Target branch id (from branch_list).' }, message: { type: 'string', description: 'Message body to deliver.' }, kind: { type: 'string', enum: ['message', 'directive', 'result'], description: 'Message kind; defaults to message.' }, correlation_id: { type: 'string', description: 'Optional correlation id for reply tracking.' }, reply_to: { type: 'string', description: 'Optional message id this message replies to.' } }, ['to_branch', 'message']),
368
+ t('branch_read', 'Read inbound messages and recent activity from another branch in this conversation. Only available when branch communication is enabled. Marks inbound messages read. Returns the branch metadata, inbound messages, and recent Build Block activity (final results and recent events).', { branch: { type: 'string', description: 'Branch id to read (from branch_list).' }, max_chars: { type: 'number', minimum: 100, maximum: 16000, description: 'Maximum characters for final-result text; defaults to 8000.' } }, ['branch']),
369
+ t('branch_create', 'Create a new conversation branch at a historical block position (a user message index) with a new initial instruction. Only available when branch communication is enabled. The new branch becomes an additional running branch. Use this to spin off alternative work from a past point without disturbing existing branches.', { message_index: { type: 'number', minimum: 0, description: '0-based index of a user message in the conversation history (the historical block position to branch from).' }, prompt: { type: 'string', description: 'The new branch initial instruction (becomes the branch user message).' }, message_id: { type: 'string', description: 'Optional exact message id to anchor the branch target.' }, guide_id: { type: 'string', description: 'Optional guide id to anchor the branch target.' } }, ['message_index', 'prompt']),
370
+ t('compress_tool_result', 'Compress ONE oversized tool result into a concise, format-preserving summary using the model, instead of hard truncation. Pass the artifact_id returned inline by an oversized tool result (the full raw content stays out of context, on disk). Use this when a read/grep/bash result reported an oversized_tool_result marker and you want to recover its structure (JSON arrays, table rows, code blocks, identifiers, error strings) as a compact summary. This runs an isolated compression call whose system prefix does not touch the conversation prompt cache, so it does not disturb cache hit rate.', { artifact_id: { type: 'string', description: 'The artifact_id from the oversized_tool_result marker returned inline by an oversized tool result.' }, content: { type: 'string', description: 'Optional fallback: a SHORT raw result text to compress directly when no artifact_id exists.' }, format_hint: { type: 'string', description: 'Optional description of the structure to preserve verbatim (e.g. "keep JSON arrays and file paths", "keep table columns and error strings").' } }, []),
371
+ t('background_tool', 'Run a tool call in the background WITHOUT blocking the conversation turn. Pass the target tool name and its arguments; this tool returns a background_id IMMEDIATELY, and the real tool keeps running in the background. The result is persisted and can be retrieved later with read_tool_result. Use this for long-running or non-critical tools (bash, web_fetch, long read/grep) so the conversation continues without waiting. The background result stays OUT of context until you explicitly read it, preserving prompt-cache hit rate. Orchestration/flow/subagent/question tools cannot be backgrounded.', { tool: { type: 'string', description: 'The tool name to run in the background (e.g. bash, web_fetch, read, grep).' }, args: { type: 'object', description: 'The arguments object for the target tool, matching its normal schema.' } }, ['tool']),
372
+ t('read_tool_result', 'Read the result of a background tool. Pass the background_id returned by background_tool. When status is running, returns a running marker; when done, returns the persisted result (optionally release it from storage after reading); when error, returns the failure. Background results are released from storage only when you set release=true.', { background_id: { type: 'string', description: 'The background_id returned by background_tool.' }, release: { type: 'boolean', description: 'Set true to release the persisted result from storage after reading it.' } }, ['background_id']),
373
+ t('goal_manage', 'Actively manage the persistent Goal state for this conversation. You may enter Goal mode, update (edit) its objective, mark it complete, or exit Goal mode yourself. Call this when the user asks you to pursue a persistent objective, when the objective changes, when you have verified the objective is genuinely achieved, or when you judge the Goal is no longer needed and should be cleared. This is the agent-side state control that mirrors the GUI goal panel controls. enter/update require objective; complete marks the objective verified and exits Goal mode; exit clears the Goal (and returns to Build mode) without claiming completion.', { action: { type: 'string', enum: ['enter', 'update', 'complete', 'exit'], description: 'enter=enter Goal mode and set the objective; update=edit the objective (records a change); complete=mark the objective verified-achieved and exit Goal mode; exit=clear the Goal and return to Build mode without claiming completion.' }, objective: { type: 'string', description: 'The Goal objective text. Required for enter and update.' }, reason: { type: 'string', description: 'Optional one-line reason for the state change, recorded for audit.' } }, ['action']),
374
+ t('conversation_rename', 'Rename the CURRENT conversation to a concise, descriptive title you choose. On the FIRST Build Block of a NEW conversation the runtime asks you to call this once so the conversation list shows a meaningful name instead of an auto-generated one. Keep the title short (a few words) and cache-friendly: a concrete noun phrase describing the task, never a sentence or quoted prompt.', { title: { type: 'string', description: 'The new conversation title (a short noun phrase, e.g. "Fix TUI color leak", "Add goal_manage tool").' } }, ['title']),
366
375
  t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
367
376
  t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
368
377
  t('skill', 'Search enabled skill metadata or load one exact skill body on demand. Use query when unsure, then name to load the selected skill.', { query: { type: 'string', maxLength: 200 }, name: { type: 'string', maxLength: 200 } }, []),
@@ -38,10 +38,14 @@ exports.NATIVE_TOOL_CATALOG = [
38
38
  { name: 'subagent_send', label: 'Subagent send', description: 'Persist a message to a peer mailbox.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
39
39
  { name: 'subagent_result', label: 'Subagent result', description: 'Read peer transcript and result.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
40
40
  { name: 'subagent_close', label: 'Subagent close', description: 'Close a same-conversation peer agent.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
41
+ { name: 'branch_list', label: 'Branch list', description: 'List all conversation branches and their mailbox/activity status. Only available when branch communication is enabled.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
42
+ { name: 'branch_send', label: 'Branch send', description: 'Send a message to another conversation branch. Only available when branch communication is enabled.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
43
+ { name: 'branch_read', label: 'Branch read', description: 'Read inbound messages and recent activity from another conversation branch. Only available when branch communication is enabled.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
+ { name: 'branch_create', label: 'Branch create', description: 'Create a new conversation branch at a historical block position. Only available when branch communication is enabled.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
41
45
  { name: 'linked_plan', label: 'Linked plan', description: 'Read or conservatively update the conversation-linked Markdown plan.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
42
46
  { name: 'build_history_query', label: 'Build history query', description: 'Read concrete public work details for one historical Build Block.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
43
47
  { name: 'context_compress', label: 'Context compress', description: 'Actively compress the LLM context history, leaving the displayed conversation history unchanged.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
- { name: 'context_history_manage', label: 'Context history manage', description: 'Inspect, search, restore, or fold LLM context history without touching the displayed conversation history.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
48
+ { name: 'context_history_manage', label: 'Context history manage', description: 'Inspect, search, restore, fold, or unload LLM context history without touching the displayed conversation history. Unload (remove) targets long-term entries only and takes effect after the current Build Block, for subsequent Blocks only.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
45
49
  { name: 'question', label: 'Ask question', description: 'Ask the user for structured option feedback.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
46
50
  { name: 'skill_download', label: 'Skill download', description: 'Download and install a skill.', category: 'agent', defaultEnabled: true },
47
51
  { name: 'skill', label: 'Skill', description: 'Search enabled skill metadata or load one skill body on demand.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
@@ -93,6 +93,41 @@ function samePath(left, right) {
93
93
  return normalize(left) === normalize(right);
94
94
  }
95
95
 
96
+ function isPathInside(parent, child) {
97
+ try {
98
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
99
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function isProtectedInstallPath(candidate) {
106
+ if (process.platform !== "win32") return false;
107
+ const protectedRoots = [
108
+ process.env.ProgramFiles,
109
+ process.env["ProgramFiles(x86)"],
110
+ process.env.ProgramW6432
111
+ ].filter(Boolean);
112
+ return protectedRoots.some((root) => isPathInside(root, candidate));
113
+ }
114
+
115
+ function safeWorkspacePath(root, candidate) {
116
+ const resolvedRoot = path.resolve(root);
117
+ const resolvedCandidate = path.resolve(candidate || resolvedRoot);
118
+ const executableRoot = path.dirname(process.execPath);
119
+ // A packaged TUI launched from its installation directory must never turn
120
+ // that directory into an external workspace. The runtime root owns the
121
+ // default internal workspace in this case.
122
+ if (
123
+ samePath(resolvedCandidate, resolvedRoot)
124
+ || isPathInside(resolvedRoot, resolvedCandidate)
125
+ || isPathInside(executableRoot, resolvedCandidate)
126
+ || isProtectedInstallPath(resolvedCandidate)
127
+ ) return resolvedRoot;
128
+ return resolvedCandidate;
129
+ }
130
+
96
131
  function mergeProviderConfig(currentProviders, incomingProviders) {
97
132
  return (incomingProviders || []).map((incoming) => {
98
133
  const current = (currentProviders || []).find((provider) => provider.id === incoming.id);
@@ -120,7 +155,7 @@ function createCoreRuntimeAdapter(options = {}) {
120
155
  const { FlowEngine } = require(path.join(desktopDist, "core", "flow.js"));
121
156
  const installUpdate = require(path.join(desktopDist, "core", "installUpdate.js"));
122
157
  const root = path.resolve(options.root || path.join(os.homedir(), ".Newmark"));
123
- const workspacePath = path.resolve(options.workspacePath || process.cwd());
158
+ const workspacePath = safeWorkspacePath(root, options.workspacePath || process.cwd());
124
159
  ensureRuntimeRoot(root, configModule);
125
160
 
126
161
  const agent = new Agent(root);
@@ -128,7 +163,9 @@ function createCoreRuntimeAdapter(options = {}) {
128
163
  .find((workspace) => samePath(workspace.path, workspacePath));
129
164
  const selectedWorkspace = knownWorkspace
130
165
  ? agent.selectWorkspaceFromStorage(knownWorkspace.id)
131
- : agent.addExternalWorkspace(workspacePath);
166
+ : samePath(workspacePath, root)
167
+ ? (agent.workspace.current || agent.createInternalWorkspace())
168
+ : agent.addExternalWorkspace(workspacePath);
132
169
  if (!selectedWorkspace) {
133
170
  throw new Error(
134
171
  `The current folder cannot be registered as a Newmark workspace: ${workspacePath}. ` +
@@ -454,4 +491,4 @@ function createCoreRuntimeAdapter(options = {}) {
454
491
  };
455
492
  }
456
493
 
457
- module.exports = { createCoreRuntimeAdapter, mergeProviderConfig, resolveDesktopDist, samePath, sanitizeProviders };
494
+ module.exports = { createCoreRuntimeAdapter, mergeProviderConfig, resolveDesktopDist, samePath, safeWorkspacePath, sanitizeProviders };