@narumitw/pi-subagents 0.13.0 → 0.14.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/README.md +137 -12
- package/package.json +3 -3
- package/src/agents.ts +17 -0
- package/src/config-ui.ts +271 -0
- package/src/context.ts +129 -0
- package/src/execution.ts +453 -0
- package/src/in-process-transport.ts +598 -0
- package/src/limits.ts +62 -0
- package/src/orchestration.ts +164 -0
- package/src/params.ts +59 -0
- package/src/persistence.ts +207 -0
- package/src/protocol.ts +76 -0
- package/src/registry.ts +759 -0
- package/src/render.ts +521 -0
- package/src/runner.ts +496 -0
- package/src/settings.ts +166 -0
- package/src/stateful-prompt.ts +43 -0
- package/src/stateful.ts +838 -0
- package/src/subagents.ts +36 -1682
- package/src/subprocess-transport.ts +50 -0
- package/src/transport.ts +30 -0
- package/src/workspace.ts +116 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
createAgentSession,
|
|
4
|
+
DefaultResourceLoader,
|
|
5
|
+
getAgentDir,
|
|
6
|
+
type ModelRegistry,
|
|
7
|
+
SessionManager,
|
|
8
|
+
SettingsManager,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { type AgentConfig, discoverAgents, type SubagentThinkingLevel } from "./agents.js";
|
|
11
|
+
import { redactPrivateText } from "./context.js";
|
|
12
|
+
import { resolveDefaultSubagentTimeoutMs } from "./execution.js";
|
|
13
|
+
import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
|
|
14
|
+
import type { AgentTurn, ManagedAgent, TurnOutcome } from "./registry.js";
|
|
15
|
+
import { readSubagentSettings } from "./settings.js";
|
|
16
|
+
import type { SubagentTransport } from "./transport.js";
|
|
17
|
+
|
|
18
|
+
const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
19
|
+
const DEFAULT_ABORT_GRACE_MS = 5_000;
|
|
20
|
+
|
|
21
|
+
export interface ParentRuntimeSnapshot {
|
|
22
|
+
model: Model<Api> | undefined;
|
|
23
|
+
thinkingLevel: SubagentThinkingLevel;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChildSession {
|
|
27
|
+
readonly sessionId: string;
|
|
28
|
+
readonly messages: readonly unknown[];
|
|
29
|
+
prompt(text: string): Promise<void>;
|
|
30
|
+
subscribe(listener: (event: unknown) => void): () => void;
|
|
31
|
+
abort(): Promise<void>;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
getActiveToolNames(): string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ChildSessionCreateOptions {
|
|
37
|
+
agent: ManagedAgent;
|
|
38
|
+
agentConfig: AgentConfig;
|
|
39
|
+
context?: string;
|
|
40
|
+
history: AgentTurn[];
|
|
41
|
+
modelRegistry: ModelRegistry;
|
|
42
|
+
parentRuntime: ParentRuntimeSnapshot;
|
|
43
|
+
tools?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ChildSessionFactory = (options: ChildSessionCreateOptions) => Promise<ChildSession>;
|
|
47
|
+
|
|
48
|
+
export interface InProcessTransportOptions {
|
|
49
|
+
modelRegistry: ModelRegistry;
|
|
50
|
+
getParentRuntime: () => ParentRuntimeSnapshot;
|
|
51
|
+
createSession?: ChildSessionFactory;
|
|
52
|
+
discoverAgent?: (agent: ManagedAgent) => AgentConfig | undefined;
|
|
53
|
+
defaultTimeoutMs?: number;
|
|
54
|
+
abortGraceMs?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ChildSessionRecord {
|
|
58
|
+
session: ChildSession;
|
|
59
|
+
unsubscribe: () => void;
|
|
60
|
+
lastOutput: string;
|
|
61
|
+
disposed: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type PromptSettlement =
|
|
65
|
+
| { kind: "completed" }
|
|
66
|
+
| { kind: "failed"; error: unknown }
|
|
67
|
+
| { kind: "timeout" }
|
|
68
|
+
| { kind: "aborted" };
|
|
69
|
+
|
|
70
|
+
export class InProcessTransport implements SubagentTransport {
|
|
71
|
+
readonly kind = "in-process" as const;
|
|
72
|
+
private readonly sessions = new Map<string, ChildSessionRecord>();
|
|
73
|
+
private readonly createSession: ChildSessionFactory;
|
|
74
|
+
private readonly discoverAgent: (agent: ManagedAgent) => AgentConfig | undefined;
|
|
75
|
+
private readonly defaultTimeoutMs: number;
|
|
76
|
+
private readonly abortGraceMs: number;
|
|
77
|
+
|
|
78
|
+
constructor(private readonly options: InProcessTransportOptions) {
|
|
79
|
+
this.createSession = options.createSession ?? createSdkChildSession;
|
|
80
|
+
this.discoverAgent =
|
|
81
|
+
options.discoverAgent ??
|
|
82
|
+
((agent) =>
|
|
83
|
+
discoverAgents(agent.cwd, agent.agentScope ?? "user", readSubagentSettings()).agents.find(
|
|
84
|
+
(candidate) => candidate.name === agent.agent,
|
|
85
|
+
));
|
|
86
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? resolveDefaultSubagentTimeoutMs();
|
|
87
|
+
this.abortGraceMs = options.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async runTurn(agent: ManagedAgent, task: string, signal: AbortSignal): Promise<TurnOutcome> {
|
|
91
|
+
if (signal.aborted) return interruptedOutcome("");
|
|
92
|
+
const agentConfig = this.discoverAgent(agent);
|
|
93
|
+
if (!agentConfig) {
|
|
94
|
+
return { output: "", exitCode: 1, error: `Unknown subagent: ${agent.agent}` };
|
|
95
|
+
}
|
|
96
|
+
let tools: string[] | undefined;
|
|
97
|
+
try {
|
|
98
|
+
tools = validateInProcessTools(agentConfig.tools);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return { output: "", exitCode: 1, error: errorMessage(error) };
|
|
101
|
+
}
|
|
102
|
+
let record: ChildSessionRecord;
|
|
103
|
+
try {
|
|
104
|
+
record = await this.getOrCreate(agent, agentConfig, tools);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { output: "", exitCode: 1, error: errorMessage(error) };
|
|
107
|
+
}
|
|
108
|
+
if (signal.aborted) return interruptedOutcome("");
|
|
109
|
+
const prompt = buildCurrentTurnPrompt(agent, task);
|
|
110
|
+
const timeoutMs = agentConfig.timeoutMs ?? this.defaultTimeoutMs;
|
|
111
|
+
const startingMessageCount = record.session.messages.length;
|
|
112
|
+
record.lastOutput = "";
|
|
113
|
+
const settlement = await this.runPrompt(record, prompt, signal, timeoutMs);
|
|
114
|
+
const final = latestAssistant(record.session.messages.slice(startingMessageCount));
|
|
115
|
+
const output = truncateUtf8(final.output || record.lastOutput, DEFAULT_MAX_OUTPUT_BYTES);
|
|
116
|
+
const truncated = output.truncated || agent.contextTruncated;
|
|
117
|
+
|
|
118
|
+
switch (settlement.kind) {
|
|
119
|
+
case "completed":
|
|
120
|
+
if (final.stopReason === "error") {
|
|
121
|
+
return {
|
|
122
|
+
output: output.text,
|
|
123
|
+
exitCode: 1,
|
|
124
|
+
truncated,
|
|
125
|
+
error: final.error || "In-process subagent returned an error",
|
|
126
|
+
policy: inProcessPolicy(agentConfig),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (final.stopReason === "aborted") {
|
|
130
|
+
return {
|
|
131
|
+
...interruptedOutcome(output.text),
|
|
132
|
+
truncated,
|
|
133
|
+
policy: inProcessPolicy(agentConfig),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
output: output.text,
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
truncated,
|
|
140
|
+
policy: inProcessPolicy(agentConfig),
|
|
141
|
+
};
|
|
142
|
+
case "failed":
|
|
143
|
+
return {
|
|
144
|
+
output: output.text,
|
|
145
|
+
exitCode: 1,
|
|
146
|
+
truncated,
|
|
147
|
+
error: errorMessage(settlement.error),
|
|
148
|
+
policy: inProcessPolicy(agentConfig),
|
|
149
|
+
};
|
|
150
|
+
case "timeout":
|
|
151
|
+
return {
|
|
152
|
+
output: output.text,
|
|
153
|
+
exitCode: 124,
|
|
154
|
+
truncated,
|
|
155
|
+
error: `In-process subagent timed out after ${timeoutMs}ms`,
|
|
156
|
+
policy: inProcessPolicy(agentConfig),
|
|
157
|
+
};
|
|
158
|
+
case "aborted":
|
|
159
|
+
return {
|
|
160
|
+
...interruptedOutcome(output.text),
|
|
161
|
+
truncated,
|
|
162
|
+
policy: inProcessPolicy(agentConfig),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async release(agent: ManagedAgent): Promise<void> {
|
|
168
|
+
await this.releaseById(agent.id);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async shutdown(): Promise<void> {
|
|
172
|
+
const agentIds = [...this.sessions.keys()];
|
|
173
|
+
const results = await Promise.allSettled(agentIds.map((agentId) => this.releaseById(agentId)));
|
|
174
|
+
const failures = results.flatMap((result) =>
|
|
175
|
+
result.status === "rejected" ? [result.reason] : [],
|
|
176
|
+
);
|
|
177
|
+
if (failures.length > 0) {
|
|
178
|
+
throw new AggregateError(
|
|
179
|
+
failures,
|
|
180
|
+
`Failed to dispose ${failures.length} in-process subagent session(s)`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async releaseById(agentId: string): Promise<void> {
|
|
186
|
+
const record = this.sessions.get(agentId);
|
|
187
|
+
if (!record) return;
|
|
188
|
+
this.sessions.delete(agentId);
|
|
189
|
+
if (record.disposed) return;
|
|
190
|
+
record.disposed = true;
|
|
191
|
+
const failures: unknown[] = [];
|
|
192
|
+
try {
|
|
193
|
+
record.unsubscribe();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
failures.push(error);
|
|
196
|
+
}
|
|
197
|
+
if (record.session.messages.length > 0) {
|
|
198
|
+
await settleWithin(record.session.abort(), this.abortGraceMs);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
record.session.dispose();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
failures.push(error);
|
|
204
|
+
}
|
|
205
|
+
if (failures.length > 0) {
|
|
206
|
+
throw new AggregateError(
|
|
207
|
+
failures,
|
|
208
|
+
`Failed to release in-process subagent ${agentId}: ${failures.map(errorMessage).join("; ")}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async getOrCreate(
|
|
214
|
+
agent: ManagedAgent,
|
|
215
|
+
agentConfig: AgentConfig,
|
|
216
|
+
tools: string[] | undefined,
|
|
217
|
+
): Promise<ChildSessionRecord> {
|
|
218
|
+
const existing = this.sessions.get(agent.id);
|
|
219
|
+
if (existing) return existing;
|
|
220
|
+
const session = await this.createSession({
|
|
221
|
+
agent,
|
|
222
|
+
agentConfig,
|
|
223
|
+
context: agent.context,
|
|
224
|
+
history: agent.history.map((turn) => ({ ...turn })),
|
|
225
|
+
modelRegistry: this.options.modelRegistry,
|
|
226
|
+
parentRuntime: this.options.getParentRuntime(),
|
|
227
|
+
tools,
|
|
228
|
+
});
|
|
229
|
+
const record: ChildSessionRecord = {
|
|
230
|
+
session,
|
|
231
|
+
lastOutput: "",
|
|
232
|
+
disposed: false,
|
|
233
|
+
unsubscribe: () => undefined,
|
|
234
|
+
};
|
|
235
|
+
try {
|
|
236
|
+
record.unsubscribe = session.subscribe((event) => {
|
|
237
|
+
const message = eventMessage(event);
|
|
238
|
+
if (!message) return;
|
|
239
|
+
const output = assistantText(message);
|
|
240
|
+
if (output) record.lastOutput = truncateUtf8(output, DEFAULT_MAX_OUTPUT_BYTES).text;
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
record.disposed = true;
|
|
244
|
+
try {
|
|
245
|
+
session.dispose();
|
|
246
|
+
} catch {
|
|
247
|
+
// Preserve the subscription failure, which explains why creation was rejected.
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
this.sessions.set(agent.id, record);
|
|
252
|
+
return record;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async runPrompt(
|
|
256
|
+
record: ChildSessionRecord,
|
|
257
|
+
prompt: string,
|
|
258
|
+
signal: AbortSignal,
|
|
259
|
+
timeoutMs: number,
|
|
260
|
+
): Promise<PromptSettlement> {
|
|
261
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
262
|
+
let abortHandler: (() => void) | undefined;
|
|
263
|
+
const promptSettlement: Promise<PromptSettlement> = record.session
|
|
264
|
+
.prompt(prompt)
|
|
265
|
+
.then(() => ({ kind: "completed" as const }))
|
|
266
|
+
.catch((error: unknown) => ({ kind: "failed" as const, error }));
|
|
267
|
+
const timeoutSettlement = new Promise<PromptSettlement>((resolve) => {
|
|
268
|
+
timeout = setTimeout(() => resolve({ kind: "timeout" }), timeoutMs);
|
|
269
|
+
});
|
|
270
|
+
const abortSettlement = new Promise<PromptSettlement>((resolve) => {
|
|
271
|
+
abortHandler = () => resolve({ kind: "aborted" });
|
|
272
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
273
|
+
});
|
|
274
|
+
const settlement = await Promise.race([promptSettlement, timeoutSettlement, abortSettlement]);
|
|
275
|
+
if (timeout) clearTimeout(timeout);
|
|
276
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
277
|
+
if (settlement.kind === "timeout" || settlement.kind === "aborted") {
|
|
278
|
+
await settleWithin(record.session.abort(), this.abortGraceMs);
|
|
279
|
+
const settledAfterAbort = await completesWithin(promptSettlement, this.abortGraceMs);
|
|
280
|
+
if (!settledAfterAbort) this.discardRecord(record);
|
|
281
|
+
}
|
|
282
|
+
return settlement;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private discardRecord(record: ChildSessionRecord): void {
|
|
286
|
+
for (const [agentId, candidate] of this.sessions) {
|
|
287
|
+
if (candidate === record) this.sessions.delete(agentId);
|
|
288
|
+
}
|
|
289
|
+
if (record.disposed) return;
|
|
290
|
+
record.disposed = true;
|
|
291
|
+
record.unsubscribe();
|
|
292
|
+
record.session.dispose();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function validateInProcessTools(tools: string[] | undefined): string[] | undefined {
|
|
297
|
+
if (tools === undefined) return undefined;
|
|
298
|
+
const unique = [...new Set(tools)];
|
|
299
|
+
const unsupported = unique.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool));
|
|
300
|
+
if (unsupported.length > 0) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`In-process subagents cannot load extension/custom tools: ${unsupported.join(", ")}. Use stateful.transport "subprocess" for this agent.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return unique;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function createSdkChildSession(
|
|
309
|
+
options: ChildSessionCreateOptions,
|
|
310
|
+
): Promise<ChildSession> {
|
|
311
|
+
const agentDir = getAgentDir();
|
|
312
|
+
const { loader: resourceLoader, settingsManager } = await createInProcessResourceLoader(
|
|
313
|
+
options.agent.cwd,
|
|
314
|
+
agentDir,
|
|
315
|
+
options.agentConfig.systemPrompt,
|
|
316
|
+
options.agent.agentScope === "project" || options.agent.agentScope === "both",
|
|
317
|
+
);
|
|
318
|
+
const resolved = await resolveChildModel(options);
|
|
319
|
+
const sessionManager = SessionManager.inMemory(options.agent.cwd);
|
|
320
|
+
seedChildSessionManager(sessionManager, options, resolved.model);
|
|
321
|
+
const created = await createAgentSession({
|
|
322
|
+
cwd: options.agent.cwd,
|
|
323
|
+
agentDir,
|
|
324
|
+
model: resolved.model,
|
|
325
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
326
|
+
modelRegistry: options.modelRegistry,
|
|
327
|
+
resourceLoader,
|
|
328
|
+
settingsManager,
|
|
329
|
+
sessionManager,
|
|
330
|
+
tools: options.tools,
|
|
331
|
+
noTools: options.tools?.length === 0 ? "all" : undefined,
|
|
332
|
+
});
|
|
333
|
+
const session = created.session;
|
|
334
|
+
if (options.tools !== undefined) {
|
|
335
|
+
const active = session.getActiveToolNames();
|
|
336
|
+
const expected = [...options.tools].sort();
|
|
337
|
+
if (
|
|
338
|
+
active.length !== expected.length ||
|
|
339
|
+
[...active].sort().some((name, index) => name !== expected[index])
|
|
340
|
+
) {
|
|
341
|
+
session.dispose();
|
|
342
|
+
throw new Error(
|
|
343
|
+
`In-process child activated an unexpected tool set (${active.join(", ") || "none"}); use stateful.transport "subprocess".`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
get sessionId() {
|
|
349
|
+
return session.sessionId;
|
|
350
|
+
},
|
|
351
|
+
get messages() {
|
|
352
|
+
return session.messages;
|
|
353
|
+
},
|
|
354
|
+
prompt: (text) => session.prompt(text),
|
|
355
|
+
subscribe: (listener) => session.subscribe((event) => listener(event)),
|
|
356
|
+
abort: () => session.abort(),
|
|
357
|
+
dispose: () => session.dispose(),
|
|
358
|
+
getActiveToolNames: () => session.getActiveToolNames(),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function resolveChildModel(options: ChildSessionCreateOptions): Promise<{
|
|
363
|
+
model: Model<Api>;
|
|
364
|
+
thinkingLevel: SubagentThinkingLevel;
|
|
365
|
+
}> {
|
|
366
|
+
let model = options.parentRuntime.model;
|
|
367
|
+
let modelThinkingLevel: SubagentThinkingLevel | undefined;
|
|
368
|
+
if (options.agentConfig.model) {
|
|
369
|
+
const parsed = parseModelRequest(options.agentConfig.model);
|
|
370
|
+
model = resolveConfiguredModel(parsed.model, options.modelRegistry);
|
|
371
|
+
modelThinkingLevel = parsed.thinkingLevel;
|
|
372
|
+
}
|
|
373
|
+
if (!model) model = options.modelRegistry.getAvailable()[0];
|
|
374
|
+
if (!model)
|
|
375
|
+
throw new Error("No model with configured authentication is available for in-process subagent");
|
|
376
|
+
return {
|
|
377
|
+
model,
|
|
378
|
+
thinkingLevel:
|
|
379
|
+
options.agentConfig.thinkingLevel ??
|
|
380
|
+
modelThinkingLevel ??
|
|
381
|
+
options.parentRuntime.thinkingLevel,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function parseModelRequest(value: string): {
|
|
386
|
+
model: string;
|
|
387
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
388
|
+
} {
|
|
389
|
+
const requested = value.trim();
|
|
390
|
+
const separator = requested.lastIndexOf(":");
|
|
391
|
+
if (separator > 0) {
|
|
392
|
+
const suffix = requested.slice(separator + 1);
|
|
393
|
+
if (
|
|
394
|
+
suffix === "off" ||
|
|
395
|
+
suffix === "minimal" ||
|
|
396
|
+
suffix === "low" ||
|
|
397
|
+
suffix === "medium" ||
|
|
398
|
+
suffix === "high" ||
|
|
399
|
+
suffix === "xhigh"
|
|
400
|
+
) {
|
|
401
|
+
return { model: requested.slice(0, separator), thinkingLevel: suffix };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { model: requested };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function resolveConfiguredModel(value: string, modelRegistry: ModelRegistry): Model<Api> {
|
|
408
|
+
const requested = value.trim();
|
|
409
|
+
if (!requested) throw new Error("In-process subagent model cannot be empty");
|
|
410
|
+
const slash = requested.indexOf("/");
|
|
411
|
+
if (slash > 0) {
|
|
412
|
+
const exact = modelRegistry.find(requested.slice(0, slash), requested.slice(slash + 1));
|
|
413
|
+
if (exact) return exact;
|
|
414
|
+
}
|
|
415
|
+
const lowered = requested.toLowerCase();
|
|
416
|
+
const exactMatches = modelRegistry
|
|
417
|
+
.getAll()
|
|
418
|
+
.filter((model) => model.id.toLowerCase() === lowered || model.name.toLowerCase() === lowered);
|
|
419
|
+
if (exactMatches.length === 1) return exactMatches[0];
|
|
420
|
+
const partialMatches = modelRegistry
|
|
421
|
+
.getAll()
|
|
422
|
+
.filter(
|
|
423
|
+
(model) =>
|
|
424
|
+
model.id.toLowerCase().includes(lowered) || model.name.toLowerCase().includes(lowered),
|
|
425
|
+
);
|
|
426
|
+
if (partialMatches.length === 1) return partialMatches[0];
|
|
427
|
+
const displayedMatches = partialMatches
|
|
428
|
+
.slice(0, 8)
|
|
429
|
+
.map((model) => `${model.provider}/${model.id}`);
|
|
430
|
+
const remaining = partialMatches.length - displayedMatches.length;
|
|
431
|
+
const suffix =
|
|
432
|
+
displayedMatches.length > 0
|
|
433
|
+
? `; matches: ${displayedMatches.join(", ")}${remaining > 0 ? `, and ${remaining} more` : ""}`
|
|
434
|
+
: "";
|
|
435
|
+
throw new Error(`Unable to resolve in-process subagent model ${requested}${suffix}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function createInProcessResourceLoader(
|
|
439
|
+
cwd: string,
|
|
440
|
+
agentDir: string,
|
|
441
|
+
agentSystemPrompt: string,
|
|
442
|
+
projectTrusted = false,
|
|
443
|
+
): Promise<{ loader: DefaultResourceLoader; settingsManager: SettingsManager }> {
|
|
444
|
+
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
445
|
+
settingsManager.setProjectTrusted(projectTrusted);
|
|
446
|
+
const loader = new DefaultResourceLoader({
|
|
447
|
+
cwd,
|
|
448
|
+
agentDir,
|
|
449
|
+
settingsManager,
|
|
450
|
+
noExtensions: true,
|
|
451
|
+
appendSystemPrompt: agentSystemPrompt.trim() ? [agentSystemPrompt] : [],
|
|
452
|
+
});
|
|
453
|
+
await loader.reload();
|
|
454
|
+
return { loader, settingsManager };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function seedChildSessionManager(
|
|
458
|
+
sessionManager: SessionManager,
|
|
459
|
+
options: ChildSessionCreateOptions,
|
|
460
|
+
model: Model<Api>,
|
|
461
|
+
): void {
|
|
462
|
+
let timestamp = Date.now() - (options.history.length * 2 + 1);
|
|
463
|
+
if (options.context?.trim()) {
|
|
464
|
+
sessionManager.appendMessage({
|
|
465
|
+
role: "user",
|
|
466
|
+
content: `Parent context:\n${redactPrivateText(options.context)}`,
|
|
467
|
+
timestamp: timestamp++,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
for (const turn of options.history) {
|
|
471
|
+
sessionManager.appendMessage({
|
|
472
|
+
role: "user",
|
|
473
|
+
content: redactPrivateText(turn.task),
|
|
474
|
+
timestamp: timestamp++,
|
|
475
|
+
});
|
|
476
|
+
sessionManager.appendMessage({
|
|
477
|
+
role: "assistant",
|
|
478
|
+
content: [{ type: "text", text: redactPrivateText(turn.output) }],
|
|
479
|
+
api: model.api,
|
|
480
|
+
provider: model.provider,
|
|
481
|
+
model: model.id,
|
|
482
|
+
usage: {
|
|
483
|
+
input: 0,
|
|
484
|
+
output: 0,
|
|
485
|
+
cacheRead: 0,
|
|
486
|
+
cacheWrite: 0,
|
|
487
|
+
totalTokens: 0,
|
|
488
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
489
|
+
},
|
|
490
|
+
stopReason: turn.exitCode === 0 ? "stop" : "error",
|
|
491
|
+
timestamp: timestamp++,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function buildCurrentTurnPrompt(agent: ManagedAgent, task: string): string {
|
|
497
|
+
const ids = new Set(agent.currentMailboxMessageIds ?? []);
|
|
498
|
+
const messages = agent.mailbox
|
|
499
|
+
.filter((message) => ids.has(message.id))
|
|
500
|
+
.slice(-20)
|
|
501
|
+
.map((message) => `From ${message.senderId}: ${redactPrivateText(message.content)}`)
|
|
502
|
+
.join("\n");
|
|
503
|
+
return truncateUtf8(
|
|
504
|
+
messages
|
|
505
|
+
? `${redactPrivateText(task)}\n\nMailbox messages:\n${messages}`
|
|
506
|
+
: redactPrivateText(task),
|
|
507
|
+
DEFAULT_MAX_CONTEXT_BYTES,
|
|
508
|
+
).text;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function latestAssistant(messages: readonly unknown[]): {
|
|
512
|
+
output: string;
|
|
513
|
+
stopReason?: string;
|
|
514
|
+
error?: string;
|
|
515
|
+
} {
|
|
516
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
517
|
+
const message = messages[index];
|
|
518
|
+
if (!message || typeof message !== "object") continue;
|
|
519
|
+
const candidate = message as { role?: string; stopReason?: string; errorMessage?: string };
|
|
520
|
+
if (candidate.role !== "assistant") continue;
|
|
521
|
+
return {
|
|
522
|
+
output: assistantText(message),
|
|
523
|
+
stopReason: candidate.stopReason,
|
|
524
|
+
error: candidate.errorMessage,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return { output: "" };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function eventMessage(event: unknown): unknown {
|
|
531
|
+
if (!event || typeof event !== "object") return undefined;
|
|
532
|
+
const candidate = event as { type?: string; message?: unknown };
|
|
533
|
+
return candidate.type === "message_update" || candidate.type === "message_end"
|
|
534
|
+
? candidate.message
|
|
535
|
+
: undefined;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function assistantText(message: unknown): string {
|
|
539
|
+
if (!message || typeof message !== "object") return "";
|
|
540
|
+
const content = (message as { content?: unknown }).content;
|
|
541
|
+
if (typeof content === "string") return content;
|
|
542
|
+
if (!Array.isArray(content)) return "";
|
|
543
|
+
return content
|
|
544
|
+
.filter((part): part is { type: "text"; text: string } =>
|
|
545
|
+
Boolean(
|
|
546
|
+
part &&
|
|
547
|
+
typeof part === "object" &&
|
|
548
|
+
(part as { type?: unknown }).type === "text" &&
|
|
549
|
+
typeof (part as { text?: unknown }).text === "string",
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
.map((part) => part.text)
|
|
553
|
+
.join("\n");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function interruptedOutcome(output: string): TurnOutcome {
|
|
557
|
+
return { output, exitCode: 130, aborted: true, error: "In-process subagent was aborted" };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function inProcessPolicy(agent: AgentConfig): NonNullable<TurnOutcome["policy"]> {
|
|
561
|
+
return {
|
|
562
|
+
inherited: ["modelRegistry", "authentication", "cwdResources"],
|
|
563
|
+
overridden: [
|
|
564
|
+
...(agent.model ? ["model"] : []),
|
|
565
|
+
...(agent.thinkingLevel ? ["thinkingLevel"] : []),
|
|
566
|
+
...(agent.tools ? ["tools"] : []),
|
|
567
|
+
],
|
|
568
|
+
unsupported: ["approvalPolicy", "sandboxProfile", "providerHeaders", "extensionState"],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function settleWithin(promise: Promise<unknown>, timeoutMs: number): Promise<void> {
|
|
573
|
+
await completesWithin(
|
|
574
|
+
promise.catch(() => undefined),
|
|
575
|
+
timeoutMs,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function completesWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {
|
|
580
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
581
|
+
try {
|
|
582
|
+
return await Promise.race([
|
|
583
|
+
promise.then(() => true),
|
|
584
|
+
new Promise<boolean>((resolve) => {
|
|
585
|
+
timeout = setTimeout(() => resolve(false), timeoutMs);
|
|
586
|
+
}),
|
|
587
|
+
]);
|
|
588
|
+
} finally {
|
|
589
|
+
if (timeout) clearTimeout(timeout);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function errorMessage(error: unknown): string {
|
|
594
|
+
return truncateUtf8(
|
|
595
|
+
error instanceof Error ? error.message : String(error),
|
|
596
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
597
|
+
).text;
|
|
598
|
+
}
|
package/src/limits.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024;
|
|
2
|
+
export const DEFAULT_MAX_STDERR_BYTES = 16 * 1024;
|
|
3
|
+
export const DEFAULT_MAX_CONTEXT_BYTES = 50 * 1024;
|
|
4
|
+
export const DEFAULT_MAX_MESSAGES = 200;
|
|
5
|
+
export const TRUNCATION_MARKER = "\n… [truncated by pi-subagents]";
|
|
6
|
+
export const TAIL_TRUNCATION_MARKER = "… [truncated by pi-subagents]\n";
|
|
7
|
+
|
|
8
|
+
export interface BoundedText {
|
|
9
|
+
text: string;
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
originalBytes: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeByteLimit(maxBytes: number): number {
|
|
15
|
+
if (maxBytes === Number.POSITIVE_INFINITY) return Number.MAX_SAFE_INTEGER;
|
|
16
|
+
if (!Number.isFinite(maxBytes)) return 0;
|
|
17
|
+
return Math.max(0, Math.floor(maxBytes));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function truncateUtf8(text: string, maxBytes: number): BoundedText {
|
|
21
|
+
const limit = normalizeByteLimit(maxBytes);
|
|
22
|
+
const originalBytes = Buffer.byteLength(text, "utf8");
|
|
23
|
+
if (originalBytes <= limit) return { text, truncated: false, originalBytes };
|
|
24
|
+
if (limit === 0) return { text: "", truncated: true, originalBytes };
|
|
25
|
+
|
|
26
|
+
const marker = Buffer.from(TRUNCATION_MARKER, "utf8");
|
|
27
|
+
if (marker.length >= limit) {
|
|
28
|
+
return {
|
|
29
|
+
text: Buffer.from(text, "utf8").subarray(0, limit).toString("utf8").replace(/�+$/g, ""),
|
|
30
|
+
truncated: true,
|
|
31
|
+
originalBytes,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const prefix = Buffer.from(text, "utf8").subarray(0, limit - marker.length).toString("utf8").replace(/�+$/g, "");
|
|
35
|
+
return { text: `${prefix}${TRUNCATION_MARKER}`, truncated: true, originalBytes };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function truncateUtf8Tail(text: string, maxBytes: number): BoundedText {
|
|
39
|
+
const limit = normalizeByteLimit(maxBytes);
|
|
40
|
+
const bytes = Buffer.from(text, "utf8");
|
|
41
|
+
const originalBytes = bytes.length;
|
|
42
|
+
if (originalBytes <= limit) return { text, truncated: false, originalBytes };
|
|
43
|
+
if (limit === 0) return { text: "", truncated: true, originalBytes };
|
|
44
|
+
|
|
45
|
+
const marker = Buffer.from(TAIL_TRUNCATION_MARKER, "utf8");
|
|
46
|
+
if (marker.length >= limit) {
|
|
47
|
+
return {
|
|
48
|
+
text: bytes.subarray(originalBytes - limit).toString("utf8").replace(/^�+/g, ""),
|
|
49
|
+
truncated: true,
|
|
50
|
+
originalBytes,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const suffix = bytes
|
|
54
|
+
.subarray(originalBytes - (limit - marker.length))
|
|
55
|
+
.toString("utf8")
|
|
56
|
+
.replace(/^�+/g, "");
|
|
57
|
+
return { text: `${TAIL_TRUNCATION_MARKER}${suffix}`, truncated: true, originalBytes };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function appendBounded(current: string, addition: string, maxBytes: number): BoundedText {
|
|
61
|
+
return truncateUtf8(current + addition, maxBytes);
|
|
62
|
+
}
|