@myagentroam/agent 0.9.138 → 0.9.140

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.
@@ -1,5 +1,8 @@
1
1
  import type { ToolCall, ToolExecutionResult } from '../../sdk/types.js';
2
2
  import type { CodeModeStateEntry, CodeModeStateMutation } from './state.js';
3
+ export type ProgrammaticToolResult = Omit<ToolExecutionResult, 'content'> & {
4
+ content?: string;
5
+ };
3
6
  export interface CodeModeCatalogEntry {
4
7
  readonly name: string;
5
8
  readonly description: string;
@@ -26,8 +29,8 @@ export interface CodeModeEngine {
26
29
  stateTotalBytes: number;
27
30
  sleepMs: number;
28
31
  };
29
- onToolCall(call: ToolCall): Promise<ToolExecutionResult>;
30
- onText(value: string): void;
32
+ onToolCall(call: ToolCall): Promise<ProgrammaticToolResult>;
33
+ onText(value: string, originalBytes?: number): void;
31
34
  onImage(value: unknown): void;
32
35
  onYield(): void;
33
36
  }): Promise<{
@@ -1,3 +1,4 @@
1
+ import { BoundedTextOutput } from './text-output.js';
1
2
  // This trusted module runs only in the Host-managed worker, never in the model's JS context.
2
3
  /** @returns {Promise<import('./engine.js').CodeModeEngine>} */
3
4
  export async function createCodeModeEngine(options) {
@@ -27,6 +28,7 @@ export async function createCodeModeEngine(options) {
27
28
  const state = new Map(input.state.map((entry) => [entry.key, entry.value]));
28
29
  const stateMutations = [];
29
30
  let outputBytes = 0;
31
+ let textCalls = 0;
30
32
  const notify = () => {
31
33
  wake?.();
32
34
  wake = undefined;
@@ -69,16 +71,7 @@ export async function createCodeModeEngine(options) {
69
71
  return item;
70
72
  });
71
73
  };
72
- globalThis.__text = value => {
73
- if (value === null || ['string', 'number', 'boolean', 'bigint', 'undefined'].includes(typeof value))
74
- return String(value);
75
- return JSON.stringify(value, (_key, item) => {
76
- if (['bigint', 'function', 'symbol', 'undefined'].includes(typeof item))
77
- throw Object.assign(new TypeError('Only JSON values may be emitted.'),
78
- { code: 'MAR_AGENT_TOOL_INPUT_INVALID' });
79
- return item;
80
- });
81
- };
74
+ globalThis.__text = ${streamText.toString()};
82
75
  globalThis.__deepFreeze = function deepFreeze(value) {
83
76
  if (value && typeof value === 'object' && !Object.isFrozen(value)) {
84
77
  for (const item of Object.values(value)) deepFreeze(item);
@@ -207,17 +200,32 @@ export async function createCodeModeEngine(options) {
207
200
  consumeToolCall.dispose();
208
201
  const out = vm.newObject();
209
202
  method(out, 'text', (handle) => {
210
- const result = vm.callFunction(formatText, vm.undefined, handle ?? vm.undefined);
203
+ const separatorBytes = textCalls ? 1 : 0;
204
+ const capture = new BoundedTextOutput(Math.max(0, input.limits.outputBytes - outputBytes - separatorBytes), '');
205
+ const emit = vm.newFunction('textChunk', (chunk) => {
206
+ if (!chunk || vm.typeof(chunk) !== 'string')
207
+ return {
208
+ error: guestError('MAR_AGENT_TOOL_INPUT_INVALID', 'Text output chunks must be strings.')
209
+ };
210
+ const length = vm.getProp(chunk, 'length');
211
+ const oversized = vm.getNumber(length) > 4096 * 6;
212
+ length.dispose();
213
+ if (oversized)
214
+ return {
215
+ error: guestError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Text serialization chunk limit exceeded.')
216
+ };
217
+ capture.append(vm.getString(chunk));
218
+ return vm.undefined;
219
+ });
220
+ const result = vm.callFunction(formatText, vm.undefined, handle ?? vm.undefined, emit);
221
+ emit.dispose();
211
222
  if (result.error)
212
223
  return { error: result.error };
213
224
  try {
214
- const value = vm.getString(result.value);
215
- outputBytes += Buffer.byteLength(value) + 1;
216
- if (outputBytes > input.limits.outputBytes)
217
- return {
218
- error: guestError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Text output limit exceeded.')
219
- };
220
- input.onText(value);
225
+ const captured = capture.snapshot();
226
+ outputBytes += captured.retainedBytes + separatorBytes;
227
+ textCalls++;
228
+ input.onText(captured.text, captured.originalBytes);
221
229
  }
222
230
  finally {
223
231
  result.value.dispose();
@@ -453,6 +461,86 @@ export async function createCodeModeEngine(options) {
453
461
  }
454
462
  };
455
463
  }
464
+ // Compiled inside the guest: stream bounded chunks, never build a complete serialized object.
465
+ function streamText(value, emit) {
466
+ 'use strict';
467
+ const chunks = (text, quoted = false) => {
468
+ if (quoted)
469
+ emit('"');
470
+ for (let start = 0; start < text.length;) {
471
+ let end = Math.min(text.length, start + 4096);
472
+ if (end < text.length &&
473
+ text.charCodeAt(end - 1) >= 0xd800 &&
474
+ text.charCodeAt(end - 1) <= 0xdbff &&
475
+ text.charCodeAt(end) >= 0xdc00 &&
476
+ text.charCodeAt(end) <= 0xdfff)
477
+ end--;
478
+ const part = text.slice(start, end);
479
+ emit(quoted ? JSON.stringify(part).slice(1, -1) : part);
480
+ start = end;
481
+ }
482
+ if (quoted)
483
+ emit('"');
484
+ };
485
+ if (value === null ||
486
+ ['string', 'number', 'boolean', 'bigint', 'undefined'].includes(typeof value)) {
487
+ chunks(String(value));
488
+ return;
489
+ }
490
+ const ancestors = new Set();
491
+ const write = (item, key) => {
492
+ if (item && typeof item === 'object' && typeof item.toJSON === 'function')
493
+ item = item.toJSON(key);
494
+ if (['bigint', 'function', 'symbol', 'undefined'].includes(typeof item))
495
+ throw Object.assign(new TypeError('Only JSON values may be emitted.'), {
496
+ code: 'MAR_AGENT_TOOL_INPUT_INVALID'
497
+ });
498
+ if (item instanceof Number || item instanceof String || item instanceof Boolean)
499
+ item = item.valueOf();
500
+ if (item instanceof BigInt)
501
+ throw new TypeError('BigInt objects cannot be emitted.');
502
+ if (item === null) {
503
+ emit('null');
504
+ return;
505
+ }
506
+ if (typeof item === 'string') {
507
+ chunks(item, true);
508
+ return;
509
+ }
510
+ if (typeof item === 'number' || typeof item === 'boolean') {
511
+ emit(JSON.stringify(item));
512
+ return;
513
+ }
514
+ if (ancestors.has(item))
515
+ throw new TypeError('Cyclic values cannot be emitted.');
516
+ ancestors.add(item);
517
+ if (Array.isArray(item)) {
518
+ emit('[');
519
+ const length = item.length;
520
+ for (let index = 0; index < length; index++) {
521
+ if (index)
522
+ emit(',');
523
+ write(item[index], String(index));
524
+ }
525
+ emit(']');
526
+ }
527
+ else {
528
+ emit('{');
529
+ let first = true;
530
+ for (const name of Object.keys(item)) {
531
+ if (!first)
532
+ emit(',');
533
+ first = false;
534
+ chunks(name, true);
535
+ emit(':');
536
+ write(item[name], name);
537
+ }
538
+ emit('}');
539
+ }
540
+ ancestors.delete(item);
541
+ };
542
+ write(value, '');
543
+ }
456
544
  function diagnostic(code, message) {
457
545
  return Object.assign(new Error(message), { code });
458
546
  }
@@ -0,0 +1,21 @@
1
+ export function renderCapturedText(snapshot: any, imageCount?: number): string;
2
+ /** Bounded UTF-8 capture shared by trusted engines, the bridge and retained cells. */
3
+ export class BoundedTextOutput {
4
+ constructor(maximumBytes: any, separator?: string);
5
+ maximumBytes: any;
6
+ separator: string;
7
+ append(value: any, originalBytes?: number): void;
8
+ snapshot(): {
9
+ text: string;
10
+ originalBytes: number;
11
+ retainedBytes: number;
12
+ droppedBytes: number;
13
+ };
14
+ drain(): {
15
+ text: string;
16
+ originalBytes: number;
17
+ retainedBytes: number;
18
+ droppedBytes: number;
19
+ };
20
+ #private;
21
+ }
@@ -0,0 +1,75 @@
1
+ /** Bounded UTF-8 capture shared by trusted engines, the bridge and retained cells. */
2
+ export class BoundedTextOutput {
3
+ #chunks = [];
4
+ #used = 0;
5
+ #original = 0;
6
+ #retained = 0;
7
+ #hasValue = false;
8
+ #sealed = false;
9
+ constructor(maximumBytes, separator = '\n') {
10
+ this.maximumBytes = maximumBytes;
11
+ this.separator = separator;
12
+ }
13
+ append(value, originalBytes = Buffer.byteLength(value)) {
14
+ const separator = this.#hasValue ? this.separator : '';
15
+ this.#hasValue = true;
16
+ this.#original += Buffer.byteLength(separator) + originalBytes;
17
+ this.#capture(separator);
18
+ this.#capture(value);
19
+ if (originalBytes > Buffer.byteLength(value))
20
+ this.#sealed = true;
21
+ }
22
+ #capture(value) {
23
+ const remaining = Math.max(0, this.maximumBytes - this.#used);
24
+ if (this.#sealed || !value.length)
25
+ return;
26
+ if (!remaining) {
27
+ this.#sealed = true;
28
+ return;
29
+ }
30
+ // Slice UTF-16 first so even a large injected-engine string never creates an unbounded Buffer.
31
+ let end = Math.min(value.length, remaining);
32
+ if (end < value.length &&
33
+ end > 0 &&
34
+ value.charCodeAt(end - 1) >= 0xd800 &&
35
+ value.charCodeAt(end - 1) <= 0xdbff &&
36
+ value.charCodeAt(end) >= 0xdc00 &&
37
+ value.charCodeAt(end) <= 0xdfff)
38
+ end--;
39
+ const bytes = Buffer.from(value.slice(0, end));
40
+ let length = Math.min(bytes.length, remaining);
41
+ while (length > 0 && length < bytes.length && (bytes[length] & 0xc0) === 0x80)
42
+ length--;
43
+ if (length < Buffer.byteLength(value))
44
+ this.#sealed = true;
45
+ if (!length)
46
+ return;
47
+ this.#chunks.push(bytes.toString('utf8', 0, length));
48
+ this.#used += length;
49
+ this.#retained += length;
50
+ }
51
+ snapshot() {
52
+ return {
53
+ text: this.#chunks.join(''),
54
+ originalBytes: this.#original,
55
+ retainedBytes: this.#retained,
56
+ droppedBytes: this.#original - this.#retained
57
+ };
58
+ }
59
+ drain() {
60
+ const snapshot = this.snapshot();
61
+ this.#chunks = [];
62
+ this.#original = 0;
63
+ this.#retained = 0;
64
+ this.#hasValue = false;
65
+ // Consuming output never replenishes the cell's capture budget.
66
+ return snapshot;
67
+ }
68
+ }
69
+ export function renderCapturedText(snapshot, imageCount = 0) {
70
+ const text = snapshot.droppedBytes > 0
71
+ ? `${snapshot.text}\n...[${snapshot.droppedBytes} text bytes omitted; original output: ${snapshot.originalBytes} bytes]...`
72
+ : snapshot.text;
73
+ const imageSummary = imageCount > 0 ? `Code Exec emitted ${imageCount} image${imageCount === 1 ? '' : 's'}.` : '';
74
+ return [text, imageSummary].filter(Boolean).join('\n');
75
+ }
@@ -31,7 +31,7 @@ lines.on('line', async (line) => {
31
31
  state: frame.state,
32
32
  limits: frame.limits,
33
33
  signal: controller.signal,
34
- onText: (value) => send({ kind: 'text', value }),
34
+ onText: (value, originalBytes) => send({ kind: 'text', value, originalBytes }),
35
35
  onImage: (value) => send({ kind: 'image', value }),
36
36
  onYield: () => send({ kind: 'yield' }),
37
37
  onToolCall: (call) => new Promise((resolve, reject) => {
@@ -12,6 +12,7 @@ import { createToolCallScheduler } from './scheduler.js';
12
12
  import { detectImageMime } from './view-image.js';
13
13
  import { allowsDirectTool, allowsToolCaller } from './callers.js';
14
14
  import { CodeCells } from './code-mode/cells.js';
15
+ import { BoundedTextOutput, renderCapturedText } from './code-mode/text-output.js';
15
16
  import { parseCodeModeStateMutations } from './code-mode/state.js';
16
17
  export const codeModeToolDefinition = {
17
18
  name: 'code_exec',
@@ -20,25 +21,25 @@ export const codeModeToolDefinition = {
20
21
  - Use bounded combinations, parallel independent calls, and filtering of large intermediate results to reduce model round trips.
21
22
  - 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.
22
23
  - 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.
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
+ - 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 to start managed processes when permitted; use tools.exec_wait afterward to wait, write stdin or cancel.
24
25
 
25
26
  Tool calls:
26
27
  - 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.
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.
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.
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
+ - Results use the MAR envelope: { content?: string, data?: unknown, images?: Array<{mimeType:string,dataBase64:string}>, truncated?: boolean }. Redundant JSON content is omitted when it exactly renders data; independent text remains in content. Prefer data for programmatic results: result.data.results[0].text for read source text and result.data.exitCode/result.data.stdout for exec. content, when present, is rendered text, not raw file bytes. Use only fields documented by that tool; do not invent success or ok flags.
29
+ - Tool errors reject with code/message and may be caught. A failed cell preserves already emitted output alongside its status and error; inspect it before retrying completed work. 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.
30
+ - skill, tool_search, question, code_exec, code_exec_wait and direct-only tools cannot be called here. Load Skills directly and read their instructions before the next tool call. Use direct tool_search before a later code cell for deferred tools; the current cell's tools do not change.
30
31
 
31
32
  Built-in namespaces:
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.
33
+ - out.text(value): void emits text. Strings and scalar values are stringified; JSON-compatible objects/arrays are serialized as compact JSON. Excess ordinary text is truncated with omission metadata, without stopping later code or tools. Cyclic or unsupported object values fail. Only explicitly emitted output reaches the model.
33
34
  - out.image(value: {mimeType:string,dataBase64:string}): void emits one returned image; for example out.image(result.images[0]) when that image exists and the model supports images. It does not accept a path, URL, or result.content.
34
35
  - 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.
35
36
  - 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.
36
37
  - await cell.sleep(ms) performs a cancellable Host-side delay up to 30000 ms and remains subject to the cell deadline.
37
38
  - 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
+ - Optionally start source with // @options: {"yieldMs": ${limits.codeDefaultYieldMs}, "maxOutputTokens": 10000}. yieldMs defaults to ${limits.codeDefaultYieldMs} ms and is a wait window (0..${limits.codeMaxYieldMs}), not a cell timeout. Explicit values are preserved and do not change nested tools' wait windows. A running result includes cellId; use direct code_exec_wait for new output, completion or termination.
39
40
  - 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.
40
41
  - tools, out, state, catalog and cell are frozen bindings. Do not shadow their names.
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.
42
+ - Input max 1 MiB; cumulative text capture max 1 MiB with truncation; 128 calls, 120 seconds. Tool-specific limits apply before explicit text capture. Protocol, image, memory and time limits remain hard failures. Failures do not roll back completed tool work; do not replay successful operations blindly.
42
43
 
43
44
  Examples (copy the JavaScript source, not the fences; use only tools available in the current directory):
44
45
  Independent reads, emitting only source text:
@@ -74,7 +75,13 @@ export const codeExecWaitToolDefinition = {
74
75
  type: 'object',
75
76
  properties: {
76
77
  cellId: { type: 'string' },
77
- yieldMs: { type: 'integer', minimum: 0, maximum: limits.codeMaxYieldMs },
78
+ yieldMs: {
79
+ type: 'integer',
80
+ minimum: 0,
81
+ maximum: limits.codeMaxWaitYieldMs,
82
+ default: limits.codeDefaultWaitYieldMs,
83
+ description: `Wait up to this many milliseconds. Defaults to ${limits.codeDefaultWaitYieldMs}; maximum ${limits.codeMaxWaitYieldMs}. Zero returns immediately without stopping the cell. This does not extend the cell execution deadline.`
84
+ },
78
85
  maxOutputTokens: { type: 'integer', minimum: 1, maximum: limits.codeMaxOutputTokens },
79
86
  action: {
80
87
  type: 'string',
@@ -259,9 +266,8 @@ export class CodeModeTool {
259
266
  owner: { sessionId: context.sessionId, executionId: context.executionId },
260
267
  signal: context.signal
261
268
  });
262
- const text = [];
269
+ const text = new BoundedTextOutput(limits.codeMaxOutputBytes);
263
270
  const images = [];
264
- let outputBytes = 0;
265
271
  let transportBytes = 0;
266
272
  let calls = 0;
267
273
  let complete = false;
@@ -312,11 +318,12 @@ export class CodeModeTool {
312
318
  if (complete)
313
319
  throw new MarAgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Code Mode sent data after completion.');
314
320
  if (frame.kind === 'text' && typeof frame.value === 'string') {
315
- outputBytes += Buffer.byteLength(frame.value) + 1;
316
- if (outputBytes > limits.codeMaxOutputBytes)
317
- throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Code Mode emitted too much text.');
318
- text.push(frame.value);
319
- output?.text(frame.value);
321
+ const originalBytes = frame.originalBytes ?? Buffer.byteLength(frame.value);
322
+ if (!Number.isSafeInteger(originalBytes) ||
323
+ originalBytes < Buffer.byteLength(frame.value))
324
+ throw new MarAgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Code Mode sent invalid text byte counts.');
325
+ text.append(frame.value, originalBytes);
326
+ output?.text(frame.value, originalBytes);
320
327
  }
321
328
  else if (frame.kind === 'image') {
322
329
  if (!context.codeMode?.supportsImages)
@@ -346,7 +353,11 @@ export class CodeModeTool {
346
353
  arguments: frame.arguments,
347
354
  callId: randomUUID()
348
355
  });
349
- await write({ id, ok: true, output });
356
+ const { content, ...structured } = output;
357
+ const projected = output.data !== undefined && content === JSON.stringify(output.data)
358
+ ? structured
359
+ : output;
360
+ await write({ id, ok: true, output: projected });
350
361
  }
351
362
  catch (error) {
352
363
  if (context.signal.aborted)
@@ -458,9 +469,16 @@ export class CodeModeTool {
458
469
  throw new MarAgentError('MAR_AGENT_CODE_INCOMPLETE', 'Code Mode exited without a completion message.');
459
470
  if (stateMutations.length)
460
471
  await context.codeMode.commitState?.(stateMutations);
472
+ const captured = text.snapshot();
461
473
  return {
462
- content: text.join('\n') || 'Code Mode completed without emitted text.',
463
- data: { toolCalls: calls },
474
+ content: renderCapturedText(captured, images.length) ||
475
+ 'Code Mode completed without emitted text.',
476
+ data: {
477
+ toolCalls: calls,
478
+ outputBytes: captured.originalBytes,
479
+ outputDroppedBytes: captured.droppedBytes
480
+ },
481
+ truncated: captured.droppedBytes > 0,
464
482
  ...(images.length ? { images } : {})
465
483
  };
466
484
  }
@@ -34,6 +34,9 @@ export interface ExecResult {
34
34
  cached?: boolean;
35
35
  }
36
36
  export declare const execToolDefinition: ClientToolDefinition;
37
+ export declare const execWaitToolDefinition: ClientToolDefinition;
38
+ /** Validate the public entry points before delegating to the single process manager. */
39
+ export declare function execToolInput(name: 'exec' | 'exec_wait', value: unknown): ExecInput;
37
40
  export declare class ExecTool {
38
41
  #private;
39
42
  private readonly paths;
@@ -7,7 +7,7 @@ export const execToolDefinition = {
7
7
  name: 'exec',
8
8
  allowedCallers: ['direct', 'programmatic'],
9
9
  parallelSafety: 'safe',
10
- description: 'Unrestricted general system execution. Providing command starts any platform-native command, script, text/file tool, builder, generator, network client, or program in workspace/default cwd; action may be omitted for start. poll waits up to yieldMs for new output or termination and returns incremental stdout/stderr while running; after termination, poll returns a bounded cached result with earlier output for up to five minutes while the Session remains hosted. write sends stdin to running processes; cancel terminates a running process or removes a completed result. Returns status, processId, stdout/stderr, cumulative stdoutBytes/stderrBytes, per-result dropped bytes, exitCode, and truncated; a completed status does not imply exitCode 0. Always inspect stderr/exitCode and consume terminal output or cancel.',
10
+ description: 'Start a platform-native command, script, or program in workspace/default cwd. Returns a stable processId, status, bounded stdout/stderr, cumulative byte counts, dropped-byte counts, exitCode and truncated. A completed status does not imply exitCode 0; inspect stderr/exitCode. For subsequent waiting, stdin writes or cancellation, use exec_wait with processId, not exec. Truncation does not stop the process. Cancelling the initial wait does not terminate the process.',
11
11
  outputSchema: {
12
12
  type: 'object',
13
13
  required: ['content', 'data'],
@@ -44,13 +44,9 @@ export const execToolDefinition = {
44
44
  inputSchema: {
45
45
  type: 'object',
46
46
  properties: {
47
- action: {
48
- enum: ['start', 'poll', 'write', 'cancel'],
49
- description: 'Lifecycle operation. Omit when providing command to start a process.'
50
- },
51
47
  command: {
52
48
  type: 'string',
53
- description: 'Required for start; implies start when action is omitted.'
49
+ description: 'Platform-native command to start.'
54
50
  },
55
51
  cwd: {
56
52
  type: 'string',
@@ -62,15 +58,58 @@ export const execToolDefinition = {
62
58
  minimum: 0,
63
59
  maximum: TOOL_EXECUTION_LIMITS.execMaxYieldMs,
64
60
  default: TOOL_EXECUTION_LIMITS.execDefaultYieldMs,
65
- description: `Wait before yielding output. Defaults to ${TOOL_EXECUTION_LIMITS.execDefaultYieldMs} ms; effective range is 0-${TOOL_EXECUTION_LIMITS.execMaxYieldMs} ms.`
61
+ description: `Wait before yielding output. Defaults to ${TOOL_EXECUTION_LIMITS.execDefaultYieldMs} ms. Maximum ${TOOL_EXECUTION_LIMITS.execMaxYieldMs} ms. Zero returns immediately. This does not extend the process timeout.`
62
+ }
63
+ },
64
+ required: ['command'],
65
+ additionalProperties: false
66
+ }
67
+ };
68
+ export const execWaitToolDefinition = {
69
+ name: 'exec_wait',
70
+ allowedCallers: ['direct', 'programmatic'],
71
+ parallelSafety: 'safe',
72
+ description: 'Wait for or control a managed process by processId. action defaults to poll: wait for new output or termination and return incremental stdout/stderr. write sends data to stdin; cancel terminates the process or removes its cached result. Terminal polls return a bounded snapshot marked cached for up to five minutes. Cancelling a wait does not terminate the process. Uses the same Session-owned processes as exec; cannot start a process or extend its timeout. Inspect stderr/exitCode; completed does not imply success. Supports direct and programmatic calls; in Code Mode use tools.exec_wait. The cell execution deadline still bounds waits made inside a cell.',
73
+ outputSchema: execToolDefinition.outputSchema,
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ processId: {
78
+ type: 'string',
79
+ minLength: 1,
80
+ description: 'Stable processId returned by exec.'
66
81
  },
67
- processId: { type: 'string', description: 'Required for poll, write and cancel.' },
68
- data: { type: 'string', description: 'stdin data required for write.' }
82
+ action: { enum: ['poll', 'write', 'cancel'], default: 'poll' },
83
+ yieldMs: {
84
+ type: 'integer',
85
+ minimum: 0,
86
+ maximum: TOOL_EXECUTION_LIMITS.execMaxPollYieldMs,
87
+ default: TOOL_EXECUTION_LIMITS.execDefaultYieldMs,
88
+ description: `Poll wait window in milliseconds; default ${TOOL_EXECUTION_LIMITS.execDefaultYieldMs}, maximum ${TOOL_EXECUTION_LIMITS.execMaxPollYieldMs}. Zero returns immediately. Not a process timeout.`
89
+ },
90
+ data: { type: 'string', description: 'Required stdin data for write.' }
69
91
  },
70
- required: [],
92
+ required: ['processId'],
71
93
  additionalProperties: false
72
94
  }
73
95
  };
96
+ /** Validate the public entry points before delegating to the single process manager. */
97
+ export function execToolInput(name, value) {
98
+ if (!value || typeof value !== 'object' || Array.isArray(value))
99
+ return invalidExecInput();
100
+ const input = value;
101
+ const allowed = name === 'exec'
102
+ ? ['command', 'cwd', 'timeoutMs', 'yieldMs']
103
+ : ['processId', 'action', 'yieldMs', 'data'];
104
+ if (Object.keys(input).some((key) => !allowed.includes(key)))
105
+ return invalidExecInput();
106
+ if (name === 'exec')
107
+ return parseExecInput(input);
108
+ const action = input.action ?? 'poll';
109
+ if (!['poll', 'write', 'cancel'].includes(String(action)))
110
+ return invalidExecInput();
111
+ return parseExecInput({ ...input, action });
112
+ }
74
113
  export class ExecTool {
75
114
  paths;
76
115
  maxOutputBytes;
@@ -108,7 +147,7 @@ export class ExecTool {
108
147
  if (!process || (owner !== undefined && process.ownerSessionId !== owner.sessionId))
109
148
  throw new MarAgentError('MAR_AGENT_PROCESS_NOT_FOUND', 'Managed process was not found.');
110
149
  if (input.action === 'poll' && input.yieldMs)
111
- await waitForProcessChange(process, Math.min(Math.max(input.yieldMs, 0), TOOL_EXECUTION_LIMITS.execMaxYieldMs), signal);
150
+ await waitForProcessChange(process, Math.min(Math.max(input.yieldMs, 0), TOOL_EXECUTION_LIMITS.execMaxPollYieldMs), signal);
112
151
  if (input.action === 'write')
113
152
  process.child.stdin.write(input.data);
114
153
  if (input.action === 'cancel')
@@ -403,11 +442,10 @@ function appendOutput(output, chunk, maximumBytes, receivedBytes = chunk.length)
403
442
  const content = span.subarray(output.windowOffset, output.windowOffset + output.bytes);
404
443
  output.chunks = [];
405
444
  output.bytes = 0;
406
- output.droppedBytes += overflow;
407
445
  const pushSlice = (start, end) => {
408
446
  while (start < end && (content[start] & 0xc0) === 0x80)
409
447
  start++;
410
- while (end > start && (content[end - 1] & 0xc0) === 0x80)
448
+ while (end > start && end < content.length && (content[end] & 0xc0) === 0x80)
411
449
  end--;
412
450
  if (start >= end)
413
451
  return;
@@ -417,6 +455,7 @@ function appendOutput(output, chunk, maximumBytes, receivedBytes = chunk.length)
417
455
  };
418
456
  pushSlice(0, Math.min(head, content.length));
419
457
  pushSlice(Math.min(head + overflow, content.length), content.length);
458
+ output.droppedBytes += content.length - output.bytes;
420
459
  output.window = span;
421
460
  }
422
461
  function consumeOutput(output) {
@@ -436,7 +475,9 @@ function parseExecInput(value) {
436
475
  const action = input.action == null && typeof input.command === 'string' ? 'start' : input.action;
437
476
  if (!['start', 'poll', 'write', 'cancel'].includes(String(action)))
438
477
  return invalidExecInput();
439
- const yieldMs = optionalBoundedInteger(input.yieldMs, 0, TOOL_EXECUTION_LIMITS.execMaxYieldMs);
478
+ const yieldMs = optionalBoundedInteger(input.yieldMs, 0, action === 'poll'
479
+ ? TOOL_EXECUTION_LIMITS.execMaxPollYieldMs
480
+ : TOOL_EXECUTION_LIMITS.execMaxYieldMs);
440
481
  if (action === 'start') {
441
482
  if (typeof input.command !== 'string' || input.command.trim() === '')
442
483
  return invalidExecInput();
@@ -457,7 +498,7 @@ function parseExecInput(value) {
457
498
  return {
458
499
  action,
459
500
  processId: input.processId,
460
- ...(yieldMs === undefined ? {} : { yieldMs })
501
+ yieldMs: yieldMs ?? TOOL_EXECUTION_LIMITS.execDefaultYieldMs
461
502
  };
462
503
  if (action === 'write') {
463
504
  if (typeof input.data !== 'string')
@@ -10,6 +10,7 @@ export declare const TOOL_EXECUTION_LIMITS: Readonly<{
10
10
  execMaxTimeoutMs: 3600000;
11
11
  execDefaultYieldMs: 10000;
12
12
  execMaxYieldMs: 30000;
13
+ execMaxPollYieldMs: 300000;
13
14
  execTerminationGraceMs: 1000;
14
15
  execDefaultMaxOutputBytes: 1000000;
15
16
  maxProcesses: 8;
@@ -21,8 +22,10 @@ export declare const TOOL_EXECUTION_LIMITS: Readonly<{
21
22
  codeMaxTransportBytes: 67108864;
22
23
  codeMaxCalls: 128;
23
24
  codeMaxConcurrentCells: 4;
24
- codeDefaultYieldMs: 10000;
25
+ codeDefaultYieldMs: 30000;
26
+ codeDefaultWaitYieldMs: 10000;
25
27
  codeMaxYieldMs: 30000;
28
+ codeMaxWaitYieldMs: 300000;
26
29
  codeMaxOutputTokens: 10000;
27
30
  codeCompletedRetentionMs: 300000;
28
31
  codeMaxCompletedResults: 16;
@@ -10,6 +10,7 @@ export const TOOL_EXECUTION_LIMITS = Object.freeze({
10
10
  execMaxTimeoutMs: 3_600_000,
11
11
  execDefaultYieldMs: 10_000,
12
12
  execMaxYieldMs: 30_000,
13
+ execMaxPollYieldMs: 300_000,
13
14
  execTerminationGraceMs: 1_000,
14
15
  execDefaultMaxOutputBytes: 1_000_000,
15
16
  maxProcesses: 8,
@@ -21,8 +22,10 @@ export const TOOL_EXECUTION_LIMITS = Object.freeze({
21
22
  codeMaxTransportBytes: 67_108_864,
22
23
  codeMaxCalls: 128,
23
24
  codeMaxConcurrentCells: 4,
24
- codeDefaultYieldMs: 10_000,
25
+ codeDefaultYieldMs: 30_000,
26
+ codeDefaultWaitYieldMs: 10_000,
25
27
  codeMaxYieldMs: 30_000,
28
+ codeMaxWaitYieldMs: 300_000,
26
29
  codeMaxOutputTokens: 10_000,
27
30
  codeCompletedRetentionMs: 300_000,
28
31
  codeMaxCompletedResults: 16,
@@ -2,7 +2,7 @@ import { MarAgentError } from '../error.js';
2
2
  import { AGENT_EXECUTION_POLICY } from '../runtime/execution-policy.js';
3
3
  import { modelTokenBudgetBytes } from '../runtime/token-budget.js';
4
4
  import { ApplyPatchTool } from './apply-patch.js';
5
- import { ExecTool } from './exec.js';
5
+ import { ExecTool, execToolInput } from './exec.js';
6
6
  import { CodeModeTool } from './code-mode.js';
7
7
  import { ReadTool } from './read.js';
8
8
  import { WorkspacePathResolver } from './shared/path-resolver.js';
@@ -55,16 +55,16 @@ export class LocalToolRegistry {
55
55
  return { content: JSON.stringify(result), data: result };
56
56
  }
57
57
  ],
58
- [
59
- 'exec',
58
+ ...['exec', 'exec_wait'].map((name) => [
59
+ name,
60
60
  async (arguments_, context) => {
61
- const result = await this.#exec.execute(arguments_, context.signal, {
61
+ const result = await this.#exec.execute(execToolInput(name, arguments_), context.signal, {
62
62
  sessionId: context.sessionId,
63
63
  executionId: context.executionId
64
64
  });
65
65
  return { content: JSON.stringify(result), data: result, truncated: result.truncated };
66
66
  }
67
- ],
67
+ ]),
68
68
  ...(web.webSearch
69
69
  ? [
70
70
  [
@@ -1,5 +1,5 @@
1
1
  import { applyPatchToolDefinition } from './apply-patch.js';
2
- import { execToolDefinition } from './exec.js';
2
+ import { execToolDefinition, execWaitToolDefinition } from './exec.js';
3
3
  import { codeModeToolDefinition, codeExecWaitToolDefinition } from './code-mode.js';
4
4
  import { executeQuestion, questionToolDefinition } from './question.js';
5
5
  import { readToolDefinition } from './read.js';
@@ -20,6 +20,7 @@ export const BUILTIN_TOOL_DEFINITIONS = [
20
20
  readToolDefinition,
21
21
  viewImageToolDefinition,
22
22
  execToolDefinition,
23
+ execWaitToolDefinition,
23
24
  codeModeToolDefinition,
24
25
  codeExecWaitToolDefinition,
25
26
  applyPatchToolDefinition,