@narumitw/pi-subagents 0.13.1 → 0.14.1
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/README.md +134 -10
- package/package.json +3 -3
- package/src/agents.ts +17 -0
- package/src/context.ts +129 -0
- package/src/execution.ts +69 -22
- package/src/in-process-transport.ts +598 -0
- package/src/limits.ts +62 -0
- package/src/orchestration.ts +164 -0
- package/src/persistence.ts +207 -0
- package/src/protocol.ts +76 -0
- package/src/registry.ts +759 -0
- package/src/runner.ts +202 -74
- package/src/settings.ts +55 -9
- package/src/stateful-prompt.ts +43 -0
- package/src/stateful.ts +838 -0
- package/src/subagents.ts +12 -2
- package/src/subprocess-transport.ts +50 -0
- package/src/transport.ts +30 -0
- package/src/workspace.ts +116 -0
package/src/runner.ts
CHANGED
|
@@ -11,8 +11,17 @@ import type {
|
|
|
11
11
|
AgentSource,
|
|
12
12
|
SubagentThinkingLevel,
|
|
13
13
|
} from "./agents.js";
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
appendBounded,
|
|
16
|
+
DEFAULT_MAX_CONTEXT_BYTES,
|
|
17
|
+
DEFAULT_MAX_MESSAGES,
|
|
18
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
19
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
20
|
+
truncateUtf8,
|
|
21
|
+
} from "./limits.js";
|
|
22
|
+
import { JsonLineDecoder } from "./protocol.js";
|
|
23
|
+
|
|
24
|
+
export const KILL_GRACE_MS = 5000;
|
|
16
25
|
|
|
17
26
|
export interface UsageStats {
|
|
18
27
|
input: number;
|
|
@@ -40,6 +49,14 @@ export interface SingleResult {
|
|
|
40
49
|
finalOutput?: string;
|
|
41
50
|
timedOut?: boolean;
|
|
42
51
|
timeoutMs?: number;
|
|
52
|
+
aborted?: boolean;
|
|
53
|
+
truncated?: boolean;
|
|
54
|
+
malformedEvents?: number;
|
|
55
|
+
policy?: {
|
|
56
|
+
inherited: string[];
|
|
57
|
+
overridden: string[];
|
|
58
|
+
unsupported: string[];
|
|
59
|
+
};
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
export interface SubagentDetails {
|
|
@@ -48,6 +65,7 @@ export interface SubagentDetails {
|
|
|
48
65
|
projectAgentsDir: string | null;
|
|
49
66
|
results: SingleResult[];
|
|
50
67
|
aggregator?: SingleResult;
|
|
68
|
+
isError?: boolean;
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
function getFinalOutput(messages: Message[]): string {
|
|
@@ -66,8 +84,28 @@ export function getResultFinalOutput(result: SingleResult): string {
|
|
|
66
84
|
return result.finalOutput ?? getFinalOutput(result.messages);
|
|
67
85
|
}
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
function boundMessageText(message: Message, maxBytes: number): { message: Message; bytes: number; truncated: boolean } {
|
|
88
|
+
const serializedBytes = Buffer.byteLength(JSON.stringify(message), "utf8");
|
|
89
|
+
if (serializedBytes <= maxBytes) return { message, bytes: serializedBytes, truncated: false };
|
|
90
|
+
if (!Array.isArray(message.content)) return { message, bytes: Math.min(serializedBytes, maxBytes), truncated: true };
|
|
91
|
+
let remaining = maxBytes;
|
|
92
|
+
const content = message.content
|
|
93
|
+
.filter((part) => part.type === "text")
|
|
94
|
+
.map((part) => {
|
|
95
|
+
const bounded = truncateUtf8(part.text, remaining);
|
|
96
|
+
remaining = Math.max(0, remaining - Buffer.byteLength(bounded.text, "utf8"));
|
|
97
|
+
return { ...part, text: bounded.text };
|
|
98
|
+
});
|
|
99
|
+
const boundedMessage = { ...message, content } as Message;
|
|
100
|
+
return {
|
|
101
|
+
message: boundedMessage,
|
|
102
|
+
bytes: Math.min(Buffer.byteLength(JSON.stringify(boundedMessage), "utf8"), maxBytes),
|
|
103
|
+
truncated: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildFanInContext(results: SingleResult[], maxBytes = DEFAULT_MAX_CONTEXT_BYTES): string {
|
|
108
|
+
const text = results
|
|
71
109
|
.map((result, index) => {
|
|
72
110
|
const status = result.exitCode === 0 ? "completed" : result.exitCode === -1 ? "running" : "failed";
|
|
73
111
|
const output = getResultFinalOutput(result);
|
|
@@ -79,12 +117,15 @@ export function buildFanInContext(results: SingleResult[]): string {
|
|
|
79
117
|
].join("\n\n");
|
|
80
118
|
})
|
|
81
119
|
.join("\n\n---\n\n");
|
|
120
|
+
return truncateUtf8(text, maxBytes).text;
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
export async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
85
124
|
items: TIn[],
|
|
86
125
|
concurrency: number,
|
|
87
126
|
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
127
|
+
signal?: AbortSignal,
|
|
128
|
+
onSkipped?: (item: TIn, index: number) => TOut,
|
|
88
129
|
): Promise<TOut[]> {
|
|
89
130
|
if (items.length === 0) return [];
|
|
90
131
|
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
@@ -94,6 +135,10 @@ export async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
|
94
135
|
while (true) {
|
|
95
136
|
const current = nextIndex++;
|
|
96
137
|
if (current >= items.length) return;
|
|
138
|
+
if (signal?.aborted && onSkipped) {
|
|
139
|
+
results[current] = onSkipped(items[current], current);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
97
142
|
results[current] = await fn(items[current], current);
|
|
98
143
|
}
|
|
99
144
|
});
|
|
@@ -146,30 +191,37 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
|
146
191
|
return { command: "pi", args };
|
|
147
192
|
}
|
|
148
193
|
|
|
149
|
-
function
|
|
150
|
-
if (proc.killed) return;
|
|
194
|
+
function signalProcess(proc: ReturnType<typeof spawn>, signal: NodeJS.Signals): void {
|
|
151
195
|
if (process.platform !== "win32" && proc.pid) {
|
|
152
196
|
try {
|
|
153
|
-
process.kill(-proc.pid,
|
|
197
|
+
process.kill(-proc.pid, signal);
|
|
198
|
+
return;
|
|
154
199
|
} catch {
|
|
155
|
-
|
|
200
|
+
// Fall back to signaling the immediate child when process-group signaling is unavailable.
|
|
156
201
|
}
|
|
157
|
-
} else {
|
|
158
|
-
proc.kill("SIGTERM");
|
|
159
202
|
}
|
|
203
|
+
try {
|
|
204
|
+
proc.kill(signal);
|
|
205
|
+
} catch {
|
|
206
|
+
// The process may already have exited.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
160
209
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
210
|
+
export function terminateProcess(proc: ReturnType<typeof spawn>, graceMs = KILL_GRACE_MS): () => void {
|
|
211
|
+
let closed = proc.exitCode !== null || proc.signalCode !== null;
|
|
212
|
+
const onClose = () => {
|
|
213
|
+
closed = true;
|
|
214
|
+
};
|
|
215
|
+
proc.once("close", onClose);
|
|
216
|
+
if (!closed) signalProcess(proc, "SIGTERM");
|
|
217
|
+
const escalation = setTimeout(() => {
|
|
218
|
+
if (!closed) signalProcess(proc, "SIGKILL");
|
|
219
|
+
}, graceMs);
|
|
220
|
+
escalation.unref();
|
|
221
|
+
return () => {
|
|
222
|
+
clearTimeout(escalation);
|
|
223
|
+
proc.off("close", onClose);
|
|
224
|
+
};
|
|
173
225
|
}
|
|
174
226
|
|
|
175
227
|
export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
@@ -186,6 +238,7 @@ export async function runSingleAgent(
|
|
|
186
238
|
timeoutMs: number,
|
|
187
239
|
onUpdate: OnUpdateCallback | undefined,
|
|
188
240
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
241
|
+
invocationOverride?: { command: string; argsPrefix?: string[] },
|
|
189
242
|
): Promise<SingleResult> {
|
|
190
243
|
const agent = agents.find((a) => a.name === agentName);
|
|
191
244
|
|
|
@@ -233,6 +286,26 @@ export async function runSingleAgent(
|
|
|
233
286
|
};
|
|
234
287
|
|
|
235
288
|
try {
|
|
289
|
+
const effectiveCwd = cwd ?? defaultCwd;
|
|
290
|
+
try {
|
|
291
|
+
if (!fs.statSync(effectiveCwd).isDirectory()) throw new Error("not a directory");
|
|
292
|
+
} catch (error) {
|
|
293
|
+
currentResult.exitCode = 1;
|
|
294
|
+
currentResult.stopReason = "error";
|
|
295
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
296
|
+
currentResult.errorMessage = `Invalid subagent cwd: ${effectiveCwd} (${reason})`;
|
|
297
|
+
currentResult.stderr = currentResult.errorMessage;
|
|
298
|
+
return currentResult;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (signal?.aborted) {
|
|
302
|
+
currentResult.exitCode = 130;
|
|
303
|
+
currentResult.aborted = true;
|
|
304
|
+
currentResult.stopReason = "aborted";
|
|
305
|
+
currentResult.errorMessage = "Subagent was aborted before start";
|
|
306
|
+
return currentResult;
|
|
307
|
+
}
|
|
308
|
+
|
|
236
309
|
if (agent.systemPrompt.trim()) {
|
|
237
310
|
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
238
311
|
tmpPromptDir = tmp.dir;
|
|
@@ -250,45 +323,63 @@ export async function runSingleAgent(
|
|
|
250
323
|
let timedOut = false;
|
|
251
324
|
|
|
252
325
|
const exitCode = await new Promise<number>((resolve) => {
|
|
253
|
-
const invocation =
|
|
326
|
+
const invocation = invocationOverride
|
|
327
|
+
? {
|
|
328
|
+
command: invocationOverride.command,
|
|
329
|
+
args: [...(invocationOverride.argsPrefix ?? []), ...args],
|
|
330
|
+
}
|
|
331
|
+
: getPiInvocation(args);
|
|
254
332
|
let settled = false;
|
|
333
|
+
let cleanupTermination: (() => void) | undefined;
|
|
334
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
335
|
+
let abortHandler: (() => void) | undefined;
|
|
255
336
|
const finish = (code: number) => {
|
|
256
337
|
if (settled) return;
|
|
257
338
|
settled = true;
|
|
258
|
-
clearTimeout(timeout);
|
|
339
|
+
if (timeout) clearTimeout(timeout);
|
|
340
|
+
cleanupTermination?.();
|
|
341
|
+
if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
259
342
|
resolve(code);
|
|
260
343
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
344
|
+
let proc: ReturnType<typeof spawn>;
|
|
345
|
+
try {
|
|
346
|
+
proc = spawn(invocation.command, invocation.args, {
|
|
347
|
+
cwd: effectiveCwd,
|
|
348
|
+
detached: process.platform !== "win32",
|
|
349
|
+
shell: false,
|
|
350
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
351
|
+
env: {
|
|
352
|
+
...process.env,
|
|
353
|
+
PI_SUBAGENT_DEPTH: String(
|
|
354
|
+
(Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1,
|
|
355
|
+
),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
currentResult.errorMessage = error instanceof Error ? error.message : String(error);
|
|
360
|
+
currentResult.stderr = currentResult.errorMessage;
|
|
361
|
+
finish(1);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
278
364
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
event = JSON.parse(line);
|
|
284
|
-
} catch {
|
|
365
|
+
let capturedMessageBytes = 0;
|
|
366
|
+
const addMessage = (msg: Message) => {
|
|
367
|
+
if (currentResult.messages.length >= DEFAULT_MAX_MESSAGES) {
|
|
368
|
+
currentResult.truncated = true;
|
|
285
369
|
return;
|
|
286
370
|
}
|
|
287
|
-
|
|
371
|
+
const remaining = Math.max(0, DEFAULT_MAX_OUTPUT_BYTES - capturedMessageBytes);
|
|
372
|
+
const boundedMessage = boundMessageText(msg, remaining);
|
|
373
|
+
capturedMessageBytes += boundedMessage.bytes;
|
|
374
|
+
currentResult.truncated ||= boundedMessage.truncated;
|
|
375
|
+
currentResult.messages.push(boundedMessage.message);
|
|
376
|
+
};
|
|
377
|
+
const processEvent = (raw: unknown) => {
|
|
378
|
+
if (!raw || typeof raw !== "object") return;
|
|
379
|
+
const event = raw as { type?: string; message?: Message };
|
|
288
380
|
if (event.type === "message_end" && event.message) {
|
|
289
|
-
const msg = event.message
|
|
290
|
-
|
|
291
|
-
|
|
381
|
+
const msg = event.message;
|
|
382
|
+
addMessage(msg);
|
|
292
383
|
if (msg.role === "assistant") {
|
|
293
384
|
currentResult.usage.turns++;
|
|
294
385
|
const usage = msg.usage;
|
|
@@ -305,51 +396,88 @@ export async function runSingleAgent(
|
|
|
305
396
|
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
306
397
|
}
|
|
307
398
|
emitUpdate();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (event.type === "tool_result_end" && event.message) {
|
|
311
|
-
currentResult.messages.push(event.message as Message);
|
|
399
|
+
} else if (event.type === "tool_result_end" && event.message) {
|
|
400
|
+
addMessage(event.message);
|
|
312
401
|
emitUpdate();
|
|
313
402
|
}
|
|
314
403
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
404
|
+
const decoder = new JsonLineDecoder({
|
|
405
|
+
onValue: processEvent,
|
|
406
|
+
onMalformed: () => {
|
|
407
|
+
currentResult.malformedEvents = (currentResult.malformedEvents ?? 0) + 1;
|
|
408
|
+
},
|
|
409
|
+
onOversized: () => {
|
|
410
|
+
currentResult.truncated = true;
|
|
411
|
+
},
|
|
321
412
|
});
|
|
322
413
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
414
|
+
timeout = setTimeout(() => {
|
|
415
|
+
timedOut = true;
|
|
416
|
+
currentResult.timedOut = true;
|
|
417
|
+
currentResult.stopReason = "timeout";
|
|
418
|
+
currentResult.errorMessage = `Subagent timed out after ${timeoutMs}ms`;
|
|
419
|
+
const bounded = appendBounded(
|
|
420
|
+
currentResult.stderr,
|
|
421
|
+
`\nSubagent timed out after ${timeoutMs}ms.`,
|
|
422
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
423
|
+
);
|
|
424
|
+
currentResult.stderr = bounded.text;
|
|
425
|
+
currentResult.truncated ||= bounded.truncated;
|
|
426
|
+
emitUpdate();
|
|
427
|
+
cleanupTermination = terminateProcess(proc);
|
|
428
|
+
}, timeoutMs);
|
|
429
|
+
timeout.unref();
|
|
326
430
|
|
|
431
|
+
proc.stdout?.on("data", (data) => decoder.push(data));
|
|
432
|
+
proc.stderr?.on("data", (data) => {
|
|
433
|
+
const bounded = appendBounded(currentResult.stderr, data.toString(), DEFAULT_MAX_STDERR_BYTES);
|
|
434
|
+
currentResult.stderr = bounded.text;
|
|
435
|
+
currentResult.truncated ||= bounded.truncated;
|
|
436
|
+
});
|
|
327
437
|
proc.on("close", (code) => {
|
|
328
|
-
|
|
329
|
-
finish(timedOut ? 124 : (code ?? 0));
|
|
438
|
+
decoder.finish();
|
|
439
|
+
finish(timedOut ? 124 : wasAborted ? 130 : (code ?? 0));
|
|
330
440
|
});
|
|
331
|
-
|
|
332
441
|
proc.on("error", (error) => {
|
|
333
442
|
currentResult.errorMessage = error.message;
|
|
334
|
-
|
|
443
|
+
const bounded = appendBounded(
|
|
444
|
+
currentResult.stderr,
|
|
445
|
+
`${currentResult.stderr ? "\n" : ""}${error.message}`,
|
|
446
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
447
|
+
);
|
|
448
|
+
currentResult.stderr = bounded.text;
|
|
449
|
+
currentResult.truncated ||= bounded.truncated;
|
|
335
450
|
finish(1);
|
|
336
451
|
});
|
|
337
452
|
|
|
338
453
|
if (signal) {
|
|
339
|
-
|
|
454
|
+
abortHandler = () => {
|
|
455
|
+
if (timedOut || settled) return;
|
|
340
456
|
wasAborted = true;
|
|
457
|
+
currentResult.aborted = true;
|
|
341
458
|
currentResult.stopReason = "aborted";
|
|
342
459
|
currentResult.errorMessage = "Subagent was aborted";
|
|
343
|
-
terminateProcess(proc);
|
|
460
|
+
cleanupTermination = terminateProcess(proc);
|
|
344
461
|
};
|
|
345
|
-
if (signal.aborted)
|
|
346
|
-
else signal.addEventListener("abort",
|
|
462
|
+
if (signal.aborted) abortHandler();
|
|
463
|
+
else signal.addEventListener("abort", abortHandler, { once: true });
|
|
347
464
|
}
|
|
348
465
|
});
|
|
349
466
|
|
|
350
467
|
currentResult.exitCode = exitCode;
|
|
351
|
-
|
|
352
|
-
|
|
468
|
+
const final = truncateUtf8(getFinalOutput(currentResult.messages), DEFAULT_MAX_OUTPUT_BYTES);
|
|
469
|
+
currentResult.finalOutput = final.text;
|
|
470
|
+
currentResult.truncated ||= final.truncated;
|
|
471
|
+
currentResult.policy = {
|
|
472
|
+
inherited: ["environment"],
|
|
473
|
+
overridden: [
|
|
474
|
+
"cwd",
|
|
475
|
+
...(agent.model ? ["model"] : []),
|
|
476
|
+
...(thinkingLevel ? ["thinkingLevel"] : []),
|
|
477
|
+
...(agent.tools ? ["tools"] : []),
|
|
478
|
+
],
|
|
479
|
+
unsupported: ["approvalPolicy", "sandboxProfile", "providerHeaders"],
|
|
480
|
+
};
|
|
353
481
|
return currentResult;
|
|
354
482
|
} finally {
|
|
355
483
|
if (tmpPromptPath)
|
package/src/settings.ts
CHANGED
|
@@ -25,6 +25,14 @@ function isPositiveNumber(value: unknown): value is number {
|
|
|
25
25
|
return typeof value === "number" && Number.isFinite(value) && value >= 1;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function isPositiveInteger(value: unknown): value is number {
|
|
29
|
+
return isPositiveNumber(value) && Number.isSafeInteger(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
33
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | undefined {
|
|
29
37
|
if (!isPlainObject(value)) return undefined;
|
|
30
38
|
|
|
@@ -60,16 +68,54 @@ export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | un
|
|
|
60
68
|
|
|
61
69
|
export function normalizeSubagentSettings(value: unknown): SubagentSettings | undefined {
|
|
62
70
|
if (!isPlainObject(value)) return undefined;
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
const settings: SubagentSettings = {};
|
|
72
|
+
if (hasOwn(value, "agents")) {
|
|
73
|
+
if (!isPlainObject(value.agents)) return undefined;
|
|
74
|
+
const agents: Record<string, SubagentAgentConfig> = {};
|
|
75
|
+
for (const [name, rawConfig] of Object.entries(value.agents)) {
|
|
76
|
+
const config = normalizeAgentSettings(rawConfig);
|
|
77
|
+
if (config) agents[name] = config;
|
|
78
|
+
}
|
|
79
|
+
if (Object.keys(agents).length > 0) settings.agents = agents;
|
|
70
80
|
}
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
if (hasOwn(value, "stateful")) {
|
|
82
|
+
if (!isPlainObject(value.stateful)) return undefined;
|
|
83
|
+
const runtime: NonNullable<SubagentSettings["stateful"]> = {};
|
|
84
|
+
if (hasOwn(value.stateful, "transport")) {
|
|
85
|
+
if (value.stateful.transport !== "subprocess" && value.stateful.transport !== "in-process") {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
runtime.transport = value.stateful.transport;
|
|
89
|
+
}
|
|
90
|
+
for (const key of [
|
|
91
|
+
"maxAgents",
|
|
92
|
+
"maxActiveTurns",
|
|
93
|
+
"maxChildrenPerAgent",
|
|
94
|
+
"maxMailboxMessages",
|
|
95
|
+
"maxMailboxMessageBytes",
|
|
96
|
+
"idleTtlMs",
|
|
97
|
+
"maxStoredAgents",
|
|
98
|
+
] as const) {
|
|
99
|
+
if (hasOwn(value.stateful, key)) {
|
|
100
|
+
if (!isPositiveInteger(value.stateful[key])) return undefined;
|
|
101
|
+
runtime[key] = value.stateful[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (hasOwn(value.stateful, "maxDepth")) {
|
|
105
|
+
if (!isNonNegativeInteger(value.stateful.maxDepth)) return undefined;
|
|
106
|
+
runtime.maxDepth = value.stateful.maxDepth;
|
|
107
|
+
}
|
|
108
|
+
if (hasOwn(value.stateful, "retentionDays")) {
|
|
109
|
+
if (!isPositiveNumber(value.stateful.retentionDays)) return undefined;
|
|
110
|
+
runtime.retentionDays = value.stateful.retentionDays;
|
|
111
|
+
}
|
|
112
|
+
if (hasOwn(value.stateful, "enabled")) {
|
|
113
|
+
if (typeof value.stateful.enabled !== "boolean") return undefined;
|
|
114
|
+
runtime.enabled = value.stateful.enabled;
|
|
115
|
+
}
|
|
116
|
+
settings.stateful = runtime;
|
|
117
|
+
}
|
|
118
|
+
return settings;
|
|
73
119
|
}
|
|
74
120
|
|
|
75
121
|
export function readSubagentSettings(): SubagentSettings | undefined {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AgentConfig } from "./agents.js";
|
|
2
|
+
import { resolveDefaultSubagentTimeoutMs } from "./execution.js";
|
|
3
|
+
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
4
|
+
import { redactPrivateText } from "./context.js";
|
|
5
|
+
import type { ManagedAgent } from "./registry.js";
|
|
6
|
+
|
|
7
|
+
export function buildStatefulTurnPrompt(
|
|
8
|
+
record: Pick<
|
|
9
|
+
ManagedAgent,
|
|
10
|
+
"context" | "history" | "mailbox" | "currentMailboxMessageIds"
|
|
11
|
+
>,
|
|
12
|
+
task: string,
|
|
13
|
+
maxBytes = DEFAULT_MAX_CONTEXT_BYTES,
|
|
14
|
+
): { text: string; truncated: boolean } {
|
|
15
|
+
const previous = record.history
|
|
16
|
+
.map((turn) => {
|
|
17
|
+
const redactedTask = redactPrivateText(turn.task);
|
|
18
|
+
const redactedOutput = redactPrivateText(turn.output);
|
|
19
|
+
return `Task: ${redactedTask}\nOutput: ${redactedOutput}`;
|
|
20
|
+
})
|
|
21
|
+
.join("\n\n");
|
|
22
|
+
const currentMessageIds = new Set(record.currentMailboxMessageIds ?? []);
|
|
23
|
+
const messages = record.mailbox
|
|
24
|
+
.filter((message) => currentMessageIds.has(message.id))
|
|
25
|
+
.slice(-20)
|
|
26
|
+
.map((message) => `From ${message.senderId}: ${redactPrivateText(message.content)}`)
|
|
27
|
+
.join("\n");
|
|
28
|
+
const context = [
|
|
29
|
+
`Current task:\n${redactPrivateText(task)}`,
|
|
30
|
+
messages ? `Mailbox messages:\n${messages}` : "",
|
|
31
|
+
previous ? `Prior subagent turns:\n${previous}` : "",
|
|
32
|
+
record.context ? `Parent context:\n${redactPrivateText(record.context)}` : "",
|
|
33
|
+
]
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join("\n\n---\n\n");
|
|
36
|
+
return truncateUtf8(context, maxBytes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveStatefulTurnTimeout(
|
|
40
|
+
agent: Pick<AgentConfig, "timeoutMs"> | undefined,
|
|
41
|
+
): number {
|
|
42
|
+
return agent?.timeoutMs ?? resolveDefaultSubagentTimeoutMs();
|
|
43
|
+
}
|