/** * Pi Loop Extension - Custom Tools * * Registers the loop_report tool and other tools that the LLM uses * to communicate node completion and status. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { StringEnum } from "@earendil-works/pi-ai"; import type { LoopReport } from "./types.ts"; /** * Register the loop_report tool. * This is the primary way LLM nodes signal completion to the loop runtime. * * The agent calls loop_report when it believes the current LLM node is done. * The report is a proposal — gates decide whether to accept it. */ export function registerLoopReportTool( pi: ExtensionAPI, onReport: (report: LoopReport) => void, ): void { pi.registerTool({ name: "loop_report", label: "Loop Report", description: `Report completion of the current loop node. Call this tool when you have completed the work for the current node and are ready for validation. Provide a summary of what was done, any artifacts produced, files changed, commands run, and risks identified.`, promptSnippet: "Report loop node completion with summary, artifacts, and evidence", promptGuidelines: [ "Use loop_report when you have completed the current loop node's goal and are ready for validation.", "Always include a summary and confidence level in your loop_report.", ], parameters: Type.Object({ summary: Type.String({ description: "Summary of what was accomplished in this node" }), status: StringEnum(["ready_for_validation", "completed", "failed"] as const), confidence: StringEnum(["low", "medium", "high"] as const), artifacts: Type.Optional( Type.Record(Type.String(), Type.Unknown(), { description: "Optional structured artifacts produced", }), ), changedFiles: Type.Optional( Type.Array(Type.String(), { description: "Files that were changed" }), ), commandsRun: Type.Optional( Type.Array(Type.String(), { description: "Commands that were executed" }), ), risks: Type.Optional( Type.Array(Type.String(), { description: "Risks or concerns" }), ), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { // We can't access runId easily here — the runtime sets it via a closure // The onReport callback will fill in runId and nodeId onReport({ runId: "", // filled in by runtime nodeId: "", // filled in by runtime status: params.status, summary: params.summary, confidence: params.confidence, artifacts: params.artifacts, changedFiles: params.changedFiles, commandsRun: params.commandsRun, risks: params.risks, }); return { content: [ { type: "text", text: `Report received (status: ${params.status}). Your report will be validated by the loop gate.`, }, ], details: { reported: true, status: params.status, }, }; }, }); }