@myagentroam/agent 0.9.83 → 0.9.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,6 +80,11 @@ const rawModelSchema = z
80
80
  });
81
81
  if (value.protocol !== 'OPENAI_RESPONSES' && value.responsesPreviousResponseId)
82
82
  context.addIssue({ code: 'custom', message: 'Responses continuation requires Responses.' });
83
+ if (value.responsesPreviousResponseId && value.responsesTransport?.transport !== 'WEBSOCKET')
84
+ context.addIssue({
85
+ code: 'custom',
86
+ message: 'Responses continuation requires WebSocket transport.'
87
+ });
83
88
  if (value.protocol !== 'OPENAI_RESPONSES' && value.responsesTransport !== undefined)
84
89
  context.addIssue({ code: 'custom', message: 'Responses transport requires Responses.' });
85
90
  if (value.protocol !== 'OPENAI_RESPONSES' && value.responsesEncoding !== undefined)
@@ -1,4 +1,12 @@
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
+ }
2
10
  export interface ClientToolDefinition {
3
11
  name: string;
4
12
  description: string;
@@ -11,7 +19,7 @@ export interface ClientToolDefinition {
11
19
  export interface ModelMessage {
12
20
  role: 'user' | 'assistant' | 'tool' | 'tool_call' | 'provider';
13
21
  content: string;
14
- contextKind?: 'environment';
22
+ contextKind?: 'environment' | 'private';
15
23
  callId?: string;
16
24
  name?: string;
17
25
  arguments?: unknown;
@@ -146,11 +154,18 @@ export interface ModelAdapter {
146
154
  readonly configuration: MarAgentModelConfiguration;
147
155
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
148
156
  createTurnSession?(): ModelTurnSession;
157
+ /**
158
+ * Prepares an equivalent model rebind without mutating live state.
159
+ * The returned commit callback must be synchronous and non-throwing.
160
+ */
161
+ prepareReconfigure?(configuration: MarAgentModelConfiguration): (() => void) | undefined;
162
+ restoreContinuation?(checkpoint: ModelContinuationCheckpoint): boolean;
149
163
  close?(): Promise<void>;
150
164
  }
151
165
  export interface ModelTurnSession {
152
166
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
153
167
  /** Drops incremental response state after a local context replacement. */
154
168
  resetContinuation?(): void;
169
+ continuationCheckpoint?(): ModelContinuationCheckpoint | undefined;
155
170
  close(): Promise<void>;
156
171
  }
@@ -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, ModelEvent, ModelRequest, ModelTurnSession } from './contracts.js';
4
+ import type { ModelAdapter, ModelAttemptDiagnostic, ModelContinuationCheckpoint, 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>> & {
@@ -11,6 +11,7 @@ interface ResponsesSessionState {
11
11
  socket?: WebSocket;
12
12
  endpoint?: string;
13
13
  credentialRevision?: number;
14
+ credentialFingerprint?: string;
14
15
  connectionId?: string;
15
16
  proxyType?: 'DIRECT' | 'SOCKS5';
16
17
  lastRequest?: ResponsesRequestBody;
@@ -18,18 +19,20 @@ interface ResponsesSessionState {
18
19
  readonly responseId: string;
19
20
  readonly outputItems: readonly Record<string, unknown>[];
20
21
  };
22
+ restoredContinuation?: ModelContinuationCheckpoint;
21
23
  }
22
24
  interface PreparedResponsesRequest {
23
25
  readonly fullBody: ResponsesRequestBody;
24
26
  readonly body: ResponsesRequestBody;
25
27
  readonly continuation: boolean;
26
- readonly requiresExistingSocket: boolean;
27
28
  }
28
29
  export declare class OpenAiResponsesAdapter implements ModelAdapter {
29
30
  #private;
30
- readonly configuration: MarAgentModelConfiguration;
31
31
  readonly prefixIdentity: `${string}-${string}-${string}-${string}-${string}`;
32
32
  constructor(configuration: MarAgentModelConfiguration);
33
+ get configuration(): MarAgentModelConfiguration;
34
+ prepareReconfigure(configuration: MarAgentModelConfiguration): (() => void) | undefined;
35
+ restoreContinuation(checkpoint: ModelContinuationCheckpoint): boolean;
33
36
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
34
37
  createTurnSession(): ModelTurnSession;
35
38
  close(): Promise<void>;
@@ -42,11 +45,11 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
42
45
  private readonly adapter;
43
46
  private readonly state;
44
47
  private readonly cacheLease;
45
- private readonly allowExternalContinuation;
46
- constructor(adapter: OpenAiResponsesAdapter, state: ResponsesSessionState, cacheLease: boolean, allowExternalContinuation: boolean);
48
+ constructor(adapter: OpenAiResponsesAdapter, state: ResponsesSessionState, cacheLease: boolean);
47
49
  start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
48
50
  close(): Promise<void>;
49
51
  resetContinuation(): void;
52
+ continuationCheckpoint(): ModelContinuationCheckpoint | undefined;
50
53
  prepareRequest(request: ModelRequest): PreparedResponsesRequest;
51
54
  fullRequestBody(): ResponsesRequestBody;
52
55
  currentTurnState(): string | undefined;
@@ -61,7 +64,6 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
61
64
  readonly reason: ModelCredentialAcquireReason;
62
65
  readonly signal: AbortSignal;
63
66
  readonly continuation: boolean;
64
- readonly requiresExistingSocket: boolean;
65
67
  readonly credentialAcquired: (revision: number) => void;
66
68
  }): Promise<WebSocket>;
67
69
  private connectWebSocket;
@@ -13,16 +13,38 @@ const X_CODEX_TURN_STATE = 'x-codex-turn-state';
13
13
  const RESPONSES_LITE_HEADER = 'x-openai-internal-codex-responses-lite';
14
14
  const RESPONSES_LITE_METADATA = 'ws_request_header_x_openai_internal_codex_responses_lite';
15
15
  export class OpenAiResponsesAdapter {
16
- configuration;
17
16
  prefixIdentity = randomUUID();
18
17
  #cachedState = {};
19
18
  #cacheLeased = false;
20
19
  #closed = false;
20
+ #configuration;
21
21
  constructor(configuration) {
22
- this.configuration = configuration;
22
+ this.#configuration = configuration;
23
+ }
24
+ get configuration() {
25
+ return this.#configuration;
26
+ }
27
+ prepareReconfigure(configuration) {
28
+ if (!sameResponsesAdapterConfiguration(this.#configuration, configuration))
29
+ return undefined;
30
+ return () => {
31
+ this.#configuration = configuration;
32
+ };
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;
23
45
  }
24
46
  async *start(request, signal) {
25
- const turn = new OpenAiResponsesTurnSession(this, {}, false, true);
47
+ const turn = new OpenAiResponsesTurnSession(this, {}, false);
26
48
  try {
27
49
  yield* turn.start(request, signal);
28
50
  }
@@ -39,7 +61,7 @@ export class OpenAiResponsesAdapter {
39
61
  this.#cachedState = {};
40
62
  this.#cacheLeased = true;
41
63
  }
42
- return new OpenAiResponsesTurnSession(this, state, cacheLease, false);
64
+ return new OpenAiResponsesTurnSession(this, state, cacheLease);
43
65
  }
44
66
  async close() {
45
67
  if (this.#closed)
@@ -53,14 +75,13 @@ export class OpenAiResponsesAdapter {
53
75
  return;
54
76
  }
55
77
  if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN)
56
- invalidateResponsesWebSocket(state, true);
78
+ invalidateResponsesWebSocket(state, false);
57
79
  this.#cachedState = state;
58
80
  this.#cacheLeased = false;
59
81
  }
60
82
  async *startTurn(request, signal, turn, prepared) {
61
83
  let useContinuation = prepared.continuation;
62
84
  let requestBody = prepared.body;
63
- let requiresExistingSocket = prepared.requiresExistingSocket;
64
85
  let credentialReason = 'REQUEST';
65
86
  let unauthorizedRetryUsed = false;
66
87
  let invalidContinuationFallbackUsed = false;
@@ -87,7 +108,7 @@ export class OpenAiResponsesAdapter {
87
108
  credentialReason: attemptCredentialReason
88
109
  });
89
110
  try {
90
- for await (const event of this.startAttempt(request, signal, requestBody, useContinuation, credentialReason, requiresExistingSocket, turn)) {
111
+ for await (const event of this.startAttempt(request, signal, requestBody, useContinuation, credentialReason, turn)) {
91
112
  if (!sawSemanticEvent && !isSemanticEvent(event)) {
92
113
  prelude.push(event);
93
114
  continue;
@@ -143,7 +164,6 @@ export class OpenAiResponsesAdapter {
143
164
  invalidContinuationFallbackUsed = true;
144
165
  useContinuation = false;
145
166
  requestBody = prepared.fullBody;
146
- requiresExistingSocket = false;
147
167
  turn.invalidateWebSocket();
148
168
  credentialReason = 'RECONNECT';
149
169
  await reportFailure(true, {
@@ -192,14 +212,13 @@ export class OpenAiResponsesAdapter {
192
212
  : 'REQUEST';
193
213
  useContinuation = false;
194
214
  requestBody = prepared.fullBody;
195
- requiresExistingSocket = false;
196
215
  if (error instanceof MarAgentError && error.details?.transport === 'WEBSOCKET')
197
216
  turn.invalidateWebSocket();
198
217
  await waitForModelRetry(retryAfter(error), attempt, signal, delayMs);
199
218
  }
200
219
  }
201
220
  }
202
- async *startAttempt(request, signal, body, continuation, credentialReason, requiresExistingSocket, turn) {
221
+ async *startAttempt(request, signal, body, continuation, credentialReason, turn) {
203
222
  let latestCredentialRevision;
204
223
  const fetchResponse = async (requestBody) => {
205
224
  const serializedBody = JSON.stringify(requestBody);
@@ -233,7 +252,6 @@ export class OpenAiResponsesAdapter {
233
252
  reason: credentialReason,
234
253
  signal,
235
254
  continuation,
236
- requiresExistingSocket,
237
255
  turn,
238
256
  credentialAcquired: (revision) => (latestCredentialRevision = revision)
239
257
  });
@@ -517,21 +535,28 @@ export class OpenAiResponsesAdapter {
517
535
  });
518
536
  }
519
537
  }
538
+ function sameResponsesAdapterConfiguration(current, next) {
539
+ const { apiKey: currentApiKey, credentialProvider: currentCredentialProvider, imageGeneration: _currentImageGeneration, ...currentTransport } = current;
540
+ const { apiKey: nextApiKey, credentialProvider: nextCredentialProvider, imageGeneration: _nextImageGeneration, ...nextTransport } = next;
541
+ const credentialsAreRebindable = (currentCredentialProvider !== undefined && nextCredentialProvider !== undefined) ||
542
+ (currentApiKey !== undefined &&
543
+ nextApiKey !== undefined &&
544
+ currentApiKey.reveal() === nextApiKey.reveal());
545
+ return credentialsAreRebindable && isDeepStrictEqual(currentTransport, nextTransport);
546
+ }
520
547
  class OpenAiResponsesTurnSession {
521
548
  adapter;
522
549
  state;
523
550
  cacheLease;
524
- allowExternalContinuation;
525
551
  #active = false;
526
552
  #closed = false;
527
553
  #turnState;
528
554
  #fullBody;
529
555
  #connectionDiagnostic;
530
- constructor(adapter, state, cacheLease, allowExternalContinuation) {
556
+ constructor(adapter, state, cacheLease) {
531
557
  this.adapter = adapter;
532
558
  this.state = state;
533
559
  this.cacheLease = cacheLease;
534
- this.allowExternalContinuation = allowExternalContinuation;
535
560
  }
536
561
  async *start(request, signal) {
537
562
  if (this.#closed)
@@ -548,7 +573,7 @@ class OpenAiResponsesTurnSession {
548
573
  this.#fullBody = prepared.fullBody;
549
574
  if (this.adapter.configuration.responsesEncoding === 'LITE' &&
550
575
  this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
551
- this.adapter.configuration.responsesPreviousResponseId &&
576
+ responsesContinuationEnabled(this.adapter.configuration) &&
552
577
  this.state.lastRequest === undefined &&
553
578
  request.allowTools !== false &&
554
579
  request.toolChoice !== 'none' &&
@@ -561,7 +586,7 @@ class OpenAiResponsesTurnSession {
561
586
  for await (const event of this.adapter.startTurn({
562
587
  ...request,
563
588
  onAttemptDiagnostic: async (diagnostic) => request.onAttemptDiagnostic?.({ ...diagnostic, prewarm: true })
564
- }, signal, this, { fullBody: warmBody, body: warmBody, continuation: false, requiresExistingSocket: false })) {
589
+ }, signal, this, { fullBody: warmBody, body: warmBody, continuation: false })) {
565
590
  if (event.type === 'response.started' && event.responseId !== 'unknown')
566
591
  warmId = event.responseId;
567
592
  if (event.type === 'completed')
@@ -601,12 +626,13 @@ class OpenAiResponsesTurnSession {
601
626
  completed = event.reason === 'completed' || event.reason === 'tool_use';
602
627
  yield event;
603
628
  }
604
- if (this.adapter.configuration.responsesPreviousResponseId &&
629
+ if (responsesContinuationEnabled(this.adapter.configuration) &&
605
630
  completed &&
606
631
  responseId !== undefined &&
607
632
  (outputItems.length > 0 || !sawUnrepresentedOutput)) {
608
633
  this.state.lastRequest = prepared.fullBody;
609
634
  this.state.lastResponse = { responseId, outputItems };
635
+ delete this.state.restoredContinuation;
610
636
  }
611
637
  else
612
638
  clearResponsesContinuation(this.state);
@@ -629,17 +655,32 @@ class OpenAiResponsesTurnSession {
629
655
  resetContinuation() {
630
656
  clearResponsesContinuation(this.state);
631
657
  }
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
+ }
632
675
  prepareRequest(request) {
633
676
  if (this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
634
677
  this.state.socket !== undefined &&
635
678
  this.state.socket.readyState !== WebSocket.OPEN)
636
- invalidateResponsesWebSocket(this.state, true);
679
+ invalidateResponsesWebSocket(this.state, false);
637
680
  const fullBody = responsesRequestBody(this.adapter.configuration, request, request.messages, this.adapter.prefixIdentity);
638
- if (this.adapter.configuration.responsesPreviousResponseId) {
681
+ if (responsesContinuationEnabled(this.adapter.configuration)) {
639
682
  const incremental = incrementalResponsesInput(this.state, fullBody);
640
- if (incremental !== undefined &&
641
- (this.adapter.configuration.responsesTransport?.transport !== 'WEBSOCKET' ||
642
- this.state.socket?.readyState === WebSocket.OPEN)) {
683
+ if (incremental !== undefined) {
643
684
  const responseId = this.state.lastResponse.responseId;
644
685
  delete this.state.lastResponse;
645
686
  return {
@@ -649,31 +690,14 @@ class OpenAiResponsesTurnSession {
649
690
  input: incremental,
650
691
  previous_response_id: responseId
651
692
  },
652
- continuation: true,
653
- requiresExistingSocket: this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET'
654
- };
655
- }
656
- if (this.allowExternalContinuation &&
657
- request.continuation !== undefined &&
658
- this.adapter.configuration.responsesTransport?.transport !== 'WEBSOCKET') {
659
- const deltaBody = responsesRequestBody(this.adapter.configuration, request, request.continuation.deltaMessages);
660
- return {
661
- fullBody,
662
- body: {
663
- ...fullBody,
664
- input: deltaBody.input,
665
- previous_response_id: request.continuation.previousResponseId
666
- },
667
- continuation: true,
668
- requiresExistingSocket: false
693
+ continuation: true
669
694
  };
670
695
  }
671
696
  }
672
697
  return {
673
698
  fullBody,
674
699
  body: fullBody,
675
- continuation: false,
676
- requiresExistingSocket: false
700
+ continuation: false
677
701
  };
678
702
  }
679
703
  fullRequestBody() {
@@ -714,14 +738,9 @@ class OpenAiResponsesTurnSession {
714
738
  async webSocketForRequest(input) {
715
739
  const endpoint = webSocketEndpoint(input.configuration.baseUrl);
716
740
  const existingSocket = this.state.socket;
717
- if (input.requiresExistingSocket &&
718
- (existingSocket?.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint)) {
719
- invalidateResponsesWebSocket(this.state, true);
720
- throw unavailableContinuationConnection();
721
- }
722
741
  if (existingSocket !== undefined &&
723
742
  (existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint))
724
- invalidateResponsesWebSocket(this.state, true);
743
+ invalidateResponsesWebSocket(this.state, false);
725
744
  if (this.state.socket === undefined) {
726
745
  const handshakeCredential = await acquireResponsesCredential(input.configuration, input.reason);
727
746
  input.credentialAcquired(handshakeCredential.revision);
@@ -729,10 +748,9 @@ class OpenAiResponsesTurnSession {
729
748
  }
730
749
  const requestCredential = await acquireResponsesCredential(input.configuration, input.reason);
731
750
  input.credentialAcquired(requestCredential.revision);
732
- if (requestCredential.revision !== this.state.credentialRevision) {
733
- invalidateResponsesWebSocket(this.state, true);
734
- if (input.continuation)
735
- throw unavailableContinuationConnection();
751
+ if (requestCredential.revision !== this.state.credentialRevision ||
752
+ responsesCredentialFingerprint(requestCredential) !== this.state.credentialFingerprint) {
753
+ invalidateResponsesWebSocket(this.state, false);
736
754
  await this.connectWebSocket(endpoint, requestCredential, input.promptCacheKey, input.signal);
737
755
  }
738
756
  if (input.signal.aborted) {
@@ -787,6 +805,7 @@ class OpenAiResponsesTurnSession {
787
805
  this.state.socket = socket;
788
806
  this.state.endpoint = endpoint;
789
807
  this.state.credentialRevision = credential.revision;
808
+ this.state.credentialFingerprint = responsesCredentialFingerprint(credential);
790
809
  this.state.connectionId = connectionId;
791
810
  this.state.proxyType = proxyType;
792
811
  }
@@ -864,10 +883,16 @@ function uuidV5(namespace, value) {
864
883
  function incrementalResponsesInput(state, current) {
865
884
  const previous = state.lastRequest;
866
885
  const completion = state.lastResponse;
867
- if (previous === undefined ||
868
- completion === undefined ||
869
- !responsesRequestPropertiesMatch(previous, current))
870
- return undefined;
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
+ }
871
896
  const baseline = [...previous.input, ...completion.outputItems];
872
897
  if (current.input.length < baseline.length)
873
898
  return undefined;
@@ -876,32 +901,41 @@ function incrementalResponsesInput(state, current) {
876
901
  return undefined;
877
902
  return current.input.slice(baseline.length);
878
903
  }
879
- function responsesRequestPropertiesMatch(previous, current) {
880
- const keys = [
881
- 'model',
882
- 'instructions',
883
- 'tools',
884
- 'tool_choice',
885
- 'parallel_tool_calls',
886
- 'reasoning',
887
- 'store',
888
- 'stream',
889
- 'include',
890
- 'prompt_cache_key',
891
- 'text',
892
- 'client_metadata'
893
- ];
894
- return keys.every((key) => isDeepStrictEqual(previous[key], current[key]));
895
- }
896
904
  function clearResponsesContinuation(state) {
897
905
  delete state.lastRequest;
898
906
  delete state.lastResponse;
907
+ delete state.restoredContinuation;
908
+ }
909
+ function hashResponsesValue(value) {
910
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
911
+ }
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
+ function responsesContinuationEnabled(configuration) {
920
+ return (configuration.responsesPreviousResponseId === true &&
921
+ configuration.responsesTransport?.transport === 'WEBSOCKET');
922
+ }
923
+ function responsesCredentialFingerprint(credential) {
924
+ return hashResponsesValue({
925
+ bearer: credential.bearer.reveal(),
926
+ headers: Object.fromEntries(Object.entries(credential.headers).map(([name, value]) => [
927
+ name,
928
+ typeof value === 'string' ? value : value.reveal()
929
+ ])),
930
+ socks5Url: credential.socks5Url?.reveal()
931
+ });
899
932
  }
900
933
  function invalidateResponsesWebSocket(state, clearContinuation) {
901
934
  const socket = state.socket;
902
935
  delete state.socket;
903
936
  delete state.endpoint;
904
937
  delete state.credentialRevision;
938
+ delete state.credentialFingerprint;
905
939
  delete state.connectionId;
906
940
  delete state.proxyType;
907
941
  if (clearContinuation)
@@ -911,16 +945,6 @@ function invalidateResponsesWebSocket(state, clearContinuation) {
911
945
  socket.readyState !== WebSocket.CLOSING)
912
946
  terminateWebSocket(socket);
913
947
  }
914
- function unavailableContinuationConnection() {
915
- return new MarAgentError('previous_response_not_found', 'The previous response is unavailable on the current WebSocket connection.', {
916
- details: {
917
- reason: 'OPENAI_RESPONSES_CONTINUATION_CONNECTION_UNAVAILABLE',
918
- transport: 'WEBSOCKET',
919
- continuation: true
920
- },
921
- retryable: true
922
- });
923
- }
924
948
  function turnStateFromEvent(event) {
925
949
  for (const headers of [
926
950
  record(event.headers),
@@ -958,7 +982,6 @@ async function* webSocketResponseEvents(input) {
958
982
  reason: input.reason,
959
983
  signal: input.signal,
960
984
  continuation: input.continuation,
961
- requiresExistingSocket: input.requiresExistingSocket,
962
985
  credentialAcquired: input.credentialAcquired
963
986
  });
964
987
  const body = input.turn.withTurnStateMetadata(input.body);
@@ -95,7 +95,9 @@ export function buildNativeCompactionMessages(messages, input = {}) {
95
95
  const focus = input.focus?.trim();
96
96
  const contract = input.highDensityCompaction ? highDensityCompactionPrompt() : compactionPrompt();
97
97
  return [
98
- ...messages.map((message) => {
98
+ ...messages
99
+ .filter((message) => message.contextKind !== 'private')
100
+ .map((message) => {
99
101
  if (!message.images?.length)
100
102
  return message;
101
103
  const textOnly = { ...message };
@@ -139,6 +139,7 @@ export function restoreModelContext(payload, providerModelId) {
139
139
  role: 'provider',
140
140
  provider: 'ANTHROPIC_MESSAGES',
141
141
  providerModelId: modelId,
142
+ contextKind: 'private',
142
143
  content: '',
143
144
  item
144
145
  }
package/dist/sdk/agent.js CHANGED
@@ -55,6 +55,7 @@ export async function createMarAgent(options) {
55
55
  throw new MarAgentError('MAR_AGENT_HOST_INCOMPATIBLE', 'Host mode does not match Agent mode.');
56
56
  const store = await JsonlSessionStore.open(description.homeDirectory);
57
57
  const active = new Map();
58
+ const activeExecutionAdapters = new Set();
58
59
  const executionResults = new Map();
59
60
  const sessionRuntimes = new Map();
60
61
  let disposed = false;
@@ -354,6 +355,7 @@ export async function createMarAgent(options) {
354
355
  const titleModel = [...executionModels.values()].find((model) => model.titleGeneration) ??
355
356
  executionModels.get(input.modelId ?? executionDefaultModelId);
356
357
  titleAdapter = createAdapter(titleModel.id, executionModels);
358
+ activeExecutionAdapters.add(titleAdapter);
357
359
  titleTask = generateSessionTitle(titleAdapter, input.prompt, titleModel.defaultReasoningEffort, controller.signal, recordModelAttempt('TITLE')).catch(() => undefined);
358
360
  }
359
361
  }
@@ -370,12 +372,17 @@ export async function createMarAgent(options) {
370
372
  turnId
371
373
  });
372
374
  selectedAdapter = sessionAdapter(input.modelId ?? executionDefaultModelId, executionModels);
375
+ const selectedModel = executionModels.get(input.modelId ?? executionDefaultModelId);
376
+ const restoredContinuation = latestModelContinuationCheckpoint(contextRecords, selectedModel.id);
377
+ if (restoredContinuation !== undefined)
378
+ selectedAdapter.restoreContinuation?.(restoredContinuation);
373
379
  if (options.adapterFactory !== undefined &&
374
- selectedAdapter.createTurnSession === undefined)
380
+ selectedAdapter.createTurnSession === undefined) {
375
381
  executionScopedAdapters.add(selectedAdapter);
382
+ activeExecutionAdapters.add(selectedAdapter);
383
+ }
376
384
  const selected = createModelTurnSession(selectedAdapter);
377
385
  selectedTurn = selected;
378
- const selectedModel = executionModels.get(input.modelId ?? executionDefaultModelId);
379
386
  let lastServerUsage = latestModelUsage(contextRecords, selectedModel.id);
380
387
  const compactUserTokenLimit = Math.min(20_000, Math.floor(selectedModel.contextWindowTokens / 8));
381
388
  const reasoningEffort = input.reasoningEffort ?? selectedModel.defaultReasoningEffort;
@@ -819,6 +826,9 @@ export async function createMarAgent(options) {
819
826
  content: '',
820
827
  provider: event.provider,
821
828
  providerModelId: selectedModel.id,
829
+ ...(event.provider === 'ANTHROPIC_MESSAGES'
830
+ ? { contextKind: 'private' }
831
+ : {}),
822
832
  item: event.item
823
833
  };
824
834
  messages.push(message);
@@ -991,6 +1001,17 @@ export async function createMarAgent(options) {
991
1001
  };
992
1002
  else if (selectedModel.responsesPreviousResponseId)
993
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
+ });
994
1015
  if (pendingTools.length === 0 && mailbox.length > 0) {
995
1016
  await appendMailboxMessages();
996
1017
  finalAnswer = '';
@@ -1270,7 +1291,16 @@ export async function createMarAgent(options) {
1270
1291
  await selectedTurn?.close().catch(() => undefined);
1271
1292
  if (titleAdapter !== undefined && titleAdapter !== selectedAdapter)
1272
1293
  executionScopedAdapters.add(titleAdapter);
1273
- await closeModelAdapters(executionScopedAdapters).catch(() => undefined);
1294
+ try {
1295
+ await closeModelAdapters(executionScopedAdapters);
1296
+ }
1297
+ catch {
1298
+ // Execution cleanup is best effort.
1299
+ }
1300
+ finally {
1301
+ for (const adapter of executionScopedAdapters)
1302
+ activeExecutionAdapters.delete(adapter);
1303
+ }
1274
1304
  subagents?.endParentExecution(executionId);
1275
1305
  await releaseSession?.();
1276
1306
  active.delete(`${sessionId}:${executionId}`);
@@ -1346,30 +1376,60 @@ export async function createMarAgent(options) {
1346
1376
  const activeSessions = activeSessionIds();
1347
1377
  if ([...activeSessions].some((sessionId) => sessionRuntimes.get(sessionId)?.sourceType !== 'subagent'))
1348
1378
  throw new MarAgentError('MAR_AGENT_EXECUTION_ACTIVE', 'Models cannot be reconfigured while a main execution is active.');
1349
- const activeAdapters = new Set();
1379
+ const activeAdapters = new Set(activeExecutionAdapters);
1350
1380
  for (const [sessionId, runtime] of sessionRuntimes)
1351
1381
  if (activeSessions.has(sessionId))
1352
1382
  for (const adapter of runtime.adapters.values())
1353
1383
  activeAdapters.add(adapter);
1354
1384
  const inactiveRuntimes = [...sessionRuntimes].flatMap(([sessionId, runtime]) => activeSessions.has(sessionId) ? [] : [runtime]);
1355
- const adapters = inactiveRuntimes
1356
- .flatMap((runtime) => [...runtime.adapters.values()])
1357
- .filter((adapter) => !activeAdapters.has(adapter));
1358
1385
  reconfiguring = true;
1359
1386
  try {
1387
+ const reconfigurationCommits = new Map();
1388
+ for (const runtime of inactiveRuntimes)
1389
+ for (const [modelId, adapter] of runtime.adapters) {
1390
+ if (activeAdapters.has(adapter))
1391
+ continue;
1392
+ const configuration = next.models.get(modelId);
1393
+ const commit = configuration === undefined ? undefined : adapter.prepareReconfigure?.(configuration);
1394
+ if (commit !== undefined)
1395
+ reconfigurationCommits.set(adapter, commit);
1396
+ }
1397
+ const retainedAdapters = new Set(reconfigurationCommits.keys());
1398
+ const adapters = [
1399
+ ...new Set(inactiveRuntimes.flatMap((runtime) => [...runtime.adapters.values()]))
1400
+ ].filter((adapter) => !activeAdapters.has(adapter) && !retainedAdapters.has(adapter));
1401
+ for (const runtime of inactiveRuntimes) {
1402
+ for (const [modelId, adapter] of runtime.adapters)
1403
+ if (!retainedAdapters.has(adapter))
1404
+ runtime.adapters.delete(modelId);
1405
+ if (runtime.responsesChain !== undefined &&
1406
+ !retainedAdapters.has(runtime.adapters.get(runtime.responsesChain.modelId)))
1407
+ runtime.responsesChain = undefined;
1408
+ }
1360
1409
  await closeModelAdapters(adapters);
1361
1410
  if (disposed)
1362
1411
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Agent is disposed.');
1412
+ for (const commit of reconfigurationCommits.values())
1413
+ commit();
1363
1414
  models = next.models;
1364
1415
  defaultModelId = next.defaultModelId;
1365
1416
  modelGeneration++;
1366
1417
  for (const provider of next.credentialProviders)
1367
1418
  credentialProviders.add(provider);
1368
1419
  for (const runtime of inactiveRuntimes) {
1369
- runtime.responsesChain = undefined;
1370
- runtime.adapters.clear();
1371
1420
  runtime.adapterGeneration = modelGeneration;
1372
1421
  }
1422
+ const liveProviders = new Set(next.credentialProviders);
1423
+ for (const adapter of activeExecutionAdapters)
1424
+ addModelCredentialProviders(liveProviders, adapter.configuration);
1425
+ for (const runtime of sessionRuntimes.values())
1426
+ for (const adapter of runtime.adapters.values())
1427
+ addModelCredentialProviders(liveProviders, adapter.configuration);
1428
+ const obsoleteProviders = [...credentialProviders].filter((provider) => !liveProviders.has(provider));
1429
+ const providerCloseResults = await Promise.allSettled(obsoleteProviders.map((provider) => provider.close()));
1430
+ for (const [index, result] of providerCloseResults.entries())
1431
+ if (result.status === 'fulfilled')
1432
+ credentialProviders.delete(obsoleteProviders[index]);
1373
1433
  }
1374
1434
  finally {
1375
1435
  reconfiguring = false;
@@ -1512,9 +1572,45 @@ function createModelTurnSession(adapter) {
1512
1572
  }
1513
1573
  },
1514
1574
  resetContinuation: () => turn.resetContinuation?.(),
1575
+ continuationCheckpoint: () => turn.continuationCheckpoint?.(),
1515
1576
  close: () => turn.close()
1516
1577
  };
1517
1578
  }
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
+ }
1518
1614
  function modelAttemptFailureDiagnostic(diagnostic, summary) {
1519
1615
  if (summary === undefined)
1520
1616
  return diagnostic;
@@ -1526,7 +1622,16 @@ function modelAttemptFailureDiagnostic(diagnostic, summary) {
1526
1622
  };
1527
1623
  }
1528
1624
  async function closeModelAdapters(adapters) {
1529
- await Promise.all([...new Set(adapters)].flatMap((adapter) => adapter.close === undefined ? [] : [adapter.close()]));
1625
+ const results = await Promise.allSettled([...new Set(adapters)].flatMap((adapter) => adapter.close === undefined ? [] : [adapter.close()]));
1626
+ const failure = results.find((result) => result.status === 'rejected');
1627
+ if (failure !== undefined)
1628
+ throw failure.reason;
1629
+ }
1630
+ function addModelCredentialProviders(target, configuration) {
1631
+ if (configuration.credentialProvider !== undefined)
1632
+ target.add(configuration.credentialProvider);
1633
+ if (configuration.imageGeneration !== undefined)
1634
+ target.add(configuration.imageGeneration.credentialProvider);
1530
1635
  }
1531
1636
  function truncateUtf8(value, maximumBytes) {
1532
1637
  const bytes = Buffer.from(value);
@@ -302,7 +302,7 @@ export class JsonlSessionStore {
302
302
  });
303
303
  const retained = input.boundaryTurnId === null ? [] : records.slice(1, boundaryIndex + 1);
304
304
  for (const record of retained) {
305
- if (record.type === 'context.gc.completed')
305
+ if (record.type === 'context.gc.completed' || record.type === 'responses.continuation')
306
306
  continue;
307
307
  await this.append(target.id, {
308
308
  type: record.type,
@@ -368,7 +368,9 @@ export class JsonlSessionStore {
368
368
  : records.findLastIndex((record) => record.turnId === input.boundaryTurnId);
369
369
  if (input.boundaryTurnId !== null && boundaryIndex < 0)
370
370
  throw new MarAgentError('MAR_AGENT_ROLLBACK_TARGET_INVALID', 'Session rollback target was not found.');
371
- const retained = records.slice(0, boundaryIndex + 1);
371
+ const retained = records
372
+ .slice(0, boundaryIndex + 1)
373
+ .filter((record) => record.type !== 'responses.continuation');
372
374
  const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
373
375
  const transactionId = randomUUID();
374
376
  let generatedTrash = [];
@@ -33,7 +33,10 @@ function parseAgentMessageInput(value) {
33
33
  return invalid();
34
34
  const input = value;
35
35
  if (Object.keys(input).some((key) => !['agentId', 'message', 'delivery'].includes(key)) ||
36
- (input.delivery !== undefined && input.delivery !== 'append' && input.delivery !== 'replace') ||
36
+ (input.delivery !== undefined &&
37
+ input.delivery !== null &&
38
+ input.delivery !== 'append' &&
39
+ input.delivery !== 'replace') ||
37
40
  typeof input.agentId !== 'string' ||
38
41
  input.agentId.trim().length === 0 ||
39
42
  typeof input.message !== 'string' ||
@@ -43,7 +46,9 @@ function parseAgentMessageInput(value) {
43
46
  return {
44
47
  agentId: input.agentId.trim(),
45
48
  message: input.message,
46
- ...(input.delivery === undefined ? {} : { delivery: input.delivery })
49
+ ...(input.delivery === 'append' || input.delivery === 'replace'
50
+ ? { delivery: input.delivery }
51
+ : {})
47
52
  };
48
53
  }
49
54
  function invalid() {
@@ -62,6 +62,7 @@ function parseAgentStartInput(value) {
62
62
  input.modelId !== null &&
63
63
  (typeof input.modelId !== 'string' || input.modelId.trim().length === 0)) ||
64
64
  (input.reasoningEffort !== undefined &&
65
+ input.reasoningEffort !== null &&
65
66
  (typeof input.reasoningEffort !== 'string' ||
66
67
  !marAgentReasoningEffortSchema.safeParse(input.reasoningEffort.trim()).success)) ||
67
68
  (input.background !== undefined &&
@@ -46,11 +46,14 @@ function parseAgentWaitInput(value) {
46
46
  const input = value;
47
47
  if (Object.keys(input).some((key) => !['agentId', 'waitFor', 'waitMs'].includes(key)) ||
48
48
  (input.agentId !== undefined &&
49
+ input.agentId !== null &&
49
50
  (typeof input.agentId !== 'string' || input.agentId.trim().length === 0)) ||
50
51
  (input.waitFor !== undefined &&
52
+ input.waitFor !== null &&
51
53
  input.waitFor !== 'completion' &&
52
54
  input.waitFor !== 'message') ||
53
55
  (input.waitMs !== undefined &&
56
+ input.waitMs !== null &&
54
57
  (!Number.isInteger(input.waitMs) ||
55
58
  input.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
56
59
  input.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.83",
3
+ "version": "0.9.85",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -42,6 +42,7 @@
42
42
  "build": "node ../scripts/clean-build-output.mjs dist && tsc -p tsconfig.json",
43
43
  "typecheck": "tsc -p tsconfig.json --noEmit",
44
44
  "test:unit": "vitest run --config vitest.config.ts test/unit",
45
+ "analyze:session": "node test/tools/session-analyzer.mjs",
45
46
  "benchmark:compaction": "node test/e2e/compaction-benchmark.mjs",
46
47
  "test:performance": "vitest run --config vitest.performance.config.ts",
47
48
  "test:memory": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.memory.config.ts",