@myagentroam/agent 0.9.156 → 0.9.158
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/model/anthropic-messages.js +20 -2
- package/dist/agent-core/model/contracts.d.ts +4 -0
- package/dist/agent-core/model/openai-responses.js +38 -1
- package/dist/agent-core/runtime/event-projection.d.ts +6 -2
- package/dist/agent-core/runtime/event-projection.js +45 -0
- package/dist/agent-core/runtime/events.d.ts +3 -0
- package/dist/agent-core/runtime/execution.js +3 -8
- package/dist/agent-core/session/catalog.d.ts +3 -0
- package/dist/agent-core/session/catalog.js +18 -0
- package/dist/agent-core/session/jsonl-store.d.ts +6 -2
- package/dist/agent-core/session/jsonl-store.js +180 -8
- package/dist/agent-core/tools/code-mode/quickjs-engine.js +12 -0
- package/package.json +1 -1
|
@@ -160,13 +160,16 @@ export class AnthropicMessagesAdapter {
|
|
|
160
160
|
delta: text(block.text) ?? ''
|
|
161
161
|
};
|
|
162
162
|
else if (blockType === 'server_tool_use') {
|
|
163
|
-
const
|
|
163
|
+
const toolName = text(block.name) ?? 'unknown';
|
|
164
|
+
const kind = toolName === 'web_fetch' ? 'WEB_FETCH' : 'WEB_SEARCH';
|
|
164
165
|
const summary = anthropicHostedInputSummary(record(block.input));
|
|
165
|
-
hostedTools.set(id, { kind, summary });
|
|
166
|
+
hostedTools.set(id, { kind, toolName, summary });
|
|
166
167
|
yield {
|
|
167
168
|
type: 'hosted_tool',
|
|
168
169
|
itemId: id,
|
|
169
170
|
kind,
|
|
171
|
+
toolName,
|
|
172
|
+
...(Object.keys(summary).length ? { input: summary } : {}),
|
|
170
173
|
status: 'RUNNING',
|
|
171
174
|
summary
|
|
172
175
|
};
|
|
@@ -188,6 +191,19 @@ export class AnthropicMessagesAdapter {
|
|
|
188
191
|
type: 'hosted_tool',
|
|
189
192
|
itemId: toolId,
|
|
190
193
|
kind: hosted.kind,
|
|
194
|
+
toolName: hosted.toolName,
|
|
195
|
+
...(Object.keys(anthropicHostedInputSummary(hosted.summary)).length
|
|
196
|
+
? { input: anthropicHostedInputSummary(hosted.summary) }
|
|
197
|
+
: {}),
|
|
198
|
+
...(sources.length
|
|
199
|
+
? {
|
|
200
|
+
output: {
|
|
201
|
+
sources,
|
|
202
|
+
...(summary.sourcesTruncated ? { sourcesTruncated: true } : {})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
: {}),
|
|
206
|
+
...(summary.sourcesTruncated ? { truncated: true } : {}),
|
|
191
207
|
status: anthropicHostedResultFailed(result) ? 'FAILED' : 'COMPLETED',
|
|
192
208
|
summary
|
|
193
209
|
};
|
|
@@ -267,6 +283,8 @@ export class AnthropicMessagesAdapter {
|
|
|
267
283
|
type: 'hosted_tool',
|
|
268
284
|
itemId: block.id,
|
|
269
285
|
kind: hosted.kind,
|
|
286
|
+
toolName: hosted.toolName,
|
|
287
|
+
input: anthropicHostedInputSummary(record(input)),
|
|
270
288
|
status: 'RUNNING',
|
|
271
289
|
summary
|
|
272
290
|
};
|
|
@@ -118,6 +118,10 @@ export type ModelEvent = {
|
|
|
118
118
|
itemId: string;
|
|
119
119
|
kind: 'WEB_SEARCH' | 'WEB_FETCH';
|
|
120
120
|
status: 'RUNNING' | 'COMPLETED' | 'FAILED';
|
|
121
|
+
toolName?: string;
|
|
122
|
+
input?: Record<string, unknown>;
|
|
123
|
+
output?: unknown;
|
|
124
|
+
truncated?: boolean;
|
|
121
125
|
summary: {
|
|
122
126
|
query?: string | undefined;
|
|
123
127
|
url?: string | undefined;
|
|
@@ -475,11 +475,25 @@ export class OpenAiResponsesAdapter {
|
|
|
475
475
|
? 'WEB_FETCH'
|
|
476
476
|
: (previous?.kind ?? 'WEB_SEARCH');
|
|
477
477
|
const summary = { ...previous?.summary, ...hostedWebActionSummary(action) };
|
|
478
|
-
|
|
478
|
+
const projected = hostedWebActionInput(action);
|
|
479
|
+
const input = { ...previous?.input, ...projected.input };
|
|
480
|
+
const truncated = previous?.truncated === true || projected.truncated;
|
|
481
|
+
hostedWebCalls.set(itemId, { kind, summary, input, truncated });
|
|
482
|
+
const output = {
|
|
483
|
+
...(summary.title === undefined ? {} : { title: summary.title }),
|
|
484
|
+
...(summary.sources === undefined ? {} : { sources: summary.sources }),
|
|
485
|
+
...(summary.sourcesTruncated === undefined
|
|
486
|
+
? {}
|
|
487
|
+
: { sourcesTruncated: summary.sourcesTruncated })
|
|
488
|
+
};
|
|
479
489
|
yield {
|
|
480
490
|
type: 'hosted_tool',
|
|
481
491
|
itemId,
|
|
482
492
|
kind,
|
|
493
|
+
toolName: 'web_search',
|
|
494
|
+
...(Object.keys(input).length ? { input } : {}),
|
|
495
|
+
...(Object.keys(output).length ? { output } : {}),
|
|
496
|
+
...(truncated || summary.sourcesTruncated === true ? { truncated: true } : {}),
|
|
483
497
|
status: type.endsWith('.failed')
|
|
484
498
|
? 'FAILED'
|
|
485
499
|
: type.endsWith('.completed') || type === 'response.output_item.done'
|
|
@@ -1510,6 +1524,29 @@ const RESPONSE_ITEM_TYPES = new Set([
|
|
|
1510
1524
|
]);
|
|
1511
1525
|
// A freeform patch item can contain the complete 4 MiB patch plus its JSON envelope.
|
|
1512
1526
|
const MAX_RESPONSE_ITEM_BYTES = 5 * 1024 * 1024;
|
|
1527
|
+
function hostedWebActionInput(action) {
|
|
1528
|
+
const input = {};
|
|
1529
|
+
let truncated = false;
|
|
1530
|
+
for (const [key, limit] of [
|
|
1531
|
+
['type', 128],
|
|
1532
|
+
['query', 10_000],
|
|
1533
|
+
['url', 2_048],
|
|
1534
|
+
['pattern', 10_000]
|
|
1535
|
+
]) {
|
|
1536
|
+
const value = action[key];
|
|
1537
|
+
if (typeof value !== 'string')
|
|
1538
|
+
continue;
|
|
1539
|
+
input[key] = value.slice(0, limit);
|
|
1540
|
+
truncated ||= value.length > limit;
|
|
1541
|
+
}
|
|
1542
|
+
if (Array.isArray(action.queries)) {
|
|
1543
|
+
const queries = action.queries.filter((value) => typeof value === 'string');
|
|
1544
|
+
input.queries = queries.slice(0, 10).map((value) => value.slice(0, 10_000));
|
|
1545
|
+
truncated ||=
|
|
1546
|
+
queries.length > 10 || queries.slice(0, 10).some((value) => value.length > 10_000);
|
|
1547
|
+
}
|
|
1548
|
+
return { input, truncated };
|
|
1549
|
+
}
|
|
1513
1550
|
function hostedWebActionSummary(action) {
|
|
1514
1551
|
const queries = Array.isArray(action.queries)
|
|
1515
1552
|
? action.queries
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { ModelUsage } from '../model/contracts.js';
|
|
2
|
-
import type { MarAgentEvent } from './events.js';
|
|
1
|
+
import type { ModelEvent, ModelUsage } from '../model/contracts.js';
|
|
2
|
+
import type { EventPayload, MarAgentEvent } from './events.js';
|
|
3
3
|
/** Keep streaming deltas live-only; exec output has one durable copy in model context records. */
|
|
4
4
|
export declare function eventForPersistence(event: MarAgentEvent): MarAgentEvent | undefined;
|
|
5
5
|
export declare function publicModelUsage(usage: ModelUsage): Omit<ModelUsage, 'rolloutBudgetUnits'>;
|
|
6
|
+
/** Provider-hosted calls are ordinary public tools, never local execution requests. */
|
|
7
|
+
export declare function publicHostedToolEvents(event: Extract<ModelEvent, {
|
|
8
|
+
type: 'hosted_tool';
|
|
9
|
+
}>): readonly EventPayload[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AGENT_EXECUTION_POLICY } from './execution-policy.js';
|
|
1
2
|
/** Keep streaming deltas live-only; exec output has one durable copy in model context records. */
|
|
2
3
|
export function eventForPersistence(event) {
|
|
3
4
|
if (event.type === 'message.delta' ||
|
|
@@ -15,3 +16,47 @@ export function publicModelUsage(usage) {
|
|
|
15
16
|
void rolloutBudgetUnits;
|
|
16
17
|
return publicUsage;
|
|
17
18
|
}
|
|
19
|
+
/** Provider-hosted calls are ordinary public tools, never local execution requests. */
|
|
20
|
+
export function publicHostedToolEvents(event) {
|
|
21
|
+
const toolName = event.toolName ?? (event.kind === 'WEB_SEARCH' ? 'web_search' : 'web_fetch');
|
|
22
|
+
const legacyInput = {
|
|
23
|
+
...(event.summary.query === undefined ? {} : { query: event.summary.query }),
|
|
24
|
+
...(event.summary.url === undefined ? {} : { url: event.summary.url })
|
|
25
|
+
};
|
|
26
|
+
const input = event.input ?? (Object.keys(legacyInput).length ? legacyInput : undefined);
|
|
27
|
+
const legacyOutput = {
|
|
28
|
+
...(event.summary.title === undefined ? {} : { title: event.summary.title }),
|
|
29
|
+
...(event.summary.sources?.length ? { sources: event.summary.sources.slice(0, 20) } : {}),
|
|
30
|
+
...(event.summary.sourcesTruncated === true || (event.summary.sources?.length ?? 0) > 20
|
|
31
|
+
? { sourcesTruncated: true }
|
|
32
|
+
: {})
|
|
33
|
+
};
|
|
34
|
+
const output = event.output ?? (Object.keys(legacyOutput).length ? legacyOutput : undefined);
|
|
35
|
+
const text = typeof output === 'string' ? output : output === undefined ? '' : JSON.stringify(output);
|
|
36
|
+
const summary = text.slice(0, AGENT_EXECUTION_POLICY.toolEventSummaryCharacters);
|
|
37
|
+
const truncated = event.truncated === true ||
|
|
38
|
+
legacyOutput.sourcesTruncated === true ||
|
|
39
|
+
summary.length < text.length;
|
|
40
|
+
const started = {
|
|
41
|
+
type: 'tool.started',
|
|
42
|
+
toolName,
|
|
43
|
+
callId: event.itemId,
|
|
44
|
+
...(input === undefined ? {} : { input }),
|
|
45
|
+
...(truncated ? { truncated: true } : {})
|
|
46
|
+
};
|
|
47
|
+
if (event.status === 'RUNNING')
|
|
48
|
+
return [started];
|
|
49
|
+
if (event.status === 'FAILED')
|
|
50
|
+
return [
|
|
51
|
+
started,
|
|
52
|
+
{
|
|
53
|
+
type: 'tool.failed',
|
|
54
|
+
toolName,
|
|
55
|
+
callId: event.itemId,
|
|
56
|
+
code: 'HOSTED_TOOL_FAILED',
|
|
57
|
+
message: summary || 'Hosted tool failed.',
|
|
58
|
+
truncated
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
return [started, { type: 'tool.completed', toolName, callId: event.itemId, summary, truncated }];
|
|
62
|
+
}
|
|
@@ -46,6 +46,7 @@ export type MarAgentEvent = MarAgentEventBase & ({
|
|
|
46
46
|
type: 'reasoning.summary';
|
|
47
47
|
text: string;
|
|
48
48
|
} | {
|
|
49
|
+
/** Legacy persisted event; the Catalog exposes standard tool events instead. */
|
|
49
50
|
type: 'hosted_tool.updated';
|
|
50
51
|
itemId: string;
|
|
51
52
|
kind: 'WEB_SEARCH' | 'WEB_FETCH';
|
|
@@ -71,6 +72,7 @@ export type MarAgentEvent = MarAgentEventBase & ({
|
|
|
71
72
|
callId: string;
|
|
72
73
|
parentCallId?: string;
|
|
73
74
|
input?: unknown;
|
|
75
|
+
truncated?: boolean;
|
|
74
76
|
} | {
|
|
75
77
|
type: 'tool.completed';
|
|
76
78
|
toolName: string;
|
|
@@ -88,6 +90,7 @@ export type MarAgentEvent = MarAgentEventBase & ({
|
|
|
88
90
|
parentCallId?: string;
|
|
89
91
|
code: string;
|
|
90
92
|
message?: string;
|
|
93
|
+
truncated?: boolean;
|
|
91
94
|
} | {
|
|
92
95
|
type: 'usage.updated';
|
|
93
96
|
inputTokens?: number | undefined;
|
|
@@ -3,7 +3,7 @@ import { isProviderContextOverflow, recoverModelStream } from '../model/recovery
|
|
|
3
3
|
import { modelStreamEventObservedAt } from '../model/sse.js';
|
|
4
4
|
import { modelOutputContinuationMessage } from '../prompts/output.js';
|
|
5
5
|
import { executeRoundTools } from '../tools/execution.js';
|
|
6
|
-
import { publicModelUsage } from './event-projection.js';
|
|
6
|
+
import { publicHostedToolEvents, publicModelUsage } from './event-projection.js';
|
|
7
7
|
import { PlanOutputPrefix, splitPlanOutput } from './plan-output.js';
|
|
8
8
|
/** Own the single model/tool loop; the SDK owns its handle, terminal events and cleanup. */
|
|
9
9
|
export class ExecutionLoop {
|
|
@@ -215,13 +215,8 @@ export class ExecutionLoop {
|
|
|
215
215
|
}
|
|
216
216
|
else if (event.type === 'hosted_tool') {
|
|
217
217
|
if (mode !== 'compact')
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
itemId: event.itemId,
|
|
221
|
-
kind: event.kind,
|
|
222
|
-
status: event.status,
|
|
223
|
-
summary: event.summary
|
|
224
|
-
});
|
|
218
|
+
for (const toolEvent of publicHostedToolEvents(event))
|
|
219
|
+
await input.publish(toolEvent);
|
|
225
220
|
}
|
|
226
221
|
else if (event.type === 'citation') {
|
|
227
222
|
if (mode !== 'compact')
|
|
@@ -4,6 +4,8 @@ import type { SessionListInput, SessionSource } from './types.js';
|
|
|
4
4
|
export interface MarAgentSessionSummary {
|
|
5
5
|
id: string;
|
|
6
6
|
title: string;
|
|
7
|
+
/** First non-empty user input, whitespace-normalized and bounded to 160 characters. */
|
|
8
|
+
firstUserMessagePreview?: string;
|
|
7
9
|
workspace: string;
|
|
8
10
|
createdAt: string;
|
|
9
11
|
lastActivityAt: string;
|
|
@@ -54,6 +56,7 @@ export interface MarAgentSessionImage {
|
|
|
54
56
|
/** Public session catalog facade. Storage details remain SDK-private. */
|
|
55
57
|
export interface MarAgentSessionCatalog {
|
|
56
58
|
list(input?: SessionListInput): Promise<MarAgentSessionPage>;
|
|
59
|
+
firstUserMessagePreview(sessionId: string): Promise<string | undefined>;
|
|
57
60
|
executionConfiguration(sessionId: string): Promise<{
|
|
58
61
|
readonly modelId: string;
|
|
59
62
|
readonly modelName?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MarAgentError } from '../error.js';
|
|
2
|
+
import { publicHostedToolEvents } from '../runtime/event-projection.js';
|
|
2
3
|
import { JsonlSessionStore } from './jsonl-store.js';
|
|
3
4
|
const MAX_INPUT_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
4
5
|
const INPUT_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
@@ -6,6 +7,7 @@ export async function openMarAgentSessionCatalog(input) {
|
|
|
6
7
|
const store = await JsonlSessionStore.open(input.homeDirectory);
|
|
7
8
|
return {
|
|
8
9
|
list: (options) => store.list(options),
|
|
10
|
+
firstUserMessagePreview: (sessionId) => store.firstUserMessagePreview(sessionId),
|
|
9
11
|
executionConfiguration: (sessionId) => store.executionConfiguration(sessionId),
|
|
10
12
|
history: async (sessionId, options = {}) => {
|
|
11
13
|
const beforeSequence = parseCursor(options.cursor);
|
|
@@ -39,6 +41,22 @@ export async function openMarAgentSessionCatalog(input) {
|
|
|
39
41
|
}
|
|
40
42
|
];
|
|
41
43
|
}
|
|
44
|
+
if (isMarAgentEvent(record.payload) && record.payload.type === 'hosted_tool.updated') {
|
|
45
|
+
const legacy = record.payload;
|
|
46
|
+
return publicHostedToolEvents({ ...legacy, type: 'hosted_tool' }).map((event) => ({
|
|
47
|
+
kind: 'event',
|
|
48
|
+
sequence: record.sequence,
|
|
49
|
+
event: {
|
|
50
|
+
sequence: legacy.sequence,
|
|
51
|
+
timestamp: legacy.timestamp,
|
|
52
|
+
sessionId: legacy.sessionId,
|
|
53
|
+
executionId: legacy.executionId,
|
|
54
|
+
...(legacy.turnId === undefined ? {} : { turnId: legacy.turnId }),
|
|
55
|
+
...event,
|
|
56
|
+
eventId: `${legacy.eventId}:${event.type}`
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
42
60
|
if (isMarAgentEvent(record.payload))
|
|
43
61
|
return [{ kind: 'event', sequence: record.sequence, event: record.payload }];
|
|
44
62
|
return [];
|
|
@@ -64,6 +64,8 @@ interface SessionMeta {
|
|
|
64
64
|
todoSequence?: number;
|
|
65
65
|
todoRevision: number;
|
|
66
66
|
executionModelIndexed?: true;
|
|
67
|
+
firstUserMessageIndexed?: true;
|
|
68
|
+
firstUserMessagePreview?: string;
|
|
67
69
|
modelId?: string;
|
|
68
70
|
modelName?: string;
|
|
69
71
|
reasoningEffort?: string;
|
|
@@ -184,12 +186,13 @@ export declare class JsonlSessionStore {
|
|
|
184
186
|
reasoningEffort?: string;
|
|
185
187
|
modelName?: string;
|
|
186
188
|
modelId?: string;
|
|
187
|
-
id: string;
|
|
188
|
-
title: string;
|
|
189
189
|
workspace: string;
|
|
190
190
|
createdAt: string;
|
|
191
191
|
lastActivityAt: string;
|
|
192
192
|
source: SessionSource;
|
|
193
|
+
firstUserMessagePreview?: string;
|
|
194
|
+
id: string;
|
|
195
|
+
title: string;
|
|
193
196
|
}[];
|
|
194
197
|
}>;
|
|
195
198
|
listSubagentChildren(parentSessionId: string): Promise<{
|
|
@@ -216,6 +219,7 @@ export declare class JsonlSessionStore {
|
|
|
216
219
|
readonly reasoningEffort?: string;
|
|
217
220
|
} | undefined>;
|
|
218
221
|
sessionTree(sessionId: string): Promise<readonly string[]>;
|
|
222
|
+
firstUserMessagePreview(sessionId: string): Promise<string | undefined>;
|
|
219
223
|
delete(sessionId: string): Promise<{
|
|
220
224
|
readonly deleted: boolean;
|
|
221
225
|
}>;
|
|
@@ -158,6 +158,7 @@ export class JsonlSessionStore {
|
|
|
158
158
|
closed: false,
|
|
159
159
|
indexDirty: false,
|
|
160
160
|
dirtySessionVersions: new Map(),
|
|
161
|
+
previewOnlyDirtySessions: new Set(),
|
|
161
162
|
pendingPublishIds: new Set(),
|
|
162
163
|
codeStateWrites: new Map(),
|
|
163
164
|
recordWrites: new Map(),
|
|
@@ -176,19 +177,26 @@ export class JsonlSessionStore {
|
|
|
176
177
|
if (!disk)
|
|
177
178
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session index is missing.');
|
|
178
179
|
const dirty = new Map(this.#shared.dirtySessionVersions);
|
|
180
|
+
const previewOnly = new Set(this.#shared.previewOnlyDirtySessions);
|
|
179
181
|
const sessions = new Map(disk.sessions.map((session) => [session.id, session]));
|
|
180
182
|
const created = new Set(createdIds);
|
|
181
183
|
for (const session of this.#shared.index.sessions) {
|
|
182
184
|
const current = sessions.get(session.id);
|
|
183
185
|
// Disk membership is authoritative: a stale in-memory snapshot cannot revive a deletion.
|
|
184
186
|
if (created.has(session.id) || (current && dirty.has(session.id)))
|
|
185
|
-
sessions.set(session.id,
|
|
187
|
+
sessions.set(session.id, current && previewOnly.has(session.id)
|
|
188
|
+
? mergeFirstUserMessagePreviewCache(current, session)
|
|
189
|
+
: structuredClone(session));
|
|
186
190
|
}
|
|
187
191
|
for (const deletedId of typeof deletedIds === 'string' ? [deletedIds] : (deletedIds ?? []))
|
|
188
192
|
sessions.delete(deletedId);
|
|
189
193
|
const updated = {
|
|
190
194
|
version: 1,
|
|
191
|
-
revision:
|
|
195
|
+
revision: createdIds.length > 0 ||
|
|
196
|
+
deletedIds !== undefined ||
|
|
197
|
+
[...dirty.keys()].some((id) => !previewOnly.has(id))
|
|
198
|
+
? Math.max(disk.revision, this.#shared.index.revision) + 1
|
|
199
|
+
: disk.revision,
|
|
192
200
|
sessions: [...sessions.values()]
|
|
193
201
|
};
|
|
194
202
|
await persistSessionIndex(path, updated);
|
|
@@ -196,8 +204,10 @@ export class JsonlSessionStore {
|
|
|
196
204
|
// A different creation still needs its own index transaction to publish its directory.
|
|
197
205
|
if (this.#shared.pendingPublishIds.has(id) && !created.has(id))
|
|
198
206
|
continue;
|
|
199
|
-
if (this.#shared.dirtySessionVersions.get(id) === version)
|
|
207
|
+
if (this.#shared.dirtySessionVersions.get(id) === version) {
|
|
200
208
|
this.#shared.dirtySessionVersions.delete(id);
|
|
209
|
+
this.#shared.previewOnlyDirtySessions.delete(id);
|
|
210
|
+
}
|
|
201
211
|
}
|
|
202
212
|
for (const id of created)
|
|
203
213
|
this.#shared.pendingPublishIds.delete(id);
|
|
@@ -234,7 +244,11 @@ export class JsonlSessionStore {
|
|
|
234
244
|
sessions: disk.sessions
|
|
235
245
|
.map((session) => {
|
|
236
246
|
const current = local.get(session.id);
|
|
237
|
-
|
|
247
|
+
if (!current || !this.#shared.dirtySessionVersions.has(session.id))
|
|
248
|
+
return session;
|
|
249
|
+
return this.#shared.previewOnlyDirtySessions.has(session.id)
|
|
250
|
+
? mergeFirstUserMessagePreviewCache(session, current)
|
|
251
|
+
: current;
|
|
238
252
|
})
|
|
239
253
|
.concat(this.#shared.index.sessions.filter((session) => this.#shared.pendingPublishIds.has(session.id) && !published.has(session.id)))
|
|
240
254
|
};
|
|
@@ -272,7 +286,12 @@ export class JsonlSessionStore {
|
|
|
272
286
|
source: normalizeSessionSource(input.source),
|
|
273
287
|
nextSequence: 1,
|
|
274
288
|
automaticTitlePending: input.title === undefined,
|
|
275
|
-
resume: {
|
|
289
|
+
resume: {
|
|
290
|
+
version: 1,
|
|
291
|
+
todoRevision: 0,
|
|
292
|
+
executionModelIndexed: true,
|
|
293
|
+
firstUserMessageIndexed: true
|
|
294
|
+
},
|
|
276
295
|
segments: []
|
|
277
296
|
};
|
|
278
297
|
const publication = await this.#beginUnpublishedSession(id);
|
|
@@ -520,7 +539,12 @@ export class JsonlSessionStore {
|
|
|
520
539
|
source: { type: 'user' },
|
|
521
540
|
nextSequence: 1,
|
|
522
541
|
automaticTitlePending: false,
|
|
523
|
-
resume: {
|
|
542
|
+
resume: {
|
|
543
|
+
version: 1,
|
|
544
|
+
todoRevision: 0,
|
|
545
|
+
executionModelIndexed: true,
|
|
546
|
+
firstUserMessageIndexed: true
|
|
547
|
+
},
|
|
524
548
|
segments: []
|
|
525
549
|
};
|
|
526
550
|
const publication = await this.#beginUnpublishedSession(target.id);
|
|
@@ -852,7 +876,7 @@ export class JsonlSessionStore {
|
|
|
852
876
|
meta.lastActivityAt = clearedAt;
|
|
853
877
|
meta.nextSequence = 2;
|
|
854
878
|
meta.storageTransactionId = transactionId;
|
|
855
|
-
meta.resume = { version: 1, todoRevision: 0 };
|
|
879
|
+
meta.resume = { version: 1, todoRevision: 0, firstUserMessageIndexed: true };
|
|
856
880
|
meta.segments = staged.segments;
|
|
857
881
|
this.#shared.segments.set(sessionId, 1);
|
|
858
882
|
this.#shared.index.revision++;
|
|
@@ -1316,6 +1340,9 @@ export class JsonlSessionStore {
|
|
|
1316
1340
|
const sessions = ordered.slice(start, start + limit).map((session) => ({
|
|
1317
1341
|
id: session.id,
|
|
1318
1342
|
title: session.title,
|
|
1343
|
+
...(session.resume?.firstUserMessagePreview === undefined
|
|
1344
|
+
? {}
|
|
1345
|
+
: { firstUserMessagePreview: session.resume.firstUserMessagePreview }),
|
|
1319
1346
|
workspace: session.workspace,
|
|
1320
1347
|
createdAt: session.createdAt,
|
|
1321
1348
|
lastActivityAt: session.lastActivityAt,
|
|
@@ -1403,6 +1430,11 @@ export class JsonlSessionStore {
|
|
|
1403
1430
|
const resume = meta.resume;
|
|
1404
1431
|
if (resume?.version !== 1 || !Number.isSafeInteger(resume.todoRevision))
|
|
1405
1432
|
return;
|
|
1433
|
+
if (resume.firstUserMessageIndexed === true && resume.firstUserMessagePreview === undefined) {
|
|
1434
|
+
const preview = userMessagePreviewFromRecord(record);
|
|
1435
|
+
if (preview !== undefined)
|
|
1436
|
+
resume.firstUserMessagePreview = preview;
|
|
1437
|
+
}
|
|
1406
1438
|
const executionModel = executionModelFromRecord(record);
|
|
1407
1439
|
if (executionModel !== undefined) {
|
|
1408
1440
|
resume.executionModelIndexed = true;
|
|
@@ -1488,6 +1520,56 @@ export class JsonlSessionStore {
|
|
|
1488
1520
|
contextRecords: state.contextRecords
|
|
1489
1521
|
};
|
|
1490
1522
|
}
|
|
1523
|
+
async firstUserMessagePreview(sessionId) {
|
|
1524
|
+
this.#assertOpen();
|
|
1525
|
+
await this.#refreshIndex();
|
|
1526
|
+
await this.#ensureFirstUserMessageIndexed(sessionId);
|
|
1527
|
+
return this.#shared.index.sessions.find((session) => session.id === sessionId)?.resume
|
|
1528
|
+
?.firstUserMessagePreview;
|
|
1529
|
+
}
|
|
1530
|
+
async #ensureFirstUserMessageIndexed(sessionId) {
|
|
1531
|
+
for (;;) {
|
|
1532
|
+
const before = this.#shared.index.sessions.find((session) => session.id === sessionId);
|
|
1533
|
+
if (before === undefined)
|
|
1534
|
+
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
1535
|
+
if (before.resume?.firstUserMessageIndexed === true)
|
|
1536
|
+
return;
|
|
1537
|
+
const nextSequence = before.nextSequence;
|
|
1538
|
+
const transactionId = before.storageTransactionId;
|
|
1539
|
+
let preview;
|
|
1540
|
+
try {
|
|
1541
|
+
for (const path of await this.#segmentPaths(sessionId)) {
|
|
1542
|
+
for await (const record of readRecordsFromHead(path)) {
|
|
1543
|
+
preview = userMessagePreviewFromRecord(record);
|
|
1544
|
+
if (preview !== undefined)
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
if (preview !== undefined)
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
catch (error) {
|
|
1552
|
+
if (error.code !== 'ENOENT' &&
|
|
1553
|
+
!(error instanceof MarAgentError && error.code === 'MAR_AGENT_SESSION_NOT_FOUND'))
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
await this.#refreshIndex();
|
|
1557
|
+
const current = this.#shared.index.sessions.find((session) => session.id === sessionId);
|
|
1558
|
+
if (current === undefined)
|
|
1559
|
+
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
1560
|
+
if (current.resume?.firstUserMessageIndexed === true)
|
|
1561
|
+
return;
|
|
1562
|
+
if (current.nextSequence !== nextSequence || current.storageTransactionId !== transactionId)
|
|
1563
|
+
continue;
|
|
1564
|
+
const resume = (current.resume ??= { version: 1, todoRevision: 0 });
|
|
1565
|
+
resume.firstUserMessageIndexed = true;
|
|
1566
|
+
if (preview !== undefined)
|
|
1567
|
+
resume.firstUserMessagePreview = preview;
|
|
1568
|
+
// Derived preview caches do not change list membership, ordering or filters.
|
|
1569
|
+
this.#markIndexDirty(sessionId, true);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1491
1573
|
async #ensureExecutionModelIndexed(sessionId) {
|
|
1492
1574
|
const pending = this.#shared.executionModelReads.get(sessionId);
|
|
1493
1575
|
if (pending !== undefined)
|
|
@@ -1586,7 +1668,11 @@ export class JsonlSessionStore {
|
|
|
1586
1668
|
if (this.#leaseClosed || this.#shared.closed)
|
|
1587
1669
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Session store is closed.');
|
|
1588
1670
|
}
|
|
1589
|
-
#markIndexDirty(sessionId) {
|
|
1671
|
+
#markIndexDirty(sessionId, previewOnly = false) {
|
|
1672
|
+
if (!previewOnly)
|
|
1673
|
+
this.#shared.previewOnlyDirtySessions.delete(sessionId);
|
|
1674
|
+
else if (!this.#shared.dirtySessionVersions.has(sessionId))
|
|
1675
|
+
this.#shared.previewOnlyDirtySessions.add(sessionId);
|
|
1590
1676
|
this.#shared.indexDirty = true;
|
|
1591
1677
|
this.#shared.dirtySessionVersions.set(sessionId, (this.#shared.dirtySessionVersions.get(sessionId) ?? 0) + 1);
|
|
1592
1678
|
}
|
|
@@ -1679,6 +1765,26 @@ export class JsonlSessionStore {
|
|
|
1679
1765
|
JsonlSessionStore.#instances.delete(this.#shared.root);
|
|
1680
1766
|
}
|
|
1681
1767
|
}
|
|
1768
|
+
/** Merge a derived cache only onto the unchanged history it was computed from. */
|
|
1769
|
+
function mergeFirstUserMessagePreviewCache(current, cached) {
|
|
1770
|
+
if (cached.resume?.firstUserMessageIndexed !== true ||
|
|
1771
|
+
current.nextSequence !== cached.nextSequence ||
|
|
1772
|
+
current.storageTransactionId !== cached.storageTransactionId ||
|
|
1773
|
+
current.resume?.firstUserMessagePreview !== undefined ||
|
|
1774
|
+
(current.resume?.firstUserMessageIndexed === true &&
|
|
1775
|
+
cached.resume.firstUserMessagePreview === undefined))
|
|
1776
|
+
return current;
|
|
1777
|
+
return {
|
|
1778
|
+
...current,
|
|
1779
|
+
resume: {
|
|
1780
|
+
...(current.resume ?? { version: 1, todoRevision: 0 }),
|
|
1781
|
+
firstUserMessageIndexed: true,
|
|
1782
|
+
...(cached.resume?.firstUserMessagePreview === undefined
|
|
1783
|
+
? {}
|
|
1784
|
+
: { firstUserMessagePreview: cached.resume.firstUserMessagePreview })
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1682
1788
|
async function readSessionIndex(path) {
|
|
1683
1789
|
let raw;
|
|
1684
1790
|
try {
|
|
@@ -2157,7 +2263,9 @@ function deriveExecutionState(records) {
|
|
|
2157
2263
|
let todoRevision = 0;
|
|
2158
2264
|
let todoItems = [];
|
|
2159
2265
|
let executionModel;
|
|
2266
|
+
let firstUserMessagePreview;
|
|
2160
2267
|
for (const record of records) {
|
|
2268
|
+
firstUserMessagePreview ??= userMessagePreviewFromRecord(record);
|
|
2161
2269
|
if (record.type === 'session.created')
|
|
2162
2270
|
automaticTitle =
|
|
2163
2271
|
record.payload?.automaticTitle === true;
|
|
@@ -2198,10 +2306,21 @@ function deriveExecutionState(records) {
|
|
|
2198
2306
|
...(todoSequence === undefined ? {} : { todoSequence }),
|
|
2199
2307
|
todoRevision,
|
|
2200
2308
|
executionModelIndexed: true,
|
|
2309
|
+
firstUserMessageIndexed: true,
|
|
2310
|
+
...(firstUserMessagePreview === undefined ? {} : { firstUserMessagePreview }),
|
|
2201
2311
|
...(executionModel === undefined ? {} : executionModel)
|
|
2202
2312
|
}
|
|
2203
2313
|
};
|
|
2204
2314
|
}
|
|
2315
|
+
function userMessagePreviewFromRecord(record) {
|
|
2316
|
+
if (record.type !== 'turn.user' ||
|
|
2317
|
+
!isObject(record.payload) ||
|
|
2318
|
+
record.payload.inputSource === 'parent_agent' ||
|
|
2319
|
+
typeof record.payload.prompt !== 'string')
|
|
2320
|
+
return undefined;
|
|
2321
|
+
const preview = record.payload.prompt.trim().replace(/\s+/gu, ' ').slice(0, 160);
|
|
2322
|
+
return preview || undefined;
|
|
2323
|
+
}
|
|
2205
2324
|
function executionModelFromRecord(record) {
|
|
2206
2325
|
if (record.type !== 'execution.header' || !isObject(record.payload))
|
|
2207
2326
|
return undefined;
|
|
@@ -2254,10 +2373,63 @@ function sameResumeIndex(left, right) {
|
|
|
2254
2373
|
left.todoSequence === right.todoSequence &&
|
|
2255
2374
|
left.todoRevision === right.todoRevision &&
|
|
2256
2375
|
left.executionModelIndexed === right.executionModelIndexed &&
|
|
2376
|
+
left.firstUserMessageIndexed === right.firstUserMessageIndexed &&
|
|
2377
|
+
left.firstUserMessagePreview === right.firstUserMessagePreview &&
|
|
2257
2378
|
left.modelId === right.modelId &&
|
|
2258
2379
|
left.modelName === right.modelName &&
|
|
2259
2380
|
left.reasoningEffort === right.reasoningEffort);
|
|
2260
2381
|
}
|
|
2382
|
+
/** Read only the prefix needed by discovery; never load the entire transcript. */
|
|
2383
|
+
async function* readRecordsFromHead(path) {
|
|
2384
|
+
const handle = await open(path, 'r');
|
|
2385
|
+
try {
|
|
2386
|
+
const { size } = await handle.stat();
|
|
2387
|
+
const chunk = Buffer.allocUnsafe(REVERSE_READ_CHUNK_BYTES);
|
|
2388
|
+
let position = 0;
|
|
2389
|
+
let pending = Buffer.alloc(0);
|
|
2390
|
+
while (position < size) {
|
|
2391
|
+
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, size - position), position);
|
|
2392
|
+
if (bytesRead === 0)
|
|
2393
|
+
break;
|
|
2394
|
+
position += bytesRead;
|
|
2395
|
+
const data = Buffer.concat([pending, chunk.subarray(0, bytesRead)]);
|
|
2396
|
+
let start = 0;
|
|
2397
|
+
for (;;) {
|
|
2398
|
+
const end = data.indexOf(0x0a, start);
|
|
2399
|
+
if (end < 0)
|
|
2400
|
+
break;
|
|
2401
|
+
const line = data.subarray(start, end).toString('utf8');
|
|
2402
|
+
start = end + 1;
|
|
2403
|
+
if (!line)
|
|
2404
|
+
continue;
|
|
2405
|
+
let record;
|
|
2406
|
+
try {
|
|
2407
|
+
record = JSON.parse(line);
|
|
2408
|
+
}
|
|
2409
|
+
catch (cause) {
|
|
2410
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
|
|
2411
|
+
cause
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
yield record;
|
|
2415
|
+
}
|
|
2416
|
+
pending = Buffer.from(data.subarray(start));
|
|
2417
|
+
}
|
|
2418
|
+
if (pending.length > 0) {
|
|
2419
|
+
let record;
|
|
2420
|
+
try {
|
|
2421
|
+
record = JSON.parse(pending.toString('utf8'));
|
|
2422
|
+
}
|
|
2423
|
+
catch {
|
|
2424
|
+
return; // Readers ignore only an interrupted final line.
|
|
2425
|
+
}
|
|
2426
|
+
yield record;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
finally {
|
|
2430
|
+
await handle.close();
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2261
2433
|
async function readRecords(path) {
|
|
2262
2434
|
const value = await readFile(path, 'utf8');
|
|
2263
2435
|
const lines = value.split('\n');
|
|
@@ -13,6 +13,18 @@ export async function createCodeModeEngine(options) {
|
|
|
13
13
|
if (disposed || runtime)
|
|
14
14
|
throw diagnostic('MAR_AGENT_CODE_FAILED', 'Engine is not reusable.');
|
|
15
15
|
runtime = quickjs.newRuntime();
|
|
16
|
+
// QuickJS 0.32.0 caches the job's context-pointer view across WASM memory growth
|
|
17
|
+
// (upstream #240). A detached view creates an unowned Context and breaks disposal.
|
|
18
|
+
// Keep this dependency-specific correction inside the worker engine; preserve its
|
|
19
|
+
// pointer allocation/free lifecycle and always read from the current heap.
|
|
20
|
+
const newMutablePointerArray = runtime.memory.newMutablePointerArray.bind(runtime.memory);
|
|
21
|
+
runtime.memory.newMutablePointerArray = (length) => {
|
|
22
|
+
const pointer = newMutablePointerArray(length);
|
|
23
|
+
Object.defineProperty(pointer.value, 'typedArray', {
|
|
24
|
+
get: () => new Int32Array(runtime.module.HEAP8.buffer, pointer.value.ptr, length)
|
|
25
|
+
});
|
|
26
|
+
return pointer;
|
|
27
|
+
};
|
|
16
28
|
runtime.setMemoryLimit(input.limits.memoryBytes);
|
|
17
29
|
runtime.setMaxStackSize(input.limits.stackBytes);
|
|
18
30
|
let executionMs = 0;
|