@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.
Files changed (40) hide show
  1. package/dist/host/local-agent-host.d.ts +5 -0
  2. package/dist/host/local-agent-host.js +2 -1
  3. package/dist/prompts/index.d.ts +2 -1
  4. package/dist/prompts/index.js +2 -2
  5. package/dist/prompts/resources.d.ts +1 -0
  6. package/dist/prompts/resources.js +8 -2
  7. package/dist/prompts/workflow.d.ts +1 -1
  8. package/dist/prompts/workflow.js +7 -2
  9. package/dist/runtime/context-gc.js +10 -6
  10. package/dist/runtime/context-projection.js +1 -1
  11. package/dist/sdk/agent.js +163 -88
  12. package/dist/sdk/types.d.ts +17 -2
  13. package/dist/session/context-gc-checkpoint.d.ts +1 -1
  14. package/dist/session/context-gc-checkpoint.js +11 -5
  15. package/dist/session/context-gc-replacements.d.ts +2 -0
  16. package/dist/session/context-gc-replacements.js +18 -0
  17. package/dist/session/jsonl-store.d.ts +5 -2
  18. package/dist/session/jsonl-store.js +105 -12
  19. package/dist/subagent/session-controller.d.ts +2 -0
  20. package/dist/subagent/session-controller.js +23 -6
  21. package/dist/tools/callers.d.ts +2 -0
  22. package/dist/tools/callers.js +7 -1
  23. package/dist/tools/code-mode/cells.d.ts +20 -0
  24. package/dist/tools/code-mode/cells.js +250 -0
  25. package/dist/tools/code-mode/dispatch.d.ts +21 -0
  26. package/dist/tools/code-mode/dispatch.js +53 -0
  27. package/dist/tools/code-mode/engine.d.ts +1 -0
  28. package/dist/tools/code-mode/quickjs-engine.js +4 -0
  29. package/dist/tools/code-mode/worker.js +1 -0
  30. package/dist/tools/code-mode.d.ts +15 -4
  31. package/dist/tools/code-mode.js +83 -17
  32. package/dist/tools/execution-limits.d.ts +5 -0
  33. package/dist/tools/execution-limits.js +5 -0
  34. package/dist/tools/local-registry.d.ts +5 -0
  35. package/dist/tools/local-registry.js +5 -1
  36. package/dist/tools/registry.js +2 -1
  37. package/dist/tools/scheduler.d.ts +2 -0
  38. package/dist/tools/scheduler.js +11 -0
  39. package/dist/tools/tool-search.js +12 -6
  40. 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
- /** Direct model tools and bounded input-only declarations for the programmatic surface. */
8
- export declare function projectCodeModeTools(tools: readonly ClientToolDefinition[]): ClientToolDefinition[];
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
  }
@@ -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: 'code_mode',
17
+ name: 'code_exec',
17
18
  description: `Run JavaScript to orchestrate or compose tool calls.
18
- - Use for bounded combinations of tools or filtering large intermediate results. A single direct tool call is preferable when that tool allows direct calls and composition adds no benefit.
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, code_mode 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.
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
- /** Direct model tools and bounded input-only declarations for the programmatic surface. */
66
- export function projectCodeModeTools(tools) {
67
- const direct = tools.filter((tool) => allowsToolCaller(tool, 'direct'));
68
- if (!direct.some((tool) => tool.name === 'code_mode'))
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 = tools.filter((tool) => allowsToolCaller(tool, '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: hidden.map((tool) => ({
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 === 'code_mode'
109
+ return direct.map((tool) => tool.name === 'code_exec'
84
110
  ? {
85
111
  ...tool,
86
- description: `${codeModeToolDefinition.description}\nProgrammatic tools (other schemas are provided by their direct tool definitions): ${directory}`
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 === 'code_mode' || !available.has(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
- ['code_mode', (arguments_, context) => this.#code.execute(arguments_, context)],
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
  }