@myagentroam/agent 0.9.84 → 0.9.86

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.
@@ -80,6 +80,11 @@ const rawModelSchema = z
80
80
  });
81
81
  if (value.protocol !== 'OPENAI_RESPONSES' && value.responsesPreviousResponseId)
82
82
  context.addIssue({ code: 'custom', message: 'Responses continuation requires Responses.' });
83
+ if (value.responsesPreviousResponseId && value.responsesTransport?.transport !== 'WEBSOCKET')
84
+ context.addIssue({
85
+ code: 'custom',
86
+ message: 'Responses continuation requires WebSocket transport.'
87
+ });
83
88
  if (value.protocol !== 'OPENAI_RESPONSES' && value.responsesTransport !== undefined)
84
89
  context.addIssue({ code: 'custom', message: 'Responses transport requires Responses.' });
85
90
  if (value.protocol !== 'OPENAI_RESPONSES' && value.responsesEncoding !== undefined)
@@ -1,12 +1,10 @@
1
1
  import type { MarAgentModelConfiguration, MarAgentReasoningEffort, ModelCredentialAcquireReason } from './configuration.js';
2
2
  export interface ModelContinuationCheckpoint {
3
- readonly version: 1;
3
+ readonly version: 2;
4
4
  readonly protocol: 'OPENAI_RESPONSES';
5
5
  readonly configurationHash: string;
6
6
  readonly responseId: string;
7
- readonly requestPropertiesHash: string;
8
7
  readonly inputPrefixLength: number;
9
- readonly inputPrefixHash: string;
10
8
  }
11
9
  export interface ClientToolDefinition {
12
10
  name: string;
@@ -45,8 +45,7 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
45
45
  private readonly adapter;
46
46
  private readonly state;
47
47
  private readonly cacheLease;
48
- private readonly allowExternalContinuation;
49
- constructor(adapter: OpenAiResponsesAdapter, state: ResponsesSessionState, cacheLease: boolean, allowExternalContinuation: boolean);
48
+ constructor(adapter: OpenAiResponsesAdapter, state: ResponsesSessionState, cacheLease: boolean);
50
49
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
51
50
  close(): Promise<void>;
52
51
  resetContinuation(): void;
@@ -35,14 +35,21 @@ export class OpenAiResponsesAdapter {
35
35
  if (this.#cacheLeased ||
36
36
  this.#cachedState.lastRequest !== undefined ||
37
37
  this.#cachedState.lastResponse !== undefined ||
38
+ !responsesPersistedContinuationEnabled(this.#configuration) ||
39
+ checkpoint.version !== 2 ||
38
40
  checkpoint.protocol !== 'OPENAI_RESPONSES' ||
39
- checkpoint.configurationHash !== responsesConfigurationHash(this.#configuration))
41
+ typeof checkpoint.responseId !== 'string' ||
42
+ checkpoint.responseId.length === 0 ||
43
+ checkpoint.responseId.length > 1024 ||
44
+ !Number.isSafeInteger(checkpoint.inputPrefixLength) ||
45
+ checkpoint.inputPrefixLength < 0 ||
46
+ checkpoint.configurationHash !== responsesContinuationIdentityHash(this.#configuration))
40
47
  return false;
41
48
  this.#cachedState.restoredContinuation = checkpoint;
42
49
  return true;
43
50
  }
44
51
  async *start(request, signal) {
45
- const turn = new OpenAiResponsesTurnSession(this, {}, false, true);
52
+ const turn = new OpenAiResponsesTurnSession(this, {}, false);
46
53
  try {
47
54
  yield* turn.start(request, signal);
48
55
  }
@@ -59,7 +66,7 @@ export class OpenAiResponsesAdapter {
59
66
  this.#cachedState = {};
60
67
  this.#cacheLeased = true;
61
68
  }
62
- return new OpenAiResponsesTurnSession(this, state, cacheLease, false);
69
+ return new OpenAiResponsesTurnSession(this, state, cacheLease);
63
70
  }
64
71
  async close() {
65
72
  if (this.#closed)
@@ -546,17 +553,15 @@ class OpenAiResponsesTurnSession {
546
553
  adapter;
547
554
  state;
548
555
  cacheLease;
549
- allowExternalContinuation;
550
556
  #active = false;
551
557
  #closed = false;
552
558
  #turnState;
553
559
  #fullBody;
554
560
  #connectionDiagnostic;
555
- constructor(adapter, state, cacheLease, allowExternalContinuation) {
561
+ constructor(adapter, state, cacheLease) {
556
562
  this.adapter = adapter;
557
563
  this.state = state;
558
564
  this.cacheLease = cacheLease;
559
- this.allowExternalContinuation = allowExternalContinuation;
560
565
  }
561
566
  async *start(request, signal) {
562
567
  if (this.#closed)
@@ -573,7 +578,7 @@ class OpenAiResponsesTurnSession {
573
578
  this.#fullBody = prepared.fullBody;
574
579
  if (this.adapter.configuration.responsesEncoding === 'LITE' &&
575
580
  this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
576
- this.adapter.configuration.responsesPreviousResponseId &&
581
+ responsesContinuationEnabled(this.adapter.configuration) &&
577
582
  this.state.lastRequest === undefined &&
578
583
  request.allowTools !== false &&
579
584
  request.toolChoice !== 'none' &&
@@ -626,7 +631,7 @@ class OpenAiResponsesTurnSession {
626
631
  completed = event.reason === 'completed' || event.reason === 'tool_use';
627
632
  yield event;
628
633
  }
629
- if (this.adapter.configuration.responsesPreviousResponseId &&
634
+ if (responsesContinuationEnabled(this.adapter.configuration) &&
630
635
  completed &&
631
636
  responseId !== undefined &&
632
637
  (outputItems.length > 0 || !sawUnrepresentedOutput)) {
@@ -656,19 +661,19 @@ class OpenAiResponsesTurnSession {
656
661
  clearResponsesContinuation(this.state);
657
662
  }
658
663
  continuationCheckpoint() {
664
+ if (!responsesPersistedContinuationEnabled(this.adapter.configuration))
665
+ return undefined;
659
666
  const request = this.state.lastRequest;
660
667
  const response = this.state.lastResponse;
661
668
  if (request === undefined || response === undefined)
662
669
  return this.state.restoredContinuation;
663
670
  const baseline = [...request.input, ...response.outputItems];
664
671
  return {
665
- version: 1,
672
+ version: 2,
666
673
  protocol: 'OPENAI_RESPONSES',
667
- configurationHash: responsesConfigurationHash(this.adapter.configuration),
674
+ configurationHash: responsesContinuationIdentityHash(this.adapter.configuration),
668
675
  responseId: response.responseId,
669
- requestPropertiesHash: responsesRequestPropertiesHash(request),
670
- inputPrefixLength: baseline.length,
671
- inputPrefixHash: hashResponsesValue(baseline)
676
+ inputPrefixLength: baseline.length
672
677
  };
673
678
  }
674
679
  prepareRequest(request) {
@@ -677,7 +682,7 @@ class OpenAiResponsesTurnSession {
677
682
  this.state.socket.readyState !== WebSocket.OPEN)
678
683
  invalidateResponsesWebSocket(this.state, false);
679
684
  const fullBody = responsesRequestBody(this.adapter.configuration, request, request.messages, this.adapter.prefixIdentity);
680
- if (this.adapter.configuration.responsesPreviousResponseId) {
685
+ if (responsesContinuationEnabled(this.adapter.configuration)) {
681
686
  const incremental = incrementalResponsesInput(this.state, fullBody);
682
687
  if (incremental !== undefined) {
683
688
  const responseId = this.state.lastResponse.responseId;
@@ -692,20 +697,6 @@ class OpenAiResponsesTurnSession {
692
697
  continuation: true
693
698
  };
694
699
  }
695
- if (this.allowExternalContinuation &&
696
- request.continuation !== undefined &&
697
- this.adapter.configuration.responsesTransport?.transport !== 'WEBSOCKET') {
698
- const deltaBody = responsesRequestBody(this.adapter.configuration, request, request.continuation.deltaMessages);
699
- return {
700
- fullBody,
701
- body: {
702
- ...fullBody,
703
- input: deltaBody.input,
704
- previous_response_id: request.continuation.previousResponseId
705
- },
706
- continuation: true
707
- };
708
- }
709
700
  }
710
701
  return {
711
702
  fullBody,
@@ -898,17 +889,11 @@ function incrementalResponsesInput(state, current) {
898
889
  const completion = state.lastResponse;
899
890
  if (previous === undefined || completion === undefined) {
900
891
  const restored = state.restoredContinuation;
901
- if (restored === undefined ||
902
- restored.requestPropertiesHash !== responsesRequestPropertiesHash(current) ||
903
- current.input.length < restored.inputPrefixLength ||
904
- restored.inputPrefixHash !==
905
- hashResponsesValue(current.input.slice(0, restored.inputPrefixLength)))
892
+ if (restored === undefined || current.input.length < restored.inputPrefixLength)
906
893
  return undefined;
907
894
  state.lastResponse = { responseId: restored.responseId, outputItems: [] };
908
895
  return current.input.slice(restored.inputPrefixLength);
909
896
  }
910
- if (!responsesRequestPropertiesMatch(previous, current))
911
- return undefined;
912
897
  const baseline = [...previous.input, ...completion.outputItems];
913
898
  if (current.input.length < baseline.length)
914
899
  return undefined;
@@ -917,37 +902,27 @@ function incrementalResponsesInput(state, current) {
917
902
  return undefined;
918
903
  return current.input.slice(baseline.length);
919
904
  }
920
- function responsesRequestPropertiesMatch(previous, current) {
921
- return RESPONSES_REQUEST_PROPERTY_KEYS.every((key) => isDeepStrictEqual(previous[key], current[key]));
922
- }
923
905
  function clearResponsesContinuation(state) {
924
906
  delete state.lastRequest;
925
907
  delete state.lastResponse;
926
908
  delete state.restoredContinuation;
927
909
  }
928
- const RESPONSES_REQUEST_PROPERTY_KEYS = [
929
- 'model',
930
- 'instructions',
931
- 'tools',
932
- 'tool_choice',
933
- 'parallel_tool_calls',
934
- 'reasoning',
935
- 'store',
936
- 'stream',
937
- 'include',
938
- 'prompt_cache_key',
939
- 'text',
940
- 'client_metadata'
941
- ];
942
- function responsesRequestPropertiesHash(request) {
943
- return hashResponsesValue(Object.fromEntries(RESPONSES_REQUEST_PROPERTY_KEYS.map((key) => [key, request[key]])));
944
- }
945
910
  function hashResponsesValue(value) {
946
911
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
947
912
  }
948
- function responsesConfigurationHash(configuration) {
949
- const { apiKey: _apiKey, credentialProvider: _credentialProvider, imageGeneration: _imageGeneration, ...identity } = configuration;
950
- return hashResponsesValue(identity);
913
+ function responsesContinuationIdentityHash(configuration) {
914
+ return hashResponsesValue({
915
+ endpoint: new URL(responsesEndpoint(configuration.baseUrl)).toString(),
916
+ modelId: configuration.modelId,
917
+ responsesEncoding: configuration.responsesEncoding ?? 'STANDARD'
918
+ });
919
+ }
920
+ function responsesContinuationEnabled(configuration) {
921
+ return (configuration.responsesPreviousResponseId === true &&
922
+ configuration.responsesTransport?.transport === 'WEBSOCKET');
923
+ }
924
+ function responsesPersistedContinuationEnabled(configuration) {
925
+ return responsesContinuationEnabled(configuration) && configuration.responsesEncoding !== 'LITE';
951
926
  }
952
927
  function responsesCredentialFingerprint(credential) {
953
928
  return hashResponsesValue({
@@ -1489,11 +1464,17 @@ function isInvalidPreviousResponseFields(code, message) {
1489
1464
  normalizedCode === 'invalid_previous_response_id')
1490
1465
  return true;
1491
1466
  const normalizedMessage = message?.toLowerCase().replaceAll('`', '') ?? '';
1467
+ if ((normalizedCode === 'unsupported_parameter' ||
1468
+ normalizedCode === 'unsupported_previous_response_id') &&
1469
+ normalizedMessage.includes('previous_response_id'))
1470
+ return true;
1492
1471
  return (normalizedMessage.includes('previous_response_id') &&
1493
1472
  (normalizedMessage.includes('invalid') ||
1494
1473
  normalizedMessage.includes('not found') ||
1495
1474
  normalizedMessage.includes('does not exist') ||
1496
1475
  normalizedMessage.includes('expired') ||
1476
+ normalizedMessage.includes('not supported') ||
1477
+ normalizedMessage.includes('unsupported') ||
1497
1478
  normalizedMessage.includes('unavailable')));
1498
1479
  }
1499
1480
  const RESPONSE_ITEM_TYPES = new Set([
@@ -632,7 +632,9 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
632
632
  }
633
633
  for (const startGroup of execGroups) {
634
634
  const start = invocations.get(startGroup.callId);
635
- if (!start || start.input.action !== 'start')
635
+ if (!start ||
636
+ (start.input.action !== 'start' &&
637
+ !(start.input.action == null && typeof start.input.command === 'string')))
636
638
  continue;
637
639
  const lifecycle = [start];
638
640
  let processId;
package/dist/sdk/agent.js CHANGED
@@ -1600,14 +1600,12 @@ function isModelContinuationCheckpoint(value) {
1600
1600
  if (value === null || typeof value !== 'object' || Array.isArray(value))
1601
1601
  return false;
1602
1602
  const checkpoint = value;
1603
- return (checkpoint.version === 1 &&
1603
+ return (checkpoint.version === 2 &&
1604
1604
  checkpoint.protocol === 'OPENAI_RESPONSES' &&
1605
1605
  isBoundedString(checkpoint.configurationHash, 64) &&
1606
1606
  isBoundedString(checkpoint.responseId, 1_024) &&
1607
- isBoundedString(checkpoint.requestPropertiesHash, 64) &&
1608
1607
  Number.isSafeInteger(checkpoint.inputPrefixLength) &&
1609
- checkpoint.inputPrefixLength >= 0 &&
1610
- isBoundedString(checkpoint.inputPrefixHash, 64));
1608
+ checkpoint.inputPrefixLength >= 0);
1611
1609
  }
1612
1610
  function isBoundedString(value, maximumLength) {
1613
1611
  return typeof value === 'string' && value.length > 0 && value.length <= maximumLength;
@@ -74,7 +74,11 @@ export function contextGcWebOutputReplacement(toolName, originalContent, truncat
74
74
  export function contextGcExecInputReplacement(arguments_, formatVersion = 3) {
75
75
  if (!isRecord(arguments_))
76
76
  return `<context_gc kind="terminal_exec_input"${formatVersion === 3 ? ' formatVersion="3"' : ''} originalBytes="${Buffer.byteLength(stableJson(arguments_))}"${formatVersion === 3 ? '' : ` originalSha256="${sha256(stableJson(arguments_))}"`} />`;
77
- const action = typeof arguments_.action === 'string' ? arguments_.action : 'unknown';
77
+ const action = typeof arguments_.action === 'string'
78
+ ? arguments_.action
79
+ : typeof arguments_.command === 'string'
80
+ ? 'start'
81
+ : 'unknown';
78
82
  const original = stableJson(arguments_);
79
83
  const retained = {
80
84
  action,
@@ -33,7 +33,10 @@ function parseAgentMessageInput(value) {
33
33
  return invalid();
34
34
  const input = value;
35
35
  if (Object.keys(input).some((key) => !['agentId', 'message', 'delivery'].includes(key)) ||
36
- (input.delivery !== undefined && input.delivery !== 'append' && input.delivery !== 'replace') ||
36
+ (input.delivery !== undefined &&
37
+ input.delivery !== null &&
38
+ input.delivery !== 'append' &&
39
+ input.delivery !== 'replace') ||
37
40
  typeof input.agentId !== 'string' ||
38
41
  input.agentId.trim().length === 0 ||
39
42
  typeof input.message !== 'string' ||
@@ -43,7 +46,9 @@ function parseAgentMessageInput(value) {
43
46
  return {
44
47
  agentId: input.agentId.trim(),
45
48
  message: input.message,
46
- ...(input.delivery === undefined ? {} : { delivery: input.delivery })
49
+ ...(input.delivery === 'append' || input.delivery === 'replace'
50
+ ? { delivery: input.delivery }
51
+ : {})
47
52
  };
48
53
  }
49
54
  function invalid() {
@@ -62,6 +62,7 @@ function parseAgentStartInput(value) {
62
62
  input.modelId !== null &&
63
63
  (typeof input.modelId !== 'string' || input.modelId.trim().length === 0)) ||
64
64
  (input.reasoningEffort !== undefined &&
65
+ input.reasoningEffort !== null &&
65
66
  (typeof input.reasoningEffort !== 'string' ||
66
67
  !marAgentReasoningEffortSchema.safeParse(input.reasoningEffort.trim()).success)) ||
67
68
  (input.background !== undefined &&
@@ -46,11 +46,14 @@ function parseAgentWaitInput(value) {
46
46
  const input = value;
47
47
  if (Object.keys(input).some((key) => !['agentId', 'waitFor', 'waitMs'].includes(key)) ||
48
48
  (input.agentId !== undefined &&
49
+ input.agentId !== null &&
49
50
  (typeof input.agentId !== 'string' || input.agentId.trim().length === 0)) ||
50
51
  (input.waitFor !== undefined &&
52
+ input.waitFor !== null &&
51
53
  input.waitFor !== 'completion' &&
52
54
  input.waitFor !== 'message') ||
53
55
  (input.waitMs !== undefined &&
56
+ input.waitMs !== null &&
54
57
  (!Number.isInteger(input.waitMs) ||
55
58
  input.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
56
59
  input.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs)))
@@ -2,7 +2,7 @@ import type { ClientToolDefinition } from '../model/contracts.js';
2
2
  import { type LocalProcessExecutor } from '../host/local-process-executor.js';
3
3
  import { WorkspacePathResolver } from './shared/path-resolver.js';
4
4
  export type ExecInput = {
5
- action: 'start';
5
+ action?: 'start';
6
6
  command: string;
7
7
  cwd?: string;
8
8
  timeoutMs?: number;
@@ -5,8 +5,8 @@ import { MarAgentError } from '../error.js';
5
5
  import { TOOL_EXECUTION_LIMITS } from './execution-limits.js';
6
6
  export const execToolDefinition = {
7
7
  name: 'exec',
8
- parallelSafety: 'serial',
9
- description: 'Unrestricted general system execution. start runs any platform-native command, script, text/file tool, builder, generator, network client, or program in workspace/default cwd and may return running with processId; 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.',
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.',
10
10
  outputSchema: {
11
11
  type: 'object',
12
12
  required: ['content', 'data'],
@@ -42,8 +42,14 @@ export const execToolDefinition = {
42
42
  inputSchema: {
43
43
  type: 'object',
44
44
  properties: {
45
- action: { enum: ['start', 'poll', 'write', 'cancel'], description: 'Lifecycle operation.' },
46
- command: { type: 'string', description: 'Required only for start.' },
45
+ action: {
46
+ enum: ['start', 'poll', 'write', 'cancel'],
47
+ description: 'Lifecycle operation. Omit when providing command to start a process.'
48
+ },
49
+ command: {
50
+ type: 'string',
51
+ description: 'Required for start; implies start when action is omitted.'
52
+ },
47
53
  cwd: {
48
54
  type: 'string',
49
55
  description: 'Optional relative/absolute start directory; defaults to workspace.'
@@ -59,7 +65,7 @@ export const execToolDefinition = {
59
65
  processId: { type: 'string', description: 'Required for poll, write and cancel.' },
60
66
  data: { type: 'string', description: 'stdin data required for write.' }
61
67
  },
62
- required: ['action'],
68
+ required: [],
63
69
  additionalProperties: false
64
70
  }
65
71
  };
@@ -71,6 +77,7 @@ export class ExecTool {
71
77
  shell;
72
78
  processExecutor;
73
79
  #processes = new Map();
80
+ #startingProcesses = new Set();
74
81
  constructor(paths, maxOutputBytes = TOOL_EXECUTION_LIMITS.execDefaultMaxOutputBytes, maxProcesses = TOOL_EXECUTION_LIMITS.maxProcesses, environment = process.env, shell, processExecutor = directLocalProcessExecutor) {
75
82
  this.paths = paths;
76
83
  this.maxOutputBytes = maxOutputBytes;
@@ -83,7 +90,7 @@ export class ExecTool {
83
90
  if (signal?.aborted)
84
91
  throw executionAbortReason(signal);
85
92
  const input = parseExecInput(arguments_);
86
- if (input.action !== 'start') {
93
+ if ('processId' in input) {
87
94
  const process = this.#processes.get(input.processId);
88
95
  if (!process || (owner !== undefined && process.ownerSessionId !== owner.sessionId))
89
96
  throw new MarAgentError('MAR_AGENT_PROCESS_NOT_FOUND', 'Managed process was not found.');
@@ -98,38 +105,53 @@ export class ExecTool {
98
105
  this.#processes.delete(input.processId);
99
106
  return result;
100
107
  }
101
- if (this.#processes.size >= this.maxProcesses)
108
+ if (this.#processes.size + this.#startingProcesses.size >= this.maxProcesses)
102
109
  throw new MarAgentError('MAR_AGENT_PROCESS_LIMIT', 'The managed process limit was reached.');
103
- const id = randomUUID();
104
- const cwd = await this.paths.resolveWithDisplay(input.cwd ?? '.');
105
- const child = await this.processExecutor.spawn({
106
- command: input.command,
107
- cwd: cwd.physicalPath,
108
- shell: this.shell ?? true,
109
- environment: this.environment,
110
- detached: process.platform !== 'win32',
111
- ...(owner === undefined ? {} : { owner }),
112
- ...(signal === undefined ? {} : { signal })
113
- });
114
- let resolveExited;
115
- let resolveSettled;
116
- const managed = {
117
- child,
110
+ let resolveStarting;
111
+ const starting = {
118
112
  ...(owner === undefined ? {} : { ownerSessionId: owner.sessionId }),
119
- command: input.command,
120
- cwd: cwd.displayPath,
121
- stdout: createOutputBuffer(),
122
- stderr: createOutputBuffer(),
123
- status: 'running',
124
- exited: new Promise((resolve) => (resolveExited = resolve)),
125
- settled: new Promise((resolve) => (resolveSettled = resolve)),
126
- resolveExited,
127
- resolveSettled,
128
- exitObserved: child.exitCode !== null,
129
- finalized: false,
130
- waiters: new Set()
113
+ settled: new Promise((resolve) => (resolveStarting = resolve)),
114
+ resolveSettled: () => resolveStarting()
131
115
  };
132
- this.#processes.set(id, managed);
116
+ this.#startingProcesses.add(starting);
117
+ const id = randomUUID();
118
+ let child;
119
+ let managed;
120
+ try {
121
+ const cwd = await this.paths.resolveWithDisplay(input.cwd ?? '.');
122
+ child = await this.processExecutor.spawn({
123
+ command: input.command,
124
+ cwd: cwd.physicalPath,
125
+ shell: this.shell ?? true,
126
+ environment: this.environment,
127
+ detached: process.platform !== 'win32',
128
+ ...(owner === undefined ? {} : { owner }),
129
+ ...(signal === undefined ? {} : { signal })
130
+ });
131
+ let resolveExited;
132
+ let resolveSettled;
133
+ managed = {
134
+ child,
135
+ ...(owner === undefined ? {} : { ownerSessionId: owner.sessionId }),
136
+ command: input.command,
137
+ cwd: cwd.displayPath,
138
+ stdout: createOutputBuffer(),
139
+ stderr: createOutputBuffer(),
140
+ status: 'running',
141
+ exited: new Promise((resolve) => (resolveExited = resolve)),
142
+ settled: new Promise((resolve) => (resolveSettled = resolve)),
143
+ resolveExited,
144
+ resolveSettled,
145
+ exitObserved: child.exitCode !== null,
146
+ finalized: false,
147
+ waiters: new Set()
148
+ };
149
+ this.#processes.set(id, managed);
150
+ }
151
+ finally {
152
+ this.#startingProcesses.delete(starting);
153
+ starting.resolveSettled();
154
+ }
133
155
  child.stdout.on('data', (chunk) => {
134
156
  if (managed.finalized)
135
157
  return;
@@ -182,6 +204,9 @@ export class ExecTool {
182
204
  : []);
183
205
  }
184
206
  async disposeSession(sessionId) {
207
+ await Promise.all([...this.#startingProcesses]
208
+ .filter((process) => process.ownerSessionId === sessionId)
209
+ .map((process) => process.settled));
185
210
  const owned = [...this.#processes.entries()].filter(([, process]) => process.ownerSessionId === sessionId);
186
211
  await Promise.all(owned.map(([, process]) => process.status === 'running' ? this.#terminate(process, false) : process.settled));
187
212
  for (const [processId] of owned)
@@ -239,6 +264,7 @@ export class ExecTool {
239
264
  };
240
265
  }
241
266
  async dispose() {
267
+ await Promise.all([...this.#startingProcesses].map((process) => process.settled));
242
268
  await Promise.all([...this.#processes.values()].map((process) => process.status === 'running' ? this.#terminate(process, false) : process.settled));
243
269
  this.#processes.clear();
244
270
  }
@@ -332,7 +358,7 @@ function parseExecInput(value) {
332
358
  if (!value || typeof value !== 'object')
333
359
  return invalidExecInput();
334
360
  const input = value;
335
- const action = input.action;
361
+ const action = input.action == null && typeof input.command === 'string' ? 'start' : input.action;
336
362
  if (!['start', 'poll', 'write', 'cancel'].includes(String(action)))
337
363
  return invalidExecInput();
338
364
  const yieldMs = optionalBoundedInteger(input.yieldMs, 0, TOOL_EXECUTION_LIMITS.execMaxYieldMs);
@@ -343,7 +369,7 @@ function parseExecInput(value) {
343
369
  return invalidExecInput();
344
370
  const timeoutMs = optionalBoundedInteger(input.timeoutMs, 1, TOOL_EXECUTION_LIMITS.execMaxTimeoutMs);
345
371
  return {
346
- action,
372
+ ...(input.action === 'start' ? { action: 'start' } : {}),
347
373
  command: input.command,
348
374
  ...(typeof input.cwd === 'string' ? { cwd: input.cwd } : {}),
349
375
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.84",
3
+ "version": "0.9.86",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -42,6 +42,7 @@
42
42
  "build": "node ../scripts/clean-build-output.mjs dist && tsc -p tsconfig.json",
43
43
  "typecheck": "tsc -p tsconfig.json --noEmit",
44
44
  "test:unit": "vitest run --config vitest.config.ts test/unit",
45
+ "analyze:session": "node test/tools/session-analyzer.mjs",
45
46
  "benchmark:compaction": "node test/e2e/compaction-benchmark.mjs",
46
47
  "test:performance": "vitest run --config vitest.performance.config.ts",
47
48
  "test:memory": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.memory.config.ts",