mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-08 18:57:43 +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);
|
return await handleChat(newRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ FULL_TOOLS = [
|
|||||||
"RenderOutput",
|
"RenderOutput",
|
||||||
"InvokeAgent",
|
"InvokeAgent",
|
||||||
"Agent",
|
"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]:
|
def _get_denied_tool_names(tool) -> set[str]:
|
||||||
@@ -865,7 +870,22 @@ class AgentManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_, mode_sys_prompt, _ = self._resolve_mode(session.mode)
|
_, 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()
|
outputs_ctx = self._build_outputs_context()
|
||||||
browser_ctx = self._build_browser_context(session.dashboard_id, selected_browser_ids=selected_browser_ids)
|
browser_ctx = self._build_browser_context(session.dashboard_id, selected_browser_ids=selected_browser_ids)
|
||||||
global_settings = load_settings()
|
global_settings = load_settings()
|
||||||
@@ -1004,18 +1024,67 @@ class AgentManager:
|
|||||||
options_kwargs["env"] = {
|
options_kwargs["env"] = {
|
||||||
"ANTHROPIC_API_KEY": "9router",
|
"ANTHROPIC_API_KEY": "9router",
|
||||||
"ANTHROPIC_BASE_URL": "http://localhost:20128",
|
"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
|
# NOTE: do NOT pass `--bare`. It internally sets
|
||||||
options_kwargs["extra_args"] = {"bare": None}
|
# CLAUDE_CODE_SIMPLE=1, which short-circuits the default
|
||||||
logger.info("[MCP-DEBUG] Using 9Router (bare mode)")
|
# 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:
|
else:
|
||||||
raise ValueError("No AI provider configured. Set an API key or connect a subscription.")
|
raise ValueError("No AI provider configured. Set an API key or connect a subscription.")
|
||||||
if mcp_servers:
|
if mcp_servers:
|
||||||
options_kwargs["mcp_servers"] = mcp_servers
|
options_kwargs["mcp_servers"] = mcp_servers
|
||||||
mcp_json_len = len(json.dumps({"mcpServers": 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}")
|
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:
|
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:
|
if session.max_turns:
|
||||||
options_kwargs["max_turns"] = session.max_turns
|
options_kwargs["max_turns"] = session.max_turns
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ async def ensure_running():
|
|||||||
if result.stdout.strip():
|
if result.stdout.strip():
|
||||||
logger.info("Dev mode: killing stale standalone 9Router to use next dev instead")
|
logger.info("Dev mode: killing stale standalone 9Router to use next dev instead")
|
||||||
_sp.run(["pkill", "-f", "next-server"], timeout=5)
|
_sp.run(["pkill", "-f", "next-server"], timeout=5)
|
||||||
import asyncio
|
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
else:
|
else:
|
||||||
logger.info("9Router already running on port %d", NINE_ROUTER_PORT)
|
logger.info("9Router already running on port %d", NINE_ROUTER_PORT)
|
||||||
@@ -160,12 +159,31 @@ async def ensure_running():
|
|||||||
cwd = None
|
cwd = None
|
||||||
env = {**os.environ, "PORT": str(NINE_ROUTER_PORT)}
|
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:
|
try:
|
||||||
_process = subprocess.Popen(
|
_process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=_stdout,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=_stderr,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user