@myagentroam/agent 0.9.85 → 0.9.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/model/contracts.d.ts +0 -14
- package/dist/model/openai-responses.d.ts +1 -4
- package/dist/model/openai-responses.js +53 -51
- package/dist/runtime/context-gc.js +3 -1
- package/dist/sdk/agent.js +84 -102
- package/dist/session/context-gc-replacements.js +5 -1
- package/dist/session/jsonl-store.d.ts +2 -0
- package/dist/session/jsonl-store.js +120 -1
- package/dist/tools/exec.d.ts +1 -1
- package/dist/tools/exec.js +63 -37
- package/package.json +1 -1
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import type { MarAgentModelConfiguration, MarAgentReasoningEffort, ModelCredentialAcquireReason } from './configuration.js';
|
|
2
|
-
export interface ModelContinuationCheckpoint {
|
|
3
|
-
readonly version: 2;
|
|
4
|
-
readonly protocol: 'OPENAI_RESPONSES';
|
|
5
|
-
readonly configurationHash: string;
|
|
6
|
-
readonly responseId: string;
|
|
7
|
-
readonly inputPrefixLength: number;
|
|
8
|
-
readonly inputPrefixHash: string;
|
|
9
|
-
}
|
|
10
2
|
export interface ClientToolDefinition {
|
|
11
3
|
name: string;
|
|
12
4
|
description: string;
|
|
@@ -76,10 +68,6 @@ export interface ModelRequest {
|
|
|
76
68
|
mimeType: string;
|
|
77
69
|
dataBase64: string;
|
|
78
70
|
}[];
|
|
79
|
-
continuation?: {
|
|
80
|
-
previousResponseId: string;
|
|
81
|
-
deltaMessages: readonly ModelMessage[];
|
|
82
|
-
};
|
|
83
71
|
onAttemptDiagnostic?: (diagnostic: ModelAttemptDiagnostic) => Promise<void> | void;
|
|
84
72
|
}
|
|
85
73
|
export interface ModelUsage {
|
|
@@ -159,13 +147,11 @@ export interface ModelAdapter {
|
|
|
159
147
|
* The returned commit callback must be synchronous and non-throwing.
|
|
160
148
|
*/
|
|
161
149
|
prepareReconfigure?(configuration: MarAgentModelConfiguration): (() => void) | undefined;
|
|
162
|
-
restoreContinuation?(checkpoint: ModelContinuationCheckpoint): boolean;
|
|
163
150
|
close?(): Promise<void>;
|
|
164
151
|
}
|
|
165
152
|
export interface ModelTurnSession {
|
|
166
153
|
start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
|
|
167
154
|
/** Drops incremental response state after a local context replacement. */
|
|
168
155
|
resetContinuation?(): void;
|
|
169
|
-
continuationCheckpoint?(): ModelContinuationCheckpoint | undefined;
|
|
170
156
|
close(): Promise<void>;
|
|
171
157
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MarAgentError } from '../error.js';
|
|
2
2
|
import { type MarAgentModelConfiguration, type ModelCredentialAcquireReason } from './configuration.js';
|
|
3
3
|
import WebSocket from 'ws';
|
|
4
|
-
import type { ModelAdapter, ModelAttemptDiagnostic,
|
|
4
|
+
import type { ModelAdapter, ModelAttemptDiagnostic, ModelEvent, ModelRequest, ModelTurnSession } from './contracts.js';
|
|
5
5
|
export declare const OPENAI_RESPONSES_EARLY_EOF_REASON = "OPENAI_RESPONSES_EARLY_EOF";
|
|
6
6
|
export declare const OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT_REASON = "OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT";
|
|
7
7
|
type ResponsesRequestBody = Readonly<Record<string, unknown>> & {
|
|
@@ -19,7 +19,6 @@ interface ResponsesSessionState {
|
|
|
19
19
|
readonly responseId: string;
|
|
20
20
|
readonly outputItems: readonly Record<string, unknown>[];
|
|
21
21
|
};
|
|
22
|
-
restoredContinuation?: ModelContinuationCheckpoint;
|
|
23
22
|
}
|
|
24
23
|
interface PreparedResponsesRequest {
|
|
25
24
|
readonly fullBody: ResponsesRequestBody;
|
|
@@ -32,7 +31,6 @@ export declare class OpenAiResponsesAdapter implements ModelAdapter {
|
|
|
32
31
|
constructor(configuration: MarAgentModelConfiguration);
|
|
33
32
|
get configuration(): MarAgentModelConfiguration;
|
|
34
33
|
prepareReconfigure(configuration: MarAgentModelConfiguration): (() => void) | undefined;
|
|
35
|
-
restoreContinuation(checkpoint: ModelContinuationCheckpoint): boolean;
|
|
36
34
|
start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
|
|
37
35
|
createTurnSession(): ModelTurnSession;
|
|
38
36
|
close(): Promise<void>;
|
|
@@ -49,7 +47,6 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
|
|
|
49
47
|
start(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;
|
|
50
48
|
close(): Promise<void>;
|
|
51
49
|
resetContinuation(): void;
|
|
52
|
-
continuationCheckpoint(): ModelContinuationCheckpoint | undefined;
|
|
53
50
|
prepareRequest(request: ModelRequest): PreparedResponsesRequest;
|
|
54
51
|
fullRequestBody(): ResponsesRequestBody;
|
|
55
52
|
currentTurnState(): string | undefined;
|
|
@@ -31,18 +31,6 @@ export class OpenAiResponsesAdapter {
|
|
|
31
31
|
this.#configuration = configuration;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
restoreContinuation(checkpoint) {
|
|
35
|
-
if (this.#cacheLeased ||
|
|
36
|
-
this.#cachedState.lastRequest !== undefined ||
|
|
37
|
-
this.#cachedState.lastResponse !== undefined ||
|
|
38
|
-
!responsesContinuationEnabled(this.#configuration) ||
|
|
39
|
-
checkpoint.version !== 2 ||
|
|
40
|
-
checkpoint.protocol !== 'OPENAI_RESPONSES' ||
|
|
41
|
-
checkpoint.configurationHash !== responsesContinuationIdentityHash(this.#configuration))
|
|
42
|
-
return false;
|
|
43
|
-
this.#cachedState.restoredContinuation = checkpoint;
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
34
|
async *start(request, signal) {
|
|
47
35
|
const turn = new OpenAiResponsesTurnSession(this, {}, false);
|
|
48
36
|
try {
|
|
@@ -74,8 +62,10 @@ export class OpenAiResponsesAdapter {
|
|
|
74
62
|
invalidateResponsesWebSocket(state, true);
|
|
75
63
|
return;
|
|
76
64
|
}
|
|
77
|
-
if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN)
|
|
65
|
+
if (state.socket !== undefined && state.socket.readyState !== WebSocket.OPEN) {
|
|
78
66
|
invalidateResponsesWebSocket(state, false);
|
|
67
|
+
clearResponsesContinuation(state);
|
|
68
|
+
}
|
|
79
69
|
this.#cachedState = state;
|
|
80
70
|
this.#cacheLeased = false;
|
|
81
71
|
}
|
|
@@ -155,6 +145,24 @@ export class OpenAiResponsesAdapter {
|
|
|
155
145
|
...(options.retryDelayMs === undefined ? {} : { retryDelayMs: options.retryDelayMs }),
|
|
156
146
|
...(options.retryReason === undefined ? {} : { retryReason: options.retryReason })
|
|
157
147
|
});
|
|
148
|
+
if (!signal.aborted &&
|
|
149
|
+
!sawSemanticEvent &&
|
|
150
|
+
useContinuation &&
|
|
151
|
+
!invalidContinuationFallbackUsed &&
|
|
152
|
+
isLocalContinuationReset(error)) {
|
|
153
|
+
invalidContinuationFallbackUsed = true;
|
|
154
|
+
useContinuation = false;
|
|
155
|
+
requestBody = prepared.fullBody;
|
|
156
|
+
turn.resetContinuation();
|
|
157
|
+
turn.invalidateWebSocket();
|
|
158
|
+
retryState.attempts = Math.max(0, retryState.attempts - 1);
|
|
159
|
+
credentialReason = 'RECONNECT';
|
|
160
|
+
await reportFailure(true, {
|
|
161
|
+
retryDelayMs: 0,
|
|
162
|
+
retryReason: 'INVALID_CONTINUATION'
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
158
166
|
if (!signal.aborted &&
|
|
159
167
|
attemptNumber < maxAttempts &&
|
|
160
168
|
!sawSemanticEvent &&
|
|
@@ -164,6 +172,7 @@ export class OpenAiResponsesAdapter {
|
|
|
164
172
|
invalidContinuationFallbackUsed = true;
|
|
165
173
|
useContinuation = false;
|
|
166
174
|
requestBody = prepared.fullBody;
|
|
175
|
+
turn.resetContinuation();
|
|
167
176
|
turn.invalidateWebSocket();
|
|
168
177
|
credentialReason = 'RECONNECT';
|
|
169
178
|
await reportFailure(true, {
|
|
@@ -632,7 +641,6 @@ class OpenAiResponsesTurnSession {
|
|
|
632
641
|
(outputItems.length > 0 || !sawUnrepresentedOutput)) {
|
|
633
642
|
this.state.lastRequest = prepared.fullBody;
|
|
634
643
|
this.state.lastResponse = { responseId, outputItems };
|
|
635
|
-
delete this.state.restoredContinuation;
|
|
636
644
|
}
|
|
637
645
|
else
|
|
638
646
|
clearResponsesContinuation(this.state);
|
|
@@ -655,28 +663,13 @@ class OpenAiResponsesTurnSession {
|
|
|
655
663
|
resetContinuation() {
|
|
656
664
|
clearResponsesContinuation(this.state);
|
|
657
665
|
}
|
|
658
|
-
continuationCheckpoint() {
|
|
659
|
-
if (!responsesContinuationEnabled(this.adapter.configuration))
|
|
660
|
-
return undefined;
|
|
661
|
-
const request = this.state.lastRequest;
|
|
662
|
-
const response = this.state.lastResponse;
|
|
663
|
-
if (request === undefined || response === undefined)
|
|
664
|
-
return this.state.restoredContinuation;
|
|
665
|
-
const baseline = [...request.input, ...response.outputItems];
|
|
666
|
-
return {
|
|
667
|
-
version: 2,
|
|
668
|
-
protocol: 'OPENAI_RESPONSES',
|
|
669
|
-
configurationHash: responsesContinuationIdentityHash(this.adapter.configuration),
|
|
670
|
-
responseId: response.responseId,
|
|
671
|
-
inputPrefixLength: baseline.length,
|
|
672
|
-
inputPrefixHash: hashResponsesValue(baseline)
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
666
|
prepareRequest(request) {
|
|
676
667
|
if (this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
|
|
677
668
|
this.state.socket !== undefined &&
|
|
678
|
-
this.state.socket.readyState !== WebSocket.OPEN)
|
|
669
|
+
this.state.socket.readyState !== WebSocket.OPEN) {
|
|
679
670
|
invalidateResponsesWebSocket(this.state, false);
|
|
671
|
+
clearResponsesContinuation(this.state);
|
|
672
|
+
}
|
|
680
673
|
const fullBody = responsesRequestBody(this.adapter.configuration, request, request.messages, this.adapter.prefixIdentity);
|
|
681
674
|
if (responsesContinuationEnabled(this.adapter.configuration)) {
|
|
682
675
|
const incremental = incrementalResponsesInput(this.state, fullBody);
|
|
@@ -739,8 +732,17 @@ class OpenAiResponsesTurnSession {
|
|
|
739
732
|
const endpoint = webSocketEndpoint(input.configuration.baseUrl);
|
|
740
733
|
const existingSocket = this.state.socket;
|
|
741
734
|
if (existingSocket !== undefined &&
|
|
742
|
-
(existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint))
|
|
735
|
+
(existingSocket.readyState !== WebSocket.OPEN || this.state.endpoint !== endpoint)) {
|
|
743
736
|
invalidateResponsesWebSocket(this.state, false);
|
|
737
|
+
if (input.continuation) {
|
|
738
|
+
clearResponsesContinuation(this.state);
|
|
739
|
+
throw localContinuationReset();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (input.continuation && this.state.socket === undefined) {
|
|
743
|
+
clearResponsesContinuation(this.state);
|
|
744
|
+
throw localContinuationReset();
|
|
745
|
+
}
|
|
744
746
|
if (this.state.socket === undefined) {
|
|
745
747
|
const handshakeCredential = await acquireResponsesCredential(input.configuration, input.reason);
|
|
746
748
|
input.credentialAcquired(handshakeCredential.revision);
|
|
@@ -751,6 +753,10 @@ class OpenAiResponsesTurnSession {
|
|
|
751
753
|
if (requestCredential.revision !== this.state.credentialRevision ||
|
|
752
754
|
responsesCredentialFingerprint(requestCredential) !== this.state.credentialFingerprint) {
|
|
753
755
|
invalidateResponsesWebSocket(this.state, false);
|
|
756
|
+
if (input.continuation) {
|
|
757
|
+
clearResponsesContinuation(this.state);
|
|
758
|
+
throw localContinuationReset();
|
|
759
|
+
}
|
|
754
760
|
await this.connectWebSocket(endpoint, requestCredential, input.promptCacheKey, input.signal);
|
|
755
761
|
}
|
|
756
762
|
if (input.signal.aborted) {
|
|
@@ -810,6 +816,12 @@ class OpenAiResponsesTurnSession {
|
|
|
810
816
|
this.state.proxyType = proxyType;
|
|
811
817
|
}
|
|
812
818
|
}
|
|
819
|
+
function localContinuationReset() {
|
|
820
|
+
return new MarAgentError('MAR_AGENT_RESPONSES_CONTINUATION_RESET', 'The Responses connection changed before the incremental request was sent.');
|
|
821
|
+
}
|
|
822
|
+
function isLocalContinuationReset(error) {
|
|
823
|
+
return error instanceof MarAgentError && error.code === 'MAR_AGENT_RESPONSES_CONTINUATION_RESET';
|
|
824
|
+
}
|
|
813
825
|
function responsesRequestBody(configuration, request, messages, prefixIdentity = '') {
|
|
814
826
|
const tools = request.allowTools === false
|
|
815
827
|
? []
|
|
@@ -883,16 +895,8 @@ function uuidV5(namespace, value) {
|
|
|
883
895
|
function incrementalResponsesInput(state, current) {
|
|
884
896
|
const previous = state.lastRequest;
|
|
885
897
|
const completion = state.lastResponse;
|
|
886
|
-
if (previous === undefined || completion === undefined)
|
|
887
|
-
|
|
888
|
-
if (restored === undefined ||
|
|
889
|
-
current.input.length < restored.inputPrefixLength ||
|
|
890
|
-
restored.inputPrefixHash !==
|
|
891
|
-
hashResponsesValue(current.input.slice(0, restored.inputPrefixLength)))
|
|
892
|
-
return undefined;
|
|
893
|
-
state.lastResponse = { responseId: restored.responseId, outputItems: [] };
|
|
894
|
-
return current.input.slice(restored.inputPrefixLength);
|
|
895
|
-
}
|
|
898
|
+
if (previous === undefined || completion === undefined)
|
|
899
|
+
return undefined;
|
|
896
900
|
const baseline = [...previous.input, ...completion.outputItems];
|
|
897
901
|
if (current.input.length < baseline.length)
|
|
898
902
|
return undefined;
|
|
@@ -904,18 +908,10 @@ function incrementalResponsesInput(state, current) {
|
|
|
904
908
|
function clearResponsesContinuation(state) {
|
|
905
909
|
delete state.lastRequest;
|
|
906
910
|
delete state.lastResponse;
|
|
907
|
-
delete state.restoredContinuation;
|
|
908
911
|
}
|
|
909
912
|
function hashResponsesValue(value) {
|
|
910
913
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
911
914
|
}
|
|
912
|
-
function responsesContinuationIdentityHash(configuration) {
|
|
913
|
-
return hashResponsesValue({
|
|
914
|
-
endpoint: new URL(responsesEndpoint(configuration.baseUrl)).toString(),
|
|
915
|
-
modelId: configuration.modelId,
|
|
916
|
-
responsesEncoding: configuration.responsesEncoding ?? 'STANDARD'
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
915
|
function responsesContinuationEnabled(configuration) {
|
|
920
916
|
return (configuration.responsesPreviousResponseId === true &&
|
|
921
917
|
configuration.responsesTransport?.transport === 'WEBSOCKET');
|
|
@@ -1460,11 +1456,17 @@ function isInvalidPreviousResponseFields(code, message) {
|
|
|
1460
1456
|
normalizedCode === 'invalid_previous_response_id')
|
|
1461
1457
|
return true;
|
|
1462
1458
|
const normalizedMessage = message?.toLowerCase().replaceAll('`', '') ?? '';
|
|
1459
|
+
if ((normalizedCode === 'unsupported_parameter' ||
|
|
1460
|
+
normalizedCode === 'unsupported_previous_response_id') &&
|
|
1461
|
+
normalizedMessage.includes('previous_response_id'))
|
|
1462
|
+
return true;
|
|
1463
1463
|
return (normalizedMessage.includes('previous_response_id') &&
|
|
1464
1464
|
(normalizedMessage.includes('invalid') ||
|
|
1465
1465
|
normalizedMessage.includes('not found') ||
|
|
1466
1466
|
normalizedMessage.includes('does not exist') ||
|
|
1467
1467
|
normalizedMessage.includes('expired') ||
|
|
1468
|
+
normalizedMessage.includes('not supported') ||
|
|
1469
|
+
normalizedMessage.includes('unsupported') ||
|
|
1468
1470
|
normalizedMessage.includes('unavailable')));
|
|
1469
1471
|
}
|
|
1470
1472
|
const RESPONSE_ITEM_TYPES = new Set([
|
|
@@ -632,7 +632,9 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
632
632
|
}
|
|
633
633
|
for (const startGroup of execGroups) {
|
|
634
634
|
const start = invocations.get(startGroup.callId);
|
|
635
|
-
if (!start ||
|
|
635
|
+
if (!start ||
|
|
636
|
+
(start.input.action !== 'start' &&
|
|
637
|
+
!(start.input.action == null && typeof start.input.command === 'string')))
|
|
636
638
|
continue;
|
|
637
639
|
const lifecycle = [start];
|
|
638
640
|
let processId;
|
package/dist/sdk/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { posix } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { describeMarAgentError, MarAgentError } from '../error.js';
|
|
4
5
|
import { negotiateHost } from '../host/contracts.js';
|
|
5
6
|
import { AnthropicMessagesAdapter, isAnthropicRecoverableStreamError } from '../model/anthropic-messages.js';
|
|
@@ -101,7 +102,6 @@ export async function createMarAgent(options) {
|
|
|
101
102
|
sessionRuntime = {
|
|
102
103
|
sourceType: sessionSource.type,
|
|
103
104
|
adapterGeneration: modelGeneration,
|
|
104
|
-
responsesChain: undefined,
|
|
105
105
|
adapters: new Map()
|
|
106
106
|
};
|
|
107
107
|
sessionRuntimes.set(sessionId, sessionRuntime);
|
|
@@ -133,7 +133,6 @@ export async function createMarAgent(options) {
|
|
|
133
133
|
return;
|
|
134
134
|
const retainedByActiveSessions = activeAdaptersExcept(sessionId);
|
|
135
135
|
await closeModelAdapters([...sessionRuntime.adapters.values()].filter((adapter) => !retainedByActiveSessions.has(adapter)));
|
|
136
|
-
sessionRuntime.responsesChain = undefined;
|
|
137
136
|
sessionRuntime.adapters.clear();
|
|
138
137
|
sessionRuntime.adapterGeneration = generation;
|
|
139
138
|
};
|
|
@@ -253,9 +252,17 @@ export async function createMarAgent(options) {
|
|
|
253
252
|
const contextRecords = sessionState.contextRecords;
|
|
254
253
|
const contextualWorldStateGeneration = latestContextualWorldStateGeneration(contextRecords);
|
|
255
254
|
const restoredContextualWorldState = sessionRuntime.contextualWorldState;
|
|
255
|
+
const persistedContextualWorldState = parsePersistedContextualWorldState(await store.readContextualWorldState(sessionId));
|
|
256
256
|
let contextualWorldState;
|
|
257
|
-
if (restoredContextualWorldState
|
|
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,
|
|
@@ -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
|
-
|
|
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.
|
|
819
|
-
responseId = event.responseId;
|
|
820
|
-
else if (event.type === 'response.item') {
|
|
799
|
+
if (event.type === 'response.item') {
|
|
821
800
|
// Compaction keeps only its successful checkpoint, never intermediate native output.
|
|
822
801
|
if (mode === 'compact')
|
|
823
802
|
continue;
|
|
@@ -955,8 +934,6 @@ export async function createMarAgent(options) {
|
|
|
955
934
|
catch (error) {
|
|
956
935
|
planOutputPrefixes.clear();
|
|
957
936
|
if (await recoverModelStream(error, modelRetryState, manualCompaction?.signal ?? controller.signal)) {
|
|
958
|
-
sessionRuntime.responsesChain = undefined;
|
|
959
|
-
responseId = undefined;
|
|
960
937
|
finalAnswer = '';
|
|
961
938
|
if (mode === 'compact')
|
|
962
939
|
messages.splice(roundMessageCount);
|
|
@@ -971,16 +948,13 @@ export async function createMarAgent(options) {
|
|
|
971
948
|
!sawModelSemanticEvent &&
|
|
972
949
|
isProviderContextOverflow(error) &&
|
|
973
950
|
useManualCompactionFallback?.()) {
|
|
974
|
-
sessionRuntime.responsesChain = undefined;
|
|
975
951
|
modelRetryState.attempts = 0;
|
|
976
|
-
responseId = undefined;
|
|
977
952
|
finalAnswer = '';
|
|
978
953
|
continue;
|
|
979
954
|
}
|
|
980
955
|
else if (mode !== 'compact' &&
|
|
981
956
|
!sawModelSemanticEvent &&
|
|
982
957
|
isProviderContextOverflow(error)) {
|
|
983
|
-
sessionRuntime.responsesChain = undefined;
|
|
984
958
|
if (!recoveringFromContextOverflow && (await compactContext())) {
|
|
985
959
|
modelRetryState.attempts = 0;
|
|
986
960
|
recoveringFromContextOverflow = true;
|
|
@@ -992,26 +966,6 @@ export async function createMarAgent(options) {
|
|
|
992
966
|
throw error;
|
|
993
967
|
}
|
|
994
968
|
recoveringFromContextOverflow = false;
|
|
995
|
-
if (selectedModel.responsesPreviousResponseId && responseId)
|
|
996
|
-
sessionRuntime.responsesChain = {
|
|
997
|
-
modelId: selectedModel.id,
|
|
998
|
-
responseId,
|
|
999
|
-
requestSignature,
|
|
1000
|
-
messageCount: messages.length
|
|
1001
|
-
};
|
|
1002
|
-
else if (selectedModel.responsesPreviousResponseId)
|
|
1003
|
-
sessionRuntime.responsesChain = undefined;
|
|
1004
|
-
const continuationCheckpoint = selected.continuationCheckpoint?.();
|
|
1005
|
-
if (mode !== 'compact' && continuationCheckpoint !== undefined)
|
|
1006
|
-
await store.append(sessionId, {
|
|
1007
|
-
type: 'responses.continuation',
|
|
1008
|
-
payload: {
|
|
1009
|
-
modelId: selectedModel.id,
|
|
1010
|
-
checkpoint: continuationCheckpoint
|
|
1011
|
-
},
|
|
1012
|
-
executionId,
|
|
1013
|
-
turnId
|
|
1014
|
-
});
|
|
1015
969
|
if (pendingTools.length === 0 && mailbox.length > 0) {
|
|
1016
970
|
await appendMailboxMessages();
|
|
1017
971
|
finalAnswer = '';
|
|
@@ -1186,7 +1140,6 @@ export async function createMarAgent(options) {
|
|
|
1186
1140
|
}
|
|
1187
1141
|
});
|
|
1188
1142
|
if (committed) {
|
|
1189
|
-
sessionRuntime.responsesChain = undefined;
|
|
1190
1143
|
selected.resetContinuation?.();
|
|
1191
1144
|
const refreshed = await restoreMessages(store, sessionId, committed.records, {
|
|
1192
1145
|
includeImages: true,
|
|
@@ -1208,6 +1161,7 @@ export async function createMarAgent(options) {
|
|
|
1208
1161
|
previousMayContainResources: compactHistoryMayContainRetainedResources(committed.records)
|
|
1209
1162
|
});
|
|
1210
1163
|
sessionRuntime.contextualWorldState = contextualWorldState;
|
|
1164
|
+
await persistContextualWorldState(store, sessionId, contextualWorldState);
|
|
1211
1165
|
}
|
|
1212
1166
|
}
|
|
1213
1167
|
if (controller.signal.aborted) {
|
|
@@ -1402,9 +1356,6 @@ export async function createMarAgent(options) {
|
|
|
1402
1356
|
for (const [modelId, adapter] of runtime.adapters)
|
|
1403
1357
|
if (!retainedAdapters.has(adapter))
|
|
1404
1358
|
runtime.adapters.delete(modelId);
|
|
1405
|
-
if (runtime.responsesChain !== undefined &&
|
|
1406
|
-
!retainedAdapters.has(runtime.adapters.get(runtime.responsesChain.modelId)))
|
|
1407
|
-
runtime.responsesChain = undefined;
|
|
1408
1359
|
}
|
|
1409
1360
|
await closeModelAdapters(adapters);
|
|
1410
1361
|
if (disposed)
|
|
@@ -1572,45 +1523,9 @@ function createModelTurnSession(adapter) {
|
|
|
1572
1523
|
}
|
|
1573
1524
|
},
|
|
1574
1525
|
resetContinuation: () => turn.resetContinuation?.(),
|
|
1575
|
-
continuationCheckpoint: () => turn.continuationCheckpoint?.(),
|
|
1576
1526
|
close: () => turn.close()
|
|
1577
1527
|
};
|
|
1578
1528
|
}
|
|
1579
|
-
function latestModelContinuationCheckpoint(records, modelId) {
|
|
1580
|
-
let checkpoint;
|
|
1581
|
-
for (const record of records) {
|
|
1582
|
-
if (record.type === 'compact.completed' || record.type === 'context.gc.completed') {
|
|
1583
|
-
checkpoint = undefined;
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
if (record.type !== 'responses.continuation')
|
|
1587
|
-
continue;
|
|
1588
|
-
const payload = record.payload;
|
|
1589
|
-
if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
|
|
1590
|
-
continue;
|
|
1591
|
-
const candidateModelId = payload.modelId;
|
|
1592
|
-
const candidate = payload.checkpoint;
|
|
1593
|
-
if (candidateModelId !== modelId || !isModelContinuationCheckpoint(candidate))
|
|
1594
|
-
continue;
|
|
1595
|
-
checkpoint = candidate;
|
|
1596
|
-
}
|
|
1597
|
-
return checkpoint;
|
|
1598
|
-
}
|
|
1599
|
-
function isModelContinuationCheckpoint(value) {
|
|
1600
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
1601
|
-
return false;
|
|
1602
|
-
const checkpoint = value;
|
|
1603
|
-
return (checkpoint.version === 2 &&
|
|
1604
|
-
checkpoint.protocol === 'OPENAI_RESPONSES' &&
|
|
1605
|
-
isBoundedString(checkpoint.configurationHash, 64) &&
|
|
1606
|
-
isBoundedString(checkpoint.responseId, 1_024) &&
|
|
1607
|
-
Number.isSafeInteger(checkpoint.inputPrefixLength) &&
|
|
1608
|
-
checkpoint.inputPrefixLength >= 0 &&
|
|
1609
|
-
isBoundedString(checkpoint.inputPrefixHash, 64));
|
|
1610
|
-
}
|
|
1611
|
-
function isBoundedString(value, maximumLength) {
|
|
1612
|
-
return typeof value === 'string' && value.length > 0 && value.length <= maximumLength;
|
|
1613
|
-
}
|
|
1614
1529
|
function modelAttemptFailureDiagnostic(diagnostic, summary) {
|
|
1615
1530
|
if (summary === undefined)
|
|
1616
1531
|
return diagnostic;
|
|
@@ -1907,6 +1822,73 @@ function latestContextualWorldStateGeneration(records) {
|
|
|
1907
1822
|
generation = record.recordId;
|
|
1908
1823
|
return generation;
|
|
1909
1824
|
}
|
|
1825
|
+
const MAX_CONTEXTUAL_WORLD_STATE_ENTRIES = 1_024;
|
|
1826
|
+
const MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES = 8 * 1024 * 1024;
|
|
1827
|
+
function parsePersistedContextualWorldState(payload) {
|
|
1828
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
|
|
1829
|
+
return undefined;
|
|
1830
|
+
const record = payload;
|
|
1831
|
+
if (record.version !== 1)
|
|
1832
|
+
return undefined;
|
|
1833
|
+
const generation = record.generation === null
|
|
1834
|
+
? undefined
|
|
1835
|
+
: typeof record.generation === 'string' && record.generation.length <= 128
|
|
1836
|
+
? record.generation
|
|
1837
|
+
: null;
|
|
1838
|
+
if (generation === null)
|
|
1839
|
+
return undefined;
|
|
1840
|
+
const instructionSnapshot = nullableWorldStateString(record.instructionSnapshot);
|
|
1841
|
+
const resourceSnapshot = nullableWorldStateString(record.resourceSnapshot);
|
|
1842
|
+
if (instructionSnapshot === null || resourceSnapshot === null || !Array.isArray(record.entries))
|
|
1843
|
+
return undefined;
|
|
1844
|
+
if (record.entries.length > MAX_CONTEXTUAL_WORLD_STATE_ENTRIES)
|
|
1845
|
+
return undefined;
|
|
1846
|
+
const entries = [];
|
|
1847
|
+
let contentBytes = 0;
|
|
1848
|
+
for (const value of record.entries) {
|
|
1849
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
1850
|
+
return undefined;
|
|
1851
|
+
const entry = value;
|
|
1852
|
+
if (!Number.isSafeInteger(entry.index) ||
|
|
1853
|
+
entry.index < 0 ||
|
|
1854
|
+
(entry.kind !== 'instructions' && entry.kind !== 'resources') ||
|
|
1855
|
+
typeof entry.content !== 'string' ||
|
|
1856
|
+
entry.content.length === 0)
|
|
1857
|
+
return undefined;
|
|
1858
|
+
contentBytes += Buffer.byteLength(entry.content);
|
|
1859
|
+
if (contentBytes > MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES)
|
|
1860
|
+
return undefined;
|
|
1861
|
+
entries.push({
|
|
1862
|
+
index: entry.index,
|
|
1863
|
+
kind: entry.kind,
|
|
1864
|
+
content: entry.content
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
generation,
|
|
1869
|
+
initialized: true,
|
|
1870
|
+
instructionSnapshot,
|
|
1871
|
+
resourceSnapshot,
|
|
1872
|
+
entries
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function nullableWorldStateString(value) {
|
|
1876
|
+
if (value === null)
|
|
1877
|
+
return undefined;
|
|
1878
|
+
return typeof value === 'string' &&
|
|
1879
|
+
Buffer.byteLength(value) <= MAX_CONTEXTUAL_WORLD_STATE_CONTENT_BYTES
|
|
1880
|
+
? value
|
|
1881
|
+
: null;
|
|
1882
|
+
}
|
|
1883
|
+
async function persistContextualWorldState(store, sessionId, state) {
|
|
1884
|
+
await store.writeContextualWorldState(sessionId, {
|
|
1885
|
+
version: 1,
|
|
1886
|
+
generation: state.generation ?? null,
|
|
1887
|
+
instructionSnapshot: state.instructionSnapshot ?? null,
|
|
1888
|
+
resourceSnapshot: state.resourceSnapshot ?? null,
|
|
1889
|
+
entries: state.entries
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1910
1892
|
function compactHistoryMayContainRetainedResources(records) {
|
|
1911
1893
|
return records.some((record) => compactSummaryFromCheckpoint(record)?.includes('# Retained Session resources'));
|
|
1912
1894
|
}
|
|
@@ -74,7 +74,11 @@ export function contextGcWebOutputReplacement(toolName, originalContent, truncat
|
|
|
74
74
|
export function contextGcExecInputReplacement(arguments_, formatVersion = 3) {
|
|
75
75
|
if (!isRecord(arguments_))
|
|
76
76
|
return `<context_gc kind="terminal_exec_input"${formatVersion === 3 ? ' formatVersion="3"' : ''} originalBytes="${Buffer.byteLength(stableJson(arguments_))}"${formatVersion === 3 ? '' : ` originalSha256="${sha256(stableJson(arguments_))}"`} />`;
|
|
77
|
-
const action = typeof arguments_.action === 'string'
|
|
77
|
+
const action = typeof arguments_.action === 'string'
|
|
78
|
+
? arguments_.action
|
|
79
|
+
: typeof arguments_.command === 'string'
|
|
80
|
+
? 'start'
|
|
81
|
+
: 'unknown';
|
|
78
82
|
const original = stableJson(arguments_);
|
|
79
83
|
const retained = {
|
|
80
84
|
action,
|
|
@@ -94,6 +94,8 @@ export declare class JsonlSessionStore {
|
|
|
94
94
|
source?: SessionSource;
|
|
95
95
|
}): Promise<SessionMeta>;
|
|
96
96
|
sessionDirectory(sessionId: string): string;
|
|
97
|
+
readContextualWorldState(sessionId: string): Promise<unknown | undefined>;
|
|
98
|
+
writeContextualWorldState(sessionId: string, state: unknown): Promise<void>;
|
|
97
99
|
persistInputImages(sessionId: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
|
|
98
100
|
readInputImages(sessionId: string, value: unknown): Promise<RestoredSessionInputImage[]>;
|
|
99
101
|
readInputImage(sessionId: string, input: {
|
|
@@ -12,6 +12,8 @@ const REVERSE_READ_CHUNK_BYTES = 64 * 1024;
|
|
|
12
12
|
const MAX_INPUT_IMAGES = 5;
|
|
13
13
|
const MAX_INPUT_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
14
14
|
const MAX_INPUT_IMAGES_BYTES = 10 * 1024 * 1024;
|
|
15
|
+
const MAX_CONTEXTUAL_WORLD_STATE_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
const CONTEXTUAL_WORLD_STATE_FILE = 'context-world-state.json';
|
|
15
17
|
const INPUT_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
16
18
|
export class JsonlSessionStore {
|
|
17
19
|
static #instances = new Map();
|
|
@@ -54,6 +56,8 @@ export class JsonlSessionStore {
|
|
|
54
56
|
catch {
|
|
55
57
|
// A missing index starts empty; JSONL remains authoritative for explicit session reads.
|
|
56
58
|
}
|
|
59
|
+
for (const session of index.sessions)
|
|
60
|
+
await recoverContextualWorldStateTransactions(root, session);
|
|
57
61
|
return {
|
|
58
62
|
root,
|
|
59
63
|
index,
|
|
@@ -157,6 +161,57 @@ export class JsonlSessionStore {
|
|
|
157
161
|
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
158
162
|
return join(this.#shared.root, 'sessions', sessionId);
|
|
159
163
|
}
|
|
164
|
+
async readContextualWorldState(sessionId) {
|
|
165
|
+
const path = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
166
|
+
try {
|
|
167
|
+
const pathInfo = await lstat(path);
|
|
168
|
+
if (!pathInfo.isFile())
|
|
169
|
+
return undefined;
|
|
170
|
+
const file = await open(path, 'r');
|
|
171
|
+
try {
|
|
172
|
+
const info = await file.stat();
|
|
173
|
+
if (!info.isFile() || info.size > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
|
|
174
|
+
return undefined;
|
|
175
|
+
const envelope = JSON.parse(await file.readFile('utf8'));
|
|
176
|
+
const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
|
|
177
|
+
if (envelope.version !== 1 ||
|
|
178
|
+
!Number.isSafeInteger(envelope.historySequence) ||
|
|
179
|
+
envelope.historySequence < 1 ||
|
|
180
|
+
meta === undefined ||
|
|
181
|
+
envelope.historySequence >= meta.nextSequence)
|
|
182
|
+
return undefined;
|
|
183
|
+
return envelope.state;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await file.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async writeContextualWorldState(sessionId, state) {
|
|
194
|
+
const directory = this.sessionDirectory(sessionId);
|
|
195
|
+
const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
|
|
196
|
+
if (!meta)
|
|
197
|
+
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
198
|
+
const path = join(directory, CONTEXTUAL_WORLD_STATE_FILE);
|
|
199
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
200
|
+
const serialized = JSON.stringify({
|
|
201
|
+
version: 1,
|
|
202
|
+
historySequence: meta.nextSequence - 1,
|
|
203
|
+
state
|
|
204
|
+
});
|
|
205
|
+
if (Buffer.byteLength(serialized) > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
|
|
206
|
+
throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Contextual world state exceeds the storage limit.');
|
|
207
|
+
try {
|
|
208
|
+
await writeFile(temporary, serialized, { mode: 0o600, flag: 'wx' });
|
|
209
|
+
await rename(temporary, path);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await rm(temporary, { force: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
160
215
|
async persistInputImages(sessionId, images) {
|
|
161
216
|
this.#assertOpen();
|
|
162
217
|
if (images.length === 0)
|
|
@@ -373,6 +428,10 @@ export class JsonlSessionStore {
|
|
|
373
428
|
.filter((record) => record.type !== 'responses.continuation');
|
|
374
429
|
const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
|
|
375
430
|
const transactionId = randomUUID();
|
|
431
|
+
const targetNextSequence = (retained.at(-1)?.sequence ?? 0) + 1;
|
|
432
|
+
const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
433
|
+
const contextualWorldStateBackup = `${contextualWorldStatePath}.${targetNextSequence}.${transactionId}.rollback-backup`;
|
|
434
|
+
let contextualWorldStateStaged = false;
|
|
376
435
|
let generatedTrash = [];
|
|
377
436
|
let inputTrash = [];
|
|
378
437
|
const originalMeta = structuredClone(meta);
|
|
@@ -386,6 +445,7 @@ export class JsonlSessionStore {
|
|
|
386
445
|
const installedPaths = [];
|
|
387
446
|
let committed = false;
|
|
388
447
|
try {
|
|
448
|
+
contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
|
|
389
449
|
generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(generatedImageFileNames(retained)), transactionId);
|
|
390
450
|
inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName)), transactionId);
|
|
391
451
|
for (const item of backups)
|
|
@@ -398,7 +458,7 @@ export class JsonlSessionStore {
|
|
|
398
458
|
const completedAt = new Date().toISOString();
|
|
399
459
|
meta.title = retainedSessionTitle(retained, meta.title);
|
|
400
460
|
meta.lastActivityAt = completedAt;
|
|
401
|
-
meta.nextSequence =
|
|
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/dist/tools/exec.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ClientToolDefinition } from '../model/contracts.js';
|
|
|
2
2
|
import { type LocalProcessExecutor } from '../host/local-process-executor.js';
|
|
3
3
|
import { WorkspacePathResolver } from './shared/path-resolver.js';
|
|
4
4
|
export type ExecInput = {
|
|
5
|
-
action
|
|
5
|
+
action?: 'start';
|
|
6
6
|
command: string;
|
|
7
7
|
cwd?: string;
|
|
8
8
|
timeoutMs?: number;
|
package/dist/tools/exec.js
CHANGED
|
@@ -5,8 +5,8 @@ import { MarAgentError } from '../error.js';
|
|
|
5
5
|
import { TOOL_EXECUTION_LIMITS } from './execution-limits.js';
|
|
6
6
|
export const execToolDefinition = {
|
|
7
7
|
name: 'exec',
|
|
8
|
-
parallelSafety: '
|
|
9
|
-
description: 'Unrestricted general system execution.
|
|
8
|
+
parallelSafety: 'safe',
|
|
9
|
+
description: 'Unrestricted general system execution. Providing command starts any platform-native command, script, text/file tool, builder, generator, network client, or program in workspace/default cwd; action may be omitted for start. poll waits up to yieldMs for new output or termination and returns only output produced since the prior result; write sends stdin; cancel terminates the process tree. Returns status, processId, incremental stdout/stderr, cumulative stdoutBytes/stderrBytes, per-result dropped bytes, exitCode, and truncated; a completed status does not imply exitCode 0. Always inspect stderr/exitCode and consume terminal output or cancel.',
|
|
10
10
|
outputSchema: {
|
|
11
11
|
type: 'object',
|
|
12
12
|
required: ['content', 'data'],
|
|
@@ -42,8 +42,14 @@ export const execToolDefinition = {
|
|
|
42
42
|
inputSchema: {
|
|
43
43
|
type: 'object',
|
|
44
44
|
properties: {
|
|
45
|
-
action: {
|
|
46
|
-
|
|
45
|
+
action: {
|
|
46
|
+
enum: ['start', 'poll', 'write', 'cancel'],
|
|
47
|
+
description: 'Lifecycle operation. Omit when providing command to start a process.'
|
|
48
|
+
},
|
|
49
|
+
command: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Required for start; implies start when action is omitted.'
|
|
52
|
+
},
|
|
47
53
|
cwd: {
|
|
48
54
|
type: 'string',
|
|
49
55
|
description: 'Optional relative/absolute start directory; defaults to workspace.'
|
|
@@ -59,7 +65,7 @@ export const execToolDefinition = {
|
|
|
59
65
|
processId: { type: 'string', description: 'Required for poll, write and cancel.' },
|
|
60
66
|
data: { type: 'string', description: 'stdin data required for write.' }
|
|
61
67
|
},
|
|
62
|
-
required: [
|
|
68
|
+
required: [],
|
|
63
69
|
additionalProperties: false
|
|
64
70
|
}
|
|
65
71
|
};
|
|
@@ -71,6 +77,7 @@ export class ExecTool {
|
|
|
71
77
|
shell;
|
|
72
78
|
processExecutor;
|
|
73
79
|
#processes = new Map();
|
|
80
|
+
#startingProcesses = new Set();
|
|
74
81
|
constructor(paths, maxOutputBytes = TOOL_EXECUTION_LIMITS.execDefaultMaxOutputBytes, maxProcesses = TOOL_EXECUTION_LIMITS.maxProcesses, environment = process.env, shell, processExecutor = directLocalProcessExecutor) {
|
|
75
82
|
this.paths = paths;
|
|
76
83
|
this.maxOutputBytes = maxOutputBytes;
|
|
@@ -83,7 +90,7 @@ export class ExecTool {
|
|
|
83
90
|
if (signal?.aborted)
|
|
84
91
|
throw executionAbortReason(signal);
|
|
85
92
|
const input = parseExecInput(arguments_);
|
|
86
|
-
if (
|
|
93
|
+
if ('processId' in input) {
|
|
87
94
|
const process = this.#processes.get(input.processId);
|
|
88
95
|
if (!process || (owner !== undefined && process.ownerSessionId !== owner.sessionId))
|
|
89
96
|
throw new MarAgentError('MAR_AGENT_PROCESS_NOT_FOUND', 'Managed process was not found.');
|
|
@@ -98,38 +105,53 @@ export class ExecTool {
|
|
|
98
105
|
this.#processes.delete(input.processId);
|
|
99
106
|
return result;
|
|
100
107
|
}
|
|
101
|
-
if (this.#processes.size >= this.maxProcesses)
|
|
108
|
+
if (this.#processes.size + this.#startingProcesses.size >= this.maxProcesses)
|
|
102
109
|
throw new MarAgentError('MAR_AGENT_PROCESS_LIMIT', 'The managed process limit was reached.');
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const child = await this.processExecutor.spawn({
|
|
106
|
-
command: input.command,
|
|
107
|
-
cwd: cwd.physicalPath,
|
|
108
|
-
shell: this.shell ?? true,
|
|
109
|
-
environment: this.environment,
|
|
110
|
-
detached: process.platform !== 'win32',
|
|
111
|
-
...(owner === undefined ? {} : { owner }),
|
|
112
|
-
...(signal === undefined ? {} : { signal })
|
|
113
|
-
});
|
|
114
|
-
let resolveExited;
|
|
115
|
-
let resolveSettled;
|
|
116
|
-
const managed = {
|
|
117
|
-
child,
|
|
110
|
+
let resolveStarting;
|
|
111
|
+
const starting = {
|
|
118
112
|
...(owner === undefined ? {} : { ownerSessionId: owner.sessionId }),
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
stdout: createOutputBuffer(),
|
|
122
|
-
stderr: createOutputBuffer(),
|
|
123
|
-
status: 'running',
|
|
124
|
-
exited: new Promise((resolve) => (resolveExited = resolve)),
|
|
125
|
-
settled: new Promise((resolve) => (resolveSettled = resolve)),
|
|
126
|
-
resolveExited,
|
|
127
|
-
resolveSettled,
|
|
128
|
-
exitObserved: child.exitCode !== null,
|
|
129
|
-
finalized: false,
|
|
130
|
-
waiters: new Set()
|
|
113
|
+
settled: new Promise((resolve) => (resolveStarting = resolve)),
|
|
114
|
+
resolveSettled: () => resolveStarting()
|
|
131
115
|
};
|
|
132
|
-
this.#
|
|
116
|
+
this.#startingProcesses.add(starting);
|
|
117
|
+
const id = randomUUID();
|
|
118
|
+
let child;
|
|
119
|
+
let managed;
|
|
120
|
+
try {
|
|
121
|
+
const cwd = await this.paths.resolveWithDisplay(input.cwd ?? '.');
|
|
122
|
+
child = await this.processExecutor.spawn({
|
|
123
|
+
command: input.command,
|
|
124
|
+
cwd: cwd.physicalPath,
|
|
125
|
+
shell: this.shell ?? true,
|
|
126
|
+
environment: this.environment,
|
|
127
|
+
detached: process.platform !== 'win32',
|
|
128
|
+
...(owner === undefined ? {} : { owner }),
|
|
129
|
+
...(signal === undefined ? {} : { signal })
|
|
130
|
+
});
|
|
131
|
+
let resolveExited;
|
|
132
|
+
let resolveSettled;
|
|
133
|
+
managed = {
|
|
134
|
+
child,
|
|
135
|
+
...(owner === undefined ? {} : { ownerSessionId: owner.sessionId }),
|
|
136
|
+
command: input.command,
|
|
137
|
+
cwd: cwd.displayPath,
|
|
138
|
+
stdout: createOutputBuffer(),
|
|
139
|
+
stderr: createOutputBuffer(),
|
|
140
|
+
status: 'running',
|
|
141
|
+
exited: new Promise((resolve) => (resolveExited = resolve)),
|
|
142
|
+
settled: new Promise((resolve) => (resolveSettled = resolve)),
|
|
143
|
+
resolveExited,
|
|
144
|
+
resolveSettled,
|
|
145
|
+
exitObserved: child.exitCode !== null,
|
|
146
|
+
finalized: false,
|
|
147
|
+
waiters: new Set()
|
|
148
|
+
};
|
|
149
|
+
this.#processes.set(id, managed);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
this.#startingProcesses.delete(starting);
|
|
153
|
+
starting.resolveSettled();
|
|
154
|
+
}
|
|
133
155
|
child.stdout.on('data', (chunk) => {
|
|
134
156
|
if (managed.finalized)
|
|
135
157
|
return;
|
|
@@ -182,6 +204,9 @@ export class ExecTool {
|
|
|
182
204
|
: []);
|
|
183
205
|
}
|
|
184
206
|
async disposeSession(sessionId) {
|
|
207
|
+
await Promise.all([...this.#startingProcesses]
|
|
208
|
+
.filter((process) => process.ownerSessionId === sessionId)
|
|
209
|
+
.map((process) => process.settled));
|
|
185
210
|
const owned = [...this.#processes.entries()].filter(([, process]) => process.ownerSessionId === sessionId);
|
|
186
211
|
await Promise.all(owned.map(([, process]) => process.status === 'running' ? this.#terminate(process, false) : process.settled));
|
|
187
212
|
for (const [processId] of owned)
|
|
@@ -239,6 +264,7 @@ export class ExecTool {
|
|
|
239
264
|
};
|
|
240
265
|
}
|
|
241
266
|
async dispose() {
|
|
267
|
+
await Promise.all([...this.#startingProcesses].map((process) => process.settled));
|
|
242
268
|
await Promise.all([...this.#processes.values()].map((process) => process.status === 'running' ? this.#terminate(process, false) : process.settled));
|
|
243
269
|
this.#processes.clear();
|
|
244
270
|
}
|
|
@@ -332,7 +358,7 @@ function parseExecInput(value) {
|
|
|
332
358
|
if (!value || typeof value !== 'object')
|
|
333
359
|
return invalidExecInput();
|
|
334
360
|
const input = value;
|
|
335
|
-
const action = input.action;
|
|
361
|
+
const action = input.action == null && typeof input.command === 'string' ? 'start' : input.action;
|
|
336
362
|
if (!['start', 'poll', 'write', 'cancel'].includes(String(action)))
|
|
337
363
|
return invalidExecInput();
|
|
338
364
|
const yieldMs = optionalBoundedInteger(input.yieldMs, 0, TOOL_EXECUTION_LIMITS.execMaxYieldMs);
|
|
@@ -343,7 +369,7 @@ function parseExecInput(value) {
|
|
|
343
369
|
return invalidExecInput();
|
|
344
370
|
const timeoutMs = optionalBoundedInteger(input.timeoutMs, 1, TOOL_EXECUTION_LIMITS.execMaxTimeoutMs);
|
|
345
371
|
return {
|
|
346
|
-
action,
|
|
372
|
+
...(input.action === 'start' ? { action: 'start' } : {}),
|
|
347
373
|
command: input.command,
|
|
348
374
|
...(typeof input.cwd === 'string' ? { cwd: input.cwd } : {}),
|
|
349
375
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|