@felan-ai/felan 0.1.0-alpha.3 → 0.2.0
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/NOTICE +1 -1
- package/README.md +69 -11
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +25 -2
- package/dist/application.js.map +1 -1
- package/dist/extensions.d.ts +13 -1
- package/dist/extensions.d.ts.map +1 -1
- package/dist/extensions.js +46 -5
- package/dist/extensions.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +13 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +159 -124
- package/dist/runtime.js.map +1 -1
- package/dist/settings.d.ts +7 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +15 -3
- package/dist/settings.js.map +1 -1
- package/dist/subagents/catalog.d.ts +8 -0
- package/dist/subagents/catalog.d.ts.map +1 -0
- package/dist/subagents/catalog.js +132 -0
- package/dist/subagents/catalog.js.map +1 -0
- package/dist/subagents/host.d.ts +86 -0
- package/dist/subagents/host.d.ts.map +1 -0
- package/dist/subagents/host.js +994 -0
- package/dist/subagents/host.js.map +1 -0
- package/dist/subagents/presenter.d.ts +11 -0
- package/dist/subagents/presenter.d.ts.map +1 -0
- package/dist/subagents/presenter.js +34 -0
- package/dist/subagents/presenter.js.map +1 -0
- package/dist/subagents/store.d.ts +17 -0
- package/dist/subagents/store.d.ts.map +1 -0
- package/dist/subagents/store.js +34 -0
- package/dist/subagents/store.js.map +1 -0
- package/dist/system-prompt.d.ts +4 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +22 -0
- package/dist/system-prompt.js.map +1 -0
- package/package.json +8 -7
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { HostAgentRuntime, SessionManager, createAgentCoreSession, } from '@felan-ai/agent-core';
|
|
4
|
+
import { bindSubagentSession, } from '@felan-ai/ext-subagents';
|
|
5
|
+
import { CURRENT_SESSION_VERSION } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { createLocalExtensionImporter } from '../extensions.js';
|
|
7
|
+
import { loadLocalChildSystemPromptAppends } from '../system-prompt.js';
|
|
8
|
+
import { discoverLocalSubagents } from './catalog.js';
|
|
9
|
+
import { LocalSubagentStore } from './store.js';
|
|
10
|
+
const EXT_SUBAGENTS = '@felan-ai/ext-subagents';
|
|
11
|
+
const MAX_RESULT_BYTES = 256 * 1024;
|
|
12
|
+
export class LocalSubagentHost {
|
|
13
|
+
descriptors;
|
|
14
|
+
policy;
|
|
15
|
+
#manager;
|
|
16
|
+
#sessionId;
|
|
17
|
+
constructor(manager, sessionId) {
|
|
18
|
+
this.#manager = manager;
|
|
19
|
+
this.#sessionId = sessionId;
|
|
20
|
+
this.descriptors = manager.descriptors;
|
|
21
|
+
this.policy = manager.policy;
|
|
22
|
+
}
|
|
23
|
+
static async create(options) {
|
|
24
|
+
const definitions = await discoverLocalSubagents(options.cwd, options.agentDir);
|
|
25
|
+
const manager = await LocalSubagentManager.create(options, definitions);
|
|
26
|
+
return new LocalSubagentHost(manager, options.sessionId);
|
|
27
|
+
}
|
|
28
|
+
attachParent(port) {
|
|
29
|
+
return this.#manager.attachParent(this.#sessionId, port);
|
|
30
|
+
}
|
|
31
|
+
spawn(request, signal) {
|
|
32
|
+
return this.#manager.spawn(this.#sessionId, request, signal);
|
|
33
|
+
}
|
|
34
|
+
list(options) {
|
|
35
|
+
return this.#manager.list(this.#sessionId, options);
|
|
36
|
+
}
|
|
37
|
+
getResult(agentId, options, signal) {
|
|
38
|
+
return this.#manager.getResult(this.#sessionId, agentId, options, signal);
|
|
39
|
+
}
|
|
40
|
+
steer(agentId, message) {
|
|
41
|
+
return this.#manager.steer(this.#sessionId, agentId, message);
|
|
42
|
+
}
|
|
43
|
+
cancel(agentId, reason) {
|
|
44
|
+
return this.#manager.cancel(this.#sessionId, agentId, reason);
|
|
45
|
+
}
|
|
46
|
+
subscribe(listener) {
|
|
47
|
+
return this.#manager.subscribe(listener);
|
|
48
|
+
}
|
|
49
|
+
async shutdown() {
|
|
50
|
+
await this.#manager.shutdown();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class LocalSubagentManager {
|
|
54
|
+
definitions;
|
|
55
|
+
descriptors;
|
|
56
|
+
policy;
|
|
57
|
+
#options;
|
|
58
|
+
#store;
|
|
59
|
+
#children = new Map();
|
|
60
|
+
#ports = new Map();
|
|
61
|
+
#jobs = new Map();
|
|
62
|
+
#queue = [];
|
|
63
|
+
#listeners = new Set();
|
|
64
|
+
#deliveries = new Map();
|
|
65
|
+
#concurrency;
|
|
66
|
+
#maxDepth;
|
|
67
|
+
#active = 0;
|
|
68
|
+
#closed = false;
|
|
69
|
+
#save = Promise.resolve();
|
|
70
|
+
#control = Promise.resolve();
|
|
71
|
+
#shutdown;
|
|
72
|
+
constructor(options, definitions) {
|
|
73
|
+
this.#options = options;
|
|
74
|
+
this.definitions = new Map(definitions.map((definition) => [definition.descriptor.id, definition]));
|
|
75
|
+
this.descriptors = Object.freeze(definitions.map((definition) => definition.descriptor));
|
|
76
|
+
this.#concurrency = boundedInteger(options.settings?.concurrency, 4, 1, 32);
|
|
77
|
+
this.#maxDepth = boundedInteger(options.settings?.maxDepth, 3, 1, 10);
|
|
78
|
+
this.policy = Object.freeze({
|
|
79
|
+
maxPromptBytes: 128 * 1024,
|
|
80
|
+
maxDescriptionBytes: 512,
|
|
81
|
+
maxSteerBytes: 32 * 1024,
|
|
82
|
+
});
|
|
83
|
+
this.#store = new LocalSubagentStore(options.agentDir, options.sessionId);
|
|
84
|
+
}
|
|
85
|
+
static async create(options, definitions) {
|
|
86
|
+
const manager = new LocalSubagentManager(options, definitions);
|
|
87
|
+
const loaded = await manager.#store.load();
|
|
88
|
+
let reconciled = false;
|
|
89
|
+
for (const stored of loaded) {
|
|
90
|
+
const child = fromStored(stored);
|
|
91
|
+
if (child.record.status === 'queued' || child.record.status === 'running') {
|
|
92
|
+
child.record = terminalRecord(child.record, 'cancelled', {
|
|
93
|
+
code: 'host_unavailable',
|
|
94
|
+
message: 'Local subagent was interrupted when the previous Felan process exited',
|
|
95
|
+
});
|
|
96
|
+
child.completionPending = child.deliveryId !== undefined;
|
|
97
|
+
reconciled = true;
|
|
98
|
+
}
|
|
99
|
+
manager.#children.set(child.record.agentId, child);
|
|
100
|
+
}
|
|
101
|
+
if (reconciled)
|
|
102
|
+
await manager.#persist();
|
|
103
|
+
return manager;
|
|
104
|
+
}
|
|
105
|
+
attachParent(sessionId, port) {
|
|
106
|
+
this.#ports.set(sessionId, port);
|
|
107
|
+
const replay = setTimeout(() => {
|
|
108
|
+
this.#deliverPending(sessionId).catch(() => { });
|
|
109
|
+
}, 0);
|
|
110
|
+
replay.unref();
|
|
111
|
+
return () => {
|
|
112
|
+
clearTimeout(replay);
|
|
113
|
+
if (this.#ports.get(sessionId) === port)
|
|
114
|
+
this.#ports.delete(sessionId);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async spawn(parentSessionId, request, signal) {
|
|
118
|
+
if (this.#closed)
|
|
119
|
+
return failure('host_unavailable', 'Local subagent host is closed');
|
|
120
|
+
if (signal?.aborted)
|
|
121
|
+
return failure('invalid_request', 'Subagent startup was aborted');
|
|
122
|
+
if (byteLength(request.prompt) > this.policy.maxPromptBytes
|
|
123
|
+
|| byteLength(request.description) > this.policy.maxDescriptionBytes) {
|
|
124
|
+
return failure('invalid_request', 'Subagent prompt or description exceeds local policy');
|
|
125
|
+
}
|
|
126
|
+
if (request.maxTurns !== undefined && (!Number.isInteger(request.maxTurns) || request.maxTurns < 1 || request.maxTurns > 100)) {
|
|
127
|
+
return failure('invalid_request', 'maxTurns must be an integer from 1 to 100');
|
|
128
|
+
}
|
|
129
|
+
if (request.timeoutSeconds !== undefined
|
|
130
|
+
&& (!Number.isInteger(request.timeoutSeconds) || request.timeoutSeconds < 1 || request.timeoutSeconds > 86_400)) {
|
|
131
|
+
return failure('invalid_request', 'timeoutSeconds must be an integer from 1 to 86400');
|
|
132
|
+
}
|
|
133
|
+
const definition = this.definitions.get(request.type);
|
|
134
|
+
if (!definition)
|
|
135
|
+
return failure('unknown_agent_type', `Unknown subagent type: ${request.type}`);
|
|
136
|
+
const resolvedModel = this.#resolveModel(request.model, definition.descriptor.defaultModel);
|
|
137
|
+
if (!resolvedModel.ok)
|
|
138
|
+
return resolvedModel;
|
|
139
|
+
if (!this.#supportsThinking(resolvedModel.value, request.thinking)) {
|
|
140
|
+
return failure('unsupported_thinking', 'The resolved model does not support the requested thinking level');
|
|
141
|
+
}
|
|
142
|
+
const normalizedRequest = {
|
|
143
|
+
...request,
|
|
144
|
+
...(resolvedModel.value === undefined ? {} : { model: resolvedModel.value }),
|
|
145
|
+
};
|
|
146
|
+
const admitted = await this.#serializeControl(async () => {
|
|
147
|
+
if (this.#closed)
|
|
148
|
+
return failure('host_unavailable', 'Local subagent host is closed');
|
|
149
|
+
if (signal?.aborted)
|
|
150
|
+
return failure('invalid_request', 'Subagent startup was aborted');
|
|
151
|
+
const context = this.#sessionContext(parentSessionId);
|
|
152
|
+
if (!context)
|
|
153
|
+
return failure('parent_unavailable', 'Parent subagent is unavailable');
|
|
154
|
+
if (context.depth >= this.#maxDepth) {
|
|
155
|
+
return failure('depth_exceeded', 'Maximum local subagent depth reached');
|
|
156
|
+
}
|
|
157
|
+
if (request.inheritContext && !this.#ports.has(parentSessionId)) {
|
|
158
|
+
return failure('parent_unavailable', 'Parent context is unavailable');
|
|
159
|
+
}
|
|
160
|
+
const parent = context.child;
|
|
161
|
+
const parentJob = this.#jobs.get(parentSessionId);
|
|
162
|
+
if (parent
|
|
163
|
+
&& (isTerminal(parent.record.status) || parentJob?.terminalStatus !== undefined)) {
|
|
164
|
+
return failure('parent_unavailable', 'Parent subagent is no longer active');
|
|
165
|
+
}
|
|
166
|
+
if (parent
|
|
167
|
+
&& !this.definitions.get(parent.record.type)?.descriptor.allowNesting) {
|
|
168
|
+
return failure('depth_exceeded', `${parent.record.type} does not allow nested subagents`);
|
|
169
|
+
}
|
|
170
|
+
if (!request.runInBackground && this.#active >= this.#concurrency) {
|
|
171
|
+
return failure('capacity_unavailable', 'Foreground capacity is unavailable; retry in background');
|
|
172
|
+
}
|
|
173
|
+
let initialMessage;
|
|
174
|
+
try {
|
|
175
|
+
initialMessage = await this.#initialMessage(parentSessionId, normalizedRequest);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return failure('parent_unavailable', 'Parent context could not be captured');
|
|
179
|
+
}
|
|
180
|
+
if (this.#closed)
|
|
181
|
+
return failure('host_unavailable', 'Local subagent host is closed');
|
|
182
|
+
if (signal?.aborted)
|
|
183
|
+
return failure('invalid_request', 'Subagent startup was aborted');
|
|
184
|
+
const agentId = randomUUID();
|
|
185
|
+
const now = new Date().toISOString();
|
|
186
|
+
const record = {
|
|
187
|
+
agentId,
|
|
188
|
+
parentSessionId,
|
|
189
|
+
rootSessionId: context.rootSessionId,
|
|
190
|
+
type: request.type,
|
|
191
|
+
description: request.description,
|
|
192
|
+
status: 'queued',
|
|
193
|
+
createdAt: now,
|
|
194
|
+
};
|
|
195
|
+
const child = {
|
|
196
|
+
record,
|
|
197
|
+
request: normalizedRequest,
|
|
198
|
+
depth: context.depth + 1,
|
|
199
|
+
...(request.runInBackground ? { deliveryId: randomUUID() } : {}),
|
|
200
|
+
completionPending: false,
|
|
201
|
+
};
|
|
202
|
+
const job = this.#job(child, initialMessage);
|
|
203
|
+
if (!request.runInBackground) {
|
|
204
|
+
job.slotHeld = true;
|
|
205
|
+
this.#active += 1;
|
|
206
|
+
}
|
|
207
|
+
this.#children.set(agentId, child);
|
|
208
|
+
this.#jobs.set(agentId, job);
|
|
209
|
+
try {
|
|
210
|
+
await this.#persist();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
this.#children.delete(agentId);
|
|
214
|
+
this.#jobs.delete(agentId);
|
|
215
|
+
if (job.slotHeld) {
|
|
216
|
+
job.slotHeld = false;
|
|
217
|
+
this.#active -= 1;
|
|
218
|
+
}
|
|
219
|
+
return failure('host_unavailable', 'Local subagent startup could not be persisted');
|
|
220
|
+
}
|
|
221
|
+
if (signal?.aborted) {
|
|
222
|
+
this.#children.delete(agentId);
|
|
223
|
+
this.#jobs.delete(agentId);
|
|
224
|
+
if (job.slotHeld) {
|
|
225
|
+
job.slotHeld = false;
|
|
226
|
+
this.#active -= 1;
|
|
227
|
+
}
|
|
228
|
+
await this.#persist();
|
|
229
|
+
return failure('invalid_request', 'Subagent startup was aborted');
|
|
230
|
+
}
|
|
231
|
+
this.#emit();
|
|
232
|
+
return success({ child, job });
|
|
233
|
+
});
|
|
234
|
+
if (!admitted.ok)
|
|
235
|
+
return admitted;
|
|
236
|
+
const { child, job } = admitted.value;
|
|
237
|
+
if (request.runInBackground) {
|
|
238
|
+
const queued = cloneRecord(child.record);
|
|
239
|
+
this.#queue.push(job);
|
|
240
|
+
this.#drain();
|
|
241
|
+
return success(queued);
|
|
242
|
+
}
|
|
243
|
+
const abortForeground = () => {
|
|
244
|
+
this.#cancelTree(child.record.agentId, 'Subagent was cancelled').catch(() => { });
|
|
245
|
+
};
|
|
246
|
+
const running = this.#start(job);
|
|
247
|
+
signal?.addEventListener('abort', abortForeground, { once: true });
|
|
248
|
+
if (signal?.aborted)
|
|
249
|
+
abortForeground();
|
|
250
|
+
try {
|
|
251
|
+
await running;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
signal?.removeEventListener('abort', abortForeground);
|
|
255
|
+
}
|
|
256
|
+
return success(await this.#resultRecord(child));
|
|
257
|
+
}
|
|
258
|
+
async list(parentSessionId, options) {
|
|
259
|
+
return this.#serializeControl(async () => {
|
|
260
|
+
if (!this.#sessionContext(parentSessionId))
|
|
261
|
+
return failure('parent_unavailable', 'Parent subagent is unavailable');
|
|
262
|
+
const direct = new Set([parentSessionId]);
|
|
263
|
+
if (options.includeDescendants) {
|
|
264
|
+
let changed = true;
|
|
265
|
+
while (changed) {
|
|
266
|
+
changed = false;
|
|
267
|
+
for (const child of this.#latestChildren()) {
|
|
268
|
+
if (direct.has(child.record.parentSessionId) && !direct.has(child.record.agentId)) {
|
|
269
|
+
direct.add(child.record.agentId);
|
|
270
|
+
changed = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const records = this.#latestChildren()
|
|
276
|
+
.filter((child) => direct.has(child.record.parentSessionId))
|
|
277
|
+
.map((child) => cloneRecord(child.record))
|
|
278
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
279
|
+
return success(records);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async getResult(parentSessionId, agentId, options, signal) {
|
|
283
|
+
const authorized = await this.#serializeControl(async () => this.#directChild(parentSessionId, agentId));
|
|
284
|
+
if (!authorized.ok)
|
|
285
|
+
return authorized;
|
|
286
|
+
let child = authorized.value;
|
|
287
|
+
if (options.wait && !isTerminal(child.record.status)) {
|
|
288
|
+
await this.#wait(agentId, options.timeoutSeconds ?? 3_600, signal);
|
|
289
|
+
const refreshed = await this.#serializeControl(async () => this.#directChild(parentSessionId, agentId));
|
|
290
|
+
if (!refreshed.ok)
|
|
291
|
+
return refreshed;
|
|
292
|
+
child = refreshed.value;
|
|
293
|
+
}
|
|
294
|
+
return success(await this.#resultRecord(child));
|
|
295
|
+
}
|
|
296
|
+
async steer(parentSessionId, agentId, message) {
|
|
297
|
+
const prepared = await this.#serializeControl(async () => {
|
|
298
|
+
if (this.#closed)
|
|
299
|
+
return failure('host_unavailable', 'Local subagent host is closed');
|
|
300
|
+
const authorized = this.#directChild(parentSessionId, agentId);
|
|
301
|
+
if (!authorized.ok)
|
|
302
|
+
return authorized;
|
|
303
|
+
const child = authorized.value;
|
|
304
|
+
if (child.record.status === 'queued' || child.record.status === 'running') {
|
|
305
|
+
const steered = await this.#steerActive(child, message);
|
|
306
|
+
return steered.ok ? success({ kind: 'active', child: steered.value }) : steered;
|
|
307
|
+
}
|
|
308
|
+
if (child.record.status !== 'completed' || !child.sessionFile) {
|
|
309
|
+
return failure('not_steerable', 'Only retained completed subagents can be continued');
|
|
310
|
+
}
|
|
311
|
+
return success({
|
|
312
|
+
kind: 'completed',
|
|
313
|
+
child,
|
|
314
|
+
sessionFile: child.sessionFile,
|
|
315
|
+
deliveryId: child.completionPending ? child.deliveryId : undefined,
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
if (!prepared.ok)
|
|
319
|
+
return prepared;
|
|
320
|
+
if (prepared.value.kind === 'active')
|
|
321
|
+
return success(cloneRecord(prepared.value.child.record));
|
|
322
|
+
const completion = prepared.value;
|
|
323
|
+
try {
|
|
324
|
+
await preflightSessionFile(completion.sessionFile, agentId);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return failure('not_steerable', 'The retained subagent session is unavailable');
|
|
328
|
+
}
|
|
329
|
+
if (completion.deliveryId) {
|
|
330
|
+
await this.#deliver(completion.child, completion.deliveryId).catch(() => { });
|
|
331
|
+
}
|
|
332
|
+
const continued = await this.#serializeControl(async () => {
|
|
333
|
+
if (this.#closed)
|
|
334
|
+
return failure('host_unavailable', 'Local subagent host is closed');
|
|
335
|
+
const authorized = this.#directChild(parentSessionId, agentId);
|
|
336
|
+
if (!authorized.ok)
|
|
337
|
+
return authorized;
|
|
338
|
+
const child = authorized.value;
|
|
339
|
+
if (child.sessionFile !== completion.sessionFile) {
|
|
340
|
+
return failure('not_steerable', 'The retained subagent session changed before continuation');
|
|
341
|
+
}
|
|
342
|
+
if (child.record.status === 'queued' || child.record.status === 'running') {
|
|
343
|
+
const steered = await this.#steerActive(child, message);
|
|
344
|
+
return steered.ok ? success({ child: steered.value, job: undefined }) : steered;
|
|
345
|
+
}
|
|
346
|
+
if (child.record.status !== 'completed') {
|
|
347
|
+
return failure('not_steerable', 'The retained subagent session changed before continuation');
|
|
348
|
+
}
|
|
349
|
+
if (child.completionPending) {
|
|
350
|
+
return failure('not_steerable', 'The previous subagent completion is still pending delivery');
|
|
351
|
+
}
|
|
352
|
+
const previousRecord = child.record;
|
|
353
|
+
const previousDeliveryId = child.deliveryId;
|
|
354
|
+
const previousCompletionPending = child.completionPending;
|
|
355
|
+
child.record = {
|
|
356
|
+
agentId: child.record.agentId,
|
|
357
|
+
parentSessionId: child.record.parentSessionId,
|
|
358
|
+
rootSessionId: child.record.rootSessionId,
|
|
359
|
+
type: child.record.type,
|
|
360
|
+
description: child.record.description,
|
|
361
|
+
status: 'queued',
|
|
362
|
+
createdAt: child.record.createdAt,
|
|
363
|
+
};
|
|
364
|
+
child.deliveryId = randomUUID();
|
|
365
|
+
child.completionPending = false;
|
|
366
|
+
const job = this.#job(child, message);
|
|
367
|
+
this.#jobs.set(agentId, job);
|
|
368
|
+
try {
|
|
369
|
+
await this.#persist();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
this.#jobs.delete(agentId);
|
|
373
|
+
child.record = previousRecord;
|
|
374
|
+
child.completionPending = previousCompletionPending;
|
|
375
|
+
if (previousDeliveryId === undefined)
|
|
376
|
+
delete child.deliveryId;
|
|
377
|
+
else
|
|
378
|
+
child.deliveryId = previousDeliveryId;
|
|
379
|
+
return failure('host_unavailable', 'Subagent continuation could not be persisted');
|
|
380
|
+
}
|
|
381
|
+
this.#queue.push(job);
|
|
382
|
+
this.#emit();
|
|
383
|
+
return success({ child, job });
|
|
384
|
+
});
|
|
385
|
+
if (!continued.ok)
|
|
386
|
+
return continued;
|
|
387
|
+
if (continued.value.job)
|
|
388
|
+
this.#drain();
|
|
389
|
+
return success(cloneRecord(continued.value.child.record));
|
|
390
|
+
}
|
|
391
|
+
async cancel(parentSessionId, agentId, reason = 'Cancelled by parent') {
|
|
392
|
+
const authorized = this.#directChild(parentSessionId, agentId);
|
|
393
|
+
if (!authorized.ok)
|
|
394
|
+
return authorized;
|
|
395
|
+
await this.#cancelTree(agentId, reason);
|
|
396
|
+
return success(cloneRecord(this.#latest(agentId).record));
|
|
397
|
+
}
|
|
398
|
+
subscribe(listener) {
|
|
399
|
+
this.#listeners.add(listener);
|
|
400
|
+
listener(this.#latestChildren().map(localView));
|
|
401
|
+
return () => this.#listeners.delete(listener);
|
|
402
|
+
}
|
|
403
|
+
async shutdown() {
|
|
404
|
+
this.#shutdown ??= this.#performShutdown();
|
|
405
|
+
await this.#shutdown;
|
|
406
|
+
}
|
|
407
|
+
async #performShutdown() {
|
|
408
|
+
this.#closed = true;
|
|
409
|
+
await this.#serializeControl(async () => { });
|
|
410
|
+
for (const child of this.#latestChildren()) {
|
|
411
|
+
if (!isTerminal(child.record.status))
|
|
412
|
+
await this.#cancelTree(child.record.agentId, 'Local host exited');
|
|
413
|
+
}
|
|
414
|
+
await Promise.allSettled(this.#deliveries.values());
|
|
415
|
+
await this.#save;
|
|
416
|
+
}
|
|
417
|
+
async #steerActive(child, message) {
|
|
418
|
+
const job = this.#jobs.get(child.record.agentId);
|
|
419
|
+
if (!job || job.terminalStatus) {
|
|
420
|
+
return failure('not_steerable', 'Subagent session is unavailable');
|
|
421
|
+
}
|
|
422
|
+
if (job.control)
|
|
423
|
+
await job.control.steer(message);
|
|
424
|
+
else
|
|
425
|
+
job.queuedMessages.push(message);
|
|
426
|
+
return success(child);
|
|
427
|
+
}
|
|
428
|
+
#job(child, initialMessage) {
|
|
429
|
+
return {
|
|
430
|
+
child,
|
|
431
|
+
controller: new AbortController(),
|
|
432
|
+
initialMessage,
|
|
433
|
+
waiters: [],
|
|
434
|
+
queuedMessages: [],
|
|
435
|
+
slotHeld: false,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
#drain() {
|
|
439
|
+
this.#serializeControl(async () => {
|
|
440
|
+
while (!this.#closed && this.#active < this.#concurrency && this.#queue.length > 0) {
|
|
441
|
+
const job = this.#queue.shift();
|
|
442
|
+
if (job.child.record.status !== 'queued')
|
|
443
|
+
continue;
|
|
444
|
+
await this.#beginRun(job);
|
|
445
|
+
}
|
|
446
|
+
}).catch(() => { });
|
|
447
|
+
}
|
|
448
|
+
async #start(job) {
|
|
449
|
+
await this.#serializeControl(() => this.#beginRun(job));
|
|
450
|
+
await job.runPromise;
|
|
451
|
+
}
|
|
452
|
+
async #beginRun(job) {
|
|
453
|
+
if (job.runPromise || job.child.record.status !== 'queued')
|
|
454
|
+
return;
|
|
455
|
+
if (!job.slotHeld) {
|
|
456
|
+
if (this.#active >= this.#concurrency) {
|
|
457
|
+
this.#queue.unshift(job);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
job.slotHeld = true;
|
|
461
|
+
this.#active += 1;
|
|
462
|
+
}
|
|
463
|
+
job.child.record = {
|
|
464
|
+
...job.child.record,
|
|
465
|
+
status: 'running',
|
|
466
|
+
startedAt: new Date().toISOString(),
|
|
467
|
+
};
|
|
468
|
+
await this.#persist();
|
|
469
|
+
this.#emit();
|
|
470
|
+
job.runPromise = this.#run(job);
|
|
471
|
+
}
|
|
472
|
+
async #run(job) {
|
|
473
|
+
const child = job.child;
|
|
474
|
+
let timeout;
|
|
475
|
+
let outcome;
|
|
476
|
+
try {
|
|
477
|
+
if (child.request.timeoutSeconds) {
|
|
478
|
+
timeout = setTimeout(() => {
|
|
479
|
+
this.#cancelTree(child.record.agentId, `Subagent timed out after ${child.request.timeoutSeconds} seconds`, 'timed_out').catch(() => { });
|
|
480
|
+
}, child.request.timeoutSeconds * 1_000);
|
|
481
|
+
timeout.unref();
|
|
482
|
+
}
|
|
483
|
+
const definition = this.definitions.get(child.record.type);
|
|
484
|
+
if (!job.controller.signal.aborted) {
|
|
485
|
+
const runner = this.#options.runChild ?? ((input) => this.#runAgentCore(input));
|
|
486
|
+
const queuedAtStart = job.queuedMessages.splice(0);
|
|
487
|
+
outcome = await runner({
|
|
488
|
+
sessionId: child.record.agentId,
|
|
489
|
+
depth: child.depth,
|
|
490
|
+
subagents: new LocalSubagentHost(this, child.record.agentId),
|
|
491
|
+
request: child.request,
|
|
492
|
+
definition,
|
|
493
|
+
cwd: this.#options.cwd,
|
|
494
|
+
signal: job.controller.signal,
|
|
495
|
+
initialMessage: [job.initialMessage, ...queuedAtStart].join('\n\n'),
|
|
496
|
+
onReady: async (control) => {
|
|
497
|
+
job.control = control;
|
|
498
|
+
if (job.terminalStatus)
|
|
499
|
+
this.#cancelControl(job);
|
|
500
|
+
await job.cancelPromise;
|
|
501
|
+
if (job.terminalStatus)
|
|
502
|
+
return;
|
|
503
|
+
const pending = job.queuedMessages.splice(0);
|
|
504
|
+
for (const message of pending)
|
|
505
|
+
await control.steer(message);
|
|
506
|
+
},
|
|
507
|
+
...(child.sessionFile === undefined ? {} : { sessionFile: child.sessionFile }),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
await job.cancelPromise;
|
|
511
|
+
outcome ??= {};
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
await job.cancelPromise;
|
|
515
|
+
outcome = { error: { code: 'internal_error', message: 'Local subagent failed unexpectedly' } };
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
if (timeout)
|
|
519
|
+
clearTimeout(timeout);
|
|
520
|
+
await this.#serializeControl(async () => {
|
|
521
|
+
if (outcome?.sessionFile)
|
|
522
|
+
child.sessionFile = outcome.sessionFile;
|
|
523
|
+
if (job.terminalStatus) {
|
|
524
|
+
child.record = terminalRecord(child.record, job.terminalStatus, job.terminalError);
|
|
525
|
+
}
|
|
526
|
+
else if (outcome?.turnLimitReached) {
|
|
527
|
+
child.record = terminalRecord(child.record, 'cancelled', {
|
|
528
|
+
code: 'turn_limit_reached',
|
|
529
|
+
message: `Subagent reached its ${child.request.maxTurns} turn limit`,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
else if (outcome?.error) {
|
|
533
|
+
child.record = terminalRecord(child.record, 'failed', outcome.error);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
child.record = {
|
|
537
|
+
...terminalRecord(child.record, 'completed'),
|
|
538
|
+
result: boundResult(outcome?.result ?? ''),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (job.slotHeld) {
|
|
542
|
+
job.slotHeld = false;
|
|
543
|
+
this.#active -= 1;
|
|
544
|
+
}
|
|
545
|
+
this.#jobs.delete(child.record.agentId);
|
|
546
|
+
if (child.deliveryId)
|
|
547
|
+
child.completionPending = true;
|
|
548
|
+
await this.#persist();
|
|
549
|
+
this.#notify(job);
|
|
550
|
+
this.#emit();
|
|
551
|
+
});
|
|
552
|
+
if (child.deliveryId)
|
|
553
|
+
await this.#deliver(child, child.deliveryId);
|
|
554
|
+
this.#drain();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
#requestCancellation(job, status, error) {
|
|
558
|
+
if (job.terminalStatus || isTerminal(job.child.record.status))
|
|
559
|
+
return;
|
|
560
|
+
job.terminalStatus = status;
|
|
561
|
+
job.terminalError = error;
|
|
562
|
+
job.controller.abort();
|
|
563
|
+
this.#cancelControl(job);
|
|
564
|
+
}
|
|
565
|
+
#cancelControl(job) {
|
|
566
|
+
if (!job.control || job.cancelPromise)
|
|
567
|
+
return;
|
|
568
|
+
job.cancelPromise = job.control.cancel().catch(() => { });
|
|
569
|
+
}
|
|
570
|
+
async #runAgentCore(input) {
|
|
571
|
+
await mkdir(this.#store.sessionDirectory(), { recursive: true });
|
|
572
|
+
if (input.signal.aborted)
|
|
573
|
+
return {};
|
|
574
|
+
const sessionManager = input.sessionFile
|
|
575
|
+
? SessionManager.open(input.sessionFile, this.#store.sessionDirectory(), input.cwd)
|
|
576
|
+
: SessionManager.create(input.cwd, this.#store.sessionDirectory(), { id: input.sessionId });
|
|
577
|
+
const model = input.request.model ? this.#exactModel(input.request.model) : undefined;
|
|
578
|
+
if (input.request.model && !model) {
|
|
579
|
+
return { error: { code: 'unsupported_model', message: 'The requested model is unavailable' } };
|
|
580
|
+
}
|
|
581
|
+
const canNest = input.definition.descriptor.allowNesting && input.depth < this.#maxDepth;
|
|
582
|
+
const extensionPackages = canNest
|
|
583
|
+
? this.#options.extensionPackages
|
|
584
|
+
: this.#options.extensionPackages.filter((packageName) => packageName !== EXT_SUBAGENTS);
|
|
585
|
+
if (input.signal.aborted)
|
|
586
|
+
return {};
|
|
587
|
+
const appendSystemPrompt = await loadLocalChildSystemPromptAppends(this.#options.agentDir, input.definition.prompt);
|
|
588
|
+
const created = await createAgentCoreSession({
|
|
589
|
+
runtime: this.#options.runtimeFactory?.(input.cwd) ?? new HostAgentRuntime(input.cwd),
|
|
590
|
+
extensionPackages,
|
|
591
|
+
importExtension: createLocalExtensionImporter(input.subagents, this.#options.importExtension),
|
|
592
|
+
modelRuntime: this.#options.modelRuntime,
|
|
593
|
+
settingsManager: this.#options.settingsManager,
|
|
594
|
+
sessionManager,
|
|
595
|
+
appendSystemPrompt,
|
|
596
|
+
...(this.#options.skillPaths === undefined ? {} : { skillPaths: this.#options.skillPaths }),
|
|
597
|
+
...(model === undefined ? {} : { model }),
|
|
598
|
+
...(input.request.thinking === undefined ? {} : { thinkingLevel: input.request.thinking }),
|
|
599
|
+
});
|
|
600
|
+
bindSubagentSession({ host: input.subagents, session: created.session });
|
|
601
|
+
try {
|
|
602
|
+
if (input.signal.aborted)
|
|
603
|
+
return sessionFileOutcome(created.session.sessionFile);
|
|
604
|
+
if (input.definition.capability === 'read-only') {
|
|
605
|
+
created.session.setActiveToolsByName(created.session.getActiveToolNames().filter((name) => !['bash', 'edit', 'write'].includes(name)));
|
|
606
|
+
}
|
|
607
|
+
await created.session.bindExtensions({ mode: 'print' });
|
|
608
|
+
if (input.signal.aborted)
|
|
609
|
+
return sessionFileOutcome(created.session.sessionFile);
|
|
610
|
+
let turns = 0;
|
|
611
|
+
let turnLimitReached = false;
|
|
612
|
+
let cancellation;
|
|
613
|
+
const cancel = () => {
|
|
614
|
+
cancellation ??= created.session.abort();
|
|
615
|
+
return cancellation;
|
|
616
|
+
};
|
|
617
|
+
const unsubscribe = created.session.subscribe((event) => {
|
|
618
|
+
if (event.type !== 'turn_end')
|
|
619
|
+
return;
|
|
620
|
+
turns += 1;
|
|
621
|
+
if (input.request.maxTurns
|
|
622
|
+
&& turns >= input.request.maxTurns
|
|
623
|
+
&& event.message.role === 'assistant'
|
|
624
|
+
&& event.message.stopReason !== 'stop') {
|
|
625
|
+
turnLimitReached = true;
|
|
626
|
+
cancel().catch(() => { });
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
const abort = () => {
|
|
630
|
+
cancel().catch(() => { });
|
|
631
|
+
};
|
|
632
|
+
input.signal.addEventListener('abort', abort, { once: true });
|
|
633
|
+
try {
|
|
634
|
+
if (input.signal.aborted) {
|
|
635
|
+
await cancel();
|
|
636
|
+
return sessionFileOutcome(created.session.sessionFile);
|
|
637
|
+
}
|
|
638
|
+
await input.onReady({
|
|
639
|
+
steer: (message) => created.session.steer(message),
|
|
640
|
+
cancel,
|
|
641
|
+
});
|
|
642
|
+
if (input.signal.aborted) {
|
|
643
|
+
await cancel();
|
|
644
|
+
return sessionFileOutcome(created.session.sessionFile);
|
|
645
|
+
}
|
|
646
|
+
await created.session.prompt(input.initialMessage);
|
|
647
|
+
await cancellation;
|
|
648
|
+
const assistant = [...created.session.messages].reverse().find((message) => message.role === 'assistant');
|
|
649
|
+
if (!assistant) {
|
|
650
|
+
return {
|
|
651
|
+
error: { code: 'internal_error', message: 'Subagent completed without an assistant response' },
|
|
652
|
+
...sessionFileOutcome(created.session.sessionFile),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (assistant.stopReason === 'error') {
|
|
656
|
+
return {
|
|
657
|
+
error: { code: 'internal_error', message: 'Subagent model request failed' },
|
|
658
|
+
...sessionFileOutcome(created.session.sessionFile),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const result = assistant.content
|
|
662
|
+
.filter((part) => part.type === 'text')
|
|
663
|
+
.map((part) => part.text)
|
|
664
|
+
.join('\n');
|
|
665
|
+
return {
|
|
666
|
+
result,
|
|
667
|
+
...sessionFileOutcome(created.session.sessionFile),
|
|
668
|
+
...(turnLimitReached ? { turnLimitReached: true } : {}),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
input.signal.removeEventListener('abort', abort);
|
|
673
|
+
await cancellation;
|
|
674
|
+
unsubscribe();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
created.session.dispose();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
#resolveModel(selector, defaultModel) {
|
|
682
|
+
const value = selector ?? defaultModel;
|
|
683
|
+
if (!value)
|
|
684
|
+
return success(undefined);
|
|
685
|
+
if (value === 'inherit') {
|
|
686
|
+
return failure('unsupported_model', 'Parent model is unavailable');
|
|
687
|
+
}
|
|
688
|
+
const model = this.#exactModel(value);
|
|
689
|
+
return model && this.#options.modelRuntime.hasConfiguredAuth(model.provider)
|
|
690
|
+
? success(`${model.provider}/${model.id}`)
|
|
691
|
+
: failure('unsupported_model', 'The requested model is unavailable or unauthenticated');
|
|
692
|
+
}
|
|
693
|
+
#supportsThinking(modelReference, thinking) {
|
|
694
|
+
if (thinking === undefined || thinking === 'off')
|
|
695
|
+
return true;
|
|
696
|
+
if (!modelReference)
|
|
697
|
+
return false;
|
|
698
|
+
const model = this.#exactModel(modelReference);
|
|
699
|
+
if (!model?.reasoning)
|
|
700
|
+
return false;
|
|
701
|
+
const mapped = model.thinkingLevelMap?.[thinking];
|
|
702
|
+
return thinking === 'xhigh' || thinking === 'max'
|
|
703
|
+
? mapped !== undefined && mapped !== null
|
|
704
|
+
: mapped !== null;
|
|
705
|
+
}
|
|
706
|
+
#exactModel(reference) {
|
|
707
|
+
const separator = reference.indexOf('/');
|
|
708
|
+
if (separator <= 0 || separator === reference.length - 1)
|
|
709
|
+
return undefined;
|
|
710
|
+
return this.#options.modelRuntime.getModel(reference.slice(0, separator), reference.slice(separator + 1));
|
|
711
|
+
}
|
|
712
|
+
async #initialMessage(parentSessionId, request) {
|
|
713
|
+
if (!request.inheritContext)
|
|
714
|
+
return request.prompt;
|
|
715
|
+
const snapshot = await this.#ports.get(parentSessionId)?.snapshotContext({ maxBytes: 64 * 1024 });
|
|
716
|
+
if (!snapshot || snapshot.length === 0)
|
|
717
|
+
return request.prompt;
|
|
718
|
+
return `${request.prompt}\n\nParent context:\n${snapshot.map((entry) => `${entry.role}: ${entry.text}`).join('\n\n')}`;
|
|
719
|
+
}
|
|
720
|
+
#directChild(parentSessionId, agentId) {
|
|
721
|
+
const child = this.#children.get(agentId);
|
|
722
|
+
if (!child)
|
|
723
|
+
return failure('not_found', `Subagent not found: ${agentId}`);
|
|
724
|
+
if (child.record.parentSessionId !== parentSessionId) {
|
|
725
|
+
return failure('not_child', 'The subagent is not a direct child of this session');
|
|
726
|
+
}
|
|
727
|
+
return success(child);
|
|
728
|
+
}
|
|
729
|
+
#sessionContext(sessionId) {
|
|
730
|
+
if (sessionId === this.#options.sessionId) {
|
|
731
|
+
return { rootSessionId: sessionId, depth: 0 };
|
|
732
|
+
}
|
|
733
|
+
const child = this.#children.get(sessionId);
|
|
734
|
+
if (!child)
|
|
735
|
+
return undefined;
|
|
736
|
+
return { rootSessionId: child.record.rootSessionId, depth: child.depth, child };
|
|
737
|
+
}
|
|
738
|
+
async #cancelTree(agentId, reason, status = 'cancelled') {
|
|
739
|
+
const cancellation = await this.#serializeControl(async () => {
|
|
740
|
+
const descendants = this.#latestChildren()
|
|
741
|
+
.filter((entry) => entry.record.parentSessionId === agentId && !isTerminal(entry.record.status))
|
|
742
|
+
.map((entry) => entry.record.agentId);
|
|
743
|
+
const child = this.#latest(agentId);
|
|
744
|
+
if (!child || isTerminal(child.record.status)) {
|
|
745
|
+
return { descendants, cleanup: Promise.resolve(), deliveryId: undefined };
|
|
746
|
+
}
|
|
747
|
+
const job = this.#jobs.get(agentId);
|
|
748
|
+
if (child.record.status === 'queued') {
|
|
749
|
+
const queueIndex = job ? this.#queue.indexOf(job) : -1;
|
|
750
|
+
if (queueIndex >= 0)
|
|
751
|
+
this.#queue.splice(queueIndex, 1);
|
|
752
|
+
job?.controller.abort();
|
|
753
|
+
if (job?.slotHeld) {
|
|
754
|
+
job.slotHeld = false;
|
|
755
|
+
this.#active -= 1;
|
|
756
|
+
}
|
|
757
|
+
child.record = terminalRecord(child.record, status, {
|
|
758
|
+
code: 'host_unavailable',
|
|
759
|
+
message: reason,
|
|
760
|
+
});
|
|
761
|
+
if (child.deliveryId)
|
|
762
|
+
child.completionPending = true;
|
|
763
|
+
this.#jobs.delete(agentId);
|
|
764
|
+
await this.#persist();
|
|
765
|
+
if (job)
|
|
766
|
+
this.#notify(job);
|
|
767
|
+
this.#emit();
|
|
768
|
+
return { descendants, cleanup: Promise.resolve(), deliveryId: child.deliveryId };
|
|
769
|
+
}
|
|
770
|
+
if (!job)
|
|
771
|
+
return { descendants, cleanup: Promise.resolve(), deliveryId: undefined };
|
|
772
|
+
this.#requestCancellation(job, status, {
|
|
773
|
+
code: 'host_unavailable',
|
|
774
|
+
message: reason,
|
|
775
|
+
});
|
|
776
|
+
return { descendants, cleanup: job.runPromise ?? Promise.resolve(), deliveryId: undefined };
|
|
777
|
+
});
|
|
778
|
+
await Promise.all(cancellation.descendants.map((childId) => this.#cancelTree(childId, reason, status)));
|
|
779
|
+
await cancellation.cleanup;
|
|
780
|
+
if (cancellation.deliveryId) {
|
|
781
|
+
const child = this.#latest(agentId);
|
|
782
|
+
if (child)
|
|
783
|
+
await this.#deliver(child, cancellation.deliveryId);
|
|
784
|
+
this.#drain();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async #wait(agentId, timeoutSeconds, signal) {
|
|
788
|
+
const job = this.#jobs.get(agentId);
|
|
789
|
+
if (!job || isTerminal(job.child.record.status) || signal?.aborted)
|
|
790
|
+
return;
|
|
791
|
+
await new Promise((resolve) => {
|
|
792
|
+
let timeout;
|
|
793
|
+
const finish = () => {
|
|
794
|
+
if (timeout)
|
|
795
|
+
clearTimeout(timeout);
|
|
796
|
+
signal?.removeEventListener('abort', finish);
|
|
797
|
+
resolve();
|
|
798
|
+
};
|
|
799
|
+
job.waiters.push(finish);
|
|
800
|
+
signal?.addEventListener('abort', finish, { once: true });
|
|
801
|
+
if (timeoutSeconds)
|
|
802
|
+
timeout = setTimeout(finish, timeoutSeconds * 1_000);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
#notify(job) {
|
|
806
|
+
for (const waiter of job.waiters.splice(0))
|
|
807
|
+
waiter();
|
|
808
|
+
}
|
|
809
|
+
async #deliver(child, deliveryId) {
|
|
810
|
+
if (!child.completionPending || child.deliveryId !== deliveryId)
|
|
811
|
+
return;
|
|
812
|
+
const existing = this.#deliveries.get(deliveryId);
|
|
813
|
+
if (existing)
|
|
814
|
+
return existing;
|
|
815
|
+
const notice = completionNotice(child.record, deliveryId);
|
|
816
|
+
const delivery = (async () => {
|
|
817
|
+
const port = this.#ports.get(child.record.parentSessionId);
|
|
818
|
+
if (!port)
|
|
819
|
+
return;
|
|
820
|
+
const outcome = await port.deliverCompletion(notice);
|
|
821
|
+
if (outcome === 'delivered') {
|
|
822
|
+
await this.#serializeControl(async () => {
|
|
823
|
+
if (child.deliveryId !== deliveryId)
|
|
824
|
+
return;
|
|
825
|
+
child.completionPending = false;
|
|
826
|
+
await this.#persist();
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
else if (outcome === 'queued' && !this.#closed) {
|
|
830
|
+
const retry = setTimeout(() => {
|
|
831
|
+
this.#deliver(child, deliveryId).catch(() => { });
|
|
832
|
+
}, 10);
|
|
833
|
+
retry.unref();
|
|
834
|
+
}
|
|
835
|
+
})();
|
|
836
|
+
this.#deliveries.set(deliveryId, delivery);
|
|
837
|
+
try {
|
|
838
|
+
await delivery;
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
if (this.#deliveries.get(deliveryId) === delivery)
|
|
842
|
+
this.#deliveries.delete(deliveryId);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async #deliverPending(parentSessionId) {
|
|
846
|
+
const children = [...this.#children.values()]
|
|
847
|
+
.filter((child) => child.record.parentSessionId === parentSessionId && child.completionPending);
|
|
848
|
+
for (const child of children) {
|
|
849
|
+
if (child.deliveryId)
|
|
850
|
+
await this.#deliver(child, child.deliveryId);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
#latest(agentId) {
|
|
854
|
+
return this.#children.get(agentId);
|
|
855
|
+
}
|
|
856
|
+
#latestChildren() {
|
|
857
|
+
return [...this.#children.values()];
|
|
858
|
+
}
|
|
859
|
+
#emit() {
|
|
860
|
+
const records = this.#latestChildren().map(localView);
|
|
861
|
+
for (const listener of this.#listeners)
|
|
862
|
+
listener(records);
|
|
863
|
+
}
|
|
864
|
+
async #persist() {
|
|
865
|
+
const snapshot = [...this.#children.values()].map(toStored);
|
|
866
|
+
this.#save = this.#save.then(() => this.#store.save(snapshot));
|
|
867
|
+
await this.#save;
|
|
868
|
+
}
|
|
869
|
+
async #serializeControl(operation) {
|
|
870
|
+
const previous = this.#control;
|
|
871
|
+
let release;
|
|
872
|
+
this.#control = new Promise((resolve) => {
|
|
873
|
+
release = resolve;
|
|
874
|
+
});
|
|
875
|
+
await previous;
|
|
876
|
+
try {
|
|
877
|
+
return await operation();
|
|
878
|
+
}
|
|
879
|
+
finally {
|
|
880
|
+
release();
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async #resultRecord(child) {
|
|
884
|
+
const record = cloneRecord(child.record);
|
|
885
|
+
if (record.status !== 'completed' || !child.sessionFile || !record.completedAt)
|
|
886
|
+
return record;
|
|
887
|
+
try {
|
|
888
|
+
const result = await finalAssistantText(child.sessionFile, record.completedAt);
|
|
889
|
+
return result === undefined ? record : { ...record, result };
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return record;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function completionNotice(record, deliveryId) {
|
|
897
|
+
return {
|
|
898
|
+
deliveryId,
|
|
899
|
+
parentSessionId: record.parentSessionId,
|
|
900
|
+
agentId: record.agentId,
|
|
901
|
+
type: record.type,
|
|
902
|
+
status: record.status,
|
|
903
|
+
...(record.result === undefined ? {} : { summary: record.result.slice(0, 4_000) }),
|
|
904
|
+
...(record.error === undefined ? {} : { error: record.error }),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function terminalRecord(record, status, error) {
|
|
908
|
+
return {
|
|
909
|
+
...record,
|
|
910
|
+
status,
|
|
911
|
+
completedAt: new Date().toISOString(),
|
|
912
|
+
...(error === undefined ? {} : { error }),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function isTerminal(status) {
|
|
916
|
+
return status === 'completed' || status === 'failed' || status === 'timed_out' || status === 'cancelled';
|
|
917
|
+
}
|
|
918
|
+
function success(value) {
|
|
919
|
+
return { ok: true, value };
|
|
920
|
+
}
|
|
921
|
+
function failure(code, message) {
|
|
922
|
+
return { ok: false, error: { code, message } };
|
|
923
|
+
}
|
|
924
|
+
function cloneRecord(record) {
|
|
925
|
+
return structuredClone(record);
|
|
926
|
+
}
|
|
927
|
+
function localView(child) {
|
|
928
|
+
return cloneRecord(child.record);
|
|
929
|
+
}
|
|
930
|
+
function fromStored(stored) {
|
|
931
|
+
return structuredClone(stored);
|
|
932
|
+
}
|
|
933
|
+
function toStored(child) {
|
|
934
|
+
return structuredClone(child);
|
|
935
|
+
}
|
|
936
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
937
|
+
return value === undefined || !Number.isInteger(value)
|
|
938
|
+
? fallback
|
|
939
|
+
: Math.max(minimum, Math.min(maximum, value));
|
|
940
|
+
}
|
|
941
|
+
function boundResult(result) {
|
|
942
|
+
const bytes = Buffer.from(result);
|
|
943
|
+
return bytes.byteLength <= MAX_RESULT_BYTES
|
|
944
|
+
? result
|
|
945
|
+
: `${new TextDecoder().decode(bytes.subarray(0, MAX_RESULT_BYTES))}\n[truncated]`;
|
|
946
|
+
}
|
|
947
|
+
function byteLength(value) {
|
|
948
|
+
return Buffer.byteLength(value);
|
|
949
|
+
}
|
|
950
|
+
function sessionFileOutcome(sessionFile) {
|
|
951
|
+
return sessionFile === undefined ? {} : { sessionFile };
|
|
952
|
+
}
|
|
953
|
+
async function preflightSessionFile(sessionFile, expectedSessionId) {
|
|
954
|
+
const contents = await readFile(sessionFile, 'utf8');
|
|
955
|
+
let header;
|
|
956
|
+
for (const line of contents.split('\n')) {
|
|
957
|
+
if (!line.trim())
|
|
958
|
+
continue;
|
|
959
|
+
const entry = JSON.parse(line);
|
|
960
|
+
header ??= entry;
|
|
961
|
+
}
|
|
962
|
+
if (typeof header !== 'object'
|
|
963
|
+
|| header === null
|
|
964
|
+
|| Reflect.get(header, 'type') !== 'session'
|
|
965
|
+
|| Reflect.get(header, 'version') !== CURRENT_SESSION_VERSION
|
|
966
|
+
|| Reflect.get(header, 'id') !== expectedSessionId
|
|
967
|
+
|| typeof Reflect.get(header, 'timestamp') !== 'string'
|
|
968
|
+
|| typeof Reflect.get(header, 'cwd') !== 'string') {
|
|
969
|
+
throw new Error('Retained session file has an invalid header');
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async function finalAssistantText(sessionFile, completedAt) {
|
|
973
|
+
let result;
|
|
974
|
+
for (const line of (await readFile(sessionFile, 'utf8')).split('\n')) {
|
|
975
|
+
if (!line)
|
|
976
|
+
continue;
|
|
977
|
+
const entry = JSON.parse(line);
|
|
978
|
+
if (entry.type !== 'message'
|
|
979
|
+
|| entry.message?.role !== 'assistant'
|
|
980
|
+
|| typeof entry.timestamp !== 'string'
|
|
981
|
+
|| entry.timestamp > completedAt
|
|
982
|
+
|| !Array.isArray(entry.message.content))
|
|
983
|
+
continue;
|
|
984
|
+
result = entry.message.content
|
|
985
|
+
.filter((part) => (typeof part === 'object'
|
|
986
|
+
&& part !== null
|
|
987
|
+
&& Reflect.get(part, 'type') === 'text'
|
|
988
|
+
&& typeof Reflect.get(part, 'text') === 'string'))
|
|
989
|
+
.map((part) => part.text)
|
|
990
|
+
.join('\n');
|
|
991
|
+
}
|
|
992
|
+
return result;
|
|
993
|
+
}
|
|
994
|
+
//# sourceMappingURL=host.js.map
|