@myagentroam/agent 0.10.59 → 0.10.61
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/runtime/compaction/history.d.ts +1 -1
- package/dist/agent-core/runtime/compaction/retained-context.d.ts +1 -1
- package/dist/agent-core/runtime/compaction/retained-context.js +1 -1
- package/dist/agent-core/runtime/compaction/smart.d.ts +1 -1
- package/dist/agent-core/runtime/compaction/smart.js +1 -1
- package/dist/agent-core/runtime/context/gc-runtime.d.ts +1 -1
- package/dist/agent-core/runtime/context/gc-runtime.js +20 -14
- package/dist/agent-core/runtime/context/gc.d.ts +1 -1
- package/dist/agent-core/runtime/context/gc.js +4 -10
- package/dist/agent-core/runtime/context/projection.d.ts +1 -1
- package/dist/agent-core/runtime/context/projection.js +3 -5
- package/dist/agent-core/runtime/context/restore.d.ts +4 -4
- package/dist/agent-core/runtime/context/restore.js +1 -1
- package/dist/agent-core/runtime/context/usage.d.ts +2 -2
- package/dist/agent-core/runtime/context/usage.js +1 -1
- package/dist/agent-core/runtime/context/world-state.d.ts +2 -2
- package/dist/agent-core/runtime/context/world-state.js +1 -1
- package/dist/agent-core/sdk/agent.js +2 -2
- package/dist/agent-core/sdk/extension-agents.js +5 -1
- package/dist/agent-core/sdk/native-steer.d.ts +1 -1
- package/dist/agent-core/sdk/session.d.ts +19 -13
- package/dist/agent-core/sdk/session.js +35 -40
- package/dist/agent-core/sdk/subagent-backend.d.ts +1 -1
- package/dist/agent-core/sdk/subagent-backend.js +20 -17
- package/dist/agent-core/session/catalog.js +1 -1
- package/dist/agent-core/session/context-gc-checkpoint.d.ts +4 -8
- package/dist/agent-core/session/context-gc-checkpoint.js +82 -137
- package/dist/agent-core/session/context-gc-observations.d.ts +1 -1
- package/dist/agent-core/session/context-gc-replacements.d.ts +4 -7
- package/dist/agent-core/session/context-gc-replacements.js +25 -59
- package/dist/agent-core/session/context-gc-subagent.d.ts +0 -3
- package/dist/agent-core/session/context-gc-subagent.js +2 -16
- package/dist/agent-core/session/deferred-input.d.ts +1 -1
- package/dist/agent-core/session/store/current-state.d.ts +17 -0
- package/dist/agent-core/session/store/current-state.js +53 -0
- package/dist/agent-core/session/store/directory-index.d.ts +18 -0
- package/dist/agent-core/session/store/directory-index.js +169 -0
- package/dist/agent-core/session/store/execution-state.d.ts +18 -0
- package/dist/agent-core/session/store/execution-state.js +187 -0
- package/dist/agent-core/session/store/history.d.ts +25 -0
- package/dist/agent-core/session/store/history.js +157 -0
- package/dist/agent-core/session/store/images.d.ts +21 -0
- package/dist/agent-core/session/store/images.js +248 -0
- package/dist/agent-core/session/store/index.d.ts +4 -0
- package/dist/agent-core/session/store/index.js +2 -0
- package/dist/agent-core/session/{jsonl-store.d.ts → store/jsonl-store.d.ts} +14 -94
- package/dist/agent-core/session/{jsonl-store.js → store/jsonl-store.js} +323 -1269
- package/dist/agent-core/session/store/records.d.ts +23 -0
- package/dist/agent-core/session/store/records.js +249 -0
- package/dist/agent-core/session/store/types.d.ts +84 -0
- package/dist/agent-core/session/store/types.js +1 -0
- package/dist/agent-core/session/tool-result-resources.d.ts +1 -1
- package/dist/agent-core/subagent/scheduler.js +1 -1
- package/dist/agent-core/subagent/session-controller.d.ts +5 -2
- package/dist/agent-core/subagent/session-controller.js +99 -15
- package/dist/agent-core/tools/code-mode/state.d.ts +1 -1
- package/dist/agent-core/tools/code-mode/state.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SessionRecord } from './types.js';
|
|
2
|
+
export interface HistoryPagePositions {
|
|
3
|
+
readonly signature: string;
|
|
4
|
+
readonly offsets: Map<number, number>;
|
|
5
|
+
}
|
|
6
|
+
export declare const MAX_SEGMENT_BYTES: number;
|
|
7
|
+
/** Read only the prefix needed by discovery; never load the entire transcript. */
|
|
8
|
+
export declare function readRecordsFromHead(path: string): AsyncGenerator<SessionRecord>;
|
|
9
|
+
export declare function readRecords(path: string): Promise<SessionRecord[]>;
|
|
10
|
+
/** Repair only the final record under the Session writer lock; never skip middle damage. */
|
|
11
|
+
export declare function repairSegmentTail(path: string): Promise<void>;
|
|
12
|
+
export declare function readRecordPageFromTail(path: string, beforeSequence: number, limit: number, positions: Map<string, HistoryPagePositions>): Promise<{
|
|
13
|
+
records: SessionRecord[];
|
|
14
|
+
hasEarlier: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
/** Decode complete records newest-first, releasing the file as soon as a reader stops. */
|
|
17
|
+
export declare function readRecordsFromTail(path: string, page?: {
|
|
18
|
+
readonly beforeSequence: number;
|
|
19
|
+
readonly limit: number;
|
|
20
|
+
readonly positions: Map<string, HistoryPagePositions>;
|
|
21
|
+
}): AsyncGenerator<SessionRecord>;
|
|
22
|
+
export declare function segmentNumberFromPath(path: string): number | undefined;
|
|
23
|
+
export declare function appendRecordBytes(path: string, serialized: string): Promise<void>;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { open, readFile } from 'node:fs/promises';
|
|
2
|
+
import { MarAgentError } from '../../error.js';
|
|
3
|
+
export const MAX_SEGMENT_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
const REVERSE_READ_CHUNK_BYTES = 64 * 1024;
|
|
5
|
+
const MAX_HISTORY_PAGE_FILES = 32;
|
|
6
|
+
const MAX_HISTORY_PAGE_OFFSETS = 128;
|
|
7
|
+
/** Read only the prefix needed by discovery; never load the entire transcript. */
|
|
8
|
+
export async function* readRecordsFromHead(path) {
|
|
9
|
+
const handle = await open(path, 'r');
|
|
10
|
+
try {
|
|
11
|
+
const { size } = await handle.stat();
|
|
12
|
+
let position = 0;
|
|
13
|
+
let pending = [];
|
|
14
|
+
while (position < size) {
|
|
15
|
+
const chunk = Buffer.allocUnsafe(Math.min(REVERSE_READ_CHUNK_BYTES, size - position));
|
|
16
|
+
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, size - position), position);
|
|
17
|
+
if (bytesRead === 0)
|
|
18
|
+
break;
|
|
19
|
+
position += bytesRead;
|
|
20
|
+
const data = chunk.subarray(0, bytesRead);
|
|
21
|
+
let start = 0;
|
|
22
|
+
for (;;) {
|
|
23
|
+
const end = data.indexOf(0x0a, start);
|
|
24
|
+
if (end < 0)
|
|
25
|
+
break;
|
|
26
|
+
const part = data.subarray(start, end);
|
|
27
|
+
const line = (pending.length === 0 ? part : Buffer.concat([...pending, part])).toString('utf8');
|
|
28
|
+
pending = [];
|
|
29
|
+
start = end + 1;
|
|
30
|
+
if (!line)
|
|
31
|
+
continue;
|
|
32
|
+
let record;
|
|
33
|
+
try {
|
|
34
|
+
record = JSON.parse(line);
|
|
35
|
+
}
|
|
36
|
+
catch (cause) {
|
|
37
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
|
|
38
|
+
cause
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
yield record;
|
|
42
|
+
}
|
|
43
|
+
if (start < data.length)
|
|
44
|
+
pending.push(data.subarray(start));
|
|
45
|
+
}
|
|
46
|
+
if (pending.length > 0) {
|
|
47
|
+
let record;
|
|
48
|
+
try {
|
|
49
|
+
record = JSON.parse(Buffer.concat(pending).toString('utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return; // Readers ignore only an interrupted final line.
|
|
53
|
+
}
|
|
54
|
+
yield record;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await handle.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function readRecords(path) {
|
|
62
|
+
const value = await readFile(path, 'utf8');
|
|
63
|
+
const lines = value.split('\n');
|
|
64
|
+
if (lines.at(-1) === '')
|
|
65
|
+
lines.pop();
|
|
66
|
+
const records = [];
|
|
67
|
+
for (const [index, line] of lines.entries()) {
|
|
68
|
+
if (!line)
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
records.push(JSON.parse(line));
|
|
72
|
+
}
|
|
73
|
+
catch (cause) {
|
|
74
|
+
if (index === lines.length - 1 && !value.endsWith('\n'))
|
|
75
|
+
break;
|
|
76
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
|
|
77
|
+
cause
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return records;
|
|
82
|
+
}
|
|
83
|
+
/** Repair only the final record under the Session writer lock; never skip middle damage. */
|
|
84
|
+
export async function repairSegmentTail(path) {
|
|
85
|
+
const data = await readFile(path);
|
|
86
|
+
if (data.length === 0 || data.at(-1) === 10)
|
|
87
|
+
return;
|
|
88
|
+
const boundary = data.lastIndexOf(10);
|
|
89
|
+
let complete = false;
|
|
90
|
+
try {
|
|
91
|
+
const record = JSON.parse(data.subarray(boundary + 1).toString('utf8'));
|
|
92
|
+
complete =
|
|
93
|
+
record?.schemaVersion === 1 &&
|
|
94
|
+
Number.isSafeInteger(record.sequence) &&
|
|
95
|
+
typeof record.sessionId === 'string' &&
|
|
96
|
+
typeof record.type === 'string';
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// An interrupted final line is discarded before another record can be appended.
|
|
100
|
+
}
|
|
101
|
+
const handle = await open(path, 'r+');
|
|
102
|
+
try {
|
|
103
|
+
if (complete) {
|
|
104
|
+
const { bytesWritten } = await handle.write(Buffer.from('\n'), 0, 1, data.length);
|
|
105
|
+
if (bytesWritten !== 1)
|
|
106
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session tail repair is incomplete.');
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
await handle.truncate(boundary + 1);
|
|
110
|
+
await handle.sync();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await handle.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function readRecordPageFromTail(path, beforeSequence, limit, positions) {
|
|
117
|
+
const newestFirst = [];
|
|
118
|
+
for await (const record of readRecordsFromTail(path, { beforeSequence, limit, positions })) {
|
|
119
|
+
if (!(record.sequence < beforeSequence))
|
|
120
|
+
continue;
|
|
121
|
+
newestFirst.push(record);
|
|
122
|
+
if (newestFirst.length > limit)
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
records: newestFirst.slice(0, limit).reverse(),
|
|
127
|
+
hasEarlier: newestFirst.length > limit
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Decode complete records newest-first, releasing the file as soon as a reader stops. */
|
|
131
|
+
export async function* readRecordsFromTail(path, page) {
|
|
132
|
+
const handle = await open(path, 'r');
|
|
133
|
+
try {
|
|
134
|
+
const metadata = await handle.stat({ bigint: true });
|
|
135
|
+
const size = Number(metadata.size);
|
|
136
|
+
let position = size;
|
|
137
|
+
let cached;
|
|
138
|
+
if (page !== undefined) {
|
|
139
|
+
const signature = `${metadata.dev}:${metadata.ino}:${metadata.size}:${metadata.mtimeNs}:${metadata.ctimeNs}`;
|
|
140
|
+
const previous = page.positions.get(path);
|
|
141
|
+
cached = previous?.signature === signature ? previous : { signature, offsets: new Map() };
|
|
142
|
+
page.positions.delete(path);
|
|
143
|
+
page.positions.set(path, cached);
|
|
144
|
+
if (page.positions.size > MAX_HISTORY_PAGE_FILES)
|
|
145
|
+
page.positions.delete(page.positions.keys().next().value);
|
|
146
|
+
let nearestSequence = Number.POSITIVE_INFINITY;
|
|
147
|
+
for (const [sequence, offset] of cached.offsets) {
|
|
148
|
+
if (sequence >= page.beforeSequence && sequence < nearestSequence) {
|
|
149
|
+
nearestSequence = sequence;
|
|
150
|
+
position = offset;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let accepted = 0;
|
|
155
|
+
const remember = (record, offset) => {
|
|
156
|
+
if (page === undefined || cached === undefined || record.sequence >= page.beforeSequence)
|
|
157
|
+
return;
|
|
158
|
+
if (++accepted !== page.limit)
|
|
159
|
+
return;
|
|
160
|
+
cached.offsets.set(record.sequence, offset);
|
|
161
|
+
if (cached.offsets.size > MAX_HISTORY_PAGE_OFFSETS)
|
|
162
|
+
cached.offsets.delete(cached.offsets.keys().next().value);
|
|
163
|
+
};
|
|
164
|
+
let pendingLineParts = [];
|
|
165
|
+
let invalidFinalLineMayBeIgnored = false;
|
|
166
|
+
let inspectedFileEnd = position !== size;
|
|
167
|
+
const decode = (line, allowInvalidFinalLine) => {
|
|
168
|
+
if (line.length === 0)
|
|
169
|
+
return undefined;
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(line.toString('utf8'));
|
|
172
|
+
}
|
|
173
|
+
catch (cause) {
|
|
174
|
+
if (allowInvalidFinalLine)
|
|
175
|
+
return undefined;
|
|
176
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
|
|
177
|
+
cause
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
while (position > 0) {
|
|
182
|
+
const start = Math.max(0, position - REVERSE_READ_CHUNK_BYTES);
|
|
183
|
+
const chunk = Buffer.allocUnsafe(position - start);
|
|
184
|
+
let bytesRead = 0;
|
|
185
|
+
while (bytesRead < chunk.length) {
|
|
186
|
+
const result = await handle.read(chunk, bytesRead, chunk.length - bytesRead, start + bytesRead);
|
|
187
|
+
if (result.bytesRead === 0)
|
|
188
|
+
break;
|
|
189
|
+
bytesRead += result.bytesRead;
|
|
190
|
+
}
|
|
191
|
+
const value = chunk.subarray(0, bytesRead);
|
|
192
|
+
if (!inspectedFileEnd) {
|
|
193
|
+
inspectedFileEnd = true;
|
|
194
|
+
invalidFinalLineMayBeIgnored = value.at(-1) !== 0x0a;
|
|
195
|
+
}
|
|
196
|
+
let lineEnd = value.length;
|
|
197
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
198
|
+
if (value[index] !== 0x0a)
|
|
199
|
+
continue;
|
|
200
|
+
const currentPart = value.subarray(index + 1, lineEnd);
|
|
201
|
+
const line = pendingLineParts.length === 0
|
|
202
|
+
? currentPart
|
|
203
|
+
: Buffer.concat([currentPart, ...pendingLineParts.reverse()]);
|
|
204
|
+
const record = decode(line, invalidFinalLineMayBeIgnored);
|
|
205
|
+
invalidFinalLineMayBeIgnored = false;
|
|
206
|
+
pendingLineParts = [];
|
|
207
|
+
lineEnd = index;
|
|
208
|
+
if (record !== undefined) {
|
|
209
|
+
remember(record, start + index + 1);
|
|
210
|
+
yield record;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (lineEnd > 0)
|
|
214
|
+
pendingLineParts.push(value.subarray(0, lineEnd));
|
|
215
|
+
position = start;
|
|
216
|
+
}
|
|
217
|
+
if (pendingLineParts.length > 0) {
|
|
218
|
+
const record = decode(Buffer.concat(pendingLineParts.reverse()), invalidFinalLineMayBeIgnored);
|
|
219
|
+
if (record !== undefined) {
|
|
220
|
+
remember(record, 0);
|
|
221
|
+
yield record;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
await handle.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export function segmentNumberFromPath(path) {
|
|
230
|
+
const match = /events-(\d{6})\.jsonl$/.exec(path);
|
|
231
|
+
return match === null ? undefined : Number(match[1]);
|
|
232
|
+
}
|
|
233
|
+
export async function appendRecordBytes(path, serialized) {
|
|
234
|
+
const handle = await open(path, 'a', 0o600);
|
|
235
|
+
try {
|
|
236
|
+
const bytes = Buffer.from(serialized);
|
|
237
|
+
let offset = 0;
|
|
238
|
+
while (offset < bytes.length) {
|
|
239
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset);
|
|
240
|
+
if (bytesWritten === 0)
|
|
241
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session append is incomplete.');
|
|
242
|
+
offset += bytesWritten;
|
|
243
|
+
}
|
|
244
|
+
await handle.sync();
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await handle.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ModelInputMetadata } from '../../model/contracts.js';
|
|
2
|
+
import type { TodoSnapshot } from '../../tools/agent/todo.js';
|
|
3
|
+
import type { SessionSource } from '../types.js';
|
|
4
|
+
export interface SessionRecord {
|
|
5
|
+
schemaVersion: 1;
|
|
6
|
+
recordId: string;
|
|
7
|
+
sequence: number;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
executionId?: string;
|
|
11
|
+
turnId?: string;
|
|
12
|
+
type: string;
|
|
13
|
+
payload: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface SessionExecutionState {
|
|
16
|
+
automaticTitlePending: boolean;
|
|
17
|
+
todo: TodoSnapshot;
|
|
18
|
+
contextRecords: SessionRecord[];
|
|
19
|
+
}
|
|
20
|
+
export interface CompactUserInput {
|
|
21
|
+
content: string;
|
|
22
|
+
metadata?: ModelInputMetadata;
|
|
23
|
+
}
|
|
24
|
+
export interface CompactHistoryMessage extends CompactUserInput {
|
|
25
|
+
role: 'user';
|
|
26
|
+
kind: 'user_input' | 'extension_input' | 'summary';
|
|
27
|
+
}
|
|
28
|
+
export interface SessionInputImage {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly mimeType: string;
|
|
31
|
+
readonly dataBase64: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SessionInputImageMetadata {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly mimeType: string;
|
|
36
|
+
readonly byteSize: number;
|
|
37
|
+
readonly fileName: string;
|
|
38
|
+
readonly sha256: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RestoredSessionInputImage {
|
|
41
|
+
readonly mimeType: string;
|
|
42
|
+
readonly dataBase64: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SessionInputImageData extends SessionInputImageMetadata {
|
|
45
|
+
readonly dataBase64: string;
|
|
46
|
+
}
|
|
47
|
+
export interface SessionMeta {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
workspace: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
lastActivityAt: string;
|
|
53
|
+
source?: SessionSource;
|
|
54
|
+
nextSequence: number;
|
|
55
|
+
automaticTitlePending?: boolean;
|
|
56
|
+
storageTransactionId?: string;
|
|
57
|
+
resume?: {
|
|
58
|
+
version: 1;
|
|
59
|
+
userTurns?: number;
|
|
60
|
+
compactSequence?: number;
|
|
61
|
+
gcSequence?: number;
|
|
62
|
+
gcGeneration?: number;
|
|
63
|
+
gcBaseSequence?: number;
|
|
64
|
+
todoSequence?: number;
|
|
65
|
+
todoRevision: number;
|
|
66
|
+
executionModelIndexed?: true;
|
|
67
|
+
firstUserMessageIndexed?: true;
|
|
68
|
+
firstUserMessagePreview?: string;
|
|
69
|
+
modelId?: string;
|
|
70
|
+
modelName?: string;
|
|
71
|
+
reasoningEffort?: string;
|
|
72
|
+
};
|
|
73
|
+
segments?: Array<{
|
|
74
|
+
number: number;
|
|
75
|
+
firstSequence: number;
|
|
76
|
+
lastSequence: number;
|
|
77
|
+
bytes: number;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export interface SessionIndex {
|
|
81
|
+
version: 1;
|
|
82
|
+
revision: number;
|
|
83
|
+
sessions: SessionMeta[];
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AvailableToolResultResource } from '../tools/result-extensions.js';
|
|
2
|
-
import type { SessionRecord } from './
|
|
2
|
+
import type { SessionRecord } from './store/index.js';
|
|
3
3
|
export interface ToolResultResourceInput {
|
|
4
4
|
resourceRef: string;
|
|
5
5
|
cursor?: string;
|
|
@@ -12,7 +12,7 @@ export class SubagentScheduler {
|
|
|
12
12
|
#disposed = false;
|
|
13
13
|
#createdInExecution = 0;
|
|
14
14
|
#runner;
|
|
15
|
-
constructor(runner, limits = { maxTasks:
|
|
15
|
+
constructor(runner, limits = { maxTasks: 16, maxConcurrent: 16 }) {
|
|
16
16
|
this.limits = limits;
|
|
17
17
|
this.#runner = runner;
|
|
18
18
|
}
|
|
@@ -3,7 +3,7 @@ import type { CoreOperationContext } from '../extensions/operation.js';
|
|
|
3
3
|
import type { MarAgentReasoningEffort } from '../model/configuration.js';
|
|
4
4
|
import type { ExecutionResult } from '../runtime/contracts.js';
|
|
5
5
|
import type { MarAgentEvent } from '../runtime/events.js';
|
|
6
|
-
import type { SessionRecord, SessionSource } from '../session/
|
|
6
|
+
import type { SessionRecord, SessionSource } from '../session/store/index.js';
|
|
7
7
|
import type { SubagentManyOutput, SubagentManyOutputOptions } from './contracts.js';
|
|
8
8
|
import type { SubagentController, SubagentMessageOptions, SubagentOutputOptions, SubagentRunOptions, SubagentTaskInput, SubagentTaskSnapshot } from './scheduler.js';
|
|
9
9
|
export interface SubagentModelOption {
|
|
@@ -78,7 +78,7 @@ export interface SubagentSessionControllerBackend {
|
|
|
78
78
|
}>;
|
|
79
79
|
deleteSession(sessionId: string): Promise<void>;
|
|
80
80
|
/** Returns children in durable creation order. */
|
|
81
|
-
listChildren(parentSessionId: string): Promise<readonly PersistedSubagentSession[]>;
|
|
81
|
+
listChildren(parentSessionId: string): Promise<readonly Omit<PersistedSubagentSession, 'records'>[]>;
|
|
82
82
|
loadChild(parentSessionId: string, sessionId: string): Promise<PersistedSubagentSession | undefined>;
|
|
83
83
|
}
|
|
84
84
|
export declare class SubagentSessionController implements SubagentController {
|
|
@@ -109,6 +109,9 @@ export declare class SubagentSessionController implements SubagentController {
|
|
|
109
109
|
withOperation<T>(operation: SubagentParentOperation, run: () => T): T;
|
|
110
110
|
inspect(agentId: string): Promise<SubagentTaskSnapshot>;
|
|
111
111
|
inspectAll(): readonly SubagentTaskSnapshot[];
|
|
112
|
+
/** Full listing is explicit; ordinary opens and executions never require it. */
|
|
113
|
+
prepareAll(signal?: AbortSignal): Promise<void>;
|
|
114
|
+
prepareSessionResources(signal?: AbortSignal): Promise<void>;
|
|
112
115
|
collectModelUpdates(): readonly {
|
|
113
116
|
snapshot: SubagentTaskSnapshot;
|
|
114
117
|
truncated: boolean;
|
|
@@ -4,12 +4,16 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { MarAgentError, describeMarAgentError } from '../error.js';
|
|
5
5
|
import { TOOL_EXECUTION_LIMITS } from '../tools/execution-limits.js';
|
|
6
6
|
import { parseAgentWaitTargets, waitForAgentCondition } from './wait.js';
|
|
7
|
+
const RECENT_TERMINAL_RESOURCE_LIMIT = 16;
|
|
7
8
|
export class SubagentSessionController {
|
|
8
9
|
parentSessionId;
|
|
9
10
|
rootSessionId;
|
|
10
11
|
backend;
|
|
11
12
|
limits;
|
|
12
13
|
#agents = new Map();
|
|
14
|
+
// Discovery order is independent of the order in which history is requested.
|
|
15
|
+
#discovered = new Map();
|
|
16
|
+
#loading = new Map();
|
|
13
17
|
#queue = [];
|
|
14
18
|
#completionWaiters = new Set();
|
|
15
19
|
#activityWaiters = new Set();
|
|
@@ -35,7 +39,7 @@ export class SubagentSessionController {
|
|
|
35
39
|
#retainedExecution = new AsyncLocalStorage();
|
|
36
40
|
#execution;
|
|
37
41
|
#pendingDeliveries = new Set();
|
|
38
|
-
constructor(parentSessionId, rootSessionId, backend, limits = { maxTasks:
|
|
42
|
+
constructor(parentSessionId, rootSessionId, backend, limits = { maxTasks: 16, maxConcurrent: 16 }) {
|
|
39
43
|
this.parentSessionId = parentSessionId;
|
|
40
44
|
this.rootSessionId = rootSessionId;
|
|
41
45
|
this.backend = backend;
|
|
@@ -43,11 +47,12 @@ export class SubagentSessionController {
|
|
|
43
47
|
}
|
|
44
48
|
static async open(input) {
|
|
45
49
|
const controller = new SubagentSessionController(input.parentSessionId, input.rootSessionId, input.backend, input.limits);
|
|
46
|
-
const sessions =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const sessions = await input.backend.listChildren(input.parentSessionId);
|
|
51
|
+
// Map insertion order is creation order; values independently rank activity.
|
|
52
|
+
for (const session of sessions)
|
|
53
|
+
controller.#discovered.set(session.id, 0);
|
|
54
|
+
for (const session of [...sessions].sort((a, b) => a.lastActivityAt.localeCompare(b.lastActivityAt))) {
|
|
55
|
+
controller.#discovered.set(session.id, ++controller.#resourceRevision);
|
|
51
56
|
}
|
|
52
57
|
return controller;
|
|
53
58
|
}
|
|
@@ -98,7 +103,49 @@ export class SubagentSessionController {
|
|
|
98
103
|
inspectAll() {
|
|
99
104
|
if (this.#disposed)
|
|
100
105
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
101
|
-
return
|
|
106
|
+
return this.#orderedStates().map((state) => this.#snapshot(state, { full: true }));
|
|
107
|
+
}
|
|
108
|
+
/** Full listing is explicit; ordinary opens and executions never require it. */
|
|
109
|
+
async prepareAll(signal) {
|
|
110
|
+
for (const id of this.#discovered.keys()) {
|
|
111
|
+
signal?.throwIfAborted();
|
|
112
|
+
await this.#restoreDiscovered(id);
|
|
113
|
+
}
|
|
114
|
+
signal?.throwIfAborted();
|
|
115
|
+
}
|
|
116
|
+
async prepareSessionResources(signal) {
|
|
117
|
+
const candidates = [...this.#discovered]
|
|
118
|
+
.filter(([id]) => {
|
|
119
|
+
const state = this.#agents.get(id);
|
|
120
|
+
return !state || isTerminalState(state);
|
|
121
|
+
})
|
|
122
|
+
.sort(([a, ar], [b, br]) => (this.#agents.get(b)?.resourceRevision ?? br) -
|
|
123
|
+
(this.#agents.get(a)?.resourceRevision ?? ar));
|
|
124
|
+
let prepared = 0;
|
|
125
|
+
for (const [id] of candidates) {
|
|
126
|
+
signal?.throwIfAborted();
|
|
127
|
+
const state = await this.#restoreDiscovered(id);
|
|
128
|
+
if (state && isTerminalState(state) && ++prepared === RECENT_TERMINAL_RESOURCE_LIMIT)
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
}
|
|
133
|
+
async #restoreDiscovered(agentId) {
|
|
134
|
+
try {
|
|
135
|
+
return await this.#find(agentId);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!(error instanceof MarAgentError) || error.code !== 'MAR_AGENT_SUBAGENT_NOT_FOUND')
|
|
139
|
+
throw error;
|
|
140
|
+
this.#discovered.delete(agentId);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
#orderedStates() {
|
|
145
|
+
return [...this.#discovered.keys()].flatMap((id) => {
|
|
146
|
+
const state = this.#agents.get(id);
|
|
147
|
+
return state ? [state] : [];
|
|
148
|
+
});
|
|
102
149
|
}
|
|
103
150
|
collectModelUpdates() {
|
|
104
151
|
return [...this.#agents.values()]
|
|
@@ -223,6 +270,7 @@ export class SubagentSessionController {
|
|
|
223
270
|
if (!native)
|
|
224
271
|
(this.#retainedExecution.getStore()?.budget ?? this.#creationBudget).count++;
|
|
225
272
|
this.#agents.set(state.agentId, state);
|
|
273
|
+
this.#discovered.set(state.agentId, state.resourceRevision);
|
|
226
274
|
const knownModel = execution.models.get(input.modelId ?? execution.modelId);
|
|
227
275
|
if (knownModel) {
|
|
228
276
|
state.modelId = knownModel.id;
|
|
@@ -335,7 +383,16 @@ export class SubagentSessionController {
|
|
|
335
383
|
if (options.untilSettled)
|
|
336
384
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'An unbounded wait requires an agent id.');
|
|
337
385
|
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
338
|
-
const
|
|
386
|
+
const hasLive = [...this.#agents.values()].some((state) => !isTerminalState(state));
|
|
387
|
+
if (!hasLive) {
|
|
388
|
+
for (const id of [...this.#discovered.keys()].reverse()) {
|
|
389
|
+
options.signal?.throwIfAborted();
|
|
390
|
+
if (await this.#restoreDiscovered(id))
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
options.signal?.throwIfAborted();
|
|
395
|
+
const states = this.#orderedStates();
|
|
339
396
|
if (states.length === 0)
|
|
340
397
|
throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
|
|
341
398
|
const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
|
|
@@ -625,7 +682,7 @@ export class SubagentSessionController {
|
|
|
625
682
|
const terminal = states
|
|
626
683
|
.filter((state) => state.status !== 'queued' && state.status !== 'running')
|
|
627
684
|
.sort((left, right) => right.resourceRevision - left.resourceRevision)
|
|
628
|
-
.slice(0,
|
|
685
|
+
.slice(0, RECENT_TERMINAL_RESOURCE_LIMIT);
|
|
629
686
|
return [...active, ...terminal].map((state) => ({
|
|
630
687
|
agentId: state.agentId,
|
|
631
688
|
status: state.status,
|
|
@@ -946,6 +1003,7 @@ export class SubagentSessionController {
|
|
|
946
1003
|
}
|
|
947
1004
|
async #deleteUnstartedSession(agentId, error) {
|
|
948
1005
|
this.#agents.delete(agentId);
|
|
1006
|
+
this.#discovered.delete(agentId);
|
|
949
1007
|
try {
|
|
950
1008
|
await this.backend.deleteSession(agentId);
|
|
951
1009
|
}
|
|
@@ -957,15 +1015,41 @@ export class SubagentSessionController {
|
|
|
957
1015
|
throw error;
|
|
958
1016
|
}
|
|
959
1017
|
async #find(agentId) {
|
|
1018
|
+
if (this.#disposed)
|
|
1019
|
+
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
960
1020
|
const known = this.#agents.get(agentId);
|
|
961
1021
|
if (known)
|
|
962
1022
|
return known;
|
|
963
|
-
const
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
1023
|
+
const pending = this.#loading.get(agentId);
|
|
1024
|
+
if (pending)
|
|
1025
|
+
return pending;
|
|
1026
|
+
const load = (async () => {
|
|
1027
|
+
const persisted = await this.backend.loadChild(this.parentSessionId, agentId);
|
|
1028
|
+
if (this.#disposed)
|
|
1029
|
+
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
1030
|
+
if (!persisted)
|
|
1031
|
+
throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
|
|
1032
|
+
const existing = this.#agents.get(agentId);
|
|
1033
|
+
if (existing)
|
|
1034
|
+
return existing;
|
|
1035
|
+
const restored = restoreAgentState(persisted);
|
|
1036
|
+
let revision = this.#discovered.get(agentId);
|
|
1037
|
+
if (revision === undefined) {
|
|
1038
|
+
revision = ++this.#resourceRevision;
|
|
1039
|
+
this.#discovered.set(agentId, revision);
|
|
1040
|
+
}
|
|
1041
|
+
restored.resourceRevision = revision;
|
|
1042
|
+
this.#agents.set(agentId, restored);
|
|
1043
|
+
return restored;
|
|
1044
|
+
})();
|
|
1045
|
+
this.#loading.set(agentId, load);
|
|
1046
|
+
try {
|
|
1047
|
+
return await load;
|
|
1048
|
+
}
|
|
1049
|
+
finally {
|
|
1050
|
+
if (this.#loading.get(agentId) === load)
|
|
1051
|
+
this.#loading.delete(agentId);
|
|
1052
|
+
}
|
|
969
1053
|
}
|
|
970
1054
|
#requireExecution() {
|
|
971
1055
|
if (this.#disposed)
|
|
@@ -5,5 +5,5 @@ type StateRecord = {
|
|
|
5
5
|
readonly payload: unknown;
|
|
6
6
|
};
|
|
7
7
|
export declare function parseCodeModeStateMutations(value: unknown): CodeModeStateMutation[];
|
|
8
|
-
export declare function restoreCodeModeState(records: readonly StateRecord[]): CodeModeStateEntry[];
|
|
8
|
+
export declare function restoreCodeModeState(records: readonly StateRecord[], initialState?: CodeModeStateEntry[]): CodeModeStateEntry[];
|
|
9
9
|
export declare function applyCodeModeStateMutations(entries: readonly CodeModeStateEntry[], mutations: readonly CodeModeStateMutation[]): CodeModeStateEntry[];
|
|
@@ -16,8 +16,8 @@ export function parseCodeModeStateMutations(value) {
|
|
|
16
16
|
throw invalidState();
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
export function restoreCodeModeState(records) {
|
|
20
|
-
let state =
|
|
19
|
+
export function restoreCodeModeState(records, initialState = []) {
|
|
20
|
+
let state = initialState;
|
|
21
21
|
for (const record of records) {
|
|
22
22
|
if (record.type !== 'code.state.updated')
|
|
23
23
|
continue;
|