chore: remove deprecated calc tool as it is completely useless with eval

This commit is contained in:
can1357
2026-05-30 04:05:26 +02:00
parent b5b7f7f894
commit 9ce250eb99
12 changed files with 6 additions and 657 deletions
+1 -2
View File
@@ -233,11 +233,10 @@ Stealth's on by default, so pages see a normal user instead of a headless bot. T
**Misc**
- `calc` — deterministic arithmetic — no model in the loop.
- `resolve` — apply or discard a queued preview action.
- `search_tool_bm25` — BM25 over the hidden tool index; activates top matches mid-session.
Setting-gated, off by default: `github`, `calc`, `inspect_image`, `render_mermaid`, `checkpoint`, `rewind`, `search_tool_bm25`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project.
Setting-gated, off by default: `github`, `inspect_image`, `render_mermaid`, `checkpoint`, `rewind`, `search_tool_bm25`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project.
[Full reference →](https://omp.sh/docs/tools)
-71
View File
@@ -1,71 +0,0 @@
# calc
> Evaluates one or more arithmetic expressions and returns formatted numeric results.
## Source
- Entry: `packages/coding-agent/src/tools/calculator.ts`
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/calculator.md`
- Key collaborators:
- `packages/coding-agent/src/tui.ts` — status lines and tree-list rendering
- `packages/coding-agent/src/tools/render-utils.ts` — preview limits and formatting helpers
## Inputs
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `calculations` | `Calculation[]` | Yes | Batch of expressions to evaluate in order. |
### `Calculation`
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `expression` | `string` | Yes | Arithmetic expression string. |
| `prefix` | `string` | Yes | Prepended verbatim to the rendered numeric result. |
| `suffix` | `string` | Yes | Appended verbatim to the rendered numeric result. |
## Outputs
- Single-shot result.
- `content[0].text` is the newline-joined `prefix + value + suffix` string for each calculation.
- `details.results` is an array of `{ expression, value, output }`.
- On renderer fallback, if `details` is missing but `content[0].text` exists, the TUI tries to pair each output line with the original expressions from call args.
## Flow
1. `execute()` wraps evaluation in `untilAborted(...)`.
2. For each entry, `evaluateExpression(...)` tokenizes the expression, parses it with a recursive-descent parser, rejects non-finite outputs, and normalizes `-0` to `0`.
3. `tokenizeExpression(...)` accepts whitespace, parentheses, operators, and number literals; any other character throws immediately.
4. `ExpressionParser` applies precedence in this order: `+ -`, `* / %`, unary `+ -`, exponentiation `**`, parentheses/literals.
5. Exponentiation is right-associative (`2 ** 3 ** 2` parses as `2 ** (3 ** 2)`).
6. Each numeric result is formatted with `String(value)` and wrapped with the provided `prefix` and `suffix`.
7. The tool returns text output plus structured `details`.
## Side Effects
- Background work / cancellation
- Supports abort via `untilAborted(...)`.
- Session state
- None.
- Filesystem / Network / Subprocesses
- None.
## Limits & Caps
- Supported operators: `+`, `-`, `*`, `/`, `%`, `**` (`packages/coding-agent/src/tools/calculator.ts`).
- Supported numeric literals:
- decimal integers/floats, including leading-dot forms like `.5`
- scientific notation like `1e10`, `2.5E-3`
- hexadecimal `0x...`
- binary `0b...`
- octal `0o...`
- Results must be finite; `Infinity` and `NaN` are rejected.
- The renderer collapses long result lists using `PREVIEW_LIMITS.COLLAPSED_ITEMS` from `packages/coding-agent/src/tools/render-utils.ts`.
## Errors
- Invalid characters: e.g. `Invalid character "x" in expression`.
- Malformed numbers: invalid prefixed literal, invalid exponent, invalid number.
- Syntax errors: `Unexpected token in expression`, `Unexpected end of expression`, `Missing closing parenthesis`, `Expression is empty`.
- Non-finite arithmetic: `Expression result is not a finite number`.
- Any evaluation error aborts the whole batch; the tool does not return partial successes.
## Notes
- Despite the schema example showing `sqrt(16)`, the parser does not support functions, identifiers, units, or constants; only numeric literals, operators, and parentheses are accepted.
- Precision is plain JavaScript `number` semantics throughout, including floating-point rounding behavior.
- `/` and `%` use JavaScript numeric operators directly; there is no integer-only mode or unit handling.
- Unary operators bind tighter than `*`/`/`/`%` but looser than exponentiation because unary parsing delegates to `#parsePower()`.
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Removed
- Removed the `calc` tool (deterministic arithmetic evaluator) and its `calc.enabled` setting. The model can compute via `eval` instead.
## [15.5.14] - 2026-05-29
### Added
@@ -2017,16 +2017,6 @@ export const SETTINGS_SCHEMA = {
},
},
"calc.enabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
label: "Calculator",
description: "Enable the calculator tool for basic calculations",
},
},
"tts.enabled": {
type: "boolean",
default: false,
File diff suppressed because one or more lines are too long
@@ -1227,17 +1227,6 @@
return html;
}
function renderCalc(name, args, result, ctx) {
let html = toolHead('calc');
const exprs = args.expressions || (args.expression ? [args.expression] : []);
for (const e of exprs) html += codeBlock(String(e), 'plaintext');
if (result) {
const output = ctx.getResultText();
if (output) html += formatExpandableOutput(output, 6);
}
return html;
}
function renderJob(name, args, result, ctx) {
const badges = [];
const pollIds = Array.isArray(args.poll) ? args.poll : Array.isArray(args.jobs) ? args.jobs : Array.isArray(args.jobIds) ? args.jobIds : [];
@@ -1558,8 +1547,6 @@
yield: renderYield,
report_finding: renderReportFinding,
report_tool_issue: renderReportToolIssue,
calc: renderCalc,
calculator: renderCalc,
await: renderJob,
poll: renderJob,
cancel_job: renderJob,
@@ -1,10 +0,0 @@
Performs basic calculations.
<instruction>
- Supports +, -, *, /, %, ** and parentheses
- Supports decimal, hex (0x), binary (0b), and octal (0o) literals
</instruction>
<output>
Returns each calculation result with its prefix and suffix applied.
</output>
@@ -1,541 +0,0 @@
import type { AgentTool, AgentToolResult } 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 { prompt, untilAborted } from "@oh-my-pi/pi-utils";
import * as z from "zod/v4";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import calculatorDescription from "../prompts/tools/calculator.md" with { type: "text" };
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
import type { ToolSession } from ".";
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS, TRUNCATE_LENGTHS } from "./render-utils";
// =============================================================================
// Token Types
// =============================================================================
/** Supported arithmetic operators (** is exponentiation). */
type Operator = "+" | "-" | "*" | "/" | "%" | "**";
/**
* Lexer token variants:
* - number: parsed numeric value with original string for error messages
* - operator: arithmetic operator
* - paren: grouping parenthesis
*/
type Token =
| { type: "number"; value: number; raw: string }
| { type: "operator"; value: Operator }
| { type: "paren"; value: "(" | ")" };
const calculatorSchema = z.object({
calculations: z
.array(
z.object({
expression: z.string().describe("math expression"),
prefix: z.string().describe("prefix text"),
suffix: z.string().describe("suffix text"),
}),
)
.describe("calculations to evaluate"),
});
export interface CalculatorToolDetails {
results: Array<{ expression: string; value: number; output: string }>;
}
// =============================================================================
// Character classification helpers for numeric literal parsing
// =============================================================================
function isDigit(ch: string): boolean {
return ch >= "0" && ch <= "9";
}
function isHexDigit(ch: string): boolean {
return (ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "f") || (ch >= "A" && ch <= "F");
}
function isBinaryDigit(ch: string): boolean {
return ch === "0" || ch === "1";
}
function isOctalDigit(ch: string): boolean {
return ch >= "0" && ch <= "7";
}
// =============================================================================
// Tokenizer
// =============================================================================
/**
* Tokenize a math expression into numbers, operators, and parentheses.
*
* Number formats supported:
* - Decimal: 123, 3.14, .5
* - Scientific: 1e10, 2.5E-3
* - Hexadecimal: 0xFF
* - Binary: 0b1010
* - Octal: 0o755
*/
function tokenizeExpression(expression: string): Token[] {
const tokens: Token[] = [];
let i = 0;
while (i < expression.length) {
const ch = expression[i];
// Skip whitespace
if (ch.trim() === "") {
i += 1;
continue;
}
if (ch === "(" || ch === ")") {
tokens.push({ type: "paren", value: ch });
i += 1;
continue;
}
// Check ** before single * to handle exponentiation
if (ch === "*" && expression[i + 1] === "*") {
tokens.push({ type: "operator", value: "**" });
i += 2;
continue;
}
if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%") {
tokens.push({ type: "operator", value: ch });
i += 1;
continue;
}
// Number parsing: starts with digit or decimal point followed by digit
const next = expression[i + 1];
const numberStart = isDigit(ch) || (ch === "." && next !== undefined && isDigit(next));
if (!numberStart) {
throw new Error(`Invalid character "${ch}" in expression`);
}
const start = i;
// Handle prefixed literals (0x, 0b, 0o)
if (ch === "0" && next !== undefined) {
const prefix = next.toLowerCase();
if (prefix === "x" || prefix === "b" || prefix === "o") {
i += 2; // Skip "0x" / "0b" / "0o"
let hasDigit = false;
while (i < expression.length) {
const digit = expression[i];
const valid =
prefix === "x" ? isHexDigit(digit) : prefix === "b" ? isBinaryDigit(digit) : isOctalDigit(digit);
if (!valid) break;
hasDigit = true;
i += 1;
}
if (!hasDigit) {
throw new Error(`Invalid numeric literal starting at "${expression.slice(start, i)}"`);
}
const raw = expression.slice(start, i);
const value = Number(raw); // JS Number() handles 0x/0b/0o natively
if (!Number.isFinite(value)) {
throw new Error(`Invalid number "${raw}"`);
}
tokens.push({ type: "number", value, raw });
continue;
}
}
// Parse decimal number: integer part
let hasDigits = false;
while (i < expression.length && isDigit(expression[i])) {
hasDigits = true;
i += 1;
}
// Fractional part
if (expression[i] === ".") {
i += 1;
while (i < expression.length && isDigit(expression[i])) {
hasDigits = true;
i += 1;
}
}
if (!hasDigits) {
throw new Error(`Invalid number starting at "${expression.slice(start, i + 1)}"`);
}
// Scientific notation exponent (e.g., 1e10, 2.5E-3)
if (expression[i] === "e" || expression[i] === "E") {
i += 1;
if (expression[i] === "+" || expression[i] === "-") {
i += 1;
}
let hasExponentDigits = false;
while (i < expression.length && isDigit(expression[i])) {
hasExponentDigits = true;
i += 1;
}
if (!hasExponentDigits) {
throw new Error(`Invalid exponent in "${expression.slice(start, i)}"`);
}
}
const raw = expression.slice(start, i);
const value = Number(raw);
if (!Number.isFinite(value)) {
throw new Error(`Invalid number "${raw}"`);
}
tokens.push({ type: "number", value, raw });
}
return tokens;
}
// =============================================================================
// Recursive Descent Parser
// =============================================================================
/**
* Recursive descent parser for arithmetic expressions.
*
* Operator precedence (lowest to highest):
* 1. Addition, subtraction (+, -)
* 2. Multiplication, division, modulo (*, /, %)
* 3. Unary plus/minus (+x, -x)
* 4. Exponentiation (**)
* 5. Parentheses and literals
*
* Each precedence level has its own parse method. Lower precedence methods
* call higher precedence methods, building the AST implicitly through
* the call stack.
*/
class ExpressionParser {
#index = 0;
constructor(private readonly tokens: Token[]) {}
/** Parse the full expression and ensure all tokens are consumed. */
parse(): number {
const value = this.#parseExpression();
if (this.#index < this.tokens.length) {
throw new Error("Unexpected token in expression");
}
return value;
}
/**
* Parse addition and subtraction (lowest precedence).
* Left-associative: 1 - 2 - 3 = (1 - 2) - 3
*/
#parseExpression(): number {
let value = this.#parseTerm();
while (true) {
if (this.#matchOperator("+")) {
value += this.#parseTerm();
continue;
}
if (this.#matchOperator("-")) {
value -= this.#parseTerm();
continue;
}
break;
}
return value;
}
/**
* Parse multiplication, division, and modulo.
* Left-associative: 8 / 4 / 2 = (8 / 4) / 2
*/
#parseTerm(): number {
let value = this.#parseUnary();
while (true) {
if (this.#matchOperator("*")) {
value *= this.#parseUnary();
continue;
}
if (this.#matchOperator("/")) {
value /= this.#parseUnary();
continue;
}
if (this.#matchOperator("%")) {
value %= this.#parseUnary();
continue;
}
break;
}
return value;
}
/**
* Parse unary + and - operators.
* Recursive to handle chained unary: --x, +-x
*/
#parseUnary(): number {
if (this.#matchOperator("+")) {
return this.#parseUnary();
}
if (this.#matchOperator("-")) {
return -this.#parseUnary();
}
return this.#parsePower();
}
/**
* Parse exponentiation operator.
* Right-associative: 2 ** 3 ** 2 = 2 ** (3 ** 2) = 512
* Achieved by recursive call to parsePower for the right operand.
*/
#parsePower(): number {
let value = this.#parsePrimary();
if (this.#matchOperator("**")) {
value = value ** this.#parsePower(); // Right-associative via recursion
}
return value;
}
/**
* Parse primary expressions: number literals and parenthesized subexpressions.
* Parentheses restart parsing at lowest precedence (parseExpression).
*/
#parsePrimary(): number {
const token = this.#peek();
if (!token) {
throw new Error("Unexpected end of expression");
}
if (token.type === "number") {
this.#index += 1;
return token.value;
}
if (token.type === "paren" && token.value === "(") {
this.#index += 1;
const value = this.#parseExpression(); // Reset to lowest precedence
if (!this.#matchParen(")")) {
throw new Error("Missing closing parenthesis");
}
return value;
}
throw new Error("Unexpected token in expression");
}
/** Consume operator if it matches, advancing the token index. */
#matchOperator(value: Operator): boolean {
const token = this.tokens[this.#index];
if (token && token.type === "operator" && token.value === value) {
this.#index += 1;
return true;
}
return false;
}
/** Consume parenthesis if it matches, advancing the token index. */
#matchParen(value: "(" | ")"): boolean {
const token = this.tokens[this.#index];
if (token && token.type === "paren" && token.value === value) {
this.#index += 1;
return true;
}
return false;
}
/** Look at current token without consuming it. */
#peek(): Token | undefined {
return this.tokens[this.#index];
}
}
// =============================================================================
// Expression Evaluator
// =============================================================================
/**
* Evaluate a math expression string and return the numeric result.
*
* Pipeline: expression string -> tokens -> parse tree (implicit) -> value
*
* @throws Error on syntax errors, empty expressions, or non-finite results (Infinity, NaN)
*/
function evaluateExpression(expression: string): number {
const tokens = tokenizeExpression(expression);
if (tokens.length === 0) {
throw new Error("Expression is empty");
}
const parser = new ExpressionParser(tokens);
const value = parser.parse();
if (!Number.isFinite(value)) {
throw new Error("Expression result is not a finite number");
}
// Normalize -0 to 0 for consistent output
return Object.is(value, -0) ? 0 : value;
}
function formatResult(value: number): string {
return String(value);
}
// ═══════════════════════════════════════════════════════════════════════════
// Tool Class
// ═══════════════════════════════════════════════════════════════════════════
type CalculatorParams = z.infer<typeof calculatorSchema>;
/**
* Calculator tool for evaluating mathematical expressions.
*
* Supports decimal, hex (0x), binary (0b), octal (0o) literals,
* standard arithmetic operators, and parentheses.
*/
export class CalculatorTool implements AgentTool<typeof calculatorSchema, CalculatorToolDetails> {
readonly name = "calc";
readonly approval = "read" as const;
readonly label = "Calc";
readonly summary = "Evaluate a mathematical expression";
readonly loadMode = "discoverable";
readonly description: string;
readonly parameters = calculatorSchema;
readonly strict = true;
constructor(_session: ToolSession) {
this.description = prompt.render(calculatorDescription);
}
async execute(
_toolCallId: string,
{ calculations }: CalculatorParams,
signal?: AbortSignal,
): Promise<AgentToolResult<CalculatorToolDetails>> {
return untilAborted(signal, async () => {
const results = calculations.map(calc => {
const value = evaluateExpression(calc.expression);
const output = `${calc.prefix}${formatResult(value)}${calc.suffix}`;
return { expression: calc.expression, value, output };
});
const outputText = results.map(result => result.output).join("\n");
return {
content: [{ type: "text", text: outputText }],
details: { results },
};
});
}
}
// =============================================================================
// TUI Renderer
// =============================================================================
interface CalculatorRenderArgs {
calculations?: Array<{ expression: string; prefix?: string; suffix?: string }>;
}
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
/**
* TUI renderer for calculator tool calls and results.
* Handles both collapsed (preview) and expanded (full) display modes.
*/
export const calculatorToolRenderer = {
/**
* Render the tool call header showing the first expression and count.
* Format: "Calc <expression> (N calcs)"
*/
renderCall(args: CalculatorRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
const count = args.calculations?.length ?? 0;
const firstExpression = args.calculations?.[0]?.expression;
const description = firstExpression ? truncateToWidth(firstExpression, TRUNCATE_LENGTHS.TITLE) : undefined;
const meta = count > 0 ? [formatCount("calc", count)] : [];
const text = renderStatusLine({ icon: "pending", title: "Calc", description, meta }, uiTheme);
return new Text(text, 0, 0);
},
/**
* Render calculation results as a tree list.
* Collapsed mode shows first N items with expand hint; expanded shows all.
*/
renderResult(
result: { content: Array<{ type: string; text?: string }>; details?: CalculatorToolDetails; isError?: boolean },
options: RenderResultOptions,
uiTheme: Theme,
args?: CalculatorRenderArgs,
): Component {
const details = result.details;
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
if (result.isError) {
const header = renderStatusLine({ icon: "error", title: "Calc" }, uiTheme);
const renderedLines = [header, formatErrorMessage(textContent, uiTheme)];
return {
render() {
return renderedLines;
},
invalidate() {},
};
}
// Prefer structured details; fall back to parsing text content
let outputs = details?.results?.map(entry => `${entry.expression} = ${entry.output}`) ?? [];
if (outputs.length === 0 && textContent.trim()) {
const rawOutputs = textContent.split("\n").filter(line => line.trim().length > 0);
const expressions = args?.calculations?.map(calc => calc.expression) ?? [];
if (expressions.length === rawOutputs.length && expressions.length > 0) {
outputs = rawOutputs.map((output, index) => `${expressions[index]} = ${output}`);
} else {
outputs = rawOutputs;
}
}
if (outputs.length === 0) {
const header = renderStatusLine({ icon: "warning", title: "Calc" }, uiTheme);
const renderedLines = [header, formatEmptyMessage("No results", uiTheme)];
return {
render() {
return renderedLines;
},
invalidate() {},
};
}
const description = args?.calculations?.[0]?.expression
? truncateToWidth(args.calculations[0].expression, TRUNCATE_LENGTHS.TITLE)
: undefined;
const header = renderStatusLine(
{ icon: "success", title: "Calc", description, meta: [formatCount("result", outputs.length)] },
uiTheme,
);
let cached: RenderCache | undefined;
return {
render(width) {
const { expanded } = options;
const key = new Hasher().bool(expanded).u32(width).digest();
if (cached?.key === key) return cached.lines;
const treeLines = renderTreeList(
{
items: outputs,
expanded,
maxCollapsed: COLLAPSED_LIST_LIMIT,
itemType: "result",
renderItem: output => uiTheme.fg("toolOutput", output),
},
uiTheme,
);
const lines = [header, ...treeLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
cached = { key, lines };
return lines;
},
invalidate() {
cached = undefined;
},
};
},
mergeCallAndResult: true,
};
-4
View File
@@ -28,7 +28,6 @@ import { AstEditTool } from "./ast-edit";
import { AstGrepTool } from "./ast-grep";
import { BashTool } from "./bash";
import { BrowserTool } from "./browser";
import { CalculatorTool } from "./calculator";
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
import { DebugTool } from "./debug";
import { EvalTool } from "./eval";
@@ -69,7 +68,6 @@ export * from "./ast-edit";
export * from "./ast-grep";
export * from "./bash";
export * from "./browser";
export * from "./calculator";
export * from "./checkpoint";
export * from "./debug";
export * from "./eval";
@@ -286,7 +284,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
ask: AskTool.createIf,
debug: DebugTool.createIf,
eval: s => new EvalTool(s),
calc: s => new CalculatorTool(s),
ssh: loadSshTool,
github: GithubTool.createIf,
find: s => new FindTool(s),
@@ -455,7 +452,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
if (name === "web_search") return session.settings.get("web_search.enabled");
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
if (name === "search_tool_bm25") return discoveryActive;
if (name === "calc") return session.settings.get("calc.enabled");
if (name === "browser") return session.settings.get("browser.enabled");
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
if (name === "irc") {
@@ -16,7 +16,6 @@ import { astEditToolRenderer } from "./ast-edit";
import { astGrepToolRenderer } from "./ast-grep";
import { bashToolRenderer } from "./bash";
import { browserToolRenderer } from "./browser/render";
import { calculatorToolRenderer } from "./calculator";
import { debugToolRenderer } from "./debug";
import { evalToolRenderer } from "./eval";
import { findToolRenderer } from "./find";
@@ -54,7 +53,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
recipe: recipeToolRenderer as ToolRenderer,
debug: debugToolRenderer as ToolRenderer,
eval: evalToolRenderer as ToolRenderer,
calc: calculatorToolRenderer as ToolRenderer,
edit: editToolRenderer as ToolRenderer,
apply_patch: editToolRenderer as ToolRenderer,
find: findToolRenderer as ToolRenderer,
@@ -24,7 +24,6 @@ const allToolsSettings = Settings.isolated({
"lsp.enabled": true,
"inspect_image.enabled": true,
"web_search.enabled": true,
"calc.enabled": true,
"browser.enabled": true,
"checkpoint.enabled": true,
"irc.enabled": true,
@@ -205,7 +205,6 @@ describe("createTools", () => {
"web_search.enabled": false,
"browser.enabled": false,
"inspect_image.enabled": false,
"calc.enabled": false,
}),
});
const tools = await createTools(session);
@@ -219,7 +218,6 @@ describe("createTools", () => {
expect(names).not.toContain("web_search");
expect(names).not.toContain("browser");
expect(names).not.toContain("inspect_image");
expect(names).not.toContain("calc");
});
it("always includes resolve regardless of plan-mode setting", async () => {