Files
oh-my-pi/packages/coding-agent/src/goals/tools/goal-tool.ts
T
can1357 64fcdc308f refactor(coding-agent)!: removed StringEnum helper and shortened tool schema descriptions
- Replaced all StringEnum(...) usages with z.enum([...]) across tools, examples, and tests.
- Removed StringEnum re-export from @oh-my-pi/pi-coding-agent public API.
- Condensed verbose tool parameter descriptions to minimal lowercase phrases.
- Renamed AuthCredentialStore to SqliteAuthCredentialStore at usage sites.
2026-05-16 19:26:32 +02:00

232 lines
7.4 KiB
TypeScript

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"]).describe("goal operation"),
objective: z.string().describe("goal objective").optional(),
token_budget: z.number().int().describe("token budget").optional(),
});
export type GoalToolInput = z.infer<typeof goalSchema>;
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<typeof goalSchema, GoalToolDetails> {
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<GoalToolDetails>,
_context?: AgentToolContext,
): Promise<AgentToolResult<GoalToolDetails>> {
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?.enabled ? state.goal : 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";
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,
};