@myagentroam/agent 0.9.107 → 0.9.108

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.
@@ -30,6 +30,8 @@ export interface ExecResult {
30
30
  stderrBytes: number;
31
31
  stdoutDroppedBytes: number;
32
32
  stderrDroppedBytes: number;
33
+ /** A terminal snapshot replay rather than new process output. */
34
+ cached?: boolean;
33
35
  }
34
36
  export declare const execToolDefinition: ClientToolDefinition;
35
37
  export declare class ExecTool {
@@ -6,7 +6,7 @@ import { TOOL_EXECUTION_LIMITS } from './execution-limits.js';
6
6
  export const execToolDefinition = {
7
7
  name: 'exec',
8
8
  parallelSafety: 'safe',
9
- description: 'Unrestricted general system execution. Providing command starts any platform-native command, script, text/file tool, builder, generator, network client, or program in workspace/default cwd; action may be omitted for start. poll waits up to yieldMs for new output or termination and returns only output produced since the prior result; write sends stdin; cancel terminates the process tree. Returns status, processId, incremental stdout/stderr, cumulative stdoutBytes/stderrBytes, per-result dropped bytes, exitCode, and truncated; a completed status does not imply exitCode 0. Always inspect stderr/exitCode and consume terminal output or cancel.',
9
+ description: 'Unrestricted general system execution. Providing command starts any platform-native command, script, text/file tool, builder, generator, network client, or program in workspace/default cwd; action may be omitted for start. poll waits up to yieldMs for new output or termination and returns incremental output while running; after termination, poll returns a bounded cached result with earlier output for up to five minutes while the Session remains hosted. write sends stdin to running processes; cancel terminates a running process or removes a completed result. Returns status, processId, stdout/stderr, cumulative stdoutBytes/stderrBytes, per-result dropped bytes, exitCode, and truncated; a completed status does not imply exitCode 0. Always inspect stderr/exitCode and consume terminal output or cancel.',
10
10
  outputSchema: {
11
11
  type: 'object',
12
12
  required: ['content', 'data'],
@@ -34,7 +34,8 @@ export const execToolDefinition = {
34
34
  stdoutBytes: { type: 'number' },
35
35
  stderrBytes: { type: 'number' },
36
36
  stdoutDroppedBytes: { type: 'number' },
37
- stderrDroppedBytes: { type: 'number' }
37
+ stderrDroppedBytes: { type: 'number' },
38
+ cached: { type: 'boolean' }
38
39
  }
39
40
  }
40
41
  }
@@ -77,6 +78,7 @@ export class ExecTool {
77
78
  shell;
78
79
  processExecutor;
79
80
  #processes = new Map();
81
+ #completed = new Map();
80
82
  #startingProcesses = new Set();
81
83
  constructor(paths, maxOutputBytes = TOOL_EXECUTION_LIMITS.execDefaultMaxOutputBytes, maxProcesses = TOOL_EXECUTION_LIMITS.maxProcesses, environment = process.env, shell, processExecutor = directLocalProcessExecutor) {
82
84
  this.paths = paths;
@@ -92,6 +94,16 @@ export class ExecTool {
92
94
  const input = parseExecInput(arguments_);
93
95
  if ('processId' in input) {
94
96
  const process = this.#processes.get(input.processId);
97
+ const completed = this.#completed.get(input.processId);
98
+ if (completed) {
99
+ if (owner !== undefined && completed.ownerSessionId !== owner.sessionId)
100
+ throw new MarAgentError('MAR_AGENT_PROCESS_NOT_FOUND', 'Managed process was not found.');
101
+ if (input.action === 'write')
102
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Completed process cannot accept input.');
103
+ if (input.action === 'cancel')
104
+ this.#removeCompleted(input.processId);
105
+ return { ...completed.result, cached: true };
106
+ }
95
107
  if (!process || (owner !== undefined && process.ownerSessionId !== owner.sessionId))
96
108
  throw new MarAgentError('MAR_AGENT_PROCESS_NOT_FOUND', 'Managed process was not found.');
97
109
  if (input.action === 'poll' && input.yieldMs)
@@ -101,8 +113,11 @@ export class ExecTool {
101
113
  if (input.action === 'cancel')
102
114
  await this.#terminate(process, true);
103
115
  const result = this.#result(input.processId, process);
104
- if (process.status !== 'running')
105
- this.#processes.delete(input.processId);
116
+ if (process.status !== 'running') {
117
+ this.#retainCompleted(input.processId, process);
118
+ if (input.action === 'cancel')
119
+ this.#removeCompleted(input.processId);
120
+ }
106
121
  return result;
107
122
  }
108
123
  if (this.#processes.size + this.#startingProcesses.size >= this.maxProcesses)
@@ -137,6 +152,8 @@ export class ExecTool {
137
152
  cwd: cwd.displayPath,
138
153
  stdout: createOutputBuffer(),
139
154
  stderr: createOutputBuffer(),
155
+ stdoutHistory: createOutputBuffer(),
156
+ stderrHistory: createOutputBuffer(),
140
157
  status: 'running',
141
158
  exited: new Promise((resolve) => (resolveExited = resolve)),
142
159
  settled: new Promise((resolve) => (resolveSettled = resolve)),
@@ -155,53 +172,65 @@ export class ExecTool {
155
172
  child.stdout.on('data', (chunk) => {
156
173
  if (managed.finalized)
157
174
  return;
158
- appendOutput(managed.stdout, Buffer.from(managed.stdout.decoder.write(chunk)), this.maxOutputBytes, chunk.length);
175
+ const decoded = Buffer.from(managed.stdout.decoder.write(chunk));
176
+ appendOutput(managed.stdout, decoded, this.maxOutputBytes, chunk.length);
177
+ appendOutput(managed.stdoutHistory, decoded, this.maxOutputBytes, chunk.length);
159
178
  notifyProcessChange(managed);
160
179
  });
161
180
  child.stderr.on('data', (chunk) => {
162
181
  if (managed.finalized)
163
182
  return;
164
- appendOutput(managed.stderr, Buffer.from(managed.stderr.decoder.write(chunk)), this.maxOutputBytes, chunk.length);
183
+ const decoded = Buffer.from(managed.stderr.decoder.write(chunk));
184
+ appendOutput(managed.stderr, decoded, this.maxOutputBytes, chunk.length);
185
+ appendOutput(managed.stderrHistory, decoded, this.maxOutputBytes, chunk.length);
165
186
  notifyProcessChange(managed);
166
187
  });
167
188
  child.once('exit', (code) => {
168
189
  managed.exitObserved = true;
169
190
  managed.exitCode = code;
170
191
  managed.resolveExited();
171
- void finalizeAfterOutputGrace(managed, this.maxOutputBytes);
192
+ void finalizeAfterOutputGrace(managed, this.maxOutputBytes).then(() => this.#retainCompleted(id, managed));
172
193
  });
173
194
  child.once('close', (code) => {
174
195
  managed.exitObserved = true;
175
196
  managed.resolveExited();
176
197
  finalizeManagedProcess(managed, this.maxOutputBytes, code);
198
+ this.#retainCompleted(id, managed);
177
199
  });
178
200
  child.once('error', (error) => {
179
201
  if (managed.finalized)
180
202
  return;
181
- appendOutput(managed.stderr, Buffer.from(error.message), this.maxOutputBytes);
203
+ const message = Buffer.from(error.message);
204
+ appendOutput(managed.stderr, message, this.maxOutputBytes);
205
+ appendOutput(managed.stderrHistory, message, this.maxOutputBytes);
182
206
  notifyProcessChange(managed);
183
207
  });
184
208
  managed.timer = setTimeout(() => {
185
- void this.#terminate(managed, true);
209
+ void this.#terminate(managed, true).then(() => this.#retainCompleted(id, managed));
186
210
  }, Math.min(input.timeoutMs ?? TOOL_EXECUTION_LIMITS.execDefaultTimeoutMs, TOOL_EXECUTION_LIMITS.execMaxTimeoutMs));
187
211
  managed.timer.unref();
188
212
  await waitForExitOrDelay(managed.settled, Math.min(input.yieldMs ?? TOOL_EXECUTION_LIMITS.execDefaultYieldMs, TOOL_EXECUTION_LIMITS.execMaxYieldMs), signal);
189
213
  const result = this.#result(id, managed);
190
214
  if (managed.status !== 'running')
191
- this.#processes.delete(id);
215
+ this.#retainCompleted(id, managed);
192
216
  return result;
193
217
  }
194
218
  listSessionResources(sessionId) {
195
- return [...this.#processes.entries()].flatMap(([processId, process]) => process.ownerSessionId === sessionId
196
- ? [
197
- {
198
- processId,
199
- status: process.status,
200
- command: process.command,
201
- cwd: process.cwd
202
- }
203
- ]
204
- : []);
219
+ return [
220
+ ...[...this.#processes.entries()].flatMap(([processId, process]) => process.ownerSessionId === sessionId
221
+ ? [{ processId, status: process.status, command: process.command, cwd: process.cwd }]
222
+ : []),
223
+ ...[...this.#completed.entries()].flatMap(([processId, process]) => process.ownerSessionId === sessionId
224
+ ? [
225
+ {
226
+ processId,
227
+ status: process.result.status,
228
+ command: process.command,
229
+ cwd: process.cwd
230
+ }
231
+ ]
232
+ : [])
233
+ ];
205
234
  }
206
235
  async disposeSession(sessionId) {
207
236
  await Promise.all([...this.#startingProcesses]
@@ -211,6 +240,45 @@ export class ExecTool {
211
240
  await Promise.all(owned.map(([, process]) => process.status === 'running' ? this.#terminate(process, false) : process.settled));
212
241
  for (const [processId] of owned)
213
242
  this.#processes.delete(processId);
243
+ for (const [processId, process] of this.#completed)
244
+ if (process.ownerSessionId === sessionId)
245
+ this.#removeCompleted(processId);
246
+ }
247
+ #removeCompleted(id) {
248
+ const completed = this.#completed.get(id);
249
+ if (!completed)
250
+ return;
251
+ clearTimeout(completed.timer);
252
+ this.#completed.delete(id);
253
+ }
254
+ #retainCompleted(id, process) {
255
+ if (process.status === 'running' || !process.finalized || this.#processes.get(id) !== process)
256
+ return;
257
+ this.#processes.delete(id);
258
+ const stdout = consumeOutput(process.stdoutHistory);
259
+ const stderr = consumeOutput(process.stderrHistory);
260
+ const timer = setTimeout(() => this.#removeCompleted(id), TOOL_EXECUTION_LIMITS.execCompletedRetentionMs);
261
+ timer.unref();
262
+ this.#completed.set(id, {
263
+ ...(process.ownerSessionId === undefined ? {} : { ownerSessionId: process.ownerSessionId }),
264
+ command: process.command,
265
+ cwd: process.cwd,
266
+ timer,
267
+ result: {
268
+ status: process.status,
269
+ processId: id,
270
+ stdout: stdout.value,
271
+ stderr: stderr.value,
272
+ ...(process.exitCode === undefined ? {} : { exitCode: process.exitCode }),
273
+ truncated: stdout.droppedBytes > 0 || stderr.droppedBytes > 0,
274
+ stdoutBytes: process.stdout.totalBytes,
275
+ stderrBytes: process.stderr.totalBytes,
276
+ stdoutDroppedBytes: stdout.droppedBytes,
277
+ stderrDroppedBytes: stderr.droppedBytes
278
+ }
279
+ });
280
+ while (this.#completed.size > TOOL_EXECUTION_LIMITS.execMaxCompletedResults)
281
+ this.#removeCompleted(this.#completed.keys().next().value);
214
282
  }
215
283
  async #terminate(managed, waitForOutputClose) {
216
284
  if (managed.terminate)
@@ -252,7 +320,7 @@ export class ExecTool {
252
320
  const stderr = consumeOutput(process.stderr);
253
321
  return {
254
322
  status: process.status,
255
- ...(process.status === 'running' ? { processId: id } : {}),
323
+ processId: id,
256
324
  stdout: stdout.value,
257
325
  stderr: stderr.value,
258
326
  ...(process.exitCode === undefined ? {} : { exitCode: process.exitCode }),
@@ -267,6 +335,8 @@ export class ExecTool {
267
335
  await Promise.all([...this.#startingProcesses].map((process) => process.settled));
268
336
  await Promise.all([...this.#processes.values()].map((process) => process.status === 'running' ? this.#terminate(process, false) : process.settled));
269
337
  this.#processes.clear();
338
+ for (const id of this.#completed.keys())
339
+ this.#removeCompleted(id);
270
340
  }
271
341
  }
272
342
  function createOutputBuffer() {
@@ -283,8 +353,12 @@ function finalizeManagedProcess(managed, maximumOutputBytes, exitCode) {
283
353
  if (managed.finalized)
284
354
  return;
285
355
  managed.finalized = true;
286
- appendOutput(managed.stdout, Buffer.from(managed.stdout.decoder.end()), maximumOutputBytes);
287
- appendOutput(managed.stderr, Buffer.from(managed.stderr.decoder.end()), maximumOutputBytes);
356
+ const stdoutEnd = Buffer.from(managed.stdout.decoder.end());
357
+ const stderrEnd = Buffer.from(managed.stderr.decoder.end());
358
+ appendOutput(managed.stdout, stdoutEnd, maximumOutputBytes);
359
+ appendOutput(managed.stderr, stderrEnd, maximumOutputBytes);
360
+ appendOutput(managed.stdoutHistory, stdoutEnd, maximumOutputBytes);
361
+ appendOutput(managed.stderrHistory, stderrEnd, maximumOutputBytes);
288
362
  managed.status = managed.status === 'cancelled' ? 'cancelled' : 'completed';
289
363
  if (exitCode !== undefined)
290
364
  managed.exitCode = exitCode;
@@ -13,6 +13,8 @@ export declare const TOOL_EXECUTION_LIMITS: Readonly<{
13
13
  execTerminationGraceMs: 1000;
14
14
  execDefaultMaxOutputBytes: 1000000;
15
15
  maxProcesses: 8;
16
+ execMaxCompletedResults: 16;
17
+ execCompletedRetentionMs: 300000;
16
18
  codeMaxInputBytes: 1048576;
17
19
  codeMaxOutputBytes: 1048576;
18
20
  codeMaxFrameBytes: 33554432;
@@ -13,6 +13,8 @@ export const TOOL_EXECUTION_LIMITS = Object.freeze({
13
13
  execTerminationGraceMs: 1_000,
14
14
  execDefaultMaxOutputBytes: 1_000_000,
15
15
  maxProcesses: 8,
16
+ execMaxCompletedResults: 16,
17
+ execCompletedRetentionMs: 300_000,
16
18
  codeMaxInputBytes: 1_048_576,
17
19
  codeMaxOutputBytes: 1_048_576,
18
20
  codeMaxFrameBytes: 33_554_432,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.107",
3
+ "version": "0.9.108",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",