@narumitw/pi-subagents 3.3.2 → 3.4.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 +73 -24
- package/dist/child-communication-bridge.ts +3 -2
- package/dist/child-communication-bridge.ts.map +1 -1
- package/dist/child-readiness-probe.ts +114 -17
- package/dist/child-readiness-probe.ts.map +2 -2
- package/dist/chunks/{chunk-FY5HIU6Y.ts → chunk-5OXDVXXO.ts} +1 -1
- package/dist/chunks/{chunk-FY5HIU6Y.ts.map → chunk-5OXDVXXO.ts.map} +1 -1
- package/dist/chunks/chunk-B3RWNGDX.ts +192 -0
- package/dist/chunks/chunk-B3RWNGDX.ts.map +7 -0
- package/dist/chunks/{chunk-ZOAFWLDJ.ts → chunk-PRIDTAJL.ts} +4 -16
- package/dist/chunks/chunk-PRIDTAJL.ts.map +7 -0
- package/dist/chunks/{chunk-YMVSJ5GE.ts → chunk-YV2P6XJX.ts} +46 -17
- package/dist/chunks/chunk-YV2P6XJX.ts.map +7 -0
- package/dist/chunks/{menu-KAIUEWL7.ts → menu-2UH4ICLL.ts} +74 -3
- package/dist/chunks/menu-2UH4ICLL.ts.map +7 -0
- package/dist/index.ts +553 -292
- package/dist/index.ts.map +3 -3
- package/docs/attachments.md +9 -3
- package/docs/design-principles.md +3 -1
- package/docs/tools.md +15 -1
- package/package.json +6 -6
- package/src/broker-credentials.ts +44 -13
- package/src/child-communication-tools.ts +2 -19
- package/src/child-readiness-probe.ts +144 -18
- package/src/child-readiness.ts +118 -0
- package/src/menu.ts +82 -3
- package/src/message-broker.ts +23 -10
- package/src/process.ts +59 -167
- package/src/resource-attachments.ts +1 -3
- package/src/rpc-line-decoder.ts +61 -0
- package/src/runtime.ts +21 -9
- package/src/settings.ts +2 -2
- package/src/subagents.ts +110 -18
- package/src/tools.ts +83 -37
- package/src/types.ts +1 -0
- package/src/widget-renderer.ts +119 -0
- package/src/widget.ts +245 -121
- package/dist/chunks/chunk-3FCHXUKZ.ts +0 -98
- package/dist/chunks/chunk-3FCHXUKZ.ts.map +0 -7
- package/dist/chunks/chunk-YMVSJ5GE.ts.map +0 -7
- package/dist/chunks/chunk-ZOAFWLDJ.ts.map +0 -7
- package/dist/chunks/menu-KAIUEWL7.ts.map +0 -7
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const MAX_EVENT_LINE_BYTES = 256 * 1024;
|
|
2
|
+
|
|
3
|
+
/** Frame RPC output by LF bytes before strict UTF-8 decoding, so one bad line cannot lose later events. */
|
|
4
|
+
export class JsonLineDecoder {
|
|
5
|
+
private fragments: Buffer[] = [];
|
|
6
|
+
private bytes = 0;
|
|
7
|
+
private dropping = false;
|
|
8
|
+
private readonly decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly onValue: (value: unknown) => void,
|
|
12
|
+
private readonly onMalformed: () => void,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
push(chunk: Buffer | string): void {
|
|
16
|
+
const data = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
17
|
+
let offset = 0;
|
|
18
|
+
while (offset < data.length) {
|
|
19
|
+
const newline = data.indexOf(0x0a, offset);
|
|
20
|
+
const end = newline < 0 ? data.length : newline;
|
|
21
|
+
const fragment = data.subarray(offset, end);
|
|
22
|
+
if (!this.dropping) {
|
|
23
|
+
if (this.bytes + fragment.length > MAX_EVENT_LINE_BYTES) {
|
|
24
|
+
this.clear();
|
|
25
|
+
this.dropping = true;
|
|
26
|
+
this.onMalformed();
|
|
27
|
+
} else if (fragment.length > 0) {
|
|
28
|
+
// Copy only the bounded partial frame, not its possibly much larger source chunk.
|
|
29
|
+
this.fragments.push(Buffer.from(fragment));
|
|
30
|
+
this.bytes += fragment.length;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (newline < 0) return;
|
|
34
|
+
this.complete();
|
|
35
|
+
offset = newline + 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
finish(): void {
|
|
40
|
+
this.complete();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private complete(): void {
|
|
44
|
+
const dropping = this.dropping;
|
|
45
|
+
const line = Buffer.concat(this.fragments, this.bytes);
|
|
46
|
+
this.clear();
|
|
47
|
+
this.dropping = false;
|
|
48
|
+
if (dropping || line.length === 0) return;
|
|
49
|
+
try {
|
|
50
|
+
const text = this.decoder.decode(line);
|
|
51
|
+
if (text.trim()) this.onValue(JSON.parse(text));
|
|
52
|
+
} catch {
|
|
53
|
+
this.onMalformed();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private clear(): void {
|
|
58
|
+
this.fragments = [];
|
|
59
|
+
this.bytes = 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -29,7 +29,9 @@ interface InternalJob extends JobSummary {
|
|
|
29
29
|
resolveTerminal: () => void;
|
|
30
30
|
task?: Promise<void>;
|
|
31
31
|
taskText: string;
|
|
32
|
+
modelOverrides: SubagentModelOverrides;
|
|
32
33
|
thinkingLevel: SubagentThinkingLevel;
|
|
34
|
+
defaultThinkingLevel: SubagentThinkingLevel;
|
|
33
35
|
stopRequest?: StopRequest;
|
|
34
36
|
control?: ChildControl;
|
|
35
37
|
controlReady: Promise<ChildControl>;
|
|
@@ -39,7 +41,6 @@ interface InternalJob extends JobSummary {
|
|
|
39
41
|
result?: string;
|
|
40
42
|
error?: string;
|
|
41
43
|
limitations: string[];
|
|
42
|
-
deliverySent: boolean;
|
|
43
44
|
generation: number;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -48,11 +49,18 @@ export interface RuntimeDependencies {
|
|
|
48
49
|
now?: () => number;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
/** Resolved IDs that differ from the effective parent pair captured at spawn. */
|
|
53
|
+
export interface SubagentModelOverrides {
|
|
54
|
+
provider?: string;
|
|
55
|
+
model?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ActiveJobDisplay extends SubagentModelOverrides {
|
|
52
59
|
jobId: string;
|
|
53
60
|
task: string;
|
|
54
61
|
state: Extract<SubagentJobState, "queued" | "running">;
|
|
55
62
|
thinkingLevel: SubagentThinkingLevel;
|
|
63
|
+
defaultThinkingLevel: SubagentThinkingLevel;
|
|
56
64
|
elapsedMs: number;
|
|
57
65
|
timeout?: number;
|
|
58
66
|
tools: string[];
|
|
@@ -64,8 +72,11 @@ export interface StartJobInput {
|
|
|
64
72
|
skills: string[];
|
|
65
73
|
extensions: ExtensionAttachment[];
|
|
66
74
|
toolSources: Record<string, string[]>;
|
|
75
|
+
provider: string;
|
|
67
76
|
model: string;
|
|
77
|
+
modelOverrides?: SubagentModelOverrides;
|
|
68
78
|
thinkingLevel: SubagentThinkingLevel;
|
|
79
|
+
defaultThinkingLevel: SubagentThinkingLevel;
|
|
69
80
|
cwd: string;
|
|
70
81
|
timeout?: number;
|
|
71
82
|
projectTrusted: boolean;
|
|
@@ -77,7 +88,6 @@ export class SubagentRuntime {
|
|
|
77
88
|
private readonly now: () => number;
|
|
78
89
|
private counter = 0;
|
|
79
90
|
private generation = 0;
|
|
80
|
-
private deliveryEnabled = false;
|
|
81
91
|
private sessionActive = false;
|
|
82
92
|
private omittedJobs = 0;
|
|
83
93
|
private readonly jobListeners = new Set<() => void>();
|
|
@@ -96,7 +106,6 @@ export class SubagentRuntime {
|
|
|
96
106
|
this.generation++;
|
|
97
107
|
this.jobs.clear();
|
|
98
108
|
this.omittedJobs = 0;
|
|
99
|
-
this.deliveryEnabled = true;
|
|
100
109
|
this.sessionActive = true;
|
|
101
110
|
this.notifyJobsChanged();
|
|
102
111
|
}
|
|
@@ -119,7 +128,9 @@ export class SubagentRuntime {
|
|
|
119
128
|
jobId: job.jobId,
|
|
120
129
|
task: job.taskText,
|
|
121
130
|
state: job.state,
|
|
131
|
+
...job.modelOverrides,
|
|
122
132
|
thinkingLevel: job.thinkingLevel,
|
|
133
|
+
defaultThinkingLevel: job.defaultThinkingLevel,
|
|
123
134
|
elapsedMs: Math.max(0, now - (job.startedAt ?? job.createdAt)),
|
|
124
135
|
...(job.timeout !== undefined ? { timeout: job.timeout } : {}),
|
|
125
136
|
tools: [...job.tools],
|
|
@@ -136,6 +147,7 @@ export class SubagentRuntime {
|
|
|
136
147
|
if (active >= MAX_ACTIVE_JOBS) {
|
|
137
148
|
throw new Error(`Active subagent job limit reached (${MAX_ACTIVE_JOBS}).`);
|
|
138
149
|
}
|
|
150
|
+
const { provider, model } = input;
|
|
139
151
|
const jobId = `job_${this.now().toString(36)}_${(++this.counter).toString(36)}`;
|
|
140
152
|
const communication = this.broker.issueCredentials({
|
|
141
153
|
jobId,
|
|
@@ -162,7 +174,9 @@ export class SubagentRuntime {
|
|
|
162
174
|
controller,
|
|
163
175
|
tools: effectiveTools,
|
|
164
176
|
taskText: input.task,
|
|
177
|
+
modelOverrides: { ...input.modelOverrides },
|
|
165
178
|
thinkingLevel: input.thinkingLevel,
|
|
179
|
+
defaultThinkingLevel: input.defaultThinkingLevel,
|
|
166
180
|
terminal,
|
|
167
181
|
resolveTerminal,
|
|
168
182
|
controlReady,
|
|
@@ -170,7 +184,6 @@ export class SubagentRuntime {
|
|
|
170
184
|
rejectControl,
|
|
171
185
|
sendQueue: Promise.resolve(),
|
|
172
186
|
limitations: [],
|
|
173
|
-
deliverySent: false,
|
|
174
187
|
generation: this.generation,
|
|
175
188
|
};
|
|
176
189
|
this.jobs.set(jobId, job);
|
|
@@ -197,7 +210,8 @@ export class SubagentRuntime {
|
|
|
197
210
|
toolSources: Object.fromEntries(
|
|
198
211
|
Object.entries(input.toolSources).map(([tool, sources]) => [tool, [...sources]]),
|
|
199
212
|
),
|
|
200
|
-
|
|
213
|
+
provider,
|
|
214
|
+
model,
|
|
201
215
|
thinkingLevel: input.thinkingLevel,
|
|
202
216
|
cwd: input.cwd,
|
|
203
217
|
timeout: input.timeout,
|
|
@@ -355,7 +369,6 @@ export class SubagentRuntime {
|
|
|
355
369
|
|
|
356
370
|
async shutdown(): Promise<void> {
|
|
357
371
|
if (!this.sessionActive) return;
|
|
358
|
-
this.deliveryEnabled = false;
|
|
359
372
|
this.sessionActive = false;
|
|
360
373
|
const active = [...this.jobs.values()].filter((job) => !isTerminal(job.state));
|
|
361
374
|
await Promise.allSettled(
|
|
@@ -414,8 +427,7 @@ export class SubagentRuntime {
|
|
|
414
427
|
}
|
|
415
428
|
|
|
416
429
|
private deliver(job: InternalJob): void {
|
|
417
|
-
if (!this.
|
|
418
|
-
job.deliverySent = true;
|
|
430
|
+
if (!this.sessionActive || job.generation !== this.generation) return;
|
|
419
431
|
const payload = this.waitResult(job, false);
|
|
420
432
|
try {
|
|
421
433
|
this.pi.sendMessage(
|
package/src/settings.ts
CHANGED
|
@@ -11,8 +11,8 @@ export interface SubagentSettings {
|
|
|
11
11
|
|
|
12
12
|
export const DEFAULT_SUBAGENT_SETTINGS: Readonly<SubagentSettings> = Object.freeze({
|
|
13
13
|
showWidget: true,
|
|
14
|
-
showJobId:
|
|
15
|
-
showTools:
|
|
14
|
+
showJobId: false,
|
|
15
|
+
showTools: false,
|
|
16
16
|
showTiming: true,
|
|
17
17
|
});
|
|
18
18
|
|
package/src/subagents.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
subagentSettingsPath,
|
|
9
9
|
} from "./settings.js";
|
|
10
10
|
import { registerSubagentTools, type SubagentToolsDependencies } from "./tools.js";
|
|
11
|
-
import { createSubagentWidgetController } from "./widget.js";
|
|
11
|
+
import { createSubagentWidgetController, type SubagentCyclePhase } from "./widget.js";
|
|
12
12
|
|
|
13
13
|
export interface SubagentsDependencies extends SubagentToolsDependencies {
|
|
14
14
|
settingsStore?: Pick<SubagentSettingsStore, "userPath" | "load" | "save" | "flush">;
|
|
@@ -24,9 +24,20 @@ export default function subagents(pi: ExtensionAPI, dependencies: SubagentsDepen
|
|
|
24
24
|
let owner = new AbortController();
|
|
25
25
|
let ready = false;
|
|
26
26
|
let menuOpen = false;
|
|
27
|
+
let cyclePhase: SubagentCyclePhase = "idle";
|
|
27
28
|
let settings: SubagentSettings = { ...DEFAULT_SUBAGENT_SETTINGS };
|
|
28
29
|
const menuTasks = new Set<Promise<void>>();
|
|
29
|
-
const ownsSession = (ctx: ExtensionContext) =>
|
|
30
|
+
const ownsSession = (ctx: ExtensionContext) =>
|
|
31
|
+
activeContext !== undefined && ctx.sessionManager === activeContext.sessionManager;
|
|
32
|
+
|
|
33
|
+
const joinCleanup = async (steps: (() => void | Promise<void>)[], errors: unknown[] = []): Promise<void> => {
|
|
34
|
+
const results = await Promise.allSettled(steps.map(async (step) => step()));
|
|
35
|
+
for (const result of results) {
|
|
36
|
+
if (result.status === "rejected") errors.push(result.reason);
|
|
37
|
+
}
|
|
38
|
+
if (errors.length === 1) throw errors[0];
|
|
39
|
+
if (errors.length > 1) throw new AggregateError(errors, "Subagents session cleanup failed");
|
|
40
|
+
};
|
|
30
41
|
|
|
31
42
|
const openSettings = async (
|
|
32
43
|
ctx: ExtensionCommandContext,
|
|
@@ -79,7 +90,7 @@ export default function subagents(pi: ExtensionAPI, dependencies: SubagentsDepen
|
|
|
79
90
|
menuOpen = true;
|
|
80
91
|
const generation = sessionGeneration;
|
|
81
92
|
const signal = owner.signal;
|
|
82
|
-
const isCurrent = () => generation === sessionGeneration && ownsSession(ctx)
|
|
93
|
+
const isCurrent = () => generation === sessionGeneration && !signal.aborted && ownsSession(ctx);
|
|
83
94
|
const task = (async () => {
|
|
84
95
|
if (route === "settings") {
|
|
85
96
|
await openSettings(ctx, signal, isCurrent);
|
|
@@ -101,6 +112,7 @@ export default function subagents(pi: ExtensionAPI, dependencies: SubagentsDepen
|
|
|
101
112
|
await tools.runtime.cancel(jobId);
|
|
102
113
|
},
|
|
103
114
|
showSettings: (actionSignal) => openSettings(ctx, actionSignal, isCurrent),
|
|
115
|
+
widget: widget.controls(ctx),
|
|
104
116
|
});
|
|
105
117
|
})();
|
|
106
118
|
menuTasks.add(task);
|
|
@@ -118,24 +130,39 @@ export default function subagents(pi: ExtensionAPI, dependencies: SubagentsDepen
|
|
|
118
130
|
});
|
|
119
131
|
|
|
120
132
|
pi.on("session_start", async (_event, ctx) => {
|
|
133
|
+
const previousContext = activeContext;
|
|
134
|
+
const generation = ++sessionGeneration;
|
|
135
|
+
ready = false;
|
|
136
|
+
menuOpen = false;
|
|
121
137
|
owner.abort();
|
|
122
|
-
if (activeContext) widget.shutdown(activeContext);
|
|
123
138
|
owner = new AbortController();
|
|
139
|
+
const signal = owner.signal;
|
|
124
140
|
activeContext = ctx;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
cyclePhase = ctx.signal === undefined ? "idle" : "running";
|
|
142
|
+
const isCurrent = () => generation === sessionGeneration && !signal.aborted && ownsSession(ctx);
|
|
143
|
+
const errors: unknown[] = [];
|
|
144
|
+
try {
|
|
145
|
+
if (previousContext) widget.shutdown(previousContext);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
errors.push(error);
|
|
148
|
+
}
|
|
149
|
+
// A failed UI handoff must still stop old children, menus, and pending saves.
|
|
150
|
+
// Do not initialize another runtime or widget after that failure.
|
|
151
|
+
const failedHandoff = errors.length > 0;
|
|
152
|
+
await joinCleanup(
|
|
153
|
+
[
|
|
154
|
+
() => (failedHandoff ? tools.shutdown() : tools.startSession()),
|
|
155
|
+
...[...menuTasks].map((task) => () => task),
|
|
156
|
+
...(failedHandoff ? [() => store.flush()] : []),
|
|
157
|
+
],
|
|
158
|
+
errors,
|
|
159
|
+
);
|
|
133
160
|
if (!isCurrent()) return;
|
|
134
161
|
const loaded = await store.load();
|
|
135
162
|
if (!isCurrent()) return;
|
|
136
163
|
settings = { ...loaded.settings };
|
|
164
|
+
widget.start(ctx, settings, cyclePhase);
|
|
137
165
|
ready = true;
|
|
138
|
-
widget.start(ctx, settings);
|
|
139
166
|
if (loaded.diagnostics.length > 0) {
|
|
140
167
|
const message = sanitizeSubagentDisplayText(`Subagent settings: ${loaded.diagnostics.join(" ")}`);
|
|
141
168
|
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
@@ -143,16 +170,81 @@ export default function subagents(pi: ExtensionAPI, dependencies: SubagentsDepen
|
|
|
143
170
|
}
|
|
144
171
|
});
|
|
145
172
|
|
|
146
|
-
pi.on("
|
|
173
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
147
174
|
if (!ownsSession(ctx)) return;
|
|
175
|
+
cyclePhase = "preparing";
|
|
176
|
+
if (ready) widget.preparing(ctx);
|
|
177
|
+
});
|
|
178
|
+
pi.on("session_before_compact", (event, ctx) => {
|
|
179
|
+
if (!ownsSession(ctx)) return;
|
|
180
|
+
if (event.reason === "manual") {
|
|
181
|
+
cyclePhase = "maintenance";
|
|
182
|
+
if (ready) widget.maintenance(ctx);
|
|
183
|
+
} else {
|
|
184
|
+
if (cyclePhase !== "running") cyclePhase = "auto-preparing";
|
|
185
|
+
if (ready) widget.automaticCompaction(ctx);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
pi.on("session_before_tree", (_event, ctx) => {
|
|
189
|
+
if (!ownsSession(ctx)) return;
|
|
190
|
+
cyclePhase = "maintenance";
|
|
191
|
+
if (ready) widget.maintenance(ctx);
|
|
192
|
+
});
|
|
193
|
+
pi.on("input", (_event, ctx) => {
|
|
194
|
+
// Discard a canceled maintenance/preparation guard before a new idle prompt.
|
|
195
|
+
// Do not count input as a consumed user message.
|
|
196
|
+
if (!ownsSession(ctx) || !ctx.isIdle()) return;
|
|
197
|
+
cyclePhase = "idle";
|
|
198
|
+
if (ready) widget.clearCycle(ctx);
|
|
199
|
+
});
|
|
200
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
201
|
+
if (!ownsSession(ctx)) return;
|
|
202
|
+
cyclePhase = "running";
|
|
203
|
+
if (ready) widget.agentStarted(ctx);
|
|
204
|
+
});
|
|
205
|
+
pi.on("message_start", (event, ctx) => {
|
|
206
|
+
// Input submission is not consumption. Extension-generated user messages count too.
|
|
207
|
+
if (ready && ownsSession(ctx) && event.message.role === "user") widget.userMessage(ctx);
|
|
208
|
+
});
|
|
209
|
+
pi.on("turn_end", (event, ctx) => {
|
|
210
|
+
if (ready && ownsSession(ctx) && event.message.role === "assistant")
|
|
211
|
+
widget.turnEnded(ctx, event.message.stopReason);
|
|
212
|
+
});
|
|
213
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
214
|
+
if (!ownsSession(ctx)) return;
|
|
215
|
+
cyclePhase = "idle";
|
|
216
|
+
if (ready) widget.settled(ctx, event.aborted);
|
|
217
|
+
});
|
|
218
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
219
|
+
if (!ownsSession(ctx)) return;
|
|
220
|
+
cyclePhase = "maintenance";
|
|
221
|
+
if (!ready) return;
|
|
148
222
|
sessionGeneration++;
|
|
223
|
+
ready = false;
|
|
224
|
+
menuOpen = false;
|
|
149
225
|
owner.abort();
|
|
226
|
+
owner = new AbortController();
|
|
227
|
+
widget.start(ctx, settings, cyclePhase);
|
|
228
|
+
ready = true;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
232
|
+
if (!ownsSession(ctx)) return;
|
|
233
|
+
sessionGeneration++;
|
|
150
234
|
ready = false;
|
|
151
235
|
menuOpen = false;
|
|
152
236
|
activeContext = undefined;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
237
|
+
cyclePhase = "idle";
|
|
238
|
+
owner.abort();
|
|
239
|
+
const errors: unknown[] = [];
|
|
240
|
+
try {
|
|
241
|
+
widget.shutdown(ctx);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
errors.push(error);
|
|
244
|
+
}
|
|
245
|
+
await joinCleanup(
|
|
246
|
+
[() => tools.shutdown(), ...[...menuTasks].map((task) => () => task), () => store.flush()],
|
|
247
|
+
errors,
|
|
248
|
+
);
|
|
157
249
|
});
|
|
158
250
|
}
|
package/src/tools.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Static, Type } from "typebox";
|
|
4
|
-
import { assertChildBootstrapCapacity } from "./broker-credentials.js";
|
|
5
|
-
import { CHILD_COMMUNICATION_TOOL_NAMES } from "./child-communication-tools.js";
|
|
4
|
+
import { assertChildBootstrapCapacity, MAX_BOOTSTRAP_BYTES } from "./broker-credentials.js";
|
|
6
5
|
import {
|
|
7
6
|
type BrokerInboundMessage,
|
|
8
7
|
MAX_IDENTIFIER_LENGTH,
|
|
9
8
|
MAX_MESSAGE_BYTES,
|
|
10
9
|
MessageBroker,
|
|
10
|
+
optionalIdentifier,
|
|
11
|
+
requiredIdentifier,
|
|
11
12
|
sanitizeTerminalText,
|
|
12
13
|
validateMessage,
|
|
13
14
|
} from "./message-broker.js";
|
|
14
15
|
import { BROKER_MESSAGE_TYPE } from "./message-renderers.js";
|
|
15
16
|
import { modelVisibleJson, requireBoundedModelText } from "./model-output.js";
|
|
16
|
-
import { assertChildCommandCapacity } from "./process.js";
|
|
17
|
+
import { assertChildCommandCapacity, selectedChildTools } from "./process.js";
|
|
17
18
|
import {
|
|
18
19
|
MAX_ATTACHED_EXTENSIONS,
|
|
19
20
|
MAX_ATTACHED_SKILLS,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
MAX_SELECTED_TOOLS,
|
|
23
24
|
resolveResourceAttachments,
|
|
24
25
|
} from "./resource-attachments.js";
|
|
25
|
-
import { type RuntimeDependencies, SubagentRuntime } from "./runtime.js";
|
|
26
|
+
import { type RuntimeDependencies, type SubagentModelOverrides, SubagentRuntime } from "./runtime.js";
|
|
26
27
|
import { prepareTimeoutArguments, resolveTimeoutMs } from "./timeout.js";
|
|
27
28
|
import {
|
|
28
29
|
CHILD_CORE_TOOL_NAMES,
|
|
@@ -42,6 +43,21 @@ const SpawnParameters = Type.Object(
|
|
|
42
43
|
description: "Self-contained task, constraints, and expected result. Maximum 50 KiB.",
|
|
43
44
|
maxLength: MAX_TASK_BYTES,
|
|
44
45
|
}),
|
|
46
|
+
provider: Type.Optional(
|
|
47
|
+
Type.String({
|
|
48
|
+
description: "Exact child provider ID. Defaults to the main agent's effective provider at spawn time.",
|
|
49
|
+
minLength: 1,
|
|
50
|
+
maxLength: MAX_BOOTSTRAP_BYTES,
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
model: Type.Optional(
|
|
54
|
+
Type.String({
|
|
55
|
+
description:
|
|
56
|
+
"Exact model ID within the selected provider. Defaults independently to the main agent's effective model ID at spawn time. Unlisted models fail without fallback.",
|
|
57
|
+
minLength: 1,
|
|
58
|
+
maxLength: MAX_BOOTSTRAP_BYTES,
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
45
61
|
tools: Type.Optional(
|
|
46
62
|
Type.Array(
|
|
47
63
|
StringEnum(CHILD_CORE_TOOL_NAMES, {
|
|
@@ -209,23 +225,25 @@ export function registerSubagentTools(
|
|
|
209
225
|
if (ctx.cwd !== cwd || ctx.isProjectTrusted() !== projectTrusted) {
|
|
210
226
|
throw new Error("Subagent spawn context changed during attachment validation; retry the request.");
|
|
211
227
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const model = resolveChildModel(ctx, attachments.extensions.length > 0);
|
|
216
|
-
const thinkingLevel = resolveThinkingLevel(params.thinkingLevel ?? ctx.thinkingLevel ?? pi.getThinkingLevel());
|
|
228
|
+
const { provider, model, modelOverrides } = resolveChildModel(ctx, params, attachments.extensions.length > 0);
|
|
229
|
+
const defaultThinkingLevel = resolveThinkingLevel(ctx.thinkingLevel ?? pi.getThinkingLevel());
|
|
230
|
+
const thinkingLevel = resolveThinkingLevel(params.thinkingLevel ?? defaultThinkingLevel);
|
|
217
231
|
resolveTimeoutMs(params.timeout);
|
|
218
232
|
assertChildCommandCapacity(
|
|
219
233
|
{
|
|
220
234
|
tools,
|
|
221
235
|
skills: attachments.skills,
|
|
222
236
|
extensions: attachments.extensions,
|
|
223
|
-
model,
|
|
224
237
|
thinkingLevel,
|
|
225
238
|
projectTrusted,
|
|
226
239
|
},
|
|
227
240
|
dependencies.platform,
|
|
228
241
|
);
|
|
242
|
+
assertChildBootstrapCapacity(selectedChildTools({ tools, extensions: attachments.extensions }), {
|
|
243
|
+
provider,
|
|
244
|
+
model,
|
|
245
|
+
thinkingLevel,
|
|
246
|
+
});
|
|
229
247
|
throwIfAborted(signal, "Subagent spawn was cancelled");
|
|
230
248
|
return toolResult(
|
|
231
249
|
runtime.start({
|
|
@@ -234,8 +252,11 @@ export function registerSubagentTools(
|
|
|
234
252
|
skills: attachments.skills,
|
|
235
253
|
extensions: attachments.extensions,
|
|
236
254
|
toolSources: attachments.toolSources,
|
|
255
|
+
provider,
|
|
237
256
|
model,
|
|
257
|
+
modelOverrides,
|
|
238
258
|
thinkingLevel,
|
|
259
|
+
defaultThinkingLevel,
|
|
239
260
|
cwd,
|
|
240
261
|
timeout: params.timeout,
|
|
241
262
|
projectTrusted,
|
|
@@ -388,19 +409,62 @@ function resolveTools(value: unknown): string[] {
|
|
|
388
409
|
return tools;
|
|
389
410
|
}
|
|
390
411
|
|
|
391
|
-
function resolveChildModel(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
412
|
+
function resolveChildModel(
|
|
413
|
+
ctx: ExtensionContext,
|
|
414
|
+
overrides: Pick<SpawnArguments, "provider" | "model">,
|
|
415
|
+
hasAttachedExtensions: boolean,
|
|
416
|
+
): { provider: string; model: string; modelOverrides: SubagentModelOverrides } {
|
|
417
|
+
// Read the effective parent pair once, after asynchronous attachment preflight.
|
|
418
|
+
const parent = ctx.model;
|
|
419
|
+
if (!parent && (overrides.provider === undefined || overrides.model === undefined)) {
|
|
420
|
+
throw new Error("Subagent model is unavailable because no main-agent model is selected.");
|
|
421
|
+
}
|
|
422
|
+
const provider = validateModelIdentifier(
|
|
423
|
+
overrides.provider === undefined ? parent?.provider : overrides.provider,
|
|
424
|
+
"provider",
|
|
425
|
+
);
|
|
426
|
+
const model = validateModelIdentifier(overrides.model === undefined ? parent?.id : overrides.model, "model");
|
|
427
|
+
const displayProvider = sanitizeTerminalText(provider).slice(0, 128);
|
|
428
|
+
if (!hasAttachedExtensions && ctx.modelRegistry.getRegisteredProviderIds().includes(provider)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`Subagent model provider ${displayProvider} is unavailable because children disable parent extensions.`,
|
|
431
|
+
);
|
|
397
432
|
}
|
|
398
|
-
if (ctx.modelRegistry.getProviderAuthStatus(
|
|
433
|
+
if (ctx.modelRegistry.getProviderAuthStatus(provider).source === "runtime") {
|
|
399
434
|
throw new Error(
|
|
400
|
-
`Subagent model provider ${
|
|
435
|
+
`Subagent model provider ${displayProvider} uses a process-local runtime API key. Configure stored or environment credentials that child processes can read.`,
|
|
401
436
|
);
|
|
402
437
|
}
|
|
403
|
-
return
|
|
438
|
+
return {
|
|
439
|
+
provider,
|
|
440
|
+
model,
|
|
441
|
+
modelOverrides: {
|
|
442
|
+
...(provider !== parent?.provider ? { provider } : {}),
|
|
443
|
+
...(model !== parent?.id ? { model } : {}),
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function validateModelIdentifier(value: unknown, field: "provider" | "model"): string {
|
|
449
|
+
// Each ID must fit the shared bootstrap before character scans or sanitization.
|
|
450
|
+
if (
|
|
451
|
+
typeof value === "string" &&
|
|
452
|
+
(value.length > MAX_BOOTSTRAP_BYTES || Buffer.byteLength(value, "utf8") > MAX_BOOTSTRAP_BYTES)
|
|
453
|
+
) {
|
|
454
|
+
throw new Error(`Subagent ${field} exceeds the ${MAX_BOOTSTRAP_BYTES}-byte child bootstrap size limit.`);
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
typeof value !== "string" ||
|
|
458
|
+
!value.trim() ||
|
|
459
|
+
[...value].some((character) => {
|
|
460
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
461
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
462
|
+
}) ||
|
|
463
|
+
sanitizeTerminalText(value) !== value
|
|
464
|
+
) {
|
|
465
|
+
throw new Error(`Subagent ${field} must be a non-empty exact ID without control characters.`);
|
|
466
|
+
}
|
|
467
|
+
return value;
|
|
404
468
|
}
|
|
405
469
|
|
|
406
470
|
function resolveThinkingLevel(value: unknown): SubagentThinkingLevel {
|
|
@@ -430,29 +494,11 @@ function resolveMainSendArguments(params: SendArguments): MainSendSelection {
|
|
|
430
494
|
: { kind: "response", requestId: requestId ?? "", message: params.message };
|
|
431
495
|
}
|
|
432
496
|
|
|
433
|
-
function optionalIdentifier(value: unknown, field: string): string | undefined {
|
|
434
|
-
return value === undefined ? undefined : requiredIdentifier(value, field);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
497
|
function requiredString(value: unknown, field: string): string {
|
|
438
498
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Subagent ${field} is required.`);
|
|
439
499
|
return value.trim();
|
|
440
500
|
}
|
|
441
501
|
|
|
442
|
-
function requiredIdentifier(value: unknown, field: string): string {
|
|
443
|
-
const identifier = requiredString(value, field);
|
|
444
|
-
if (
|
|
445
|
-
identifier.length > MAX_IDENTIFIER_LENGTH ||
|
|
446
|
-
[...identifier].some((character) => {
|
|
447
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
448
|
-
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
449
|
-
})
|
|
450
|
-
) {
|
|
451
|
-
throw new Error(`Subagent ${field} is invalid.`);
|
|
452
|
-
}
|
|
453
|
-
return identifier;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
502
|
function assertNotNested(): void {
|
|
457
503
|
if ((Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) > 0) {
|
|
458
504
|
throw new Error("Nested subagents are not supported by pi-subagents.");
|