From 2914334a2e3a94866c78908e535e78e0b41a7431 Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso <4096860+jkyberneees@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:32:37 +0200 Subject: [PATCH] fix: polish native tool descriptions for discoverability and turn efficiency --- cmd/odek/bg_tools.go | 11 +++++++---- cmd/odek/browser_tool.go | 9 +++------ cmd/odek/file_tool.go | 9 ++++----- cmd/odek/perf_tools.go | 6 +++--- cmd/odek/session_search_tool.go | 2 +- cmd/odek/shell.go | 5 ++--- cmd/odek/subagent_tool.go | 16 +++++++--------- cmd/odek/transcribe_tool.go | 4 ++-- cmd/odek/vision_tool.go | 2 +- cmd/odek/web_search_tool.go | 2 +- internal/memory/tool.go | 1 + 11 files changed, 32 insertions(+), 35 deletions(-) diff --git a/cmd/odek/bg_tools.go b/cmd/odek/bg_tools.go index 435b628a..1a052e9d 100644 --- a/cmd/odek/bg_tools.go +++ b/cmd/odek/bg_tools.go @@ -323,11 +323,12 @@ func (t *bgStartTool) Name() string { return "bg_start" } func (t *bgStartTool) Description() string { return `Start a shell command in the background and return immediately. -Use for long-running work: dev servers, watchers, fuzz runs, batch jobs. +Use for long-running work: builds, full test suites, dev servers, watchers, fuzz runs, batch jobs. The command runs detached from the conversation: you keep working while it runs, and a completion notice with the exit status and an output tail is -delivered to you automatically when it finishes (if notices are enabled). -If notices are disabled, poll with bg_status / bg_output instead. +drained into a later iteration when it finishes (if notices are enabled). +If notices are disabled, or your current turn depends on the result, poll +with bg_status / bg_output before ending the turn. timeout_seconds: optional kill timer; 0 or absent = run until session end (operator cap may clamp explicit values). Jobs are killed when the session or the process ends. Output is capped; retrieve it with bg_output.` @@ -415,7 +416,9 @@ func (t *bgStatusTool) Name() string { return "bg_status" } func (t *bgStatusTool) Description() string { return `Get the status of one background job: running/exited/failed/timeout/killed, exit code, duration, and output size. Returns {"status":"unknown"} for ids -that never existed, were started by another session, or died with a restart.` +that never existed, were started by another session, died with a restart, +or whose finished record was evicted (the oldest finished jobs are pruned +when the per-session record cap is exceeded).` } func (t *bgStatusTool) Schema() any { diff --git a/cmd/odek/browser_tool.go b/cmd/odek/browser_tool.go index 903ed5b5..997f6ee3 100644 --- a/cmd/odek/browser_tool.go +++ b/cmd/odek/browser_tool.go @@ -120,18 +120,15 @@ func (t *browserTool) checkRedirect(req *http.Request, via []*http.Request) erro func (t *browserTool) Name() string { return "browser" } func (t *browserTool) Description() string { - return `Navigate and interact with web pages. Supports four actions: + return `Navigate and interact with web pages. Four actions: navigate — Fetch a URL and extract page content + interactive elements snapshot — Return the current page's text view with ref IDs for elements click — Follow a link or interact with an element by ref ID back — Return to the previous page in navigation history -Note: Uses regex-based HTML parsing with NO JavaScript execution. Best for server-rendered HTML pages. SPAs and JS-heavy sites may return limited content. - -Use browser_navigate(url) first, then browser_snapshot() to see interactive -elements with their ref IDs (e.g. @e1, @e2), then browser_click(ref) to -follow links or interact with buttons.` +Typical flow: navigate(url), then snapshot() to get element ref IDs (e.g. @e1), then click(ref). +Note: regex-based HTML parsing with NO JavaScript execution. Best for server-rendered HTML pages; SPAs and JS-heavy sites may return limited content.` } // browserArgs holds all possible parameters for the browser tool. diff --git a/cmd/odek/file_tool.go b/cmd/odek/file_tool.go index 4a5f02b0..5c70cfa7 100644 --- a/cmd/odek/file_tool.go +++ b/cmd/odek/file_tool.go @@ -366,8 +366,7 @@ func (t *writeFileTool) Name() string { return "write_file" } func (t *writeFileTool) Description() string { return `Write content to a file, completely replacing existing content. Creates parent directories automatically. OVERWRITES the entire file. -Use patch for targeted edits. -CRITICAL: Use the EXACT path specified in the task. Do not simplify or drop directories from the path.` +Use patch for targeted edits; never simplify or drop directories from the path.` } func (t *writeFileTool) Schema() any { @@ -540,8 +539,9 @@ func (t *searchFilesTool) Description() string { Two modes: target="content" searches inside files for a regex pattern, target="files" finds files by glob pattern. Results are sorted by modification time (newest first). -For performance, ALWAYS use file_glob (e.g. '*.go', '*.py', '*.md') and a -narrow path — without file_glob, every file in the tree is scanned.` +For 2+ patterns at once, use multi_grep instead — one parallel pass. +Always pass file_glob ('*.go', '*.py', …) and a narrow path; without +file_glob every file in the tree is scanned.` } func (t *searchFilesTool) Schema() any { @@ -1552,7 +1552,6 @@ Zero-fork — pure Go filepath walk with no subprocess. Examples: glob(pattern="*.go") — all Go files in current directory glob(pattern="**/*.py") — all Python files recursively - glob(pattern="*.json", path="config/") — JSON files in config/ glob(pattern="test_*") — files starting with test_ Returns an array of {path, size, is_dir} for each match.` diff --git a/cmd/odek/perf_tools.go b/cmd/odek/perf_tools.go index 551fd7b7..1ef919cc 100644 --- a/cmd/odek/perf_tools.go +++ b/cmd/odek/perf_tools.go @@ -91,7 +91,7 @@ type batchPatchTool struct { func (t *batchPatchTool) Name() string { return "batch_patch" } func (t *batchPatchTool) Description() string { - return `Apply up to 10 find-replace edits across files in a single call. Edits are applied sequentially — this is NOT one atomic transaction: at the first failing edit the remaining edits are skipped (early-stop) and the edits already applied are kept. Each individual edit uses O_NOFOLLOW read + atomic temp+rename write, same as the patch tool.` + return `Apply up to 10 find-replace edits in one call — across one or more files, including several edits to the same file; prefer this over N sequential patch calls whenever you have more than one edit. Edits are applied sequentially — this is NOT one atomic transaction: at the first failing edit the remaining edits are skipped (early-stop) and the edits already applied are kept. Each individual edit uses O_NOFOLLOW read + atomic temp+rename write, same as the patch tool.` } type batchPatchArg struct { @@ -652,7 +652,7 @@ func (t *httpBatchTool) checkRedirect(req *http.Request, via []*http.Request) er func (t *httpBatchTool) Name() string { return "http_batch" } func (t *httpBatchTool) Description() string { - return `Fetch multiple URLs in parallel. Returns status code, content length, and error for each URL. Does NOT parse HTML — it's a lightweight parallel fetch for APIs, docs, and data files. Max 10 URLs per call.` + return `Check multiple URLs in parallel — returns HTTP status code, content length, and error per URL; response BODIES are NOT returned (read content with browser or shell instead). Best for link-health and availability checks, not content fetching. Max 10 URLs per call.` } type httpBatchReq struct { @@ -1327,7 +1327,7 @@ type multiGrepTool struct { func (t *multiGrepTool) Name() string { return "multi_grep" } func (t *multiGrepTool) Description() string { - return `Search for multiple regex patterns in parallel across files. Each pattern runs its own directory walk with bounded concurrency. Returns structured {pattern, path, line, content} results. Replaces N serial search_files calls. Directly targets the multi_search benchmark.` + return `Search for multiple regex patterns in parallel across files. Each pattern runs its own directory walk with bounded concurrency. Returns structured {pattern, path, line, content} results. Replaces N serial search_files calls.` } type grepMatch struct { diff --git a/cmd/odek/session_search_tool.go b/cmd/odek/session_search_tool.go index 9f97299e..70bf426c 100644 --- a/cmd/odek/session_search_tool.go +++ b/cmd/odek/session_search_tool.go @@ -27,7 +27,7 @@ func newSessionSearchTool(store *session.Store) *sessionSearchTool { func (t *sessionSearchTool) Name() string { return "session_search" } func (t *sessionSearchTool) Description() string { - return `Search and retrieve past agent sessions. Actions: list (recent sessions), search (semantic keyword search through full message content), get (full session by ID including ALL messages), find (sessions by task/title). Uses semantic vector search for the search action — it finds sessions whose conversation content is relevant to your query, even when titles don't match. Use OR between keywords for broad recall. + return `Search and retrieve past agent sessions. Actions: list (recent sessions), search (keyword search through full message content), get (full session by ID including ALL messages), find (sessions by task/title). Matching is thresholded keyword scoring — it finds sessions whose conversation content shares your query terms, even when titles don't match; use distinctive words from the original conversation, not paraphrases. Use OR between keywords for broad recall. IMPORTANT: After search returns matching sessions, use get (not search) to read the actual conversation content. get returns the full session_messages array with every user and assistant message.` } diff --git a/cmd/odek/shell.go b/cmd/odek/shell.go index 3638b3f1..1b4af02f 100644 --- a/cmd/odek/shell.go +++ b/cmd/odek/shell.go @@ -137,7 +137,7 @@ func (t *shellTool) Name() string { return "shell" } func (t *shellTool) Description() string { return `Run a shell command and return its output. -Use for: reading files, listing directories, running tests, building code, and git operations. +Use for builds, test suites, git operations, package management, and scripts that need a real process. For file inspection prefer the zero-fork tools: read_file, glob, tree, search_files, head_tail. In sandbox mode (--sandbox), commands run inside the Docker container with restricted permissions. In host mode (default), commands run with the same permissions as the odek process. @@ -145,8 +145,7 @@ Risk classes: safe, local_write, system_write, destructive, network_egress, code High-risk operations may prompt for approval (configurable via dangerous section in odek.json). The gate fails closed: an unrecognised command classifies as "unknown" and is denied by default. -Output is fully buffered: nothing is returned until the command finishes. For known long-running -commands (builds, test suites), set timeout_seconds explicitly so a stuck command fails fast.` +Output is fully buffered: nothing is returned until the command finishes; set timeout_seconds for known long-running commands so a stuck one fails fast. Work that would block the turn for minutes (builds, full test suites, dev servers, watchers) belongs in bg_start instead.` } func (t *shellTool) Schema() any { diff --git a/cmd/odek/subagent_tool.go b/cmd/odek/subagent_tool.go index 6564a703..b13ac605 100644 --- a/cmd/odek/subagent_tool.go +++ b/cmd/odek/subagent_tool.go @@ -180,25 +180,23 @@ func (t *delegateTasksTool) Description() string { Example: decomposing "build a REST API" into "create user model", "create auth middleware", "create route handlers". Key rules: -- Each sub-agent has a fresh context (no parent history) -- Sub-agents run in parallel up to the configured concurrency limit (subagent.max_concurrency, falling back to max_concurrency) +- Each sub-agent has a fresh context (no parent history) — pass everything it needs in goal/context +- Sub-agents run in parallel up to the configured concurrency cap - Sub-agents NEVER prompt for approvals — denied operations are listed in each result's denials array (tool/class/reason); escalate by performing the operation yourself or asking the user -- Sub-agents get a wall-clock budget (subagent.timeout_seconds, default 30m, hard max 30m) and an iteration budget (subagent.max_iterations, default 15) — it is told both at spawn and warned as it approaches them -- Trust is non-increasing downward: a child's effective trust is min(parent trust, declared trust_level) — an untrusted task tree cannot spawn trusted children -- Sub-agents can use all tools (shell, read/write files, etc.), capped by trust_level and max_risk -- Delegation depth is capped (subagent.max_depth, default 2) — do leaf work yourself when close to the cap +- Sub-agents get a wall-clock budget and an iteration budget, and are told both at spawn +- Trust is non-increasing downward: an untrusted task tree cannot spawn trusted children +- Delegation depth is capped — do leaf work yourself when close to the cap - After all complete, synthesize the results into a cohesive answer Result delivery — two channels per sub-agent: - Headline: the sub-agent's final answer, capped at ~2000 characters. Treat it as a status summary, not the full result; a trailing … means it was cut. -- Artifacts: deliverables the sub-agent wrote as files (it is instructed to stage anything larger than a headline in its task's artifact dir) are validated and listed under "artifacts:" — id, type, byte size, one-line summary. Text artifacts up to 32 KB are inlined in full; fetch larger ones with artifact_read(id). +- Artifacts: file deliverables are validated and listed under "artifacts:" — id, type, byte size, one-line summary. Only text/* artifacts ≤32 KB are inlined in full (JSON/binary are metadata-only); fetch anything else with artifact_read(id). - For artifact-heavy tasks (reports, audits, reviews, generated files), put it in ` + "`guidance`" + `: "Write the full deliverable as a flat file in your artifact dir; keep the final answer to a short headline." Then read file-backed artifacts with artifact_read before synthesizing. Output format per sub-agent (headline stays SHORT — status, artifact names, key decisions; the files carry the detail): - Status: built / blocked / failed, one line -- Artifact file names (omit when everything fit inline) - Key decisions made -- artifacts: file-backed deliverables (inlined when ≤32 KB; artifact_read otherwise)` +- artifacts: file-backed deliverables (inlined when text ≤32 KB; artifact_read otherwise)` } func (t *delegateTasksTool) Schema() any { diff --git a/cmd/odek/transcribe_tool.go b/cmd/odek/transcribe_tool.go index b8f08279..5feffe95 100644 --- a/cmd/odek/transcribe_tool.go +++ b/cmd/odek/transcribe_tool.go @@ -164,7 +164,7 @@ func newTranscribeTool(dc danger.DangerousConfig, tc config.TranscriptionConfig) func (t *transcribeTool) Name() string { return "transcribe" } func (t *transcribeTool) Description() string { - return `Transcribe an audio file to text using a local whisper model (whisper.cpp CLI). Returns transcribed text with segments and duration. Requires whisper CLI and a model file to be installed locally.` + return `Transcribe an audio file to text using a local whisper model (whisper.cpp CLI). Returns transcribed text with segments and duration. Requires whisper CLI and a model file to be installed locally; native audio input is WAV/MP3/FLAC — other containers are auto-converted via ffmpeg, so if conversion fails, supply WAV/MP3/FLAC directly instead of retrying.` } type transcribeArgs struct { @@ -193,7 +193,7 @@ func (t *transcribeTool) Schema() any { "properties": map[string]any{ "path": map[string]any{ "type": "string", - "description": "Path to the audio file (OGG, WAV, MP3, etc.).", + "description": "Path to the audio file (WAV, MP3, FLAC native; others are converted via ffmpeg).", }, "language": map[string]any{ "type": "string", diff --git a/cmd/odek/vision_tool.go b/cmd/odek/vision_tool.go index e05e4a8b..c4589674 100644 --- a/cmd/odek/vision_tool.go +++ b/cmd/odek/vision_tool.go @@ -193,7 +193,7 @@ func newVisionTool(dc danger.DangerousConfig, vc config.VisionConfig) *visionToo func (t *visionTool) Name() string { return "vision" } func (t *visionTool) Description() string { - return `Analyze an image or video file using MiniCPM-V 4.6, a local 1.3B multimodal model (llama-mtmd-cli). Images are described directly; videos are sampled into evenly-spaced frames and analyzed together. Supports JPEG, PNG, GIF, WebP, BMP for images and MP4, MOV, AVI, MKV, WebM for video. Requires llama-mtmd-cli and MiniCPM-V 4.6 model files (bundled in the Docker image).` + return `Analyze an image or video file using MiniCPM-V 4.6, a local 1.3B multimodal model (llama-mtmd-cli). Images are described directly; videos are sampled into evenly-spaced frames and analyzed together. Image formats: JPEG, PNG, GIF, WebP, BMP. Video formats: MP4, MOV, AVI, MKV, WebM — video analysis additionally requires ffmpeg and ffprobe in PATH; if video fails while images work, convert or extract frames instead of retrying. Requires llama-mtmd-cli and MiniCPM-V 4.6 model files (bundled in the Docker image).` } type visionArgs struct { diff --git a/cmd/odek/web_search_tool.go b/cmd/odek/web_search_tool.go index 1960e708..fbfdd2e7 100644 --- a/cmd/odek/web_search_tool.go +++ b/cmd/odek/web_search_tool.go @@ -84,7 +84,7 @@ func (t *webSearchTool) checkRedirect(req *http.Request, via []*http.Request) er func (t *webSearchTool) Name() string { return "web_search" } func (t *webSearchTool) Description() string { - return `Search the web via a self-hosted SearXNG metasearch instance. Returns ranked results (title, url, snippet, engine) plus any direct answers. Use this to find pages, then fetch the most relevant URLs with the browser or http_batch tools. Results come from external search engines and are treated as untrusted content.` + return `Search the web via a self-hosted SearXNG metasearch instance. Returns ranked results (title, url, snippet, engine) plus any direct answers. Use this to find pages, then fetch the most relevant URLs with the browser (http_batch returns response metadata only, not page content). Results come from external search engines and are treated as untrusted content.` } type webSearchArgs struct { diff --git a/internal/memory/tool.go b/internal/memory/tool.go index b0c3082b..87bc5300 100644 --- a/internal/memory/tool.go +++ b/internal/memory/tool.go @@ -71,6 +71,7 @@ func NewMemoryTool(mm *MemoryManager) *MemoryTool { func (t *MemoryTool) Name() string { return "memory" } func (t *MemoryTool) Description() string { return "Manage persistent memory across sessions: read, add, update, remove facts, consolidate related entries, or search past episode summaries. " + + "For finding past sessions by their conversation content, use session_search — memory targets curated facts and episodes, not full session transcripts. " + "You maintain the user/env fact files: when a target is at cap, remove or replace the lowest-value entries yourself — " + "records recoverable from git/GitHub (release notes, merged PRs) evict first; pointers to untracked local work evict last. " + "Use action=stats to check per-entry sizes and fill before writing."