@myagentroam/agent 0.9.137 → 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 (38) 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 +1 -1
  4. package/dist/prompts/index.js +1 -1
  5. package/dist/prompts/resources.d.ts +1 -0
  6. package/dist/prompts/resources.js +8 -2
  7. package/dist/prompts/workflow.js +2 -2
  8. package/dist/runtime/context-gc.js +10 -6
  9. package/dist/runtime/context-projection.js +1 -1
  10. package/dist/sdk/agent.js +88 -53
  11. package/dist/sdk/types.d.ts +13 -1
  12. package/dist/session/context-gc-checkpoint.d.ts +1 -1
  13. package/dist/session/context-gc-checkpoint.js +11 -5
  14. package/dist/session/context-gc-replacements.d.ts +2 -0
  15. package/dist/session/context-gc-replacements.js +18 -0
  16. package/dist/session/jsonl-store.d.ts +5 -2
  17. package/dist/session/jsonl-store.js +105 -12
  18. package/dist/subagent/session-controller.d.ts +2 -0
  19. package/dist/subagent/session-controller.js +23 -6
  20. package/dist/tools/callers.js +2 -1
  21. package/dist/tools/code-mode/cells.d.ts +20 -0
  22. package/dist/tools/code-mode/cells.js +250 -0
  23. package/dist/tools/code-mode/dispatch.d.ts +21 -0
  24. package/dist/tools/code-mode/dispatch.js +53 -0
  25. package/dist/tools/code-mode/engine.d.ts +1 -0
  26. package/dist/tools/code-mode/quickjs-engine.js +4 -0
  27. package/dist/tools/code-mode/worker.js +1 -0
  28. package/dist/tools/code-mode.d.ts +10 -1
  29. package/dist/tools/code-mode.js +51 -10
  30. package/dist/tools/execution-limits.d.ts +5 -0
  31. package/dist/tools/execution-limits.js +5 -0
  32. package/dist/tools/local-registry.d.ts +5 -0
  33. package/dist/tools/local-registry.js +5 -1
  34. package/dist/tools/registry.js +2 -1
  35. package/dist/tools/scheduler.d.ts +2 -0
  36. package/dist/tools/scheduler.js +11 -0
  37. package/dist/tools/tool-search.js +2 -2
  38. package/package.json +1 -1
@@ -157,6 +157,9 @@ export class JsonlSessionStore {
157
157
  indexDirty: false,
158
158
  dirtySessionVersions: new Map(),
159
159
  pendingPublishIds: new Set(),
160
+ codeStateWrites: new Map(),
161
+ recordWrites: new Map(),
162
+ writerLeases: new Map(),
160
163
  segments: new Map(),
161
164
  executionModelReads: new Map()
162
165
  };
@@ -897,6 +900,22 @@ export class JsonlSessionStore {
897
900
  };
898
901
  }
899
902
  async append(sessionId, input) {
903
+ return this.#serializeRecordOperation(sessionId, () => this.#appendRecord(sessionId, input));
904
+ }
905
+ async #serializeRecordOperation(sessionId, action) {
906
+ const operation = (this.#shared.recordWrites.get(sessionId) ?? Promise.resolve())
907
+ .catch(() => undefined)
908
+ .then(action);
909
+ this.#shared.recordWrites.set(sessionId, operation);
910
+ try {
911
+ return await operation;
912
+ }
913
+ finally {
914
+ if (this.#shared.recordWrites.get(sessionId) === operation)
915
+ this.#shared.recordWrites.delete(sessionId);
916
+ }
917
+ }
918
+ async #appendRecord(sessionId, input) {
900
919
  this.#assertOpen();
901
920
  const meta = this.#shared.index.sessions.find((item) => item.id === sessionId) ??
902
921
  this.#shared.pendingSessions.get(sessionId);
@@ -974,6 +993,9 @@ export class JsonlSessionStore {
974
993
  return record;
975
994
  }
976
995
  async read(sessionId) {
996
+ return this.#serializeRecordOperation(sessionId, () => this.#readRecords(sessionId));
997
+ }
998
+ async #readRecords(sessionId) {
977
999
  this.#assertOpen();
978
1000
  try {
979
1001
  const records = [];
@@ -991,17 +1013,32 @@ export class JsonlSessionStore {
991
1013
  return restoreCodeModeState(await this.read(sessionId));
992
1014
  }
993
1015
  async commitCodeModeState(sessionId, mutations, input) {
994
- const parsed = parseCodeModeStateMutations(mutations);
995
- if (parsed.length === 0)
996
- return this.readCodeModeState(sessionId);
997
- const state = applyCodeModeStateMutations(await this.readCodeModeState(sessionId), parsed);
998
- await this.append(sessionId, {
999
- type: 'code.state.updated',
1000
- payload: { mutations: parsed },
1001
- executionId: input.executionId,
1002
- turnId: input.turnId
1016
+ const operation = (this.#shared.codeStateWrites.get(sessionId) ?? Promise.resolve())
1017
+ .catch(() => undefined)
1018
+ .then(async () => {
1019
+ const parsed = parseCodeModeStateMutations(mutations);
1020
+ if (parsed.length === 0)
1021
+ return this.readCodeModeState(sessionId);
1022
+ const current = await this.readCodeModeState(sessionId);
1023
+ if (input.expectedState && !isDeepStrictEqual(current, input.expectedState))
1024
+ throw new MarAgentError('MAR_AGENT_CODE_STATE_CONFLICT', 'Session state changed while this cell was running.');
1025
+ const state = applyCodeModeStateMutations(current, parsed);
1026
+ await this.append(sessionId, {
1027
+ type: 'code.state.updated',
1028
+ payload: { mutations: parsed },
1029
+ executionId: input.executionId,
1030
+ turnId: input.turnId
1031
+ });
1032
+ return state;
1003
1033
  });
1004
- return state;
1034
+ this.#shared.codeStateWrites.set(sessionId, operation);
1035
+ try {
1036
+ return await operation;
1037
+ }
1038
+ finally {
1039
+ if (this.#shared.codeStateWrites.get(sessionId) === operation)
1040
+ this.#shared.codeStateWrites.delete(sessionId);
1041
+ }
1005
1042
  }
1006
1043
  async readExecutionState(sessionId) {
1007
1044
  this.#assertOpen();
@@ -1155,8 +1192,64 @@ export class JsonlSessionStore {
1155
1192
  ...(input.turnId ? { turnId: input.turnId } : {})
1156
1193
  });
1157
1194
  }
1195
+ /** A warm cell retains physical writer ownership between Executions of the same Agent. */
1196
+ async acquireSessionLease(sessionId, owner) {
1197
+ return this.#serializeRecordOperation(sessionId, async () => {
1198
+ this.#assertOpen();
1199
+ let lease = this.#shared.writerLeases.get(sessionId);
1200
+ if (lease) {
1201
+ if (lease.executing || owner === undefined || lease.owner !== owner)
1202
+ throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session already has a writer.');
1203
+ lease.executing = true;
1204
+ }
1205
+ else {
1206
+ const release = await this.#acquireSessionLease(sessionId);
1207
+ lease = { owner, executing: true, retained: 0, release };
1208
+ this.#shared.writerLeases.set(sessionId, lease);
1209
+ }
1210
+ const current = lease;
1211
+ let released = false;
1212
+ return () => this.#serializeRecordOperation(sessionId, async () => {
1213
+ if (released)
1214
+ return;
1215
+ released = true;
1216
+ current.executing = false;
1217
+ await this.#releaseUnusedWriter(sessionId, current);
1218
+ });
1219
+ });
1220
+ }
1221
+ hasRetainedSessionLease(sessionId, owner) {
1222
+ const lease = this.#shared.writerLeases.get(sessionId);
1223
+ return lease?.owner === owner && lease.retained > 0;
1224
+ }
1225
+ async retainSessionLease(sessionId, owner) {
1226
+ return this.#serializeRecordOperation(sessionId, async () => {
1227
+ const lease = this.#shared.writerLeases.get(sessionId);
1228
+ if (!lease || !lease.executing || lease.owner !== owner)
1229
+ throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Retained work requires its originating Session writer.');
1230
+ lease.retained++;
1231
+ let released = false;
1232
+ return () => this.#serializeRecordOperation(sessionId, async () => {
1233
+ if (released)
1234
+ return;
1235
+ released = true;
1236
+ lease.retained--;
1237
+ await this.#releaseUnusedWriter(sessionId, lease);
1238
+ });
1239
+ });
1240
+ }
1241
+ async #releaseUnusedWriter(sessionId, lease) {
1242
+ if (lease.executing || lease.retained)
1243
+ return;
1244
+ try {
1245
+ await lease.release();
1246
+ }
1247
+ finally {
1248
+ this.#shared.writerLeases.delete(sessionId);
1249
+ }
1250
+ }
1158
1251
  /** Recover files under the sole writer lock; flush dirty index state before releasing it. */
1159
- async acquireSessionLease(sessionId) {
1252
+ async #acquireSessionLease(sessionId) {
1160
1253
  this.#assertOpen();
1161
1254
  await this.#refreshIndex();
1162
1255
  if (!this.#shared.index.sessions.some((item) => item.id === sessionId))
@@ -1509,7 +1602,7 @@ export class JsonlSessionStore {
1509
1602
  const latestPath = paths.at(-1);
1510
1603
  await repairSegmentTail(latestPath);
1511
1604
  const records = await readRecords(latestPath);
1512
- await recoverPendingInputImages(this.sessionDirectory(sessionId), () => this.read(sessionId));
1605
+ await recoverPendingInputImages(this.sessionDirectory(sessionId), () => this.#readRecords(sessionId));
1513
1606
  const last = records.at(-1);
1514
1607
  const match = /events-(\d{6})\.jsonl$/.exec(latestPath);
1515
1608
  const segment = Number(match?.[1] ?? 1);
@@ -83,6 +83,8 @@ export declare class SubagentSessionController implements SubagentController {
83
83
  };
84
84
  }): Promise<SubagentSessionController>;
85
85
  beginParentExecution(execution: SubagentParentExecution): Promise<void>;
86
+ /** A retained cell must not borrow a later model Execution's identity or task budget. */
87
+ captureExecution(): SubagentController;
86
88
  endParentExecution(executionId: string): void;
87
89
  run(input: SubagentTaskInput, options?: SubagentRunOptions): Promise<SubagentTaskSnapshot>;
88
90
  output(agentId: string, options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { MarAgentError, describeMarAgentError } from '../error.js';
3
4
  import { TOOL_EXECUTION_LIMITS } from '../tools/execution-limits.js';
@@ -14,7 +15,8 @@ export class SubagentSessionController {
14
15
  #resourceRevision = 0;
15
16
  #running = 0;
16
17
  #disposed = false;
17
- #createdInExecution = 0;
18
+ #creationBudget = { count: 0 };
19
+ #retainedExecution = new AsyncLocalStorage();
18
20
  #execution;
19
21
  constructor(parentSessionId, rootSessionId, backend, limits = { maxTasks: 8, maxConcurrent: 4 }) {
20
22
  this.parentSessionId = parentSessionId;
@@ -36,10 +38,23 @@ export class SubagentSessionController {
36
38
  if (this.#disposed)
37
39
  throw new MarAgentError('MAR_AGENT_SUBAGENT_LIMIT', 'Subagent controller is disposed.');
38
40
  this.#execution = execution;
39
- this.#createdInExecution = 0;
41
+ this.#creationBudget = { count: 0 };
40
42
  for (const state of this.#agents.values())
41
43
  await this.#publishTerminal(state);
42
44
  }
45
+ /** A retained cell must not borrow a later model Execution's identity or task budget. */
46
+ captureExecution() {
47
+ const context = { execution: this.#requireExecution(), budget: this.#creationBudget };
48
+ return {
49
+ run: (input, options) => this.#retainedExecution.run(context, () => this.run(input, options)),
50
+ message: (id, message, options) => this.#retainedExecution.run(context, () => this.message(id, message, options)),
51
+ output: (id, options) => this.output(id, options),
52
+ outputAny: (options) => this.outputAny(options),
53
+ cancel: (id) => this.cancel(id),
54
+ listSessionResources: () => this.listSessionResources(),
55
+ dispose: () => this.dispose()
56
+ };
57
+ }
43
58
  endParentExecution(executionId) {
44
59
  if (this.#execution?.executionId === executionId)
45
60
  this.#execution = undefined;
@@ -48,7 +63,8 @@ export class SubagentSessionController {
48
63
  options.signal?.throwIfAborted();
49
64
  const execution = this.#requireExecution();
50
65
  validateTaskInput(input);
51
- if (this.#createdInExecution >= this.limits.maxTasks)
66
+ if ((this.#retainedExecution.getStore()?.budget ?? this.#creationBudget).count >=
67
+ this.limits.maxTasks)
52
68
  throw new MarAgentError('MAR_AGENT_SUBAGENT_LIMIT', 'Subagent task limit was reached.');
53
69
  const source = {
54
70
  type: 'subagent',
@@ -85,7 +101,7 @@ export class SubagentSessionController {
85
101
  terminalObserved: false,
86
102
  resourceRevision: ++this.#resourceRevision
87
103
  };
88
- this.#createdInExecution++;
104
+ (this.#retainedExecution.getStore()?.budget ?? this.#creationBudget).count++;
89
105
  this.#agents.set(state.agentId, state);
90
106
  const knownModel = execution.models.get(input.modelId ?? execution.modelId);
91
107
  if (knownModel) {
@@ -589,9 +605,10 @@ export class SubagentSessionController {
589
605
  #requireExecution() {
590
606
  if (this.#disposed)
591
607
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
592
- if (!this.#execution)
608
+ const execution = this.#retainedExecution.getStore()?.execution ?? this.#execution;
609
+ if (!execution)
593
610
  throw new MarAgentError('MAR_AGENT_SUBAGENT_FAILED', 'Subagent control requires an active parent execution.');
594
- return this.#execution;
611
+ return execution;
595
612
  }
596
613
  #receiptSnapshot(state) {
597
614
  return this.#snapshot(state, {
@@ -12,7 +12,8 @@ export function toolCallers(tool) {
12
12
  }
13
13
  export function allowsToolCaller(tool, caller) {
14
14
  // Control tools cannot be widened by a Host override or historical definition.
15
- if (caller === 'programmatic' && ['code_mode', 'tool_search', 'question'].includes(tool.name))
15
+ if (caller === 'programmatic' &&
16
+ ['code_exec', 'code_exec_wait', 'code_mode', 'tool_search', 'question'].includes(tool.name))
16
17
  return false;
17
18
  return toolCallers(tool).includes(caller);
18
19
  }
@@ -0,0 +1,20 @@
1
+ import type { ToolExecutionContext, ToolExecutionResult } from '../../sdk/types.js';
2
+ export interface CellOutput {
3
+ text(value: string): void;
4
+ image(value: NonNullable<ToolExecutionResult['images']>[number]): void;
5
+ yield(): void;
6
+ }
7
+ type Status = 'running' | 'completed' | 'failed' | 'cancelled';
8
+ /** Session-owned execution, separate from the cancellable model-tool wait. */
9
+ export declare class CodeCells {
10
+ #private;
11
+ start(value: unknown, context: ToolExecutionContext, run: (code: string, context: ToolExecutionContext, output: CellOutput) => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>;
12
+ wait(value: unknown, context: ToolExecutionContext): Promise<ToolExecutionResult>;
13
+ snapshot(sessionId: string): {
14
+ cellId: string;
15
+ status: Status;
16
+ executionId: string;
17
+ }[];
18
+ dispose(sessionId?: string): Promise<void>;
19
+ }
20
+ export {};
@@ -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 });
@@ -3,7 +3,9 @@ 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;
8
+ export declare const codeExecWaitToolDefinition: ClientToolDefinition;
7
9
  /** Deferred definitions travel in search results, not in the stable Code Mode description. */
8
10
  export declare function projectCodeModeTools(tools: readonly ClientToolDefinition[], residentTools?: readonly ClientToolDefinition[]): ClientToolDefinition[];
9
11
  /** Preserve native search definitions while providing the same discovery evidence to other protocols. */
@@ -14,7 +16,14 @@ export declare class CodeModeTool {
14
16
  private readonly environment;
15
17
  private readonly executor;
16
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
+ }[];
17
26
  constructor(paths: WorkspacePathResolver, environment?: NodeJS.ProcessEnv, executor?: LocalProcessExecutor, engineFactory?: CodeModeEngineFactory);
18
- execute(value: unknown, context: ToolExecutionContext): Promise<ToolExecutionResult>;
27
+ execute(value: unknown, context: ToolExecutionContext, output?: CellOutput): Promise<ToolExecutionResult>;
19
28
  dispose(sessionId?: string): Promise<void>;
20
29
  }