@myagentroam/agent 0.11.28 → 0.11.30

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,7 @@
1
1
  export declare class AsyncQueue<T> implements AsyncIterable<T> {
2
2
  #private;
3
+ /** Best-effort live values are delivered only to an actively waiting consumer. */
4
+ pushIfWaiting(value: T): void;
3
5
  push(value: T): void;
4
6
  close(): void;
5
7
  discardIfUnobserved(): boolean;
@@ -6,6 +6,11 @@ export class AsyncQueue {
6
6
  #closed = false;
7
7
  #observed = false;
8
8
  #discarded = false;
9
+ /** Best-effort live values are delivered only to an actively waiting consumer. */
10
+ pushIfWaiting(value) {
11
+ if (this.#waiterOffset < this.#waiters.length)
12
+ this.push(value);
13
+ }
9
14
  push(value) {
10
15
  if (this.#closed || this.#discarded)
11
16
  return;
@@ -12,6 +12,7 @@ export function toolEventSummary(text) {
12
12
  /** Keep streaming deltas live-only; exec output has one durable copy in model context records. */
13
13
  export function eventForPersistence(event) {
14
14
  if (event.type === 'message.delta' ||
15
+ event.type === 'tool.output' ||
15
16
  event.type === 'input.received' ||
16
17
  (event.type === 'context.compacting' && event.async !== true && event.stage === undefined) ||
17
18
  event.type === 'model.performance' ||
@@ -91,6 +91,12 @@ export type MarAgentEvent = MarAgentEventBase & ({
91
91
  input?: unknown;
92
92
  data?: import('../tools/public-metadata.js').PublicToolMetadata;
93
93
  truncated?: boolean;
94
+ } | {
95
+ type: 'tool.output';
96
+ toolName: 'exec' | 'exec_wait';
97
+ callId: string;
98
+ parentCallId?: string;
99
+ output: import('../tools/contracts.js').ExecOutputSnapshot;
94
100
  } | {
95
101
  type: 'tool.completed';
96
102
  resultExtensions?: readonly import('../tools/result-extensions.js').ToolResultExtension[];
@@ -319,7 +319,10 @@ export function createSessionOpener(services) {
319
319
  messageSources.set(message, stored);
320
320
  }
321
321
  }
322
- queue.push(full);
322
+ if (full.type === 'tool.output')
323
+ queue.pushIfWaiting(full);
324
+ else
325
+ queue.push(full);
323
326
  compactRequests.publish(full);
324
327
  if (internal.publishHostEvents !== false) {
325
328
  try {
@@ -346,7 +349,9 @@ export function createSessionOpener(services) {
346
349
  }
347
350
  }
348
351
  });
349
- emitTail = operation.catch(() => undefined);
352
+ // Best-effort previews follow prior durable events without delaying later results.
353
+ if (full.type !== 'tool.output')
354
+ emitTail = operation.catch(() => undefined);
350
355
  return operation;
351
356
  };
352
357
  const recordModelAttempt = (purpose) => (diagnostic) => {
@@ -2006,6 +2011,7 @@ export function createSessionOpener(services) {
2006
2011
  }
2007
2012
  catch (error) {
2008
2013
  // Preserve the first failure/cancellation; successful executions still report release errors.
2014
+ // eslint-disable-next-line no-unsafe-finally -- Successful results must not hide writer lease release failures.
2009
2015
  if (!failedExecution)
2010
2016
  throw error;
2011
2017
  }
@@ -14,7 +14,7 @@ export type { HostExecutionExtensions, HostSessionResourceSnapshot, HostSkill, M
14
14
  export type { AccessMode, ExecutionMode, ExecutionResult, MarAgentImageMetadata, RolloutBudgetOptions, RunInput, RunInputImage } from '../runtime/contracts.js';
15
15
  export type { MarAgentEvent, MarAgentEventBase } from '../runtime/events.js';
16
16
  export type { CompactInput, ExecutionHandle } from '../runtime/execution-handle.js';
17
- export type { CodeCellEventData, MarAgentToolArtifact, MarAgentToolImageArtifact, McpCatalogTool, McpToolCallContext, McpToolCatalog, McpToolCatalogContext, ToolCall, ToolExecutionContext, ToolExecutionResult } from '../tools/contracts.js';
17
+ export type { CodeCellEventData, ExecOutputSnapshot, MarAgentToolArtifact, MarAgentToolImageArtifact, McpCatalogTool, McpToolCallContext, McpToolCatalog, McpToolCatalogContext, ToolCall, ToolExecutionContext, ToolExecutionResult } from '../tools/contracts.js';
18
18
  export interface SteerInput {
19
19
  readonly expectedExecutionId: string;
20
20
  readonly prompt: string;
@@ -1,5 +1,5 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { permissionStopFromError } from '../permissions.js';
2
+ import { RuntimeResourceHandles } from '../runtime-resource-handles.js';
3
3
  import { MarAgentError, describeMarAgentError } from '../../error.js';
4
4
  import { truncateModelText } from '../../runtime/token-budget.js';
5
5
  import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
@@ -39,6 +39,7 @@ function options(value, wait) {
39
39
  /** Session-owned execution, separate from the cancellable model-tool wait. */
40
40
  export class CodeCells {
41
41
  #cells = new Map();
42
+ #handles = new RuntimeResourceHandles('cell', (id) => this.#cells.has(id));
42
43
  #closed = false;
43
44
  #disposing = new Set();
44
45
  async start(value, context, run) {
@@ -75,7 +76,7 @@ export class CodeCells {
75
76
  throw new MarAgentError('MAR_AGENT_PROCESS_LIMIT', `The Local Host shared running-cell limit (${limits.codeMaxConcurrentCells}) was reached.`);
76
77
  const controller = new AbortController();
77
78
  const cell = {
78
- cellId: randomUUID(),
79
+ cellId: this.#handles.create(),
79
80
  sessionId: context.sessionId,
80
81
  executionId: context.executionId,
81
82
  controller,
@@ -160,6 +161,7 @@ export class CodeCells {
160
161
  async wait(value, context) {
161
162
  const input = options(value, true);
162
163
  context.signal.throwIfAborted();
164
+ this.#handles.assertType(input.cellId);
163
165
  const cell = this.#cells.get(input.cellId);
164
166
  if (!cell || cell.sessionId !== context.sessionId)
165
167
  throw new MarAgentError('MAR_AGENT_CODE_CELL_NOT_FOUND', 'Code Exec cell is unavailable in this Session.');
@@ -4,7 +4,7 @@ import type { McpToolCallContext, ToolCall, ToolExecutionResult } from '../contr
4
4
  import type { ToolRuntimeContext } from '../runtime-context.js';
5
5
  import type { createSessionToolScheduler } from '../scheduler.js';
6
6
  type ToolEvent = Extract<MarAgentEvent, {
7
- type: 'tool.started' | 'tool.completed' | 'tool.failed';
7
+ type: 'tool.started' | 'tool.output' | 'tool.completed' | 'tool.failed';
8
8
  }>;
9
9
  type Payload<T = ToolEvent> = T extends ToolEvent ? Omit<T, 'eventId' | 'sequence' | 'timestamp' | 'sessionId' | 'executionId' | 'turnId'> : never;
10
10
  /** Capture only the cell's immutable dispatch identity, never the model loop or its AbortSignal. */
@@ -26,6 +26,7 @@ export function createCodeToolDispatch(input) {
26
26
  const expected = input.expectations.get(call.name);
27
27
  const result = await executeRegisteredTool(call.name, call.arguments, callId, {
28
28
  ...input.context,
29
+ emit: (event) => event.type === 'tool.output' ? input.emit(event) : input.context.emit(event),
29
30
  parentCallId: input.parentCallId,
30
31
  signal,
31
32
  ...(expected ? { expectedMcpTool: expected } : {})
@@ -45,7 +45,7 @@ Built-in namespaces:
45
45
  - catalog.tools() returns a read-only array of the tools callable in this cell, including name, description and inputSchema. It does not discover or authorize additional tools.
46
46
  - await cell.sleep(ms) performs a cancellable delay up to ${limits.codeSleepMaxMs} ms.
47
47
  - await cell.yield_control() returns accumulated output to the model immediately while this cell continues running.
48
- - Optionally start source with // @options: {"yieldMs": ${limits.codeDefaultYieldMs}}. yieldMs defaults to ${limits.codeDefaultYieldMs} ms and controls this call's wait window (0..${limits.codeMaxYieldMs}), not nested tool timeouts or wait windows. A running result includes cellId; use direct code_exec_wait for new output, completion or termination.
48
+ - Optionally start source with // @options: {"yieldMs": ${limits.codeDefaultYieldMs}}. yieldMs defaults to ${limits.codeDefaultYieldMs} ms and controls this call's wait window (0..${limits.codeMaxYieldMs}), not nested tool timeouts or wait windows. A running result includes cellId (c_ plus 12 lowercase hex characters); use direct code_exec_wait for new output, completion or termination.
49
49
  - waitOutcome separates snapshot, completion, timeout, and yield from the cell status. A foreground steer returns interrupted with reason=steer without stopping the cell or its internal tool waits.
50
50
  - 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.
51
51
  - tools, observe, state, catalog and cell are frozen bindings. Do not shadow their names.
@@ -84,7 +84,10 @@ export const codeExecWaitToolDefinition = {
84
84
  inputSchema: {
85
85
  type: 'object',
86
86
  properties: {
87
- cellId: { type: 'string' },
87
+ cellId: {
88
+ type: 'string',
89
+ description: 'Copy the complete cellId returned by code_exec (c_ followed by 12 lowercase hex characters). Never use a processId (p_); use exec_wait for processes.'
90
+ },
88
91
  yieldMs: {
89
92
  type: 'integer',
90
93
  minimum: 0,
@@ -30,6 +30,8 @@ export interface ToolExecutionContext {
30
30
  mode: ExecutionMode;
31
31
  signal: AbortSignal;
32
32
  waitInterrupt?: import('./wait.js').ToolWaitInterrupt | undefined;
33
+ /** Live-only process output; does not consume the eventual tool result. */
34
+ onOutput?: (output: ExecOutputSnapshot) => Promise<void>;
33
35
  sessionId: string;
34
36
  executionId: string;
35
37
  /** Current model identity for neutral Host-owned tool policies. */
@@ -90,6 +92,12 @@ export interface MarAgentToolImageArtifact {
90
92
  fileName: string;
91
93
  }
92
94
  export type MarAgentToolArtifact = MarAgentToolImageArtifact;
95
+ export interface ExecOutputSnapshot {
96
+ processId: string;
97
+ stdout: string;
98
+ stderr: string;
99
+ truncated: boolean;
100
+ }
93
101
  export interface ToolExecutionResult {
94
102
  calls?: readonly import('./code-mode/observations.js').CodeToolCallRecord[];
95
103
  content: string;
@@ -14,6 +14,8 @@ export declare const TOOL_EXECUTION_LIMITS: Readonly<{
14
14
  execMaxPollYieldMs: 300000;
15
15
  execTerminationGraceMs: 1000;
16
16
  execDefaultMaxOutputBytes: 1000000;
17
+ execProgressMaxOutputBytes: 8192;
18
+ execProgressIntervalMs: 100;
17
19
  maxProcesses: 64;
18
20
  execMaxCompletedResults: 16;
19
21
  execCompletedRetentionMs: 300000;
@@ -14,6 +14,8 @@ export const TOOL_EXECUTION_LIMITS = Object.freeze({
14
14
  execMaxPollYieldMs: 300_000,
15
15
  execTerminationGraceMs: 1_000,
16
16
  execDefaultMaxOutputBytes: 1_000_000,
17
+ execProgressMaxOutputBytes: 8_192,
18
+ execProgressIntervalMs: 100,
17
19
  maxProcesses: 64,
18
20
  execMaxCompletedResults: 16,
19
21
  execCompletedRetentionMs: 300_000,
@@ -1,6 +1,6 @@
1
1
  import { type LocalProcessExecutor } from '../../host/local-process-executor.js';
2
2
  import type { ClientToolDefinition } from '../../model/contracts.js';
3
- import type { ExecResult, ToolExecutionResult } from '../contracts.js';
3
+ import type { ExecOutputSnapshot, ExecResult, ToolExecutionResult } from '../contracts.js';
4
4
  import { type ToolWaitInterrupt } from '../wait.js';
5
5
  import { WorkspacePathResolver } from '../shared/path-resolver.js';
6
6
  import { type CoreProcessInput, type CoreProcessHandle } from '../../extensions/processes.js';
@@ -40,7 +40,7 @@ export declare class ExecTool {
40
40
  private readonly shell?;
41
41
  private readonly processExecutor;
42
42
  constructor(paths: WorkspacePathResolver, maxOutputBytes?: number, maxProcesses?: number, environment?: NodeJS.ProcessEnv, shell?: string | undefined, processExecutor?: LocalProcessExecutor);
43
- execute(arguments_: unknown, signal?: AbortSignal, owner?: NonNullable<import('../../host/local-process-executor.js').LocalProcessSpawnRequest['owner']>, waitInterrupt?: ToolWaitInterrupt): Promise<ExecResult>;
43
+ execute(arguments_: unknown, signal?: AbortSignal, owner?: NonNullable<import('../../host/local-process-executor.js').LocalProcessSpawnRequest['owner']>, waitInterrupt?: ToolWaitInterrupt, onOutput?: (output: ExecOutputSnapshot) => Promise<void>): Promise<ExecResult>;
44
44
  spawn(input: CoreProcessInput, call: CoreOperationContext): Promise<CoreProcessHandle>;
45
45
  listSessionResources(sessionId: string): {
46
46
  processId: string;
@@ -1,4 +1,4 @@
1
- import { randomUUID } from 'node:crypto';
1
+ import { RuntimeResourceHandles } from '../runtime-resource-handles.js';
2
2
  import { StringDecoder } from 'node:string_decoder';
3
3
  import { MarAgentError } from '../../error.js';
4
4
  import { directLocalProcessExecutor, signalProcessTree } from '../../host/local-process-executor.js';
@@ -30,7 +30,7 @@ export const execToolDefinition = {
30
30
  name: 'exec',
31
31
  allowedCallers: ['direct', 'programmatic'],
32
32
  parallelSafety: 'safe',
33
- 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, call exec_wait directly with processId, not exec. Truncation does not stop the process. Cancelling the initial wait does not terminate the process. waitOutcome distinguishes snapshot, completion, timeout, and yield. A foreground wait may return interrupted with reason=steer; the process keeps running.',
33
+ description: 'Start a platform-native command, script, or program in workspace/default cwd. Returns a stable processId (p_ plus 12 lowercase hex characters), 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, call exec_wait directly with processId, not exec. Truncation does not stop the process. Cancelling the initial wait does not terminate the process. waitOutcome distinguishes snapshot, completion, and timeout. A foreground wait may return interrupted with reason=steer; the process keeps running.',
34
34
  outputSchema: {
35
35
  type: 'object',
36
36
  required: ['content', 'data'],
@@ -104,7 +104,7 @@ export const execWaitToolDefinition = {
104
104
  name: 'exec_wait',
105
105
  allowedCallers: ['direct', 'programmatic'],
106
106
  parallelSafety: 'safe',
107
- 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. Poll results include waitOutcome; interrupted with reason=steer ends only a foreground wait, never the process.',
107
+ description: 'Wait for or control a managed process by processId. action defaults to poll: collect output until termination or the wait window ends, then return incremental stdout/stderr. New output does not end the wait. 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. Poll results include waitOutcome; interrupted with reason=steer ends only a foreground wait, never the process.',
108
108
  outputSchema: execToolDefinition.outputSchema,
109
109
  inputSchema: {
110
110
  type: 'object',
@@ -112,7 +112,7 @@ export const execWaitToolDefinition = {
112
112
  processId: {
113
113
  type: 'string',
114
114
  minLength: 1,
115
- description: 'Stable processId returned by exec.'
115
+ description: 'Copy the complete processId returned by exec (p_ followed by 12 lowercase hex characters). Never use a cellId (c_); use code_exec_wait for cells.'
116
116
  },
117
117
  action: { enum: ['poll', 'write', 'cancel'], default: 'poll' },
118
118
  yieldMs: {
@@ -154,6 +154,7 @@ export class ExecTool {
154
154
  processExecutor;
155
155
  #processes = new Map();
156
156
  #completed = new Map();
157
+ #handles = new RuntimeResourceHandles('process', (id) => this.#processes.has(id) || this.#completed.has(id));
157
158
  #startingProcesses = new Set();
158
159
  constructor(paths, maxOutputBytes = TOOL_EXECUTION_LIMITS.execDefaultMaxOutputBytes, maxProcesses = TOOL_EXECUTION_LIMITS.maxProcesses, environment = process.env, shell, processExecutor = directLocalProcessExecutor) {
159
160
  this.paths = paths;
@@ -163,11 +164,12 @@ export class ExecTool {
163
164
  this.shell = shell;
164
165
  this.processExecutor = processExecutor;
165
166
  }
166
- async execute(arguments_, signal, owner, waitInterrupt) {
167
+ async execute(arguments_, signal, owner, waitInterrupt, onOutput) {
167
168
  if (signal?.aborted)
168
169
  throw executionAbortReason(signal);
169
170
  const input = parseExecInput(arguments_);
170
171
  if ('processId' in input) {
172
+ this.#handles.assertType(input.processId);
171
173
  const process = this.#processes.get(input.processId);
172
174
  const completed = this.#completed.get(input.processId);
173
175
  if (completed) {
@@ -191,7 +193,7 @@ export class ExecTool {
191
193
  throw new MarAgentError('MAR_AGENT_PROCESS_NOT_FOUND', 'Managed process was not found.');
192
194
  let outcome;
193
195
  if (input.action === 'poll' && input.yieldMs)
194
- outcome = await waitForProcessChange(process, Math.min(Math.max(input.yieldMs, 0), TOOL_EXECUTION_LIMITS.execMaxPollYieldMs), signal, waitInterrupt);
196
+ outcome = await waitForProcessCompletion(input.processId, process, Math.min(Math.max(input.yieldMs, 0), TOOL_EXECUTION_LIMITS.execMaxPollYieldMs), signal, waitInterrupt, onOutput);
195
197
  if (input.action === 'write') {
196
198
  if (process.status !== 'running')
197
199
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Completed process cannot accept input.');
@@ -214,9 +216,7 @@ export class ExecTool {
214
216
  ? 'completion'
215
217
  : outcome === 'interrupted'
216
218
  ? 'interrupted'
217
- : outcome === 'ready'
218
- ? 'yield'
219
- : 'timeout')
219
+ : 'timeout')
220
220
  : {})
221
221
  };
222
222
  }
@@ -243,18 +243,7 @@ export class ExecTool {
243
243
  const yieldMs = Math.min(input.yieldMs ?? TOOL_EXECUTION_LIMITS.execDefaultYieldMs, TOOL_EXECUTION_LIMITS.execMaxYieldMs);
244
244
  const outcome = yieldMs === 0
245
245
  ? undefined
246
- : await waitForToolCondition({
247
- ready: () => managed.finalized,
248
- subscribe: (notify) => {
249
- managed.waiters.add(notify);
250
- return () => {
251
- managed.waiters.delete(notify);
252
- };
253
- },
254
- waitMs: yieldMs,
255
- signal,
256
- interrupt: waitInterrupt
257
- });
246
+ : await waitForProcessCompletion(id, managed, yieldMs, signal, waitInterrupt, onOutput);
258
247
  const result = this.#result(id, managed);
259
248
  if (managed.status !== 'running')
260
249
  this.#retainCompleted(id, managed);
@@ -294,7 +283,7 @@ export class ExecTool {
294
283
  details: { reasonCode: error?.code ?? 'UNKNOWN' }
295
284
  });
296
285
  }
297
- const id = randomUUID();
286
+ const id = this.#handles.create();
298
287
  let resolveExited;
299
288
  let resolveSettled;
300
289
  const managed = {
@@ -800,27 +789,88 @@ function notifyProcessChange(managed) {
800
789
  for (const resolve of managed.waiters)
801
790
  resolve();
802
791
  }
803
- async function waitForProcessChange(managed, waitMs, signal, interrupt) {
804
- let changed = false;
805
- return waitForToolCondition({
806
- ready: () => managed.status !== 'running' ||
807
- managed.stdout.bytes > 0 ||
808
- managed.stderr.bytes > 0 ||
809
- changed,
810
- subscribe: (notify) => {
811
- const listener = () => {
812
- changed = true;
813
- notify();
814
- };
815
- managed.waiters.add(listener);
816
- return () => {
817
- managed.waiters.delete(listener);
818
- };
819
- },
820
- waitMs,
821
- signal,
822
- interrupt
823
- });
792
+ async function waitForProcessCompletion(processId, managed, waitMs, signal, interrupt, onOutput) {
793
+ const stopOutput = onOutput && watchProcessOutput(processId, managed, onOutput);
794
+ try {
795
+ return await waitForToolCondition({
796
+ ready: () => managed.finalized,
797
+ subscribe: (notify) => {
798
+ managed.waiters.add(notify);
799
+ return () => {
800
+ managed.waiters.delete(notify);
801
+ };
802
+ },
803
+ waitMs,
804
+ signal,
805
+ interrupt
806
+ });
807
+ }
808
+ finally {
809
+ stopOutput?.();
810
+ }
811
+ }
812
+ /** Coalesce display snapshots without consuming output or queueing behind a slow observer. */
813
+ function watchProcessOutput(processId, managed, onOutput) {
814
+ let stopped = false;
815
+ let dirty = false;
816
+ let timer;
817
+ let inFlight;
818
+ let failed = false;
819
+ const publish = () => {
820
+ dirty = false;
821
+ const preview = (output) => {
822
+ const bounded = createOutputBuffer();
823
+ for (const chunk of output.chunks)
824
+ appendOutput(bounded, chunk, TOOL_EXECUTION_LIMITS.execProgressMaxOutputBytes);
825
+ const result = consumeOutput(bounded);
826
+ return { ...result, droppedBytes: result.droppedBytes + output.droppedBytes };
827
+ };
828
+ const stdout = preview(managed.stdout);
829
+ const stderr = preview(managed.stderr);
830
+ inFlight = Promise.resolve()
831
+ .then(() => {
832
+ if (stopped)
833
+ return;
834
+ return onOutput({
835
+ processId,
836
+ stdout: stdout.value,
837
+ stderr: stderr.value,
838
+ truncated: stdout.droppedBytes > 0 || stderr.droppedBytes > 0
839
+ });
840
+ })
841
+ .catch(() => {
842
+ // Display delivery must not change the process result or wait deadline.
843
+ failed = true;
844
+ })
845
+ .finally(() => {
846
+ inFlight = undefined;
847
+ if (dirty)
848
+ schedule();
849
+ });
850
+ };
851
+ const schedule = () => {
852
+ if (stopped || failed || timer || inFlight)
853
+ return;
854
+ timer = setTimeout(() => {
855
+ timer = undefined;
856
+ publish();
857
+ }, TOOL_EXECUTION_LIMITS.execProgressIntervalMs);
858
+ timer.unref();
859
+ };
860
+ const changed = () => {
861
+ if (managed.stdout.bytes === 0 && managed.stderr.bytes === 0)
862
+ return;
863
+ dirty = true;
864
+ schedule();
865
+ };
866
+ managed.waiters.add(changed);
867
+ changed();
868
+ return () => {
869
+ stopped = true;
870
+ managed.waiters.delete(changed);
871
+ if (timer)
872
+ clearTimeout(timer);
873
+ };
824
874
  }
825
875
  function executionAbortReason(signal) {
826
876
  return (signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
@@ -90,7 +90,7 @@ export class LocalToolRegistry {
90
90
  ? {}
91
91
  : { ownerInstanceId: context.operation.ownerInstanceId })
92
92
  })
93
- }, context.waitInterrupt);
93
+ }, context.waitInterrupt, context.onOutput);
94
94
  return formatExecResult(result);
95
95
  }
96
96
  ])
@@ -4,6 +4,12 @@ import type { AgentSkill } from '../runtime/context/instructions.js';
4
4
  import type { SubagentController, SubagentLatestMessage } from '../subagent/scheduler.js';
5
5
  import type { TodoStore } from './agent/todo.js';
6
6
  export type ToolRuntimeEvent = {
7
+ type: 'tool.output';
8
+ toolName: 'exec' | 'exec_wait';
9
+ callId: string;
10
+ parentCallId?: string;
11
+ output: import('./contracts.js').ExecOutputSnapshot;
12
+ } | {
7
13
  type: 'sleep.started';
8
14
  callId: string;
9
15
  durationMs: number;
@@ -0,0 +1,21 @@
1
+ declare const types: {
2
+ readonly cell: {
3
+ readonly prefix: "c_";
4
+ readonly field: "cellId";
5
+ readonly tool: "code_exec_wait";
6
+ };
7
+ readonly process: {
8
+ readonly prefix: "p_";
9
+ readonly field: "processId";
10
+ readonly tool: "exec_wait";
11
+ };
12
+ };
13
+ /** Typed runtime handles; resource owners retain registration and lifecycle authority. */
14
+ export declare class RuntimeResourceHandles {
15
+ private readonly kind;
16
+ private readonly isReserved;
17
+ constructor(kind: keyof typeof types, isReserved: (id: string) => boolean);
18
+ create(): string;
19
+ assertType(id: string): void;
20
+ }
21
+ export {};
@@ -0,0 +1,28 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { MarAgentError } from '../error.js';
3
+ const types = {
4
+ cell: { prefix: 'c_', field: 'cellId', tool: 'code_exec_wait' },
5
+ process: { prefix: 'p_', field: 'processId', tool: 'exec_wait' }
6
+ };
7
+ /** Typed runtime handles; resource owners retain registration and lifecycle authority. */
8
+ export class RuntimeResourceHandles {
9
+ kind;
10
+ isReserved;
11
+ constructor(kind, isReserved) {
12
+ this.kind = kind;
13
+ this.isReserved = isReserved;
14
+ }
15
+ create() {
16
+ let id;
17
+ do {
18
+ id = `${types[this.kind].prefix}${randomBytes(6).toString('hex')}`;
19
+ } while (this.isReserved(id));
20
+ return id;
21
+ }
22
+ assertType(id) {
23
+ const expected = types[this.kind];
24
+ const actual = Object.values(types).find((type) => id.startsWith(type.prefix));
25
+ if (actual && actual !== expected)
26
+ throw new MarAgentError('MAR_AGENT_RESOURCE_HANDLE_TYPE_MISMATCH', `Expected ${expected.field} (${expected.prefix}...). This looks like ${actual.field}; use ${actual.tool} for that resource.`, { details: { expected: expected.field, received: actual.field, tool: actual.tool } });
27
+ }
28
+ }
@@ -45,10 +45,22 @@ export async function executeCapturedTool(kind, call, context) {
45
45
  ...(context.expectedMcpTool ? { expectedTool: context.expectedMcpTool } : {})
46
46
  });
47
47
  }
48
+ const { name, callId } = call;
48
49
  return context.host.executeTool(call, {
49
50
  mode: context.mode,
50
51
  signal: context.signal,
51
52
  waitInterrupt: context.waitInterrupt,
53
+ ...((name === 'exec' || name === 'exec_wait') && callId !== undefined
54
+ ? {
55
+ onOutput: (output) => context.emit({
56
+ type: 'tool.output',
57
+ toolName: name,
58
+ callId,
59
+ ...(context.parentCallId === undefined ? {} : { parentCallId: context.parentCallId }),
60
+ output
61
+ })
62
+ }
63
+ : {}),
52
64
  sessionId: context.sessionId,
53
65
  executionId: context.executionId,
54
66
  ...(context.modelId === undefined ? {} : { modelId: context.modelId }),
package/dist/index.d.ts CHANGED
@@ -44,7 +44,7 @@ export { openMarAgentSessionCatalog, type MarAgentContextUsage, type MarAgentHis
44
44
  export type { SessionListInput, SessionSource } from './agent-core/session/types.js';
45
45
  export type { SessionStore, SessionResource, SessionStoreMaintenance, SessionStoreSession, SessionRecordInput, SessionStoreCompactInput, SessionRecord, SessionExecutionState, CompactHistoryMessage, CompactUserInput, SessionInputImage, SessionInputImageMetadata, SessionInputImageData, RestoredSessionInputImage, CodeModeStateEntry, TodoSnapshot, TodoItem, CodeToolCallRecord, CodeObservation, ModelMessage, ModelUsage, ModelInputMetadata, ModelTextBlock, ModelContextualBlock, EnvironmentContextState, ContextGcCheckpointDraft, ContextGcBase, ContextGcPlatform, ContextGcEconomicsPolicy, ContextGcMetrics, ContextGcReplacement, ContextGcTarget, ContextGcReadItemReplacement, ContextGcWebToolName, ContextGcSubagentToolName, AvailableToolResultResource } from './agent-core/session/store/contracts.js';
46
46
  export type { OpenMarAgentSessionCatalogInput } from './agent-core/session/catalog.js';
47
- export type { ApplyPatchFileResult, ApplyPatchResult, ApplyPatchPartialResult, ClientToolDefinition, MarAgentToolArtifact, MarAgentToolImageArtifact, McpCatalogTool, McpToolCallContext, McpToolCatalog, McpToolCatalogContext, ToolCall, ToolCaller, ToolExecutionContext, ToolExecutionResult } from './agent-core/tools/contracts.js';
47
+ export type { ApplyPatchFileResult, ApplyPatchResult, ApplyPatchPartialResult, ExecOutputSnapshot, ClientToolDefinition, MarAgentToolArtifact, MarAgentToolImageArtifact, McpCatalogTool, McpToolCallContext, McpToolCatalog, McpToolCatalogContext, ToolCall, ToolCaller, ToolExecutionContext, ToolExecutionResult } from './agent-core/tools/contracts.js';
48
48
  export { MCP_DEFINITION_LIMITS, assertMcpToolCatalog, assertMcpToolDefinition } from './agent-core/tools/mcp/catalog.js';
49
49
  export type { WorkspacePathMount } from './agent-core/tools/shared/path-resolver.js';
50
50
  export type { CodeModeEngine, CodeModeEngineFactory, CreateCodeModeEngine } from './agent-core/tools/code-mode/engine.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.28",
3
+ "version": "0.11.30",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",