164 lines
6.0 KiB
TypeScript
164 lines
6.0 KiB
TypeScript
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
import { Type } from "typebox";
|
|
import { loadAgents } from "./agents.ts";
|
|
import { loadConfig, resolveSpawn, type Diagnostics } from "./config.ts";
|
|
import { SubprocessRpcRunner } from "./runner.ts";
|
|
import { Supervisor } from "./supervisor.ts";
|
|
import type { SpawnRequest } from "./types.ts";
|
|
|
|
let supervisor: Supervisor | undefined;
|
|
let lastDiagnostics: Diagnostics = { warnings: [] };
|
|
|
|
export default function subagents(pi: ExtensionAPI) {
|
|
const getSupervisor = (ctx: ExtensionContext): Supervisor => {
|
|
if (!supervisor) supervisor = new Supervisor(new SubprocessRpcRunner(), cwdOf(ctx));
|
|
return supervisor;
|
|
};
|
|
|
|
const resolve = (ctx: ExtensionContext, request: SpawnRequest): SpawnRequest => {
|
|
const diagnostics: Diagnostics = { warnings: [] };
|
|
const cwd = cwdOf(ctx);
|
|
const trusted = isProjectTrusted(ctx);
|
|
const config = loadConfig(cwd, trusted, diagnostics);
|
|
const agents = loadAgents(cwd, trusted, diagnostics);
|
|
lastDiagnostics = diagnostics;
|
|
return resolveSpawn(request, config, agents);
|
|
};
|
|
|
|
pi.registerTool({
|
|
name: "subagent_spawn",
|
|
label: "Spawn subagent",
|
|
description: "Start one ad hoc independent subagent and return immediately with its child id",
|
|
parameters: Type.Object({
|
|
prompt: Type.String({ description: "Prompt for the delegated subagent" }),
|
|
agent: Type.Optional(Type.String({ description: "Named agent definition to use" })),
|
|
context: Type.Optional(Type.Literal("independent")),
|
|
model: Type.Optional(Type.String({ description: "Optional model selector for the child" })),
|
|
thinking: Type.Optional(Type.String({ description: "Optional thinking level for the child" })),
|
|
tools: Type.Optional(Type.String({ description: "Tool profile name" })),
|
|
}),
|
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
const accepted = getSupervisor(ctx).spawn(resolve(ctx, params as SpawnRequest));
|
|
ctx.ui?.notify?.(`Started subagent ${accepted.id}`, "info");
|
|
return textResult(accepted);
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "subagent_list",
|
|
label: "List subagents",
|
|
description: "List active and recent subagents for this parent session",
|
|
parameters: Type.Object({}),
|
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
return textResult(getSupervisor(ctx).list());
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "subagent_status",
|
|
label: "Get subagent status",
|
|
description: "Get current lifecycle status for one subagent",
|
|
parameters: Type.Object({
|
|
id: Type.String({ description: "Subagent id returned by subagent_spawn" }),
|
|
}),
|
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
return textResult(getSupervisor(ctx).status(String((params as { id: unknown }).id)));
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "subagent_result",
|
|
label: "Get subagent result",
|
|
description: "Return still-running before completion and the final answer after completion",
|
|
parameters: Type.Object({
|
|
id: Type.String({ description: "Subagent id returned by subagent_spawn" }),
|
|
}),
|
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
return textResult(getSupervisor(ctx).result(String((params as { id: unknown }).id)));
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "subagent_cancel",
|
|
label: "Cancel subagent",
|
|
description: "Cancel a running subagent",
|
|
parameters: Type.Object({
|
|
id: Type.String({ description: "Subagent id returned by subagent_spawn" }),
|
|
}),
|
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
return textResult(await getSupervisor(ctx).cancel(String((params as { id: unknown }).id)));
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("subagent-spawn", {
|
|
description: "Start an ad hoc independent subagent",
|
|
handler: async (args, ctx) => {
|
|
const accepted = getSupervisor(ctx).spawn(resolve(ctx, parseSpawnArgs(args)));
|
|
ctx.ui.notify(`Started subagent ${accepted.id}`, "info");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("subagent-list", {
|
|
description: "Show subagent status records",
|
|
handler: async (_args, ctx) => {
|
|
ctx.ui.notify(JSON.stringify(getSupervisor(ctx).list(), null, 2), "info");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("subagent-status", {
|
|
description: "Show a subagent status by id",
|
|
handler: async (args, ctx) => {
|
|
ctx.ui.notify(JSON.stringify(getSupervisor(ctx).status(args.trim()), null, 2), "info");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("subagent-result", {
|
|
description: "Show a subagent result by id",
|
|
handler: async (args, ctx) => {
|
|
ctx.ui.notify(JSON.stringify(getSupervisor(ctx).result(args.trim()), null, 2), "info");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("subagent-diagnostics", {
|
|
description: "Show subagent configuration diagnostics from the last load",
|
|
handler: async (_args, ctx) => {
|
|
ctx.ui.notify(JSON.stringify(lastDiagnostics, null, 2), "info");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("subagent-cancel", {
|
|
description: "Cancel a running subagent by id",
|
|
handler: async (args, ctx) => {
|
|
ctx.ui.notify(JSON.stringify(await getSupervisor(ctx).cancel(args.trim()), null, 2), "info");
|
|
},
|
|
});
|
|
|
|
pi.on("session_shutdown", async () => {
|
|
await supervisor?.shutdown();
|
|
supervisor = undefined;
|
|
});
|
|
}
|
|
|
|
function parseSpawnArgs(args: string): SpawnRequest {
|
|
const match = /^--agent\s+(\S+)\s+([\s\S]+)$/u.exec(args.trim());
|
|
if (!match) return { prompt: args };
|
|
return { agent: match[1], prompt: match[2] };
|
|
}
|
|
|
|
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
const value = (ctx as unknown as { isProjectTrusted?: () => boolean }).isProjectTrusted?.();
|
|
return value === true;
|
|
}
|
|
|
|
function cwdOf(ctx: ExtensionContext): string {
|
|
const sessionCwd = (ctx as unknown as { sessionManager?: { getCwd?: () => string }; cwd?: string }).sessionManager?.getCwd?.();
|
|
return sessionCwd ?? (ctx as unknown as { cwd?: string }).cwd ?? process.cwd();
|
|
}
|
|
|
|
function textResult(value: unknown) {
|
|
return {
|
|
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
details: value,
|
|
};
|
|
}
|