@myagentroam/agent 0.9.86 → 0.9.88
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,
|
|
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,10 @@ 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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
state.lastResponse = { responseId: restored.responseId, outputItems: [] };
|
|
895
|
-
return current.input.slice(restored.inputPrefixLength);
|
|
896
|
-
}
|
|
898
|
+
if (previous === undefined || completion === undefined)
|
|
899
|
+
return undefined;
|
|
900
|
+
if (!responsesContinuationPropertiesMatch(previous, current))
|
|
901
|
+
return undefined;
|
|
897
902
|
const baseline = [...previous.input, ...completion.outputItems];
|
|
898
903
|
if (current.input.length < baseline.length)
|
|
899
904
|
return undefined;
|
|
@@ -902,28 +907,33 @@ function incrementalResponsesInput(state, current) {
|
|
|
902
907
|
return undefined;
|
|
903
908
|
return current.input.slice(baseline.length);
|
|
904
909
|
}
|
|
910
|
+
function responsesContinuationPropertiesMatch(previous, current) {
|
|
911
|
+
const properties = [
|
|
912
|
+
'model',
|
|
913
|
+
'instructions',
|
|
914
|
+
'tools',
|
|
915
|
+
'tool_choice',
|
|
916
|
+
'parallel_tool_calls',
|
|
917
|
+
'reasoning',
|
|
918
|
+
'store',
|
|
919
|
+
'stream',
|
|
920
|
+
'include',
|
|
921
|
+
'prompt_cache_key',
|
|
922
|
+
'text'
|
|
923
|
+
];
|
|
924
|
+
return properties.every((property) => isDeepStrictEqual(previous[property], current[property]));
|
|
925
|
+
}
|
|
905
926
|
function clearResponsesContinuation(state) {
|
|
906
927
|
delete state.lastRequest;
|
|
907
928
|
delete state.lastResponse;
|
|
908
|
-
delete state.restoredContinuation;
|
|
909
929
|
}
|
|
910
930
|
function hashResponsesValue(value) {
|
|
911
931
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
912
932
|
}
|
|
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
933
|
function responsesContinuationEnabled(configuration) {
|
|
921
934
|
return (configuration.responsesPreviousResponseId === true &&
|
|
922
935
|
configuration.responsesTransport?.transport === 'WEBSOCKET');
|
|
923
936
|
}
|
|
924
|
-
function responsesPersistedContinuationEnabled(configuration) {
|
|
925
|
-
return responsesContinuationEnabled(configuration) && configuration.responsesEncoding !== 'LITE';
|
|
926
|
-
}
|
|
927
937
|
function responsesCredentialFingerprint(credential) {
|
|
928
938
|
return hashResponsesValue({
|
|
929
939
|
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
|
|
258
|
-
restoredContextualWorldState.generation
|
|
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,
|
|
@@ -546,9 +549,7 @@ export async function createMarAgent(options) {
|
|
|
546
549
|
if (mode === 'compact') {
|
|
547
550
|
const compactSystemPrompt = initialSystemPrompt;
|
|
548
551
|
const compactHistory = [...messages];
|
|
549
|
-
const projectedHistory = normalizeMessagesForModel(insertContextualWorldState(compactHistory, contextualWorldState.entries, {
|
|
550
|
-
excludeResources: true
|
|
551
|
-
}), { supportsImages: false });
|
|
552
|
+
const projectedHistory = normalizeMessagesForModel(insertContextualWorldState(compactHistory, contextualWorldState.entries), { supportsImages: false });
|
|
552
553
|
const nativeMessages = buildNativeCompactionMessages(projectedHistory, {
|
|
553
554
|
focus: input.prompt,
|
|
554
555
|
highDensityCompaction: selectedModel.highDensityCompaction
|
|
@@ -604,9 +605,7 @@ export async function createMarAgent(options) {
|
|
|
604
605
|
let compactSummary = '';
|
|
605
606
|
const compactSystemPrompt = buildExecutionSystemPrompt(executionTools, 'run');
|
|
606
607
|
const compactHistory = retainCurrent ? messages.slice(0, -1) : messages;
|
|
607
|
-
const projectedHistory = normalizeMessagesForModel(insertContextualWorldState(compactHistory, contextualWorldState.entries, {
|
|
608
|
-
excludeResources: true
|
|
609
|
-
}), { supportsImages: false });
|
|
608
|
+
const projectedHistory = normalizeMessagesForModel(insertContextualWorldState(compactHistory, contextualWorldState.entries), { supportsImages: false });
|
|
610
609
|
const nativeCompactMessages = buildNativeCompactionMessages(projectedHistory, {
|
|
611
610
|
highDensityCompaction: selectedModel.highDensityCompaction
|
|
612
611
|
});
|
|
@@ -659,14 +658,12 @@ export async function createMarAgent(options) {
|
|
|
659
658
|
}
|
|
660
659
|
catch (error) {
|
|
661
660
|
if (await recoverModelStream(error, compactionRetryState, operation.signal)) {
|
|
662
|
-
sessionRuntime.responsesChain = undefined;
|
|
663
661
|
continue;
|
|
664
662
|
}
|
|
665
663
|
if (isProviderContextOverflow(error) && usingNativeMessages) {
|
|
666
664
|
usingNativeMessages = false;
|
|
667
665
|
compactMessages = fallbackMessages();
|
|
668
666
|
compactionRetryState.attempts = 0;
|
|
669
|
-
sessionRuntime.responsesChain = undefined;
|
|
670
667
|
continue;
|
|
671
668
|
}
|
|
672
669
|
if (isProviderContextOverflow(error))
|
|
@@ -707,7 +704,7 @@ export async function createMarAgent(options) {
|
|
|
707
704
|
previousMayContainResources: compactSummary.includes('# Retained Session resources')
|
|
708
705
|
});
|
|
709
706
|
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
710
|
-
|
|
707
|
+
await persistContextualWorldState(store, sessionId, contextualWorldState);
|
|
711
708
|
lastServerContextTokens = undefined;
|
|
712
709
|
lastServerUsage = undefined;
|
|
713
710
|
await emit({ type: 'context.compacted', compactionId });
|
|
@@ -731,7 +728,6 @@ export async function createMarAgent(options) {
|
|
|
731
728
|
await appendMailboxMessages();
|
|
732
729
|
let stop;
|
|
733
730
|
let sawTool = false;
|
|
734
|
-
let responseId;
|
|
735
731
|
let latestUsage;
|
|
736
732
|
const pendingTools = [];
|
|
737
733
|
let incompleteToolCall = false;
|
|
@@ -741,27 +737,9 @@ export async function createMarAgent(options) {
|
|
|
741
737
|
const systemPrompt = mode === 'compact'
|
|
742
738
|
? buildExecutionSystemPrompt(roundTools, 'run')
|
|
743
739
|
: 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
740
|
const requestContextMessages = pendingOutputContinuation === undefined
|
|
754
741
|
? messages
|
|
755
742
|
: [...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
743
|
const modelMessages = mode === 'compact'
|
|
766
744
|
? normalizeMessagesForModel(requestContextMessages, { supportsImages: false })
|
|
767
745
|
: normalizeMessagesForModel(insertContextualWorldState(requestContextMessages, contextualWorldState.entries), {
|
|
@@ -808,16 +786,13 @@ export async function createMarAgent(options) {
|
|
|
808
786
|
? {}
|
|
809
787
|
: { allowTools: false }),
|
|
810
788
|
promptCacheKey: sessionId,
|
|
811
|
-
...(continuation ? { continuation } : {}),
|
|
812
789
|
reasoningEffort,
|
|
813
790
|
...(modelInputImages?.length ? { images: modelInputImages } : {}),
|
|
814
791
|
onAttemptDiagnostic: recordModelAttempt(mode === 'compact' ? 'COMPACTION' : 'EXECUTION')
|
|
815
792
|
})) {
|
|
816
793
|
if (event.type !== 'response.started' && event.type !== 'usage')
|
|
817
794
|
sawModelSemanticEvent = true;
|
|
818
|
-
if (event.type === 'response.
|
|
819
|
-
responseId = event.responseId;
|
|
820
|
-
else if (event.type === 'response.item') {
|
|
795
|
+
if (event.type === 'response.item') {
|
|
821
796
|
// Compaction keeps only its successful checkpoint, never intermediate native output.
|
|
822
797
|
if (mode === 'compact')
|
|
823
798
|
continue;
|
|
@@ -955,8 +930,6 @@ export async function createMarAgent(options) {
|
|
|
955
930
|
catch (error) {
|
|
956
931
|
planOutputPrefixes.clear();
|
|
957
932
|
if (await recoverModelStream(error, modelRetryState, manualCompaction?.signal ?? controller.signal)) {
|
|
958
|
-
sessionRuntime.responsesChain = undefined;
|
|
959
|
-
responseId = undefined;
|
|
960
933
|
finalAnswer = '';
|
|
961
934
|
if (mode === 'compact')
|
|
962
935
|
messages.splice(roundMessageCount);
|
|
@@ -971,16 +944,13 @@ export async function createMarAgent(options) {
|
|
|
971
944
|
!sawModelSemanticEvent &&
|
|
972
945
|
isProviderContextOverflow(error) &&
|
|
973
946
|
useManualCompactionFallback?.()) {
|
|
974
|
-
sessionRuntime.responsesChain = undefined;
|
|
975
947
|
modelRetryState.attempts = 0;
|
|
976
|
-
responseId = undefined;
|
|
977
948
|
finalAnswer = '';
|
|
978
949
|
continue;
|
|
979
950
|
}
|
|
980
951
|
else if (mode !== 'compact' &&
|
|
981
952
|
!sawModelSemanticEvent &&
|
|
982
953
|
isProviderContextOverflow(error)) {
|
|
983
|
-
sessionRuntime.responsesChain = undefined;
|
|
984
954
|
if (!recoveringFromContextOverflow && (await compactContext())) {
|
|
985
955
|
modelRetryState.attempts = 0;
|
|
986
956
|
recoveringFromContextOverflow = true;
|
|
@@ -992,26 +962,6 @@ export async function createMarAgent(options) {
|
|
|
992
962
|
throw error;
|
|
993
963
|
}
|
|
994
964
|
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
965
|
if (pendingTools.length === 0 && mailbox.length > 0) {
|
|
1016
966
|
await appendMailboxMessages();
|
|
1017
967
|
finalAnswer = '';
|
|
@@ -1186,7 +1136,6 @@ export async function createMarAgent(options) {
|
|
|
1186
1136
|
}
|
|
1187
1137
|
});
|
|
1188
1138
|
if (committed) {
|
|
1189
|
-
sessionRuntime.responsesChain = undefined;
|
|
1190
1139
|
selected.resetContinuation?.();
|
|
1191
1140
|
const refreshed = await restoreMessages(store, sessionId, committed.records, {
|
|
1192
1141
|
includeImages: true,
|
|
@@ -1208,6 +1157,7 @@ export async function createMarAgent(options) {
|
|
|
1208
1157
|
previousMayContainResources: compactHistoryMayContainRetainedResources(committed.records)
|
|
1209
1158
|
});
|
|
1210
1159
|
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
1160
|
+
await persistContextualWorldState(store, sessionId, contextualWorldState);
|
|
1211
1161
|
}
|
|
1212
1162
|
}
|
|
1213
1163
|
if (controller.signal.aborted) {
|
|
@@ -1402,9 +1352,6 @@ export async function createMarAgent(options) {
|
|
|
1402
1352
|
for (const [modelId, adapter] of runtime.adapters)
|
|
1403
1353
|
if (!retainedAdapters.has(adapter))
|
|
1404
1354
|
runtime.adapters.delete(modelId);
|
|
1405
|
-
if (runtime.responsesChain !== undefined &&
|
|
1406
|
-
!retainedAdapters.has(runtime.adapters.get(runtime.responsesChain.modelId)))
|
|
1407
|
-
runtime.responsesChain = undefined;
|
|
1408
1355
|
}
|
|
1409
1356
|
await closeModelAdapters(adapters);
|
|
1410
1357
|
if (disposed)
|
|
@@ -1572,44 +1519,9 @@ function createModelTurnSession(adapter) {
|
|
|
1572
1519
|
}
|
|
1573
1520
|
},
|
|
1574
1521
|
resetContinuation: () => turn.resetContinuation?.(),
|
|
1575
|
-
continuationCheckpoint: () => turn.continuationCheckpoint?.(),
|
|
1576
1522
|
close: () => turn.close()
|
|
1577
1523
|
};
|
|
1578
1524
|
}
|
|
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
1525
|
function modelAttemptFailureDiagnostic(diagnostic, summary) {
|
|
1614
1526
|
if (summary === undefined)
|
|
1615
1527
|
return diagnostic;
|
|
@@ -1886,12 +1798,10 @@ function modelIdFromPayload(payload, key = 'modelId') {
|
|
|
1886
1798
|
const value = payload[key];
|
|
1887
1799
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
1888
1800
|
}
|
|
1889
|
-
function insertContextualWorldState(messages, entries
|
|
1801
|
+
function insertContextualWorldState(messages, entries) {
|
|
1890
1802
|
const result = [...messages];
|
|
1891
1803
|
let offset = 0;
|
|
1892
1804
|
for (const entry of entries) {
|
|
1893
|
-
if (options.excludeResources && entry.kind === 'resources')
|
|
1894
|
-
continue;
|
|
1895
1805
|
const insertionIndex = Math.max(0, Math.min(entry.index + offset, result.length));
|
|
1896
1806
|
result.splice(insertionIndex, 0, { role: 'user', content: entry.content });
|
|
1897
1807
|
offset++;
|
|
@@ -1906,6 +1816,73 @@ function latestContextualWorldStateGeneration(records) {
|
|
|
1906
1816
|
generation = record.recordId;
|
|
1907
1817
|
return generation;
|
|
1908
1818
|
}
|
|
1819
|
+
const MAX_CONTEXTUAL_WORLD_STATE_ENTRIES = 1_024;
|
|
1820
|
+
const MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES = 8 * 1024 * 1024;
|
|
1821
|
+
function parsePersistedContextualWorldState(payload) {
|
|
1822
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
|
|
1823
|
+
return undefined;
|
|
1824
|
+
const record = payload;
|
|
1825
|
+
if (record.version !== 1)
|
|
1826
|
+
return undefined;
|
|
1827
|
+
const generation = record.generation === null
|
|
1828
|
+
? undefined
|
|
1829
|
+
: typeof record.generation === 'string' && record.generation.length <= 128
|
|
1830
|
+
? record.generation
|
|
1831
|
+
: null;
|
|
1832
|
+
if (generation === null)
|
|
1833
|
+
return undefined;
|
|
1834
|
+
const instructionSnapshot = nullableWorldStateString(record.instructionSnapshot);
|
|
1835
|
+
const resourceSnapshot = nullableWorldStateString(record.resourceSnapshot);
|
|
1836
|
+
if (instructionSnapshot === null || resourceSnapshot === null || !Array.isArray(record.entries))
|
|
1837
|
+
return undefined;
|
|
1838
|
+
if (record.entries.length > MAX_CONTEXTUAL_WORLD_STATE_ENTRIES)
|
|
1839
|
+
return undefined;
|
|
1840
|
+
const entries = [];
|
|
1841
|
+
let contentBytes = 0;
|
|
1842
|
+
for (const value of record.entries) {
|
|
1843
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
1844
|
+
return undefined;
|
|
1845
|
+
const entry = value;
|
|
1846
|
+
if (!Number.isSafeInteger(entry.index) ||
|
|
1847
|
+
entry.index < 0 ||
|
|
1848
|
+
(entry.kind !== 'instructions' && entry.kind !== 'resources') ||
|
|
1849
|
+
typeof entry.content !== 'string' ||
|
|
1850
|
+
entry.content.length === 0)
|
|
1851
|
+
return undefined;
|
|
1852
|
+
contentBytes += Buffer.byteLength(entry.content);
|
|
1853
|
+
if (contentBytes > MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES)
|
|
1854
|
+
return undefined;
|
|
1855
|
+
entries.push({
|
|
1856
|
+
index: entry.index,
|
|
1857
|
+
kind: entry.kind,
|
|
1858
|
+
content: entry.content
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
return {
|
|
1862
|
+
generation,
|
|
1863
|
+
initialized: true,
|
|
1864
|
+
instructionSnapshot,
|
|
1865
|
+
resourceSnapshot,
|
|
1866
|
+
entries
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
function nullableWorldStateString(value) {
|
|
1870
|
+
if (value === null)
|
|
1871
|
+
return undefined;
|
|
1872
|
+
return typeof value === 'string' &&
|
|
1873
|
+
Buffer.byteLength(value) <= MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES
|
|
1874
|
+
? value
|
|
1875
|
+
: null;
|
|
1876
|
+
}
|
|
1877
|
+
async function persistContextualWorldState(store, sessionId, state) {
|
|
1878
|
+
await store.writeContextualWorldState(sessionId, {
|
|
1879
|
+
version: 1,
|
|
1880
|
+
generation: state.generation ?? null,
|
|
1881
|
+
instructionSnapshot: state.instructionSnapshot ?? null,
|
|
1882
|
+
resourceSnapshot: state.resourceSnapshot ?? null,
|
|
1883
|
+
entries: state.entries
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1909
1886
|
function compactHistoryMayContainRetainedResources(records) {
|
|
1910
1887
|
return records.some((record) => compactSummaryFromCheckpoint(record)?.includes('# Retained Session resources'));
|
|
1911
1888
|
}
|
|
@@ -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 =
|
|
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) {
|