Initial commit

This commit is contained in:
Amos
2026-06-26 22:25:19 +03:00
commit c6c00f89a7
29 changed files with 14358 additions and 0 deletions
+324
View File
@@ -0,0 +1,324 @@
/**
* Extension loaded into sub-agents.
* - Shows agent identity + available tools as a styled widget above the editor (toggle with Ctrl+J)
* - Provides a `subagent_done` tool for autonomous agents to self-terminate
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Box, Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { writeFileSync } from "node:fs";
import { createSubagentActivityRecorder } from "./activity.ts";
export function shouldMarkUserTookOver(agentStarted: boolean): boolean {
return agentStarted;
}
export function shouldAutoExitOnAgentEnd(
_userTookOver: boolean,
messages: any[] | undefined,
): boolean {
// Manual input should not strand an auto-exit subagent. If the latest agent
// turn completed normally, close the session. Escape/abort still leaves it
// open for inspection or another prompt.
//
// stopReason: "error" (e.g. exhausted retries on a provider overload) also
// returns true — we want to shut down so the parent is woken up — but we
// pair this with findLatestAssistantError() so the parent learns it was an
// error, not a clean completion.
if (messages) {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg?.role === "assistant") {
return msg.stopReason !== "aborted";
}
}
}
return true;
}
export interface SubagentErrorInfo {
errorMessage: string;
stopReason: "error";
}
/**
* If the last assistant message in the turn ended with `stopReason: "error"`
* (typically auto-retry exhausted on an overload / rate limit / server error),
* return its error info so the parent orchestrator can surface a clear
* failure instead of silently treating the run as completed.
*
* Returns `null` when the latest assistant turn completed normally or was
* aborted by the user (handled separately by shouldAutoExitOnAgentEnd).
*/
export function findLatestAssistantError(
messages: any[] | undefined,
): SubagentErrorInfo | null {
if (!messages) return null;
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg?.role !== "assistant") continue;
if (msg.stopReason !== "error") return null;
const raw = typeof msg.errorMessage === "string" ? msg.errorMessage.trim() : "";
return {
errorMessage: raw || "Subagent agent loop ended with stopReason=error (no errorMessage field).",
stopReason: "error",
};
}
return null;
}
export function parseDeniedTools(rawValue: string | undefined): string[] {
return (rawValue ?? "")
.split(",")
.map((value) => value.trim())
.filter(Boolean);
}
export default function (pi: ExtensionAPI) {
let toolNames: string[] = [];
let denied: string[] = [];
let expanded = false;
// Read subagent identity from env vars (set by parent orchestrator)
const subagentName = process.env.PI_SUBAGENT_NAME ?? "";
const subagentAgent = process.env.PI_SUBAGENT_AGENT ?? "";
const deniedToolsValue = process.env.PI_DENY_TOOLS;
const autoExit = process.env.PI_SUBAGENT_AUTO_EXIT === "1";
const recorder = createSubagentActivityRecorder({
runningChildId: process.env.PI_SUBAGENT_ID,
activityFile: process.env.PI_SUBAGENT_ACTIVITY_FILE,
});
function renderWidget(ctx: { ui: { setWidget: Function } }, _theme: any) {
ctx.ui.setWidget(
"subagent-tools",
(_tui: any, theme: any) => {
const box = new Box(1, 0, (text: string) => theme.bg("toolSuccessBg", text));
const label = subagentAgent || subagentName;
const agentTag = label ? theme.bold(theme.fg("accent", `[${label}]`)) : "";
if (expanded) {
// Expanded: full tool list + denied
const countInfo = theme.fg("dim", ` — ${toolNames.length} available`);
const hint = theme.fg("muted", " (Ctrl+J to collapse)");
const toolList = toolNames
.map((name: string) => theme.fg("dim", name))
.join(theme.fg("muted", ", "));
let deniedLine = "";
if (denied.length > 0) {
const deniedList = denied
.map((name: string) => theme.fg("error", name))
.join(theme.fg("muted", ", "));
deniedLine = "\n" + theme.fg("muted", "denied: ") + deniedList;
}
const content = new Text(
`${agentTag}${countInfo}${hint}\n${toolList}${deniedLine}`,
0,
0,
);
box.addChild(content);
} else {
// Collapsed: one-line summary
const countInfo = theme.fg("dim", ` — ${toolNames.length} tools`);
const deniedInfo =
denied.length > 0
? theme.fg("dim", " · ") + theme.fg("error", `${denied.length} denied`)
: "";
const hint = theme.fg("muted", " (Ctrl+J to expand)");
const content = new Text(`${agentTag}${countInfo}${deniedInfo}${hint}`, 0, 0);
box.addChild(content);
}
return box;
},
{ placement: "aboveEditor" },
);
}
let userTookOver = false;
let agentStarted = false;
// Show widget + status bar on session start
pi.on("session_start", (_event, ctx) => {
recorder.sessionStart();
const tools = pi.getAllTools();
toolNames = tools.map((t) => t.name).sort();
denied = parseDeniedTools(deniedToolsValue);
renderWidget(ctx, null);
});
pi.on("input", () => {
recorder.input();
// Ignore the initial task message that starts an autonomous subagent.
// Only inputs after the first agent run has started count as user takeover.
if (!shouldMarkUserTookOver(agentStarted)) return;
userTookOver = true;
});
pi.on("before_agent_start", () => {
recorder.beforeAgentStart();
});
pi.on("agent_start", () => {
agentStarted = true;
recorder.agentStart();
});
pi.on("agent_end", (event, ctx) => {
const messages = (event as any).messages as any[] | undefined;
const shouldExit = autoExit && shouldAutoExitOnAgentEnd(userTookOver, messages);
if (shouldExit) {
// Surface stopReason: "error" turns (auto-retry exhausted, provider
// overload, etc.) to the parent via the .exit sidecar so the watcher
// can report a clear failure with the underlying error message.
// Without this the parent would only see exit code 0 and a stale
// assistant message, mistaking the crash for a successful completion.
const errorInfo = findLatestAssistantError(messages);
const sessionFile = process.env.PI_SUBAGENT_SESSION;
if (errorInfo && sessionFile) {
try {
writeFileSync(
`${sessionFile}.exit`,
JSON.stringify({
type: "error",
errorMessage: errorInfo.errorMessage,
stopReason: errorInfo.stopReason,
}),
);
} catch {
// Best effort — even without the sidecar, watcher's session-file
// fallback can still recover the errorMessage.
}
}
recorder.agentEndDone();
ctx.shutdown();
return;
}
recorder.agentEndWaiting();
if (autoExit) {
// Reset any recorded manual input marker. Auto-exit is decided by whether
// the latest agent turn completed normally, not by who initiated it.
userTookOver = false;
}
});
pi.on("turn_start", (event) => {
recorder.turnStart((event as any).turnIndex);
});
pi.on("turn_end", (event) => {
recorder.turnEnd((event as any).turnIndex);
});
pi.on("before_provider_request", () => {
recorder.beforeProviderRequest();
});
pi.on("after_provider_response", () => {
recorder.afterProviderResponse();
});
pi.on("message_update", (event) => {
recorder.messageUpdate((event as any).assistantMessageEvent?.type);
});
pi.on("tool_execution_start", (event) => {
recorder.toolExecutionStart((event as any).toolCallId, (event as any).toolName);
});
pi.on("tool_call", (event) => {
recorder.toolCall((event as any).toolCallId, (event as any).toolName);
});
pi.on("tool_execution_update", (event) => {
recorder.toolExecutionUpdate((event as any).toolCallId, (event as any).toolName);
});
pi.on("tool_result", (event) => {
recorder.toolResult((event as any).toolCallId, (event as any).toolName);
});
pi.on("tool_execution_end", (event) => {
recorder.toolExecutionEnd((event as any).toolCallId, (event as any).toolName);
});
pi.on("session_shutdown", (event) => {
recorder.sessionShutdown((event as any).reason);
});
// Toggle expand/collapse with Ctrl+J
pi.registerShortcut("ctrl+j", {
description: "Toggle subagent tools widget",
handler: (ctx) => {
expanded = !expanded;
renderWidget(ctx, null);
},
});
pi.registerTool({
name: "caller_ping",
label: "Caller Ping",
description:
"Send a help request to the parent agent and exit this session. " +
"The parent will be notified with your message and can resume this session with a response. " +
"Use when you're stuck, need clarification, or need the parent to take action.",
parameters: Type.Object({
message: Type.String({ description: "What you need help with" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const sessionFile = process.env.PI_SUBAGENT_SESSION;
if (!sessionFile) {
throw new Error(
"caller_ping is only available in subagent contexts. " +
"PI_SUBAGENT_SESSION environment variable is not set.",
);
}
recorder.callerPing();
const exitData = {
type: "ping" as const,
name: process.env.PI_SUBAGENT_NAME ?? "subagent",
message: params.message,
};
writeFileSync(`${sessionFile}.exit`, JSON.stringify(exitData));
ctx.shutdown();
return {
content: [{ type: "text", text: "Ping sent. Session will exit and parent will be notified." }],
details: {},
};
},
});
pi.registerTool({
name: "subagent_done",
label: "Subagent Done",
description:
"Call this tool when you have completed your task. " +
"It will close this session and return your results to the main session. " +
"Your LAST assistant message before calling this becomes the summary returned to the caller.",
parameters: Type.Object({}),
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
const sessionFile = process.env.PI_SUBAGENT_SESSION;
recorder.subagentDone();
if (sessionFile) {
writeFileSync(`${sessionFile}.exit`, JSON.stringify({ type: "done" }));
}
ctx.shutdown();
return {
content: [{ type: "text", text: "Shutting down subagent session." }],
details: {},
};
},
});
}