@myagentroam/agent 0.9.85 → 0.9.87

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,12 +1,4 @@
1
1
  import type { MarAgentModelConfiguration, MarAgentReasoningEffort, ModelCredentialAcquireReason } from './configuration.js';
2
- export interface ModelContinuationCheckpoint {
3
- readonly version: 2;
4
- readonly protocol: 'OPENAI_RESPONSES';
5
- readonly configurationHash: string;
6
- readonly responseId: string;
7
- readonly inputPrefixLength: number;
8
- readonly inputPrefixHash: string;
9
- }
10
2
  export interface ClientToolDefinition {
11
3
  name: string;
12
4
  description: string;
@@ -76,10 +68,6 @@ export interface ModelRequest {
76
68
  mimeType: string;
77
69
  dataBase64: string;
78
70
  }[];
79
- continuation?: {
80
- previousResponseId: string;
81
- deltaMessages: readonly ModelMessage[];
82
- };
83
71
  onAttemptDiagnostic?: (diagnostic: ModelAttemptDiagnostic) => Promise<void> | void;
84
72
  }
85
73
  export interface ModelUsage {
@@ -159,13 +147,11 @@ export interface ModelAdapter {
159
147
  * The returned commit callback must be synchronous and non-throwing.
160
148
  */
161
149
  prepareReconfigure?(configuration: MarAgentModelConfiguration): (() => void) | undefined;
162
- restoreContinuation?(checkpoint: ModelContinuationCheckpoint): boolean;
163
150
  close?(): Promise<void>;
164
151
  }
165
152
  export interface ModelTurnSession {
166
153
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
167
154
  /** Drops incremental response state after a local context replacement. */
168
155
  resetContinuation?(): void;
169
- continuationCheckpoint?(): ModelContinuationCheckpoint | undefined;
170
156
  close(): Promise<void>;
171
157
  }
@@ -1,7 +1,7 @@
1
1
  import { MarAgentError } from '../error.js';
2
2
  import { type MarAgentModelConfiguration, type ModelCredentialAcquireReason } from './configuration.js';
3
3
  import WebSocket from 'ws';
4
- import type { ModelAdapter, ModelAttemptDiagnostic, ModelContinuationCheckpoint, ModelEvent, ModelRequest, ModelTurnSession } from './contracts.js';
4
+ import type { ModelAdapter, ModelAttemptDiagnostic, ModelEvent, ModelRequest, ModelTurnSession } from './contracts.js';
5
5
  export declare const OPENAI_RESPONSES_EARLY_EOF_REASON = "OPENAI_RESPONSES_EARLY_EOF";
6
6
  export declare const OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT_REASON = "OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT";
7
7
  type ResponsesRequestBody = Readonly<Record<string, unknown>> & {
@@ -19,7 +19,6 @@ interface ResponsesSessionState {
19
19
  readonly responseId: string;
20
20
  readonly outputItems: readonly Record<string, unknown>[];
21
21
  };
22
- restoredContinuation?: ModelContinuationCheckpoint;
23
22
  }
24
23
  interface PreparedResponsesRequest {
25
24
  readonly fullBody: ResponsesRequestBody;
@@ -32,7 +31,6 @@ export declare class OpenAiResponsesAdapter implements ModelAdapter {
32
31
  constructor(configuration: MarAgentModelConfiguration);
33
32
  get configuration(): MarAgentModelConfiguration;
34
33
  prepareReconfigure(configuration: MarAgentModelConfiguration): (() => void) | undefined;
35
- restoreContinuation(checkpoint: ModelContinuationCheckpoint): boolean;
36
34
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
37
35
  createTurnSession(): ModelTurnSession;
38
36
  close(): Promise<void>;
@@ -49,7 +47,6 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
49
47
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
50
48
  close(): Promise<void>;
51
49
  resetContinuation(): void;
52
- continuationCheckpoint(): ModelContinuationCheckpoint | undefined;
53
50
  prepareRequest(request: ModelRequest): PreparedResponsesRequest;
54
51
  fullRequestBody(): ResponsesRequestBody;
55
52
  currentTurnState(): string | undefined;
@@ -31,18 +31,6 @@ export class OpenAiResponsesAdapter {
31
31
  this.#configuration = configuration;
32
32
  };
33
33
  }
34
- restoreContinuation(checkpoint) {
35
- if (this.#cacheLeased ||
36
- this.#cachedState.lastRequest !== undefined ||
37
- this.#cachedState.lastResponse !== undefined ||
38
- !responsesContinuationEnabled(this.#configuration) ||
39
- checkpoint.version !== 2 ||
40
- checkpoint.protocol !== 'OPENAI_RESPONSES' ||
41
- checkpoint.configurationHash !== responsesContinuationIdentityHash(this.#configuration))
42
- return false;
43
- this.#cachedState.restoredContinuation = checkpoint;
44
- return true;
45
- }
46
34
  async *start(request, signal) {
47
35
  const turn = new OpenAiResponsesTurnSession(this, {}, false);
48
36
  try {
@@ -74,8 +62,10 @@ export class OpenAiResponsesAdapter {
74
62
  invalidateResponsesWebSocket(state, true);
75
63
  return;
76
64
  }
77
- if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN)
65
+ if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN) {
78
66
  invalidateResponsesWebSocket(state, false);
67
+ clearResponsesContinuation(state);
68
+ }
79
69
  this.#cachedState = state;
80
70
  this.#cacheLeased = false;
81
71
  }
@@ -155,6 +145,24 @@ export class OpenAiResponsesAdapter {
155
145
  ...(options.retryDelayMs === undefined ? {} : { retryDelayMs: options.retryDelayMs }),
156
146
  ...(options.retryReason === undefined ? {} : { retryReason: options.retryReason })
157
147
  });
148
+ if (!signal.aborted &&
149
+ !sawSemanticEvent &&
150
+ useContinuation &&
151
+ !invalidContinuationFallbackUsed &&
152
+ isLocalContinuationReset(error)) {
153
+ invalidContinuationFallbackUsed = true;
154
+ useContinuation = false;
155
+ requestBody = prepared.fullBody;
156
+ turn.resetContinuation();
157
+ turn.invalidateWebSocket();
158
+ retryState.attempts = Math.max(0, retryState.attempts - 1);
159
+ credentialReason = 'RECONNECT';
160
+ await reportFailure(true, {
161
+ retryDelayMs: 0,
162
+ retryReason: 'INVALID_CONTINUATION'
163
+ });
164
+ continue;
165
+ }
158
166
  if (!signal.aborted &&
159
167
  attemptNumber < maxAttempts &&
160
168
  !sawSemanticEvent &&
@@ -164,6 +172,7 @@ export class OpenAiResponsesAdapter {
164
172
  invalidContinuationFallbackUsed = true;
165
173
  useContinuation = false;
166
174
  requestBody = prepared.fullBody;
175
+ turn.resetContinuation();
167
176
  turn.invalidateWebSocket();
168
177
  credentialReason = 'RECONNECT';
169
178
  await reportFailure(true, {
@@ -632,7 +641,6 @@ class OpenAiResponsesTurnSession {
632
641
  (outputItems.length > 0 || !sawUnrepresentedOutput)) {
633
642
  this.state.lastRequest = prepared.fullBody;
634
643
  this.state.lastResponse = { responseId, outputItems };
635
- delete this.state.restoredContinuation;
636
644
  }
637
645
  else
638
646
  clearResponsesContinuation(this.state);
@@ -655,28 +663,13 @@ class OpenAiResponsesTurnSession {
655
663
  resetContinuation() {
656
664
  clearResponsesContinuation(this.state);
657
665
  }
658
- continuationCheckpoint() {
659
- if (!responsesContinuationEnabled(this.adapter.configuration))
660
- return undefined;
661
- const request = this.state.lastRequest;
662
- const response = this.state.lastResponse;
663
- if (request === undefined || response === undefined)
664
- return this.state.restoredContinuation;
665
- const baseline = [...request.input, ...response.outputItems];
666
- return {
667
- version: 2,
668
- protocol: 'OPENAI_RESPONSES',
669
- configurationHash: responsesContinuationIdentityHash(this.adapter.configuration),
670
- responseId: response.responseId,
671
- inputPrefixLength: baseline.length,
672
- inputPrefixHash: hashResponsesValue(baseline)
673
- };
674
- }
675
666
  prepareRequest(request) {
676
667
  if (this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
677
668
  this.state.socket !== undefined &&
678
- this.state.socket.readyState !== WebSocket.OPEN)
669
+ this.state.socket.readyState !== WebSocket.OPEN) {
679
670
  invalidateResponsesWebSocket(this.state, false);
671
+ clearResponsesContinuation(this.state);
672
+ }
680
673
  const fullBody = responsesRequestBody(this.adapter.configuration, request, request.messages, this.adapter.prefixIdentity);
681
674
  if (responsesContinuationEnabled(this.adapter.configuration)) {
682
675
  const incremental = incrementalResponsesInput(this.state, fullBody);
@@ -739,8 +732,17 @@ class OpenAiResponsesTurnSession {
739
732
  const endpoint = webSocketEndpoint(input.configuration.baseUrl);
740
733
  const existingSocket = this.state.socket;
741
734
  if (existingSocket !== undefined &&
742
- (existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint))
735
+ (existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint)) {
743
736
  invalidateResponsesWebSocket(this.state, false);
737
+ if (input.continuation) {
738
+ clearResponsesContinuation(this.state);
739
+ throw localContinuationReset();
740
+ }
741
+ }
742
+ if (input.continuation && this.state.socket === undefined) {
743
+ clearResponsesContinuation(this.state);
744
+ throw localContinuationReset();
745
+ }
744
746
  if (this.state.socket === undefined) {
745
747
  const handshakeCredential = await acquireResponsesCredential(input.configuration, input.reason);
746
748
  input.credentialAcquired(handshakeCredential.revision);
@@ -751,6 +753,10 @@ class OpenAiResponsesTurnSession {
751
753
  if (requestCredential.revision !== this.state.credentialRevision ||
752
754
  responsesCredentialFingerprint(requestCredential) !== this.state.credentialFingerprint) {
753
755
  invalidateResponsesWebSocket(this.state, false);
756
+ if (input.continuation) {
757
+ clearResponsesContinuation(this.state);
758
+ throw localContinuationReset();
759
+ }
754
760
  await this.connectWebSocket(endpoint, requestCredential, input.promptCacheKey, input.signal);
755
761
  }
756
762
  if (input.signal.aborted) {
@@ -810,6 +816,12 @@ class OpenAiResponsesTurnSession {
810
816
  this.state.proxyType = proxyType;
811
817
  }
812
818
  }
819
+ function localContinuationReset() {
820
+ return new MarAgentError('MAR_AGENT_RESPONSES_CONTINUATION_RESET', 'The Responses connection changed before the incremental request was sent.');
821
+ }
822
+ function isLocalContinuationReset(error) {
823
+ return error instanceof MarAgentError && error.code === 'MAR_AGENT_RESPONSES_CONTINUATION_RESET';
824
+ }
813
825
  function responsesRequestBody(configuration, request, messages, prefixIdentity = '') {
814
826
  const tools = request.allowTools === false
815
827
  ? []
@@ -883,16 +895,8 @@ function uuidV5(namespace, value) {
883
895
  function incrementalResponsesInput(state, current) {
884
896
  const previous = state.lastRequest;
885
897
  const completion = state.lastResponse;
886
- if (previous === undefined || completion === undefined) {
887
- const restored = state.restoredContinuation;
888
- if (restored === undefined ||
889
- current.input.length < restored.inputPrefixLength ||
890
- restored.inputPrefixHash !==
891
- hashResponsesValue(current.input.slice(0, restored.inputPrefixLength)))
892
- return undefined;
893
- state.lastResponse = { responseId: restored.responseId, outputItems: [] };
894
- return current.input.slice(restored.inputPrefixLength);
895
- }
898
+ if (previous === undefined || completion === undefined)
899
+ return undefined;
896
900
  const baseline = [...previous.input, ...completion.outputItems];
897
901
  if (current.input.length < baseline.length)
898
902
  return undefined;
@@ -904,18 +908,10 @@ function incrementalResponsesInput(state, current) {
904
908
  function clearResponsesContinuation(state) {
905
909
  delete state.lastRequest;
906
910
  delete state.lastResponse;
907
- delete state.restoredContinuation;
908
911
  }
909
912
  function hashResponsesValue(value) {
910
913
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
911
914
  }
912
- function responsesContinuationIdentityHash(configuration) {
913
- return hashResponsesValue({
914
- endpoint: new URL(responsesEndpoint(configuration.baseUrl)).toString(),
915
- modelId: configuration.modelId,
916
- responsesEncoding: configuration.responsesEncoding ?? 'STANDARD'
917
- });
918
- }
919
915
  function responsesContinuationEnabled(configuration) {
920
916
  return (configuration.responsesPreviousResponseId === true &&
921
917
  configuration.responsesTransport?.transport === 'WEBSOCKET');
@@ -1460,11 +1456,17 @@ function isInvalidPreviousResponseFields(code, message) {
1460
1456
  normalizedCode === 'invalid_previous_response_id')
1461
1457
  return true;
1462
1458
  const normalizedMessage = message?.toLowerCase().replaceAll('`', '') ?? '';
1459
+ if ((normalizedCode === 'unsupported_parameter' ||
1460
+ normalizedCode === 'unsupported_previous_response_id') &&
1461
+ normalizedMessage.includes('previous_response_id'))
1462
+ return true;
1463
1463
  return (normalizedMessage.includes('previous_response_id') &&
1464
1464
  (normalizedMessage.includes('invalid') ||
1465
1465
  normalizedMessage.includes('not found') ||
1466
1466
  normalizedMessage.includes('does not exist') ||
1467
1467
  normalizedMessage.includes('expired') ||
1468
+ normalizedMessage.includes('not supported') ||
1469
+ normalizedMessage.includes('unsupported') ||
1468
1470
  normalizedMessage.includes('unavailable')));
1469
1471
  }
1470
1472
  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
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { posix } from 'node:path';
3
+ import { isDeepStrictEqual } from 'node:util';
3
4
  import { describeMarAgentError, MarAgentError } from '../error.js';
4
5
  import { negotiateHost } from '../host/contracts.js';
5
6
  import { AnthropicMessagesAdapter, isAnthropicRecoverableStreamError } from '../model/anthropic-messages.js';
@@ -101,7 +102,6 @@ export async function createMarAgent(options) {
101
102
  sessionRuntime = {
102
103
  sourceType: sessionSource.type,
103
104
  adapterGeneration: modelGeneration,
104
- responsesChain: undefined,
105
105
  adapters: new Map()
106
106
  };
107
107
  sessionRuntimes.set(sessionId, sessionRuntime);
@@ -133,7 +133,6 @@ export async function createMarAgent(options) {
133
133
  return;
134
134
  const retainedByActiveSessions = activeAdaptersExcept(sessionId);
135
135
  await closeModelAdapters([...sessionRuntime.adapters.values()].filter((adapter) => !retainedByActiveSessions.has(adapter)));
136
- sessionRuntime.responsesChain = undefined;
137
136
  sessionRuntime.adapters.clear();
138
137
  sessionRuntime.adapterGeneration = generation;
139
138
  };
@@ -253,9 +252,17 @@ export async function createMarAgent(options) {
253
252
  const contextRecords = sessionState.contextRecords;
254
253
  const contextualWorldStateGeneration = latestContextualWorldStateGeneration(contextRecords);
255
254
  const restoredContextualWorldState = sessionRuntime.contextualWorldState;
255
+ const persistedContextualWorldState = parsePersistedContextualWorldState(await store.readContextualWorldState(sessionId));
256
256
  let contextualWorldState;
257
- if (restoredContextualWorldState === undefined ||
258
- restoredContextualWorldState.generation !== contextualWorldStateGeneration) {
257
+ if (restoredContextualWorldState !== undefined &&
258
+ restoredContextualWorldState.generation === contextualWorldStateGeneration)
259
+ contextualWorldState = restoredContextualWorldState;
260
+ else if (persistedContextualWorldState !== undefined &&
261
+ persistedContextualWorldState.generation === contextualWorldStateGeneration) {
262
+ contextualWorldState = persistedContextualWorldState;
263
+ sessionRuntime.contextualWorldState = contextualWorldState;
264
+ }
265
+ else {
259
266
  contextualWorldState = {
260
267
  generation: contextualWorldStateGeneration,
261
268
  initialized: false,
@@ -265,8 +272,6 @@ export async function createMarAgent(options) {
265
272
  };
266
273
  sessionRuntime.contextualWorldState = contextualWorldState;
267
274
  }
268
- else
269
- contextualWorldState = restoredContextualWorldState;
270
275
  const compactMayContainAgentInstructions = contextRecords.some((record) => compactSummaryFromCheckpoint(record) !== undefined);
271
276
  const compactMayContainRetainedResources = compactHistoryMayContainRetainedResources(contextRecords);
272
277
  const restored = await restoreMessages(store, sessionId, contextRecords, {
@@ -320,8 +325,6 @@ export async function createMarAgent(options) {
320
325
  }));
321
326
  const currentExecutionToolImages = [];
322
327
  let currentExecutionUserIndex;
323
- if (mode === 'compact')
324
- sessionRuntime.responsesChain = undefined;
325
328
  if (mode !== 'compact') {
326
329
  await recordEnvironment();
327
330
  const inputImageMetadata = input.images?.length
@@ -373,9 +376,6 @@ export async function createMarAgent(options) {
373
376
  });
374
377
  selectedAdapter = sessionAdapter(input.modelId ?? executionDefaultModelId, executionModels);
375
378
  const selectedModel = executionModels.get(input.modelId ?? executionDefaultModelId);
376
- const restoredContinuation = latestModelContinuationCheckpoint(contextRecords, selectedModel.id);
377
- if (restoredContinuation !== undefined)
378
- selectedAdapter.restoreContinuation?.(restoredContinuation);
379
379
  if (options.adapterFactory !== undefined &&
380
380
  selectedAdapter.createTurnSession === undefined) {
381
381
  executionScopedAdapters.add(selectedAdapter);
@@ -487,6 +487,7 @@ export async function createMarAgent(options) {
487
487
  };
488
488
  };
489
489
  let currentResourceSnapshot = retainedSessionResourcesSnapshot(await loadRetainedSessionResources());
490
+ const previousContextualWorldState = contextualWorldState;
490
491
  contextualWorldState = updateContextualWorldState(contextualWorldState, agentInstructionInsertIndex, {
491
492
  instructionSnapshot: currentInstructionSnapshot,
492
493
  instructionContent: renderAgentInstructions(instructionEntries, compactMayContainAgentInstructions ||
@@ -495,6 +496,8 @@ export async function createMarAgent(options) {
495
496
  previousMayContainResources: compactMayContainRetainedResources
496
497
  });
497
498
  sessionRuntime.contextualWorldState = contextualWorldState;
499
+ if (!isDeepStrictEqual(previousContextualWorldState, contextualWorldState))
500
+ await persistContextualWorldState(store, sessionId, contextualWorldState);
498
501
  const buildExecutionSystemPrompt = (availableTools = executionTools, promptMode = mode) => buildSystemPrompt({
499
502
  mode: promptMode,
500
503
  platform: description.platform,
@@ -659,14 +662,12 @@ export async function createMarAgent(options) {
659
662
  }
660
663
  catch (error) {
661
664
  if (await recoverModelStream(error, compactionRetryState, operation.signal)) {
662
- sessionRuntime.responsesChain = undefined;
663
665
  continue;
664
666
  }
665
667
  if (isProviderContextOverflow(error) && usingNativeMessages) {
666
668
  usingNativeMessages = false;
667
669
  compactMessages = fallbackMessages();
668
670
  compactionRetryState.attempts = 0;
669
- sessionRuntime.responsesChain = undefined;
670
671
  continue;
671
672
  }
672
673
  if (isProviderContextOverflow(error))
@@ -707,7 +708,7 @@ export async function createMarAgent(options) {
707
708
  previousMayContainResources: compactSummary.includes('# Retained Session resources')
708
709
  });
709
710
  sessionRuntime.contextualWorldState = contextualWorldState;
710
- sessionRuntime.responsesChain = undefined;
711
+ await persistContextualWorldState(store, sessionId, contextualWorldState);
711
712
  lastServerContextTokens = undefined;
712
713
  lastServerUsage = undefined;
713
714
  await emit({ type: 'context.compacted', compactionId });
@@ -731,7 +732,6 @@ export async function createMarAgent(options) {
731
732
  await appendMailboxMessages();
732
733
  let stop;
733
734
  let sawTool = false;
734
- let responseId;
735
735
  let latestUsage;
736
736
  const pendingTools = [];
737
737
  let incompleteToolCall = false;
@@ -741,27 +741,9 @@ export async function createMarAgent(options) {
741
741
  const systemPrompt = mode === 'compact'
742
742
  ? buildExecutionSystemPrompt(roundTools, 'run')
743
743
  : buildExecutionSystemPrompt(roundTools);
744
- const requestSignature = createHash('sha256')
745
- .update(stableJson({
746
- modelId: selectedModel.id,
747
- systemPrompt,
748
- contextualWorldState: contextualWorldState.entries,
749
- tools: roundTools,
750
- reasoningEffort
751
- }))
752
- .digest('hex');
753
744
  const requestContextMessages = pendingOutputContinuation === undefined
754
745
  ? messages
755
746
  : [...messages, pendingOutputContinuation];
756
- const continuation = selectedModel.responsesPreviousResponseId &&
757
- sessionRuntime.responsesChain?.modelId === selectedModel.id &&
758
- sessionRuntime.responsesChain.requestSignature === requestSignature &&
759
- sessionRuntime.responsesChain.messageCount <= requestContextMessages.length
760
- ? {
761
- previousResponseId: sessionRuntime.responsesChain.responseId,
762
- deltaMessages: requestContextMessages.slice(sessionRuntime.responsesChain.messageCount)
763
- }
764
- : undefined;
765
747
  const modelMessages = mode === 'compact'
766
748
  ? normalizeMessagesForModel(requestContextMessages, { supportsImages: false })
767
749
  : normalizeMessagesForModel(insertContextualWorldState(requestContextMessages, contextualWorldState.entries), {
@@ -808,16 +790,13 @@ export async function createMarAgent(options) {
808
790
  ? {}
809
791
  : { allowTools: false }),
810
792
  promptCacheKey: sessionId,
811
- ...(continuation ? { continuation } : {}),
812
793
  reasoningEffort,
813
794
  ...(modelInputImages?.length ? { images: modelInputImages } : {}),
814
795
  onAttemptDiagnostic: recordModelAttempt(mode === 'compact' ? 'COMPACTION' : 'EXECUTION')
815
796
  })) {
816
797
  if (event.type !== 'response.started' && event.type !== 'usage')
817
798
  sawModelSemanticEvent = true;
818
- if (event.type === 'response.started')
819
- responseId = event.responseId;
820
- else if (event.type === 'response.item') {
799
+ if (event.type === 'response.item') {
821
800
  // Compaction keeps only its successful checkpoint, never intermediate native output.
822
801
  if (mode === 'compact')
823
802
  continue;
@@ -955,8 +934,6 @@ export async function createMarAgent(options) {
955
934
  catch (error) {
956
935
  planOutputPrefixes.clear();
957
936
  if (await recoverModelStream(error, modelRetryState, manualCompaction?.signal ?? controller.signal)) {
958
- sessionRuntime.responsesChain = undefined;
959
- responseId = undefined;
960
937
  finalAnswer = '';
961
938
  if (mode === 'compact')
962
939
  messages.splice(roundMessageCount);
@@ -971,16 +948,13 @@ export async function createMarAgent(options) {
971
948
  !sawModelSemanticEvent &&
972
949
  isProviderContextOverflow(error) &&
973
950
  useManualCompactionFallback?.()) {
974
- sessionRuntime.responsesChain = undefined;
975
951
  modelRetryState.attempts = 0;
976
- responseId = undefined;
977
952
  finalAnswer = '';
978
953
  continue;
979
954
  }
980
955
  else if (mode !== 'compact' &&
981
956
  !sawModelSemanticEvent &&
982
957
  isProviderContextOverflow(error)) {
983
- sessionRuntime.responsesChain = undefined;
984
958
  if (!recoveringFromContextOverflow && (await compactContext())) {
985
959
  modelRetryState.attempts = 0;
986
960
  recoveringFromContextOverflow = true;
@@ -992,26 +966,6 @@ export async function createMarAgent(options) {
992
966
  throw error;
993
967
  }
994
968
  recoveringFromContextOverflow = false;
995
- if (selectedModel.responsesPreviousResponseId && responseId)
996
- sessionRuntime.responsesChain = {
997
- modelId: selectedModel.id,
998
- responseId,
999
- requestSignature,
1000
- messageCount: messages.length
1001
- };
1002
- else if (selectedModel.responsesPreviousResponseId)
1003
- sessionRuntime.responsesChain = undefined;
1004
- const continuationCheckpoint = selected.continuationCheckpoint?.();
1005
- if (mode !== 'compact' && continuationCheckpoint !== undefined)
1006
- await store.append(sessionId, {
1007
- type: 'responses.continuation',
1008
- payload: {
1009
- modelId: selectedModel.id,
1010
- checkpoint: continuationCheckpoint
1011
- },
1012
- executionId,
1013
- turnId
1014
- });
1015
969
  if (pendingTools.length === 0 && mailbox.length > 0) {
1016
970
  await appendMailboxMessages();
1017
971
  finalAnswer = '';
@@ -1186,7 +1140,6 @@ export async function createMarAgent(options) {
1186
1140
  }
1187
1141
  });
1188
1142
  if (committed) {
1189
- sessionRuntime.responsesChain = undefined;
1190
1143
  selected.resetContinuation?.();
1191
1144
  const refreshed = await restoreMessages(store, sessionId, committed.records, {
1192
1145
  includeImages: true,
@@ -1208,6 +1161,7 @@ export async function createMarAgent(options) {
1208
1161
  previousMayContainResources: compactHistoryMayContainRetainedResources(committed.records)
1209
1162
  });
1210
1163
  sessionRuntime.contextualWorldState = contextualWorldState;
1164
+ await persistContextualWorldState(store, sessionId, contextualWorldState);
1211
1165
  }
1212
1166
  }
1213
1167
  if (controller.signal.aborted) {
@@ -1402,9 +1356,6 @@ export async function createMarAgent(options) {
1402
1356
  for (const [modelId, adapter] of runtime.adapters)
1403
1357
  if (!retainedAdapters.has(adapter))
1404
1358
  runtime.adapters.delete(modelId);
1405
- if (runtime.responsesChain !== undefined &&
1406
- !retainedAdapters.has(runtime.adapters.get(runtime.responsesChain.modelId)))
1407
- runtime.responsesChain = undefined;
1408
1359
  }
1409
1360
  await closeModelAdapters(adapters);
1410
1361
  if (disposed)
@@ -1572,45 +1523,9 @@ function createModelTurnSession(adapter) {
1572
1523
  }
1573
1524
  },
1574
1525
  resetContinuation: () => turn.resetContinuation?.(),
1575
- continuationCheckpoint: () => turn.continuationCheckpoint?.(),
1576
1526
  close: () => turn.close()
1577
1527
  };
1578
1528
  }
1579
- function latestModelContinuationCheckpoint(records, modelId) {
1580
- let checkpoint;
1581
- for (const record of records) {
1582
- if (record.type === 'compact.completed' || record.type === 'context.gc.completed') {
1583
- checkpoint = undefined;
1584
- continue;
1585
- }
1586
- if (record.type !== 'responses.continuation')
1587
- continue;
1588
- const payload = record.payload;
1589
- if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
1590
- continue;
1591
- const candidateModelId = payload.modelId;
1592
- const candidate = payload.checkpoint;
1593
- if (candidateModelId !== modelId || !isModelContinuationCheckpoint(candidate))
1594
- continue;
1595
- checkpoint = candidate;
1596
- }
1597
- return checkpoint;
1598
- }
1599
- function isModelContinuationCheckpoint(value) {
1600
- if (value === null || typeof value !== 'object' || Array.isArray(value))
1601
- return false;
1602
- const checkpoint = value;
1603
- return (checkpoint.version === 2 &&
1604
- checkpoint.protocol === 'OPENAI_RESPONSES' &&
1605
- isBoundedString(checkpoint.configurationHash, 64) &&
1606
- isBoundedString(checkpoint.responseId, 1_024) &&
1607
- Number.isSafeInteger(checkpoint.inputPrefixLength) &&
1608
- checkpoint.inputPrefixLength >= 0 &&
1609
- isBoundedString(checkpoint.inputPrefixHash, 64));
1610
- }
1611
- function isBoundedString(value, maximumLength) {
1612
- return typeof value === 'string' && value.length > 0 && value.length <= maximumLength;
1613
- }
1614
1529
  function modelAttemptFailureDiagnostic(diagnostic, summary) {
1615
1530
  if (summary === undefined)
1616
1531
  return diagnostic;
@@ -1907,6 +1822,73 @@ function latestContextualWorldStateGeneration(records) {
1907
1822
  generation = record.recordId;
1908
1823
  return generation;
1909
1824
  }
1825
+ const MAX_CONTEXTUAL_WORLD_STATE_ENTRIES = 1_024;
1826
+ const MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES = 8 * 1024 * 1024;
1827
+ function parsePersistedContextualWorldState(payload) {
1828
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
1829
+ return undefined;
1830
+ const record = payload;
1831
+ if (record.version !== 1)
1832
+ return undefined;
1833
+ const generation = record.generation === null
1834
+ ? undefined
1835
+ : typeof record.generation === 'string' && record.generation.length <= 128
1836
+ ? record.generation
1837
+ : null;
1838
+ if (generation === null)
1839
+ return undefined;
1840
+ const instructionSnapshot = nullableWorldStateString(record.instructionSnapshot);
1841
+ const resourceSnapshot = nullableWorldStateString(record.resourceSnapshot);
1842
+ if (instructionSnapshot === null || resourceSnapshot === null || !Array.isArray(record.entries))
1843
+ return undefined;
1844
+ if (record.entries.length > MAX_CONTEXTUAL_WORLD_STATE_ENTRIES)
1845
+ return undefined;
1846
+ const entries = [];
1847
+ let contentBytes = 0;
1848
+ for (const value of record.entries) {
1849
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
1850
+ return undefined;
1851
+ const entry = value;
1852
+ if (!Number.isSafeInteger(entry.index) ||
1853
+ entry.index < 0 ||
1854
+ (entry.kind !== 'instructions' && entry.kind !== 'resources') ||
1855
+ typeof entry.content !== 'string' ||
1856
+ entry.content.length === 0)
1857
+ return undefined;
1858
+ contentBytes += Buffer.byteLength(entry.content);
1859
+ if (contentBytes > MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES)
1860
+ return undefined;
1861
+ entries.push({
1862
+ index: entry.index,
1863
+ kind: entry.kind,
1864
+ content: entry.content
1865
+ });
1866
+ }
1867
+ return {
1868
+ generation,
1869
+ initialized: true,
1870
+ instructionSnapshot,
1871
+ resourceSnapshot,
1872
+ entries
1873
+ };
1874
+ }
1875
+ function nullableWorldStateString(value) {
1876
+ if (value === null)
1877
+ return undefined;
1878
+ return typeof value === 'string' &&
1879
+ Buffer.byteLength(value) <= MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES
1880
+ ? value
1881
+ : null;
1882
+ }
1883
+ async function persistContextualWorldState(store, sessionId, state) {
1884
+ await store.writeContextualWorldState(sessionId, {
1885
+ version: 1,
1886
+ generation: state.generation ?? null,
1887
+ instructionSnapshot: state.instructionSnapshot ?? null,
1888
+ resourceSnapshot: state.resourceSnapshot ?? null,
1889
+ entries: state.entries
1890
+ });
1891
+ }
1910
1892
  function compactHistoryMayContainRetainedResources(records) {
1911
1893
  return records.some((record) => compactSummaryFromCheckpoint(record)?.includes('# Retained Session resources'));
1912
1894
  }
@@ -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,
@@ -94,6 +94,8 @@ export declare class JsonlSessionStore {
94
94
  source?: SessionSource;
95
95
  }): Promise<SessionMeta>;
96
96
  sessionDirectory(sessionId: string): string;
97
+ readContextualWorldState(sessionId: string): Promise<unknown | undefined>;
98
+ writeContextualWorldState(sessionId: string, state: unknown): Promise<void>;
97
99
  persistInputImages(sessionId: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
98
100
  readInputImages(sessionId: string, value: unknown): Promise<RestoredSessionInputImage[]>;
99
101
  readInputImage(sessionId: string, input: {
@@ -12,6 +12,8 @@ const REVERSE_READ_CHUNK_BYTES = 64 * 1024;
12
12
  const MAX_INPUT_IMAGES = 5;
13
13
  const MAX_INPUT_IMAGE_BYTES = 5 * 1024 * 1024;
14
14
  const MAX_INPUT_IMAGES_BYTES = 10 * 1024 * 1024;
15
+ const MAX_CONTEXTUAL_WORLD_STATE_BYTES = 8 * 1024 * 1024;
16
+ const CONTEXTUAL_WORLD_STATE_FILE = 'context-world-state.json';
15
17
  const INPUT_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
16
18
  export class JsonlSessionStore {
17
19
  static #instances = new Map();
@@ -54,6 +56,8 @@ export class JsonlSessionStore {
54
56
  catch {
55
57
  // A missing index starts empty; JSONL remains authoritative for explicit session reads.
56
58
  }
59
+ for (const session of index.sessions)
60
+ await recoverContextualWorldStateTransactions(root, session);
57
61
  return {
58
62
  root,
59
63
  index,
@@ -157,6 +161,57 @@ export class JsonlSessionStore {
157
161
  throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
158
162
  return join(this.#shared.root, 'sessions', sessionId);
159
163
  }
164
+ async readContextualWorldState(sessionId) {
165
+ const path = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
166
+ try {
167
+ const pathInfo = await lstat(path);
168
+ if (!pathInfo.isFile())
169
+ return undefined;
170
+ const file = await open(path, 'r');
171
+ try {
172
+ const info = await file.stat();
173
+ if (!info.isFile() || info.size > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
174
+ return undefined;
175
+ const envelope = JSON.parse(await file.readFile('utf8'));
176
+ const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
177
+ if (envelope.version !== 1 ||
178
+ !Number.isSafeInteger(envelope.historySequence) ||
179
+ envelope.historySequence < 1 ||
180
+ meta === undefined ||
181
+ envelope.historySequence >= meta.nextSequence)
182
+ return undefined;
183
+ return envelope.state;
184
+ }
185
+ finally {
186
+ await file.close();
187
+ }
188
+ }
189
+ catch {
190
+ return undefined;
191
+ }
192
+ }
193
+ async writeContextualWorldState(sessionId, state) {
194
+ const directory = this.sessionDirectory(sessionId);
195
+ const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
196
+ if (!meta)
197
+ throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
198
+ const path = join(directory, CONTEXTUAL_WORLD_STATE_FILE);
199
+ const temporary = `${path}.${randomUUID()}.tmp`;
200
+ const serialized = JSON.stringify({
201
+ version: 1,
202
+ historySequence: meta.nextSequence - 1,
203
+ state
204
+ });
205
+ if (Buffer.byteLength(serialized) > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
206
+ throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Contextual world state exceeds the storage limit.');
207
+ try {
208
+ await writeFile(temporary, serialized, { mode: 0o600, flag: 'wx' });
209
+ await rename(temporary, path);
210
+ }
211
+ finally {
212
+ await rm(temporary, { force: true });
213
+ }
214
+ }
160
215
  async persistInputImages(sessionId, images) {
161
216
  this.#assertOpen();
162
217
  if (images.length === 0)
@@ -373,6 +428,10 @@ export class JsonlSessionStore {
373
428
  .filter((record) => record.type !== 'responses.continuation');
374
429
  const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
375
430
  const transactionId = randomUUID();
431
+ const targetNextSequence = (retained.at(-1)?.sequence ?? 0) + 1;
432
+ const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
433
+ const contextualWorldStateBackup = `${contextualWorldStatePath}.${targetNextSequence}.${transactionId}.rollback-backup`;
434
+ let contextualWorldStateStaged = false;
376
435
  let generatedTrash = [];
377
436
  let inputTrash = [];
378
437
  const originalMeta = structuredClone(meta);
@@ -386,6 +445,7 @@ export class JsonlSessionStore {
386
445
  const installedPaths = [];
387
446
  let committed = false;
388
447
  try {
448
+ contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
389
449
  generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(generatedImageFileNames(retained)), transactionId);
390
450
  inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName)), transactionId);
391
451
  for (const item of backups)
@@ -398,7 +458,7 @@ export class JsonlSessionStore {
398
458
  const completedAt = new Date().toISOString();
399
459
  meta.title = retainedSessionTitle(retained, meta.title);
400
460
  meta.lastActivityAt = completedAt;
401
- meta.nextSequence = (retained.at(-1)?.sequence ?? 0) + 1;
461
+ meta.nextSequence = targetNextSequence;
402
462
  meta.automaticTitlePending = state.automaticTitlePending;
403
463
  meta.resume = state.resume;
404
464
  meta.segments = staged.segments;
@@ -407,6 +467,8 @@ export class JsonlSessionStore {
407
467
  this.#shared.indexDirty = true;
408
468
  await this.#saveIndex();
409
469
  committed = true;
470
+ if (contextualWorldStateStaged)
471
+ await rm(contextualWorldStateBackup, { force: true });
410
472
  await Promise.all([
411
473
  ...backups.map((item) => rm(item.backup, { force: true }).catch(() => undefined)),
412
474
  ...generatedTrash.map((item) => rm(item.trash, { force: true }).catch(() => undefined)),
@@ -425,6 +487,10 @@ export class JsonlSessionStore {
425
487
  if (!committed) {
426
488
  await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
427
489
  await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
490
+ if (contextualWorldStateStaged)
491
+ await restoreRenamedFiles([
492
+ { from: contextualWorldStateBackup, to: contextualWorldStatePath }
493
+ ]);
428
494
  await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
429
495
  restoreSessionMeta(meta, originalMeta);
430
496
  this.#shared.index.revision = originalIndexRevision;
@@ -489,6 +555,9 @@ export class JsonlSessionStore {
489
555
  ];
490
556
  const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
491
557
  const transactionId = randomUUID();
558
+ const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
559
+ const contextualWorldStateBackup = `${contextualWorldStatePath}.2.${transactionId}.clear-backup`;
560
+ let contextualWorldStateStaged = false;
492
561
  let generatedTrash = [];
493
562
  let inputTrash = [];
494
563
  const originalMeta = structuredClone(meta);
@@ -502,6 +571,7 @@ export class JsonlSessionStore {
502
571
  const installedPaths = [];
503
572
  let committed = false;
504
573
  try {
574
+ contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
505
575
  generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(), transactionId);
506
576
  inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(), transactionId);
507
577
  for (const item of backups)
@@ -519,6 +589,8 @@ export class JsonlSessionStore {
519
589
  this.#shared.indexDirty = true;
520
590
  await this.#saveIndex();
521
591
  committed = true;
592
+ if (contextualWorldStateStaged)
593
+ await rm(contextualWorldStateBackup, { force: true });
522
594
  await Promise.all([
523
595
  ...backups.map((item) => rm(item.backup, { force: true }).catch(() => undefined)),
524
596
  ...generatedTrash.map((item) => rm(item.trash, { force: true }).catch(() => undefined)),
@@ -537,6 +609,10 @@ export class JsonlSessionStore {
537
609
  if (!committed) {
538
610
  await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
539
611
  await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
612
+ if (contextualWorldStateStaged)
613
+ await restoreRenamedFiles([
614
+ { from: contextualWorldStateBackup, to: contextualWorldStatePath }
615
+ ]);
540
616
  await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
541
617
  restoreSessionMeta(meta, originalMeta);
542
618
  this.#shared.index.revision = originalIndexRevision;
@@ -1072,6 +1148,49 @@ async function sessionDirectoryExists(path) {
1072
1148
  throw error;
1073
1149
  }
1074
1150
  }
1151
+ async function renameIfExists(from, to) {
1152
+ try {
1153
+ await rename(from, to);
1154
+ return true;
1155
+ }
1156
+ catch (error) {
1157
+ if (error.code === 'ENOENT')
1158
+ return false;
1159
+ throw error;
1160
+ }
1161
+ }
1162
+ async function recoverContextualWorldStateTransactions(root, meta) {
1163
+ const directory = join(root, 'sessions', meta.id);
1164
+ let names;
1165
+ try {
1166
+ names = await readdir(directory);
1167
+ }
1168
+ catch (error) {
1169
+ if (error.code === 'ENOENT')
1170
+ return;
1171
+ throw error;
1172
+ }
1173
+ const pattern = /^context-world-state\.json\.(\d+)\.[0-9a-f-]+\.(?:rollback|clear)-backup$/u;
1174
+ const backups = names.filter((name) => pattern.test(name));
1175
+ if (backups.length === 0)
1176
+ return;
1177
+ const eventNames = names.filter((name) => /^events-\d{6}\.jsonl$/u.test(name)).sort();
1178
+ const latestEventName = eventNames.at(-1);
1179
+ if (latestEventName === undefined)
1180
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.');
1181
+ const latestRecords = await readRecords(join(directory, latestEventName));
1182
+ const actualNextSequence = (latestRecords.at(-1)?.sequence ?? 0) + 1;
1183
+ const canonical = join(directory, CONTEXTUAL_WORLD_STATE_FILE);
1184
+ for (const name of backups) {
1185
+ const match = pattern.exec(name);
1186
+ const backup = join(directory, name);
1187
+ const targetNextSequence = Number(match[1]);
1188
+ if (actualNextSequence === targetNextSequence || (await sessionDirectoryExists(canonical)))
1189
+ await rm(backup, { force: true });
1190
+ else
1191
+ await rename(backup, canonical);
1192
+ }
1193
+ }
1075
1194
  function generatedImageFileNames(records) {
1076
1195
  const names = new Set();
1077
1196
  for (const record of records) {
@@ -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.85",
3
+ "version": "0.9.87",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",