newmark-agent 0.3.11 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.example.json +6 -0
- package/dist/cli-commands.d.ts +8 -0
- package/dist/cli-commands.js +216 -16
- package/dist/cli-discovery.d.ts +15 -0
- package/dist/cli-discovery.js +182 -0
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.js +25 -1
- package/dist/context/domain/types.d.ts +37 -0
- package/dist/context/services/context-orchestrator.js +2 -0
- package/dist/conversation-utility-host.bundle.cjs +1503 -214
- package/dist/conversation-utility-host.js +3 -0
- package/dist/core/agent.d.ts +157 -8
- package/dist/core/agent.js +1176 -112
- package/dist/core/agentKernel/agent-loop.js +29 -3
- package/dist/core/agentKernel/types.d.ts +7 -0
- package/dist/core/agentKernelRunner.d.ts +2 -0
- package/dist/core/agentKernelRunner.js +174 -27
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.js +24 -6
- package/dist/core/conversationKernel.d.ts +5 -0
- package/dist/core/conversationKernel.js +30 -1
- package/dist/core/dshCompatibility.d.ts +198 -0
- package/dist/core/dshCompatibility.js +600 -0
- package/dist/core/electronUtilityAgentClient.d.ts +4 -0
- package/dist/core/electronUtilityAgentClient.js +4 -0
- package/dist/core/electronUtilityRuntimePool.d.ts +19 -0
- package/dist/core/electronUtilityRuntimePool.js +76 -0
- package/dist/core/flow-runner.js +1 -1
- package/dist/core/mcpManager.d.ts +1 -0
- package/dist/core/mcpManager.js +100 -10
- package/dist/core/modelValidationStore.d.ts +4 -1
- package/dist/core/modelValidationStore.js +7 -1
- package/dist/core/subagent.d.ts +6 -0
- package/dist/core/subagent.js +22 -1
- package/dist/core/toolPolicy.d.ts +6 -0
- package/dist/core/toolPolicy.js +49 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/utilityAgentProtocol.d.ts +8 -1
- package/dist/core/workspace.d.ts +15 -0
- package/dist/core/workspace.js +62 -1
- package/dist/core/wslAgentClient.d.ts +4 -0
- package/dist/core/wslAgentClient.js +4 -0
- package/dist/core/wslAgentProtocol.d.ts +8 -1
- package/dist/core/wslAgentRuntimePool.d.ts +12 -0
- package/dist/core/wslAgentRuntimePool.js +71 -0
- package/dist/launcher.js +48 -11
- package/dist/llm/provider.d.ts +9 -6
- package/dist/llm/provider.js +89 -36
- package/dist/main.js +326 -52
- package/dist/preload.js +17 -0
- package/dist/providers/chat-completions.adapter.js +42 -20
- package/dist/providers/provider-adapter.d.ts +3 -0
- package/dist/providers/provider-events.d.ts +7 -0
- package/dist/providers/provider-events.js +44 -0
- package/dist/providers/responses.adapter.js +1 -3
- package/dist/toolchain/registry/tool-registry.d.ts +13 -1
- package/dist/toolchain/registry/tool-registry.js +8 -0
- package/dist/toolchain/registry-seeder.js +51 -5
- package/dist/tools/index.js +11 -2
- package/dist/tools/nativeTools.js +5 -1
- package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
- package/dist/tui/src/app.js +47 -13
- package/dist/tui/src/render.js +23 -7
- package/dist/tui/src/state.js +61 -9
- package/dist/ui/index.html +2775 -284
- package/dist/ui/lucide-sprite.svg +26 -0
- package/dist/wsl-agent-host.bundle.cjs +1503 -214
- package/dist/wsl-agent-host.js +3 -0
- package/package.json +16 -5
|
@@ -16,6 +16,7 @@ class ElectronUtilityRuntimePool {
|
|
|
16
16
|
restarting = new Set();
|
|
17
17
|
quarantined = new Map();
|
|
18
18
|
disposing = new Set();
|
|
19
|
+
forceStopPromises = new Map();
|
|
19
20
|
capacityTail = Promise.resolve();
|
|
20
21
|
accessSequence = 0;
|
|
21
22
|
constructor(root, hostScript, createClient = target => new electronUtilityAgentClient_1.ElectronUtilityAgentClient(root, hostScript, target), options = {}) {
|
|
@@ -202,6 +203,20 @@ class ElectronUtilityRuntimePool {
|
|
|
202
203
|
this.release(entry, true);
|
|
203
204
|
}
|
|
204
205
|
}
|
|
206
|
+
async contextCompress(target, options = {}) {
|
|
207
|
+
const entry = await this.acquire((0, conversationTarget_1.normalizeConversationTarget)(target));
|
|
208
|
+
try {
|
|
209
|
+
if (entry.stopIntent)
|
|
210
|
+
return { ok: false, error: 'Context compression is unavailable while this conversation is stopping.' };
|
|
211
|
+
if (!entry.client.contextCompress)
|
|
212
|
+
return { ok: false, error: 'Context compression is unavailable in this runtime.' };
|
|
213
|
+
entry.lastSnapshot = null;
|
|
214
|
+
return await entry.client.contextCompress(options);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
this.release(entry, true);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
205
220
|
async rateAutoRoute(target, score, routeId = '') {
|
|
206
221
|
const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
|
|
207
222
|
const entry = await this.acquireExisting(normalized);
|
|
@@ -342,6 +357,67 @@ class ElectronUtilityRuntimePool {
|
|
|
342
357
|
if (entry)
|
|
343
358
|
await this.stopEntry(entry);
|
|
344
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Immediately terminate a target runtime for destructive lifecycle actions.
|
|
362
|
+
* Archive is deliberately stronger than the user-facing two-click Stop
|
|
363
|
+
* contract: the target may be running, already stopping, or holding an
|
|
364
|
+
* active prompt lease. Give the kernel only the short checkpoint window
|
|
365
|
+
* owned by forceTerminateEntry, then hard-stop and evict the client so no
|
|
366
|
+
* delayed runtime event can keep the archived target resident.
|
|
367
|
+
*/
|
|
368
|
+
async forceStopTarget(target) {
|
|
369
|
+
const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
|
|
370
|
+
const existing = this.forceStopPromises.get(normalized.runtimeKey);
|
|
371
|
+
if (existing)
|
|
372
|
+
return existing;
|
|
373
|
+
const operation = this.forceStopTargetInternal(normalized);
|
|
374
|
+
this.forceStopPromises.set(normalized.runtimeKey, operation);
|
|
375
|
+
try {
|
|
376
|
+
await operation;
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
if (this.forceStopPromises.get(normalized.runtimeKey) === operation) {
|
|
380
|
+
this.forceStopPromises.delete(normalized.runtimeKey);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async forceStopTargetInternal(target) {
|
|
385
|
+
let entry;
|
|
386
|
+
await this.serializeCapacity(async () => {
|
|
387
|
+
entry = this.entries.get(target.runtimeKey);
|
|
388
|
+
if (entry)
|
|
389
|
+
this.disposing.add(target.runtimeKey);
|
|
390
|
+
});
|
|
391
|
+
if (!entry)
|
|
392
|
+
return;
|
|
393
|
+
const intent = entry.stopIntent || {
|
|
394
|
+
runId: entry.lastRunId,
|
|
395
|
+
generation: entry.lastGeneration,
|
|
396
|
+
checkpointed: false,
|
|
397
|
+
forcePromise: null,
|
|
398
|
+
};
|
|
399
|
+
if (!entry.stopIntent)
|
|
400
|
+
entry.stopIntent = intent;
|
|
401
|
+
try {
|
|
402
|
+
await this.forceTerminateEntry(entry, intent);
|
|
403
|
+
// forceStop is expected to disconnect the utility child. If a client
|
|
404
|
+
// reports a stale connected flag, retry the hard boundary once before
|
|
405
|
+
// declaring archive unsafe.
|
|
406
|
+
if (entry.client.status().connected)
|
|
407
|
+
await entry.client.forceStop();
|
|
408
|
+
if (entry.client.status().connected) {
|
|
409
|
+
throw new Error(`Electron utility runtime ${target.runtimeKey} remained connected after force stop`);
|
|
410
|
+
}
|
|
411
|
+
if (this.entries.get(target.runtimeKey) === entry) {
|
|
412
|
+
this.entries.delete(target.runtimeKey);
|
|
413
|
+
entry.unsubscribe();
|
|
414
|
+
entry.client.setHostToolHandler(null);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
this.disposing.delete(target.runtimeKey);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
345
421
|
async stopAll() {
|
|
346
422
|
const targets = Array.from(this.entries.values(), entry => entry.target);
|
|
347
423
|
const results = await Promise.allSettled(targets.map(target => this.stopTarget(target)));
|
package/dist/core/flow-runner.js
CHANGED
|
@@ -126,7 +126,7 @@ async function runFlowBuild(agent, prompt, options) {
|
|
|
126
126
|
if (!options.signal?.aborted && typeof agent.emitWorkEvent === 'function') {
|
|
127
127
|
agent.emitWorkEvent({ type: 'error', content: reportedError.message, runId });
|
|
128
128
|
}
|
|
129
|
-
agent.finishConversationWorkRun(runId, options.signal?.aborted ? 'interrupted' : 'error');
|
|
129
|
+
agent.finishConversationWorkRun(runId, options.signal?.aborted ? 'interrupted' : 'error', undefined, options.signal?.aborted ? '' : reportedError.message);
|
|
130
130
|
agent.flushWorkspaceConversationState();
|
|
131
131
|
}
|
|
132
132
|
throw reportedError;
|
|
@@ -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
|
}
|
|
@@ -2,8 +2,11 @@ import { ModelValidationCache, ModelValidationRecord } from './modelValidation';
|
|
|
2
2
|
/** Durable validation evidence. The file contains probe metadata only, never prompts, tool arguments or credentials. */
|
|
3
3
|
export declare class FileModelValidationCache implements ModelValidationCache {
|
|
4
4
|
private readonly filePath;
|
|
5
|
+
private readonly readOnly;
|
|
5
6
|
private readonly records;
|
|
6
|
-
constructor(rootPath: string
|
|
7
|
+
constructor(rootPath: string, options?: {
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
});
|
|
7
10
|
get(modelKey: string): ModelValidationRecord | undefined;
|
|
8
11
|
set(record: ModelValidationRecord): void;
|
|
9
12
|
delete(modelKey: string): void;
|
|
@@ -42,9 +42,11 @@ function key(model) {
|
|
|
42
42
|
/** Durable validation evidence. The file contains probe metadata only, never prompts, tool arguments or credentials. */
|
|
43
43
|
class FileModelValidationCache {
|
|
44
44
|
filePath;
|
|
45
|
+
readOnly;
|
|
45
46
|
records = new Map();
|
|
46
|
-
constructor(rootPath) {
|
|
47
|
+
constructor(rootPath, options = {}) {
|
|
47
48
|
this.filePath = path.join(rootPath, 'model-validation', 'records.json');
|
|
49
|
+
this.readOnly = options.readOnly === true;
|
|
48
50
|
this.load();
|
|
49
51
|
}
|
|
50
52
|
get(modelKey) {
|
|
@@ -53,11 +55,15 @@ class FileModelValidationCache {
|
|
|
53
55
|
}
|
|
54
56
|
set(record) {
|
|
55
57
|
this.records.set(record.modelKey || key(record.model), JSON.parse(JSON.stringify(record)));
|
|
58
|
+
if (this.readOnly)
|
|
59
|
+
return;
|
|
56
60
|
this.save();
|
|
57
61
|
}
|
|
58
62
|
delete(modelKey) {
|
|
59
63
|
if (!this.records.delete(modelKey))
|
|
60
64
|
return;
|
|
65
|
+
if (this.readOnly)
|
|
66
|
+
return;
|
|
61
67
|
this.save();
|
|
62
68
|
}
|
|
63
69
|
load() {
|
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;
|
|
@@ -52,6 +61,12 @@ export declare class WorkspaceManager {
|
|
|
52
61
|
private saveState;
|
|
53
62
|
private findWorkspace;
|
|
54
63
|
private restoreCurrent;
|
|
64
|
+
/**
|
|
65
|
+
* Re-read the registry and persisted current-workspace pointer after another
|
|
66
|
+
* Newmark entrypoint updates Work/*.json. This intentionally does not create
|
|
67
|
+
* a workspace: a refresh must reflect the shared on-disk state exactly.
|
|
68
|
+
*/
|
|
69
|
+
reloadFromStorage(): WorkspaceInfo | null;
|
|
55
70
|
private saveInternal;
|
|
56
71
|
private saveExternal;
|
|
57
72
|
private sleepSync;
|
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;
|
|
@@ -358,6 +405,20 @@ class WorkspaceManager {
|
|
|
358
405
|
this.saveState();
|
|
359
406
|
}
|
|
360
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Re-read the registry and persisted current-workspace pointer after another
|
|
410
|
+
* Newmark entrypoint updates Work/*.json. This intentionally does not create
|
|
411
|
+
* a workspace: a refresh must reflect the shared on-disk state exactly.
|
|
412
|
+
*/
|
|
413
|
+
reloadFromStorage() {
|
|
414
|
+
if (this.detached)
|
|
415
|
+
return this.current;
|
|
416
|
+
this.scan();
|
|
417
|
+
this.validate();
|
|
418
|
+
this.current = null;
|
|
419
|
+
this.restoreCurrent();
|
|
420
|
+
return this.current;
|
|
421
|
+
}
|
|
361
422
|
saveInternal() {
|
|
362
423
|
if (this.detached)
|
|
363
424
|
return;
|
|
@@ -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>;
|