@narumitw/pi-subagents 0.54.0 → 1.0.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 +22 -13
- package/package.json +1 -1
- package/src/automation-registration.ts +137 -0
- package/src/automation-tool.ts +40 -0
- package/src/automation.ts +6 -156
- package/src/cached-module-loader.ts +18 -0
- package/src/completion-delivery.ts +80 -8
- package/src/config-registration.ts +89 -0
- package/src/config-ui.ts +2 -2
- package/src/consult-registration.ts +132 -0
- package/src/consult-tool.ts +95 -0
- package/src/consult.ts +37 -204
- package/src/create-stateful-transport.ts +106 -18
- package/src/inspect-registration.ts +64 -0
- package/src/inspect-tool.ts +43 -0
- package/src/inspect.ts +11 -69
- package/src/params.ts +1 -1
- package/src/persistence.ts +105 -6
- package/src/pi-args.ts +41 -0
- package/src/registry-types.ts +21 -4
- package/src/registry.ts +204 -36
- package/src/render.ts +2 -6
- package/src/runner-outcome.ts +31 -0
- package/src/runner.ts +18 -79
- package/src/stateful-guidance.ts +1 -1
- package/src/stateful-render.ts +0 -1
- package/src/stateful-tool-params.ts +13 -20
- package/src/stateful.ts +24 -20
- package/src/subagents.ts +81 -28
- package/src/verified-execution-contract.ts +3 -31
- package/src/verified-execution-schema.ts +32 -0
|
@@ -12,6 +12,9 @@ const COMPLETION_BATCH_DELAY_MS = 10;
|
|
|
12
12
|
|
|
13
13
|
interface CompletionMetadata {
|
|
14
14
|
protocol: typeof PI_SUBAGENTS_RPC_PROTOCOL;
|
|
15
|
+
completionId: string;
|
|
16
|
+
runId: string;
|
|
17
|
+
generation: number;
|
|
15
18
|
agentId: string;
|
|
16
19
|
agent: string;
|
|
17
20
|
state: string;
|
|
@@ -38,13 +41,15 @@ type CompletionPi = Pick<ExtensionAPI, "sendMessage">;
|
|
|
38
41
|
|
|
39
42
|
export interface CompletionDeliveryBrokerOptions {
|
|
40
43
|
onDeliveryError?: (error: unknown) => void;
|
|
41
|
-
|
|
44
|
+
onAcknowledged?: (completions: readonly AgentTurnCompletion[], acknowledgedAt: number) => void;
|
|
42
45
|
now?: () => number;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/** Owns bounded completion batching and at most one idle-root wake for one parent session. */
|
|
46
49
|
export class CompletionDeliveryBroker {
|
|
47
50
|
private pending: AgentTurnCompletion[] = [];
|
|
51
|
+
private readonly knownCompletionIds = new Set<string>();
|
|
52
|
+
private awaitingParentAck: AgentTurnCompletion[] = [];
|
|
48
53
|
private flushTimer?: NodeJS.Timeout;
|
|
49
54
|
private wakeInFlight = false;
|
|
50
55
|
private closed = false;
|
|
@@ -57,7 +62,8 @@ export class CompletionDeliveryBroker {
|
|
|
57
62
|
) {}
|
|
58
63
|
|
|
59
64
|
enqueue(completion: AgentTurnCompletion): void {
|
|
60
|
-
if (this.closed) return;
|
|
65
|
+
if (this.closed || this.knownCompletionIds.has(completion.completionId)) return;
|
|
66
|
+
this.knownCompletionIds.add(completion.completionId);
|
|
61
67
|
this.pending.push(completion);
|
|
62
68
|
this.scheduleFlush();
|
|
63
69
|
}
|
|
@@ -72,6 +78,14 @@ export class CompletionDeliveryBroker {
|
|
|
72
78
|
this.scheduleFlush();
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
onParentContext(messages: readonly unknown[]): void {
|
|
82
|
+
this.acknowledgeVisible(completionIdsFromContext(messages));
|
|
83
|
+
if (this.awaitingParentAck.length > 0) {
|
|
84
|
+
this.pending = [...this.awaitingParentAck.splice(0), ...this.pending];
|
|
85
|
+
}
|
|
86
|
+
this.scheduleFlush();
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
onParentSettled(): void {
|
|
76
90
|
this.wakeInFlight = false;
|
|
77
91
|
this.scheduleFlush();
|
|
@@ -88,18 +102,21 @@ export class CompletionDeliveryBroker {
|
|
|
88
102
|
let canWake = this.shouldWakeRoot();
|
|
89
103
|
for (let index = 0; index < batches.length; index++) {
|
|
90
104
|
const triggerTurn = canWake && index === batches.length - 1;
|
|
91
|
-
const
|
|
105
|
+
const batch = batches[index];
|
|
106
|
+
const message = buildCompletionMessage(batch);
|
|
92
107
|
if (triggerTurn) this.wakeInFlight = true;
|
|
108
|
+
this.awaitingParentAck.push(...batch);
|
|
93
109
|
try {
|
|
94
110
|
this.pi.sendMessage(message, { deliverAs: "steer", triggerTurn });
|
|
95
|
-
this.notifyDelivered(batches[index]);
|
|
96
111
|
} catch (primaryError) {
|
|
112
|
+
this.removeAwaiting(batch);
|
|
97
113
|
if (triggerTurn) this.wakeInFlight = false;
|
|
98
114
|
canWake = false;
|
|
115
|
+
this.awaitingParentAck.push(...batch);
|
|
99
116
|
try {
|
|
100
117
|
this.pi.sendMessage(message, { deliverAs: "nextTurn", triggerTurn: false });
|
|
101
|
-
this.notifyDelivered(batches[index]);
|
|
102
118
|
} catch (fallbackError) {
|
|
119
|
+
this.removeAwaiting(batch);
|
|
103
120
|
this.pending = [...batches.slice(index).flat(), ...this.pending];
|
|
104
121
|
try {
|
|
105
122
|
this.options.onDeliveryError?.(
|
|
@@ -122,6 +139,8 @@ export class CompletionDeliveryBroker {
|
|
|
122
139
|
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
123
140
|
this.flushTimer = undefined;
|
|
124
141
|
this.pending = [];
|
|
142
|
+
this.awaitingParentAck = [];
|
|
143
|
+
this.knownCompletionIds.clear();
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
private scheduleFlush(): void {
|
|
@@ -132,14 +151,34 @@ export class CompletionDeliveryBroker {
|
|
|
132
151
|
}, COMPLETION_BATCH_DELAY_MS);
|
|
133
152
|
}
|
|
134
153
|
|
|
135
|
-
private
|
|
154
|
+
private acknowledgeVisible(visibleIds: ReadonlySet<string>): void {
|
|
155
|
+
if (visibleIds.size === 0) return;
|
|
156
|
+
const completions = [...this.awaitingParentAck, ...this.pending].filter((completion) =>
|
|
157
|
+
visibleIds.has(completion.completionId),
|
|
158
|
+
);
|
|
159
|
+
if (completions.length === 0) return;
|
|
160
|
+
const acknowledgedIds = new Set(completions.map((completion) => completion.completionId));
|
|
161
|
+
this.awaitingParentAck = this.awaitingParentAck.filter(
|
|
162
|
+
(completion) => !acknowledgedIds.has(completion.completionId),
|
|
163
|
+
);
|
|
164
|
+
this.pending = this.pending.filter(
|
|
165
|
+
(completion) => !acknowledgedIds.has(completion.completionId),
|
|
166
|
+
);
|
|
167
|
+
for (const completion of completions) this.knownCompletionIds.delete(completion.completionId);
|
|
136
168
|
try {
|
|
137
|
-
this.options.
|
|
169
|
+
this.options.onAcknowledged?.(completions, (this.options.now ?? Date.now)());
|
|
138
170
|
} catch {
|
|
139
|
-
//
|
|
171
|
+
// Context assembly already observed the message, so observer failures cannot retract it.
|
|
140
172
|
}
|
|
141
173
|
}
|
|
142
174
|
|
|
175
|
+
private removeAwaiting(completions: readonly AgentTurnCompletion[]): void {
|
|
176
|
+
const removed = new Set(completions.map((completion) => completion.completionId));
|
|
177
|
+
this.awaitingParentAck = this.awaitingParentAck.filter(
|
|
178
|
+
(completion) => !removed.has(completion.completionId),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
143
182
|
private isRootIdle(): boolean {
|
|
144
183
|
try {
|
|
145
184
|
return this.ctx.isIdle();
|
|
@@ -158,6 +197,33 @@ export class CompletionDeliveryBroker {
|
|
|
158
197
|
}
|
|
159
198
|
}
|
|
160
199
|
|
|
200
|
+
function completionIdsFromContext(messages: readonly unknown[]): Set<string> {
|
|
201
|
+
const ids = new Set<string>();
|
|
202
|
+
for (const message of messages) {
|
|
203
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) continue;
|
|
204
|
+
const record = message as Record<string, unknown>;
|
|
205
|
+
if (record.role !== "custom" || record.customType !== "pi-subagent-completion") continue;
|
|
206
|
+
const details = record.details;
|
|
207
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) continue;
|
|
208
|
+
const metadata = details as Record<string, unknown>;
|
|
209
|
+
if (
|
|
210
|
+
metadata.protocol === PI_SUBAGENTS_RPC_PROTOCOL &&
|
|
211
|
+
typeof metadata.completionId === "string"
|
|
212
|
+
) {
|
|
213
|
+
ids.add(metadata.completionId);
|
|
214
|
+
}
|
|
215
|
+
if (!Array.isArray(metadata.completions)) continue;
|
|
216
|
+
for (const completion of metadata.completions) {
|
|
217
|
+
if (!completion || typeof completion !== "object" || Array.isArray(completion)) continue;
|
|
218
|
+
const item = completion as Record<string, unknown>;
|
|
219
|
+
if (item.protocol === PI_SUBAGENTS_RPC_PROTOCOL && typeof item.completionId === "string") {
|
|
220
|
+
ids.add(item.completionId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return ids;
|
|
225
|
+
}
|
|
226
|
+
|
|
161
227
|
function chunkCompletions(completions: AgentTurnCompletion[]): AgentTurnCompletion[][] {
|
|
162
228
|
const batches: AgentTurnCompletion[][] = [];
|
|
163
229
|
for (let index = 0; index < completions.length; index += MAX_COMPLETIONS_PER_MESSAGE) {
|
|
@@ -203,6 +269,9 @@ function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionM
|
|
|
203
269
|
function completionMetadata(completion: AgentTurnCompletion): CompletionMetadata {
|
|
204
270
|
return {
|
|
205
271
|
protocol: PI_SUBAGENTS_RPC_PROTOCOL,
|
|
272
|
+
completionId: completion.completionId,
|
|
273
|
+
runId: completion.runId,
|
|
274
|
+
generation: completion.generation,
|
|
206
275
|
agentId: completion.agent.id,
|
|
207
276
|
agent: completion.agent.agent,
|
|
208
277
|
state: completion.agent.state,
|
|
@@ -233,6 +302,9 @@ export function buildDetachedCompletionMessage(completion: AgentTurnCompletion):
|
|
|
233
302
|
[
|
|
234
303
|
"Message Type: SUBAGENT_COMPLETION",
|
|
235
304
|
`Protocol: ${PI_SUBAGENTS_RPC_PROTOCOL}`,
|
|
305
|
+
`Completion ID: ${completion.completionId}`,
|
|
306
|
+
`Run ID: ${completion.runId}`,
|
|
307
|
+
`Generation: ${completion.generation}`,
|
|
236
308
|
`Agent ID: ${completion.agent.id}`,
|
|
237
309
|
`Agent: ${agentName}`,
|
|
238
310
|
`Task: ${task}`,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { cachedModuleLoader } from "./cached-module-loader.js";
|
|
3
|
+
import { showSubagentHelp, showSubagentStatus } from "./config-status.js";
|
|
4
|
+
import type { SubagentMenuOwner, SubagentSettingsRuntime } from "./config-ui.js";
|
|
5
|
+
|
|
6
|
+
const SUBCOMMANDS = [
|
|
7
|
+
{ value: "settings", label: "settings", description: "Configure subagent user settings" },
|
|
8
|
+
{ value: "status", label: "status", description: "Show effective subagent settings" },
|
|
9
|
+
{ value: "help", label: "help", description: "Show subagent settings help" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
type ConfigUiModule = Pick<
|
|
13
|
+
typeof import("./config-ui.js"),
|
|
14
|
+
"showSubagentManager" | "showSubagentSettings"
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export interface ConfigRegistrationDependencies {
|
|
18
|
+
loadConfigUi?: () => Promise<ConfigUiModule>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerSubagentConfigLifecycle(pi: ExtensionAPI): SubagentMenuOwner {
|
|
22
|
+
const owner: SubagentMenuOwner = { generation: 0, controller: new AbortController() };
|
|
23
|
+
pi.on("session_start", () => {
|
|
24
|
+
owner.generation += 1;
|
|
25
|
+
owner.controller.abort(new DOMException("Subagent session replaced", "AbortError"));
|
|
26
|
+
owner.controller = new AbortController();
|
|
27
|
+
});
|
|
28
|
+
pi.on("session_shutdown", () => {
|
|
29
|
+
owner.generation += 1;
|
|
30
|
+
owner.controller.abort(new DOMException("Subagent session shut down", "AbortError"));
|
|
31
|
+
});
|
|
32
|
+
return owner;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerSubagentConfigCommand(
|
|
36
|
+
pi: ExtensionAPI,
|
|
37
|
+
runtime: SubagentSettingsRuntime,
|
|
38
|
+
owner = registerSubagentConfigLifecycle(pi),
|
|
39
|
+
dependencies: ConfigRegistrationDependencies = {},
|
|
40
|
+
): void {
|
|
41
|
+
const loadConfigUi = cachedModuleLoader(
|
|
42
|
+
dependencies.loadConfigUi ?? (() => import("./config-ui.js")),
|
|
43
|
+
);
|
|
44
|
+
pi.registerCommand("subagents", {
|
|
45
|
+
description: "Manage current-session subagents and user settings",
|
|
46
|
+
getArgumentCompletions(prefix: string) {
|
|
47
|
+
const normalized = prefix.trim().toLowerCase();
|
|
48
|
+
const matches = SUBCOMMANDS.filter((item) => item.value.startsWith(normalized));
|
|
49
|
+
return matches.length > 0 ? matches : null;
|
|
50
|
+
},
|
|
51
|
+
async handler(args, ctx) {
|
|
52
|
+
const subcommand = args.trim().toLowerCase();
|
|
53
|
+
if (!subcommand && ctx.mode !== "tui") {
|
|
54
|
+
showSubagentStatus(ctx, runtime);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (subcommand === "status") {
|
|
58
|
+
showSubagentStatus(ctx, runtime);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (subcommand === "help") {
|
|
62
|
+
showSubagentHelp(ctx, runtime);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!subcommand || subcommand === "settings") {
|
|
66
|
+
const generation = owner.generation;
|
|
67
|
+
const controller = owner.controller;
|
|
68
|
+
const isCurrent = () =>
|
|
69
|
+
generation === owner.generation &&
|
|
70
|
+
controller === owner.controller &&
|
|
71
|
+
!controller.signal.aborted;
|
|
72
|
+
let configUi: ConfigUiModule;
|
|
73
|
+
try {
|
|
74
|
+
configUi = await loadConfigUi();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!isCurrent()) return;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (!isCurrent()) return;
|
|
80
|
+
if (!subcommand) await configUi.showSubagentManager(pi, ctx, runtime, owner);
|
|
81
|
+
else await configUi.showSubagentSettings(ctx, runtime, owner);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (ctx.mode === "tui" || ctx.hasUI) {
|
|
85
|
+
ctx.ui.notify(`Unknown /subagents subcommand: ${subcommand}`, "warning");
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
package/src/config-ui.ts
CHANGED
|
@@ -168,7 +168,7 @@ function registerSubagentPrimaryCommand(
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
async function showSubagentManager(
|
|
171
|
+
export async function showSubagentManager(
|
|
172
172
|
pi: ExtensionAPI,
|
|
173
173
|
ctx: ExtensionCommandContext,
|
|
174
174
|
runtime: SubagentSettingsRuntime,
|
|
@@ -681,7 +681,7 @@ async function showSubagentManager(
|
|
|
681
681
|
});
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
-
async function showSubagentSettings(
|
|
684
|
+
export async function showSubagentSettings(
|
|
685
685
|
ctx: ExtensionCommandContext,
|
|
686
686
|
runtime: SubagentSettingsRuntime,
|
|
687
687
|
owner: SubagentMenuOwner,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
|
|
3
|
+
import type { RegisterSubagentConsultOptions } from "./consult.js";
|
|
4
|
+
import { renderConsultCall, renderConsultResult } from "./consult-render.js";
|
|
5
|
+
import { type ConsultDetails, SubagentConsultParams } from "./consult-tool.js";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_CONSULT_RESOURCE_POLICY,
|
|
8
|
+
DEFAULT_CONSULTATION_CWD_POLICY,
|
|
9
|
+
} from "./settings/inspection.js";
|
|
10
|
+
|
|
11
|
+
interface ConsultExecutionModule {
|
|
12
|
+
executeSubagentConsult: typeof import("./consult.js").executeSubagentConsult;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ConsultRegistrationDependencies {
|
|
16
|
+
loadExecution?: () => Promise<ConsultExecutionModule>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerSubagentConsult(
|
|
20
|
+
pi: ExtensionAPI,
|
|
21
|
+
options: RegisterSubagentConsultOptions,
|
|
22
|
+
dependencies: ConsultRegistrationDependencies = {},
|
|
23
|
+
): (catalog: string) => void {
|
|
24
|
+
const loadExecution = cachedModuleLoader(
|
|
25
|
+
dependencies.loadExecution ?? (() => import("./consult.js")),
|
|
26
|
+
);
|
|
27
|
+
let generation = 0;
|
|
28
|
+
const active = new Set<AbortController>();
|
|
29
|
+
const activeWork = new Set<Promise<unknown>>();
|
|
30
|
+
const cancelActive = (reason: string) => {
|
|
31
|
+
generation++;
|
|
32
|
+
for (const controller of active) {
|
|
33
|
+
controller.abort(new DOMException(reason, "AbortError"));
|
|
34
|
+
}
|
|
35
|
+
active.clear();
|
|
36
|
+
};
|
|
37
|
+
const cancelAndWaitForWork = async (reason: string) => {
|
|
38
|
+
cancelActive(reason);
|
|
39
|
+
await Promise.allSettled([...activeWork]);
|
|
40
|
+
};
|
|
41
|
+
pi.on("session_start", () => cancelAndWaitForWork("Subagent consultation session replaced"));
|
|
42
|
+
pi.on("session_shutdown", () => cancelAndWaitForWork("Subagent consultation session shut down"));
|
|
43
|
+
|
|
44
|
+
const baseDescription = () =>
|
|
45
|
+
`Run one ephemeral subagent synchronously under enforced read-only tool and resource policies and return its answer. The child can use only the effective subset of Pi's built-in read, grep, find, and ls tools. Shell commands, file writes, extension tools, detached lifecycle operations, and persistent agent state are disabled. Working-directory target policy: ${options.getSettings()?.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY}; configured trusted-target resources: ${options.getSettings()?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY}; allowed targets without effective trust inherit no target/project resources. This is not a filesystem sandbox.`;
|
|
46
|
+
const definition: ToolDefinition<typeof SubagentConsultParams, ConsultDetails> = {
|
|
47
|
+
name: "subagent_consult",
|
|
48
|
+
label: "Consult Read-only Subagent",
|
|
49
|
+
description: baseDescription(),
|
|
50
|
+
promptSnippet: "Consult one constrained read-only subagent and wait for its answer",
|
|
51
|
+
promptGuidelines: [
|
|
52
|
+
"Use subagent_consult for bounded reconnaissance, planning, or review whose result is required in the current turn.",
|
|
53
|
+
"Set subagent_consult timeoutMs to the shortest realistic work deadline for the task difficulty; split oversized consultations instead of extending the deadline merely to compensate for broad scope.",
|
|
54
|
+
"Implementation-shaped tasks remain read-only and can return only analysis or instructions.",
|
|
55
|
+
],
|
|
56
|
+
parameters: SubagentConsultParams,
|
|
57
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
58
|
+
const ownerGeneration = generation;
|
|
59
|
+
const ownedController = new AbortController();
|
|
60
|
+
active.add(ownedController);
|
|
61
|
+
const combined = combineAbortSignals(signal, ownedController.signal);
|
|
62
|
+
const work = (async () => {
|
|
63
|
+
throwIfAborted(combined.signal, "Subagent consultation loading was cancelled");
|
|
64
|
+
let executionModule: ConsultExecutionModule;
|
|
65
|
+
try {
|
|
66
|
+
executionModule = await loadExecution();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throwIfAborted(combined.signal, "Subagent consultation loading was cancelled");
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
throwIfAborted(combined.signal, "Subagent consultation loading was cancelled");
|
|
72
|
+
if (ownerGeneration !== generation) {
|
|
73
|
+
throw new DOMException("Subagent consultation owner was replaced", "AbortError");
|
|
74
|
+
}
|
|
75
|
+
return executionModule.executeSubagentConsult(
|
|
76
|
+
params,
|
|
77
|
+
combined.signal,
|
|
78
|
+
onUpdate,
|
|
79
|
+
ctx,
|
|
80
|
+
options,
|
|
81
|
+
() => ownerGeneration === generation,
|
|
82
|
+
);
|
|
83
|
+
})();
|
|
84
|
+
activeWork.add(work);
|
|
85
|
+
try {
|
|
86
|
+
return await work;
|
|
87
|
+
} finally {
|
|
88
|
+
combined.dispose();
|
|
89
|
+
active.delete(ownedController);
|
|
90
|
+
activeWork.delete(work);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
renderCall(args, theme) {
|
|
94
|
+
return renderConsultCall(args, theme);
|
|
95
|
+
},
|
|
96
|
+
renderResult(result, renderOptions, theme, context) {
|
|
97
|
+
return renderConsultResult(result, renderOptions, theme, context);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
pi.registerTool<typeof SubagentConsultParams, ConsultDetails>(definition);
|
|
101
|
+
pi.on("tool_result", (event) => {
|
|
102
|
+
if (event.toolName !== "subagent_consult") return;
|
|
103
|
+
if ((event.details as ConsultDetails | undefined)?.isError) return { isError: true };
|
|
104
|
+
});
|
|
105
|
+
return (catalog: string) => {
|
|
106
|
+
definition.description = catalog ? `${baseDescription()}\n\n${catalog}` : baseDescription();
|
|
107
|
+
pi.registerTool<typeof SubagentConsultParams, ConsultDetails>(definition);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function combineAbortSignals(
|
|
112
|
+
external: AbortSignal | undefined,
|
|
113
|
+
owned: AbortSignal,
|
|
114
|
+
): { signal: AbortSignal; dispose(): void } {
|
|
115
|
+
if (!external) return { signal: owned, dispose() {} };
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const sources = [external, owned];
|
|
118
|
+
const listeners = sources.map((source) => {
|
|
119
|
+
const listener = () => {
|
|
120
|
+
if (!controller.signal.aborted) controller.abort(source.reason);
|
|
121
|
+
};
|
|
122
|
+
if (source.aborted) listener();
|
|
123
|
+
else source.addEventListener("abort", listener, { once: true });
|
|
124
|
+
return { source, listener };
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
dispose() {
|
|
129
|
+
for (const { source, listener } of listeners) source.removeEventListener("abort", listener);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import type { AgentScope, ConsultResourcePolicy } from "./agents/types.js";
|
|
4
|
+
import { THINKING_LEVELS } from "./agents/types.js";
|
|
5
|
+
import { DEFAULT_MAX_CONTEXT_BYTES, MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
|
|
6
|
+
|
|
7
|
+
const ConsultScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
8
|
+
default: "user",
|
|
9
|
+
description: "Agent definition scope. Project scopes require a trusted project.",
|
|
10
|
+
});
|
|
11
|
+
const ConsultThinkingSchema = StringEnum(THINKING_LEVELS);
|
|
12
|
+
|
|
13
|
+
export const SubagentConsultParams = Type.Object(
|
|
14
|
+
{
|
|
15
|
+
agent: Type.String({ minLength: 1 }),
|
|
16
|
+
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
17
|
+
agentScope: Type.Optional(ConsultScopeSchema),
|
|
18
|
+
confirmProjectAgents: Type.Optional(Type.Boolean({ default: true })),
|
|
19
|
+
cwd: Type.Optional(Type.String({ minLength: 1 })),
|
|
20
|
+
timeoutMs: Type.Optional(
|
|
21
|
+
Type.Number({
|
|
22
|
+
minimum: 1,
|
|
23
|
+
maximum: MAX_SUBAGENT_TIMEOUT_MS,
|
|
24
|
+
description:
|
|
25
|
+
"Work deadline selected for the consultation difficulty. On expiry, Pi aborts the work and makes one separately bounded summary attempt.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
thinkingLevel: Type.Optional(ConsultThinkingSchema),
|
|
29
|
+
},
|
|
30
|
+
{ additionalProperties: false },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export type SubagentConsultParams = Static<typeof SubagentConsultParams>;
|
|
34
|
+
|
|
35
|
+
export interface ConsultProgressActivity {
|
|
36
|
+
type: "text" | "toolCall";
|
|
37
|
+
text?: string;
|
|
38
|
+
name?: "read" | "grep" | "find" | "ls";
|
|
39
|
+
args?: Record<string, string | number | boolean>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ConsultProgress {
|
|
43
|
+
phase: "starting" | "running";
|
|
44
|
+
recentActivity: ConsultProgressActivity[];
|
|
45
|
+
recentActivityTotal: number;
|
|
46
|
+
actualProvider?: string;
|
|
47
|
+
actualModel?: string;
|
|
48
|
+
usage: {
|
|
49
|
+
input: number;
|
|
50
|
+
output: number;
|
|
51
|
+
cacheRead: number;
|
|
52
|
+
cacheWrite: number;
|
|
53
|
+
cost: number;
|
|
54
|
+
contextTokens: number;
|
|
55
|
+
turns: number;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ConsultDetails {
|
|
60
|
+
agent: string;
|
|
61
|
+
agentSource: string;
|
|
62
|
+
agentScope: AgentScope;
|
|
63
|
+
cwd: string;
|
|
64
|
+
model?: string;
|
|
65
|
+
thinkingLevel?: string;
|
|
66
|
+
timeoutMs: number;
|
|
67
|
+
policy: {
|
|
68
|
+
requestedTools: string[] | null;
|
|
69
|
+
effectiveTools: string[];
|
|
70
|
+
cwdBoundary: "current-workspace" | "external";
|
|
71
|
+
targetTrust: {
|
|
72
|
+
kind: string;
|
|
73
|
+
projectTrusted: boolean;
|
|
74
|
+
sourcePath?: string;
|
|
75
|
+
warning?: string;
|
|
76
|
+
};
|
|
77
|
+
requestedResources: ConsultResourcePolicy;
|
|
78
|
+
effectiveResources: {
|
|
79
|
+
policy: ConsultResourcePolicy;
|
|
80
|
+
projectResources: boolean;
|
|
81
|
+
contextFiles: boolean;
|
|
82
|
+
skills: boolean;
|
|
83
|
+
promptTemplates: boolean;
|
|
84
|
+
};
|
|
85
|
+
resourceDowngradeReason?: string;
|
|
86
|
+
extensions: "disabled";
|
|
87
|
+
sessionPersistence: "disabled";
|
|
88
|
+
retainedAgent: false;
|
|
89
|
+
};
|
|
90
|
+
child?: Record<string, unknown>;
|
|
91
|
+
progress?: ConsultProgress;
|
|
92
|
+
cancelled?: boolean;
|
|
93
|
+
isError?: boolean;
|
|
94
|
+
truncated?: boolean;
|
|
95
|
+
}
|