feat(pi): label subagent work items
This commit is contained in:
@@ -115,9 +115,10 @@ test("named spawn resolves overrides, frontmatter, config, and defaults", () =>
|
|||||||
const config = loadConfig(cwd, true, diag, agentDir);
|
const config = loadConfig(cwd, true, diag, agentDir);
|
||||||
const agents = loadAgents(cwd, true, diag, agentDir);
|
const agents = loadAgents(cwd, true, diag, agentDir);
|
||||||
|
|
||||||
const resolved = resolveSpawn({ agent: "review", prompt: "check this", thinking: "low" }, config, agents);
|
const resolved = resolveSpawn({ agent: "review", prompt: "check this", label: "Review migration", thinking: "low" }, config, agents);
|
||||||
|
|
||||||
assert.equal(resolved.prompt, "check this");
|
assert.equal(resolved.prompt, "check this");
|
||||||
|
assert.equal(resolved.label, "Review migration");
|
||||||
assert.equal(resolved.context, "independent");
|
assert.equal(resolved.context, "independent");
|
||||||
assert.equal(resolved.model, "inherit");
|
assert.equal(resolved.model, "inherit");
|
||||||
assert.equal(resolved.thinking, "low");
|
assert.equal(resolved.thinking, "low");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { loadAgents } from "./agents.ts";
|
|||||||
import { loadConfig, resolveSpawn, type Diagnostics } from "./config.ts";
|
import { loadConfig, resolveSpawn, type Diagnostics } from "./config.ts";
|
||||||
import { SubprocessRpcRunner } from "./runner.ts";
|
import { SubprocessRpcRunner } from "./runner.ts";
|
||||||
import { Supervisor } from "./supervisor.ts";
|
import { Supervisor } from "./supervisor.ts";
|
||||||
|
import { milestoneNotification } from "./status.ts";
|
||||||
import type { SpawnRequest, SubagentStatus } from "./types.ts";
|
import type { SpawnRequest, SubagentStatus } from "./types.ts";
|
||||||
import { widget } from "./ui.ts";
|
import { widget } from "./ui.ts";
|
||||||
|
|
||||||
@@ -23,7 +24,11 @@ export default function subagents(pi: ExtensionAPI) {
|
|||||||
supervisor = new Supervisor(new SubprocessRpcRunner(), cwd, {
|
supervisor = new Supervisor(new SubprocessRpcRunner(), cwd, {
|
||||||
maxConcurrent: config.maxConcurrent,
|
maxConcurrent: config.maxConcurrent,
|
||||||
recentTerminalTtlMs: config.recentTerminalTtlMs,
|
recentTerminalTtlMs: config.recentTerminalTtlMs,
|
||||||
onMilestone: (status, event) => pi.appendEntry("subagent_milestone", { event, status }),
|
onMilestone: (status, event) => {
|
||||||
|
pi.appendEntry("subagent_milestone", { event, status });
|
||||||
|
const notification = milestoneNotification(status, event);
|
||||||
|
if (notification) ctx.ui?.notify?.(notification.message, notification.level);
|
||||||
|
},
|
||||||
onChange: (statuses) => {
|
onChange: (statuses) => {
|
||||||
lastStatuses = statuses;
|
lastStatuses = statuses;
|
||||||
updateUi(ctx, config.ui.enabled);
|
updateUi(ctx, config.ui.enabled);
|
||||||
@@ -51,6 +56,7 @@ export default function subagents(pi: ExtensionAPI) {
|
|||||||
description: "Start one ad hoc independent subagent and return immediately with its child id",
|
description: "Start one ad hoc independent subagent and return immediately with its child id",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
prompt: Type.String({ description: "Prompt for the delegated subagent" }),
|
prompt: Type.String({ description: "Prompt for the delegated subagent" }),
|
||||||
|
label: Type.Optional(Type.String({ description: "Human-readable label for this work item" })),
|
||||||
agent: Type.Optional(Type.String({ description: "Named agent definition to use" })),
|
agent: Type.Optional(Type.String({ description: "Named agent definition to use" })),
|
||||||
context: Type.Optional(Type.Union([Type.Literal("independent"), Type.Literal("fork")])),
|
context: Type.Optional(Type.Union([Type.Literal("independent"), Type.Literal("fork")])),
|
||||||
model: Type.Optional(Type.String({ description: "Optional model selector for the child" })),
|
model: Type.Optional(Type.String({ description: "Optional model selector for the child" })),
|
||||||
@@ -59,7 +65,7 @@ export default function subagents(pi: ExtensionAPI) {
|
|||||||
}),
|
}),
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
const accepted = getSupervisor(ctx).spawn(resolve(ctx, params as SpawnRequest));
|
const accepted = getSupervisor(ctx).spawn(resolve(ctx, params as SpawnRequest));
|
||||||
ctx.ui?.notify?.(`Started subagent ${accepted.id}`, "info");
|
ctx.ui?.notify?.(`Started subagent ${accepted.label}`, "info");
|
||||||
return textResult(accepted);
|
return textResult(accepted);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -72,6 +78,7 @@ export default function subagents(pi: ExtensionAPI) {
|
|||||||
subagents: Type.Array(
|
subagents: Type.Array(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
prompt: Type.String({ description: "Prompt for the delegated subagent" }),
|
prompt: Type.String({ description: "Prompt for the delegated subagent" }),
|
||||||
|
label: Type.Optional(Type.String({ description: "Human-readable label for this work item" })),
|
||||||
agent: Type.Optional(Type.String({ description: "Named agent definition to use" })),
|
agent: Type.Optional(Type.String({ description: "Named agent definition to use" })),
|
||||||
context: Type.Optional(Type.Union([Type.Literal("independent"), Type.Literal("fork")])),
|
context: Type.Optional(Type.Union([Type.Literal("independent"), Type.Literal("fork")])),
|
||||||
model: Type.Optional(Type.String({ description: "Optional model selector for the child" })),
|
model: Type.Optional(Type.String({ description: "Optional model selector for the child" })),
|
||||||
@@ -164,7 +171,7 @@ export default function subagents(pi: ExtensionAPI) {
|
|||||||
description: "Start an ad hoc independent subagent",
|
description: "Start an ad hoc independent subagent",
|
||||||
handler: async (args, ctx) => {
|
handler: async (args, ctx) => {
|
||||||
const accepted = getSupervisor(ctx).spawn(resolve(ctx, parseSpawnArgs(args)));
|
const accepted = getSupervisor(ctx).spawn(resolve(ctx, parseSpawnArgs(args)));
|
||||||
ctx.ui.notify(`Started subagent ${accepted.id}`, "info");
|
ctx.ui.notify(`Started subagent ${accepted.label}`, "info");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,6 +257,7 @@ function parseSpawnArgs(args: string): SpawnRequest {
|
|||||||
const flag = parts.shift();
|
const flag = parts.shift();
|
||||||
const value = parts.shift();
|
const value = parts.shift();
|
||||||
if (flag === "--agent") request.agent = value;
|
if (flag === "--agent") request.agent = value;
|
||||||
|
else if (flag === "--label") request.label = value;
|
||||||
else if (flag === "--context" && (value === "independent" || value === "fork")) request.context = value;
|
else if (flag === "--context" && (value === "independent" || value === "fork")) request.context = value;
|
||||||
else if (flag === "--tools") request.tools = value;
|
else if (flag === "--tools") request.tools = value;
|
||||||
else if (flag === "--model") request.model = value;
|
else if (flag === "--model") request.model = value;
|
||||||
|
|||||||
@@ -51,14 +51,18 @@ test("child RPC process disables discovery while explicitly loading subagents ex
|
|||||||
|
|
||||||
const { SubprocessRpcRunner } = await import("./runner.ts");
|
const { SubprocessRpcRunner } = await import("./runner.ts");
|
||||||
const runner = new SubprocessRpcRunner();
|
const runner = new SubprocessRpcRunner();
|
||||||
await runner.start("child-1", { prompt: "work" }, "/tmp", events());
|
await runner.start("child-1", { prompt: "work", label: "Review migration" }, "/tmp", events());
|
||||||
|
|
||||||
assert.equal(spawn.mock.callCount(), 1);
|
assert.equal(spawn.mock.callCount(), 1);
|
||||||
const args = calls[0].args;
|
const args = calls[0].args;
|
||||||
const noExtensionsIndex = args.indexOf("--no-extensions");
|
const noExtensionsIndex = args.indexOf("--no-extensions");
|
||||||
const extensionIndex = args.indexOf("--extension");
|
const extensionIndex = args.indexOf("--extension");
|
||||||
|
|
||||||
|
const nameIndex = args.indexOf("--name");
|
||||||
|
|
||||||
assert.notEqual(noExtensionsIndex, -1, "child args keep automatic extension discovery disabled");
|
assert.notEqual(noExtensionsIndex, -1, "child args keep automatic extension discovery disabled");
|
||||||
|
assert.notEqual(nameIndex, -1, "child args include a process name");
|
||||||
|
assert.equal(args[nameIndex + 1], "subagent Review migration");
|
||||||
assert.notEqual(extensionIndex, -1, "child args explicitly load the subagents extension entry");
|
assert.notEqual(extensionIndex, -1, "child args explicitly load the subagents extension entry");
|
||||||
assert.equal(args[extensionIndex + 1], fileURLToPath(new URL("./index.ts", import.meta.url)));
|
assert.equal(args[extensionIndex + 1], fileURLToPath(new URL("./index.ts", import.meta.url)));
|
||||||
assert.ok(noExtensionsIndex < extensionIndex);
|
assert.ok(noExtensionsIndex < extensionIndex);
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class RpcChildHandle implements ChildHandle {
|
|||||||
|
|
||||||
export class SubprocessRpcRunner implements ChildRunner {
|
export class SubprocessRpcRunner implements ChildRunner {
|
||||||
async start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise<ChildHandle> {
|
async start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise<ChildHandle> {
|
||||||
const args = [process.argv[1], "--mode", "rpc", "--no-extensions", "--extension", subagentsExtensionPath(), "--name", `subagent ${id}`, ...contextArgs(request), ...toolArgs(request), ...modelArgs(request)];
|
const args = [process.argv[1], "--mode", "rpc", "--no-extensions", "--extension", subagentsExtensionPath(), "--name", `subagent ${request.label ?? id}`, ...contextArgs(request), ...toolArgs(request), ...modelArgs(request)];
|
||||||
const child = spawn(process.execPath, args, {
|
const child = spawn(process.execPath, args, {
|
||||||
cwd,
|
cwd,
|
||||||
env: childEnvironment(),
|
env: childEnvironment(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ChildRecord, SpawnAccepted, SubagentResult, SubagentStatus } from "./types.ts";
|
import { SUBAGENT_STATES, SUBAGENT_TERMINAL_STATES } from "./types.ts";
|
||||||
|
import type { ChildRecord, SpawnAccepted, SubagentResult, SubagentState, SubagentStatus } from "./types.ts";
|
||||||
|
|
||||||
export function toAccepted(status: SubagentStatus): SpawnAccepted {
|
export function toAccepted(status: SubagentStatus): SpawnAccepted {
|
||||||
return {
|
return {
|
||||||
@@ -17,9 +18,10 @@ export function cloneStatus(status: SubagentStatus): SubagentStatus {
|
|||||||
|
|
||||||
export function cloneResult(record: ChildRecord): SubagentResult {
|
export function cloneResult(record: ChildRecord): SubagentResult {
|
||||||
const status = cloneStatus(record.status);
|
const status = cloneStatus(record.status);
|
||||||
const terminal = ["completed", "failed", "cancelled", "timed_out", "orphaned"].includes(status.state);
|
const terminal = isTerminalState(status.state);
|
||||||
return {
|
return {
|
||||||
id: status.id,
|
id: status.id,
|
||||||
|
label: status.label,
|
||||||
state: status.state,
|
state: status.state,
|
||||||
running: !terminal,
|
running: !terminal,
|
||||||
resultAvailable: status.resultAvailable,
|
resultAvailable: status.resultAvailable,
|
||||||
@@ -30,6 +32,19 @@ export function cloneResult(record: ChildRecord): SubagentResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTerminalState(state: SubagentState): boolean {
|
||||||
|
return (SUBAGENT_TERMINAL_STATES as readonly string[]).includes(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function milestoneNotification(status: SubagentStatus, event: string): { message: string; level: "info" | "error" } | undefined {
|
||||||
|
if (!isSubagentState(event) || !isTerminalState(event)) return undefined;
|
||||||
|
return { message: `Subagent ${status.label} ${event}`, level: event === "completed" ? "info" : "error" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSubagentState(value: string): value is SubagentState {
|
||||||
|
return (SUBAGENT_STATES as readonly string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
export function elapsedMs(status: Pick<SubagentStatus, "startedAt" | "completedAt">): number {
|
export function elapsedMs(status: Pick<SubagentStatus, "startedAt" | "completedAt">): number {
|
||||||
const start = Date.parse(status.startedAt);
|
const start = Date.parse(status.startedAt);
|
||||||
const end = status.completedAt ? Date.parse(status.completedAt) : Date.now();
|
const end = status.completedAt ? Date.parse(status.completedAt) : Date.now();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
import { milestoneNotification } from "./status.ts";
|
||||||
import { Supervisor } from "./supervisor.ts";
|
import { Supervisor } from "./supervisor.ts";
|
||||||
import type { ChildHandle, ChildRunner, RunnerEvents, SpawnRequest } from "./types.ts";
|
import type { ChildHandle, ChildRunner, RunnerEvents, SpawnRequest } from "./types.ts";
|
||||||
|
import { widget } from "./ui.ts";
|
||||||
|
|
||||||
class FakeHandle implements ChildHandle {
|
class FakeHandle implements ChildHandle {
|
||||||
cancelCalls = 0;
|
cancelCalls = 0;
|
||||||
@@ -110,6 +112,77 @@ test("completed children ignore later cancel", async () => {
|
|||||||
assert.equal(runner.starts[0].handle.cancelCalls, 0);
|
assert.equal(runner.starts[0].handle.cancelCalls, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("explicit labels are reused across accepted status list and result surfaces", async () => {
|
||||||
|
const runner = new FakeRunner();
|
||||||
|
const supervisor = new Supervisor(runner, "/tmp");
|
||||||
|
const label = "Review risky migration";
|
||||||
|
|
||||||
|
const accepted = supervisor.spawn({ prompt: "inspect the migration plan", label } as SpawnRequest & { label: string });
|
||||||
|
await sleep(0);
|
||||||
|
runner.starts[0].events.completed("done", "agent_settled");
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
{
|
||||||
|
accepted: accepted.label,
|
||||||
|
status: supervisor.status(accepted.id).label,
|
||||||
|
list: supervisor.list().find((status) => status.id === accepted.id)?.label,
|
||||||
|
result: (supervisor.result(accepted.id) as { label?: string }).label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accepted: label,
|
||||||
|
status: label,
|
||||||
|
list: label,
|
||||||
|
result: label,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ad hoc fallback labels are prompt-derived and reused by widget and result surfaces", async () => {
|
||||||
|
const runner = new FakeRunner();
|
||||||
|
const supervisor = new Supervisor(runner, "/tmp");
|
||||||
|
const prompt = " Audit\n\tguest enablement plan ";
|
||||||
|
const label = "Audit guest enablement plan";
|
||||||
|
|
||||||
|
const accepted = supervisor.spawn({ prompt });
|
||||||
|
await sleep(0);
|
||||||
|
runner.starts[0].events.completed("done", "agent_settled");
|
||||||
|
const statuses = supervisor.list();
|
||||||
|
const inspectorLines = widget(statuses, true)().render(240);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
{
|
||||||
|
accepted: accepted.label,
|
||||||
|
childRequest: runner.starts[0].request.label,
|
||||||
|
status: supervisor.status(accepted.id).label,
|
||||||
|
list: statuses.find((status) => status.id === accepted.id)?.label,
|
||||||
|
result: supervisor.result(accepted.id).label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accepted: label,
|
||||||
|
childRequest: label,
|
||||||
|
status: label,
|
||||||
|
list: label,
|
||||||
|
result: label,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.ok(inspectorLines.some((line) => line.includes(`${accepted.id} ${label} independent completed`)), inspectorLines.join("\n"));
|
||||||
|
assert.doesNotMatch(accepted.label, /^ad-hoc sg-/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("milestone notifications use the stored label", async () => {
|
||||||
|
const runner = new FakeRunner();
|
||||||
|
const supervisor = new Supervisor(runner, "/tmp");
|
||||||
|
const accepted = supervisor.spawn({ prompt: "work", label: "Review migration" });
|
||||||
|
await sleep(0);
|
||||||
|
runner.starts[0].events.completed("done", "agent_settled");
|
||||||
|
|
||||||
|
assert.deepEqual(milestoneNotification(supervisor.status(accepted.id), "completed"), {
|
||||||
|
message: "Subagent Review migration completed",
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
assert.equal(milestoneNotification(supervisor.status(accepted.id), "running"), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
test("shutdown clears recent terminal expiry timer", async () => {
|
test("shutdown clears recent terminal expiry timer", async () => {
|
||||||
const runner = new FakeRunner();
|
const runner = new FakeRunner();
|
||||||
let changes = 0;
|
let changes = 0;
|
||||||
@@ -128,17 +201,22 @@ test("shutdown clears recent terminal expiry timer", async () => {
|
|||||||
assert.equal(changes, afterShutdown);
|
assert.equal(changes, afterShutdown);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("batch spawn returns accepted ids and per-entry failures", async () => {
|
test("batch spawn returns explicit labels on accepted child requests and statuses while preserving failures", async () => {
|
||||||
const runner = new FakeRunner();
|
const runner = new FakeRunner();
|
||||||
const supervisor = new Supervisor(runner, "/tmp");
|
const supervisor = new Supervisor(runner, "/tmp");
|
||||||
|
|
||||||
const result = supervisor.spawnBatch([{ prompt: "one" }, { prompt: "" }, { prompt: "two" }]);
|
const result = supervisor.spawnBatch([
|
||||||
|
{ prompt: "one", label: "Review docs" },
|
||||||
|
{ prompt: "" },
|
||||||
|
{ prompt: "two", label: "Check tests" },
|
||||||
|
]);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
|
|
||||||
assert.equal(result.accepted.length, 2);
|
assert.deepEqual(result.accepted.map((accepted) => accepted.label), ["Review docs", "Check tests"]);
|
||||||
assert.equal(result.failed.length, 1);
|
assert.equal(result.failed.length, 1);
|
||||||
assert.equal(result.failed[0].index, 1);
|
assert.equal(result.failed[0].index, 1);
|
||||||
assert.equal(runner.starts.length, 2);
|
assert.deepEqual(runner.starts.map((start) => start.request.label), ["Review docs", "Check tests"]);
|
||||||
|
assert.deepEqual(result.accepted.map((accepted) => supervisor.status(accepted.id).label), ["Review docs", "Check tests"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("maxConcurrent preserves queued records", async () => {
|
test("maxConcurrent preserves queued records", async () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
SubagentWaitMode,
|
SubagentWaitMode,
|
||||||
SubagentWaitResult,
|
SubagentWaitResult,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
import { cloneResult, cloneStatus, toAccepted } from "./status.ts";
|
import { cloneResult, cloneStatus, isTerminalState, toAccepted } from "./status.ts";
|
||||||
|
|
||||||
interface RunningChild {
|
interface RunningChild {
|
||||||
record: ChildRecord;
|
record: ChildRecord;
|
||||||
@@ -157,7 +157,7 @@ export class Supervisor {
|
|||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const status: SubagentStatus = {
|
const status: SubagentStatus = {
|
||||||
id,
|
id,
|
||||||
label: request.agent ?? `ad-hoc ${id}`,
|
label: deriveLabel(request, id),
|
||||||
agent: request.agent,
|
agent: request.agent,
|
||||||
adHoc: !request.agent,
|
adHoc: !request.agent,
|
||||||
context: this.resolveContext(request.context),
|
context: this.resolveContext(request.context),
|
||||||
@@ -172,7 +172,7 @@ export class Supervisor {
|
|||||||
lastEventAt: now,
|
lastEventAt: now,
|
||||||
resultAvailable: false,
|
resultAvailable: false,
|
||||||
};
|
};
|
||||||
const child: RunningChild = { record: { status }, request: { ...request, prompt, context: status.context, tools: status.tools } };
|
const child: RunningChild = { record: { status }, request: { ...request, prompt, label: status.label, context: status.context, tools: status.tools } };
|
||||||
this.children.set(id, child);
|
this.children.set(id, child);
|
||||||
this.emitMilestone(child, "accepted");
|
this.emitMilestone(child, "accepted");
|
||||||
this.queue.push(child);
|
this.queue.push(child);
|
||||||
@@ -418,6 +418,32 @@ export class Supervisor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTerminal(state: SubagentStatus["state"]): boolean {
|
function deriveLabel(request: SpawnRequest, id: string): string {
|
||||||
return ["completed", "failed", "cancelled", "timed_out", "orphaned"].includes(state);
|
const explicit = normalizeLabel(request.label);
|
||||||
|
if (explicit) return explicit;
|
||||||
|
const agent = normalizeLabel(request.agent);
|
||||||
|
if (agent) return agent;
|
||||||
|
return promptLabel(request.prompt) ?? `ad-hoc ${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptLabel(prompt: string): string | undefined {
|
||||||
|
const normalized = normalizeLabel(prompt);
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
return truncateLabel(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLabel(value: unknown): string | undefined {
|
||||||
|
if (typeof value !== "string") return undefined;
|
||||||
|
const normalized = value.replace(/\s+/gu, " ").trim();
|
||||||
|
return normalized || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateLabel(label: string): string {
|
||||||
|
const maxLength = 80;
|
||||||
|
if (label.length <= maxLength) return label;
|
||||||
|
return `${label.slice(0, maxLength - 1).trimEnd()}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminal(state: SubagentStatus["state"]): boolean {
|
||||||
|
return isTerminalState(state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
export type ContextMode = "independent" | "fork";
|
export type ContextMode = "independent" | "fork";
|
||||||
|
|
||||||
export type SubagentState =
|
export const SUBAGENT_STATES = ["queued", "starting", "running", "settling", "completed", "failed", "cancelled", "timed_out", "orphaned"] as const;
|
||||||
| "queued"
|
export const SUBAGENT_TERMINAL_STATES = ["completed", "failed", "cancelled", "timed_out", "orphaned"] as const;
|
||||||
| "starting"
|
|
||||||
| "running"
|
export type SubagentState = (typeof SUBAGENT_STATES)[number];
|
||||||
| "settling"
|
|
||||||
| "completed"
|
|
||||||
| "failed"
|
|
||||||
| "cancelled"
|
|
||||||
| "timed_out"
|
|
||||||
| "orphaned";
|
|
||||||
|
|
||||||
export interface ToolProfile {
|
export interface ToolProfile {
|
||||||
activeTools: string[] | null;
|
activeTools: string[] | null;
|
||||||
@@ -17,6 +11,7 @@ export interface ToolProfile {
|
|||||||
|
|
||||||
export interface SpawnRequest {
|
export interface SpawnRequest {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
label?: string;
|
||||||
context?: ContextMode;
|
context?: ContextMode;
|
||||||
agent?: string;
|
agent?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -60,6 +55,7 @@ export interface SubagentStatus {
|
|||||||
|
|
||||||
export interface SubagentResult {
|
export interface SubagentResult {
|
||||||
id: string;
|
id: string;
|
||||||
|
label: string;
|
||||||
state: SubagentState;
|
state: SubagentState;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
resultAvailable: boolean;
|
resultAvailable: boolean;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { isTerminalState } from "./status.ts";
|
||||||
import type { SubagentStatus } from "./types.ts";
|
import type { SubagentStatus } from "./types.ts";
|
||||||
|
|
||||||
export function renderSummary(statuses: SubagentStatus[]): string[] {
|
export function renderSummary(statuses: SubagentStatus[]): string[] {
|
||||||
const running = statuses.filter((status) => ["starting", "running", "settling"].includes(status.state)).length;
|
const running = statuses.filter((status) => ["starting", "running", "settling"].includes(status.state)).length;
|
||||||
const queued = statuses.filter((status) => status.state === "queued").length;
|
const queued = statuses.filter((status) => status.state === "queued").length;
|
||||||
const terminal = statuses.filter((status) => ["completed", "failed", "cancelled", "timed_out", "orphaned"].includes(status.state)).length;
|
const terminal = statuses.filter((status) => isTerminalState(status.state)).length;
|
||||||
if (running === 0 && queued === 0 && terminal === 0) return [];
|
if (running === 0 && queued === 0 && terminal === 0) return [];
|
||||||
return [`subagents: ${running} running · ${queued} queued · ${terminal} recent`];
|
return [`subagents: ${running} running · ${queued} queued · ${terminal} recent`];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user