fix(core): keep agent-tool render idempotent so no-persona sessions share the tool constant

_render_agent_tool_descriptions rebuilt self._tools and reassigned it on every session init, deep-copying task_agent even with no model aliases and no personas to inject — the single-model CLI case the docstring says is skipped. This regressed after the persona-discoverability change removed the early 'if self._registry is None: return' guard, breaking the session._tools is INTERACTIVE_TOOLS invariant (test_session_without_mcp).

Gate the reassignment on whether a description actually changed: keep the original tool object when the render is a no-op, fork self._tools only when something was injected. Restores the shared-constant invariant, makes repeated renders idempotent, and preserves clear-stale-on-reload (an emptied registry still takes the changed path).
This commit is contained in:
Patrick Buckley
2026-07-06 20:42:00 -07:00
parent a61d454df5
commit bd37bcd1ec
+18 -2
View File
@@ -2589,6 +2589,7 @@ class ChatSession:
persona_base[fn["name"]] = (prop or {}).get("description", "")
new_tools: list[dict[str, Any]] = []
changed = False
for tool in self._tools:
fn = tool.get("function") or {}
name = fn.get("name", "")
@@ -2621,8 +2622,23 @@ class ChatSession:
if prop is not None:
base = persona_base.get(name, "")
prop["description"] = f"{base} {persona_line}".strip() if persona_line else base
new_tools.append(new_tool)
self._tools = new_tools
if new_tool == tool:
# The rendered description is identical to what this tool
# already carries — nothing dynamic to inject (no aliases,
# no personas). Keep the original object so a session with
# nothing to render keeps sharing the pristine module-level
# tool list instead of allocating a per-session copy.
new_tools.append(tool)
else:
new_tools.append(new_tool)
changed = True
# Swap in the rebuilt list only when a description actually changed.
# This keeps the render idempotent and allocation-free when there is
# nothing to inject, so the shared constants (e.g. INTERACTIVE_TOOLS)
# stay referenced by such sessions; a reload that clears a prior
# render still takes the ``changed`` path and replaces the stale list.
if changed:
self._tools = new_tools
@staticmethod
def _persona_property(props: dict[str, Any]) -> dict[str, Any] | None: