Verified against han_solo/letta_client.py and han_solo/server.py on 2026-06-09. Every step traced to code, not memory.
Ren's tool set is declarative, driven by one source of truth: the CANONICAL_REN_TOOL_NAMES set in han_solo/letta_client.py (line 17). On every server startup — and on demand — ensure_ren_tools() forces Ren's actual Letta attachments to match that set exactly (adds missing, removes extras). You don't attach tools by hand; you edit the canonical set and let the sync enforce it.
The tool must already be implemented and registered on the han-solo FastMCP server (code in han_solo/tools/). Quick check: call it, or hit GET /api/admin/list-mcp-tools (server.py:107). If it returns, it's live.
Edit han_solo/letta_client.py → CANONICAL_REN_TOOL_NAMES (line 17). Add the exact tool-name string.
tool_rules are auto-derived by canonical_ren_tool_rules() (line 63): every tool gets continue_loop automatically except those in REN_EXIT_LOOP_TOOLS (line 60), currently only send_message.REN_EXIT_LOOP_TOOLS if the tool is meant to END Ren's turn. Otherwise leave it out — normal tools must be continue_loop or her agent loop dies after one call (the step_count=1 bug).mainhan_solo/ is the Render-deployed package. Pushing triggers an auto-deploy.
On boot, the lifespan hook (server.py:2977 _lifespan → line 2989) calls ensure_ren_tools(), which re-syncs Ren's attachments to the new canonical set. In most cases this alone completes the attach.
POST /api/admin/sync-mcp-tools (server.py:2922). The handler (server.py:148) runs sync_mcp_tools() — discovers MCP tools and registers any missing into Letta's registry — then ensure_ren_tools() to attach. Idempotent; safe to call more than once.
verify_agent_tools() → check all_match: true and that attached_count reflects the new total.Ren tools+rules synced — {N} tools, {N} rules.ensure_ren_tools() sends PATCH /v1/agents/{id} with both (letta_client.py:278). Letta's PATCH nulls any omitted field (comment line 291). The function handles this; never hand-roll a PATCH that sends only one.llm_config in the same PATCH "silently drops letta_memory_core tools."ensure_ren_tools() skips it and logs Tools not found in Letta registry: … (line 328). Step 5's sync_mcp_tools() prevents this.get_session_brief (called Letta back 4×) and search_signals. Before attaching any tool, check its implementation for a callback into Letta's agent loop (e.g. letta.send_chat_message(...)). Reading/writing core blocks via the management API (letta.read_core_block / letta.write_core_block) is safe — that's how read_core_memory / write_core_memory already work.| Tool | Data path | Safe to attach? |
|---|---|---|
read_portrait | letta.read_core_block (management API) | ✅ Clean |
read_all_portraits | letta.read_core_block (management API) | ✅ Clean |
write_portrait | letta.write_core_block / create_core_block | ✅ Clean |
read_maya_history | db.list_sessions_by_agent (pure DB read) | ✅ Clean |
add_portrait_signal | reads/writes core block then letta.send_chat_message() | ⛔ Gotcha D |
add_portrait_signal injects a chat message into Ren's own agent (portraits.py), with hardcoded text "Claude just added a portrait signal…". If Ren calls it on herself mid-turn it risks a deadlock and misattributes authorship. Fix required before attaching: make it caller-aware — skip the self-notification (and correct the attribution) when Ren is the caller. Then add its name to the canonical set.
sync_mcp_tools() registers any that are missing, so the runbook covers it either way.