@myagentroam/agent 0.11.20 → 0.11.22
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/agent-core/error.d.ts +1 -1
- package/dist/agent-core/error.js +20 -14
- package/dist/agent-core/extensions/compaction.d.ts +6 -0
- package/dist/agent-core/extensions/compaction.js +16 -2
- package/dist/agent-core/extensions/model-request.d.ts +5 -9
- package/dist/agent-core/extensions/model-request.js +14 -3
- package/dist/agent-core/extensions/prompt-input.js +3 -1
- package/dist/agent-core/host/contracts.d.ts +8 -0
- package/dist/agent-core/host/local-agent-host.d.ts +2 -0
- package/dist/agent-core/host/local-agent-host.js +3 -0
- package/dist/agent-core/model/anthropic-messages.js +15 -26
- package/dist/agent-core/model/catalog-file.d.ts +2 -0
- package/dist/agent-core/model/configuration.d.ts +3 -1
- package/dist/agent-core/model/configuration.js +1 -0
- package/dist/agent-core/model/contracts.d.ts +5 -8
- package/dist/agent-core/model/image.d.ts +17 -0
- package/dist/agent-core/model/image.js +53 -0
- package/dist/agent-core/model/openai-responses.d.ts +7 -2
- package/dist/agent-core/model/openai-responses.js +81 -27
- package/dist/agent-core/runtime/compaction/retained-context.js +2 -9
- package/dist/agent-core/runtime/context/gc-runtime.d.ts +3 -1
- package/dist/agent-core/runtime/context/gc-runtime.js +17 -6
- package/dist/agent-core/runtime/context/gc-tokens.js +13 -0
- package/dist/agent-core/runtime/context/gc.d.ts +2 -1
- package/dist/agent-core/runtime/context/gc.js +75 -12
- package/dist/agent-core/runtime/context/projection.js +4 -0
- package/dist/agent-core/runtime/context/request-budget.d.ts +2 -4
- package/dist/agent-core/runtime/context/restore.d.ts +2 -0
- package/dist/agent-core/runtime/context/restore.js +19 -3
- package/dist/agent-core/runtime/contracts.d.ts +2 -3
- package/dist/agent-core/runtime/execution.d.ts +1 -1
- package/dist/agent-core/runtime/execution.js +10 -4
- package/dist/agent-core/runtime/resource-publication.d.ts +8 -0
- package/dist/agent-core/runtime/resource-publication.js +60 -0
- package/dist/agent-core/sdk/agent.js +16 -0
- package/dist/agent-core/sdk/model-runtime.js +2 -0
- package/dist/agent-core/sdk/resource-input.d.ts +31 -0
- package/dist/agent-core/sdk/resource-input.js +342 -0
- package/dist/agent-core/sdk/session.js +144 -39
- package/dist/agent-core/session/catalog.d.ts +4 -0
- package/dist/agent-core/session/catalog.js +4 -0
- package/dist/agent-core/session/context-gc-checkpoint.d.ts +6 -4
- package/dist/agent-core/session/context-gc-checkpoint.js +41 -16
- package/dist/agent-core/session/inline-media.d.ts +45 -0
- package/dist/agent-core/session/inline-media.js +163 -0
- package/dist/agent-core/session/store/contracts.d.ts +11 -0
- package/dist/agent-core/session/store/history.d.ts +1 -1
- package/dist/agent-core/session/store/history.js +26 -2
- package/dist/agent-core/session/store/images.d.ts +4 -6
- package/dist/agent-core/session/store/images.js +18 -111
- package/dist/agent-core/session/store/jsonl-store.d.ts +9 -0
- package/dist/agent-core/session/store/jsonl-store.js +100 -28
- package/dist/agent-core/session/store/resources.d.ts +26 -0
- package/dist/agent-core/session/store/resources.js +196 -0
- package/dist/agent-core/session/store/types.d.ts +7 -6
- package/dist/agent-core/session/transaction-recovery.js +1 -1
- package/dist/agent-core/tools/code-mode/cells.js +3 -4
- package/dist/agent-core/tools/code-mode/index.js +32 -8
- package/dist/agent-core/tools/code-mode/observations.d.ts +5 -16
- package/dist/agent-core/tools/code-mode/quickjs-engine.js +23 -26
- package/dist/agent-core/tools/contracts.d.ts +15 -5
- package/dist/agent-core/tools/execution-limits.d.ts +3 -1
- package/dist/agent-core/tools/execution-limits.js +3 -1
- package/dist/agent-core/tools/execution.d.ts +2 -1
- package/dist/agent-core/tools/execution.js +31 -4
- package/dist/agent-core/tools/input-diagnostics/source-preview.d.ts +10 -0
- package/dist/agent-core/tools/input-diagnostics/source-preview.js +64 -0
- package/dist/agent-core/tools/local/apply-patch-parser.js +2 -53
- package/dist/agent-core/tools/local/apply-patch.js +1 -1
- package/dist/agent-core/tools/local/image-processing.d.ts +24 -0
- package/dist/agent-core/tools/local/image-processing.js +135 -0
- package/dist/agent-core/tools/local/read.js +1 -1
- package/dist/agent-core/tools/local/view-image.d.ts +4 -0
- package/dist/agent-core/tools/local/view-image.js +154 -25
- package/dist/agent-core/tools/output-projection.d.ts +2 -4
- package/dist/agent-core/tools/output-projection.js +81 -10
- package/dist/agent-core/tools/runtime-context.d.ts +1 -0
- package/dist/agent-core/tools/runtime.js +20 -1
- package/dist/cli/main.js +2 -1
- package/dist/index.d.ts +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { MarAgentError } from '../error.js';
|
|
2
|
+
import { appliedInputRecord } from './deferred-input.js';
|
|
3
|
+
const MiB = 1_048_576;
|
|
4
|
+
export const INLINE_MEDIA_POLICY = Object.freeze({
|
|
5
|
+
historicalInlineMediaTrigger: 7 * MiB,
|
|
6
|
+
historicalInlineMediaTarget: 5 * MiB,
|
|
7
|
+
requestInlineMediaHardLimit: 12 * MiB
|
|
8
|
+
});
|
|
9
|
+
export function parseInlineMediaGc(value) {
|
|
10
|
+
if (!value || typeof value !== 'object')
|
|
11
|
+
return undefined;
|
|
12
|
+
const item = value;
|
|
13
|
+
const state = parseInlineMediaState(item.state);
|
|
14
|
+
return state?.evictedThrough &&
|
|
15
|
+
state.consideredThrough &&
|
|
16
|
+
Number.isSafeInteger(item.beforeBytes) &&
|
|
17
|
+
item.beforeBytes > 0 &&
|
|
18
|
+
Number.isSafeInteger(item.afterBytes) &&
|
|
19
|
+
item.afterBytes >= 0 &&
|
|
20
|
+
item.afterBytes <= INLINE_MEDIA_POLICY.historicalInlineMediaTarget &&
|
|
21
|
+
item.afterBytes < item.beforeBytes &&
|
|
22
|
+
(item.reason === 'SOFT_LIMIT' || item.reason === 'HARD_LIMIT' || item.reason === 'TEXT_GC') &&
|
|
23
|
+
(item.reason !== 'SOFT_LIMIT' ||
|
|
24
|
+
item.beforeBytes > INLINE_MEDIA_POLICY.historicalInlineMediaTrigger) &&
|
|
25
|
+
(item.reason !== 'TEXT_GC' ||
|
|
26
|
+
item.beforeBytes > INLINE_MEDIA_POLICY.historicalInlineMediaTarget)
|
|
27
|
+
? { reason: item.reason, state, beforeBytes: item.beforeBytes, afterBytes: item.afterBytes }
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
export function inlineMediaSource(message) {
|
|
31
|
+
return message.callId !== undefined
|
|
32
|
+
? `tool:${message.callId}`
|
|
33
|
+
: message.metadata?.recordId !== undefined
|
|
34
|
+
? `input:${message.metadata.recordId}`
|
|
35
|
+
: message;
|
|
36
|
+
}
|
|
37
|
+
/** GC may advance only a durable, already-considered source prefix. */
|
|
38
|
+
export function inlineMediaGcAnchored(records, media) {
|
|
39
|
+
const history = inlineMediaHistory(records);
|
|
40
|
+
const positions = new Map();
|
|
41
|
+
const key = (cursor) => JSON.stringify([cursor?.sourceId, cursor?.imageIndex]);
|
|
42
|
+
for (const cursor of history.cursors) {
|
|
43
|
+
const identity = key({ sourceId: cursor.sourceId, imageIndex: cursor.imageIndex });
|
|
44
|
+
if (!positions.has(identity))
|
|
45
|
+
positions.set(identity, { index: positions.size, rollout: cursor.rollout });
|
|
46
|
+
}
|
|
47
|
+
const evicted = positions.get(key(media.state.evictedThrough));
|
|
48
|
+
const considered = positions.get(key(media.state.consideredThrough));
|
|
49
|
+
const previous = positions.get(key(history.state?.evictedThrough));
|
|
50
|
+
return (evicted !== undefined &&
|
|
51
|
+
considered !== undefined &&
|
|
52
|
+
key(media.state.consideredThrough) === key(history.state?.consideredThrough) &&
|
|
53
|
+
evicted.index <= considered.index &&
|
|
54
|
+
evicted.index > (previous?.index ?? -1) &&
|
|
55
|
+
(media.reason === 'HARD_LIMIT' || history.rollouts - evicted.rollout >= 2));
|
|
56
|
+
}
|
|
57
|
+
export function inlineMediaBytes(image) {
|
|
58
|
+
const bytes = image.dataBase64 !== undefined
|
|
59
|
+
? image.dataBase64.length
|
|
60
|
+
: image.byteSize !== undefined
|
|
61
|
+
? 4 * Math.ceil(image.byteSize / 3)
|
|
62
|
+
: undefined;
|
|
63
|
+
if (bytes === undefined)
|
|
64
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Resource size is unavailable.');
|
|
65
|
+
return bytes + Buffer.byteLength(`data:${image.mimeType};base64,`);
|
|
66
|
+
}
|
|
67
|
+
export function inlineMediaLimit() {
|
|
68
|
+
throw new MarAgentError('MAR_AGENT_INLINE_MEDIA_LIMIT', 'Inline images exceed the 12 MiB request limit. Reduce this image batch or use view_image with resize or region before retrying.');
|
|
69
|
+
}
|
|
70
|
+
/** Final encoding guard; URL-only images do not consume the inline budget. */
|
|
71
|
+
export function assertInlineMediaBudget(request) {
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (const message of request.messages)
|
|
74
|
+
for (const image of message.images ?? [])
|
|
75
|
+
if (image.dataBase64 !== undefined)
|
|
76
|
+
total += inlineMediaBytes(image);
|
|
77
|
+
for (const image of request.images ?? [])
|
|
78
|
+
if (image.dataBase64 !== undefined)
|
|
79
|
+
total += inlineMediaBytes(image);
|
|
80
|
+
if (total > INLINE_MEDIA_POLICY.requestInlineMediaHardLimit)
|
|
81
|
+
inlineMediaLimit();
|
|
82
|
+
}
|
|
83
|
+
export function parseInlineMediaState(value) {
|
|
84
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
85
|
+
return undefined;
|
|
86
|
+
const state = value;
|
|
87
|
+
const cursor = (value) => {
|
|
88
|
+
if (!value || typeof value !== 'object')
|
|
89
|
+
return false;
|
|
90
|
+
const item = value;
|
|
91
|
+
return (typeof item.sourceId === 'string' &&
|
|
92
|
+
item.sourceId.length > 0 &&
|
|
93
|
+
item.sourceId.length <= 256 &&
|
|
94
|
+
Number.isSafeInteger(item.imageIndex) &&
|
|
95
|
+
item.imageIndex >= 0);
|
|
96
|
+
};
|
|
97
|
+
if (state.version !== 1 ||
|
|
98
|
+
(state.consideredThrough !== undefined && !cursor(state.consideredThrough)) ||
|
|
99
|
+
(state.evictedThrough !== undefined && !cursor(state.evictedThrough)))
|
|
100
|
+
return undefined;
|
|
101
|
+
return structuredClone(state);
|
|
102
|
+
}
|
|
103
|
+
/** Recover ordering once at the lifecycle boundary, including GC-omitted sources. */
|
|
104
|
+
export function inlineMediaHistory(records) {
|
|
105
|
+
const cursors = [];
|
|
106
|
+
let rollouts = 0;
|
|
107
|
+
let state;
|
|
108
|
+
const add = (sourceId, images) => {
|
|
109
|
+
if (!sourceId || !Array.isArray(images))
|
|
110
|
+
return;
|
|
111
|
+
for (let imageIndex = 0; imageIndex < images.length; imageIndex++)
|
|
112
|
+
cursors.push({ sourceId, imageIndex, rollout: rollouts });
|
|
113
|
+
};
|
|
114
|
+
for (const record of records) {
|
|
115
|
+
const payload = record.payload;
|
|
116
|
+
if (!payload || typeof payload !== 'object')
|
|
117
|
+
continue;
|
|
118
|
+
if (record.type === 'model.attempt' &&
|
|
119
|
+
payload.purpose === 'EXECUTION' &&
|
|
120
|
+
payload.state === 'SUCCEEDED')
|
|
121
|
+
rollouts++;
|
|
122
|
+
if (record.type === 'compact.completed') {
|
|
123
|
+
cursors.splice(0);
|
|
124
|
+
state = parseInlineMediaState(payload.inlineMediaState);
|
|
125
|
+
for (const item of (payload.retainedModelContext ?? [])) {
|
|
126
|
+
const applied = appliedInputRecord({ ...record, type: 'model.context', payload: item });
|
|
127
|
+
if (applied) {
|
|
128
|
+
add(`input:${applied.recordId}`, applied.payload.images);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const key = inlineMediaSource(item);
|
|
132
|
+
add(typeof key === 'string' ? key : undefined, item.images);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (record.type === 'resource.window') {
|
|
136
|
+
state = parseInlineMediaState(payload);
|
|
137
|
+
}
|
|
138
|
+
else if (record.type === 'context.gc.completed' && payload.inlineMedia !== undefined) {
|
|
139
|
+
const media = parseInlineMediaGc(payload.inlineMedia);
|
|
140
|
+
if (!media)
|
|
141
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Inline media GC state is invalid.');
|
|
142
|
+
state = media.state;
|
|
143
|
+
}
|
|
144
|
+
else if ((record.type === 'turn.user' || record.type === 'extension.input') &&
|
|
145
|
+
payload.deferred !== true) {
|
|
146
|
+
add(`input:${record.recordId}`, payload.images);
|
|
147
|
+
}
|
|
148
|
+
else if (record.type === 'model.context') {
|
|
149
|
+
const applied = appliedInputRecord(record);
|
|
150
|
+
if (applied) {
|
|
151
|
+
add(`input:${applied.recordId}`, applied.payload.images);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const key = typeof payload.inputRef === 'string'
|
|
155
|
+
? `input:${payload.inputRef}`
|
|
156
|
+
: typeof payload.callId === 'string'
|
|
157
|
+
? `tool:${payload.callId}`
|
|
158
|
+
: undefined;
|
|
159
|
+
add(key, payload.images);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { cursors, rollouts, ...(state ? { state } : {}) };
|
|
163
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { SessionListInput, SessionSource, SessionTitleUpdateInput } from '.
|
|
|
10
10
|
import type { CompactHistoryMessage, RestoredSessionInputImage, SessionExecutionState, SessionInputImage, SessionInputImageData, SessionInputImageMetadata, SessionRecord } from './types.js';
|
|
11
11
|
export type { CompactHistoryMessage, CompactUserInput, RestoredSessionInputImage, SessionExecutionState, SessionInputImage, SessionInputImageData, SessionInputImageMetadata, SessionRecord } from './types.js';
|
|
12
12
|
export type { CodeModeStateEntry } from '../../tools/code-mode/state.js';
|
|
13
|
+
export type { SessionResource } from './resources.js';
|
|
13
14
|
export type { TodoSnapshot, TodoItem } from '../../tools/contracts.js';
|
|
14
15
|
export type { CodeToolCallRecord, CodeObservation } from '../../tools/code-mode/observations.js';
|
|
15
16
|
export type { ModelMessage, ModelUsage, ModelInputMetadata, ModelTextBlock, ModelContextualBlock } from '../../model/contracts.js';
|
|
@@ -38,6 +39,7 @@ export interface SessionStoreCompactInput {
|
|
|
38
39
|
summary: string;
|
|
39
40
|
replacementHistory?: readonly CompactHistoryMessage[];
|
|
40
41
|
retainedModelContext?: readonly ModelMessage[];
|
|
42
|
+
inlineMediaState?: import('../inline-media.js').InlineMediaState;
|
|
41
43
|
pendingInputs?: readonly SessionRecord[];
|
|
42
44
|
snapshotRecordId?: string;
|
|
43
45
|
snapshotMessageCount?: number;
|
|
@@ -116,6 +118,15 @@ export interface SessionStore {
|
|
|
116
118
|
}): Promise<SessionRecord>;
|
|
117
119
|
commitContextGc(sessionId: string, draft: ContextGcCheckpointDraft, planningRecords?: readonly SessionRecord[]): Promise<SessionRecord>;
|
|
118
120
|
persistInputImages(sessionId: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
|
|
121
|
+
persistResource(sessionId: string, image: import('../../model/image.js').ModelImage): Promise<import('./resources.js').SessionResource>;
|
|
122
|
+
setResourceUrl(sessionId: string, resourceId: string, published: {
|
|
123
|
+
url: string;
|
|
124
|
+
urlExpiresAt: number;
|
|
125
|
+
}): Promise<import('./resources.js').SessionResource>;
|
|
126
|
+
readResource(sessionId: string, resourceId: string): Promise<{
|
|
127
|
+
mimeType: string;
|
|
128
|
+
dataBase64: string;
|
|
129
|
+
} | undefined>;
|
|
119
130
|
readInputImages(sessionId: string, value: unknown): Promise<RestoredSessionInputImage[]>;
|
|
120
131
|
readInputImage(sessionId: string, input: {
|
|
121
132
|
readonly sequence: number;
|
|
@@ -8,7 +8,7 @@ interface StagedSessionSegments {
|
|
|
8
8
|
readonly segments: NonNullable<SessionMeta['segments']>;
|
|
9
9
|
}
|
|
10
10
|
export declare function stageSessionSegments(sessionDirectory: string, records: readonly SessionRecord[], operation?: 'rollback' | 'fork'): Promise<StagedSessionSegments>;
|
|
11
|
-
export declare function stageUnreferencedImages(sessionDirectory: string, directoryName: '
|
|
11
|
+
export declare function stageUnreferencedImages(sessionDirectory: string, directoryName: 'generated_images' | 'tool_results' | 'resources', retained: ReadonlySet<string>, transactionId: string): Promise<Array<{
|
|
12
12
|
from: string;
|
|
13
13
|
to: string;
|
|
14
14
|
trash: string;
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { open, readdir, rename, rm } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { MarAgentError } from '../../error.js';
|
|
5
|
+
import { parseInlineMediaState } from '../inline-media.js';
|
|
5
6
|
import { MAX_SEGMENT_BYTES } from './records.js';
|
|
6
7
|
export async function renameIfExists(from, to) {
|
|
7
8
|
try {
|
|
@@ -117,11 +118,32 @@ export function forkedPayload(type, value, sourceSessionId, targetSessionId, tit
|
|
|
117
118
|
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
118
119
|
return value;
|
|
119
120
|
const payload = { ...value };
|
|
121
|
+
const remapWindow = (value) => {
|
|
122
|
+
const state = parseInlineMediaState(value);
|
|
123
|
+
if (!state)
|
|
124
|
+
return value;
|
|
125
|
+
for (const cursor of [state.consideredThrough, state.evictedThrough]) {
|
|
126
|
+
if (!cursor?.sourceId.startsWith('input:'))
|
|
127
|
+
continue;
|
|
128
|
+
const target = identities.get(cursor.sourceId.slice(6));
|
|
129
|
+
if (!target)
|
|
130
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork media reference is missing.');
|
|
131
|
+
cursor.sourceId = `input:${target.recordId}`;
|
|
132
|
+
}
|
|
133
|
+
return state;
|
|
134
|
+
};
|
|
135
|
+
if (type === 'resource.window')
|
|
136
|
+
return remapWindow(payload);
|
|
120
137
|
if (payload.sessionId === sourceSessionId)
|
|
121
138
|
payload.sessionId = targetSessionId;
|
|
122
139
|
const application = (input) => {
|
|
123
|
-
if (typeof input.inputRef !== 'string')
|
|
124
|
-
|
|
140
|
+
if (typeof input.inputRef !== 'string') {
|
|
141
|
+
const recordId = isObject(input.metadata) ? input.metadata.recordId : undefined;
|
|
142
|
+
const identity = typeof recordId === 'string' ? identities.get(recordId) : undefined;
|
|
143
|
+
return identity && isObject(input.metadata)
|
|
144
|
+
? { ...input, metadata: { ...input.metadata, recordId: identity.recordId } }
|
|
145
|
+
: input;
|
|
146
|
+
}
|
|
125
147
|
const identity = identities.get(input.inputRef);
|
|
126
148
|
if (!identity)
|
|
127
149
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork input reference is missing.');
|
|
@@ -136,6 +158,8 @@ export function forkedPayload(type, value, sourceSessionId, targetSessionId, tit
|
|
|
136
158
|
if (type === 'model.context')
|
|
137
159
|
return application(payload);
|
|
138
160
|
if (type === 'compact.completed') {
|
|
161
|
+
if (payload.inlineMediaState)
|
|
162
|
+
payload.inlineMediaState = remapWindow(payload.inlineMediaState);
|
|
139
163
|
if (Array.isArray(payload.retainedModelContext))
|
|
140
164
|
payload.retainedModelContext = payload.retainedModelContext.map((item) => isObject(item) ? application(item) : item);
|
|
141
165
|
if (Array.isArray(payload.pendingInputs))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionRecord } from './types.js';
|
|
2
2
|
export declare const MAX_INPUT_IMAGES = 5;
|
|
3
3
|
export declare function generatedImageFileNames(records: readonly SessionRecord[]): readonly string[];
|
|
4
4
|
export declare function copyGeneratedImages(sourceSessionDirectory: string, targetSessionDirectory: string, fileNames: readonly string[]): Promise<void>;
|
|
@@ -8,14 +8,12 @@ interface InputImageReference {
|
|
|
8
8
|
readonly mimeType: string;
|
|
9
9
|
readonly byteSize: number;
|
|
10
10
|
readonly sha256: string;
|
|
11
|
+
readonly resourceId?: string;
|
|
12
|
+
readonly url?: string;
|
|
13
|
+
readonly urlExpiresAt?: number;
|
|
11
14
|
}
|
|
12
|
-
export declare function inputImageReferencesFromRecords(records: readonly SessionRecord[]): readonly InputImageReference[];
|
|
13
15
|
export declare function inputImageReferences(value: unknown): InputImageReference[];
|
|
14
16
|
export declare function inputImageReference(value: unknown): InputImageReference | undefined;
|
|
15
17
|
export declare function validInputImageFileName(fileName: string): boolean;
|
|
16
18
|
export declare function readInputImageBytes(directory: string, reference: InputImageReference): Promise<Buffer>;
|
|
17
|
-
export declare function copyInputImages(sourceSessionDirectory: string, targetSessionDirectory: string, references: readonly InputImageReference[]): Promise<void>;
|
|
18
|
-
/** A pending marker is removable only after the committed JSONL references are checked. */
|
|
19
|
-
export declare function recoverPendingInputImages(sessionDirectory: string, readHistory: () => Promise<SessionRecord[]>): Promise<void>;
|
|
20
|
-
export declare function writeInputImages(directory: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
|
|
21
19
|
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createHash
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { copyFile, lstat, mkdir, open
|
|
3
|
+
import { copyFile, lstat, mkdir, open } from 'node:fs/promises';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { MarAgentError } from '../../error.js';
|
|
6
|
+
import { isModelImageUrl } from '../../model/image.js';
|
|
6
7
|
export const MAX_INPUT_IMAGES = 5;
|
|
7
8
|
const MAX_INPUT_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
8
9
|
const MAX_INPUT_IMAGES_BYTES = 10 * 1024 * 1024;
|
|
@@ -36,23 +37,6 @@ export async function copyGeneratedImages(sourceSessionDirectory, targetSessionD
|
|
|
36
37
|
for (const fileName of fileNames)
|
|
37
38
|
await copyFile(join(source, fileName), join(target, fileName));
|
|
38
39
|
}
|
|
39
|
-
export function inputImageReferencesFromRecords(records) {
|
|
40
|
-
const references = new Map();
|
|
41
|
-
for (const record of records) {
|
|
42
|
-
if (record.type !== 'turn.user' || !isObject(record.payload))
|
|
43
|
-
continue;
|
|
44
|
-
for (const reference of inputImageReferences(record.payload.images)) {
|
|
45
|
-
const existing = references.get(reference.fileName);
|
|
46
|
-
if (existing !== undefined &&
|
|
47
|
-
(existing.mimeType !== reference.mimeType ||
|
|
48
|
-
existing.byteSize !== reference.byteSize ||
|
|
49
|
-
existing.sha256 !== reference.sha256))
|
|
50
|
-
throw corruptInputImageReference();
|
|
51
|
-
references.set(reference.fileName, reference);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return [...references.values()];
|
|
55
|
-
}
|
|
56
40
|
export function inputImageReferences(value) {
|
|
57
41
|
if (!Array.isArray(value))
|
|
58
42
|
return [];
|
|
@@ -82,14 +66,27 @@ export function inputImageReference(value) {
|
|
|
82
66
|
typeof value.fileName !== 'string' ||
|
|
83
67
|
!validInputImageFileName(value.fileName) ||
|
|
84
68
|
typeof value.sha256 !== 'string' ||
|
|
85
|
-
!/^[a-f0-9]{64}$/u.test(value.sha256)
|
|
69
|
+
!/^[a-f0-9]{64}$/u.test(value.sha256) ||
|
|
70
|
+
(value.resourceId !== undefined &&
|
|
71
|
+
(typeof value.resourceId !== 'string' ||
|
|
72
|
+
!/^[a-f0-9]{64}$/u.test(value.resourceId) ||
|
|
73
|
+
value.fileName !== `${value.resourceId}.bin`)) ||
|
|
74
|
+
(value.url !== undefined && !isModelImageUrl(value.url)) ||
|
|
75
|
+
(value.urlExpiresAt !== undefined &&
|
|
76
|
+
(typeof value.urlExpiresAt !== 'number' ||
|
|
77
|
+
!Number.isSafeInteger(value.urlExpiresAt) ||
|
|
78
|
+
value.urlExpiresAt <= 0)) ||
|
|
79
|
+
(value.urlExpiresAt !== undefined && value.url === undefined))
|
|
86
80
|
throw corruptInputImageReference();
|
|
87
81
|
return {
|
|
88
82
|
name: value.name,
|
|
89
83
|
fileName: value.fileName,
|
|
90
84
|
mimeType: value.mimeType,
|
|
91
85
|
byteSize: value.byteSize,
|
|
92
|
-
sha256: value.sha256
|
|
86
|
+
sha256: value.sha256,
|
|
87
|
+
...(typeof value.resourceId === 'string' ? { resourceId: value.resourceId } : {}),
|
|
88
|
+
...(value.url === undefined ? {} : { url: value.url }),
|
|
89
|
+
...(typeof value.urlExpiresAt === 'number' ? { urlExpiresAt: value.urlExpiresAt } : {})
|
|
93
90
|
};
|
|
94
91
|
}
|
|
95
92
|
export function validInputImageFileName(fileName) {
|
|
@@ -148,101 +145,11 @@ async function readExactBytes(handle, byteSize) {
|
|
|
148
145
|
throw corruptInputImageReference();
|
|
149
146
|
return data;
|
|
150
147
|
}
|
|
151
|
-
export async function copyInputImages(sourceSessionDirectory, targetSessionDirectory, references) {
|
|
152
|
-
if (references.length === 0)
|
|
153
|
-
return;
|
|
154
|
-
const source = join(sourceSessionDirectory, 'input_images');
|
|
155
|
-
const target = join(targetSessionDirectory, 'input_images');
|
|
156
|
-
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
157
|
-
for (const reference of references) {
|
|
158
|
-
const data = await readInputImageBytes(source, reference);
|
|
159
|
-
await writeFile(join(target, reference.fileName), data, { mode: 0o600, flag: 'wx' });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
148
|
function corruptInputImageReference(cause) {
|
|
163
149
|
return new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session input image is corrupt.', {
|
|
164
150
|
...(cause === undefined ? {} : { cause })
|
|
165
151
|
});
|
|
166
152
|
}
|
|
167
|
-
/** A pending marker is removable only after the committed JSONL references are checked. */
|
|
168
|
-
export async function recoverPendingInputImages(sessionDirectory, readHistory) {
|
|
169
|
-
const directory = join(sessionDirectory, 'input_images');
|
|
170
|
-
let names;
|
|
171
|
-
try {
|
|
172
|
-
names = await readdir(directory);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
if (error.code === 'ENOENT')
|
|
176
|
-
return;
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
const pending = names.filter((name) => /^\.[0-9a-f-]{36}\.bin\.pending$/iu.test(name));
|
|
180
|
-
const temporary = names.filter((name) => /^\.[0-9a-f-]{36}\.bin\.[0-9a-f-]{36}\.tmp$/iu.test(name));
|
|
181
|
-
if (pending.length === 0 && temporary.length === 0)
|
|
182
|
-
return;
|
|
183
|
-
const referenced = pending.length === 0
|
|
184
|
-
? new Set()
|
|
185
|
-
: new Set(inputImageReferencesFromRecords(await readHistory()).map((image) => image.fileName));
|
|
186
|
-
for (const name of pending) {
|
|
187
|
-
const fileName = name.slice(1, -'.pending'.length);
|
|
188
|
-
if (!referenced.has(fileName))
|
|
189
|
-
await rm(join(directory, fileName), { force: true });
|
|
190
|
-
await rm(join(directory, name), { force: true });
|
|
191
|
-
}
|
|
192
|
-
for (const name of temporary)
|
|
193
|
-
await rm(join(directory, name), { force: true });
|
|
194
|
-
}
|
|
195
|
-
export async function writeInputImages(directory, images) {
|
|
196
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
197
|
-
const written = [];
|
|
198
|
-
const pending = [];
|
|
199
|
-
let temporary;
|
|
200
|
-
let totalBytes = 0;
|
|
201
|
-
try {
|
|
202
|
-
const metadata = [];
|
|
203
|
-
for (const image of images) {
|
|
204
|
-
if (!INPUT_IMAGE_MIME_TYPES.has(image.mimeType))
|
|
205
|
-
throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'Image input MIME type is unsupported.');
|
|
206
|
-
const data = Buffer.from(image.dataBase64, 'base64');
|
|
207
|
-
totalBytes += data.byteLength;
|
|
208
|
-
if (data.byteLength === 0 ||
|
|
209
|
-
data.byteLength > MAX_INPUT_IMAGE_BYTES ||
|
|
210
|
-
totalBytes > MAX_INPUT_IMAGES_BYTES)
|
|
211
|
-
throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Image input exceeds the supported size limit.');
|
|
212
|
-
const fileName = `${randomUUID()}.bin`;
|
|
213
|
-
temporary = join(directory, `.${fileName}.${randomUUID()}.tmp`);
|
|
214
|
-
const handle = await open(temporary, 'wx', 0o600);
|
|
215
|
-
try {
|
|
216
|
-
await handle.writeFile(data);
|
|
217
|
-
await handle.sync();
|
|
218
|
-
}
|
|
219
|
-
finally {
|
|
220
|
-
await handle.close();
|
|
221
|
-
}
|
|
222
|
-
const marker = join(directory, `.${fileName}.pending`);
|
|
223
|
-
await writeFile(marker, '', { mode: 0o600, flag: 'wx', flush: true });
|
|
224
|
-
pending.push(marker);
|
|
225
|
-
await rename(temporary, join(directory, fileName));
|
|
226
|
-
temporary = undefined;
|
|
227
|
-
written.push(fileName);
|
|
228
|
-
metadata.push({
|
|
229
|
-
name: image.name,
|
|
230
|
-
mimeType: image.mimeType,
|
|
231
|
-
byteSize: data.byteLength,
|
|
232
|
-
fileName,
|
|
233
|
-
sha256: createHash('sha256').update(data).digest('hex')
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return metadata;
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
if (temporary !== undefined)
|
|
240
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
241
|
-
await Promise.all(pending.map((marker) => rm(marker, { force: true })));
|
|
242
|
-
await Promise.all(written.map((fileName) => rm(join(directory, fileName), { force: true }).catch(() => undefined)));
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
153
|
function isObject(value) {
|
|
247
154
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
248
155
|
}
|
|
@@ -19,6 +19,15 @@ export declare class JsonlSessionStore implements SessionStore {
|
|
|
19
19
|
writeContextualWorldState(sessionId: string, state: unknown): Promise<void>;
|
|
20
20
|
/** Mark each image pending before publication; turn.user or lease recovery resolves ownership. */
|
|
21
21
|
persistInputImages(sessionId: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
|
|
22
|
+
persistResource(sessionId: string, image: import('../../model/image.js').ModelImage): Promise<import("./resources.js").SessionResource>;
|
|
23
|
+
setResourceUrl(sessionId: string, resourceId: string, published: {
|
|
24
|
+
url: string;
|
|
25
|
+
urlExpiresAt: number;
|
|
26
|
+
}): Promise<import("./resources.js").SessionResource>;
|
|
27
|
+
readResource(sessionId: string, resourceId: string): Promise<{
|
|
28
|
+
mimeType: string;
|
|
29
|
+
dataBase64: string;
|
|
30
|
+
} | undefined>;
|
|
22
31
|
writeToolResultResource(sessionId: string, text: string): Promise<{
|
|
23
32
|
status: "available";
|
|
24
33
|
resourceRef: string;
|