751 lines
43 KiB
Go
751 lines
43 KiB
Go
// Package toolcatalog holds the JSON-schema tool definitions sent to
|
|
// the vLLM-hosted chat model on every chat request — the exact content of the monolith's
|
|
// Toolbox.Defs(), unchanged (the model has been prompted and tested
|
|
// against this precise wording; see agent.go's SystemPrompt for the
|
|
// companion prose). Presenting tool schemas is the agent service's job
|
|
// now, not the workspace service's — the workspace service only
|
|
// executes tools once the model has picked one (see
|
|
// internal/infrastructure/toolexec).
|
|
package toolcatalog
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"enkly/pkg/mathkit"
|
|
"enkly/pkg/skillkit"
|
|
"enkly/pkg/vllmkit"
|
|
)
|
|
|
|
// Defs returns every tool the agent may call, in the fixed order the
|
|
// monolith declared them.
|
|
func Defs() []vllmkit.ToolDef {
|
|
return []vllmkit.ToolDef{
|
|
{
|
|
Name: "read_file",
|
|
Description: "Read a text file in the project workspace, with 1-based line numbers prefixed (like `cat -n`). Large files are paginated: use offset/limit to page through them instead of guessing what's relevant.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{
|
|
"type": "string",
|
|
"description": "Path to the file, relative to the workspace root.",
|
|
},
|
|
"offset": map[string]any{
|
|
"type": "integer",
|
|
"description": "1-based line number to start reading from. Omit to start at line 1.",
|
|
},
|
|
"limit": map[string]any{
|
|
"type": "integer",
|
|
"description": "Maximum number of lines to return. Omit for the default (2000).",
|
|
},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
{
|
|
Name: "write_file",
|
|
Description: "Create a new file, or fully overwrite an existing one, with new content. For changes to an EXISTING file, prefer edit_file — it's cheaper and far less likely to accidentally drop unrelated content than rewriting the whole file.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{
|
|
"type": "string",
|
|
"description": "Path to the file, relative to the workspace root.",
|
|
},
|
|
"content": map[string]any{
|
|
"type": "string",
|
|
"description": "The full contents to write to the file.",
|
|
},
|
|
},
|
|
"required": []string{"path", "content"},
|
|
},
|
|
},
|
|
{
|
|
Name: "edit_file",
|
|
Description: "Make a precise, surgical edit to an existing text file by replacing one exact occurrence of old_string with new_string. old_string must match the file's current content exactly (read the file with read_file first, and include enough surrounding context to make it uniquely identify one location) and must not equal new_string. Fails loudly instead of guessing if old_string is missing or ambiguous, which is the whole point: it can't silently clobber the rest of the file the way write_file can. Returns a unified-diff preview of exactly what changed — check it before assuming the edit did what you intended.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{
|
|
"type": "string",
|
|
"description": "Path to the file, relative to the workspace root.",
|
|
},
|
|
"old_string": map[string]any{
|
|
"type": "string",
|
|
"description": "The exact text to find. Must appear exactly once in the file unless replace_all is true.",
|
|
},
|
|
"new_string": map[string]any{
|
|
"type": "string",
|
|
"description": "The text to replace it with.",
|
|
},
|
|
"replace_all": map[string]any{
|
|
"type": "boolean",
|
|
"description": "If true, replace every occurrence of old_string instead of requiring exactly one. Defaults to false.",
|
|
},
|
|
},
|
|
"required": []string{"path", "old_string", "new_string"},
|
|
},
|
|
},
|
|
{
|
|
Name: "multi_edit_file",
|
|
Description: "Apply several edit_file-style replacements to ONE file as a single atomic operation: every edit is validated (found, and unique unless its own replace_all is set) against the file as it would look after the preceding edits in the list, and if ANY edit fails, nothing is written — the file is left untouched, exactly as if none of the edits had been attempted. Prefer this over several separate edit_file calls when you already know multiple changes a single file needs (e.g. renaming a symbol at its definition and at each call site), since it's one round-trip instead of several and never leaves a file half-edited if a later edit in the batch turns out to be wrong. Returns a diff preview per edit, in order.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{
|
|
"type": "string",
|
|
"description": "Path to the file, relative to the workspace root.",
|
|
},
|
|
"edits": map[string]any{
|
|
"type": "array",
|
|
"description": "The edits to apply, in order. Each is the same shape as edit_file's arguments (minus path).",
|
|
"items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"old_string": map[string]any{"type": "string", "description": "The exact text to find."},
|
|
"new_string": map[string]any{"type": "string", "description": "The text to replace it with."},
|
|
"replace_all": map[string]any{"type": "boolean", "description": "If true, replace every occurrence of this edit's old_string. Defaults to false."},
|
|
},
|
|
"required": []string{"old_string", "new_string"},
|
|
},
|
|
},
|
|
},
|
|
"required": []string{"path", "edits"},
|
|
},
|
|
},
|
|
{
|
|
Name: "list_directory",
|
|
Description: "List files and subdirectories under a path in the project workspace, as a sorted tree (directories first). Use this to orient yourself in an unfamiliar project before reading or searching files.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{
|
|
"type": "string",
|
|
"description": "Directory to list, relative to the workspace root. Omit or use \".\" for the workspace root.",
|
|
},
|
|
"max_depth": map[string]any{
|
|
"type": "integer",
|
|
"description": "How many directory levels deep to recurse. Defaults to 3.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "glob_search",
|
|
Description: "Find files by name pattern (e.g. \"**/*.go\", \"internal/**/handlers/*.go\", \"*.md\"). `*` matches within one path segment, `**` matches across segments. Faster and more precise than list_directory when you know roughly what filename or extension you're looking for.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"pattern": map[string]any{
|
|
"type": "string",
|
|
"description": "Glob pattern, relative to the workspace root.",
|
|
},
|
|
},
|
|
"required": []string{"pattern"},
|
|
},
|
|
},
|
|
{
|
|
Name: "grep",
|
|
Description: "Search file contents across the workspace for a regular expression, returning matching file:line:text results. Use this for exact/lexical lookups (a symbol name, an error string, a TODO) — use search_codebase instead for fuzzy, conceptual ('where do we handle X') queries.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"pattern": map[string]any{
|
|
"type": "string",
|
|
"description": "RE2 regular expression to search for.",
|
|
},
|
|
"path_glob": map[string]any{
|
|
"type": "string",
|
|
"description": "Optional glob to restrict which files are searched, e.g. \"*.go\".",
|
|
},
|
|
"case_insensitive": map[string]any{
|
|
"type": "boolean",
|
|
"description": "Defaults to false.",
|
|
},
|
|
},
|
|
"required": []string{"pattern"},
|
|
},
|
|
},
|
|
{
|
|
Name: "write_todos",
|
|
Description: "Write or replace your working plan as a structured todo list, shown to the user as a checklist. Use it to break a nontrivial, multi-step request (more than ~3 steps, or anything touching several files) into tracked steps *before* starting work, and call it again to flip items to in_progress/completed as you go. Skip it for a single small, obvious change — it adds overhead, not clarity, on a one-step task.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"todos": map[string]any{
|
|
"type": "array",
|
|
"description": "The full current plan (this replaces any previous list, so include every item, not just the ones that changed).",
|
|
"items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"content": map[string]any{"type": "string"},
|
|
"status": map[string]any{
|
|
"type": "string",
|
|
"enum": []string{"pending", "in_progress", "completed"},
|
|
},
|
|
},
|
|
"required": []string{"content", "status"},
|
|
},
|
|
},
|
|
},
|
|
"required": []string{"todos"},
|
|
},
|
|
},
|
|
{
|
|
Name: "run_bash_command",
|
|
Description: "Run one command in the project workspace and return its combined stdout/stderr. Commands run directly WITHOUT a shell: no pipes, redirects, globbing, chaining or substitution (quote arguments containing special characters with single quotes). Only allowlisted programs run (go build/test/vet/fmt/list, git read-only subcommands, ls, cat, grep, find, gofmt); all paths must stay inside the workspace. After editing code, prefer running the relevant build/tests here over asserting the change is correct.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"command": map[string]any{
|
|
"type": "string",
|
|
"description": "A single command line, e.g. `go test ./...` or `grep -rn TODO .`.",
|
|
},
|
|
},
|
|
"required": []string{"command"},
|
|
},
|
|
},
|
|
{
|
|
Name: "search_codebase",
|
|
Description: "Semantic search over the indexed codebase using pgvector. Returns the most relevant code chunks for a natural-language, conceptual query.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"query": map[string]any{
|
|
"type": "string",
|
|
"description": "Natural-language description of the code you're looking for.",
|
|
},
|
|
},
|
|
"required": []string{"query"},
|
|
},
|
|
},
|
|
{
|
|
Name: "solve_ml_math",
|
|
Description: "Solve a concrete mathematics problem from the four pillars that come up when building, " +
|
|
"debugging or explaining ML code: linear algebra, calculus (derivatives/gradients/integrals), " +
|
|
"probability & statistics, numerical optimization, plus ML-specific primitives (activations, loss " +
|
|
"functions, least-squares linear regression). Computes an exact or numerically-verified answer " +
|
|
"locally — pure Go arithmetic, not the model's own mental math — so prefer this over working " +
|
|
"something out by hand whenever a request has an actual numeric or symbolic-in-form-but-numeric " +
|
|
"answer (\"what's the gradient of this loss at this point\", \"invert this matrix\", \"is this " +
|
|
"activation numerically stable\", \"fit a line through these points\"). Not for code generation or " +
|
|
"explaining a concept in prose — just for computing an answer. Supported \"operation\" values: " +
|
|
strings.Join(mathkit.Operations(), ", ") + ". Each operation's required parameters:\n" +
|
|
"- evaluate{expression, variables?}: arithmetic/function expression, e.g. \"sigmoid(w*x+b)\".\n" +
|
|
"- matrix_add/matrix_sub/matrix_mul{a, b}, matrix_transpose/matrix_determinant/matrix_inverse/" +
|
|
"matrix_rank/matrix_trace{a}: 2D arrays, e.g. a=[[1,2],[3,4]].\n" +
|
|
"- dot_product{a, b}, vector_norm{a, p?}: 1D arrays; p defaults to 2 (Euclidean).\n" +
|
|
"- dominant_eigen{a, iterations?}: largest-eigenvalue/eigenvector via power iteration.\n" +
|
|
"- derivative{expression, variable, at|value, h?}, gradient{expression, variables, at}, " +
|
|
"integral{expression, variable, from, to, steps?}: at is {\"name\": number, ...}.\n" +
|
|
"- gradient_descent{expression, variables, start, learning_rate, iterations?, tolerance?}: " +
|
|
"runs plain gradient descent and returns the trajectory.\n" +
|
|
"- newton_root{expression, variable, start, iterations?, tolerance?}: finds a root via Newton's method.\n" +
|
|
"- mean/variance/std_dev/median{data, population?}, covariance/correlation{x, y, population?}, " +
|
|
"z_score_normalize/min_max_normalize{data}, normal_pdf/normal_cdf{x, mean?, std_dev?}, " +
|
|
"binomial_pmf{k, n, p}.\n" +
|
|
"- sigmoid/relu/tanh{x}: x is a number or an array. softmax{x}: array. " +
|
|
"mse_loss/cross_entropy_loss{predictions, targets}. " +
|
|
"linear_regression{x, y}: x is a 2D feature matrix (rows=observations), y is the target vector; " +
|
|
"fits by ordinary least squares and reports coefficients, R², and residuals.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"operation": map[string]any{
|
|
"type": "string",
|
|
"description": "Which calculation to run. See the tool description for the full list and each one's required fields.",
|
|
},
|
|
"expression": map[string]any{
|
|
"type": "string",
|
|
"description": "An infix math expression for operations that take one (evaluate, derivative, gradient, integral, gradient_descent, newton_root). Supports + - * / ^, parentheses, variables, the constants pi/e, and functions sin/cos/tan/asin/acos/atan/sinh/cosh/tanh/exp/sqrt/abs/ln/log/log2/log10/floor/ceil/round/sigmoid/relu/sign/max/min/pow/mod/atan2.",
|
|
},
|
|
"variable": map[string]any{"type": "string", "description": "The single variable of differentiation/integration/root-finding (derivative, integral, newton_root)."},
|
|
"variables": map[string]any{
|
|
"description": "Either the ordered list of variable names for a multivariate operation (gradient, gradient_descent — an array of strings), OR a name->value object of extra bound variables an expression needs (evaluate, integral).",
|
|
},
|
|
"at": map[string]any{"type": "object", "description": "name->number bindings at which to evaluate a derivative/gradient, e.g. {\"x\": 1, \"y\": 2}."},
|
|
"value": map[string]any{"type": "number", "description": "Shorthand for \"at\" when derivative has exactly one variable."},
|
|
"h": map[string]any{"type": "number", "description": "Finite-difference step size for derivative/gradient. Omit for a sensible default."},
|
|
"from": map[string]any{"type": "number", "description": "Lower bound of a definite integral."},
|
|
"to": map[string]any{"type": "number", "description": "Upper bound of a definite integral."},
|
|
"steps": map[string]any{"type": "integer", "description": "Subintervals for integral (Simpson's rule). Omit for a sensible default."},
|
|
"start": map[string]any{"description": "Starting point: an array of numbers (gradient_descent, one per variable) or a single number (newton_root)."},
|
|
"learning_rate": map[string]any{"type": "number", "description": "Step size for gradient_descent. Required, must be positive."},
|
|
"iterations": map[string]any{"type": "integer", "description": "Max steps for gradient_descent/newton_root/dominant_eigen. Omit for a sensible default."},
|
|
"tolerance": map[string]any{"type": "number", "description": "Early-stop threshold for gradient_descent (gradient norm) or newton_root (|f(x)|)."},
|
|
"a": map[string]any{"description": "A matrix (2D array, e.g. [[1,2],[3,4]]) or vector (1D array), depending on the operation."},
|
|
"b": map[string]any{"description": "A second matrix or vector, same shape rules as \"a\"."},
|
|
"p": map[string]any{"type": "number", "description": "Which norm for vector_norm: 1, 2 (default), or a large number to approximate infinity."},
|
|
"data": map[string]any{"type": "array", "items": map[string]any{"type": "number"}, "description": "A 1D array of numbers for a statistics operation."},
|
|
"x": map[string]any{
|
|
"description": "Context-dependent: a number or array for sigmoid/relu/tanh/softmax/normal_pdf/normal_cdf, a data array for covariance/correlation, or a 2D feature matrix for linear_regression.",
|
|
},
|
|
"y": map[string]any{"description": "A data array for covariance/correlation, or the target vector for linear_regression."},
|
|
"population": map[string]any{
|
|
"type": "boolean",
|
|
"description": "variance/std_dev/covariance: true for the population formula (divide by n), false (default) for the unbiased sample formula (divide by n-1).",
|
|
},
|
|
"mean": map[string]any{"type": "number", "description": "Distribution mean for normal_pdf/normal_cdf. Defaults to 0."},
|
|
"std_dev": map[string]any{"type": "number", "description": "Distribution standard deviation for normal_pdf/normal_cdf. Defaults to 1."},
|
|
"k": map[string]any{"type": "integer", "description": "Number of successes for binomial_pmf."},
|
|
"n": map[string]any{"type": "integer", "description": "Number of trials for binomial_pmf."},
|
|
"predictions": map[string]any{"type": "array", "items": map[string]any{"type": "number"}, "description": "Model outputs for mse_loss/cross_entropy_loss."},
|
|
"targets": map[string]any{"type": "array", "items": map[string]any{"type": "number"}, "description": "Ground-truth values for mse_loss/cross_entropy_loss."},
|
|
},
|
|
"required": []string{"operation"},
|
|
},
|
|
},
|
|
{
|
|
Name: "read_office_skill",
|
|
Description: officeSkillToolDescription(),
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]any{
|
|
"type": "string",
|
|
"enum": skillkit.Names(),
|
|
"description": "Which skill to read. Start with \"overview\" if you're not sure which document format applies.",
|
|
},
|
|
},
|
|
"required": []string{"name"},
|
|
},
|
|
},
|
|
{
|
|
Name: "search_office_skills",
|
|
Description: "Semantically search the office-document skills (docx/xlsx/pptx/overview) for the specific " +
|
|
"sections most relevant to a question — e.g. \"can a cell hold a formula\" or \"how do I bold part " +
|
|
"of a paragraph\" — instead of reading a whole skill file to find one answer. Returns the top " +
|
|
"matching sections, each labeled with the skill and heading it came from, ranked by relevance. " +
|
|
"Prefer this over read_office_skill for a narrow, specific question; prefer read_office_skill when " +
|
|
"you want a skill's complete guidance before a multi-step document task.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"query": map[string]any{"type": "string", "description": "A specific question or short description of what you need to know."},
|
|
"skill": map[string]any{
|
|
"type": "string",
|
|
"enum": skillkit.Names(),
|
|
"description": "Optional: restrict the search to one skill. Omit to search across all of them.",
|
|
},
|
|
},
|
|
"required": []string{"query"},
|
|
},
|
|
},
|
|
// --- Document management (Word/Excel/PowerPoint), modeled on
|
|
// Claude Code's document-editing skills: the model only ever
|
|
// supplies plain structured fields (titles, paragraph text,
|
|
// cell values, bullet lines) — never raw OOXML/XML markup. All
|
|
// nine tools below are implemented by pkg/officekit, which does
|
|
// 100% of the actual XML generation/parsing server-side using
|
|
// Go's typed XML encoder (always well-formed, always correctly
|
|
// escaped). This is a deliberate safety property, not just a
|
|
// convenience: an earlier, Ollama-backed version of this service
|
|
// saw XML text round-tripped through a tool-call argument or tool
|
|
// result trigger "ollama error: XML syntax error ... element
|
|
// closed by" — Qwen's tool-calling chat template wrapped tool
|
|
// calls/results in its own XML-ish delimiters that Ollama parsed
|
|
// on its side, and a stray or truncated angle bracket in echoed
|
|
// content could break that parse. The vLLM OpenAI-compatible
|
|
// tool-calling wire format this service now speaks (pkg/vllmkit)
|
|
// carries tool calls/results as JSON instead, so that exact
|
|
// failure mode is gone, but keeping documents structured-in/
|
|
// structured-out remains the right call regardless of transport:
|
|
// the model never needs to emit markup at all. See
|
|
// agentrun/agent.go's compactToolHistory and vllmkit/client.go's
|
|
// IsToolCallParseError for the matching defensive fixes on
|
|
// whatever transport-level parse failures remain.
|
|
{
|
|
Name: "create_document",
|
|
Description: "Create a new Word document (.docx) in the workspace, either from a list of structured " +
|
|
"paragraphs (\"paragraphs\") or by converting an existing Markdown document (\"markdown\") straight " +
|
|
"into one — headings, body text, bullets. Fails if the path already exists; use edit_document to " +
|
|
"change one. The model never writes OOXML by hand: give plain text and a style per paragraph (or " +
|
|
"just Markdown source) and this tool renders a real, valid .docx file. For an ordinary document (a " +
|
|
"CV, cover letter, memo, report, README-style page), this description is everything you need — go " +
|
|
"straight to calling it, no read_office_skill round-trip required: with \"paragraphs\", lead with " +
|
|
"one heading1 for the title, use heading2/heading3 for section headers, bullet for genuinely " +
|
|
"list-shaped content (not as a stand-in for paragraph breaks), and normal for body text — split body " +
|
|
"text into real paragraphs by idea, not one giant block; with \"markdown\", just pass the source " +
|
|
"text and #/##/### headings, paragraphs and -/* lists map onto the same styles automatically. This " +
|
|
"tool can't yet do tables, images, headers/footers, or bold/italic applied to only part of a " +
|
|
"paragraph (bold/italic/underline apply to the whole paragraph; Markdown's inline **bold**/*italic* " +
|
|
"spans are flattened to plain text for the same reason); say so plainly if someone asks for one of " +
|
|
"those rather than approximating it. Reach for read_office_skill only if a request needs one of " +
|
|
"those unsupported features confirmed, edit_document's replace_text nuances, or Track Changes " +
|
|
"redlining (edit_document's *_tracked operations).",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Destination path, relative to the workspace root, ending in .docx."},
|
|
"paragraphs": map[string]any{
|
|
"type": "array",
|
|
"description": "The document's content, in order. Omit if using \"markdown\" instead.",
|
|
"items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"text": map[string]any{"type": "string", "description": "The paragraph's plain text."},
|
|
"style": map[string]any{"type": "string", "enum": []string{"normal", "heading1", "heading2", "heading3", "bullet"}, "description": "Defaults to \"normal\"."},
|
|
"bold": map[string]any{"type": "boolean", "description": "Bold the whole paragraph. Defaults to false; implied true for headings."},
|
|
"italic": map[string]any{"type": "boolean", "description": "Italicize the whole paragraph. Defaults to false."},
|
|
"underline": map[string]any{"type": "boolean", "description": "Underline the whole paragraph. Defaults to false."},
|
|
},
|
|
"required": []string{"text"},
|
|
},
|
|
},
|
|
"markdown": map[string]any{
|
|
"type": "string",
|
|
"description": "Alternative to \"paragraphs\": a complete Markdown document to convert directly into the .docx (headings, paragraphs, bullet/numbered lists). Use this instead of hand-building a paragraphs array when you already have — or would naturally write — Markdown source.",
|
|
},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
{
|
|
Name: "read_document",
|
|
Description: "Read a Word document (.docx) back as plain text, one line per paragraph (bullets prefixed \"- \"). Works on any standard .docx, including ones a user uploaded, though complex formatting, tables and images aren't represented — just the text.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Path to the .docx file, relative to the workspace root."},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
{
|
|
Name: "edit_document",
|
|
Description: "Make a structured change to an existing Word document. Plain edits: append_paragraph (add " +
|
|
"one new paragraph at the end, same fields as create_document's paragraphs) or replace_text (replace " +
|
|
"every literal occurrence of \"find\" with \"replace\" across the document's text, including text " +
|
|
"inside tables — the structured, markup-free analogue of edit_file's old_string/new_string; unlike " +
|
|
"edit_file every occurrence is replaced, and a phrase split across more than one formatting run by " +
|
|
"the document's own edit history may not be found as a single match). Redlining (Track Changes): " +
|
|
"append_paragraph_tracked and replace_text_tracked do the same edits but record them as a " +
|
|
"reviewable, Word-native tracked change (shown struck-through/underlined under Review > Track " +
|
|
"Changes) instead of rewriting the text in place — use these whenever someone asks for \"tracked " +
|
|
"changes\", \"redlining\", \"suggest an edit\", or a change that should go through review before it's " +
|
|
"final; accept_all_changes and reject_all_changes then resolve every pending tracked change in the " +
|
|
"document at once (there's no per-change accept/reject yet — say so if someone asks for that). " +
|
|
"Read the document first if you're not sure what's in it or whether it already has pending tracked " +
|
|
"changes (read_document reports them).",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Path to the .docx file, relative to the workspace root."},
|
|
"operation": map[string]any{"type": "string", "enum": []string{
|
|
"append_paragraph", "replace_text",
|
|
"append_paragraph_tracked", "replace_text_tracked",
|
|
"accept_all_changes", "reject_all_changes",
|
|
}},
|
|
"text": map[string]any{"type": "string", "description": "append_paragraph/append_paragraph_tracked: the new paragraph's text."},
|
|
"style": map[string]any{"type": "string", "enum": []string{"normal", "heading1", "heading2", "heading3", "bullet"}, "description": "append_paragraph/append_paragraph_tracked only. Defaults to \"normal\"."},
|
|
"bold": map[string]any{"type": "boolean", "description": "append_paragraph/append_paragraph_tracked only. Defaults to false; implied true for headings."},
|
|
"italic": map[string]any{"type": "boolean", "description": "append_paragraph/append_paragraph_tracked only. Defaults to false."},
|
|
"underline": map[string]any{"type": "boolean", "description": "append_paragraph/append_paragraph_tracked only. Defaults to false."},
|
|
"find": map[string]any{"type": "string", "description": "replace_text/replace_text_tracked: exact text to find."},
|
|
"replace": map[string]any{"type": "string", "description": "replace_text/replace_text_tracked: its replacement."},
|
|
"author": map[string]any{"type": "string", "description": "append_paragraph_tracked/replace_text_tracked only: the reviewer name recorded on the tracked change. Defaults to \"enkly agent\"."},
|
|
},
|
|
"required": []string{"path", "operation"},
|
|
},
|
|
},
|
|
{
|
|
Name: "create_spreadsheet",
|
|
Description: "Create a new Excel workbook (.xlsx) in the workspace, either as one sheet (\"rows\") or " +
|
|
"several (\"sheets\"). Each row is a list of cell values, top to bottom, left to right (row 1 = A1, " +
|
|
"B1, ...); a value starting with \"=\" is stored as a live Excel formula (e.g. \"=SUM(A1:A4)\"), " +
|
|
"everything else that parses as a plain number is stored as a numeric cell, and anything left over " +
|
|
"as text. Fails if the path already exists; use edit_spreadsheet to change individual cells of an " +
|
|
"existing one, or read_spreadsheet first to see what's there.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Destination path, relative to the workspace root, ending in .xlsx."},
|
|
"sheet_name": map[string]any{"type": "string", "description": "Single-sheet form only (with \"rows\"). Defaults to \"Sheet1\"."},
|
|
"rows": map[string]any{
|
|
"type": "array",
|
|
"description": "Single-sheet form: row-major grid of cell values, e.g. [[\"Name\",\"Score\"],[\"Alice\",\"92\"]]. Omit if using \"sheets\" instead.",
|
|
"items": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
|
},
|
|
"sheets": map[string]any{
|
|
"type": "array",
|
|
"description": "Multi-sheet form: a workbook with one tab per entry, in order. Use this instead of \"rows\"/\"sheet_name\" when the workbook needs more than one sheet.",
|
|
"items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]any{"type": "string", "description": "This sheet's tab name."},
|
|
"rows": map[string]any{
|
|
"type": "array",
|
|
"description": "This sheet's row-major grid of cell values.",
|
|
"items": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
|
},
|
|
},
|
|
"required": []string{"rows"},
|
|
},
|
|
},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
{
|
|
Name: "read_spreadsheet",
|
|
Description: "Read one sheet of an Excel workbook (.xlsx) back as a row-major text table. Defaults to " +
|
|
"the first sheet; when the workbook has more than one, the result's first line names the sheet " +
|
|
"read and lists every sheet's name so you can ask for a different one. Works on any standard " +
|
|
".xlsx, including ones a user uploaded.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Path to the .xlsx file, relative to the workspace root."},
|
|
"sheet": map[string]any{"type": "string", "description": "Which sheet to read: a 1-based index (\"2\") or a sheet name. Omit for the first sheet."},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
{
|
|
Name: "edit_spreadsheet",
|
|
Description: "Write a sparse set of individual cells into one sheet of an existing Excel workbook, " +
|
|
"by A1-style reference (e.g. \"B12\"). Leaves every other cell untouched; a reference past the " +
|
|
"current grid extends it. Read the spreadsheet first if you need to know what's already there or " +
|
|
"what the other sheets are called.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Path to the .xlsx file, relative to the workspace root."},
|
|
"sheet": map[string]any{"type": "string", "description": "Which sheet to write to: a 1-based index (\"2\") or a sheet name. Omit for the first sheet."},
|
|
"cells": map[string]any{
|
|
"type": "array",
|
|
"description": "The cells to write.",
|
|
"items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"cell": map[string]any{"type": "string", "description": "A1-style reference, e.g. \"B12\"."},
|
|
"value": map[string]any{"type": "string", "description": "New value. A leading \"=\" stores a live formula (e.g. \"=SUM(B2:B11)\"); otherwise stored as a number if it parses as one, else as text."},
|
|
},
|
|
"required": []string{"cell", "value"},
|
|
},
|
|
},
|
|
},
|
|
"required": []string{"path", "cells"},
|
|
},
|
|
},
|
|
{
|
|
Name: "create_presentation",
|
|
Description: "Create a new PowerPoint deck (.pptx) in the workspace from a list of slides, each a title " +
|
|
"plus flat bullet lines — no layouts or manual positioning to specify. Fails if the path already " +
|
|
"exists; use edit_presentation to add or change a slide afterward.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Destination path, relative to the workspace root, ending in .pptx."},
|
|
"slides": map[string]any{
|
|
"type": "array",
|
|
"description": "The deck's slides, in order.",
|
|
"items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"title": map[string]any{"type": "string"},
|
|
"bullets": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"required": []string{"path", "slides"},
|
|
},
|
|
},
|
|
{
|
|
Name: "read_presentation",
|
|
Description: "Read a PowerPoint deck (.pptx) back as a list of slides (title + bullets), in order. Works on any standard .pptx, including ones a user uploaded, though complex layouts/images aren't represented.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Path to the .pptx file, relative to the workspace root."},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
{
|
|
Name: "edit_presentation",
|
|
Description: "Either add_slide (append one new slide at the end — same title/bullets shape as " +
|
|
"create_presentation's slides) or update_slide (replace one existing slide's title/bullets by its " +
|
|
"1-based index) in an existing PowerPoint deck. Read the presentation first if you're not sure how " +
|
|
"many slides it has.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Path to the .pptx file, relative to the workspace root."},
|
|
"operation": map[string]any{"type": "string", "enum": []string{"add_slide", "update_slide"}},
|
|
"index": map[string]any{"type": "integer", "description": "update_slide only: 1-based slide number to replace."},
|
|
"title": map[string]any{"type": "string"},
|
|
"bullets": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
|
},
|
|
"required": []string{"path", "operation"},
|
|
},
|
|
},
|
|
{
|
|
Name: "read_pdf",
|
|
Description: "Extract the plain text content of a PDF file, best-effort — data extraction from PDFs, " +
|
|
"for reading an uploaded report/contract/paper rather than producing one. Works well on PDFs with a " +
|
|
"real embedded text layer (almost anything produced by Word, LibreOffice, a browser's \"Print to " +
|
|
"PDF\", Pandoc, or similar tools). It cannot do OCR: a scanned or image-only PDF will come back with " +
|
|
"an error saying so, not fabricated text. Layout, tables, columns, and images are not represented — " +
|
|
"only the words, roughly in reading order, with best-effort line breaks; don't rely on this for a " +
|
|
"document's exact formatting or table structure.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string", "description": "Path to the .pdf file, relative to the workspace root."},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
{
|
|
Name: "dispatch_subagent",
|
|
Description: "Launch an autonomous, read-only sub-agent (modeled after Claude Code's Task tool) to investigate " +
|
|
"the workspace for you and report back. Give it one self-contained, specific task description — it has its " +
|
|
"own fresh context and only list_directory, glob_search, grep, read_file and search_codebase (no write_file, " +
|
|
"edit_file, multi_edit_file or run_bash_command: it can never change anything). It explores on its own for up " +
|
|
"to a few rounds and returns a single written report; everything it reads along the way stays in ITS context, " +
|
|
"not yours. Reach for this instead of a long manual chain of your own search/read calls when a question is " +
|
|
"broad or open-ended (\"find every place X happens and summarize the pattern\", \"map out how auth flows " +
|
|
"through this service\", \"is there already code that does Y?\") — one dispatch_subagent call often replaces a " +
|
|
"dozen of your own tool calls and keeps your own context focused on deciding what to do with the answer. " +
|
|
"Don't use it for a single targeted lookup you could do yourself in one or two calls, and never for anything " +
|
|
"that needs to write or run something — it can't.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"task": map[string]any{
|
|
"type": "string",
|
|
"description": "A precise, self-contained description of what to investigate and what the report should " +
|
|
"cover. The sub-agent has no memory of this conversation beyond this one string, so include every " +
|
|
"detail it needs (which directory or feature, what you already know, what you want answered).",
|
|
},
|
|
},
|
|
"required": []string{"task"},
|
|
},
|
|
},
|
|
{
|
|
Name: "dispatch_workflow",
|
|
Description: "Fan a large task out into many independent sub-agents running IN PARALLEL (modeled after " +
|
|
"Claude's dynamic multi-agent orchestration), then collect, verify and summarize what came back — the " +
|
|
"tool to reach for on a large-scale, mechanically-repeated job like a codebase migration (\"rename this " +
|
|
"API across every service\", \"add a context.Context first param to every exported func in these 40 " +
|
|
"files\", \"port every handler in internal/old to the new router\"), not a single investigation " +
|
|
"(use dispatch_subagent for that). Give it a short list of self-contained steps; each one runs as its " +
|
|
"own read-only sub-agent, with the same tool access as dispatch_subagent (it investigates and reports — " +
|
|
"it does not write or run anything, same as dispatch_subagent, so plan the actual edits as a follow-up " +
|
|
"once you've seen every step's findings, exactly the way you'd use any other investigation's report). " +
|
|
"Steps with no listed dependencies start immediately and run concurrently, bounded by this deployment's " +
|
|
"configured parallelism cap; a step that lists depends_on waits for those steps to finish first, so you " +
|
|
"can express \"investigate every call site\" as 40 fully-parallel steps while still sequencing a final " +
|
|
"\"cross-check the findings against each other\" step after them. Every step's report is automatically " +
|
|
"checked against its own task description (did it actually answer what was asked, or come back empty/" +
|
|
"off-topic) before you see the results, so a weak report is flagged for you to re-dispatch or handle " +
|
|
"yourself instead of silently passing as done — this is the real-time verification and plan adjustment " +
|
|
"loop: look at which steps verified cleanly and which didn't, and adjust what you do next accordingly " +
|
|
"rather than assuming every step succeeded. Not available on every plan — if it's not enabled for this " +
|
|
"account, the tool call itself will say so; fall back to dispatch_subagent (sequential, same " +
|
|
"investigative power, just one step at a time) or to doing the work directly.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"goal": map[string]any{
|
|
"type": "string",
|
|
"description": "One or two sentences on the overall objective these steps serve — included in each " +
|
|
"step's own task so no sub-agent loses the forest for the trees.",
|
|
},
|
|
"steps": map[string]any{
|
|
"type": "array",
|
|
"description": "The independent (or dependency-ordered) units of work to run, 2 to several hundred.",
|
|
"items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"id": map[string]any{
|
|
"type": "string",
|
|
"description": "A short, unique identifier for this step (e.g. \"svc-billing\", \"file-12\"), referenced by other steps' depends_on and by the final summary.",
|
|
},
|
|
"task": map[string]any{
|
|
"type": "string",
|
|
"description": "A precise, self-contained description of this one step's work and what its report should cover — same guidance as dispatch_subagent's task argument.",
|
|
},
|
|
"depends_on": map[string]any{
|
|
"type": "array",
|
|
"description": "IDs of steps that must finish before this one starts. Omit for a step with no dependencies (the common case — most migration steps are independent of each other).",
|
|
"items": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
"required": []string{"id", "task"},
|
|
},
|
|
},
|
|
},
|
|
"required": []string{"goal", "steps"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// officeSkillToolDescription builds read_office_skill's description
|
|
// from the currently-embedded skill files, so the catalog and the
|
|
// skills (pkg/skillkit/skills/*.md) can never drift apart — adding or
|
|
// renaming a skill file is all that's needed to change what the model
|
|
// sees here, no second edit to keep in sync.
|
|
func officeSkillToolDescription() string {
|
|
var b strings.Builder
|
|
b.WriteString("Read one of enkly's office-document skills: short notes on how to use the Word/Excel/" +
|
|
"PowerPoint tools well — conventions, best practices, and what each one honestly can't do yet. The " +
|
|
"create_document/create_spreadsheet/create_presentation tool descriptions already cover the basics " +
|
|
"needed for an ordinary document, so this is optional, not a required first step — skip it for a " +
|
|
"straightforward request (a CV, a simple report, a one-sheet table of numbers) and go straight to the " +
|
|
"create_*/edit_* tool; reach for this before anything that leans on a format's sharper edges (Excel " +
|
|
"formulas or a multi-sheet workbook, a multi-slide PowerPoint deck, edit_document's replace_text run- " +
|
|
"splitting caveat, redlining with Track Changes, or converting Markdown straight into a .docx) or when " +
|
|
"you're not sure a feature someone asked for is even supported. Start with the " +
|
|
"\"overview\" skill if you're not sure which format a request needs. No need to re-read a skill you've " +
|
|
"already read earlier in this same conversation. Available skills:\n")
|
|
for _, s := range skillkit.List() {
|
|
fmt.Fprintf(&b, "- %s: %s\n", s.Name, s.Description)
|
|
}
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
// subagentToolNames is the read-only whitelist offered to a
|
|
// dispatch_subagent run: everything that can observe the workspace,
|
|
// nothing that can change it. toolexec.NewRestricted enforces this
|
|
// same whitelist at execution time — belt and suspenders, since a
|
|
// tool catalog only controls what the model is *offered*, not what it
|
|
// could still try to call.
|
|
var subagentToolNames = map[string]bool{
|
|
"read_file": true,
|
|
"list_directory": true,
|
|
"glob_search": true,
|
|
"grep": true,
|
|
"search_codebase": true,
|
|
"read_document": true,
|
|
"read_spreadsheet": true,
|
|
"read_presentation": true,
|
|
"read_office_skill": true,
|
|
"search_office_skills": true,
|
|
"read_pdf": true,
|
|
}
|
|
|
|
// SubagentDefs returns the read-only subset of Defs() handed to a
|
|
// dispatch_subagent run.
|
|
func SubagentDefs() []vllmkit.ToolDef {
|
|
all := Defs()
|
|
out := make([]vllmkit.ToolDef, 0, len(subagentToolNames))
|
|
for _, d := range all {
|
|
if subagentToolNames[d.Name] {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// IsSubagentTool reports whether name is in the read-only whitelist —
|
|
// used by toolexec.restrictedExecutor so the enforcement lives next to
|
|
// the single source of truth for the whitelist itself.
|
|
func IsSubagentTool(name string) bool {
|
|
return subagentToolNames[name]
|
|
}
|