mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-21 12:12:22 +02:00
[eric] fix extended actions (ToolSearch in allowedTools, claude_code preset+append for system_prompt and tools, ENABLE_TOOL_SEARCH=auto,
drop --bare which was stubbing the default system prompt), drop connected_tools_ctx that leaked MCP server names/emails into every turn, fix nine_router asyncio UnboundLocalError, gate 9Router subprocess output on OPENSWARM_DEBUG_9ROUTER
This commit is contained in:
@@ -50,4 +50,3 @@ export async function POST(request) {
|
||||
|
||||
return await handleChat(newRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,11 @@ FULL_TOOLS = [
|
||||
"RenderOutput",
|
||||
"InvokeAgent",
|
||||
"Agent",
|
||||
# ToolSearch is the loader the CLI uses to expose deferred tool schemas
|
||||
# on demand. Must be in the allowedTools whitelist or the model can't
|
||||
# call it, which means none of the deferred extended tools become
|
||||
# reachable even when the CLI advertises them in the system prompt.
|
||||
"ToolSearch",
|
||||
]
|
||||
|
||||
def _get_denied_tool_names(tool) -> set[str]:
|
||||
@@ -865,7 +870,22 @@ class AgentManager:
|
||||
|
||||
try:
|
||||
_, mode_sys_prompt, _ = self._resolve_mode(session.mode)
|
||||
connected_tools_ctx = self._build_connected_tools_context(session.allowed_tools)
|
||||
# MCP servers and their tool inventories are intentionally NOT
|
||||
# injected into the system prompt. The CLI's deferred-tool pool
|
||||
# already exposes them by name via ToolSearch — eagerly listing
|
||||
# connected MCPs (with account emails, full tool enumerations,
|
||||
# etc.) here would defeat the deferral and leak knowledge of
|
||||
# every connected integration into every turn. The model
|
||||
# discovers MCPs only when it actively calls ToolSearch.
|
||||
#
|
||||
# Trade-offs of this removal:
|
||||
# - Email auto-fill for Gmail/Calendar is gone. The model may
|
||||
# need to ask which account to use, or pass it explicitly.
|
||||
# - Discord guild-id "hard restriction" is gone as a prompt
|
||||
# instruction. Enforce that at the Discord MCP server's
|
||||
# tool-call layer instead — prompt rules are not a security
|
||||
# boundary.
|
||||
connected_tools_ctx = None
|
||||
outputs_ctx = self._build_outputs_context()
|
||||
browser_ctx = self._build_browser_context(session.dashboard_id, selected_browser_ids=selected_browser_ids)
|
||||
global_settings = load_settings()
|
||||
@@ -1004,18 +1024,67 @@ class AgentManager:
|
||||
options_kwargs["env"] = {
|
||||
"ANTHROPIC_API_KEY": "9router",
|
||||
"ANTHROPIC_BASE_URL": "http://localhost:20128",
|
||||
# The bundled CLI auto-disables tool search when
|
||||
# ANTHROPIC_BASE_URL isn't a first-party Anthropic host.
|
||||
# Without tool search, the entire deferred-tool pool
|
||||
# (WebSearch, NotebookEdit, TodoWrite, EnterPlanMode,
|
||||
# Cron*, Task*, etc.) becomes unreachable. Force-enable
|
||||
# in `auto` mode so the CLI surfaces them through the
|
||||
# ToolSearch loader. 9Router is a transparent SSE proxy
|
||||
# so tool_reference content blocks pass through intact.
|
||||
#
|
||||
# NOTE on context bloat: in `auto` mode, MCPs and
|
||||
# deferred builtins are still loaded eagerly when the
|
||||
# deferred-tool tokens are below ~10% of the model's
|
||||
# context window. Setting this to "true" instead would
|
||||
# force-enable tool search but the CLI's internal
|
||||
# `tengu_defer_all_bn4` Statsig flag (defaults to true
|
||||
# outside Anthropic's first-party network) then defers
|
||||
# ALL non-core tools including Read/Edit/Bash, leaving
|
||||
# the model with effectively zero tools. Until we have
|
||||
# a way to override that Statsig flag from outside the
|
||||
# binary, "auto" is the only working setting.
|
||||
"ENABLE_TOOL_SEARCH": "auto",
|
||||
}
|
||||
# --bare skips CLI's own OAuth/keychain auth, uses only ANTHROPIC_API_KEY
|
||||
options_kwargs["extra_args"] = {"bare": None}
|
||||
logger.info("[MCP-DEBUG] Using 9Router (bare mode)")
|
||||
# NOTE: do NOT pass `--bare`. It internally sets
|
||||
# CLAUDE_CODE_SIMPLE=1, which short-circuits the default
|
||||
# Claude Code system prompt to a `"You are Claude Code"`
|
||||
# stub and disables the deferred-tools / ToolSearch
|
||||
# initialization. The CLI still picks up ANTHROPIC_API_KEY
|
||||
# from env first (before OAuth/keychain), so the original
|
||||
# goal of bare mode (skip OAuth/keychain) is preserved as
|
||||
# long as ANTHROPIC_API_KEY is set above — which it is.
|
||||
logger.info("[MCP-DEBUG] Using 9Router")
|
||||
else:
|
||||
raise ValueError("No AI provider configured. Set an API key or connect a subscription.")
|
||||
if mcp_servers:
|
||||
options_kwargs["mcp_servers"] = mcp_servers
|
||||
mcp_json_len = len(json.dumps({"mcpServers": mcp_servers}))
|
||||
logger.info(f"[MCP-DEBUG] mcp_servers passed to SDK: {list(mcp_servers.keys())}, JSON length={mcp_json_len}")
|
||||
# Use the claude_code preset for BOTH the system prompt and the
|
||||
# base tool set so the CLI's default scaffolding (deferred-tools
|
||||
# listing + ToolSearch instructions) and full base tool set come
|
||||
# along for the ride. Passing a raw string for system_prompt would
|
||||
# send `--system-prompt` (REPLACE) and strip that scaffolding;
|
||||
# leaving `tools` unset makes the CLI fall back to a much smaller
|
||||
# default base set than the model expects (empirically only Bash/
|
||||
# Read/Edit get surfaced). The pair below is what stock Claude
|
||||
# Code uses, plus our composed_prompt appended on top.
|
||||
options_kwargs["tools"] = {
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
}
|
||||
if composed_prompt:
|
||||
options_kwargs["system_prompt"] = composed_prompt
|
||||
options_kwargs["system_prompt"] = {
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": composed_prompt,
|
||||
}
|
||||
else:
|
||||
options_kwargs["system_prompt"] = {
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
}
|
||||
if session.max_turns:
|
||||
options_kwargs["max_turns"] = session.max_turns
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ async def ensure_running():
|
||||
if result.stdout.strip():
|
||||
logger.info("Dev mode: killing stale standalone 9Router to use next dev instead")
|
||||
_sp.run(["pkill", "-f", "next-server"], timeout=5)
|
||||
import asyncio
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
logger.info("9Router already running on port %d", NINE_ROUTER_PORT)
|
||||
@@ -160,12 +159,31 @@ async def ensure_running():
|
||||
cwd = None
|
||||
env = {**os.environ, "PORT": str(NINE_ROUTER_PORT)}
|
||||
|
||||
# By default, 9Router's stdout/stderr go to /dev/null (Next.js dev mode
|
||||
# is extremely chatty and floods the openswarm console otherwise). When
|
||||
# debugging is needed, set OPENSWARM_DEBUG_9ROUTER=1 in the environment
|
||||
# before launching the backend — output will then be appended to
|
||||
# backend/data/9router.log line-buffered, which can be `tail -f`'d.
|
||||
if os.environ.get("OPENSWARM_DEBUG_9ROUTER"):
|
||||
_log_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"data",
|
||||
"9router.log",
|
||||
)
|
||||
os.makedirs(os.path.dirname(_log_path), exist_ok=True)
|
||||
_stdout = open(_log_path, "a", buffering=1) # line-buffered
|
||||
_stderr = subprocess.STDOUT
|
||||
logger.info(f"9Router debug logging enabled → {_log_path}")
|
||||
else:
|
||||
_stdout = subprocess.DEVNULL
|
||||
_stderr = subprocess.DEVNULL
|
||||
|
||||
try:
|
||||
_process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=_stdout,
|
||||
stderr=_stderr,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user