import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core"; import type { Component } from "@oh-my-pi/pi-tui"; import { Text } from "@oh-my-pi/pi-tui"; import { formatNumber, prompt } from "@oh-my-pi/pi-utils"; import * as z from "zod/v4"; import type { RenderResultOptions } from "../../extensibility/custom-tools/types"; import type { Theme, ThemeColor } from "../../modes/theme/theme"; import goalDescription from "../../prompts/tools/goal.md" with { type: "text" }; import { formatDuration } from "../../slash-commands/helpers/format"; import type { ToolSession } from "../../tools"; import { formatErrorMessage, TRUNCATE_LENGTHS } from "../../tools/render-utils"; import { ToolError } from "../../tools/tool-errors"; import { renderStatusLine, truncateToWidth } from "../../tui"; import { completionBudgetReport, remainingTokens } from "../runtime"; import type { Goal, GoalStatus, GoalToolDetails } from "../state"; const goalSchema = z.object({ op: z.enum(["create", "get", "complete", "resume", "drop"]).describe("goal operation"), objective: z.string().describe("goal objective").optional(), token_budget: z.number().int().describe("token budget").optional(), }); export type GoalToolInput = z.infer; export interface GoalToolResponse { goal: Goal | null; remainingTokens: number | null; completionBudgetReport: string | null; } export function buildGoalToolResponse( goal: Goal | null | undefined, options?: { includeCompletionReport?: boolean }, ): GoalToolResponse { const resolvedGoal = goal ?? null; return { goal: resolvedGoal, remainingTokens: remainingTokens(resolvedGoal), completionBudgetReport: options?.includeCompletionReport && resolvedGoal?.status === "complete" ? completionBudgetReport(resolvedGoal) : null, }; } function validateCreateParams(params: GoalToolInput): { objective: string; tokenBudget?: number } { const objective = params.objective?.trim(); if (!objective) { throw new ToolError("objective is required when op=create"); } const tokenBudget = params.token_budget; if (tokenBudget !== undefined && (!Number.isInteger(tokenBudget) || tokenBudget <= 0)) { throw new ToolError("token_budget must be a positive integer when provided"); } return { objective, tokenBudget }; } export class GoalTool implements AgentTool { readonly name = "goal"; readonly label = "Goal"; readonly description = prompt.render(goalDescription); readonly parameters = goalSchema; readonly strict = true; readonly intent = "omit" as const; readonly #session: ToolSession; constructor(session: ToolSession) { this.#session = session; } async execute( _toolCallId: string, params: GoalToolInput, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback, _context?: AgentToolContext, ): Promise> { const runtime = this.#session.getGoalRuntime?.(); if (!runtime) { throw new ToolError("Goal mode is not active."); } let response: GoalToolResponse; if (params.op === "create") { const created = await runtime.createGoal(validateCreateParams(params)); response = buildGoalToolResponse(created.goal); } else if (params.op === "get") { const state = this.#session.getGoalModeState?.(); response = buildGoalToolResponse(state?.goal ?? null); } else if (params.op === "resume") { const resumed = await runtime.resumeGoal(); response = buildGoalToolResponse(resumed.goal); } else if (params.op === "drop") { const dropped = await runtime.dropGoal(); response = buildGoalToolResponse(dropped ?? null); } else { const completed = await runtime.completeGoalFromTool(); response = buildGoalToolResponse(completed, { includeCompletionReport: true }); } let text: string; if (response.goal) { text = `Goal: ${response.goal.objective}\nStatus: ${response.goal.status}\nTokens: ${response.goal.tokensUsed} used`; if (response.goal.tokenBudget !== undefined) { text += ` / ${response.goal.tokenBudget} budget`; } if (response.remainingTokens !== null) { text += `\nRemaining tokens: ${response.remainingTokens}`; } if (response.completionBudgetReport) { text += `\n\n${response.completionBudgetReport}`; } } else { text = "No active goal."; } return { content: [{ type: "text", text }], details: { op: params.op, goal: response.goal, remainingTokens: response.remainingTokens, completionBudgetReport: response.completionBudgetReport, }, }; } } function describeOp(op: string | undefined): string { switch (op) { case "create": return "set"; case "complete": return "complete"; case "get": return "check"; case "resume": return "resume"; case "drop": return "drop"; default: return op ?? "?"; } } function goalBadgeColor(status: GoalStatus): ThemeColor { switch (status) { case "complete": return "success"; case "budget-limited": return "warning"; case "paused": case "dropped": return "muted"; default: return "accent"; } } interface GoalRenderArgs { op?: GoalToolInput["op"]; objective?: string; token_budget?: number; } export const goalToolRenderer = { renderCall(args: GoalRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component { const description = describeOp(args.op); const meta: string[] = []; const trimmedObjective = args.objective?.trim(); if (args.op === "create" && trimmedObjective) { const objective = truncateToWidth(trimmedObjective, TRUNCATE_LENGTHS.TITLE); meta.push(uiTheme.italic(uiTheme.fg("muted", `"${objective}"`))); } if (args.op === "create" && args.token_budget !== undefined) { meta.push(`budget ${formatNumber(args.token_budget)}`); } const text = renderStatusLine({ icon: "pending", title: "Goal", description, meta }, uiTheme); return new Text(text, 0, 0); }, renderResult( result: { content: Array<{ type: string; text?: string }>; details?: GoalToolDetails; isError?: boolean }, _options: RenderResultOptions, uiTheme: Theme, args?: GoalRenderArgs, ): Component { const fallbackText = result.content?.find(c => c.type === "text")?.text ?? ""; const details = result.details; const op = details?.op ?? args?.op; const description = describeOp(op); if (result.isError) { const header = renderStatusLine({ icon: "error", title: "Goal", description }, uiTheme); const body = formatErrorMessage(fallbackText || "Goal tool failed", uiTheme); return new Text([header, body].join("\n"), 0, 0); } const goal = details?.goal ?? null; if (!goal) { const header = renderStatusLine({ icon: "warning", title: "Goal", description }, uiTheme); const body = uiTheme.fg("muted", "No active goal."); return new Text([header, body].join("\n"), 0, 0); } const lines: string[] = []; lines.push( renderStatusLine( { icon: "success", title: "Goal", description, badge: { label: goal.status, color: goalBadgeColor(goal.status) }, }, uiTheme, ), ); const objectiveText = truncateToWidth(goal.objective.trim(), TRUNCATE_LENGTHS.LONG); lines.push(` ${uiTheme.italic(uiTheme.fg("muted", `"${objectiveText}"`))}`); const used = formatNumber(goal.tokensUsed); const tokensLine = goal.tokenBudget !== undefined ? `${used} / ${formatNumber(goal.tokenBudget)} tokens (${formatNumber(Math.max(0, goal.tokenBudget - goal.tokensUsed))} left)` : `${used} tokens`; lines.push(` ${uiTheme.fg("dim", tokensLine)}`); if (goal.timeUsedSeconds > 0) { lines.push(` ${uiTheme.fg("dim", `${formatDuration(goal.timeUsedSeconds * 1000)} elapsed`)}`); } const report = details?.completionBudgetReport; if (report) { lines.push(""); lines.push(uiTheme.italic(uiTheme.fg("muted", report))); } return new Text(lines.join("\n"), 0, 0); }, mergeCallAndResult: true, };