[eric] mcp: list activated server's tools in auto-continue prompt

This commit is contained in:
ciregenz
2026-05-20 00:11:09 -07:00
parent 7dc1cf45ad
commit 4eab66fc66
4 changed files with 70 additions and 2 deletions
+6 -1
View File
@@ -4614,7 +4614,12 @@ class AgentManager:
session.closed_at = None
self.sessions[session_id] = session
_delete_session_file(session_id)
# Do NOT delete the disk file here. The history list (get_history)
# reads from disk; deleting on resume meant every click on a past
# chat permanently removed it from history on the next restart.
# The disk copy stays as the durable record; subsequent turn
# completions and close_session calls overwrite it via
# _save_session, so memory and disk stay in sync.
await ws_manager.send_to_session(session_id, "agent:status", {
"session_id": session_id,
+25
View File
@@ -103,6 +103,7 @@ class ConnectionManager:
# reconcile_on_startup also marks waiting_approval sessions as
# stopped so there's nothing to answer anyway.
events = self._filter_stale_approvals(events)
events = self._strip_replayed_closes(events)
for s in events:
try:
await websocket.send_text(s)
@@ -130,6 +131,30 @@ class ConnectionManager:
"current_seq": newest if newest is not None else 0,
}
def _strip_replayed_closes(self, events: list[str]) -> list[str]:
"""Drop `agent:closed` events from a replay buffer.
agent:closed is a transition event ("session JUST closed") whose
frontend reducer (closeSessionFromWs) destructively deletes the
session from state.sessions. Replaying it on a fresh client (e.g.
a user who just clicked the closed chat in history) deletes the
session they're trying to open. The current closed state is
already conveyed by the REST hydrate (status=stopped, closed_at
set) and by the latest agent:status event in the replay, so
suppressing the transition replay is non-lossy.
"""
out: list[str] = []
for payload_str in events:
try:
parsed = json.loads(payload_str)
except (ValueError, TypeError):
out.append(payload_str)
continue
if parsed.get("event") == "agent:closed":
continue
out.append(payload_str)
return out
def _filter_stale_approvals(self, events: list[str]) -> list[str]:
"""Return events minus any `agent:approval_request` whose request_id
is no longer in pending_futures. JSON parse is per-event but replay
+11
View File
@@ -132,6 +132,7 @@ async def execute(wf: Workflow, triggered_by: str = "schedule", scheduled_for: O
"last_run_id": run.id,
})
session = None
try:
steps = [s.text for s in wf.steps if s.text and s.text.strip()]
if not steps:
@@ -207,6 +208,16 @@ async def execute(wf: Workflow, triggered_by: str = "schedule", scheduled_for: O
"last_run_at": run.finished_at,
})
finally:
# Close the workflow's agent session so closed_at is set and the
# run shows up in chat history (get_history sorts by closed_at;
# sessions with closed_at=None sort to the bottom and fall off
# the first page). close_session also drops in-memory state and
# persists the final snapshot to disk.
if session is not None:
try:
await agent_manager.close_session(session.id)
except Exception:
logger.exception("close_session failed for workflow run %s", run.id)
async with _running_lock:
_running.pop(wf.id, None)
+28 -1
View File
@@ -517,11 +517,38 @@ async def mcp_meta(action: str, request: Request):
# Auto-continue: read at turn boundary in _run_agent_loop (race-free); collapses the typical 3-prompt flow into 1.
session.pending_continuation = True
# Enumerate the just-activated server's callable tool names so the
# continuation turn can call them directly. Without this the model
# often burns a turn on tool-discovery guesses (Bash "mcp list",
# Ls /toolbox, ToolSearch fallbacks) before landing on the right
# mcp__server__action name. Cap at 16 + clip descriptions so the
# prompt stays bounded for kitchen-sink servers (google-workspace
# exposes ~30 tools). Best-effort; any lookup failure silently
# falls back to the same prompt this code shipped with before.
tool_hint = ""
try:
for t in load_all_tools():
if _sanitize_server_name(t.name) != server_name:
continue
descs = (t.tool_permissions or {}).get("_tool_descriptions", {}) or {}
if not descs:
break
lines: list[str] = []
for sub_name, desc in list(descs.items())[:16]:
short = (desc or "").strip().split("\n", 1)[0][:120]
visible = f"mcp__{server_name}__{sub_name}"
lines.append(f"- `{visible}`: {short}" if short else f"- `{visible}`")
if lines:
more = "" if len(descs) <= 16 else f"\n(+ {len(descs) - 16} more; call ToolSearch with the server name for the rest)"
tool_hint = "\n\nCallable tools on this server:\n" + "\n".join(lines) + more
break
except Exception:
logger.exception("activate: failed to build tool hint for %s", server_name)
session.pending_continuation_prompt = (
"[mcp:auto-continue] The MCP server you requested has been "
f"activated (`{server_name}`). Continue with the user's original "
"request now using the newly-available tools; do NOT ask "
"for confirmation."
"for confirmation." + tool_hint
)
return JSONResponse({"status": "activated", "server_name": server_name, "auto_continue": True})