newmark-agent 0.3.12 → 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/dist/cli-commands.d.ts +1 -0
- package/dist/cli-commands.js +11 -2
- 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 +1147 -131
- package/dist/conversation-utility-host.js +3 -0
- package/dist/core/agent.d.ts +139 -5
- package/dist/core/agent.js +962 -82
- 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 +121 -19
- package/dist/core/conversationKernel.d.ts +5 -0
- package/dist/core/conversationKernel.js +29 -0
- 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 +8 -0
- package/dist/core/electronUtilityRuntimePool.js +14 -0
- package/dist/core/mcpManager.d.ts +1 -0
- package/dist/core/mcpManager.js +100 -10
- 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 +9 -0
- package/dist/core/workspace.js +48 -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 +8 -0
- package/dist/core/wslAgentRuntimePool.js +15 -0
- package/dist/launcher.js +8 -0
- package/dist/llm/provider.d.ts +1 -1
- package/dist/llm/provider.js +4 -3
- package/dist/main.js +163 -11
- package/dist/preload.js +11 -0
- package/dist/providers/chat-completions.adapter.js +41 -15
- package/dist/providers/provider-adapter.d.ts +3 -0
- 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/ui/index.html +2530 -205
- package/dist/ui/lucide-sprite.svg +26 -0
- package/dist/wsl-agent-host.bundle.cjs +1147 -131
- package/dist/wsl-agent-host.js +3 -0
- package/package.json +4 -2
|
@@ -28,6 +28,7 @@ export interface McpServerInput {
|
|
|
28
28
|
export declare class McpManager {
|
|
29
29
|
private readonly filePath;
|
|
30
30
|
private state;
|
|
31
|
+
private preserveCorruptSource;
|
|
31
32
|
constructor(root: string);
|
|
32
33
|
list(): Array<Omit<McpServerConfig, 'env' | 'headers'> & {
|
|
33
34
|
envKeys: string[];
|
package/dist/core/mcpManager.js
CHANGED
|
@@ -45,10 +45,10 @@ function stringRecord(value, label) {
|
|
|
45
45
|
const output = {};
|
|
46
46
|
for (const [key, item] of Object.entries(value)) {
|
|
47
47
|
const cleanKey = String(key || '').trim();
|
|
48
|
-
if (!cleanKey || cleanKey.includes('\r') || cleanKey.includes('\n') || cleanKey.includes(String.fromCharCode(0)))
|
|
48
|
+
if (!cleanKey || cleanKey.includes('\r') || cleanKey.includes('\n') || cleanKey.includes(String.fromCharCode(0)) || ['__proto__', 'prototype', 'constructor'].includes(cleanKey))
|
|
49
49
|
throw new Error(`${label} contains an invalid key.`);
|
|
50
50
|
const cleanValue = String(item ?? '');
|
|
51
|
-
if (cleanValue.includes(String.fromCharCode(0)))
|
|
51
|
+
if (cleanValue.includes(String.fromCharCode(0)) || (label === 'headers' && /[\r\n]/.test(cleanValue)))
|
|
52
52
|
throw new Error(`${label} contains an invalid value.`);
|
|
53
53
|
output[cleanKey] = cleanValue;
|
|
54
54
|
}
|
|
@@ -59,11 +59,83 @@ function stringArray(value) {
|
|
|
59
59
|
return undefined;
|
|
60
60
|
if (!Array.isArray(value))
|
|
61
61
|
throw new Error('args must be a JSON array of strings.');
|
|
62
|
-
|
|
62
|
+
if (value.length > 100 || value.some(item => typeof item !== 'string' || item.length > 4000 || item.includes(String.fromCharCode(0)))) {
|
|
63
|
+
throw new Error('args must contain at most 100 strings, each no longer than 4000 characters.');
|
|
64
|
+
}
|
|
65
|
+
return [...value];
|
|
66
|
+
}
|
|
67
|
+
function publicHttpUrl(value) {
|
|
68
|
+
if (!value)
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(value);
|
|
72
|
+
if (parsed.username)
|
|
73
|
+
parsed.username = '<redacted>';
|
|
74
|
+
if (parsed.password)
|
|
75
|
+
parsed.password = '<redacted>';
|
|
76
|
+
for (const key of Array.from(parsed.searchParams.keys())) {
|
|
77
|
+
if (/(?:api.?key|authorization|access.?token|secret|password|credential)/i.test(key))
|
|
78
|
+
parsed.searchParams.set(key, '<redacted>');
|
|
79
|
+
}
|
|
80
|
+
return parsed.toString();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function parseHttpUrl(value) {
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = new URL(value);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new Error('An HTTP MCP server requires an http(s) URL.');
|
|
93
|
+
}
|
|
94
|
+
if (!['http:', 'https:'].includes(parsed.protocol))
|
|
95
|
+
throw new Error('An HTTP MCP server requires an http(s) URL.');
|
|
96
|
+
if (parsed.username || parsed.password)
|
|
97
|
+
throw new Error('Put HTTP credentials in headers, not in the MCP URL.');
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function safeLoadedServer(value) {
|
|
101
|
+
try {
|
|
102
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
103
|
+
return undefined;
|
|
104
|
+
const item = value;
|
|
105
|
+
const id = String(item.id || '').trim();
|
|
106
|
+
const name = String(item.name || '').trim();
|
|
107
|
+
const transport = item.transport === 'http' ? 'http' : item.transport === 'stdio' ? 'stdio' : undefined;
|
|
108
|
+
if (!id || !name || !transport)
|
|
109
|
+
return undefined;
|
|
110
|
+
const command = String(item.command || '').trim();
|
|
111
|
+
const url = String(item.url || '').trim();
|
|
112
|
+
if (transport === 'stdio' && !command)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (transport === 'http')
|
|
115
|
+
parseHttpUrl(url);
|
|
116
|
+
return {
|
|
117
|
+
id: id.slice(0, 200),
|
|
118
|
+
name: name.slice(0, 120),
|
|
119
|
+
enabled: item.enabled === true,
|
|
120
|
+
transport,
|
|
121
|
+
command: transport === 'stdio' ? command.slice(0, 2000) : undefined,
|
|
122
|
+
args: transport === 'stdio' ? (stringArray(item.args) || []) : undefined,
|
|
123
|
+
cwd: transport === 'stdio' ? String(item.cwd || '').trim().slice(0, 4000) || undefined : undefined,
|
|
124
|
+
url: transport === 'http' ? url.slice(0, 4000) : undefined,
|
|
125
|
+
env: transport === 'stdio' ? (stringRecord(item.env, 'env') || {}) : undefined,
|
|
126
|
+
headers: transport === 'http' ? (stringRecord(item.headers, 'headers') || {}) : undefined,
|
|
127
|
+
createdAt: String(item.createdAt || new Date(0).toISOString()),
|
|
128
|
+
updatedAt: String(item.updatedAt || item.createdAt || new Date(0).toISOString()),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
63
134
|
}
|
|
64
135
|
class McpManager {
|
|
65
136
|
filePath;
|
|
66
137
|
state;
|
|
138
|
+
preserveCorruptSource = false;
|
|
67
139
|
constructor(root) {
|
|
68
140
|
this.filePath = path.join(root, 'MCP.json');
|
|
69
141
|
this.state = this.load();
|
|
@@ -74,6 +146,7 @@ class McpManager {
|
|
|
74
146
|
return {
|
|
75
147
|
...publicServer,
|
|
76
148
|
args: [...(server.args || [])],
|
|
149
|
+
url: publicHttpUrl(server.url),
|
|
77
150
|
envKeys: Object.keys(env || {}).sort(),
|
|
78
151
|
headerKeys: Object.keys(headers || {}).sort(),
|
|
79
152
|
};
|
|
@@ -81,22 +154,33 @@ class McpManager {
|
|
|
81
154
|
}
|
|
82
155
|
upsert(input) {
|
|
83
156
|
const id = String(input.id || '').trim();
|
|
84
|
-
|
|
157
|
+
if (input.transport !== undefined && input.transport !== 'stdio' && input.transport !== 'http')
|
|
158
|
+
throw new Error('transport must be stdio or http.');
|
|
159
|
+
let existing = this.state.servers.find(server => server.id === id);
|
|
85
160
|
const name = String(input.name ?? existing?.name ?? '').trim().slice(0, 120);
|
|
86
161
|
if (!name)
|
|
87
162
|
throw new Error('MCP server name is required.');
|
|
163
|
+
if (/[\r\n\0]/.test(name))
|
|
164
|
+
throw new Error('MCP server name contains invalid characters.');
|
|
88
165
|
const transport = input.transport === 'http' ? 'http' : (input.transport === 'stdio' ? 'stdio' : existing?.transport || 'stdio');
|
|
89
166
|
const command = String(input.command ?? existing?.command ?? '').trim();
|
|
90
|
-
|
|
167
|
+
let url = String(input.url ?? existing?.url ?? '').trim();
|
|
91
168
|
if (transport === 'stdio' && !command)
|
|
92
169
|
throw new Error('A stdio MCP server requires a command.');
|
|
93
|
-
if (transport === '
|
|
94
|
-
throw new Error('
|
|
170
|
+
if (transport === 'stdio' && /[\r\n\0]/.test(command))
|
|
171
|
+
throw new Error('command contains an invalid value.');
|
|
172
|
+
if (transport === 'http')
|
|
173
|
+
url = parseHttpUrl(url);
|
|
174
|
+
if (!existing && !id) {
|
|
175
|
+
existing = this.state.servers.find(server => server.name.toLocaleLowerCase() === name.toLocaleLowerCase()
|
|
176
|
+
&& server.transport === transport
|
|
177
|
+
&& (transport === 'stdio' ? server.command === command : server.url === url));
|
|
178
|
+
}
|
|
95
179
|
const now = new Date().toISOString();
|
|
96
180
|
const server = {
|
|
97
181
|
id: existing?.id || `mcp-${(0, crypto_1.randomUUID)()}`,
|
|
98
182
|
name,
|
|
99
|
-
enabled: typeof input.enabled === 'boolean' ? input.enabled : existing?.enabled
|
|
183
|
+
enabled: typeof input.enabled === 'boolean' ? input.enabled : existing?.enabled === true,
|
|
100
184
|
transport,
|
|
101
185
|
command: transport === 'stdio' ? command.slice(0, 2000) : undefined,
|
|
102
186
|
args: transport === 'stdio' ? (stringArray(input.args) ?? existing?.args ?? []) : undefined,
|
|
@@ -134,15 +218,21 @@ class McpManager {
|
|
|
134
218
|
load() {
|
|
135
219
|
try {
|
|
136
220
|
const parsed = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
|
|
137
|
-
return { version: 1, servers: Array.isArray(parsed.servers) ? parsed.servers.filter(
|
|
221
|
+
return { version: 1, servers: Array.isArray(parsed.servers) ? parsed.servers.map(safeLoadedServer).filter((server) => !!server) : [] };
|
|
138
222
|
}
|
|
139
223
|
catch {
|
|
224
|
+
this.preserveCorruptSource = fs.existsSync(this.filePath);
|
|
140
225
|
return { version: 1, servers: [] };
|
|
141
226
|
}
|
|
142
227
|
}
|
|
143
228
|
save() {
|
|
144
229
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
145
|
-
|
|
230
|
+
if (this.preserveCorruptSource) {
|
|
231
|
+
const backupPath = `${this.filePath}.corrupt-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
232
|
+
fs.copyFileSync(this.filePath, backupPath);
|
|
233
|
+
this.preserveCorruptSource = false;
|
|
234
|
+
}
|
|
235
|
+
const tempPath = `${this.filePath}.${process.pid}-${(0, crypto_1.randomUUID)()}.tmp`;
|
|
146
236
|
fs.writeFileSync(tempPath, JSON.stringify(this.state, null, 2), 'utf8');
|
|
147
237
|
fs.renameSync(tempPath, this.filePath);
|
|
148
238
|
}
|
package/dist/core/subagent.d.ts
CHANGED
|
@@ -223,6 +223,12 @@ export declare class SubagentManager {
|
|
|
223
223
|
toRecord(idOrName: string): NewmarkSubagentRecord | undefined;
|
|
224
224
|
toToolResult(idOrName: string, output: string, ok?: boolean): NewmarkSubagentToolResult;
|
|
225
225
|
getResult(name: string): string;
|
|
226
|
+
/**
|
|
227
|
+
* 有界结果 transcript:subagent_result 注入主 Agent 上下文时,不再放大完整
|
|
228
|
+
* 消息历史(含所有中间 tool call/result 洪水)。只保留最近若干条非 tool 消息,
|
|
229
|
+
* 按字符上限截断,杜绝上下文回归。完整历史按需走 subagent_read(max_chars)。
|
|
230
|
+
*/
|
|
231
|
+
boundedResultTranscript(idOrName: string): string;
|
|
226
232
|
listActive(): SubagentInstance[];
|
|
227
233
|
listAll(): SubagentInstance[];
|
|
228
234
|
pauseScheduling(): void;
|
package/dist/core/subagent.js
CHANGED
|
@@ -193,7 +193,7 @@ class SubagentManager {
|
|
|
193
193
|
fromAgentId,
|
|
194
194
|
toAgentId: target.id,
|
|
195
195
|
kind,
|
|
196
|
-
body,
|
|
196
|
+
body: truncateText(body, 32000),
|
|
197
197
|
correlationId: details.correlationId,
|
|
198
198
|
replyTo: details.replyTo,
|
|
199
199
|
createdAt: now(),
|
|
@@ -481,6 +481,27 @@ class SubagentManager {
|
|
|
481
481
|
return '';
|
|
482
482
|
return record.result || record.messages.filter(message => message.role === 'assistant').map(message => message.content).join('\n');
|
|
483
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* 有界结果 transcript:subagent_result 注入主 Agent 上下文时,不再放大完整
|
|
486
|
+
* 消息历史(含所有中间 tool call/result 洪水)。只保留最近若干条非 tool 消息,
|
|
487
|
+
* 按字符上限截断,杜绝上下文回归。完整历史按需走 subagent_read(max_chars)。
|
|
488
|
+
*/
|
|
489
|
+
boundedResultTranscript(idOrName) {
|
|
490
|
+
const record = this.get(idOrName);
|
|
491
|
+
if (!record)
|
|
492
|
+
return '';
|
|
493
|
+
const MAX_MSG = 8; // 最近保留的消息条数
|
|
494
|
+
const MAX_CHARS = 8000; // 总字符上限
|
|
495
|
+
const messages = record.messages
|
|
496
|
+
.filter(message => message.role === 'assistant' || message.role === 'user' || message.role === 'system')
|
|
497
|
+
.slice(-MAX_MSG)
|
|
498
|
+
.map(message => `[${message.role}] ${truncateText(String(message.content || ''), 1200)}`);
|
|
499
|
+
let text = messages.join('\n');
|
|
500
|
+
if (text.length > MAX_CHARS) {
|
|
501
|
+
text = text.slice(0, MAX_CHARS) + `\n[...transcript truncated: ${record.messages.length} total messages, ${record.messages.length - MAX_MSG} older omitted; use subagent_read for full history...]`;
|
|
502
|
+
}
|
|
503
|
+
return text || '(no transcript)';
|
|
504
|
+
}
|
|
484
505
|
listActive() { return this.listAll().filter(item => item.status !== 'closed'); }
|
|
485
506
|
listAll() { return [...this.subs.values()].map(cloneRecord); }
|
|
486
507
|
pauseScheduling() {
|
|
@@ -14,6 +14,12 @@ export interface ToolPolicyDecision {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const PLAN_COMPUTER_USE_ACTIONS: readonly ["observe", "app_list", "app_observe"];
|
|
16
16
|
export declare const PLAN_BROWSER_USE_ACTIONS: readonly ["observe", "navigate", "wait", "extract"];
|
|
17
|
+
/** 判断一个工具是否可参与并行调度。缺省 false(独占)。
|
|
18
|
+
* 优先看 toolchain registry 推断的 riskLevel('read' 工具天然并发安全,
|
|
19
|
+
* seedToolchainFromDefinitions 的 inferRiskLevel 自动推断),显式白名单作兜底。
|
|
20
|
+
* 这样后续新增工具只需在 ToolExecutor.definitions() 加 schema,seed 时其
|
|
21
|
+
* riskLevel 被自动推断,'read' 工具自动并发安全——无需再改本文件。 */
|
|
22
|
+
export declare function isConcurrencySafeTool(name: string, riskLevel?: string): boolean;
|
|
17
23
|
export declare function isReadOnlyScopedToolAction(name: string, action: string): boolean;
|
|
18
24
|
export declare function toolAvailability(name: string): ToolAvailability;
|
|
19
25
|
export declare function evaluateToolPolicy(request: ToolPolicyRequest): ToolPolicyDecision;
|
package/dist/core/toolPolicy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PLAN_BROWSER_USE_ACTIONS = exports.PLAN_COMPUTER_USE_ACTIONS = void 0;
|
|
4
|
+
exports.isConcurrencySafeTool = isConcurrencySafeTool;
|
|
4
5
|
exports.isReadOnlyScopedToolAction = isReadOnlyScopedToolAction;
|
|
5
6
|
exports.toolAvailability = toolAvailability;
|
|
6
7
|
exports.evaluateToolPolicy = evaluateToolPolicy;
|
|
@@ -16,6 +17,11 @@ const MODE_SCOPED_TOOLS = new Set([
|
|
|
16
17
|
'build_history_query',
|
|
17
18
|
'context_compress',
|
|
18
19
|
'context_history_manage',
|
|
20
|
+
'compress_tool_result',
|
|
21
|
+
'background_tool',
|
|
22
|
+
'read_tool_result',
|
|
23
|
+
'goal_manage',
|
|
24
|
+
'conversation_rename',
|
|
19
25
|
'question',
|
|
20
26
|
'task',
|
|
21
27
|
'subagent_list',
|
|
@@ -23,6 +29,10 @@ const MODE_SCOPED_TOOLS = new Set([
|
|
|
23
29
|
'subagent_send',
|
|
24
30
|
'subagent_result',
|
|
25
31
|
'subagent_close',
|
|
32
|
+
'branch_list',
|
|
33
|
+
'branch_send',
|
|
34
|
+
'branch_read',
|
|
35
|
+
'branch_create',
|
|
26
36
|
]);
|
|
27
37
|
const PLAN_READ_ONLY_TOOLS = new Set([
|
|
28
38
|
'pwd',
|
|
@@ -51,12 +61,50 @@ const PLAN_READ_ONLY_TOOLS = new Set([
|
|
|
51
61
|
'subagent_send',
|
|
52
62
|
'subagent_result',
|
|
53
63
|
'subagent_close',
|
|
64
|
+
'branch_list',
|
|
65
|
+
'branch_read',
|
|
54
66
|
'question',
|
|
55
67
|
]);
|
|
56
68
|
exports.PLAN_COMPUTER_USE_ACTIONS = ['observe', 'app_list', 'app_observe'];
|
|
57
69
|
exports.PLAN_BROWSER_USE_ACTIONS = ['observe', 'navigate', 'wait', 'extract'];
|
|
58
70
|
const PLAN_COMPUTER_USE_ACTION_SET = new Set(exports.PLAN_COMPUTER_USE_ACTIONS);
|
|
59
71
|
const PLAN_BROWSER_USE_ACTION_SET = new Set(exports.PLAN_BROWSER_USE_ACTIONS);
|
|
72
|
+
/**
|
|
73
|
+
* 并发安全工具集合(DSH isConcurrencySafe 语义的 Newmark 落地)。
|
|
74
|
+
*
|
|
75
|
+
* 只有确定性无副作用的只读工具才允许与兄弟 tool call 并发执行;任何写入、
|
|
76
|
+
* shell 命令、浏览器交互、子代理编排、以及会改变对话/文件/外部状态的工具
|
|
77
|
+
* 都保持独占串行,避免盲目 Promise.all 引发竞态。缺省保守:不在集合中的
|
|
78
|
+
* 工具一律视为独占。
|
|
79
|
+
*
|
|
80
|
+
* 注意:read/grep/glob/pwd 是同一进程内的内存/文件系统只读,可安全重叠;
|
|
81
|
+
* web_search/web_fetch 只读网络,可重叠;git_status/file_audit/repo_security_audit
|
|
82
|
+
* 只读审计,可重叠。其余(含 memory_lab_read 等读接口)因可能触发内部缓存/
|
|
83
|
+
* 重建副作用,默认保守串行。
|
|
84
|
+
*/
|
|
85
|
+
const CONCURRENCY_SAFE_TOOLS = new Set([
|
|
86
|
+
'pwd',
|
|
87
|
+
'read',
|
|
88
|
+
'glob',
|
|
89
|
+
'grep',
|
|
90
|
+
'web_search',
|
|
91
|
+
'web_fetch',
|
|
92
|
+
'git_status',
|
|
93
|
+
'file_audit',
|
|
94
|
+
'repo_security_audit',
|
|
95
|
+
]);
|
|
96
|
+
/** 判断一个工具是否可参与并行调度。缺省 false(独占)。
|
|
97
|
+
* 优先看 toolchain registry 推断的 riskLevel('read' 工具天然并发安全,
|
|
98
|
+
* seedToolchainFromDefinitions 的 inferRiskLevel 自动推断),显式白名单作兜底。
|
|
99
|
+
* 这样后续新增工具只需在 ToolExecutor.definitions() 加 schema,seed 时其
|
|
100
|
+
* riskLevel 被自动推断,'read' 工具自动并发安全——无需再改本文件。 */
|
|
101
|
+
function isConcurrencySafeTool(name, riskLevel) {
|
|
102
|
+
const toolName = String(name || '').trim();
|
|
103
|
+
if (CONCURRENCY_SAFE_TOOLS.has(toolName))
|
|
104
|
+
return true;
|
|
105
|
+
// 从 toolchain registry 派生的 riskLevel:read 工具默认并发安全。
|
|
106
|
+
return riskLevel === 'read';
|
|
107
|
+
}
|
|
60
108
|
function isReadOnlyScopedToolAction(name, action) {
|
|
61
109
|
if (name === 'computer_use')
|
|
62
110
|
return PLAN_COMPUTER_USE_ACTION_SET.has(action);
|
|
@@ -97,7 +145,7 @@ function evaluateToolPolicy(request) {
|
|
|
97
145
|
}
|
|
98
146
|
}
|
|
99
147
|
if (request.isSubagent) {
|
|
100
|
-
if (name === 'skill_download' || name === 'question' || name.startsWith('automation_')) {
|
|
148
|
+
if (name === 'skill_download' || name === 'question' || name.startsWith('automation_') || name === 'goal_manage' || name === 'conversation_rename') {
|
|
101
149
|
return { ...base, allowed: false, reason: `[Subagent sandbox] Tool '${name}' is disabled for peer agents.` };
|
|
102
150
|
}
|
|
103
151
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export interface ChatMessage {
|
|
|
100
100
|
export interface AgentWorkEvent {
|
|
101
101
|
id: string;
|
|
102
102
|
conversationId: string;
|
|
103
|
-
type: 'start' | 'text' | 'response' | 'final_response' | 'tool_call' | 'tool_result' | 'status' | 'done' | 'error' | 'queue_update' | 'guide';
|
|
103
|
+
type: 'start' | 'text' | 'response' | 'final_response' | 'tool_call' | 'tool_result' | 'thought' | 'thought_result' | 'status' | 'done' | 'error' | 'queue_update' | 'guide';
|
|
104
104
|
content: string;
|
|
105
105
|
mode: string;
|
|
106
106
|
model: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BrowserControlRequest, BrowserControlResult } from './browserControl';
|
|
2
2
|
import { BrowserUseRequest } from './browserUse';
|
|
3
|
-
import { AgentPromptMessage, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationRuntimeState, ConversationStopResult } from './conversationKernel';
|
|
3
|
+
import { AgentPromptMessage, ConversationContextCompressOptions, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationRuntimeState, ConversationStopResult } from './conversationKernel';
|
|
4
4
|
import { ConversationRuntimeTarget, NormalizedConversationTarget } from './conversationTarget';
|
|
5
5
|
import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
|
|
6
6
|
import type { AutoRouteRatingResult, ConversationSnapshot } from './agent';
|
|
@@ -102,6 +102,13 @@ export type UtilityAgentRequest = {
|
|
|
102
102
|
params: {
|
|
103
103
|
target: ConversationRuntimeTarget;
|
|
104
104
|
};
|
|
105
|
+
} | {
|
|
106
|
+
id: string;
|
|
107
|
+
method: 'context_compress';
|
|
108
|
+
params: {
|
|
109
|
+
target: ConversationRuntimeTarget;
|
|
110
|
+
options?: ConversationContextCompressOptions;
|
|
111
|
+
};
|
|
105
112
|
} | {
|
|
106
113
|
id: string;
|
|
107
114
|
method: 'rate_auto_route';
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export interface WorkspaceManagerOptions {
|
|
|
23
23
|
}
|
|
24
24
|
/** Normalize persisted Windows/WSL aliases and recover paths damaged by cross-host path.resolve calls. */
|
|
25
25
|
export declare function normalizeHostWorkspacePath(input: string, platform?: NodeJS.Platform): string;
|
|
26
|
+
/**
|
|
27
|
+
* An installed package is never a writable user workspace. Older builds could
|
|
28
|
+
* persist the install directory as an external workspace when the GUI was
|
|
29
|
+
* started from its shortcut working directory; restoring that entry then
|
|
30
|
+
* tried to create `<install>\\conversations` under Program Files. Keep this
|
|
31
|
+
* policy in the workspace layer so GUI, TUI, CLI, and detached runtimes all
|
|
32
|
+
* recover the same way.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isProtectedInstallWorkspacePath(candidate: string): boolean;
|
|
26
35
|
export declare class WorkspaceManager {
|
|
27
36
|
rootPath: string;
|
|
28
37
|
private config;
|
package/dist/core/workspace.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.WorkspaceManager = void 0;
|
|
37
37
|
exports.normalizeHostWorkspacePath = normalizeHostWorkspacePath;
|
|
38
|
+
exports.isProtectedInstallWorkspacePath = isProtectedInstallWorkspacePath;
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
40
41
|
const crypto = __importStar(require("crypto"));
|
|
@@ -64,6 +65,34 @@ function normalizeHostWorkspacePath(input, platform = process.platform) {
|
|
|
64
65
|
}
|
|
65
66
|
return path.posix.resolve(raw || '.');
|
|
66
67
|
}
|
|
68
|
+
function isPathInside(parent, child) {
|
|
69
|
+
try {
|
|
70
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
71
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* An installed package is never a writable user workspace. Older builds could
|
|
79
|
+
* persist the install directory as an external workspace when the GUI was
|
|
80
|
+
* started from its shortcut working directory; restoring that entry then
|
|
81
|
+
* tried to create `<install>\\conversations` under Program Files. Keep this
|
|
82
|
+
* policy in the workspace layer so GUI, TUI, CLI, and detached runtimes all
|
|
83
|
+
* recover the same way.
|
|
84
|
+
*/
|
|
85
|
+
function isProtectedInstallWorkspacePath(candidate) {
|
|
86
|
+
const value = String(candidate || '').trim();
|
|
87
|
+
if (!value)
|
|
88
|
+
return false;
|
|
89
|
+
const roots = [path.dirname(process.execPath)];
|
|
90
|
+
if (process.platform === 'win32') {
|
|
91
|
+
roots.push(process.env.ProgramFiles || '', process.env['ProgramFiles(x86)'] || '', process.env.ProgramW6432 || '');
|
|
92
|
+
}
|
|
93
|
+
const resolved = path.resolve(value);
|
|
94
|
+
return roots.filter(Boolean).some(root => isPathInside(root, resolved));
|
|
95
|
+
}
|
|
67
96
|
class WorkspaceManager {
|
|
68
97
|
rootPath;
|
|
69
98
|
config;
|
|
@@ -135,9 +164,18 @@ class WorkspaceManager {
|
|
|
135
164
|
catch { /* empty */ }
|
|
136
165
|
try {
|
|
137
166
|
const ext = JSON.parse(fs.readFileSync(path.join(w, 'External.json'), 'utf-8'));
|
|
138
|
-
|
|
167
|
+
const normalized = Array.isArray(ext)
|
|
139
168
|
? ext.map(item => this.normalizeExternalWorkspace(item, changed => { externalChanged = externalChanged || changed; }))
|
|
140
169
|
: [];
|
|
170
|
+
this.external = normalized.filter(workspace => {
|
|
171
|
+
if (!isProtectedInstallWorkspacePath(workspace.path))
|
|
172
|
+
return true;
|
|
173
|
+
// Never restore or persist the installation directory as a user
|
|
174
|
+
// workspace. It is a migration boundary for registries written by
|
|
175
|
+
// older versions and by package-level pressure tests.
|
|
176
|
+
externalChanged = true;
|
|
177
|
+
return false;
|
|
178
|
+
});
|
|
141
179
|
}
|
|
142
180
|
catch { /* empty */ }
|
|
143
181
|
// Scan for directories not in Local.json
|
|
@@ -344,6 +382,15 @@ class WorkspaceManager {
|
|
|
344
382
|
}
|
|
345
383
|
restoreCurrent() {
|
|
346
384
|
const stateCurrent = this.readState().current || null;
|
|
385
|
+
if (stateCurrent?.path && isProtectedInstallWorkspacePath(stateCurrent.path)) {
|
|
386
|
+
// The previous current workspace may have been filtered from
|
|
387
|
+
// External.json above. Clear the pointer instead of falling back to an
|
|
388
|
+
// arbitrary external directory; Agent can create/reuse a user-root
|
|
389
|
+
// internal workspace according to its normal configuration.
|
|
390
|
+
this.current = null;
|
|
391
|
+
this.saveState();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
347
394
|
const stored = this.findWorkspace(stateCurrent);
|
|
348
395
|
if (stored) {
|
|
349
396
|
this.current = stored;
|
|
@@ -72,6 +72,10 @@ export declare class WslAgentClient {
|
|
|
72
72
|
rewind(target: ConversationRuntimeTarget, messageIndex: number): Promise<WslConversationRewindResult>;
|
|
73
73
|
enqueueGuide(target: ConversationRuntimeTarget, envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
|
|
74
74
|
checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
|
|
75
|
+
contextCompress(target: ConversationRuntimeTarget, options?: {
|
|
76
|
+
keepRecent?: number;
|
|
77
|
+
force?: boolean;
|
|
78
|
+
}): Promise<Record<string, unknown>>;
|
|
75
79
|
rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
|
|
76
80
|
setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
|
|
77
81
|
setMode(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
|
|
@@ -317,6 +317,10 @@ class WslAgentClient {
|
|
|
317
317
|
await this.start();
|
|
318
318
|
return await this.request('checkpoint', { target: await this.mapTarget(target) }, 5_000);
|
|
319
319
|
}
|
|
320
|
+
async contextCompress(target, options = {}) {
|
|
321
|
+
await this.start();
|
|
322
|
+
return await this.request('context_compress', { target: await this.mapTarget(target), options }, 120_000);
|
|
323
|
+
}
|
|
320
324
|
async rateAutoRoute(target, score, routeId = '') {
|
|
321
325
|
await this.start();
|
|
322
326
|
return await this.request('rate_auto_route', {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
|
|
2
|
-
import { AgentPromptMessage, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationStopResult } from './conversationKernel';
|
|
2
|
+
import { AgentPromptMessage, ConversationContextCompressOptions, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationStopResult } from './conversationKernel';
|
|
3
3
|
import { ConversationRuntimeTarget } from './conversationTarget';
|
|
4
4
|
import { TerminalTakeoverEvent, TerminalTakeoverOwnerFilter, TerminalTakeoverState } from '../tools/terminalTakeover';
|
|
5
5
|
import { BrowserUseRequest } from './browserUse';
|
|
@@ -114,6 +114,13 @@ export type WslAgentRequest = {
|
|
|
114
114
|
params: {
|
|
115
115
|
target: ConversationRuntimeTarget;
|
|
116
116
|
};
|
|
117
|
+
} | {
|
|
118
|
+
id: string;
|
|
119
|
+
method: 'context_compress';
|
|
120
|
+
params: {
|
|
121
|
+
target: ConversationRuntimeTarget;
|
|
122
|
+
options?: ConversationContextCompressOptions;
|
|
123
|
+
};
|
|
117
124
|
} | {
|
|
118
125
|
id: string;
|
|
119
126
|
method: 'rate_auto_route';
|
|
@@ -11,6 +11,10 @@ export interface WslTargetRuntimeClient {
|
|
|
11
11
|
requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslAgentStopResult>;
|
|
12
12
|
enqueueGuide(target: ConversationRuntimeTarget, envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
|
|
13
13
|
checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
|
|
14
|
+
contextCompress?(target: ConversationRuntimeTarget, options?: {
|
|
15
|
+
keepRecent?: number;
|
|
16
|
+
force?: boolean;
|
|
17
|
+
}): Promise<Record<string, unknown>>;
|
|
14
18
|
rateAutoRoute?(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
|
|
15
19
|
setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
|
|
16
20
|
setMode?(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
|
|
@@ -69,6 +73,10 @@ export declare class WslAgentRuntimePool {
|
|
|
69
73
|
requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslPoolStopResult>;
|
|
70
74
|
enqueueGuide(envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
|
|
71
75
|
checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
|
|
76
|
+
contextCompress(target: ConversationRuntimeTarget, options?: {
|
|
77
|
+
keepRecent?: number;
|
|
78
|
+
force?: boolean;
|
|
79
|
+
}): Promise<Record<string, unknown>>;
|
|
72
80
|
rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
|
|
73
81
|
setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
|
|
74
82
|
setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next' | null>;
|
|
@@ -214,6 +214,21 @@ class WslAgentRuntimePool {
|
|
|
214
214
|
this.release(entry, true);
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
async contextCompress(target, options = {}) {
|
|
218
|
+
const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
|
|
219
|
+
const entry = await this.acquire(normalized);
|
|
220
|
+
try {
|
|
221
|
+
if (entry.stopIntent)
|
|
222
|
+
return { ok: false, error: 'Context compression is unavailable while this conversation is stopping.' };
|
|
223
|
+
if (!entry.client.contextCompress)
|
|
224
|
+
return { ok: false, error: 'Context compression is unavailable in this runtime.' };
|
|
225
|
+
entry.lastSnapshot = null;
|
|
226
|
+
return await entry.client.contextCompress(normalized, options);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
this.release(entry, true);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
217
232
|
async rateAutoRoute(target, score, routeId = '') {
|
|
218
233
|
const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
|
|
219
234
|
const entry = await this.acquireExisting(normalized);
|
package/dist/launcher.js
CHANGED
|
@@ -49,6 +49,7 @@ const cli_help_1 = require("./cli-help");
|
|
|
49
49
|
const rawArgs = process.argv.slice(2);
|
|
50
50
|
const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
|
|
51
51
|
const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
|
|
52
|
+
const cliCommand = args.find(a => cli_commands_1.CLI_COMMANDS.includes(a));
|
|
52
53
|
const isEdit = args[0] === 'edit';
|
|
53
54
|
const editFile = isEdit ? args[1] : '';
|
|
54
55
|
const isFlow = args[0] === 'flow';
|
|
@@ -64,6 +65,13 @@ if (invalidArgument) {
|
|
|
64
65
|
console.error(`Invalid Newmark argument: ${invalidArgument}`);
|
|
65
66
|
process.exit(2);
|
|
66
67
|
}
|
|
68
|
+
// Command help is a terminating, read-only discovery operation. Resolve it
|
|
69
|
+
// before first-run initialization or GUI forwarding so `Newmark.exe send
|
|
70
|
+
// --help` cannot accidentally enter Electron or instantiate an Agent.
|
|
71
|
+
if (hasCliCommand && cliCommand && (0, cli_commands_1.cliHelpRequested)(args)) {
|
|
72
|
+
console.log((0, cli_commands_1.cliCommandHelp)(cliCommand));
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
67
75
|
function pathArgValue(values, key) {
|
|
68
76
|
const prefix = `${key}=`;
|
|
69
77
|
const inlineIdx = values.findIndex(a => a.startsWith(prefix));
|
package/dist/llm/provider.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export declare class LLMProvider {
|
|
|
97
97
|
private toTransportResponse;
|
|
98
98
|
private toNormalizedMessages;
|
|
99
99
|
private toNormalizedTools;
|
|
100
|
-
chatStreamWithTools(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, tools: unknown[], signal?: AbortSignal, reasoningTier?: string): AsyncGenerator<StreamToken>;
|
|
100
|
+
chatStreamWithTools(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, tools: unknown[], signal?: AbortSignal, reasoningTier?: string, sessionId?: string): AsyncGenerator<StreamToken>;
|
|
101
101
|
/**
|
|
102
102
|
* GitHub Models streaming path. Preserved as a dedicated implementation
|
|
103
103
|
* because the provider adapters (V2) do not serialize the GitHub Models
|
package/dist/llm/provider.js
CHANGED
|
@@ -843,7 +843,7 @@ class LLMProvider {
|
|
|
843
843
|
* The emitted request body and StreamToken stream are byte-equivalent to
|
|
844
844
|
* the legacy inlined path.
|
|
845
845
|
*/
|
|
846
|
-
async *chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier) {
|
|
846
|
+
async *chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId) {
|
|
847
847
|
const mode = this.openAITransportMode();
|
|
848
848
|
if (mode === 'responses') {
|
|
849
849
|
yield* this.adapterResponsesBridge(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier);
|
|
@@ -861,6 +861,7 @@ class LLMProvider {
|
|
|
861
861
|
maxOutputTokens: maxTokens,
|
|
862
862
|
apiKey: this.apiKey,
|
|
863
863
|
baseUrl: this.cleanBaseUrl(),
|
|
864
|
+
...(sessionId ? { sessionId } : {}),
|
|
864
865
|
};
|
|
865
866
|
const serialized = await adapter.serializeRequest(request);
|
|
866
867
|
serialized.body.stream = mode === 'chat' ? false : true;
|
|
@@ -1080,7 +1081,7 @@ class LLMProvider {
|
|
|
1080
1081
|
};
|
|
1081
1082
|
});
|
|
1082
1083
|
}
|
|
1083
|
-
async *chatStreamWithTools(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier) {
|
|
1084
|
+
async *chatStreamWithTools(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId) {
|
|
1084
1085
|
if (signal?.aborted)
|
|
1085
1086
|
throw abortFailure(signal);
|
|
1086
1087
|
if (this.protocol() === 'anthropic') {
|
|
@@ -1092,7 +1093,7 @@ class LLMProvider {
|
|
|
1092
1093
|
return;
|
|
1093
1094
|
}
|
|
1094
1095
|
if (this.useProviderAdaptersV2) {
|
|
1095
|
-
yield* this.chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier);
|
|
1096
|
+
yield* this.chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId);
|
|
1096
1097
|
return;
|
|
1097
1098
|
}
|
|
1098
1099
|
throw new Error('LLMProvider legacy OpenAI streaming was removed in dev-0.3.0: enable provider_adapters_v2 (useProviderAdaptersV2).');
|