@myagentroam/agent 0.9.136 → 0.9.138
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/host/local-agent-host.d.ts +5 -0
- package/dist/host/local-agent-host.js +2 -1
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.js +2 -2
- package/dist/prompts/resources.d.ts +1 -0
- package/dist/prompts/resources.js +8 -2
- package/dist/prompts/workflow.d.ts +1 -1
- package/dist/prompts/workflow.js +7 -2
- package/dist/runtime/context-gc.js +10 -6
- package/dist/runtime/context-projection.js +1 -1
- package/dist/sdk/agent.js +163 -88
- package/dist/sdk/types.d.ts +17 -2
- package/dist/session/context-gc-checkpoint.d.ts +1 -1
- package/dist/session/context-gc-checkpoint.js +11 -5
- package/dist/session/context-gc-replacements.d.ts +2 -0
- package/dist/session/context-gc-replacements.js +18 -0
- package/dist/session/jsonl-store.d.ts +5 -2
- package/dist/session/jsonl-store.js +105 -12
- package/dist/subagent/session-controller.d.ts +2 -0
- package/dist/subagent/session-controller.js +23 -6
- package/dist/tools/callers.d.ts +2 -0
- package/dist/tools/callers.js +7 -1
- package/dist/tools/code-mode/cells.d.ts +20 -0
- package/dist/tools/code-mode/cells.js +250 -0
- package/dist/tools/code-mode/dispatch.d.ts +21 -0
- package/dist/tools/code-mode/dispatch.js +53 -0
- package/dist/tools/code-mode/engine.d.ts +1 -0
- package/dist/tools/code-mode/quickjs-engine.js +4 -0
- package/dist/tools/code-mode/worker.js +1 -0
- package/dist/tools/code-mode.d.ts +15 -4
- package/dist/tools/code-mode.js +83 -17
- package/dist/tools/execution-limits.d.ts +5 -0
- package/dist/tools/execution-limits.js +5 -0
- package/dist/tools/local-registry.d.ts +5 -0
- package/dist/tools/local-registry.js +5 -1
- package/dist/tools/registry.js +2 -1
- package/dist/tools/scheduler.d.ts +2 -0
- package/dist/tools/scheduler.js +11 -0
- package/dist/tools/tool-search.js +12 -6
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { MarAgentError, describeMarAgentError } from '../../error.js';
|
|
3
|
+
import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
|
|
4
|
+
import { truncateModelText } from '../../runtime/token-budget.js';
|
|
5
|
+
function options(value, wait) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Code Exec options must be an object.');
|
|
8
|
+
const input = value;
|
|
9
|
+
const keys = wait
|
|
10
|
+
? ['cellId', 'yieldMs', 'maxOutputTokens', 'action']
|
|
11
|
+
: ['yieldMs', 'maxOutputTokens'];
|
|
12
|
+
if (Object.keys(input).some((key) => !keys.includes(key)))
|
|
13
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Unknown Code Exec option.');
|
|
14
|
+
const yieldMs = input.yieldMs ?? limits.codeDefaultYieldMs;
|
|
15
|
+
const maxOutputTokens = input.maxOutputTokens ?? limits.codeMaxOutputTokens;
|
|
16
|
+
if (!Number.isSafeInteger(yieldMs) ||
|
|
17
|
+
Number(yieldMs) < 0 ||
|
|
18
|
+
Number(yieldMs) > limits.codeMaxYieldMs ||
|
|
19
|
+
!Number.isSafeInteger(maxOutputTokens) ||
|
|
20
|
+
Number(maxOutputTokens) < 1 ||
|
|
21
|
+
Number(maxOutputTokens) > limits.codeMaxOutputTokens ||
|
|
22
|
+
(wait && (typeof input.cellId !== 'string' || !input.cellId)) ||
|
|
23
|
+
(input.action !== undefined && input.action !== 'poll' && input.action !== 'cancel'))
|
|
24
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Code Exec options are out of range.');
|
|
25
|
+
return {
|
|
26
|
+
cellId: input.cellId,
|
|
27
|
+
yieldMs: Number(yieldMs),
|
|
28
|
+
maxOutputTokens: Number(maxOutputTokens),
|
|
29
|
+
action: input.action ?? 'poll'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Session-owned execution, separate from the cancellable model-tool wait. */
|
|
33
|
+
export class CodeCells {
|
|
34
|
+
#cells = new Map();
|
|
35
|
+
#closed = false;
|
|
36
|
+
#disposing = new Set();
|
|
37
|
+
async start(value, context, run) {
|
|
38
|
+
const source = typeof value === 'string'
|
|
39
|
+
? value
|
|
40
|
+
: value &&
|
|
41
|
+
typeof value === 'object' &&
|
|
42
|
+
!Array.isArray(value) &&
|
|
43
|
+
Object.keys(value).length === 1 &&
|
|
44
|
+
'code' in value
|
|
45
|
+
? value.code
|
|
46
|
+
: undefined;
|
|
47
|
+
if (typeof source !== 'string' ||
|
|
48
|
+
!source.trim() ||
|
|
49
|
+
Buffer.byteLength(source) > limits.codeMaxInputBytes)
|
|
50
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Code must be a nonempty bounded JavaScript string.');
|
|
51
|
+
let settings = options({}, false);
|
|
52
|
+
const pragma = /^\/\/ @options:\s*([^\r\n]*)(?:\r?\n|$)/u.exec(source);
|
|
53
|
+
if (pragma) {
|
|
54
|
+
try {
|
|
55
|
+
settings = options(JSON.parse(pragma[1]), false);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof MarAgentError)
|
|
59
|
+
throw error;
|
|
60
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Code Exec options must be valid JSON.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
context.signal.throwIfAborted();
|
|
64
|
+
if (this.#closed || this.#disposing.has(context.sessionId))
|
|
65
|
+
throw new MarAgentError('MAR_AGENT_CODE_CANCELLED', 'Code Exec resources are being released.');
|
|
66
|
+
if ([...this.#cells.values()].filter((cell) => cell.status === 'running').length >=
|
|
67
|
+
limits.codeMaxConcurrentCells)
|
|
68
|
+
throw new MarAgentError('MAR_AGENT_PROCESS_LIMIT', 'The Code Exec cell limit was reached.');
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const cell = {
|
|
71
|
+
cellId: randomUUID(),
|
|
72
|
+
sessionId: context.sessionId,
|
|
73
|
+
executionId: context.executionId,
|
|
74
|
+
controller,
|
|
75
|
+
status: 'running',
|
|
76
|
+
text: [],
|
|
77
|
+
images: [],
|
|
78
|
+
yielded: false,
|
|
79
|
+
listeners: new Set(),
|
|
80
|
+
done: Promise.resolve()
|
|
81
|
+
};
|
|
82
|
+
this.#cells.set(cell.cellId, cell);
|
|
83
|
+
const notify = () => {
|
|
84
|
+
for (const listener of cell.listeners)
|
|
85
|
+
listener();
|
|
86
|
+
};
|
|
87
|
+
let releaseSession;
|
|
88
|
+
let outcome = 'running';
|
|
89
|
+
cell.done = Promise.resolve()
|
|
90
|
+
.then(async () => {
|
|
91
|
+
releaseSession = await context.codeMode?.retainSession?.();
|
|
92
|
+
return run(source, { ...context, signal: controller.signal }, {
|
|
93
|
+
text: (text) => cell.text.push(text),
|
|
94
|
+
image: (image) => cell.images.push(image),
|
|
95
|
+
yield: () => {
|
|
96
|
+
cell.yielded = true;
|
|
97
|
+
notify();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
})
|
|
101
|
+
.then((result) => {
|
|
102
|
+
cell.result = {
|
|
103
|
+
content: '',
|
|
104
|
+
...(result.data === undefined ? {} : { data: result.data })
|
|
105
|
+
};
|
|
106
|
+
outcome = 'completed';
|
|
107
|
+
}, (error) => {
|
|
108
|
+
cell.error = error;
|
|
109
|
+
outcome = controller.signal.aborted ? 'cancelled' : 'failed';
|
|
110
|
+
})
|
|
111
|
+
.then(async () => {
|
|
112
|
+
try {
|
|
113
|
+
await context.codeMode?.onSettled?.({ cellId: cell.cellId, status: outcome });
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
cell.error = error;
|
|
117
|
+
outcome = 'failed';
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await releaseSession?.();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
cell.error = error;
|
|
124
|
+
outcome = 'failed';
|
|
125
|
+
}
|
|
126
|
+
cell.status = outcome;
|
|
127
|
+
// Terminal capacity follows completion order, not the original start order.
|
|
128
|
+
this.#cells.delete(cell.cellId);
|
|
129
|
+
this.#cells.set(cell.cellId, cell);
|
|
130
|
+
cell.retention = setTimeout(() => this.#cells.delete(cell.cellId), limits.codeCompletedRetentionMs);
|
|
131
|
+
cell.retention.unref();
|
|
132
|
+
const completed = [...this.#cells.values()].filter((entry) => entry.sessionId === cell.sessionId && entry.status !== 'running');
|
|
133
|
+
for (const entry of completed.slice(0, Math.max(0, completed.length - limits.codeMaxCompletedResults))) {
|
|
134
|
+
clearTimeout(entry.retention);
|
|
135
|
+
this.#cells.delete(entry.cellId);
|
|
136
|
+
}
|
|
137
|
+
notify();
|
|
138
|
+
});
|
|
139
|
+
return this.#wait(cell, settings, context.signal, true, context.codeMode?.supportsImages === true);
|
|
140
|
+
}
|
|
141
|
+
async wait(value, context) {
|
|
142
|
+
const input = options(value, true);
|
|
143
|
+
context.signal.throwIfAborted();
|
|
144
|
+
const cell = this.#cells.get(input.cellId);
|
|
145
|
+
if (!cell || cell.sessionId !== context.sessionId)
|
|
146
|
+
throw new MarAgentError('MAR_AGENT_CODE_CELL_NOT_FOUND', 'Code Exec cell is unavailable in this Session.');
|
|
147
|
+
if (input.action === 'cancel' && cell.status === 'running') {
|
|
148
|
+
cell.controller.abort(new MarAgentError('MAR_AGENT_CODE_CANCELLED', 'Code Exec cell was terminated.'));
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
const abort = () => reject(context.signal.reason);
|
|
151
|
+
context.signal.addEventListener('abort', abort, { once: true });
|
|
152
|
+
void cell.done.then(() => {
|
|
153
|
+
context.signal.removeEventListener('abort', abort);
|
|
154
|
+
resolve();
|
|
155
|
+
});
|
|
156
|
+
if (context.signal.aborted)
|
|
157
|
+
abort();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return this.#wait(cell, input, context.signal, false, context.codeMode?.supportsImages === true);
|
|
161
|
+
}
|
|
162
|
+
async #wait(cell, input, signal, initial, supportsImages) {
|
|
163
|
+
if (cell.status === 'running' && !cell.yielded) {
|
|
164
|
+
await new Promise((resolve, reject) => {
|
|
165
|
+
const finish = () => {
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
cell.listeners.delete(finish);
|
|
168
|
+
signal.removeEventListener('abort', abort);
|
|
169
|
+
resolve();
|
|
170
|
+
};
|
|
171
|
+
const abort = () => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
cell.listeners.delete(finish);
|
|
174
|
+
signal.removeEventListener('abort', abort);
|
|
175
|
+
reject(signal.reason);
|
|
176
|
+
};
|
|
177
|
+
const timer = setTimeout(finish, input.yieldMs);
|
|
178
|
+
cell.listeners.add(finish);
|
|
179
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
180
|
+
if (signal.aborted)
|
|
181
|
+
abort();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
signal.throwIfAborted();
|
|
185
|
+
if (!supportsImages && (cell.images.length || cell.terminal?.images?.length))
|
|
186
|
+
throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'The current model cannot receive this cell image output.');
|
|
187
|
+
if (cell.terminal) {
|
|
188
|
+
const snapshot = cell.terminal.data;
|
|
189
|
+
const bounded = truncateModelText(snapshot.output, input.maxOutputTokens);
|
|
190
|
+
const data = { ...snapshot, output: bounded.content, cached: true };
|
|
191
|
+
return {
|
|
192
|
+
...cell.terminal,
|
|
193
|
+
data,
|
|
194
|
+
content: JSON.stringify(data),
|
|
195
|
+
truncated: cell.terminal.truncated || bounded.truncated
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const text = cell.text.splice(0).join('\n');
|
|
199
|
+
const images = cell.images.splice(0);
|
|
200
|
+
cell.yielded = false;
|
|
201
|
+
const bounded = truncateModelText(text, input.maxOutputTokens);
|
|
202
|
+
const error = cell.error === undefined
|
|
203
|
+
? undefined
|
|
204
|
+
: describeMarAgentError(cell.error, 'MAR_AGENT_CODE_FAILED');
|
|
205
|
+
const data = {
|
|
206
|
+
cellId: cell.cellId,
|
|
207
|
+
status: cell.status,
|
|
208
|
+
output: bounded.content,
|
|
209
|
+
...cell.result?.data,
|
|
210
|
+
...(error ? { error: { code: error.code, message: error.message } } : {}),
|
|
211
|
+
cached: false
|
|
212
|
+
};
|
|
213
|
+
const result = {
|
|
214
|
+
content: JSON.stringify(data),
|
|
215
|
+
data,
|
|
216
|
+
truncated: bounded.truncated,
|
|
217
|
+
...(images.length ? { images } : {})
|
|
218
|
+
};
|
|
219
|
+
if (cell.status !== 'running')
|
|
220
|
+
cell.terminal = result;
|
|
221
|
+
// Keep synchronous completion compatible; retained waits always include explicit lifecycle metadata.
|
|
222
|
+
if (initial && cell.status !== 'running') {
|
|
223
|
+
if (cell.error !== undefined)
|
|
224
|
+
throw cell.error;
|
|
225
|
+
return { ...result, content: bounded.content || 'Code Exec completed without emitted text.' };
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
snapshot(sessionId) {
|
|
230
|
+
return [...this.#cells.values()]
|
|
231
|
+
.filter((cell) => cell.sessionId === sessionId)
|
|
232
|
+
.map(({ cellId, status, executionId }) => ({ cellId, status, executionId }));
|
|
233
|
+
}
|
|
234
|
+
async dispose(sessionId) {
|
|
235
|
+
if (sessionId === undefined)
|
|
236
|
+
this.#closed = true;
|
|
237
|
+
else
|
|
238
|
+
this.#disposing.add(sessionId);
|
|
239
|
+
const cells = [...this.#cells.values()].filter((cell) => sessionId === undefined || cell.sessionId === sessionId);
|
|
240
|
+
for (const cell of cells)
|
|
241
|
+
cell.controller.abort(new MarAgentError('MAR_AGENT_CODE_CANCELLED', 'Code Exec resources were released.'));
|
|
242
|
+
await Promise.all(cells.map((cell) => cell.done));
|
|
243
|
+
for (const cell of cells) {
|
|
244
|
+
clearTimeout(cell.retention);
|
|
245
|
+
this.#cells.delete(cell.cellId);
|
|
246
|
+
}
|
|
247
|
+
if (sessionId !== undefined)
|
|
248
|
+
this.#disposing.delete(sessionId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ClientToolDefinition } from '../../model/contracts.js';
|
|
2
|
+
import type { MarAgentEvent, ToolCall, ToolExecutionResult, McpToolCallContext } from '../../sdk/types.js';
|
|
3
|
+
import type { ToolRuntimeContext } from '../runtime-context.js';
|
|
4
|
+
import type { createSessionToolScheduler } from '../scheduler.js';
|
|
5
|
+
type ToolEvent = Extract<MarAgentEvent, {
|
|
6
|
+
type: 'tool.started' | 'tool.completed' | 'tool.failed';
|
|
7
|
+
}>;
|
|
8
|
+
type Payload<T = ToolEvent> = T extends ToolEvent ? Omit<T, 'eventId' | 'sequence' | 'timestamp' | 'sessionId' | 'executionId' | 'turnId'> : never;
|
|
9
|
+
/** Capture only the cell's immutable dispatch identity, never the model loop or its AbortSignal. */
|
|
10
|
+
export declare function createCodeToolDispatch(input: {
|
|
11
|
+
context: Omit<ToolRuntimeContext, 'signal' | 'codeMode' | 'expectedMcpTool' | 'searchMcpTools'>;
|
|
12
|
+
tools: readonly ClientToolDefinition[];
|
|
13
|
+
expectations: ReadonlyMap<string, McpToolCallContext['expectedTool']>;
|
|
14
|
+
parentCallId: string;
|
|
15
|
+
schedule: ReturnType<typeof createSessionToolScheduler>;
|
|
16
|
+
emit: (event: Payload) => Promise<void>;
|
|
17
|
+
projectData: (name: string, data: unknown) => Extract<ToolEvent, {
|
|
18
|
+
type: 'tool.completed';
|
|
19
|
+
}>['data'];
|
|
20
|
+
}): (call: ToolCall, signal: AbortSignal) => Promise<ToolExecutionResult>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { MarAgentError, describeMarAgentError } from '../../error.js';
|
|
3
|
+
import { executeRegisteredTool } from '../runtime.js';
|
|
4
|
+
import { allowsToolCaller } from '../callers.js';
|
|
5
|
+
import { truncateModelText } from '../../runtime/token-budget.js';
|
|
6
|
+
import { AGENT_EXECUTION_POLICY } from '../../runtime/execution-policy.js';
|
|
7
|
+
/** Capture only the cell's immutable dispatch identity, never the model loop or its AbortSignal. */
|
|
8
|
+
export function createCodeToolDispatch(input) {
|
|
9
|
+
const definitions = new Map(input.tools.map((tool) => [tool.name, tool]));
|
|
10
|
+
return async (call, signal) => {
|
|
11
|
+
const definition = definitions.get(call.name);
|
|
12
|
+
if (!definition || !allowsToolCaller(definition, 'programmatic'))
|
|
13
|
+
throw new MarAgentError('MAR_AGENT_TOOL_NOT_AVAILABLE', 'The requested nested tool is unavailable.');
|
|
14
|
+
return input.schedule(definition, async () => {
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
const callId = call.callId ?? randomUUID();
|
|
17
|
+
const identity = { toolName: call.name, callId, parentCallId: input.parentCallId };
|
|
18
|
+
await input.emit({ type: 'tool.started', ...identity, input: call.arguments });
|
|
19
|
+
try {
|
|
20
|
+
const expected = input.expectations.get(call.name);
|
|
21
|
+
const result = await executeRegisteredTool(call.name, call.arguments, callId, {
|
|
22
|
+
...input.context,
|
|
23
|
+
signal,
|
|
24
|
+
...(expected ? { expectedMcpTool: expected } : {})
|
|
25
|
+
});
|
|
26
|
+
const bounded = truncateModelText(result.content, AGENT_EXECUTION_POLICY.modelToolOutputTokens);
|
|
27
|
+
const data = input.projectData(call.name, result.data);
|
|
28
|
+
const summary = result.content.slice(0, AGENT_EXECUTION_POLICY.toolEventSummaryCharacters);
|
|
29
|
+
await input.emit({
|
|
30
|
+
type: 'tool.completed',
|
|
31
|
+
...identity,
|
|
32
|
+
summary,
|
|
33
|
+
...(data === undefined ? {} : { data }),
|
|
34
|
+
truncated: result.truncated === true ||
|
|
35
|
+
bounded.truncated ||
|
|
36
|
+
summary.length < result.content.length,
|
|
37
|
+
...(result.artifacts ? { artifacts: result.artifacts } : {})
|
|
38
|
+
});
|
|
39
|
+
return { ...result, content: bounded.content };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const diagnostic = describeMarAgentError(error, 'MAR_AGENT_HOST_OPERATION_FAILED');
|
|
43
|
+
await input.emit({
|
|
44
|
+
type: 'tool.failed',
|
|
45
|
+
...identity,
|
|
46
|
+
code: diagnostic.code,
|
|
47
|
+
message: diagnostic.message
|
|
48
|
+
});
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -29,6 +29,7 @@ export interface CodeModeEngine {
|
|
|
29
29
|
onToolCall(call: ToolCall): Promise<ToolExecutionResult>;
|
|
30
30
|
onText(value: string): void;
|
|
31
31
|
onImage(value: unknown): void;
|
|
32
|
+
onYield(): void;
|
|
32
33
|
}): Promise<{
|
|
33
34
|
readonly stateMutations: readonly CodeModeStateMutation[];
|
|
34
35
|
}>;
|
|
@@ -307,6 +307,10 @@ export async function createCodeModeEngine(options) {
|
|
|
307
307
|
vm.setProp(vm.global, 'catalog', catalog);
|
|
308
308
|
catalog.dispose();
|
|
309
309
|
const cell = vm.newObject();
|
|
310
|
+
method(cell, 'yield_control', () => {
|
|
311
|
+
input.onYield();
|
|
312
|
+
return vm.undefined;
|
|
313
|
+
});
|
|
310
314
|
method(cell, 'sleep', (durationHandle) => {
|
|
311
315
|
if (!durationHandle || vm.typeof(durationHandle) !== 'number')
|
|
312
316
|
return {
|
|
@@ -33,6 +33,7 @@ lines.on('line', async (line) => {
|
|
|
33
33
|
signal: controller.signal,
|
|
34
34
|
onText: (value) => send({ kind: 'text', value }),
|
|
35
35
|
onImage: (value) => send({ kind: 'image', value }),
|
|
36
|
+
onYield: () => send({ kind: 'yield' }),
|
|
36
37
|
onToolCall: (call) => new Promise((resolve, reject) => {
|
|
37
38
|
const id = ++sequence;
|
|
38
39
|
pending.set(id, { resolve, reject });
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { type LocalProcessExecutor } from '../host/local-process-executor.js';
|
|
2
|
-
import type { ClientToolDefinition } from '../model/contracts.js';
|
|
2
|
+
import type { ClientToolDefinition, ModelMessage } from '../model/contracts.js';
|
|
3
3
|
import type { ToolExecutionContext, ToolExecutionResult } from '../sdk/types.js';
|
|
4
4
|
import { WorkspacePathResolver } from './shared/path-resolver.js';
|
|
5
5
|
import type { CodeModeEngineFactory } from './code-mode/engine.js';
|
|
6
|
+
import { type CellOutput } from './code-mode/cells.js';
|
|
6
7
|
export declare const codeModeToolDefinition: ClientToolDefinition;
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
export declare const codeExecWaitToolDefinition: ClientToolDefinition;
|
|
9
|
+
/** Deferred definitions travel in search results, not in the stable Code Mode description. */
|
|
10
|
+
export declare function projectCodeModeTools(tools: readonly ClientToolDefinition[], residentTools?: readonly ClientToolDefinition[]): ClientToolDefinition[];
|
|
11
|
+
/** Preserve native search definitions while providing the same discovery evidence to other protocols. */
|
|
12
|
+
export declare function projectCodeModeSearchResult(message: ModelMessage, codeModeEnabled: boolean): ModelMessage;
|
|
9
13
|
export declare class CodeModeTool {
|
|
10
14
|
#private;
|
|
11
15
|
private readonly paths;
|
|
12
16
|
private readonly environment;
|
|
13
17
|
private readonly executor;
|
|
14
18
|
private readonly engineFactory;
|
|
19
|
+
start(value: unknown, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
20
|
+
wait(value: unknown, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
21
|
+
listSessionResources(sessionId: string): {
|
|
22
|
+
cellId: string;
|
|
23
|
+
status: "completed" | "cancelled" | "running" | "failed";
|
|
24
|
+
executionId: string;
|
|
25
|
+
}[];
|
|
15
26
|
constructor(paths: WorkspacePathResolver, environment?: NodeJS.ProcessEnv, executor?: LocalProcessExecutor, engineFactory?: CodeModeEngineFactory);
|
|
16
|
-
execute(value: unknown, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
27
|
+
execute(value: unknown, context: ToolExecutionContext, output?: CellOutput): Promise<ToolExecutionResult>;
|
|
17
28
|
dispose(sessionId?: string): Promise<void>;
|
|
18
29
|
}
|
package/dist/tools/code-mode.js
CHANGED
|
@@ -10,12 +10,14 @@ import { directLocalProcessExecutor, signalProcessTree } from '../host/local-pro
|
|
|
10
10
|
import { TOOL_EXECUTION_LIMITS as limits } from './execution-limits.js';
|
|
11
11
|
import { createToolCallScheduler } from './scheduler.js';
|
|
12
12
|
import { detectImageMime } from './view-image.js';
|
|
13
|
-
import { allowsToolCaller } from './callers.js';
|
|
13
|
+
import { allowsDirectTool, allowsToolCaller } from './callers.js';
|
|
14
|
+
import { CodeCells } from './code-mode/cells.js';
|
|
14
15
|
import { parseCodeModeStateMutations } from './code-mode/state.js';
|
|
15
16
|
export const codeModeToolDefinition = {
|
|
16
|
-
name: '
|
|
17
|
+
name: 'code_exec',
|
|
17
18
|
description: `Run JavaScript to orchestrate or compose tool calls.
|
|
18
|
-
-
|
|
19
|
+
- Code Mode Only: call the programmatic tools listed below through tools.NAME inside this tool, including single-tool operations. They are omitted from the top-level tool list. Direct-only tools remain separate model tools and cannot be called here.
|
|
20
|
+
- Use bounded combinations, parallel independent calls, and filtering of large intermediate results to reduce model round trips.
|
|
19
21
|
- Evaluates source in a fresh restricted ECMAScript runtime with top-level await and Promise.all. It is not Node.js or a browser. JavaScript globals do not survive between cells; bounded JSON state is available through state.
|
|
20
22
|
- Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences. Responses uses the source directly; Anthropic uses the code field of its tool input object. In either case, do not quote or fence the source again.
|
|
21
23
|
- Use only standard ECMAScript built-ins and the explicitly documented tools, out, state, catalog and cell namespaces. Do not use Node.js, browser or Web APIs. Module imports, filesystem and network APIs are unavailable. Obtain external capabilities only through available tools. Use tools.exec for managed background processes when permitted.
|
|
@@ -24,7 +26,7 @@ Tool calls:
|
|
|
24
26
|
- tools.NAME(input) returns a Promise. Use the existing tool input schema; freeform tools such as apply_patch accept a raw string, not a JSON wrapper.
|
|
25
27
|
- Results use the MAR envelope: { content: string, data?: unknown, images?: Array<{mimeType:string,dataBase64:string}>, truncated?: boolean }. content is rendered text, not raw file bytes. Use result.data.results[0].text for read source text and result.data.exitCode/result.data.stdout for exec. Use only fields documented by that tool; do not invent success or ok flags.
|
|
26
28
|
- Tool errors reject with code/message and may be caught. Await every call; unawaited calls fail the cell. Use Promise.all for independent calls and sequential await for dependencies. Runtime preserves safe/serial barriers and Host checks.
|
|
27
|
-
- tool_search, question,
|
|
29
|
+
- tool_search, question, code_exec, code_exec_wait and direct-only tools cannot be called here. Use direct tool_search before a later code cell for deferred tools; the current cell's tools do not change.
|
|
28
30
|
|
|
29
31
|
Built-in namespaces:
|
|
30
32
|
- out.text(value): void emits text. Strings and scalar values are stringified; JSON-compatible objects/arrays are serialized as compact JSON. Cyclic or unsupported object values fail. Only explicitly emitted output reaches the model.
|
|
@@ -32,6 +34,9 @@ Built-in namespaces:
|
|
|
32
34
|
- state.get(key), state.has(key), state.set(key, value), state.delete(key) access bounded session JSON state. Mutations commit atomically only when the cell succeeds; failed or cancelled cells discard them.
|
|
33
35
|
- catalog.tools() returns a read-only snapshot of the tools callable in this cell, including name, description, inputSchema and inputMode. It does not discover or authorize additional tools.
|
|
34
36
|
- await cell.sleep(ms) performs a cancellable Host-side delay up to 30000 ms and remains subject to the cell deadline.
|
|
37
|
+
- await cell.yield_control() returns accumulated output to the model immediately while this cell continues running.
|
|
38
|
+
- Optionally start source with // @options: {"yieldMs": 10000, "maxOutputTokens": 10000}. yieldMs is a wait window (0..30000), not a cell timeout. A running result includes cellId; use direct code_exec_wait for new output, completion or termination.
|
|
39
|
+
- Registered cells are Session warm resources: ending or cancelling the model Execution cancels its wait, not the cell. Reuse retained cellId; do not start duplicate work. Host cleanup or explicit termination stops the cell.
|
|
35
40
|
- tools, out, state, catalog and cell are frozen bindings. Do not shadow their names.
|
|
36
41
|
- Input/text output max 1 MiB, 128 calls, 120 seconds. Failures do not roll back completed tool work; do not replay successful operations blindly.
|
|
37
42
|
|
|
@@ -62,16 +67,37 @@ out.text({ files: patched.data.files.length });
|
|
|
62
67
|
allowedCallers: ['direct'],
|
|
63
68
|
parallelSafety: 'serial'
|
|
64
69
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
export const codeExecWaitToolDefinition = {
|
|
71
|
+
name: 'code_exec_wait',
|
|
72
|
+
description: 'Wait for a retained Code Exec cell by cellId. Returns only new output, or a bounded terminal snapshot marked cached on repeat reads. yieldMs is a wait window, not a timeout. action defaults to poll (wait); action cancel stops the cell and waits for cleanup. Cancelling this wait does not cancel the cell. Cells belong to the current Session and cannot survive Host restart. Call directly, never inside code_exec.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
cellId: { type: 'string' },
|
|
77
|
+
yieldMs: { type: 'integer', minimum: 0, maximum: limits.codeMaxYieldMs },
|
|
78
|
+
maxOutputTokens: { type: 'integer', minimum: 1, maximum: limits.codeMaxOutputTokens },
|
|
79
|
+
action: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
enum: ['poll', 'cancel'],
|
|
82
|
+
description: 'Defaults to poll; cancel terminates the cell rather than just its current wait.'
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
required: ['cellId'],
|
|
86
|
+
additionalProperties: false
|
|
87
|
+
},
|
|
88
|
+
allowedCallers: ['direct'],
|
|
89
|
+
parallelSafety: 'safe'
|
|
90
|
+
};
|
|
91
|
+
/** Deferred definitions travel in search results, not in the stable Code Mode description. */
|
|
92
|
+
export function projectCodeModeTools(tools, residentTools = tools) {
|
|
93
|
+
const codeModeEnabled = tools.some((tool) => tool.name === 'code_exec' && allowsToolCaller(tool, 'direct'));
|
|
94
|
+
const direct = tools.filter((tool) => allowsDirectTool(tool, codeModeEnabled));
|
|
95
|
+
if (!codeModeEnabled)
|
|
69
96
|
return direct;
|
|
70
|
-
const programmatic =
|
|
71
|
-
const hidden = programmatic.filter((tool) => !allowsToolCaller(tool, 'direct'));
|
|
97
|
+
const programmatic = residentTools.filter((tool) => allowsToolCaller(tool, 'programmatic'));
|
|
72
98
|
const directory = JSON.stringify({
|
|
73
99
|
callableNames: programmatic.map((tool) => tool.name),
|
|
74
|
-
programmaticOnly:
|
|
100
|
+
programmaticOnly: programmatic.map((tool) => ({
|
|
75
101
|
name: tool.name,
|
|
76
102
|
description: tool.description,
|
|
77
103
|
inputSchema: tool.inputSchema,
|
|
@@ -80,13 +106,37 @@ export function projectCodeModeTools(tools) {
|
|
|
80
106
|
});
|
|
81
107
|
if (Buffer.byteLength(directory) > 32 * 1024)
|
|
82
108
|
throw new MarAgentError('MAR_AGENT_TOOL_EXPOSURE_LIMIT', 'Programmatic tool directory is too large.');
|
|
83
|
-
return direct.map((tool) => tool.name === '
|
|
109
|
+
return direct.map((tool) => tool.name === 'code_exec'
|
|
84
110
|
? {
|
|
85
111
|
...tool,
|
|
86
|
-
description: `${codeModeToolDefinition.description}\
|
|
112
|
+
description: `${codeModeToolDefinition.description}\nDeferred MCP definitions are returned by direct tool_search and become callable from the next model response, not later in the same response; they are not appended to this description.\nProgrammatic tools (call only inside code_exec): ${directory}`
|
|
87
113
|
}
|
|
88
114
|
: tool);
|
|
89
115
|
}
|
|
116
|
+
/** Preserve native search definitions while providing the same discovery evidence to other protocols. */
|
|
117
|
+
export function projectCodeModeSearchResult(message, codeModeEnabled) {
|
|
118
|
+
if (!message.toolSearchTools)
|
|
119
|
+
return message;
|
|
120
|
+
if (!codeModeEnabled)
|
|
121
|
+
return {
|
|
122
|
+
...message,
|
|
123
|
+
toolSearchTools: message.toolSearchTools.filter((tool) => allowsToolCaller(tool, 'direct'))
|
|
124
|
+
};
|
|
125
|
+
const tools = message.toolSearchTools.map((tool) => ({
|
|
126
|
+
name: tool.name,
|
|
127
|
+
description: allowsToolCaller(tool, 'programmatic')
|
|
128
|
+
? `Call only as tools.${tool.name} inside code_exec, never as a direct model tool.\n${tool.description}`
|
|
129
|
+
: tool.description,
|
|
130
|
+
inputSchema: tool.inputSchema,
|
|
131
|
+
inputMode: tool.inputMode ?? 'json',
|
|
132
|
+
caller: allowsToolCaller(tool, 'programmatic') ? 'programmatic' : 'direct'
|
|
133
|
+
}));
|
|
134
|
+
return {
|
|
135
|
+
...message,
|
|
136
|
+
content: JSON.stringify({ tools }),
|
|
137
|
+
toolSearchTools: tools.map(({ caller: _caller, ...tool }) => tool)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
90
140
|
function defaultEngineFactory() {
|
|
91
141
|
const core = import.meta.resolve('quickjs-emscripten-core');
|
|
92
142
|
const variant = import.meta.resolve('@jitl/quickjs-wasmfile-release-sync');
|
|
@@ -125,6 +175,16 @@ export class CodeModeTool {
|
|
|
125
175
|
environment;
|
|
126
176
|
executor;
|
|
127
177
|
engineFactory;
|
|
178
|
+
#cells = new CodeCells();
|
|
179
|
+
start(value, context) {
|
|
180
|
+
return this.#cells.start(value, context, (code, detached, output) => this.execute(code, detached, output));
|
|
181
|
+
}
|
|
182
|
+
wait(value, context) {
|
|
183
|
+
return this.#cells.wait(value, context);
|
|
184
|
+
}
|
|
185
|
+
listSessionResources(sessionId) {
|
|
186
|
+
return this.#cells.snapshot(sessionId);
|
|
187
|
+
}
|
|
128
188
|
#active = new Map();
|
|
129
189
|
constructor(paths, environment = {}, executor = directLocalProcessExecutor, engineFactory = defaultEngineFactory()) {
|
|
130
190
|
this.paths = paths;
|
|
@@ -132,7 +192,7 @@ export class CodeModeTool {
|
|
|
132
192
|
this.executor = executor;
|
|
133
193
|
this.engineFactory = engineFactory;
|
|
134
194
|
}
|
|
135
|
-
async execute(value, context) {
|
|
195
|
+
async execute(value, context, output) {
|
|
136
196
|
const code = typeof value === 'string'
|
|
137
197
|
? value
|
|
138
198
|
: value &&
|
|
@@ -160,7 +220,7 @@ export class CodeModeTool {
|
|
|
160
220
|
this.#active.set(controller, { sessionId: context.sessionId, done });
|
|
161
221
|
const timer = setTimeout(() => controller.abort(new MarAgentError('MAR_AGENT_CODE_TIMEOUT', 'Code Mode exceeded its execution time limit.')), limits.codeTimeoutMs);
|
|
162
222
|
try {
|
|
163
|
-
return await this.#run(code, { ...context, signal }, controller);
|
|
223
|
+
return await this.#run(code, { ...context, signal }, controller, output);
|
|
164
224
|
}
|
|
165
225
|
finally {
|
|
166
226
|
clearTimeout(timer);
|
|
@@ -169,12 +229,13 @@ export class CodeModeTool {
|
|
|
169
229
|
}
|
|
170
230
|
}
|
|
171
231
|
async dispose(sessionId) {
|
|
232
|
+
await this.#cells.dispose(sessionId);
|
|
172
233
|
const active = [...this.#active].filter(([, entry]) => sessionId === undefined || entry.sessionId === sessionId);
|
|
173
234
|
for (const [controller] of active)
|
|
174
235
|
controller.abort(new MarAgentError('MAR_AGENT_CODE_CANCELLED', 'Code Mode resources were released.'));
|
|
175
236
|
await Promise.all(active.map(([, entry]) => entry.done));
|
|
176
237
|
}
|
|
177
|
-
async #run(code, context, controller) {
|
|
238
|
+
async #run(code, context, controller, output) {
|
|
178
239
|
const cwd = await this.paths.resolve('.');
|
|
179
240
|
context.signal.throwIfAborted();
|
|
180
241
|
const environment = Object.fromEntries(environmentKeys.flatMap((key) => {
|
|
@@ -255,6 +316,7 @@ export class CodeModeTool {
|
|
|
255
316
|
if (outputBytes > limits.codeMaxOutputBytes)
|
|
256
317
|
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Code Mode emitted too much text.');
|
|
257
318
|
text.push(frame.value);
|
|
319
|
+
output?.text(frame.value);
|
|
258
320
|
}
|
|
259
321
|
else if (frame.kind === 'image') {
|
|
260
322
|
if (!context.codeMode?.supportsImages)
|
|
@@ -263,6 +325,10 @@ export class CodeModeTool {
|
|
|
263
325
|
if (images.length >= 5)
|
|
264
326
|
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Code Mode emitted too many images.');
|
|
265
327
|
images.push(image);
|
|
328
|
+
output?.image(image);
|
|
329
|
+
}
|
|
330
|
+
else if (frame.kind === 'yield') {
|
|
331
|
+
output?.yield();
|
|
266
332
|
}
|
|
267
333
|
else if (frame.kind === 'call' &&
|
|
268
334
|
Number.isSafeInteger(frame.id) &&
|
|
@@ -273,7 +339,7 @@ export class CodeModeTool {
|
|
|
273
339
|
seen.add(id);
|
|
274
340
|
const job = (async () => {
|
|
275
341
|
try {
|
|
276
|
-
if (frame.name === '
|
|
342
|
+
if (frame.name === 'code_exec' || !available.has(frame.name))
|
|
277
343
|
throw new MarAgentError('MAR_AGENT_TOOL_NOT_AVAILABLE', 'The requested nested tool is unavailable.');
|
|
278
344
|
const output = await invoke({
|
|
279
345
|
name: frame.name,
|
|
@@ -21,6 +21,11 @@ export declare const TOOL_EXECUTION_LIMITS: Readonly<{
|
|
|
21
21
|
codeMaxTransportBytes: 67108864;
|
|
22
22
|
codeMaxCalls: 128;
|
|
23
23
|
codeMaxConcurrentCells: 4;
|
|
24
|
+
codeDefaultYieldMs: 10000;
|
|
25
|
+
codeMaxYieldMs: 30000;
|
|
26
|
+
codeMaxOutputTokens: 10000;
|
|
27
|
+
codeCompletedRetentionMs: 300000;
|
|
28
|
+
codeMaxCompletedResults: 16;
|
|
24
29
|
codeStateMaxEntries: 128;
|
|
25
30
|
codeStateMaxMutations: 128;
|
|
26
31
|
codeStateMaxKeyBytes: 256;
|
|
@@ -21,6 +21,11 @@ export const TOOL_EXECUTION_LIMITS = Object.freeze({
|
|
|
21
21
|
codeMaxTransportBytes: 67_108_864,
|
|
22
22
|
codeMaxCalls: 128,
|
|
23
23
|
codeMaxConcurrentCells: 4,
|
|
24
|
+
codeDefaultYieldMs: 10_000,
|
|
25
|
+
codeMaxYieldMs: 30_000,
|
|
26
|
+
codeMaxOutputTokens: 10_000,
|
|
27
|
+
codeCompletedRetentionMs: 300_000,
|
|
28
|
+
codeMaxCompletedResults: 16,
|
|
24
29
|
codeStateMaxEntries: 128,
|
|
25
30
|
codeStateMaxMutations: 128,
|
|
26
31
|
codeStateMaxKeyBytes: 256,
|
|
@@ -24,6 +24,11 @@ export declare class LocalToolRegistry {
|
|
|
24
24
|
command: string;
|
|
25
25
|
cwd: string;
|
|
26
26
|
}[];
|
|
27
|
+
listCodeCells(sessionId: string): {
|
|
28
|
+
cellId: string;
|
|
29
|
+
status: "completed" | "cancelled" | "running" | "failed";
|
|
30
|
+
executionId: string;
|
|
31
|
+
}[];
|
|
27
32
|
disposeSessionResources(sessionId: string): Promise<void>;
|
|
28
33
|
dispose(): Promise<void>;
|
|
29
34
|
}
|
|
@@ -25,7 +25,8 @@ export class LocalToolRegistry {
|
|
|
25
25
|
this.#exec = new ExecTool(paths, undefined, undefined, environment, shell, web.processExecutor);
|
|
26
26
|
this.#code = new CodeModeTool(paths, environment, web.processExecutor, web.codeModeEngineFactory);
|
|
27
27
|
this.#handlers = new Map([
|
|
28
|
-
['
|
|
28
|
+
['code_exec', (arguments_, context) => this.#code.start(arguments_, context)],
|
|
29
|
+
['code_exec_wait', (arguments_, context) => this.#code.wait(arguments_, context)],
|
|
29
30
|
[
|
|
30
31
|
'read',
|
|
31
32
|
async (arguments_, context) => {
|
|
@@ -124,6 +125,9 @@ export class LocalToolRegistry {
|
|
|
124
125
|
listSessionResources(sessionId) {
|
|
125
126
|
return this.#exec.listSessionResources(sessionId);
|
|
126
127
|
}
|
|
128
|
+
listCodeCells(sessionId) {
|
|
129
|
+
return this.#code.listSessionResources(sessionId);
|
|
130
|
+
}
|
|
127
131
|
async disposeSessionResources(sessionId) {
|
|
128
132
|
await Promise.all([this.#exec.disposeSession(sessionId), this.#code.dispose(sessionId)]);
|
|
129
133
|
}
|