@myagentroam/agent 0.9.86 → 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,11 +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
- }
9
2
  export interface ClientToolDefinition {
10
3
  name: string;
11
4
  description: string;
@@ -75,10 +68,6 @@ export interface ModelRequest {
75
68
  mimeType: string;
76
69
  dataBase64: string;
77
70
  }[];
78
- continuation?: {
79
- previousResponseId: string;
80
- deltaMessages: readonly ModelMessage[];
81
- };
82
71
  onAttemptDiagnostic?: (diagnostic: ModelAttemptDiagnostic) => Promise<void> | void;
83
72
  }
84
73
  export interface ModelUsage {
@@ -158,13 +147,11 @@ export interface ModelAdapter {
158
147
  * The returned commit callback must be synchronous and non-throwing.
159
148
  */
160
149
  prepareReconfigure?(configuration: MarAgentModelConfiguration): (() => void) | undefined;
161
- restoreContinuation?(checkpoint: ModelContinuationCheckpoint): boolean;
162
150
  close?(): Promise<void>;
163
151
  }
164
152
  export interface ModelTurnSession {
165
153
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
166
154
  /** Drops incremental response state after a local context replacement. */
167
155
  resetContinuation?(): void;
168
- continuationCheckpoint?(): ModelContinuationCheckpoint | undefined;
169
156
  close(): Promise<void>;
170
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,23 +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
- !responsesPersistedContinuationEnabled(this.#configuration) ||
39
- checkpoint.version !== 2 ||
40
- checkpoint.protocol !== 'OPENAI_RESPONSES' ||
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))
47
- return false;
48
- this.#cachedState.restoredContinuation = checkpoint;
49
- return true;
50
- }
51
34
  async *start(request, signal) {
52
35
  const turn = new OpenAiResponsesTurnSession(this, {}, false);
53
36
  try {
@@ -79,8 +62,10 @@ export class OpenAiResponsesAdapter {
79
62
  invalidateResponsesWebSocket(state, true);
80
63
  return;
81
64
  }
82
- if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN)
65
+ if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN) {
83
66
  invalidateResponsesWebSocket(state, false);
67
+ clearResponsesContinuation(state);
68
+ }
84
69
  this.#cachedState = state;
85
70
  this.#cacheLeased = false;
86
71
  }
@@ -160,6 +145,24 @@ export class OpenAiResponsesAdapter {
160
145
  ...(options.retryDelayMs === undefined ? {} : { retryDelayMs: options.retryDelayMs }),
161
146
  ...(options.retryReason === undefined ? {} : { retryReason: options.retryReason })
162
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
+ }
163
166
  if (!signal.aborted &&
164
167
  attemptNumber < maxAttempts &&
165
168
  !sawSemanticEvent &&
@@ -169,6 +172,7 @@ export class OpenAiResponsesAdapter {
169
172
  invalidContinuationFallbackUsed = true;
170
173
  useContinuation = false;
171
174
  requestBody = prepared.fullBody;
175
+ turn.resetContinuation();
172
176
  turn.invalidateWebSocket();
173
177
  credentialReason = 'RECONNECT';
174
178
  await reportFailure(true, {
@@ -637,7 +641,6 @@ class OpenAiResponsesTurnSession {
637
641
  (outputItems.length > 0 || !sawUnrepresentedOutput)) {
638
642
  this.state.lastRequest = prepared.fullBody;
639
643
  this.state.lastResponse = { responseId, outputItems };
640
- delete this.state.restoredContinuation;
641
644
  }
642
645
  else
643
646
  clearResponsesContinuation(this.state);
@@ -660,27 +663,13 @@ class OpenAiResponsesTurnSession {
660
663
  resetContinuation() {
661
664
  clearResponsesContinuation(this.state);
662
665
  }
663
- continuationCheckpoint() {
664
- if (!responsesPersistedContinuationEnabled(this.adapter.configuration))
665
- return undefined;
666
- const request = this.state.lastRequest;
667
- const response = this.state.lastResponse;
668
- if (request === undefined || response === undefined)
669
- return this.state.restoredContinuation;
670
- const baseline = [...request.input, ...response.outputItems];
671
- return {
672
- version: 2,
673
- protocol: 'OPENAI_RESPONSES',
674
- configurationHash: responsesContinuationIdentityHash(this.adapter.configuration),
675
- responseId: response.responseId,
676
- inputPrefixLength: baseline.length
677
- };
678
- }
679
666
  prepareRequest(request) {
680
667
  if (this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
681
668
  this.state.socket !== undefined &&
682
- this.state.socket.readyState !== WebSocket.OPEN)
669
+ this.state.socket.readyState !== WebSocket.OPEN) {
683
670
  invalidateResponsesWebSocket(this.state, false);
671
+ clearResponsesContinuation(this.state);
672
+ }
684
673
  const fullBody = responsesRequestBody(this.adapter.configuration, request, request.messages, this.adapter.prefixIdentity);
685
674
  if (responsesContinuationEnabled(this.adapter.configuration)) {
686
675
  const incremental = incrementalResponsesInput(this.state, fullBody);
@@ -743,8 +732,17 @@ class OpenAiResponsesTurnSession {
743
732
  const endpoint = webSocketEndpoint(input.configuration.baseUrl);
744
733
  const existingSocket = this.state.socket;
745
734
  if (existingSocket !== undefined &&
746
- (existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint))
735
+ (existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint)) {
747
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
+ }
748
746
  if (this.state.socket === undefined) {
749
747
  const handshakeCredential = await acquireResponsesCredential(input.configuration, input.reason);
750
748
  input.credentialAcquired(handshakeCredential.revision);
@@ -755,6 +753,10 @@ class OpenAiResponsesTurnSession {
755
753
  if (requestCredential.revision !== this.state.credentialRevision ||
756
754
  responsesCredentialFingerprint(requestCredential) !== this.state.credentialFingerprint) {
757
755
  invalidateResponsesWebSocket(this.state, false);
756
+ if (input.continuation) {
757
+ clearResponsesContinuation(this.state);
758
+ throw localContinuationReset();
759
+ }
758
760
  await this.connectWebSocket(endpoint, requestCredential, input.promptCacheKey, input.signal);
759
761
  }
760
762
  if (input.signal.aborted) {
@@ -814,6 +816,12 @@ class OpenAiResponsesTurnSession {
814
816
  this.state.proxyType = proxyType;
815
817
  }
816
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
+ }
817
825
  function responsesRequestBody(configuration, request, messages, prefixIdentity = '') {
818
826
  const tools = request.allowTools === false
819
827
  ? []
@@ -887,13 +895,8 @@ function uuidV5(namespace, value) {
887
895
  function incrementalResponsesInput(state, current) {
888
896
  const previous = state.lastRequest;
889
897
  const completion = state.lastResponse;
890
- if (previous === undefined || completion === undefined) {
891
- const restored = state.restoredContinuation;
892
- if (restored === undefined || current.input.length < restored.inputPrefixLength)
893
- return undefined;
894
- state.lastResponse = { responseId: restored.responseId, outputItems: [] };
895
- return current.input.slice(restored.inputPrefixLength);
896
- }
898
+ if (previous === undefined || completion === undefined)
899
+ return undefined;
897
900
  const baseline = [...previous.input, ...completion.outputItems];
898
901
  if (current.input.length < baseline.length)
899
902
  return undefined;
@@ -905,25 +908,14 @@ function incrementalResponsesInput(state, current) {
905
908
  function clearResponsesContinuation(state) {
906
909
  delete state.lastRequest;
907
910
  delete state.lastResponse;
908
- delete state.restoredContinuation;
909
911
  }
910
912
  function hashResponsesValue(value) {
911
913
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
912
914
  }
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
915
  function responsesContinuationEnabled(configuration) {
921
916
  return (configuration.responsesPreviousResponseId === true &&
922
917
  configuration.responsesTransport?.transport === 'WEBSOCKET');
923
918
  }
924
- function responsesPersistedContinuationEnabled(configuration) {
925
- return responsesContinuationEnabled(configuration) && configuration.responsesEncoding !== 'LITE';
926
- }
927
919
  function responsesCredentialFingerprint(credential) {
928
920
  return hashResponsesValue({
929
921
  bearer: credential.bearer.reveal(),
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,44 +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
- }
1610
- function isBoundedString(value, maximumLength) {
1611
- return typeof value === 'string' && value.length > 0 && value.length <= maximumLength;
1612
- }
1613
1529
  function modelAttemptFailureDiagnostic(diagnostic, summary) {
1614
1530
  if (summary === undefined)
1615
1531
  return diagnostic;
@@ -1906,6 +1822,73 @@ function latestContextualWorldStateGeneration(records) {
1906
1822
  generation = record.recordId;
1907
1823
  return generation;
1908
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
+ }
1909
1892
  function compactHistoryMayContainRetainedResources(records) {
1910
1893
  return records.some((record) => compactSummaryFromCheckpoint(record)?.includes('# Retained Session resources'));
1911
1894
  }
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.86",
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",