[eric] cleanup: delete the dead CLI-Agent hook branch (SpawnAgent route replaced it), the force-denied Cron*/no-op InvokeAgent FULL_TOOLS entries, and the unused stale-model resolve_model/MODEL_MAP

This commit is contained in:
ciregenz
2026-07-16 17:09:00 -07:00
parent 1b4501f099
commit 24d197eefd
5 changed files with 7 additions and 111 deletions
@@ -12,13 +12,12 @@ from backend.apps.tools_lib.tools_lib import (
logger = logging.getLogger(__name__)
# Cron* live only in the force-deny list (path_gate): our Schedule MCP replaces the CLI scheduler, so allowing them here just churned the allow/deny lists. InvokeAgent's real tool is the mcp__openswarm-invoke-agent__ ref; the bare name was a no-op.
FULL_TOOLS = [
"Read", "Edit", "Write", "Bash", "Glob", "Grep", "AskUserQuestion",
"WebSearch", "WebFetch", "NotebookEdit", "TodoWrite",
"EnterPlanMode", "ExitPlanMode", "EnterWorktree",
"TaskOutput", "TaskStop",
"CronCreate", "CronList", "CronDelete",
"InvokeAgent",
# 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",
]
@@ -1,22 +1,19 @@
"""The SDK PostToolUse hook, lifted out of the agent loop. Runs after every tool call:
records per-tool latency, normalizes the raw tool response into displayable text, re-renders
view-builder writes (and drains build errors), materializes a spawned Agent sub-session into
the manager registry, spills oversized results to disk, and broadcasts the tool_result message.
view-builder writes (and drains build errors), spills oversized results to disk, and
broadcasts the tool_result message.
Operates on the HookContext (its `sessions` is the manager's live registry). The dict returns
and payloads are the SDK hook protocol / existing message shapes, not internal models."""
import asyncio
import logging
import time
from datetime import datetime
from typing import Dict
from uuid import uuid4
from typeguard import typechecked
from backend.apps.agents.core.models import AgentSession, Message
from backend.apps.agents.core.models import Message
from backend.apps.agents.core.ws_manager import ws_manager
from backend.apps.agents.manager.session.apply_context_window import apply_context_window
from backend.apps.agents.manager.session.history_compaction import (
truncate_large_tool_result,
wrap_platform_note,
@@ -140,68 +137,7 @@ async def post_tool_hook(ctx: HookContext, input_data: dict, tool_use_id, contex
if elapsed_ms is not None:
result_payload["elapsed_ms"] = elapsed_ms
if hook_tool_name == "Agent":
tool_input = input_data.get("tool_input", {})
agent_prompt = tool_input.get("prompt", tool_input.get("task", ""))
sub_text = content
sub_cost = 0.0
sub_tokens = {"input": 0, "output": 0}
sub_model = session.model
if isinstance(raw_response, dict):
blocks = raw_response.get("content")
if isinstance(blocks, list):
parts = [
b.get("text", "")
for b in blocks
if isinstance(b, dict) and b.get("type") == "text"
]
if parts:
sub_text = "\n".join(parts) if len(parts) > 1 else parts[0]
elif isinstance(raw_response.get("text"), str):
sub_text = raw_response["text"]
usage = raw_response.get("usage", {})
if isinstance(usage, dict):
sub_tokens["input"] = usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + usage.get("cache_read_input_tokens", 0)
# Pill-only lane: NEW (uncached) input, excludes the cached static prefix so the bubble shows what this turn added.
sub_tokens["input_fresh"] = usage.get("input_tokens", 0)
sub_tokens["output"] = usage.get("output_tokens", 0)
if raw_response.get("total_cost_usd"):
sub_cost = raw_response["total_cost_usd"]
if raw_response.get("model"):
sub_model = raw_response["model"]
sub_session_id = uuid4().hex
sub_name = agent_prompt[:50] if agent_prompt else "Sub-agent"
# Subagent context isolation invariant (Phase 3, Layer P): children DO NOT inherit the parent's active_mcps or compaction state. They start with the AgentSession defaults (empty lists). Reasoning: - Security: a parent that activated Gmail shouldn't leak Gmail tools to a subagent doing an unrelated task. The user only approved Gmail for the parent. - Token cost: subagents typically have a narrow task, they don't need the parent's full activated set. - Failure isolation: if the parent compacted history, the subagent shouldn't inherit a summary it can't re-expand. If a subagent ever needs a parent activation, the user must approve it explicitly via MCPActivate inside the subagent session, same gate as a fresh top-level chat.
sub_session = AgentSession(
id=sub_session_id,
name=sub_name,
status="completed",
model=sub_model,
mode="sub-agent",
cwd=session.cwd,
created_at=datetime.now(),
cost_usd=sub_cost,
tokens=sub_tokens,
messages=[
Message(role="user", content=agent_prompt, branch_id="main"),
Message(role="assistant", content=sub_text, branch_id="main"),
],
dashboard_id=session.dashboard_id,
parent_session_id=session_id,
# Explicit empty list (matches the model default) so the invariant is visible at the spawn site rather than relying on the field's default_factory.
active_mcps=[],
)
apply_context_window(sub_session)
ctx.sessions[sub_session_id] = sub_session
await ws_manager.broadcast_global("agent:status", {
"session_id": sub_session_id,
"status": sub_session.status,
"session": sub_session.model_dump(mode="json"),
})
result_payload["sub_session_id"] = sub_session_id
# The CLI's built-in Agent/Task sub-agent tool is hard-blocked (disallowed_tools) and replaced by the SpawnAgent MCP route, which materializes real child sessions itself; no per-tool branch needed here anymore.
result_msg = Message(role="tool_result", content=result_payload, branch_id=session.active_branch_id)
# Spill oversized tool results to per-session disk storage. The replacement keeps the first 4KB inline so the model retains some signal; the rest lives on disk for the UI to surface in the compaction drawer. Crucially this happens at *write* time (before the next turn ships history to the SDK) so the bloat never re-enters context.
try:
-11
View File
@@ -14,17 +14,6 @@ from jsonschema import validate as schema_validate, ValidationError as SchemaVal
logger = logging.getLogger(__name__)
MODEL_MAP = {
"sonnet": "claude-sonnet-4-20250514",
"opus": "claude-opus-4-20250514",
"haiku": "claude-haiku-4-5-20251001",
}
def resolve_model(short_name: str) -> str:
return MODEL_MAP.get(short_name, short_name)
def get_anthropic_client(api_model: str | None = None):
"""Create an AsyncAnthropic client using the API key from app settings.
-2
View File
@@ -28,8 +28,6 @@ from backend.apps.outputs.view_builder_templates import (
from backend.apps.settings.settings import load_settings
from backend.config.paths import OUTPUTS_DIR as DATA_DIR, OUTPUTS_WORKSPACE_DIR as WORKSPACE_DIR
from backend.apps.outputs.html_inject import (
MODEL_MAP,
resolve_model,
get_anthropic_client,
validate_against_schema,
build_data_injection,
+2 -28
View File
@@ -1,7 +1,7 @@
"""Unit coverage for the extracted PostToolUse hook (tool_result_hook). The streaming harness
mocks claude_agent_sdk.query, so it never fires the SDK's PostToolUse hooks; this pins the
behavior directly: a tool result becomes a tool_result message, and an Agent tool spawns a
sub-session into the manager's LIVE registry (the InstanceOf[dict] sharing, the subtle bit)."""
behavior directly: a tool result becomes a tool_result message, and view-builder writes surface
build/console errors into the result. SpawnAgent (the sub-agent path) is pinned in test_spawn_agent.py."""
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
@@ -42,32 +42,6 @@ async def test_normal_tool_result_appends_message_and_continues():
send.assert_awaited() # the tool_result is broadcast to the UI
@pytest.mark.asyncio
async def test_agent_tool_spawns_subsession_into_live_registry():
registry: dict = {}
ctx = p_ctx(registry)
parent_id = ctx.session_id
raw = {
"content": [{"type": "text", "text": "sub-agent did the work"}],
"usage": {"input_tokens": 7, "output_tokens": 3},
"total_cost_usd": 0.01,
"model": "sonnet",
}
with patch.object(tool_result_hook.ws_manager, "send_to_session", new=AsyncMock()), \
patch.object(tool_result_hook.ws_manager, "broadcast_global", new=AsyncMock()):
out = await tool_result_hook.post_tool_hook(
ctx, {"tool_name": "Agent", "tool_response": raw, "tool_input": {"prompt": "do x"}}, "tu1", None
)
assert out == {"continue_": True}
# exactly one NEW session registered (besides the parent), parented correctly
children = [s for sid, s in registry.items() if sid != parent_id]
assert len(children) == 1
child = children[0]
assert child.parent_session_id == parent_id
assert child.active_mcps == [] # context-isolation invariant: no inherited activations
assert "sub-agent did the work" in str(child.messages[-1].content)
@pytest.mark.asyncio
async def test_view_builder_dep_install_broadcasts_app_deps_changed():
"""An npm install in a view-builder session must tell the app card this turn