import { StringEnum } from "@earendil-works/pi-ai/compat"; import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { goalToolResponse, toToolText, type GoalToolResponse } from "./format.js"; import { createGoal, replaceGoal } from "./state.js"; import { TOOL_PROMPT_GUIDELINES } from "./prompts.js"; import type { GoalEntrySource, GoalResult, ThreadGoal } from "./types.js"; const EmptyParams = Type.Object({}); const CreateGoalParams = Type.Object({ objective: Type.String({ description: "Concrete objective to pursue until completion.", }), token_budget: Type.Optional( Type.Integer({ description: "Optional positive integer token budget.", minimum: 1, }), ), replace_existing: Type.Optional( Type.Boolean({ description: "Replace an existing non-complete goal. Use only when the user explicitly asks to set a new goal over the current one.", }), ), }); const UpdateGoalParams = Type.Object({ status: StringEnum(["complete", "blocked"] as const, { description: "Set to complete only when the objective is achieved and no required work remains. Set to blocked only after the same blocking condition has repeated for at least three consecutive goal turns and the agent is at an impasse. After a previously blocked goal is resumed, the resumed run starts a fresh blocked audit.", }), }); export interface ToolHost { getGoal(): ThreadGoal | null; setGoal(goal: ThreadGoal, source: GoalEntrySource, ctx: ExtensionContext): void; completeGoal(source: GoalEntrySource, ctx: ExtensionContext): GoalResult; blockGoal(source: GoalEntrySource, ctx: ExtensionContext): GoalResult; } function textResult( text: string, goal: ThreadGoal | null, includeCompletionBudgetReport = false, ): AgentToolResult { return { content: [{ type: "text", text }], details: { ...goalToolResponse(goal, includeCompletionBudgetReport), error: null }, }; } function throwToolError(message: string): never { throw new Error(message); } export function registerGoalTools(pi: ExtensionAPI, host: ToolHost): void { pi.registerTool({ name: "get_goal", label: "Get Goal", description: "Get the current goal and usage for this pi session.", promptSnippet: "Inspect the current goal, status, token budget, tokens used, and active elapsed time.", promptGuidelines: TOOL_PROMPT_GUIDELINES, parameters: EmptyParams, async execute() { const goal = host.getGoal(); return textResult(toToolText(goal), goal); }, }); pi.registerTool({ name: "create_goal", label: "Create Goal", description: "Create a long-running goal for this pi session.", promptSnippet: "Create one goal with an objective and optional positive token budget. Fails when a non-complete goal already exists unless replace_existing is true; replaces a completed goal.", promptGuidelines: TOOL_PROMPT_GUIDELINES, parameters: CreateGoalParams, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const current = host.getGoal(); const shouldReplaceExisting = params.replace_existing === true && current !== null && current.status !== "complete"; const result = shouldReplaceExisting ? replaceGoal(params.objective, params.token_budget ?? null) : createGoal(current, params.objective, params.token_budget ?? null); if (!result.ok || !result.goal) { throwToolError(result.message); } host.setGoal(result.goal, "tool", ctx); return textResult(toToolText(result.goal), result.goal); }, }); pi.registerTool({ name: "update_goal", label: "Update Goal", description: "Update the existing goal. Use this tool only to mark the goal achieved or genuinely blocked. Set status to complete only when the objective has actually been achieved and no required work remains. Set status to blocked only when the same blocking condition has repeated for at least three consecutive goal turns and the agent cannot make meaningful progress without user input or an external-state change. After a previously blocked goal is resumed, the resumed run starts a fresh blocked audit.", promptSnippet: "Mark the current goal complete only after an evidence-backed completion audit proves no required work remains, or blocked only after the strict blocked audit is satisfied.", promptGuidelines: TOOL_PROMPT_GUIDELINES, parameters: UpdateGoalParams, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const result = params.status === "blocked" ? host.blockGoal("tool", ctx) : host.completeGoal("tool", ctx); if (!result.ok || !result.goal) { throwToolError(result.message); } return textResult(toToolText(result.goal, params.status === "complete"), result.goal, params.status === "complete"); }, }); }