Skip to content

Rust: public per-session API to add / replace / remove client tools on a live Session #2027

Description

@colbylwilliams

Summary

Session lets an embedder supply externally-implemented tools at create time (SessionConfig.tools: Option<Vec<Tool>>, where each Tool may carry an Arc<dyn ToolHandler>), but there is no public way to change that tool set once the session is live. The per-session handler map is frozen exactly once, at create:

  1. Client::create_session collects each Tool's handler into runtime.tool_handlers, then builds SessionHandlers { tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)), .. } (src/session.rs, ~L901; identical in resume_session, ~L1166). After that the map is an immutable Arc<HashMap<String, Arc<dyn ToolHandler>>> and is never mutated.
  2. Dispatch of external_tool.requested does handlers.tools.get(&tool_name); on None it early-returns (src/session.rs, ~L1776) — there is no wildcard/fallback handler. So a tool that wasn't present at create is silently un-invokable, and a handler that was present at create can't be removed.
  3. Tool definitions reach the CLI only through the create/resume RPC (SessionConfig.tools). There is no public runtime path to add, replace, or remove definitions after create.
  4. The one runtime tool-registration wire method — sessions.registerExtensionToolsOnSession — is exposed only as SessionRpc::register_extension_tools_on_session, which is pub(crate), flagged Experimental, has no public caller, and whose own doc says it "disappears once extension discovery / launch / tool registration are owned by the runtime." Even if it were reached via the public Client::call, it wires up nothing in the in-process SessionHandlers.tools map, so a resulting tool.call still wouldn't reach any ToolHandler.

Net effect: an embedder can define/advertise externally-implemented tools only at create, and can neither add a newly-available tool nor remove an existing one mid-session.

Why this matters

An embedder that fronts multiple clients — or a single client whose published tool set changes over the life of a session — needs to reflect tool add/replace/remove on the live Session without tearing it down. Recreating the session to change its tool set destroys turn/session state and conversation history, which isn't acceptable for a long-lived session. This is the tool analogue of Session::set_model, which already lets the model change mid-session.

Current behavior (public source, for reference)

  • SessionConfig.tools: Option<Vec<Tool>> and ResumeSessionConfig.tools: Option<Vec<Tool>> are the only inputs for externally-implemented tools; both are consumed once, at create/resume.
  • Each Tool may carry an optional Arc<dyn ToolHandler>; those are collected into runtime.tool_handlers, then moved out via std::mem::take into the per-session SessionHandlers.tools map — built once, never mutated.
  • external_tool.requested dispatch: handlers.tools.get(&tool_name); None ⇒ early return (no fallback / wildcard handler).
  • The public Session surface has set_model, send, abort, rpc(), etc. — nothing for tools.
  • SessionRpc::register_extension_tools_on_session (wire sessions.registerExtensionToolsOnSession) is pub(crate) + Experimental + slated for removal, and does not populate the in-process handler map.

Proposed API

Public per-session methods on Session that update both the CLI-visible tool definitions (so the model can actually call newly-added tools and stops seeing removed ones) and the in-process handler map (so external_tool.requested routes back to the ToolHandler):

impl Session {
    /// Replace the session's externally-implemented tool set wholesale.
    /// Tools carrying a handler become invokable; any previously-registered
    /// tool not present here is removed.
    pub async fn set_tools(&self, tools: Vec<Tool>) -> Result<(), Error>;

    /// Add or replace individual tools by name (upsert), leaving others intact.
    pub async fn register_tools(&self, tools: Vec<Tool>) -> Result<(), Error>;

    /// Remove tools by name.
    pub async fn remove_tools(&self, names: &[String]) -> Result<(), Error>;
}

Requirements:

  • Update the CLI-visible tool definitions so the model gains/loses the tools for subsequent turns.
  • Update the SDK-side handler map (make the per-session tools map mutable behind the existing Arc — e.g. Arc<Mutex<..>> or an arc-swap — instead of a one-shot std::mem::take) so dispatch resolves against the current set.
  • Last-write-wins / idempotent semantics, and concurrency-safe with in-flight tool.calls.

The change is purely additive — create-time behavior is unchanged.

Alternatives considered

  • Call sessions.registerExtensionToolsOnSession via the public Client::call — not viable: it's pub(crate) / Experimental / slated for removal, and it doesn't touch the in-process handler map, so a subsequent tool.call still wouldn't reach a ToolHandler.
  • Recreate the session on every tool change — destroys turn/session state and history; not acceptable for a live session.

Related

  • Mutable sessions #941 ("Mutable sessions", closed as duplicate/tracked-elsewhere) captured the broad idea of mid-session mutability across tools, prompt, etc. This issue is the focused, tools-only slice with a concrete public API and the specific runtime blocker spelled out.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions