feat(pi): add subagent tracer bullet
This commit is contained in:
123
modules/agents/pi/extensions/subagents/index.ts
Normal file
123
modules/agents/pi/extensions/subagents/index.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
import { SubprocessRpcRunner } from "./runner.ts";
|
||||||
|
import { Supervisor } from "./supervisor.ts";
|
||||||
|
import type { SpawnRequest } from "./types.ts";
|
||||||
|
|
||||||
|
let supervisor: Supervisor | undefined;
|
||||||
|
|
||||||
|
export default function subagents(pi: ExtensionAPI) {
|
||||||
|
const getSupervisor = (ctx: ExtensionContext): Supervisor => {
|
||||||
|
if (!supervisor) supervisor = new Supervisor(new SubprocessRpcRunner(), cwdOf(ctx));
|
||||||
|
return supervisor;
|
||||||
|
};
|
||||||
|
|
||||||
|
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" }),
|
||||||
|
context: Type.Optional(Type.Literal("independent")),
|
||||||
|
model: Type.Optional(Type.String({ description: "Optional model selector for status metadata" })),
|
||||||
|
thinking: Type.Optional(Type.String({ description: "Optional thinking level for status metadata" })),
|
||||||
|
}),
|
||||||
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
|
const accepted = getSupervisor(ctx).spawn(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({ prompt: 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.on("session_shutdown", async () => {
|
||||||
|
await supervisor?.shutdown();
|
||||||
|
supervisor = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
157
modules/agents/pi/extensions/subagents/runner.ts
Normal file
157
modules/agents/pi/extensions/subagents/runner.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
|
import type { ChildHandle, ChildRunner, RunnerEvents, SpawnRequest } from "./types.ts";
|
||||||
|
|
||||||
|
interface PendingResponse {
|
||||||
|
resolve(value: unknown): void;
|
||||||
|
reject(error: Error): void;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RpcLine {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
command?: string;
|
||||||
|
success?: boolean;
|
||||||
|
data?: unknown;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RpcChildHandle implements ChildHandle {
|
||||||
|
private buffer = "";
|
||||||
|
private nextRequest = 0;
|
||||||
|
private settled = false;
|
||||||
|
private readonly pending = new Map<string, PendingResponse>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly child: ChildProcessWithoutNullStreams,
|
||||||
|
private readonly events: RunnerEvents,
|
||||||
|
) {
|
||||||
|
child.stdout.setEncoding("utf8");
|
||||||
|
child.stderr.setEncoding("utf8");
|
||||||
|
child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
||||||
|
child.stderr.on("data", (chunk) => this.events.running(`stderr: ${String(chunk).trim().slice(0, 200)}`));
|
||||||
|
child.on("error", (error) => this.fail(error.message));
|
||||||
|
child.on("close", (code, signal) => {
|
||||||
|
for (const pending of this.pending.values()) {
|
||||||
|
pending.reject(new Error(`RPC process closed before ${pending.command} response`));
|
||||||
|
}
|
||||||
|
this.pending.clear();
|
||||||
|
if (!this.settled) this.fail(`RPC process closed with code ${code ?? "null"} signal ${signal ?? "null"}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async prompt(message: string): Promise<void> {
|
||||||
|
await this.send("prompt", { message });
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancel(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.send("abort", {});
|
||||||
|
} catch {}
|
||||||
|
this.child.stdin.end();
|
||||||
|
if (!this.child.killed) this.child.kill("SIGTERM");
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStdout(chunk: string) {
|
||||||
|
this.buffer += chunk;
|
||||||
|
while (true) {
|
||||||
|
const newline = this.buffer.indexOf("\n");
|
||||||
|
if (newline === -1) return;
|
||||||
|
const line = this.buffer.slice(0, newline).replace(/\r$/, "");
|
||||||
|
this.buffer = this.buffer.slice(newline + 1);
|
||||||
|
if (line.trim() === "") continue;
|
||||||
|
this.onLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLine(line: string) {
|
||||||
|
let payload: RpcLine;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
this.events.running(`non-json rpc output: ${line.slice(0, 200)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === "response" && payload.id) {
|
||||||
|
const pending = this.pending.get(payload.id);
|
||||||
|
if (!pending) return;
|
||||||
|
this.pending.delete(payload.id);
|
||||||
|
if (payload.success) pending.resolve(payload.data);
|
||||||
|
else pending.reject(new Error(payload.error ?? payload.message ?? `${pending.command} failed`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === "agent_started") {
|
||||||
|
this.events.running("agent_started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === "agent_settled") {
|
||||||
|
this.finish().catch((error) => this.fail(error instanceof Error ? error.message : String(error)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type) this.events.running(payload.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async finish() {
|
||||||
|
if (this.settled) return;
|
||||||
|
this.settled = true;
|
||||||
|
this.events.settling();
|
||||||
|
const result = await this.send("get_last_assistant_text", {});
|
||||||
|
const text = typeof result === "string" ? result : result && typeof result === "object" && "text" in result ? String((result as { text: unknown }).text) : "";
|
||||||
|
this.events.completed(text, "agent_settled");
|
||||||
|
this.child.stdin.end();
|
||||||
|
if (!this.child.killed) this.child.kill("SIGTERM");
|
||||||
|
}
|
||||||
|
|
||||||
|
private fail(error: string) {
|
||||||
|
if (this.settled) return;
|
||||||
|
this.settled = true;
|
||||||
|
this.events.failed(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(command: string, body: Record<string, unknown>): Promise<unknown> {
|
||||||
|
const id = `subagent-${++this.nextRequest}`;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.pending.set(id, { resolve, reject, command });
|
||||||
|
this.child.stdin.write(`${JSON.stringify({ id, type: command, ...body })}\n`, (error) => {
|
||||||
|
if (!error) return;
|
||||||
|
this.pending.delete(id);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubprocessRpcRunner implements ChildRunner {
|
||||||
|
async start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise<ChildHandle> {
|
||||||
|
const args = [process.argv[1], "--mode", "rpc", "--no-extensions", "--name", `subagent ${id}`];
|
||||||
|
const child = spawn(process.execPath, args, {
|
||||||
|
cwd,
|
||||||
|
env: childEnvironment(),
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
detached: process.platform !== "win32",
|
||||||
|
});
|
||||||
|
const handle = new RpcChildHandle(child, events);
|
||||||
|
events.accepted();
|
||||||
|
void handle.prompt(independentPrompt(request)).catch((error) => events.failed(error instanceof Error ? error.message : String(error)));
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function childEnvironment(): NodeJS.ProcessEnv {
|
||||||
|
const env = { ...process.env };
|
||||||
|
delete env.PI_SESSION_ID;
|
||||||
|
delete env.PI_SESSION_FILE;
|
||||||
|
delete env.PI_PROVIDER;
|
||||||
|
delete env.PI_MODEL;
|
||||||
|
delete env.PI_REASONING_LEVEL;
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function independentPrompt(request: SpawnRequest): string {
|
||||||
|
return `You are running as a delegated subagent in independent context.\nDo not assume access to the parent conversation transcript.\nReturn a concise final answer for the parent agent.\n\nTask:\n${request.prompt}`;
|
||||||
|
}
|
||||||
38
modules/agents/pi/extensions/subagents/status.ts
Normal file
38
modules/agents/pi/extensions/subagents/status.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { ChildRecord, SpawnAccepted, SubagentResult, SubagentStatus } from "./types.ts";
|
||||||
|
|
||||||
|
export function toAccepted(status: SubagentStatus): SpawnAccepted {
|
||||||
|
return {
|
||||||
|
id: status.id,
|
||||||
|
label: status.label,
|
||||||
|
context: status.context,
|
||||||
|
tools: status.tools,
|
||||||
|
state: status.state,
|
||||||
|
hint: `Use subagent_status or subagent_result with id ${status.id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneStatus(status: SubagentStatus): SubagentStatus {
|
||||||
|
return { ...status, elapsedMs: elapsedMs(status) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneResult(record: ChildRecord): SubagentResult {
|
||||||
|
const status = cloneStatus(record.status);
|
||||||
|
const terminal = ["completed", "failed", "cancelled", "timed_out", "orphaned"].includes(status.state);
|
||||||
|
return {
|
||||||
|
id: status.id,
|
||||||
|
state: status.state,
|
||||||
|
running: !terminal,
|
||||||
|
resultAvailable: status.resultAvailable,
|
||||||
|
result: record.result,
|
||||||
|
error: status.error,
|
||||||
|
completedAt: status.completedAt,
|
||||||
|
elapsedMs: status.elapsedMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function elapsedMs(status: Pick<SubagentStatus, "startedAt" | "completedAt">): number {
|
||||||
|
const start = Date.parse(status.startedAt);
|
||||||
|
const end = status.completedAt ? Date.parse(status.completedAt) : Date.now();
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
|
||||||
|
return Math.max(0, end - start);
|
||||||
|
}
|
||||||
158
modules/agents/pi/extensions/subagents/supervisor.ts
Normal file
158
modules/agents/pi/extensions/subagents/supervisor.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import type { ChildHandle, ChildRecord, ChildRunner, ContextMode, RunnerEvents, SpawnAccepted, SpawnRequest, SubagentResult, SubagentStatus } from "./types.ts";
|
||||||
|
import { cloneResult, cloneStatus, toAccepted } from "./status.ts";
|
||||||
|
|
||||||
|
interface RunningChild {
|
||||||
|
record: ChildRecord;
|
||||||
|
handle?: ChildHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Supervisor {
|
||||||
|
private nextChild = 0;
|
||||||
|
private readonly children = new Map<string, RunningChild>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly runner: ChildRunner,
|
||||||
|
private readonly cwd: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
spawn(request: SpawnRequest): SpawnAccepted {
|
||||||
|
const prompt = typeof request.prompt === "string" ? request.prompt.trim() : "";
|
||||||
|
if (!prompt) throw new Error("prompt is required");
|
||||||
|
|
||||||
|
const id = this.allocateId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const status: SubagentStatus = {
|
||||||
|
id,
|
||||||
|
label: request.agent ?? `ad-hoc ${id}`,
|
||||||
|
agent: request.agent,
|
||||||
|
adHoc: !request.agent,
|
||||||
|
context: this.resolveContext(request.context),
|
||||||
|
state: "queued",
|
||||||
|
cwd: this.cwd,
|
||||||
|
model: request.model,
|
||||||
|
thinking: request.thinking,
|
||||||
|
tools: "pi-default",
|
||||||
|
startedAt: now,
|
||||||
|
elapsedMs: 0,
|
||||||
|
lastEvent: "queued",
|
||||||
|
lastEventAt: now,
|
||||||
|
resultAvailable: false,
|
||||||
|
};
|
||||||
|
const child: RunningChild = { record: { status } };
|
||||||
|
this.children.set(id, child);
|
||||||
|
this.setState(child.record.status, "starting", "starting");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isTerminal(child.record.status.state)) return;
|
||||||
|
void this.runner
|
||||||
|
.start(id, { ...request, prompt, context: status.context, tools: status.tools }, this.cwd, this.eventsFor(child.record))
|
||||||
|
.then((handle) => {
|
||||||
|
child.handle = handle;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.fail(child.record, error instanceof Error ? error.message : String(error));
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return toAccepted(cloneStatus(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
list(): SubagentStatus[] {
|
||||||
|
return [...this.children.values()].map((child) => cloneStatus(child.record.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
status(id: string): SubagentStatus {
|
||||||
|
return cloneStatus(this.require(id).record.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
result(id: string): SubagentResult {
|
||||||
|
return cloneResult(this.require(id).record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancel(id: string): Promise<SubagentStatus> {
|
||||||
|
const child = this.require(id);
|
||||||
|
if (isTerminal(child.record.status.state)) return cloneStatus(child.record.status);
|
||||||
|
await child.handle?.cancel();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
child.record.status.state = "cancelled";
|
||||||
|
child.record.status.completedAt = now;
|
||||||
|
child.record.status.lastEvent = "cancelled";
|
||||||
|
child.record.status.lastEventAt = now;
|
||||||
|
child.record.status.stopReason = "cancelled";
|
||||||
|
return cloneStatus(child.record.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown(): Promise<void> {
|
||||||
|
await Promise.allSettled(
|
||||||
|
[...this.children.values()].map(async (child) => {
|
||||||
|
if (!isTerminal(child.record.status.state)) await child.handle?.cancel();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private eventsFor(record: ChildRecord): RunnerEvents {
|
||||||
|
return {
|
||||||
|
accepted: (childSession) => {
|
||||||
|
if (childSession) record.status.childSession = childSession;
|
||||||
|
this.setState(record.status, "running", "prompt accepted");
|
||||||
|
},
|
||||||
|
running: (event) => {
|
||||||
|
if (!isTerminal(record.status.state)) this.setState(record.status, "running", event);
|
||||||
|
},
|
||||||
|
settling: () => {
|
||||||
|
if (!isTerminal(record.status.state)) this.setState(record.status, "settling", "agent_settled");
|
||||||
|
},
|
||||||
|
completed: (result, stopReason) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
record.result = result;
|
||||||
|
record.status.state = "completed";
|
||||||
|
record.status.completedAt = now;
|
||||||
|
record.status.lastEvent = "completed";
|
||||||
|
record.status.lastEventAt = now;
|
||||||
|
record.status.stopReason = stopReason;
|
||||||
|
record.status.resultAvailable = true;
|
||||||
|
},
|
||||||
|
failed: (error) => this.fail(record, error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private fail(record: ChildRecord, error: string) {
|
||||||
|
if (isTerminal(record.status.state)) return;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
record.status.state = "failed";
|
||||||
|
record.status.completedAt = now;
|
||||||
|
record.status.lastEvent = "failed";
|
||||||
|
record.status.lastEventAt = now;
|
||||||
|
record.status.error = error;
|
||||||
|
record.status.stopReason = "failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
private setState(status: SubagentStatus, state: SubagentStatus["state"], event: string) {
|
||||||
|
if (isTerminal(status.state)) return;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
status.state = state;
|
||||||
|
status.lastEvent = event;
|
||||||
|
status.lastEventAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private require(id: string): RunningChild {
|
||||||
|
const child = this.children.get(id);
|
||||||
|
if (!child) throw new Error(`unknown subagent id: ${id}`);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveContext(context: ContextMode | undefined): ContextMode {
|
||||||
|
if (context === undefined) return "independent";
|
||||||
|
if (context !== "independent") throw new Error("only independent context is implemented in this tracer bullet");
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private allocateId(): string {
|
||||||
|
this.nextChild += 1;
|
||||||
|
return `sg-${Date.now().toString(36)}-${this.nextChild.toString(36)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminal(state: SubagentStatus["state"]): boolean {
|
||||||
|
return ["completed", "failed", "cancelled", "timed_out", "orphaned"].includes(state);
|
||||||
|
}
|
||||||
84
modules/agents/pi/extensions/subagents/types.ts
Normal file
84
modules/agents/pi/extensions/subagents/types.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
export type ContextMode = "independent" | "fork";
|
||||||
|
|
||||||
|
export type SubagentState =
|
||||||
|
| "queued"
|
||||||
|
| "starting"
|
||||||
|
| "running"
|
||||||
|
| "settling"
|
||||||
|
| "completed"
|
||||||
|
| "failed"
|
||||||
|
| "cancelled"
|
||||||
|
| "timed_out"
|
||||||
|
| "orphaned";
|
||||||
|
|
||||||
|
export interface SpawnRequest {
|
||||||
|
prompt: string;
|
||||||
|
context?: ContextMode;
|
||||||
|
agent?: string;
|
||||||
|
model?: string;
|
||||||
|
thinking?: string;
|
||||||
|
tools?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpawnAccepted {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
context: ContextMode;
|
||||||
|
tools: string;
|
||||||
|
state: SubagentState;
|
||||||
|
hint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubagentStatus {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
agent?: string;
|
||||||
|
adHoc: boolean;
|
||||||
|
context: ContextMode;
|
||||||
|
state: SubagentState;
|
||||||
|
cwd: string;
|
||||||
|
model?: string;
|
||||||
|
thinking?: string;
|
||||||
|
tools: string;
|
||||||
|
startedAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
elapsedMs: number;
|
||||||
|
lastEvent?: string;
|
||||||
|
lastEventAt?: string;
|
||||||
|
stopReason?: string;
|
||||||
|
resultAvailable: boolean;
|
||||||
|
childSession?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubagentResult {
|
||||||
|
id: string;
|
||||||
|
state: SubagentState;
|
||||||
|
running: boolean;
|
||||||
|
resultAvailable: boolean;
|
||||||
|
result?: string;
|
||||||
|
error?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
elapsedMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildRecord {
|
||||||
|
status: SubagentStatus;
|
||||||
|
result?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunnerEvents {
|
||||||
|
accepted(childSession?: string): void;
|
||||||
|
running(event: string): void;
|
||||||
|
settling(): void;
|
||||||
|
completed(result: string, stopReason?: string): void;
|
||||||
|
failed(error: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildHandle {
|
||||||
|
cancel(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildRunner {
|
||||||
|
start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise<ChildHandle>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user