2026-05-31 23:58:26 +09:00
|
|
|
"""
|
|
|
|
|
agent_tools.py — Facade module.
|
|
|
|
|
|
|
|
|
|
Re-exports tool parsing, schemas, execution, and implementations
|
|
|
|
|
for backward compatibility. All importers continue to work unchanged.
|
|
|
|
|
|
|
|
|
|
Sub-modules:
|
|
|
|
|
- tool_parsing.py: regex patterns, parse/strip functions
|
|
|
|
|
- tool_schemas.py: FUNCTION_TOOL_SCHEMAS, function_call_to_tool_block
|
|
|
|
|
- tool_execution.py: execute_tool_block, format_tool_result, MCP helpers
|
|
|
|
|
- tool_implementations.py: all do_* tool functions
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
from collections import namedtuple
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Constants (kept here — sub-modules import from here)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
cookbook agent debug loop: persistent log files, auto-adopt orphan tmux, Codex/Claude skill parity
Three converging fixes so the chat agent + external Codex/Claude skills can actually debug a crashed serve instead of staring at a post-crash neofetch banner:
* Serves now `tee` to /tmp/odysseus-tmux/SESSION.log on the host running them. Runner saves fds 3/4 before the tee and restores them right before `exec ${SHELL}`, so the post-crash interactive zsh banner does NOT pollute the log file.
* `tail_serve_output` (chat agent) and `/api/codex/cookbook/output/{sid}` (Codex+Claude skills) both prefer the persistent log file over the tmux pane. Pane is fallback for sessions predating the tee runner. Default tail bumped 150 -> 400.
* `list_served_models` "recent log" snippet seeks to the Traceback line instead of showing the last 6 lines (which was always the bash prompt).
Cookbook auto-adoption sweep on `/api/cookbook/tasks/status`: every 20s (rate-limited) the cookbook SSHes each configured server, finds `serve-*` / `cookbook-*` tmux sessions running an actual model process (vllm/python/llama-server/etc., filtered via `pane_current_command`), and writes them into state.tasks. So when the agent falls back to raw ssh+tmux, the session appears in the Cookbook UI on the next poll.
`serve_model` error path now reads `data["detail"]` in addition to `data["error"]` so the FastAPI HTTPException message ("Invalid characters in cmd") actually reaches the agent instead of being swallowed as a generic "Serve failed". Tool description updated to warn against `cd …`/`source …`/`&&` prefixes.
Intent-without-action supervisor in agent_loop: when the model writes "Let me tail the output" / "I'll check the logs" / "Let me investigate" and ends the turn without emitting a tool call, the loop injects a sharp system nudge ("You said you would X — DO IT NOW") and continues. Capped at 2 nudges per chat so a model that genuinely cannot use the tool does not pin the loop.
Codex/Claude skill parity: adds `/cookbook/cached`, `/cookbook/presets`, `/cookbook/preset/{name}`, `/cookbook/adopt` so external agents have the same surface as the chat agent. SKILL.md docs + odysseus_api.py wrapper updated for both bundles.
`adopt_served_model` promoted to the always-on tool set so the agent has a documented fallback when serve_model rejects a cmd.
Also various cookbook UI tweaks accumulated alongside the above (cookbook.js, cookbookRunning.js, cookbookServe.js, cookbook-diagnosis.js, settings.js, style.css).
2026-06-04 23:27:18 +09:00
|
|
|
MAX_AGENT_ROUNDS = 50
|
2026-05-31 23:58:26 +09:00
|
|
|
SHELL_TIMEOUT = 60
|
|
|
|
|
PYTHON_TIMEOUT = 30
|
|
|
|
|
MAX_OUTPUT_CHARS = 10_000
|
|
|
|
|
MAX_READ_CHARS = 20_000
|
|
|
|
|
|
|
|
|
|
# Tool types that trigger execution
|
feat: Add edit_file tool + file-change diffs (#1239)
* Add edit_file tool + file-change diffs
edit_file is an exact old_string -> new_string replacement on a file on disk
(fails if old_string is missing or non-unique unless replace_all); write_file
also returns a unified diff. Diffs render collapsed in the tool bubble
(filename + +adds/-dels, theme colors); the raw JSON command box is hidden.
Security: edit_file is a sensitive filesystem-write tool, treated everywhere
write_file is —
- added to NON_ADMIN_BLOCKED_TOOLS (is_public_blocked_tool / blocked_tools_for_owner),
so on auth-enabled deployments a non-admin cannot run it; execute_tool_block
refuses it for non-admin owners.
- confined by the same path policy as read_file/write_file (allowlist +
sensitive-file deny) via _resolve_tool_path.
Disambiguation in tool descriptions + bash prompt: edit_file/write_file are the
only way to write files (they show a diff) — never edit_document (editor panel)
or a bash heredoc/redirect.
Tests (tests/test_edit_file.py): non-admin block (policy + execution gate),
successful edit, not-found old_string, non-unique old_string (+ replace_all),
and path outside the allowed roots.
Files: src/tool_execution.py, src/agent_loop.py, src/tool_schemas.py,
src/agent_tools.py, src/tool_index.py, static/js/chat.js, static/style.css,
tests/test_edit_file.py.
* Drop redundant import os in write_file closure
os is already imported at module top.
2026-06-04 18:29:10 +02:00
|
|
|
TOOL_TAGS = {"bash", "python", "web_search", "web_fetch", "read_file", "write_file", "edit_file",
|
feat: add code-navigation tools (grep, glob, ls) + read_file line ranges (#1670)
Gives the agent first-class code navigation instead of shelling out via bash
(token-heavy, unreliable on weaker models, unstructured). Mirrors the
Grep/Glob/Read primitives that Claude Code / opencode expose.
- grep: regex search over file contents across a tree. Uses ripgrep when
available (with explicit excludes so junk dirs are skipped even without a
.gitignore); falls back to a pure-Python walk+regex when rg is absent.
Returns file:line:match, capped.
- glob: find files by glob pattern (recursive), newest first.
- ls: list a directory (folders first, then files with sizes).
- read_file: optional offset/limit for line-range reads of large files
(plain-path calls stay back-compatible).
All confined by the same path policy as read_file (_resolve_tool_path:
data/tmp allowlist + sensitive-file deny). Junk dirs (.git, node_modules,
venv, __pycache__, dist/build, …) skipped. Output capped (200 hits,
400 chars/line). Admin-gated like the other filesystem tools.
Wiring: schemas + native arg->content serializer (src/tool_schemas.py), tool
tags (src/agent_tools.py), always-available + descriptions (src/tool_index.py),
admin gate (src/tool_security.py), dispatch + impls (src/tool_execution.py).
Tests: tests/test_code_nav_tools.py — match/skip-junk/ignore-case/glob-filter,
allowlist rejection, glob/ls, read-range, and the no-ripgrep Python fallback.
2026-06-04 18:37:32 +02:00
|
|
|
"grep", "glob", "ls",
|
2026-05-31 23:58:26 +09:00
|
|
|
"create_document", "update_document", "edit_document",
|
|
|
|
|
"search_chats",
|
|
|
|
|
"chat_with_model", "create_session", "list_sessions",
|
|
|
|
|
"send_to_session",
|
|
|
|
|
"pipeline",
|
|
|
|
|
"manage_session", "manage_memory", "list_models",
|
Add ask_user tool: agent-posed multiple-choice questions (#2111)
Let the agent pause and ask the user a multiple-choice question when a
task is genuinely ambiguous and the answer changes what it does next —
choosing between approaches, confirming an assumption, picking a target —
instead of guessing.
Modeled on the existing `ui_control` marker pattern: the `ask_user` tool
returns an `ask_user` payload that the agent loop emits as an SSE event
and then ends the turn. The frontend renders the question with clickable
option buttons, a free-text "Other" input, and an x to dismiss; the user's
choice is sent as the next message and the agent resumes with it in
context.
- src/tool_execution.py: `ask_user` handler — pure UI marker, no I/O.
Validates a non-empty question + 2..6 options, normalizes string/object
options, returns the payload.
- src/agent_loop.py: emit the `ask_user` event and break the round loop so
the turn ends and waits for the user's selection. Stream the question as
assistant text so it persists/replays (prevents a re-ask loop).
- Registration: TOOL_TAGS, ALWAYS_AVAILABLE, BUILTIN_TOOL_DESCRIPTIONS,
FUNCTION_TOOL_SCHEMAS, the system-prompt blurb. Not admin-gated (any
user can be asked); the structured args serialize via the default
json.dumps path.
- routes/chat_routes.py: relay the `ask_user` event to the client.
- static/js/chat.js + static/style.css: render the question card (options +
free-text Other + dismiss x; removed once answered). Reuses CSS vars and
the .modal-close button; emoji go through the monochrome-SVG pipeline.
Bump chat.js cache pin.
- tests/test_ask_user_tool.py: payload, multi flag, string options, option
cap, validation errors, serializer round-trip, registration.
2026-06-05 11:49:11 +02:00
|
|
|
"ui_control", "generate_image", "ask_user",
|
2026-05-31 23:58:26 +09:00
|
|
|
"manage_tasks", "api_call", "ask_teacher", "manage_skills",
|
|
|
|
|
"suggest_document",
|
|
|
|
|
"manage_endpoints", "manage_mcp", "manage_webhooks",
|
|
|
|
|
"manage_tokens", "manage_documents", "manage_settings",
|
|
|
|
|
"manage_notes", "manage_calendar",
|
|
|
|
|
"resolve_contact", "manage_contact", "list_email_accounts", "send_email", "list_emails",
|
|
|
|
|
"read_email", "reply_to_email", "bulk_email", "archive_email",
|
|
|
|
|
"delete_email", "mark_email_read",
|
|
|
|
|
# Cookbook tools (LLM serving + downloads). Without these
|
|
|
|
|
# entries, native function calls to e.g. list_served_models
|
|
|
|
|
# are rejected as "Unknown function call" before reaching
|
|
|
|
|
# the dispatcher — silent failure for the whole cookbook
|
|
|
|
|
# surface.
|
|
|
|
|
"download_model", "serve_model",
|
|
|
|
|
"list_served_models", "stop_served_model",
|
|
|
|
|
"list_downloads", "cancel_download",
|
|
|
|
|
"search_hf_models", "list_cached_models",
|
|
|
|
|
"list_serve_presets", "serve_preset", "adopt_served_model",
|
|
|
|
|
"list_cookbook_servers",
|
|
|
|
|
# Other tools the agent reaches for that were also missing.
|
|
|
|
|
"edit_image", "trigger_research", "manage_research",
|
|
|
|
|
# Generic loopback to any UI-button endpoint (cookbook,
|
|
|
|
|
# gallery, email folders, etc.) — agent uses this when
|
|
|
|
|
# there's no named tool wrapper for the action.
|
|
|
|
|
"app_api"}
|
|
|
|
|
|
|
|
|
|
ToolBlock = namedtuple("ToolBlock", ["tool_type", "content"])
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# MCP Manager (kept here — used by execution and agent_loop)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
_mcp_manager = None
|
|
|
|
|
|
|
|
|
|
def set_mcp_manager(manager):
|
|
|
|
|
"""Set the global MCP manager instance."""
|
|
|
|
|
global _mcp_manager
|
|
|
|
|
_mcp_manager = manager
|
|
|
|
|
|
|
|
|
|
def get_mcp_manager():
|
|
|
|
|
"""Get the global MCP manager instance."""
|
|
|
|
|
return _mcp_manager
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Helpers (kept here — used by sub-modules)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
def _truncate(text: str, limit: int = MAX_OUTPUT_CHARS) -> str:
|
2026-06-03 06:06:39 +01:00
|
|
|
# Callers treat the result as text, so always return a string: coerce a
|
|
|
|
|
# non-string (None -> "", otherwise str(...)) instead of returning it raw,
|
|
|
|
|
# which would just move the crash downstream.
|
|
|
|
|
if not isinstance(text, str):
|
|
|
|
|
text = "" if text is None else str(text)
|
2026-05-31 23:58:26 +09:00
|
|
|
if len(text) > limit:
|
|
|
|
|
return text[:limit] + f"\n... (truncated, {len(text)} chars total)"
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Re-exports from sub-modules
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Parsing
|
|
|
|
|
from src.tool_parsing import ( # noqa: E402, F401
|
|
|
|
|
parse_tool_blocks,
|
|
|
|
|
strip_tool_blocks,
|
|
|
|
|
_TOOL_NAME_MAP,
|
|
|
|
|
_TOOL_BLOCK_RE,
|
|
|
|
|
_TOOL_CALL_RE,
|
|
|
|
|
_XML_TOOL_CALL_RE,
|
|
|
|
|
_XML_INVOKE_RE,
|
|
|
|
|
_XML_PARAM_RE,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Schemas
|
|
|
|
|
from src.tool_schemas import ( # noqa: E402, F401
|
|
|
|
|
FUNCTION_TOOL_SCHEMAS,
|
|
|
|
|
function_call_to_tool_block,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Execution
|
|
|
|
|
from src.tool_execution import ( # noqa: E402, F401
|
|
|
|
|
execute_tool_block,
|
|
|
|
|
format_tool_result,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Implementations
|
|
|
|
|
from src.tool_implementations import ( # noqa: E402, F401
|
|
|
|
|
set_active_document,
|
|
|
|
|
set_active_model,
|
|
|
|
|
get_active_document,
|
|
|
|
|
do_create_document,
|
|
|
|
|
do_update_document,
|
|
|
|
|
do_edit_document,
|
|
|
|
|
do_suggest_document,
|
|
|
|
|
do_search_chats,
|
|
|
|
|
do_manage_skills,
|
|
|
|
|
do_manage_tasks,
|
|
|
|
|
do_manage_endpoints,
|
|
|
|
|
do_manage_mcp,
|
|
|
|
|
do_manage_webhooks,
|
|
|
|
|
do_manage_tokens,
|
|
|
|
|
do_manage_documents,
|
|
|
|
|
do_manage_settings,
|
|
|
|
|
do_api_call,
|
|
|
|
|
)
|