//! Built-in tool guides for the model-visible tool surface.
use std::sync::Arc;
use serde_json::json;
use super::{ToolCategory, ToolExample, ToolGuide, ToolGuideSpec};
pub const BUILTIN_GUIDE_NAMES: [&str; 19] = [
"exec_command",
"write_stdin",
"Edit",
"ExitPlanMode",
"GetFileInfo",
"Glob",
"Grep",
"session_note",
"Plan",
"Read",
"request_permissions",
"Sleep",
"Task",
"update_goal",
"loop_control",
"ViewImage",
"WebFetch",
"Workspace",
"Write",
];
pub fn builtin_tool_guide(tool_name: &str) -> Option<Arc<dyn ToolGuide>> {
builtin_guide_spec(tool_name).map(|guide| Arc::new(guide) as Arc<dyn ToolGuide>)
}
pub fn builtin_guide_spec(tool_name: &str) -> Option<ToolGuideSpec> {
match tool_name {
// apply_patch is now an alias for Edit; return the Edit guide with
// the alias name so existing callers still see relevant documentation.
"apply_patch" => Some(guide(
"apply_patch",
ToolCategory::FileWriting,
"Use for patch-only in-place updates using SEARCH/REPLACE blocks, aligned with Edit patch mode.",
"Do not use before Read on existing files; do not use for full-file rewrites; do not mix with old_string/new_string style arguments.",
&["Read", "Edit", "Write"],
vec![example(
"Apply a patch block",
json!({"file_path":"/workspace/project/src/main.rs","patch":"<<<<<<< SEARCH\nlet v = 1;\n=======\nlet v = 2;\n>>>>>>> REPLACE"}),
"Use when you want a dedicated patch tool call instead of Edit.",
)],
)),
"Read" => Some(guide(
"Read",
ToolCategory::FileReading,
"Use for reading file contents, especially before Edit/Write on existing files.",
"Do not use for writing or cross-project search; do not substitute exec_command cat/head/tail for this.",
&["Edit", "Write", "Grep"],
vec![example(
"Read a config file",
json!({"file_path":"/workspace/project/config.toml"}),
"Use before making any edits.",
)],
)),
"Write" => Some(guide(
"Write",
ToolCategory::FileWriting,
"Use for creating files or replacing full file contents.",
"Do not use for small in-place updates; prefer Edit after Read.",
&["Read", "Edit"],
vec![example(
"Write a full file",
json!({"file_path":"/workspace/project/.env.example","content":"API_KEY=\n"}),
"Suitable for full replacements.",
)],
)),
"Edit" => Some(guide(
"Edit",
ToolCategory::FileWriting,
"Use for precise in-place updates in existing files. Prefer patch mode with SEARCH/REPLACE blocks and enough context to target unique locations.",
"Do not use before Read on existing files; do not use for full-file rewrites; do not use ambiguous SEARCH blocks that match multiple locations.",
&["Read", "Write"],
vec![
example(
"Patch a specific block",
json!({"file_path":"/workspace/project/src/main.rs","patch":"<<<<<<< SEARCH\nfn b() {\n let v = 1;\n}\n=======\nfn b() {\n let v = 2;\n}\n>>>>>>> REPLACE"}),
"Preferred for repeated code because context disambiguates the target.",
),
example(
"Legacy single replacement",
json!({"file_path":"/workspace/project/src/main.rs","old_string":"foo","new_string":"bar"}),
"Use only when old_string is known to match exactly once.",
),
example(
"Legacy replacement with line hint",
json!({"file_path":"/workspace/project/src/main.rs","old_string":"value = 1","new_string":"value = 2","line_number":42}),
"Use line_number when old_string may appear in multiple places.",
),
],
)),
"Glob" => Some(guide(
"Glob",
ToolCategory::CodeSearch,
"Find files by glob pattern before deeper content search.",
"Do not use for content search; avoid unbounded root patterns like **/* without narrowing path/pattern.",
&["Grep", "Read"],
vec![example(
"Find all Rust files",
json!({"pattern":"**/*.rs","path":"/workspace/project"}),
"Use before Grep when scope is unknown.",
)],
)),
"Grep" => Some(guide(
"Grep",
ToolCategory::CodeSearch,
"Search project contents using regex; start with files_with_matches then narrow with Read/content mode.",
"Do not run broad content or multiline searches across the full workspace; always narrow with path/glob/type first.",
&["Glob", "Read"],
vec![example(
"Find function usages",
json!({"pattern":"execute_with_context","glob":"**/*.rs","output_mode":"files_with_matches","head_limit":200}),
"First identify candidate files, then use Read or scoped content mode.",
)],
)),
"exec_command" => Some(guide(
"exec_command",
ToolCategory::CommandExecution,
"Run terminal commands (build/test/git/npm/docker/gh). yield_time_ms waits up to 250–30000 ms (default 10000). Fast commands return output and exit_code directly; longer commands return status=running and session_id. Continue work or pull incremental output and final status with write_stdin. Results normally remain pull-only; when the runtime explicitly hands a quiet command to background monitoring, its terminal result returns automatically and you can continue the conversation while it runs. Output is bounded by max_output_tokens (default 2000, maximum 8192); output_omitted_bytes reports discarded output.",
"Do not use for file reads/edits/search when Read/Edit/Write/Glob/Grep can handle it, or shell echo/printf to communicate with the user. Expect automatic results only after an explicit runtime background handoff; do not use legacy BashOutput for an exec session. interactive=true supplies a stdin pipe, not a PTY; terminal-only programs may not work.",
&["write_stdin"],
vec![example(
"Start tests with a short initial wait",
json!({"cmd":"cargo test","yield_time_ms":1000,"max_output_tokens":2000}),
"Inspect exit_code when completed; save a returned session_id and poll with write_stdin when running.",
)],
)),
"write_stdin" => Some(guide(
"write_stdin",
ToolCategory::CommandExecution,
"Pull output and completion from an exec_command session owned by this conversation. Use chars=\"\" to wait/poll; yield_time_ms is bounded to 250–30000 ms (default 10000). Running results retain session_id; final results omit it and include exit_code when available. Running polling metadata reports state, elapsed_ms, output_idle_ms, consecutive_empty_readouts and exit_observed. False exit_observed means no exit has been observed, not verified liveness; true still waits for terminal output capture. After repeated empty readouts and sustained silence, a polling advisory asks you to do useful work or inspect one command-owned progress signal before polling again. Continue waiting when appropriate, or report a concrete blocker, retaining the handle for final status. In a Human-controlled Root, repeated unchanged observations produce bounded runtime feedback to this model. Decide whether the last evidence supports intentional polling or a stalled approach; use a real progress signal or change method. When durable background resumption is available, repeated quiet polling transfers the exact command to background monitoring: the conversation remains usable and its observed terminal result is delivered automatically. Otherwise finite diagnostic recovery ends with an incomplete-progress notice without a new Human question or cancelling the command. Handles are local to this Host; unread terminal results keep their existing five-minute expiry after completion, including after a bounded diagnostic run ends. Output is incremental and bounded by max_output_tokens (default 2000, maximum 8192); output_omitted_bytes reports discarded bytes. For interactive=true sessions, nonempty chars writes exact bytes to the stdin pipe; include your own newline and use eof=true to close stdin. A lone Ctrl-C (\\u0003) stops the process.",
"Do not use a conversation ID or legacy Bash ID as session_id. Do not blindly repeat identical empty polls, infer completion from silence or an existing report, or stop/restart a command only because it is quiet. Do not poll a finished session or wait for an automatic completion message. Input requires an interactive stdin pipe and does not provide PTY terminal behavior.",
&["exec_command"],
vec![
example(
"Pull a command's next output and exit status",
json!({"session_id":"<session_id-from-exec_command>","chars":"","yield_time_ms":1000}),
"While status=running, use polling progress and follow any advisory before another empty poll. Preserve the handle until final status; a nonzero exit_code is a command outcome to inspect.",
),
example(
"Answer an interactive prompt and close input",
json!({"session_id":"<session_id-from-exec_command>","chars":"yes\n","eof":true}),
"The command must have been started with interactive=true; bytes are sent exactly as supplied.",
),
],
)),
// Retained for explicit host-side legacy lookups, not default model guides.
"Bash" => Some(guide(
"Bash",
ToolCategory::CommandExecution,
"Run terminal commands (build/test/git/npm/docker/gh), optionally in background — a backgrounded command runs detached and notifies you with its result when it finishes, so you can keep working.",
"Do not use for file reads/edits/search when Read/Edit/Write/Glob/Grep can handle it; do not use shell echo/printf to communicate with the user; do not sit polling BashOutput to wait for a background command — the completion notification comes to you.",
&["BashOutput", "KillShell"],
vec![example(
"Run tests",
json!({"command":"cargo test","timeout":120000}),
"Use for build/test/CLI operations.",
)],
)),
"BashInput" => Some(guide(
"BashInput",
ToolCategory::CommandExecution,
"Send input to the stdin of an interactive background shell (spawned with Bash interactive=true) to answer a prompt, or close stdin (eof:true) to let a stdin-consumer finish.",
"Do not use without a bash_id from an interactive Bash shell; the shell must have been spawned with interactive=true (piped stdin), otherwise this returns an error. The input is written as UTF-8 bytes; eof:true sends end-of-input so a consumer that reads until EOF (cat/sort/REPL) terminates instead of hanging.",
&["Bash", "BashOutput"],
vec![
example(
"Answer a prompt",
json!({"bash_id":"<bash_id-from-Bash-interactive>","input":"y"}),
"Use when a background command is waiting for input (e.g. a confirmation prompt).",
),
example(
"Send EOF so a consumer finishes",
json!({"bash_id":"<bash_id>","input":"data","eof":true}),
"Close stdin after writing so a command that reads until EOF terminates instead of running until killed.",
),
],
)),
"BashOutput" => Some(guide(
"BashOutput",
ToolCategory::CommandExecution,
"Read the full/incremental output of a background shell on demand.",
"Do not poll a background shell to wait for it — you are notified automatically when it completes (with its exit status and an output tail). Use BashOutput only when you want its output BEFORE that notification arrives; needs a bash_id from Bash.",
&["Bash", "KillShell"],
vec![example(
"Inspect output early",
json!({"bash_id":"abc"}),
"Use to check on a still-running shell; you don't need it just to learn a shell finished.",
)],
)),
"KillShell" => Some(guide(
"KillShell",
ToolCategory::CommandExecution,
"Terminate a background shell.",
"Do not use for foreground commands; use the ID returned by Bash(run_in_background=true), not chat session_id.",
&["Bash", "BashOutput"],
vec![example(
"Stop runaway process",
json!({"shell_id":"<bash_id-from-Bash>"}),
"Use when process should no longer run.",
)],
)),
"Task" => Some(guide(
"Task",
ToolCategory::TaskManagement,
"Create or update the shared task list for the current root session tree (root + child sessions share the same task list).",
"Do not use for trivial one-step requests.",
&["Plan"],
vec![example(
"Update shared task statuses",
json!({"tasks":[{"content":"Run tests","status":"in_progress","activeForm":"Running tests"}]}),
"Keep exactly one item in_progress whenever possible.",
)],
)),
"Plan" => Some(guide(
"Plan",
ToolCategory::TaskManagement,
"Delegate a complex planning request to one runtime-enforced read-only planner child. The root remains the normal orchestrator and resumes automatically with the planner result.",
"Do not use for a trivial lookup or as a substitute for implementation after the approach is already clear.",
&["Task", "SubAgent"],
vec![example(
"Delegate an implementation plan",
json!({"task":"Inspect the current authentication flow and return an ordered migration plan with affected files, tests, risks, and explicit non-goals.","title":"Plan authentication migration","fork_last_messages":2}),
"The planner runs read-only; the parent waits internally and resumes without a mode-switch question.",
)],
)),
"ExitPlanMode" => Some(guide(
"ExitPlanMode",
ToolCategory::UserInteraction,
"Ask for confirmation before leaving plan mode.",
"Do not use if implementation can proceed directly.",
&["Task"],
vec![example(
"Plan complete",
json!({"plan":"1. Do A\n2. Do B"}),
"Use after producing a concrete implementation plan.",
)],
)),
// FileExists guide moved next to GetFileInfo (see below).
"ViewImage" => Some(guide(
"ViewImage",
ToolCategory::FileReading,
"Load a local raster image for visual inspection. Vision-capable main models receive multimodal image content. When the main model has Vision disabled, Bamboo uses the explicitly configured Vision model and returns a labeled description as untrusted tool data. This fallback applies only to this tool. Direct conversation image uploads to a main model without Vision remain rejected, even when a Vision model is configured. Missing or unsupported Vision configuration is an actionable tool error. The main model is unchanged.",
"Do not use for text files, directories, SVG/vector files, or images larger than 10 MB; use Read for textual content.",
&["Read", "GetFileInfo"],
vec![example(
"Inspect a local screenshot",
json!({"path":"/workspace/project/artifacts/screenshot.png"}),
"Supports PNG, JPEG, GIF, and WebP content and detects the format from its bytes rather than trusting the extension.",
)],
)),
"WebFetch" => Some(guide(
"WebFetch",
ToolCategory::CommandExecution,
"Fetch a webpage by URL when you need cleaned page text from a known target.",
"Do not use for broad discovery queries.",
&[],
vec![example(
"Fetch a target page",
json!({"url":"https://target-host/path","prompt":"Extract setup steps"}),
"The prompt field is context for downstream handling; WebFetch itself returns cleaned text + metadata.",
)],
)),
// GetCurrentDir is now an alias for Workspace (get mode)
"GetCurrentDir" => Some(guide(
"GetCurrentDir",
ToolCategory::CommandExecution,
"Retrieve the session's current workspace directory. (Alias for Workspace with no path argument.)",
"Do not use when absolute paths are already known.",
&["Workspace", "exec_command", "Read"],
vec![example(
"Inspect working directory",
json!({}),
"Useful before commands or file ops that rely on relative paths.",
)],
)),
"GetFileInfo" => Some(guide(
"GetFileInfo",
ToolCategory::FileReading,
"Check if a file/dir exists and read metadata (type, size, modified time) without loading content. Returns {exists: false} for missing paths, so this also covers existence checks.",
"Do not use when you need actual content; use Read instead.",
&["Read"],
vec![example(
"Check metadata before processing",
json!({"path":"/workspace/project/logs/app.log"}),
"Use to branch behavior based on file type/size.",
)],
)),
// FileExists is now an alias for GetFileInfo
"FileExists" => Some(guide(
"FileExists",
ToolCategory::FileReading,
"Check quickly whether a path exists before reading, editing, or writing conditionally. (Alias for GetFileInfo)",
"Do not use to inspect file content or metadata details.",
&["GetFileInfo", "Read", "Write"],
vec![example(
"Guard before write",
json!({"path":"/workspace/project/.env"}),
"Use as a fast existence probe before deciding create vs update.",
)],
)),
"session_note" | "memory_note" => Some(guide(
if tool_name == "memory_note" {
"memory_note"
} else {
"session_note"
},
ToolCategory::TaskManagement,
"Store durable session-scoped notes and retrieve them across turns. Use it for local context, user preferences, constraints, and compression-resistant reminders within the current workstream.",
"Do not store secrets/tokens, one-turn scratch text, or use it as the primary long-term knowledge base.",
&["Task", "session_history_current"],
vec![
example(
"Persist a durable session constraint",
json!({"action":"append","content":"User prefers pnpm and strict TypeScript."}),
"Use append for stable session/workstream facts; use replace to compress long notes.",
),
example(
"Store notes for a specific topic",
json!({"action":"append","topic":"backend-api","content":"REST endpoints finalized: /users, /orders."}),
"Use topic to keep separate workstreams isolated from each other.",
),
],
)),
"memory" => Some(guide(
"memory",
ToolCategory::TaskManagement,
"Use Session memory for current-session continuity, Project memory for durable project facts and decisions, and Global memory for cross-project user context. Recall with a short lexical query, use Jiandu's compact default top three IDs and summaries, then get only the selected item that needs full context. Query before writing; store one confirmed atomic fact with a few useful keywords, entities, and tags.",
"Do not store secrets, unverified claims, live state that you can inspect directly, embeddings, or unrelated facts in one memory. Treat canonical Project memory as trusted durable project authority but verify live state before acting; treat Dream as a low-trust derived orientation snapshot. Do not increase the query limit or get every hit by default, and merge or consolidate only the same confirmed fact.",
&["session_note", "session_history_current", "Task"],
vec![
example(
"Read the current session note topic",
json!({"action":"session_read","topic":"default","options":{"max_chars":4000}}),
"Use for continuity state inside the current session without touching durable project/global memory.",
),
example(
"Shortlist durable memories before reading full content",
json!({"action":"query","scope":"project","query":"mobile release freeze"}),
"Use Jiandu's compact default top three ID/summary results, then select only the item that needs full context.",
),
example(
"Get the selected durable memory",
json!({"action":"get","id":"mem_20260403_001","options":{"max_chars":5000}}),
"Use after query for the one selected ID whose full body or frontmatter is needed.",
),
example(
"Recall before answering about the user's own context",
json!({"action":"query","scope":"global","query":"preferred testing framework"}),
"When the user refers to their own preferences, opinions, or past decisions you don't already know — especially first-person questions ('what do I...', 'did I...', '我...?') — query memory BEFORE answering; do not say you don't know without checking.",
),
example(
"Recall a past project decision before acting",
json!({"action":"query","scope":"project","query":"HTTP Tauri IPC decision"}),
"Treat a matching canonical Project memory as trusted durable project authority, then verify any live implementation state before acting.",
),
example(
"Query before writing a durable fact",
json!({"action":"query","scope":"project","query":"mobile release freeze Tuesday"}),
"Check whether the same fact already exists before write; merge only when the selected existing item is the same confirmed fact.",
),
example(
"Write a durable project memory",
json!({"action":"write","scope":"project","type":"project","title":"Mobile release freeze is Tuesday","content":"Mobile release freeze begins Tuesday.","keywords":["mobile","release freeze","Tuesday"],"entities":["Mobile"],"tags":["release"]}),
"Write one confirmed atomic fact with a specific title and only a few lexical retrieval hints.",
),
example(
"Merge follow-up details into an existing durable memory",
json!({"action":"merge","id":"mem_20260403_001","content":"The release manager confirmed the Tuesday freeze.","keywords":["release freeze","Tuesday"],"entities":["release manager"],"tags":["confirmed"],"source_memory_ids":["mem_20260403_002"]}),
"Use ONLY when the new evidence is the same fact as the target memory and older overlapping items should be superseded. Do not merge unrelated facts together — create a separate memory instead.",
),
example(
"Split a multi-topic memory into atomic memories",
json!({"action":"split","id":"mem_20260403_001","pieces":[{"title":"User prefers pnpm","type":"user","content":"User prefers pnpm.","keywords":["pnpm","package manager"],"entities":["pnpm"],"tags":["preference"]},{"title":"Mobile release freeze is Tuesday","type":"project","content":"Mobile release freeze begins Tuesday.","keywords":["release freeze","Tuesday"],"entities":["Mobile"],"tags":["release"]}]}),
"Use when one memory has accreted several unrelated facts (a 'blob'): split archives the original and creates one atomic memory per fact, preserving lineage via supersedes.",
),
example(
"Consolidate near-duplicate memories into one",
json!({"action":"consolidate","ids":["mem_20260403_001","mem_20260403_007"],"type":"project","title":"Mobile release freeze is Tuesday","content":"Mobile release freeze begins Tuesday.","keywords":["release freeze","Tuesday"],"entities":["Mobile"],"tags":["release"]}),
"Use ONLY when two or more memories are the SAME fact: consolidate archives them all and creates one canonical atomic memory, preserving lineage via supersedes. Confirm sameness (e.g. via scan_duplicates / find_duplicates) before consolidating — never merge distinct facts.",
),
],
)),
// SetWorkspace is now an alias for Workspace (set mode)
"SetWorkspace" => Some(guide(
"SetWorkspace",
ToolCategory::CommandExecution,
"Change the current session workspace. (Alias for Workspace with path argument.)",
"Do not use with non-directory or missing paths.",
&["Workspace", "exec_command", "Read"],
vec![example(
"Switch session workspace",
json!({"path":"/workspace/project"}),
"Use before running commands or edits in another repo root.",
)],
)),
// Workspace is the unified tool replacing GetCurrentDir + SetWorkspace
"Workspace" => Some(guide(
"Workspace",
ToolCategory::CommandExecution,
"Get or set the current session workspace directory. Call without 'path' to read the current workspace; call with 'path' to change it.",
"Do not set workspace to a non-directory or missing path.",
&["exec_command", "Read"],
vec![
example(
"Get current workspace",
json!({}),
"Useful before commands or file ops that rely on relative paths.",
),
example(
"Set workspace",
json!({"path":"/workspace/project"}),
"Use before running commands or edits in another repo root.",
),
],
)),
"request_permissions" => Some(guide(
"request_permissions",
ToolCategory::UserInteraction,
"Request one or more remembered permission scopes through independent typed decisions. Batch entries are reviewed one at a time.",
"Do not use this for a one-shot operation: invoke the target tool directly so Allow once is bound to that exact tool-call occurrence.",
&["Write", "Edit", "exec_command", "WebFetch"],
vec![example(
"Request remembered file-write permission",
json!({"reason":"Need to write deployment config to /etc/nginx","permissions":[{"type":"write_file","resource":"/etc/nginx/conf.d/*"}]}),
"Use when later target operations need a session/workspace/global remembered scope.",
)],
)),
"Sleep" => Some(guide(
"Sleep",
ToolCategory::CommandExecution,
"Pause briefly when waiting for an external state change before polling again.",
"Do not use for normal reasoning pauses or long waits when another tool can fetch status directly.",
&["write_stdin", "WebFetch"],
vec![example(
"Wait before next poll",
json!({"seconds":2,"reason":"wait for background process output"}),
"Use short waits between repeated status checks.",
)],
)),
"loop_control" => Some(guide(
"loop_control",
ToolCategory::TaskManagement,
"Manage one live bounded loop in your current host-admitted Standard ordinary Root execution. Inspect status first; use its generation and control_revision for CAS, preserving operation_id and payload on a retry. Start charges earlier rounds and injects its objective before the next provider call. An accepted objective edit applies only at the next iteration admission; read status to distinguish accepted and applied revisions. Bounds can only tighten. Stop prevents the next provider admission after the current tool batch drains.",
"Do not infer host capability or permission from this guide. Do not target another Session, pause/resume, add cadence, extend or reset budgets, restart a stopped generation, or treat an accepted edit as already applied. Existing Plan, Child, Ultra and Supervisor gates still apply. User cancellation and host revocation remain authoritative.",
&["Task"],
vec![
example(
"Inspect this admitted execution",
json!({"action":"status"}),
"Observe current generation, control_revision, objective revisions and remaining host bounds.",
),
example(
"Start two bounded iterations",
json!({"action":"start","operation_id":"00000000-0000-4000-8000-000000000001","expected_revision":0,"objective":"Verify the next item using the accumulated context.","max_iterations":2,"max_total_rounds":6}),
"Use a fresh operation UUID. An absent prior loop uses revision zero; a retired prior generation must instead be named with its observed revision. Host ceilings include the round requesting start.",
),
example(
"Accept a new objective for the next iteration",
json!({"action":"update","operation_id":"00000000-0000-4000-8000-000000000003","generation":"00000000-0000-4000-8000-000000000002","expected_revision":1,"objective":"Verify the remaining item independently."}),
"Replace the example generation and revision with current status. At most one objective edit may be pending; its receipt names the effective iteration.",
),
example(
"Stop this generation",
json!({"action":"stop","operation_id":"00000000-0000-4000-8000-000000000004","generation":"00000000-0000-4000-8000-000000000002","expected_revision":2}),
"Use the observed generation and control revision. Stop is terminal for this generation and cannot replenish this activation.",
),
],
)),
"update_goal" => Some(guide(
"update_goal",
ToolCategory::TaskManagement,
"Signal the autonomous session goal's status: status \"complete\" only when the full objective is achieved and you can prove every requirement is satisfied; status \"blocked\" only after the same blocker has persisted for at least three consecutive goal turns.",
"Do not call to pause/resume or for ordinary task updates; do not mark complete merely because you are stopping or the budget is nearly spent. Completion is reverified by a side-channel check before the run ends.",
&["Task"],
vec![example(
"Mark the goal achieved",
json!({"status":"complete"}),
"Call only once every requirement is proven satisfied by current-state evidence.",
)],
)),
"load_skill" => Some(guide(
"load_skill",
ToolCategory::TaskManagement,
"Load a selected skill's SKILL.md instructions and metadata by skill_id.",
"Do not use for auxiliary resource files; use read_skill_resource for references/assets.",
&["read_skill_resource"],
vec![example(
"Load skill instructions",
json!({"skill_id":"rust-best-practices"}),
"Call this before following a matched skill's detailed workflow.",
)],
)),
"read_skill_resource" => Some(guide(
"read_skill_resource",
ToolCategory::FileReading,
"Read auxiliary files under a loaded skill directory with optional offset/limit paging.",
"Do not use for SKILL.md itself; call load_skill for primary instructions.",
&["load_skill"],
vec![example(
"Read a skill reference file",
json!({"skill_id":"rust-best-practices","resource_path":"references/chapter_01.md","offset":0,"limit":80}),
"Use when the loaded instructions point to additional files.",
)],
)),
"session_history_current" | "session_history" | "recall" | "session_inspector" => Some(guide(
match tool_name {
"session_inspector" => "session_inspector",
"recall" => "recall",
"session_history_current" => "session_history_current",
_ => "session_history",
},
ToolCategory::FileReading,
"Use the always-resident session_history_current identity to search your current Bamboo Session with search_current, then recover the hit's complete turn and nearby context with read_around or page exact bounded evidence with read_current, including history already compressed out of the model window. The host preserves complete logical turns and tool-call/result chains. The legacy session_history name remains compatible; Root schemas may additionally offer list/get_meta, privileged bounded reads, global search, and same-tree export_context only under that separate legacy identity. When export_context is present, Read its returned status/brief paths with bounded offset/limit and keep the returned revision fixed across continuation reads; exports are last persisted observations, not verified live progress. Raw Session history is transcript authority; memory is selective durable knowledge and may be stale.",
"Do not pass a Session ID, boundary, or compressed-state mutation to current-Session actions: the host derives identity and the current-call boundary. Treat cursors only as opaque continuations and reuse them unchanged. Do not use history as a broad substitute for code search. Privileged cross-session actions exist only when the supplied schema lists them, and export quota/corruption errors do not authorize deleting snapshots.",
&["session_note", "Read", "Task"],
vec![
example(
"Search this Session's stored history",
json!({"action":"search_current","query":"release checklist","limit":20}),
"Locate exact details omitted from the active model window. Returned excerpts are read-only; archived messages stay archived.",
),
example(
"Page older exact turns",
json!({"action":"read_current","direction":"backward","limit":10,"max_chars":12000,"archived_only":true}),
"Follow next_cursor unchanged when more bounded turns remain.",
),
example(
"Recover a search hit in context",
json!({"action":"read_around","message_id":"message-id-from-search","before_turns":1,"after_turns":1,"max_chars":12000}),
"The anchor and returned neighbors are complete logical turns; unavailable anchors do not reveal why they were excluded.",
),
],
)),
"scheduler" | "schedule_tasks" => Some(guide(
if tool_name == "schedule_tasks" {
"schedule_tasks"
} else {
"scheduler"
},
ToolCategory::TaskManagement,
"Manage Bamboo scheduled automation jobs for recurring or delayed work. Use it to create, inspect, modify, run, or delete schedules without going through HTTP.",
"Do not use for normal one-shot task planning inside the current conversation; use Task for active execution tracking instead.",
&["Task", "Workspace"],
vec![
example(
"Create a recurring schedule",
json!({"action":"create","name":"daily-review","trigger":{"type":"interval","every_seconds":86400},"enabled":true,"run_config":{"auto_execute":true,"task_message":"Review new tickets","workspace_path":"/workspace/project"}}),
"Use when work should recur automatically over time.",
),
example(
"Inspect schedule sessions",
json!({"action":"list_sessions","schedule_id":"sch_123"}),
"Use to see the sessions a schedule created or ran.",
),
],
)),
"SubAgent" => Some(guide(
"SubAgent",
ToolCategory::TaskManagement,
"Use only intent, target, role, message, and reply_to; omit unused fields. For chat (the default intent), put the complete assignment and needed paths in message: omit target to create a child, or reuse its returned logical ActorId to correct or continue it. role selects a profile only on creation; tools and permissions come from the runtime. intent=inspect with message=tree shows the owned tree; with target, message selects overview, diagnostics, messages, result, or error. For a page or selected full message, JSON-encode view/cursor/message_id inside message and omit unused query fields. intent=control uses target and message=cancel or retry. Children use intent=ask_parent with a bounded question. Direct parents reply with intent=chat, the exact pending ParentRequest id in reply_to, and the answer in message, omitting target and role; forced permission accepts exactly approve_once or deny. The runtime manages activation and waiting.",
"For ordinary executing Sessions, do not use proactively for simple one-step tasks. A trusted Supervisor delegates execution even for simple tasks: first use session_history list and bounded inspect to reuse existing managed Sessions for main execution. Use SubAgent only for auxiliary planning, review or verification; report a recoverable blocker if no suitable authorized target is available. Multiple children with distinct, non-overlapping assignments may run in parallel when the work splits into independent slices; never assign overlapping files or unclear shared responsibility. From a child session, create a nested child only when the current assignment explicitly authorizes nested delegation and it is necessary; do not create multiple overlapping children with unclear responsibilities.",
&["Task"],
vec![
example(
"Delegate read-only exploration",
json!({"intent":"chat","role":"explorer","message":"Scope: inspect parser entrypoints in /workspace/project. Inputs: src/parser.rs and its callers. Allowed actions: read files only. Acceptance: report data flow with file references. Non-goals: edits, commits, or delivery. Stop after reporting evidence and blockers; forked context cannot expand this assignment."}),
"Give the child a complete, narrow task and choose its read-only role.",
),
example(
"Correct an existing child",
json!({"intent":"chat","target":"child_123","message":"Focus on error handling only and report the specific risks."}),
"Reuse the returned logical ActorId to keep the correction in the same child.",
),
example(
"Inspect a child's result",
json!({"intent":"inspect","target":"child_123","message":"result"}),
"Read bounded evidence from the returned child identity; follow its cursor using a JSON-encoded query in message.",
),
example(
"Inspect the current tree",
json!({"intent":"inspect","message":"tree"}),
"Omit target to inspect the tree owned by this session.",
),
example(
"Read the next transcript page",
json!({"intent":"inspect","target":"child_123","message":"{\"view\":\"messages\",\"cursor\":\"returned_cursor\"}"}),
"Copy the returned cursor unchanged; view and cursor are inside message, not top-level fields.",
),
example(
"Answer a child's clarification",
json!({"intent":"chat","reply_to":"request_123","message":"Keep the existing error behavior and add coverage for the missing-input case."}),
"Use the exact pending ParentRequest id and omit target and role.",
),
example(
"Answer a forced-permission request",
json!({"intent":"chat","reply_to":"request_456","message":"approve_once"}),
"For the exact pending forced-permission request, message must be approve_once or deny.",
),
example(
"Ask the direct parent for clarification",
json!({"intent":"ask_parent","message":"Should the missing-input case preserve the current error text?"}),
"A child asks a bounded question and omits target, role, and reply_to; the runtime suspends it until its direct parent replies.",
),
],
)),
"deploy_agent" => Some(guide(
"deploy_agent",
ToolCategory::TaskManagement,
"Spin up a new worker agent on demand and manage its lifecycle. action=deploy launches a fresh broker-agent as a local subprocess (env=local), a Docker container (env=docker, needs image), or a remote SSH host (env=ssh, needs host); it joins the same message broker, inherits your MCP servers + skills, and returns an id you then drive with ask_agent. action=list shows running workers; action=stop tears one down. Use it to scale yourself out — parallel hands locally, sandboxed work in Docker, or compute on another machine.",
"Do not use when an existing worker (deploy_agent action=list) can take the task — reuse it with ask_agent instead of deploying a duplicate; do not use for in-context delegation that does not need a separate process (use SubAgent); always action=stop workers once their work is collected.",
&["ask_agent", "SubAgent"],
vec![
example(
"Deploy a local worker",
json!({"action":"deploy","env":"local","role":"tester","model":"anthropic:claude-opus-4-8"}),
"Use for an extra parallel agent on this machine; the returned id is ask_agent's target.",
),
example(
"Deploy a sandboxed Docker worker",
json!({"action":"deploy","env":"docker","image":"bamboo:latest","role":"researcher"}),
"Use for isolated/clean-env work; your bamboo home is mounted so it shares your config.",
),
example(
"Deploy on a remote host",
json!({"action":"deploy","env":"ssh","host":"user@build-box","role":"builder"}),
"Use to run work near other machines or to borrow remote compute.",
),
example(
"List then stop a worker",
json!({"action":"stop","id":"agent-7f8e9d"}),
"Use action=list to see running workers, then action=stop once a worker's work is done.",
),
],
)),
"ask_agent" => Some(guide(
"ask_agent",
ToolCategory::TaskManagement,
"Command another running agent (deployed locally, in Docker, or on a remote host) over the message broker and get its answer back synchronously. target is the agent's broker id (from deploy_agent, or a peer's session id). mode=query (default) is READ-ONLY — the target summarizes/extracts from its current state without changing course, safe to poll repeatedly. mode=steer is WRITE — your question is injected into the target's live conversation to redirect or advance its work (assign the next task, hand off context).",
"Do not use to talk to yourself or to an agent that does not exist yet — deploy it with deploy_agent first; do not use mode=steer when you only want to read progress (use query); for in-context delegation without a separate process, use SubAgent instead.",
&["deploy_agent", "SubAgent"],
vec![
example(
"Poll a worker's progress (read-only)",
json!({"target":"agent-1a2b3c","question":"Summarize the auth flow you found so far.","mode":"query"}),
"Use query to pull a result or check status without disturbing the target's work.",
),
example(
"Reassign or advance a worker (steer)",
json!({"target":"agent-1a2b3c","question":"Now write the fix to src/auth.rs and run the tests.","mode":"steer","timeout_secs":180}),
"Use steer to inject a new task into the target's conversation; raise timeout_secs for slow work.",
),
],
)),
_ => None,
}
}
pub fn builtin_guides() -> Vec<ToolGuideSpec> {
BUILTIN_GUIDE_NAMES
.iter()
.filter_map(|name| builtin_guide_spec(name))
.collect()
}
fn guide(
tool_name: &str,
category: ToolCategory,
when_to_use: &str,
when_not_to_use: &str,
related_tools: &[&str],
examples: Vec<ToolExample>,
) -> ToolGuideSpec {
ToolGuideSpec {
tool_name: tool_name.to_string(),
when_to_use: when_to_use.to_string(),
when_not_to_use: when_not_to_use.to_string(),
examples,
related_tools: related_tools.iter().map(|name| name.to_string()).collect(),
category,
}
}
fn example(scenario: &str, parameters: serde_json::Value, explanation: &str) -> ToolExample {
ToolExample::new(scenario, parameters, explanation)
}
#[cfg(test)]
mod tests {
use crate::BUILTIN_TOOL_NAMES;
use super::{builtin_guide_spec, BUILTIN_GUIDE_NAMES};
#[test]
fn every_builtin_tool_has_a_guide() {
for name in BUILTIN_GUIDE_NAMES {
assert!(
builtin_guide_spec(name).is_some(),
"missing guide for {}",
name
);
}
}
#[test]
fn builtin_guides_cover_all_builtin_tool_names() {
for name in BUILTIN_TOOL_NAMES {
assert!(
builtin_guide_spec(name).is_some(),
"missing builtin guide coverage for {}",
name
);
}
}
#[test]
fn loop_control_guide_examples_match_the_strict_command_contract() {
let guide = builtin_guide_spec("loop_control").expect("registered builtin guide");
for example in &guide.examples {
crate::tools::loop_control::parse_loop_control_request(&example.parameters.to_string())
.unwrap_or_else(|error| panic!("invalid loop_control example: {error}"));
}
}
#[test]
fn default_shell_guides_teach_pull_results_and_keep_legacy_lookup_explicit() {
let exec = builtin_guide_spec("exec_command").unwrap();
assert!(exec.when_to_use.contains("session_id"));
assert!(exec.when_to_use.contains("normally remain pull-only"));
assert!(exec.when_to_use.contains("background monitoring"));
assert!(exec.when_to_use.contains("output_omitted_bytes"));
assert!(exec.when_not_to_use.contains("stdin pipe, not a PTY"));
assert!(exec.examples[0].parameters.get("cmd").is_some());
let poll = builtin_guide_spec("write_stdin").unwrap();
assert!(poll.when_to_use.contains("final results omit it"));
assert!(poll.when_to_use.contains("include your own newline"));
assert!(poll.when_to_use.contains("polling advisory"));
assert!(poll.when_to_use.contains("not verified liveness"));
assert!(poll
.when_not_to_use
.contains("infer completion from silence"));
assert_eq!(poll.examples[0].parameters["chars"], "");
for legacy in ["Bash", "BashInput", "BashOutput", "KillShell"] {
assert!(!BUILTIN_GUIDE_NAMES.contains(&legacy));
assert!(builtin_guide_spec(legacy).is_some());
}
}
#[test]
fn memory_server_tool_has_fallback_guide_spec() {
let guide = builtin_guide_spec("memory").expect("memory guide should exist");
assert_eq!(guide.tool_name, "memory");
assert!(guide.examples.iter().any(|example| {
example
.parameters
.get("action")
.and_then(|value| value.as_str())
== Some("merge")
}));
}
#[test]
fn current_history_guide_teaches_exact_retrieval_and_authority_boundaries() {
let guide = builtin_guide_spec("session_history_current")
.expect("current-history guide should exist");
assert_eq!(guide.tool_name, "session_history_current");
for action in ["search_current", "read_current", "read_around"] {
assert!(guide.examples.iter().any(|example| {
example
.parameters
.get("action")
.and_then(|value| value.as_str())
== Some(action)
}));
}
assert!(guide.when_to_use.contains("always-resident"));
assert!(guide.when_to_use.contains("transcript authority"));
assert!(guide.when_to_use.contains("memory is selective"));
assert!(guide.when_not_to_use.contains("Session ID"));
}
#[test]
fn memory_guide_teaches_compact_recall_and_small_lexical_hints() {
let guide = builtin_guide_spec("memory").expect("memory guide should exist");
let examples_for = |action: &str| {
guide
.examples
.iter()
.filter(|example| {
example
.parameters
.get("action")
.and_then(|value| value.as_str())
== Some(action)
})
.collect::<Vec<_>>()
};
let queries = examples_for("query");
assert!(!queries.is_empty());
assert!(queries
.iter()
.all(|example| example.parameters.pointer("/options/limit").is_none()));
assert_eq!(examples_for("get").len(), 1);
for action in ["write", "merge", "consolidate"] {
let examples = examples_for(action);
assert_eq!(examples.len(), 1, "missing {action} example");
for field in ["keywords", "entities", "tags"] {
let hints = examples[0].parameters[field]
.as_array()
.unwrap_or_else(|| panic!("{action} example is missing {field}"));
assert!(!hints.is_empty() && hints.len() <= 3);
}
}
let split = examples_for("split");
let pieces = split[0].parameters["pieces"]
.as_array()
.expect("split example should contain pieces");
for piece in pieces {
for field in ["keywords", "entities", "tags"] {
let hints = piece[field]
.as_array()
.unwrap_or_else(|| panic!("split piece is missing {field}"));
assert!(!hints.is_empty() && hints.len() <= 3);
}
}
}
#[test]
fn subagent_guide_makes_nested_delegation_explicit_and_necessary() {
let guide = builtin_guide_spec("SubAgent").expect("SubAgent guide should exist");
assert!(guide.when_not_to_use.contains("child session"));
assert!(guide
.when_not_to_use
.contains("explicitly authorizes nested delegation"));
assert!(guide.when_not_to_use.contains("it is necessary"));
assert!(!guide
.when_not_to_use
.contains("do not spawn children from child sessions"));
assert!(guide
.when_not_to_use
.contains("non-overlapping assignments may run in parallel"));
assert!(guide.when_to_use.contains("intent=inspect"));
assert!(guide.when_to_use.contains("intent=control"));
assert!(guide.when_to_use.contains("intent=ask_parent"));
assert!(guide.when_to_use.contains("omit unused fields"));
assert!(guide.when_to_use.contains("ParentRequest"));
assert!(!guide.when_to_use.contains("action="));
for example in &guide.examples {
let fields = example
.parameters
.as_object()
.expect("SubAgent example parameters should be an object");
assert!(fields
.keys()
.all(|field| ["intent", "target", "role", "message", "reply_to"]
.contains(&field.as_str())));
assert!(
fields
.values()
.all(|value| { value.as_str().is_some_and(|text| !text.trim().is_empty()) }),
"unused fields must be omitted, not empty strings or nulls"
);
if fields.contains_key("target") {
assert!(!fields.contains_key("role"));
assert!(!fields.contains_key("reply_to"));
}
if fields.contains_key("reply_to") {
assert_eq!(fields["intent"], "chat");
assert!(!fields.contains_key("target"));
assert!(!fields.contains_key("role"));
}
}
}
#[test]
fn subagent_guide_examples_cover_delegation_inspection_and_parent_replies() {
let guide = builtin_guide_spec("SubAgent").expect("SubAgent guide should exist");
let examples = guide
.examples
.iter()
.map(|example| &example.parameters)
.collect::<Vec<_>>();
assert!(examples.iter().any(|args| {
args["intent"] == "chat" && args.get("role").is_some() && args.get("target").is_none()
}));
assert!(examples
.iter()
.any(|args| { args["intent"] == "chat" && args.get("target").is_some() }));
assert!(examples
.iter()
.any(|args| { args["intent"] == "inspect" && args["message"] == "result" }));
assert!(examples.iter().any(|args| {
args["intent"] == "inspect" && args["message"] == "tree" && args.get("target").is_none()
}));
assert!(examples.iter().any(|args| {
args["intent"] == "chat"
&& args.get("reply_to").is_some()
&& args["message"] == "approve_once"
}));
assert!(examples.iter().any(|args| {
args["intent"] == "chat"
&& args.get("reply_to").is_some()
&& args["message"] != "approve_once"
}));
let query = examples
.iter()
.find_map(|args| {
(args["intent"] == "inspect")
.then(|| args["message"].as_str())
.flatten()
.and_then(|message| serde_json::from_str::<serde_json::Value>(message).ok())
})
.expect("a valid JSON-encoded inspection page example");
assert_eq!(query["view"], "messages");
assert!(query["cursor"]
.as_str()
.is_some_and(|cursor| !cursor.is_empty()));
assert!(query.get("message_id").is_none());
}
#[test]
fn broker_server_tools_have_fallback_guide_specs() {
// deploy_agent: covers all three placements + a steer example via ask_agent.
let deploy = builtin_guide_spec("deploy_agent").expect("deploy_agent guide should exist");
assert_eq!(deploy.tool_name, "deploy_agent");
assert!(deploy.related_tools.iter().any(|t| t == "ask_agent"));
for env in ["local", "docker", "ssh"] {
assert!(
deploy.examples.iter().any(|example| example
.parameters
.get("env")
.and_then(|value| value.as_str())
== Some(env)),
"deploy_agent guide should show an env={env} example"
);
}
// ask_agent: must teach both query (read-only) and steer (write) modes.
let ask = builtin_guide_spec("ask_agent").expect("ask_agent guide should exist");
assert_eq!(ask.tool_name, "ask_agent");
assert!(ask.related_tools.iter().any(|t| t == "deploy_agent"));
for mode in ["query", "steer"] {
assert!(
ask.examples.iter().any(|example| example
.parameters
.get("mode")
.and_then(|value| value.as_str())
== Some(mode)),
"ask_agent guide should show a mode={mode} example"
);
}
}
}