@myagentroam/agent 0.9.139 → 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,6 +1,6 @@
1
1
  import type { CodeModeEngineFactory } from '../tools/code-mode/engine.js';
2
2
  import type { HostDescription } from './contracts.js';
3
- import type { LocalProcessExecutor } from './local-process-executor.js';
3
+ import { type LocalProcessExecutor } from './local-process-executor.js';
4
4
  import type { WorkspacePathMount } from '../tools/shared/path-resolver.js';
5
5
  import type { MarAgentEvent, MarAgentHost, HostExecutionExtensions, McpToolCatalogContext, McpToolCatalog, McpToolCallContext, ToolCall, ToolExecutionContext, ToolExecutionResult } from '../sdk/types.js';
6
6
  export interface LocalAgentMcpBinding {
@@ -13,6 +13,8 @@ export interface LocalAgentExecutionMcpBinding extends LocalAgentMcpBinding {
13
13
  readonly extensions?: LocalAgentExecutionExtensions;
14
14
  }
15
15
  export interface LocalAgentExecutionExtensions extends HostExecutionExtensions {
16
+ /** Host-private process environment, snapshotted when bound to an execution. */
17
+ readonly environment?: Readonly<Record<string, string>>;
16
18
  executeTool?(call: ToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
17
19
  }
18
20
  export declare class LocalAgentHost implements MarAgentHost {
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { basename, delimiter, isAbsolute, join, resolve, win32 } from 'node:path';
5
+ import { directLocalProcessExecutor } from './local-process-executor.js';
5
6
  import { MAR_AGENT_HOST_API_VERSION } from './contracts.js';
6
7
  import { MarAgentError } from '../error.js';
7
8
  import { LocalToolRegistry } from '../tools/local-registry.js';
@@ -36,6 +37,7 @@ export class LocalAgentHost {
36
37
  this.#hasWebSearch = input.webSearch !== undefined;
37
38
  this.#hasWebFetch = input.webFetch !== false;
38
39
  this.#resourceIdleTtlMs = input.resourceIdleTtlMs;
40
+ const processExecutor = input.processExecutor ?? directLocalProcessExecutor;
39
41
  this.#tools = new LocalToolRegistry(this.#workspace, this.#environment, this.#shell.path, {
40
42
  ...(input.webSearch ? { webSearch: input.webSearch } : {}),
41
43
  webFetch: this.#hasWebFetch,
@@ -44,7 +46,25 @@ export class LocalAgentHost {
44
46
  ...(input.codeModeEngineFactory === undefined
45
47
  ? {}
46
48
  : { codeModeEngineFactory: input.codeModeEngineFactory }),
47
- ...(input.processExecutor === undefined ? {} : { processExecutor: input.processExecutor }),
49
+ processExecutor: {
50
+ spawn: (request) => {
51
+ const binding = request.owner && this.#executionMcp.get(request.owner.executionId);
52
+ const environment = binding?.active ? binding.extensions?.environment : undefined;
53
+ return processExecutor.spawn(environment === undefined
54
+ ? request
55
+ : {
56
+ ...request,
57
+ environment: { ...request.environment, ...environment },
58
+ ...(request.environmentKeys === undefined
59
+ ? {}
60
+ : {
61
+ environmentKeys: [
62
+ ...new Set([...request.environmentKeys, ...Object.keys(environment)])
63
+ ]
64
+ })
65
+ });
66
+ }
67
+ },
48
68
  ...(input.restrictPaths === undefined ? {} : { restrictPaths: input.restrictPaths }),
49
69
  ...(input.workspaceWritable === undefined
50
70
  ? {}
@@ -108,7 +128,12 @@ export class LocalAgentHost {
108
128
  (async () => {
109
129
  throw new MarAgentError('MAR_AGENT_MCP_FAILED', 'MCP execution is unavailable.');
110
130
  }),
111
- extensions,
131
+ extensions: {
132
+ ...extensions,
133
+ ...(extensions.environment === undefined
134
+ ? {}
135
+ : { environment: { ...extensions.environment } })
136
+ },
112
137
  active: true
113
138
  };
114
139
  this.#currentExecutionMcp = binding;
@@ -116,7 +141,8 @@ export class LocalAgentHost {
116
141
  }
117
142
  async listExecutionExtensions(context) {
118
143
  const binding = this.#executionBinding(context);
119
- return binding?.extensions ?? this.#extensions ?? {};
144
+ const { environment: _environment, ...extensions } = binding?.extensions ?? this.#extensions ?? {};
145
+ return extensions;
120
146
  }
121
147
  emit(event) {
122
148
  this.events.push(event);
@@ -12,7 +12,9 @@ export function retainedSessionResourcesSnapshot(input) {
12
12
  ]
13
13
  : []),
14
14
  'These resources belong to this Session and survived the previous Execution. Reuse their stable IDs instead of starting duplicate processes or subagents.',
15
- processes.length === 0 ? '' : 'Cancelling a managed process releases that process resource.',
15
+ processes.length === 0
16
+ ? ''
17
+ : 'Use exec_wait with processId to wait, write stdin or cancel; in Code Mode use tools.exec_wait. Cancelling a wait leaves the process running; explicit cancel releases the process resource.',
16
18
  subagents.length === 0
17
19
  ? ''
18
20
  : 'agent_cancel stops only the current child turn and retains its Session for later messages or explicit deletion.',
@@ -40,7 +42,7 @@ export function retainedSessionResourcesUpdate(snapshot, previousSnapshot, previ
40
42
  return undefined;
41
43
  if (!snapshot)
42
44
  return previousSnapshot || previousMayContainResources
43
- ? 'The previously provided retained Session resources no longer apply. No retained processes, code cells or subagents remain.'
45
+ ? 'The previously provided retained Session resources no longer apply. No retained processes, code cells or subagents remain. Historical resource IDs are not live handles; do not poll or wait on them. Use already recorded results or saved artifacts to recover completed work before starting anything again.'
44
46
  : undefined;
45
47
  return previousSnapshot || previousMayContainResources
46
48
  ? `This replaces all previously provided retained Session resources.\n\n${snapshot}`
@@ -16,6 +16,8 @@ export function toolUsagePrompt(tools, directTools = tools) {
16
16
  clauses.push('If the request names or clearly matches an available Skill description, call skill before generic tools and follow the loaded workflow; semantic matches do not require the user to know the Skill name. Do not load unrelated Skills or claim to use one without calling skill.');
17
17
  if (names.has('exec'))
18
18
  clauses.push('Use exec for platform-native commands, scripts, verification, and mechanical edits. Prefer rg or rg --files for search. Batch independent lookups and already-decided commands, preserving failure ordering. Use scripts for mechanical substitutions instead of reproducing long unchanged text in a patch; inspect actual effects afterward.');
19
+ if (names.has('exec_wait'))
20
+ clauses.push('Use exec_wait with processId for subsequent process waits, stdin writes or cancellation; in Code Mode use tools.exec_wait. exec only starts commands.');
19
21
  if (names.has('read'))
20
22
  clauses.push('Use read to batch known files and ranges. Batch only already-located, bounded ranges that are likely to fit together; for large files or documents, locate relevant sections before reading them. Read only what can affect the next decision, continue only relevant incomplete results using returned metadata, and avoid rereading unchanged content. Do not edit from an incomplete oversized-line preview. Obtain exact source with exec; if it still cannot fit, use a preconditioned deterministic replacement and verify the result.');
21
23
  if (names.has('apply_patch'))
@@ -690,7 +690,7 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
690
690
  let protectedTokens = 0;
691
691
  let retryAfterRollouts = Infinity;
692
692
  const execGroups = [...groups.values()]
693
- .filter((group) => group.toolName === 'exec')
693
+ .filter((group) => group.toolName === 'exec' || group.toolName === 'exec_wait')
694
694
  .sort((left, right) => (left.call?.record.sequence ?? Number.MAX_SAFE_INTEGER) -
695
695
  (right.call?.record.sequence ?? Number.MAX_SAFE_INTEGER));
696
696
  const invocations = new Map();
@@ -713,15 +713,32 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
713
713
  }
714
714
  for (const startGroup of execGroups) {
715
715
  const start = invocations.get(startGroup.callId);
716
- if (!start ||
717
- (start.input.action !== 'start' &&
718
- !(start.input.action == null && typeof start.input.command === 'string')))
716
+ if (!start)
719
717
  continue;
718
+ const isStart = start.group.toolName === 'exec' &&
719
+ (start.input.action === 'start' ||
720
+ (start.input.action == null && typeof start.input.command === 'string'));
721
+ const processId = typeof start.input.processId === 'string'
722
+ ? start.input.processId
723
+ : typeof start.result.processId === 'string'
724
+ ? start.result.processId
725
+ : undefined;
726
+ if (!isStart) {
727
+ if (start.group.toolName !== 'exec_wait' || !processId)
728
+ continue;
729
+ // A process may start inside a cell or before the visible history boundary.
730
+ // Begin with its first visible wait, but never split an unfinished lifecycle.
731
+ const previous = execGroups.findLast((group) => (group.call?.record.sequence ?? Infinity) < start.group.call.record.sequence &&
732
+ ((isRecord(group.call?.message.arguments) &&
733
+ group.call.message.arguments.processId === processId) ||
734
+ invocations.get(group.callId)?.result.processId === processId));
735
+ const previousStatus = previous && invocations.get(previous.callId)?.result.status;
736
+ if (previous && previousStatus !== 'completed' && previousStatus !== 'cancelled')
737
+ continue;
738
+ }
720
739
  const lifecycle = [start];
721
- let processId;
722
740
  let terminal = start.result.status !== 'running';
723
741
  if (!terminal) {
724
- processId = typeof start.result.processId === 'string' ? start.result.processId : undefined;
725
742
  if (!processId)
726
743
  continue;
727
744
  const relatedGroups = execGroups.filter((group) => (group.call?.record.sequence ?? 0) > start.group.call.record.sequence &&
@@ -765,10 +782,10 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
765
782
  if (!existingTargets.has(contextGcTargetKey(callTarget)))
766
783
  replacements.push({
767
784
  kind: 'TOOL_CALL_INPUT',
768
- toolName: 'exec',
785
+ toolName: invocation.group.toolName,
769
786
  callId: invocation.group.callId,
770
787
  target: callTarget,
771
- replacementArguments: contextGcExecInputReplacement(invocation.input),
788
+ replacementArguments: contextGcExecInputReplacement(invocation.input, 3, invocation.group.toolName),
772
789
  providerAliasTargets: invocation.group.aliases
773
790
  .map(({ record, message }) => targetFor(record, message))
774
791
  .filter((target) => target !== undefined),
package/dist/sdk/agent.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { isDeepStrictEqual } from 'node:util';
3
3
  import { describeMarAgentError, MarAgentError } from '../error.js';
4
+ import { CodeCellExecutionError } from '../tools/code-mode/cells.js';
4
5
  import { negotiateHost } from '../host/contracts.js';
5
6
  import { AnthropicMessagesAdapter, isAnthropicRecoverableStreamError } from '../model/anthropic-messages.js';
6
7
  import { marAgentReasoningEffortSchema, resolveModelDefaultReasoningEffort, usesResponsesConfigurationUpdates } from '../model/configuration.js';
@@ -37,7 +38,6 @@ import { createSessionToolScheduler, scheduleToolCallBatch } from '../tools/sche
37
38
  import { projectCodeModeSearchResult, projectCodeModeTools } from '../tools/code-mode.js';
38
39
  import { allowsDirectTool, allowsToolCaller, toolCallers } from '../tools/callers.js';
39
40
  import { SubagentSessionController } from '../subagent/session-controller.js';
40
- const MODEL_PERFORMANCE_MIN_INTERVAL_MS = 1_000;
41
41
  /** Retry only classified recoverable failures within the attempt budget; never retry cancellation. */
42
42
  async function recoverModelStream(error, retryState, signal) {
43
43
  if (signal.aborted ||
@@ -612,7 +612,14 @@ export async function createMarAgentWithAdapter(options) {
612
612
  instructionContent: renderAgentInstructions(instructionEntries, compactMayContainAgentInstructions ||
613
613
  contextualWorldState.instructionSnapshot !== undefined),
614
614
  resourceSnapshot: currentResourceSnapshot,
615
- previousMayContainResources: compactMayContainRetainedResources
615
+ previousMayContainResources: (!contextualWorldState.initialized && compactMayContainRetainedResources) ||
616
+ (options.host.listSessionResources !== undefined &&
617
+ currentResourceSnapshot === undefined &&
618
+ messages
619
+ .slice(contextualWorldState.entries.findLast((entry) => entry.kind === 'resources')
620
+ ?.index ?? 0)
621
+ .some((message) => message.role === 'tool_call' &&
622
+ ['exec', 'code_exec', 'code_mode', 'agent_start'].includes(message.name ?? '')))
616
623
  });
617
624
  sessionRuntime.contextualWorldState = contextualWorldState;
618
625
  if (!isDeepStrictEqual(previousContextualWorldState, contextualWorldState))
@@ -900,38 +907,29 @@ export async function createMarAgentWithAdapter(options) {
900
907
  let firstModelResponseAt;
901
908
  let requestCompletedAt;
902
909
  let requestOutputTokens;
903
- let outputTokenRevision = 0;
904
- let lastPerformanceEmittedAt;
905
- let lastPublishedOutputTokenRevision = 0;
906
- let performanceEmitted = false;
907
910
  const roundMessageCount = messages.length;
908
- const emitPerformance = async (sampledAt, force = false) => {
911
+ const emitLatency = async () => {
909
912
  if (mode === 'compact' || firstModelResponseAt === undefined)
910
913
  return;
911
- const outputDurationMs = Math.max(0, Math.round(sampledAt - firstModelResponseAt));
912
- const outputTokens = requestOutputTokens;
913
- const tokensPerSecond = outputTokens !== undefined && outputDurationMs > 0
914
- ? Number(((outputTokens * 1000) / outputDurationMs).toFixed(2))
915
- : undefined;
916
- if (performanceEmitted &&
917
- (tokensPerSecond === undefined ||
918
- outputTokenRevision === lastPublishedOutputTokenRevision))
914
+ await emit({
915
+ type: 'model.performance',
916
+ latencyMs: Math.max(0, Math.round(firstModelResponseAt - requestStartedAt))
917
+ });
918
+ };
919
+ const emitFinalPerformance = async (completedAt) => {
920
+ if (mode === 'compact' || firstModelResponseAt === undefined)
919
921
  return;
920
- if (!force &&
921
- lastPerformanceEmittedAt !== undefined &&
922
- sampledAt - lastPerformanceEmittedAt < MODEL_PERFORMANCE_MIN_INTERVAL_MS)
922
+ const outputDurationMs = Math.max(0, Math.round(completedAt - firstModelResponseAt));
923
+ const outputTokens = requestOutputTokens;
924
+ if (outputTokens === undefined || outputDurationMs <= 0)
923
925
  return;
924
926
  await emit({
925
927
  type: 'model.performance',
926
928
  latencyMs: Math.max(0, Math.round(firstModelResponseAt - requestStartedAt)),
927
- ...(outputTokens === undefined ? {} : { outputTokens }),
929
+ outputTokens,
928
930
  outputDurationMs,
929
- ...(tokensPerSecond === undefined ? {} : { tokensPerSecond })
931
+ tokensPerSecond: Number(((outputTokens * 1000) / outputDurationMs).toFixed(2))
930
932
  });
931
- performanceEmitted = true;
932
- lastPerformanceEmittedAt = sampledAt;
933
- if (tokensPerSecond !== undefined)
934
- lastPublishedOutputTokenRevision = outputTokenRevision;
935
933
  };
936
934
  const persistToolCalls = async (calls) => {
937
935
  for (const call of calls) {
@@ -979,6 +977,7 @@ export async function createMarAgentWithAdapter(options) {
979
977
  onAttemptDiagnostic: recordModelAttempt(mode === 'compact' ? 'COMPACTION' : 'EXECUTION')
980
978
  })) {
981
979
  const modelResponseAt = mode !== 'compact' && isModelResponseEvent(event) ? performance.now() : undefined;
980
+ const isFirstModelResponse = modelResponseAt !== undefined && firstModelResponseAt === undefined;
982
981
  if (modelResponseAt !== undefined) {
983
982
  sawModelSemanticEvent = true;
984
983
  firstModelResponseAt ??= modelResponseAt;
@@ -1070,7 +1069,6 @@ export async function createMarAgentWithAdapter(options) {
1070
1069
  : undefined;
1071
1070
  if (outputTokens !== undefined) {
1072
1071
  requestOutputTokens = outputTokens;
1073
- outputTokenRevision++;
1074
1072
  }
1075
1073
  const reportedContext = event.usage.contextInputTokens ?? event.usage.inputTokens;
1076
1074
  if (reportedContext !== undefined &&
@@ -1121,12 +1119,12 @@ export async function createMarAgentWithAdapter(options) {
1121
1119
  stop = event.reason;
1122
1120
  requestCompletedAt = performance.now();
1123
1121
  }
1124
- if (modelResponseAt !== undefined)
1125
- await emitPerformance(modelResponseAt);
1122
+ if (isFirstModelResponse)
1123
+ await emitLatency();
1126
1124
  }
1127
1125
  if (mode !== 'compact' && firstModelResponseAt !== undefined) {
1128
1126
  const completedAt = requestCompletedAt ?? performance.now();
1129
- await emitPerformance(completedAt, true);
1127
+ await emitFinalPerformance(completedAt);
1130
1128
  }
1131
1129
  pendingOutputContinuation = undefined;
1132
1130
  incompleteToolCall = pendingTools.some((call) => call.argumentError !== undefined);
@@ -1291,6 +1289,7 @@ export async function createMarAgentWithAdapter(options) {
1291
1289
  }
1292
1290
  catch (error) {
1293
1291
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_HOST_OPERATION_FAILED');
1292
+ const partial = error instanceof CodeCellExecutionError ? error.result : undefined;
1294
1293
  await emit({
1295
1294
  type: 'tool.failed',
1296
1295
  toolName: call.name,
@@ -1301,13 +1300,16 @@ export async function createMarAgentWithAdapter(options) {
1301
1300
  });
1302
1301
  return {
1303
1302
  call,
1304
- content: JSON.stringify({
1305
- error: diagnostic.code,
1306
- message: diagnostic.message,
1307
- ...(error instanceof MarAgentError && error.retryable
1308
- ? { retryable: true }
1309
- : {})
1310
- }),
1303
+ content: partial === undefined
1304
+ ? JSON.stringify({
1305
+ error: diagnostic.code,
1306
+ message: diagnostic.message,
1307
+ ...(error instanceof MarAgentError && error.retryable
1308
+ ? { retryable: true }
1309
+ : {})
1310
+ })
1311
+ : boundedModelToolOutput(call.name, partial.content, partial.data).content,
1312
+ ...(partial?.images?.length ? { images: partial.images } : {}),
1311
1313
  failed: true,
1312
1314
  error
1313
1315
  };
@@ -2237,7 +2239,7 @@ function updateContextualWorldState(state, index, current) {
2237
2239
  if (current.instructionContent !== undefined)
2238
2240
  entries.push({ index, kind: 'instructions', content: current.instructionContent });
2239
2241
  }
2240
- const resourceContent = retainedSessionResourcesUpdate(current.resourceSnapshot, state.initialized ? state.resourceSnapshot : undefined, !state.initialized && current.previousMayContainResources);
2242
+ const resourceContent = retainedSessionResourcesUpdate(current.resourceSnapshot, state.initialized ? state.resourceSnapshot : undefined, current.previousMayContainResources);
2241
2243
  if (resourceContent !== undefined)
2242
2244
  entries.push({ index, kind: 'resources', content: resourceContent });
2243
2245
  return {
@@ -2283,7 +2285,7 @@ function estimatedRequestTokens(system, messages, tools, images) {
2283
2285
  transientImageCount * AGENT_EXECUTION_POLICY.estimatedTokensPerImage);
2284
2286
  }
2285
2287
  function boundedModelToolOutput(toolName, value, data) {
2286
- const execDiagnostics = toolName === 'exec' &&
2288
+ const execDiagnostics = (toolName === 'exec' || toolName === 'exec_wait') &&
2287
2289
  isRecord(data) &&
2288
2290
  typeof data.stdout === 'string' &&
2289
2291
  typeof data.stderr === 'string'
@@ -2390,7 +2392,7 @@ function structuredToolEventData(toolName, value) {
2390
2392
  typeof value.sha256 === 'string' &&
2391
2393
  /^[a-f0-9]{64}$/u.test(value.sha256))
2392
2394
  return value;
2393
- if (toolName === 'exec' &&
2395
+ if ((toolName === 'exec' || toolName === 'exec_wait') &&
2394
2396
  (value.status === 'running' || value.status === 'completed' || value.status === 'cancelled') &&
2395
2397
  typeof value.stdout === 'string' &&
2396
2398
  typeof value.stderr === 'string' &&
@@ -50,7 +50,7 @@ export type ContextGcReplacement = {
50
50
  readonly evidenceSequences: readonly number[];
51
51
  } | {
52
52
  readonly kind: 'TOOL_CALL_INPUT';
53
- readonly toolName: 'apply_patch' | 'exec' | 'code_mode' | 'code_exec';
53
+ readonly toolName: 'apply_patch' | 'exec' | 'exec_wait' | 'code_mode' | 'code_exec';
54
54
  readonly callId: string;
55
55
  readonly target: ContextGcTarget;
56
56
  readonly replacementArguments: unknown;
@@ -350,8 +350,9 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
350
350
  if (!isSuccessfulExecLifecycleEvidence(replacement, source, records, recordsBySequence))
351
351
  return false;
352
352
  if (replacement.kind === 'TOOL_CALL_INPUT')
353
- return isDeepStrictEqual(replacement.replacementArguments, contextGcExecInputReplacement(source.payload.arguments, execInputReplacementFormatVersion(replacement.replacementArguments)));
354
- const targetEvidence = successfulToolEvidence(records, recordsBySequence, source.sequence, 'exec');
353
+ return isDeepStrictEqual(replacement.replacementArguments, contextGcExecInputReplacement(source.payload.arguments, execInputReplacementFormatVersion(replacement.replacementArguments), replacement.toolName));
354
+ const targetEvidence = successfulToolEvidence(records, recordsBySequence, source.sequence, 'exec') ??
355
+ successfulToolEvidence(records, recordsBySequence, source.sequence, 'exec_wait');
355
356
  const result = normalizeContextGcExecResult(source.payload.content, targetEvidence?.completed.payload);
356
357
  return (result !== undefined &&
357
358
  replacement.replacementContent ===
@@ -474,7 +475,8 @@ function successfulPatchEvidenceForPath(records, recordsBySequence, evidenceSequ
474
475
  : undefined;
475
476
  }
476
477
  function isSuccessfulExecLifecycleEvidence(replacement, source, records, recordsBySequence) {
477
- const evidence = successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, 'exec');
478
+ const evidence = successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, 'exec') ??
479
+ successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, 'exec_wait');
478
480
  if (!evidence || !isRecord(evidence.call.payload) || !isRecord(evidence.result.payload))
479
481
  return false;
480
482
  const finalResult = normalizeContextGcExecResult(evidence.result.payload.content, evidence.completed.payload);
@@ -487,7 +489,7 @@ function isSuccessfulExecLifecycleEvidence(replacement, source, records, records
487
489
  targetCall.sequence > evidence.call.sequence ||
488
490
  !isRecord(targetCall.payload) ||
489
491
  targetCall.payload.role !== 'tool_call' ||
490
- targetCall.payload.name !== 'exec' ||
492
+ (targetCall.payload.name !== 'exec' && targetCall.payload.name !== 'exec_wait') ||
491
493
  targetCall.payload.callId !== replacement.callId)
492
494
  return false;
493
495
  const targetResult = replacement.kind === 'EXEC_OUTPUT'
@@ -495,7 +497,7 @@ function isSuccessfulExecLifecycleEvidence(replacement, source, records, records
495
497
  : modelToolResultForCall(records, targetCall, evidence.result.sequence);
496
498
  if (!targetResult)
497
499
  return false;
498
- const targetEvidence = successfulToolEvidence(records, recordsBySequence, targetResult.sequence, 'exec');
500
+ const targetEvidence = successfulToolEvidence(records, recordsBySequence, targetResult.sequence, targetCall.payload.name);
499
501
  if (!targetEvidence || targetEvidence.call.sequence !== targetCall.sequence)
500
502
  return false;
501
503
  if (targetEvidence.callId === evidence.callId)
@@ -631,6 +633,7 @@ function isContextGcReplacement(value) {
631
633
  value.target.role === 'tool_call' &&
632
634
  (value.toolName === 'apply_patch' ||
633
635
  value.toolName === 'exec' ||
636
+ value.toolName === 'exec_wait' ||
634
637
  value.toolName === 'code_mode' ||
635
638
  value.toolName === 'code_exec') &&
636
639
  value.target.toolName === value.toolName &&
@@ -56,7 +56,7 @@ export declare function contextGcStaleReadReplacement(input: {
56
56
  readonly formatVersion?: 1 | 2;
57
57
  }): string;
58
58
  export declare function contextGcWebOutputReplacement(toolName: ContextGcWebToolName, originalContent: string, truncated: boolean): string;
59
- export declare function contextGcExecInputReplacement(arguments_: unknown, formatVersion?: 1 | 2 | 3): unknown;
59
+ export declare function contextGcExecInputReplacement(arguments_: unknown, formatVersion?: 1 | 2 | 3, toolName?: string): unknown;
60
60
  export declare function contextGcExecOutputReplacement(result: ContextGcExecResult, formatVersion?: 1 | 2 | 3): string;
61
61
  export declare function normalizeContextGcExecResult(modelContent: unknown, completedPayload: unknown): ContextGcExecResult | undefined;
62
62
  export declare function parseContextGcExecResult(value: unknown): ContextGcExecResult | undefined;
@@ -97,14 +97,16 @@ export function contextGcWebOutputReplacement(toolName, originalContent, truncat
97
97
  contextGc: 'Detailed web tool output was reclaimed after six successful rollouts. An incomplete preview is not the complete search result or page, and GC did not access the network.'
98
98
  });
99
99
  }
100
- export function contextGcExecInputReplacement(arguments_, formatVersion = 3) {
100
+ export function contextGcExecInputReplacement(arguments_, formatVersion = 3, toolName = 'exec') {
101
101
  if (!isRecord(arguments_))
102
102
  return `<context_gc kind="terminal_exec_input"${formatVersion === 3 ? ' formatVersion="3"' : ''} originalBytes="${Buffer.byteLength(stableJson(arguments_))}"${formatVersion === 3 ? '' : ` originalSha256="${sha256(stableJson(arguments_))}"`} />`;
103
103
  const action = typeof arguments_.action === 'string'
104
104
  ? arguments_.action
105
105
  : typeof arguments_.command === 'string'
106
106
  ? 'start'
107
- : 'unknown';
107
+ : toolName === 'exec_wait'
108
+ ? 'poll'
109
+ : 'unknown';
108
110
  const original = stableJson(arguments_);
109
111
  const retained = {
110
112
  action,
@@ -190,7 +192,7 @@ export function normalizeContextGcExecResult(modelContent, completedPayload) {
190
192
  if (typeof modelContent !== 'string' ||
191
193
  !isRecord(completedPayload) ||
192
194
  completedPayload.type !== 'tool.completed' ||
193
- completedPayload.toolName !== 'exec' ||
195
+ (completedPayload.toolName !== 'exec' && completedPayload.toolName !== 'exec_wait') ||
194
196
  completedPayload.truncated !== true)
195
197
  return undefined;
196
198
  const data = completedPayload.data;
@@ -4,7 +4,7 @@ export const agentWaitToolDefinition = {
4
4
  name: 'agent_wait',
5
5
  allowedCallers: ['direct', 'programmatic'],
6
6
  parallelSafety: 'serial',
7
- description: 'Read or wait a bounded time for a child agent result. waitFor defaults to "completion"; use "message" only when a fresh public commentary/final message would change the next decision. Omit agentId to wait for whichever live child returns first, matching Codex-style untargeted waiting and avoiding UUID transcription; provide the exact agentId returned by agent_start only when observing a specific child. If no child is live, untargeted waiting reads the most recently created child. For an exec processId, use exec with action:"poll" instead. Omitting waitMs waits up to 30 seconds by default; waitMs=0 returns the current snapshot immediately. Positive waits range up to 5 minutes, and positive values below 10 seconds are raised to 10 seconds. When blocked on a live child result, prefer one 300000 ms completion wait rather than repeated short waits. Every result includes waitOutcome as snapshot, message, completion, or timeout plus the real agentId and description. A message, timeout with unread progress, or first terminal completion result includes the newly observed latest public commentary/final message; snapshots and repeated observations omit previously observed message text. latestActivity can still identify a public message or bounded tool started/completed/failed status. A queued or running result is not completion. A message wait consumes only the latest unread public message in the current Session runtime; tool activity does not wake it, and parent cancellation preserves unread progress for a later Execution. Do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. First terminal results include bounded summary, changedFiles, verification, and read evidence with path/line ranges; use that evidence to avoid repeating the full exploration, while independently checking consequential edits and claims.',
7
+ description: 'Read or wait a bounded time for a child agent result. waitFor defaults to "completion"; use "message" only when a fresh public commentary/final message would change the next decision. Omit agentId to wait for whichever live child returns first, matching Codex-style untargeted waiting and avoiding UUID transcription; provide the exact agentId returned by agent_start only when observing a specific child. If no child is live, untargeted waiting reads the most recently created child. For an exec processId, use exec_wait instead (tools.exec_wait in Code Mode). Omitting waitMs waits up to 30 seconds by default; waitMs=0 returns the current snapshot immediately. Positive waits range up to 5 minutes, and positive values below 10 seconds are raised to 10 seconds. When blocked on a live child result, prefer one 300000 ms completion wait rather than repeated short waits. Every result includes waitOutcome as snapshot, message, completion, or timeout plus the real agentId and description. A message, timeout with unread progress, or first terminal completion result includes the newly observed latest public commentary/final message; snapshots and repeated observations omit previously observed message text. latestActivity can still identify a public message or bounded tool started/completed/failed status. A queued or running result is not completion. A message wait consumes only the latest unread public message in the current Session runtime; tool activity does not wake it, and parent cancellation preserves unread progress for a later Execution. Do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. First terminal results include bounded summary, changedFiles, verification, and read evidence with path/line ranges; use that evidence to avoid repeating the full exploration, while independently checking consequential edits and claims.',
8
8
  inputSchema: {
9
9
  type: 'object',
10
10
  properties: {
@@ -8,7 +8,7 @@ export const applyPatchToolDefinition = {
8
8
  name: 'apply_patch',
9
9
  allowedCallers: ['direct', 'programmatic'],
10
10
  parallelSafety: 'serial',
11
- description: `Preferred for precise, reviewable local edits. Do not batch all task files merely to reduce tool calls. Group only a few tightly coupled edits whose combined patch is quick to generate and review; split broad or independent work into successive bounded patches before composing them. Submit a raw *** Begin Patch / *** End Patch patch. Supports Add File, Update File, Delete File, and Move to. Add File may replace an existing regular file, and Move to may replace an existing regular destination; duplicate operations on the same resolved path remain invalid. Update hunks accept @@ or @@ <change context>; context-only hunks may also be used as ordered anchors before a later changing hunk. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. Syntax and resolved paths are validated before writing, then file units are committed in order. Each file unit is atomic, and a Move keeps its source and destination atomic. On MAR_AGENT_PATCH_PARTIAL, earlier files remain applied; the error lists applied, failed, and pending paths, so retry only failed and pending paths. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts when all units succeed. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. Oversized patches are rejected without advertising defensive limits as recommended patch sizes. A first-unit MAR_AGENT_PATCH_CONFLICT applies nothing; hunk match conflicts identify the affected path and hunk so you can reread only that file before retrying.`,
11
+ description: `Preferred for precise, reviewable local edits. Do not batch all task files merely to reduce tool calls. Group only a few tightly coupled edits whose combined patch is quick to generate and review; split broad or independent work into successive bounded patches before composing them. Submit a raw *** Begin Patch / *** End Patch patch. Supports Add File, Update File, Delete File, and Move to. Add File may replace an existing regular file, and Move to may replace an existing regular destination; duplicate operations on the same resolved path remain invalid. Update hunks accept @@ or @@ <change context>; context-only hunks may also be used as ordered anchors before a later changing hunk. Omit unchanged file sections: an Update File must change or move the file, and its last hunk must not be context-only. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. Syntax and resolved paths are validated before writing, then file units are committed in order. Each file unit is atomic, and a Move keeps its source and destination atomic. On MAR_AGENT_PATCH_PARTIAL, earlier files remain applied; the error lists applied, failed, and pending paths, so retry only failed and pending paths. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts when all units succeed. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. Oversized patches are rejected without advertising defensive limits as recommended patch sizes. A first-unit MAR_AGENT_PATCH_CONFLICT applies nothing; hunk match conflicts identify the affected path and hunk so you can reread only that file before retrying.`,
12
12
  inputMode: 'freeform',
13
13
  outputSchema: {
14
14
  type: 'object',
@@ -13,7 +13,7 @@ export function toolCallers(tool) {
13
13
  export function allowsToolCaller(tool, caller) {
14
14
  // Control tools cannot be widened by a Host override or historical definition.
15
15
  if (caller === 'programmatic' &&
16
- ['code_exec', 'code_exec_wait', 'code_mode', 'tool_search', 'question'].includes(tool.name))
16
+ ['code_exec', 'code_exec_wait', 'code_mode', 'tool_search', 'question', 'skill'].includes(tool.name))
17
17
  return false;
18
18
  return toolCallers(tool).includes(caller);
19
19
  }
@@ -1,9 +1,15 @@
1
+ import { MarAgentError } from '../../error.js';
1
2
  import type { ToolExecutionContext, ToolExecutionResult } from '../../sdk/types.js';
2
3
  export interface CellOutput {
3
- text(value: string): void;
4
+ text(value: string, originalBytes?: number): void;
4
5
  image(value: NonNullable<ToolExecutionResult['images']>[number]): void;
5
6
  yield(): void;
6
7
  }
8
+ /** Keeps a failed tool call observable without discarding earlier explicit output. */
9
+ export declare class CodeCellExecutionError extends MarAgentError {
10
+ readonly result: ToolExecutionResult;
11
+ constructor(error: unknown, result: ToolExecutionResult);
12
+ }
7
13
  type Status = 'running' | 'completed' | 'failed' | 'cancelled';
8
14
  /** Session-owned execution, separate from the cancellable model-tool wait. */
9
15
  export declare class CodeCells {
@@ -2,6 +2,19 @@ import { randomUUID } from 'node:crypto';
2
2
  import { MarAgentError, describeMarAgentError } from '../../error.js';
3
3
  import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
4
4
  import { truncateModelText } from '../../runtime/token-budget.js';
5
+ import { BoundedTextOutput, renderCapturedText } from './text-output.js';
6
+ /** Keeps a failed tool call observable without discarding earlier explicit output. */
7
+ export class CodeCellExecutionError extends MarAgentError {
8
+ result;
9
+ constructor(error, result) {
10
+ const diagnostic = describeMarAgentError(error, 'MAR_AGENT_CODE_FAILED');
11
+ super(diagnostic.code, diagnostic.message.replace(`${diagnostic.code}: `, ''), {
12
+ cause: error,
13
+ ...(error instanceof MarAgentError ? { retryable: error.retryable } : {})
14
+ });
15
+ this.result = result;
16
+ }
17
+ }
5
18
  function options(value, wait) {
6
19
  if (!value || typeof value !== 'object' || Array.isArray(value))
7
20
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Code Exec options must be an object.');
@@ -11,11 +24,11 @@ function options(value, wait) {
11
24
  : ['yieldMs', 'maxOutputTokens'];
12
25
  if (Object.keys(input).some((key) => !keys.includes(key)))
13
26
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Unknown Code Exec option.');
14
- const yieldMs = input.yieldMs ?? limits.codeDefaultYieldMs;
27
+ const yieldMs = input.yieldMs ?? (wait ? limits.codeDefaultWaitYieldMs : limits.codeDefaultYieldMs);
15
28
  const maxOutputTokens = input.maxOutputTokens ?? limits.codeMaxOutputTokens;
16
29
  if (!Number.isSafeInteger(yieldMs) ||
17
30
  Number(yieldMs) < 0 ||
18
- Number(yieldMs) > limits.codeMaxYieldMs ||
31
+ Number(yieldMs) > (wait ? limits.codeMaxWaitYieldMs : limits.codeMaxYieldMs) ||
19
32
  !Number.isSafeInteger(maxOutputTokens) ||
20
33
  Number(maxOutputTokens) < 1 ||
21
34
  Number(maxOutputTokens) > limits.codeMaxOutputTokens ||
@@ -73,9 +86,10 @@ export class CodeCells {
73
86
  executionId: context.executionId,
74
87
  controller,
75
88
  status: 'running',
76
- text: [],
89
+ text: new BoundedTextOutput(limits.codeMaxOutputBytes),
77
90
  images: [],
78
91
  yielded: false,
92
+ truncated: false,
79
93
  listeners: new Set(),
80
94
  done: Promise.resolve()
81
95
  };
@@ -90,7 +104,7 @@ export class CodeCells {
90
104
  .then(async () => {
91
105
  releaseSession = await context.codeMode?.retainSession?.();
92
106
  return run(source, { ...context, signal: controller.signal }, {
93
- text: (text) => cell.text.push(text),
107
+ text: (text, originalBytes) => cell.text.append(text, originalBytes),
94
108
  image: (image) => cell.images.push(image),
95
109
  yield: () => {
96
110
  cell.yielded = true;
@@ -187,7 +201,12 @@ export class CodeCells {
187
201
  if (cell.terminal) {
188
202
  const snapshot = cell.terminal.data;
189
203
  const bounded = truncateModelText(snapshot.output, input.maxOutputTokens);
190
- const data = { ...snapshot, output: bounded.content, cached: true };
204
+ const data = {
205
+ ...snapshot,
206
+ output: bounded.content,
207
+ truncated: cell.terminal.truncated || bounded.truncated,
208
+ cached: true
209
+ };
191
210
  return {
192
211
  ...cell.terminal,
193
212
  data,
@@ -195,10 +214,12 @@ export class CodeCells {
195
214
  truncated: cell.terminal.truncated || bounded.truncated
196
215
  };
197
216
  }
198
- const text = cell.text.splice(0).join('\n');
217
+ const captured = cell.text.drain();
199
218
  const images = cell.images.splice(0);
219
+ const text = renderCapturedText(captured, images.length);
200
220
  cell.yielded = false;
201
221
  const bounded = truncateModelText(text, input.maxOutputTokens);
222
+ cell.truncated ||= captured.droppedBytes > 0 || bounded.truncated;
202
223
  const error = cell.error === undefined
203
224
  ? undefined
204
225
  : describeMarAgentError(cell.error, 'MAR_AGENT_CODE_FAILED');
@@ -207,13 +228,16 @@ export class CodeCells {
207
228
  status: cell.status,
208
229
  output: bounded.content,
209
230
  ...cell.result?.data,
231
+ outputBytes: captured.originalBytes,
232
+ outputDroppedBytes: captured.droppedBytes,
233
+ truncated: cell.truncated,
210
234
  ...(error ? { error: { code: error.code, message: error.message } } : {}),
211
235
  cached: false
212
236
  };
213
237
  const result = {
214
238
  content: JSON.stringify(data),
215
239
  data,
216
- truncated: bounded.truncated,
240
+ truncated: cell.truncated,
217
241
  ...(images.length ? { images } : {})
218
242
  };
219
243
  if (cell.status !== 'running')
@@ -221,7 +245,7 @@ export class CodeCells {
221
245
  // Keep synchronous completion compatible; retained waits always include explicit lifecycle metadata.
222
246
  if (initial && cell.status !== 'running') {
223
247
  if (cell.error !== undefined)
224
- throw cell.error;
248
+ throw new CodeCellExecutionError(cell.error, result);
225
249
  return { ...result, content: bounded.content || 'Code Exec completed without emitted text.' };
226
250
  }
227
251
  return result;