Skip to content

feat: expose runtime add/remove of native Go tools on *Kit (AddTools/RemoveTools/SetExtraTools) #74

Description

@ezynda3

Feature Description

There is no public API on *Kit to add, remove, or replace native Go tools on a live host after construction. The native tool set is frozen at host-build time via Options.Tools / Options.ExtraTools, and the only post-construction lever for native tools is per-call PromptOptions.ExtraTools (additive-only, single-call, reverted immediately after the call).

This is asymmetric with the two other tool-bearing subsystems, both of which are dynamically mutable on a live *Kit:

Subsystem Add at runtime Remove at runtime Enumerate
MCP servers AddMCPServer, AddInProcessMCPServer RemoveMCPServer GetMCPToolCount, ListMCPServers
Skills AddSkill, LoadAndAddSkill RemoveSkill, DisableSkill GetSkills
Native Go tools (only per-call PromptOptions.ExtraTools) GetTools, GetToolNames (read-only)

I'd like persistent, session-level mutation of the native tool set on *Kit, e.g.:

func (m *Kit) AddTools(tools ...Tool)            // additive, persists for the session
func (m *Kit) RemoveTools(names ...string) error // remove previously-added tools by name
func (m *Kit) SetExtraTools(tools ...Tool)       // replace the extra-tool set wholesale
func (m *Kit) GetExtraTools() []Tool             // read back what's currently added

Motivation / Use Case

I'm building progressive disclosure / dynamic tool discovery on top of pkg/kit (the pattern from Anthropic's "Advanced Tool Use" and Cloudflare's "Code Mode"). The embedding app registers 100+ native Go tools (CRM = 29, YouTube = 17, Gmail = 10, Drive = 7, workspace = 8, agent-jobs = 6, …). Shipping every schema on every turn costs 15–30K tokens of always-on tool metadata before the user has typed anything.

The goal: keep a small always-on core (workspace, search, memory, run_code, subagent) in context, expose a search_tools(query) / enable_toolset(name) meta-tool, and have the agent materialize a domain's tools on demand — then ideally drop them again when the task moves on. This needs runtime add and remove of native tools on the same live host so session history, MCP connections, memory snapshot, and the frozen system-prompt snapshot all survive.

Today none of the available levers fit:

  1. PromptOptions.ExtraTools — additive-only, single-call, auto-reverted. The model can't "see" the tool as available across subsequent turns, and there's no removal. It's designed for one-shot augmentation, not session-scoped disclosure.
  2. Rebuild the whole host (buildKitHost again with a different Options.Tools) — throws away the session manager, live MCP servers, per-host OnPrepareStep hooks, and the frozen system-prompt/memory snapshot. Heavy and stateful-lossy.
  3. Wrap every toolset as an in-process MCP server (AddInProcessMCPServer / RemoveMCPServer) — works, but it's a large amount of plumbing to convert ~100 native kit.Tools into MCP servers, adds JSON-schema round-tripping + serialization overhead, and loses the ergonomics of native typed Go dispatch and our existing per-tool renderers keyed on native tool names. We're effectively forced to give up the native tool path purely to gain runtime mutability.

Proposed Implementation

The internal machinery for this already exists — this is a thin public exposure, not an architectural change. internal/agent/agent.go already has:

// internal/agent/agent.go
func (a *Agent) SetExtraTools(extraTools []fantasy.AgentTool) {
    a.extraTools = extraTools
    a.rebuildFantasyAgent()   // recomposes coreTools + MCP tools + extraTools
}

func (a *Agent) GetExtraTools() []fantasy.AgentTool { /* defensive copy */ }

and rebuildFantasyAgent() already recombines coreTools + mcpTools + extraTools and rebuilds the underlying fantasy.Agent. Crucially, pkg/kit/kit.go already calls these internally for the per-call path in applyPromptOptions:

// pkg/kit/kit.go — applyPromptOptions (per-call ExtraTools)
if len(opts.ExtraTools) > 0 {
    prev := m.agent.GetExtraTools()
    merged := append(append([]Tool{}, prev...), opts.ExtraTools...)
    m.agent.SetExtraTools(merged)
    restores = append(restores, func() { m.agent.SetExtraTools(prev) })
}

So persistent session-level methods are essentially the same calls without the restore step, plus a name-keyed remove. Sketch:

// pkg/kit/kit.go

// AddTools additively registers native tools on the live host. They persist
// for the session and are visible to the model on the next turn. Tools with a
// name already present are replaced (last-write-wins).
func (m *Kit) AddTools(tools ...Tool) {
    m.promptOptsMu.Lock()
    defer m.promptOptsMu.Unlock()
    cur := m.agent.GetExtraTools()
    byName := map[string]Tool{}
    for _, t := range cur      { byName[t.Info().Name] = t }
    for _, t := range tools    { byName[t.Info().Name] = t } // replace/add
    m.agent.SetExtraTools(values(byName))
}

// RemoveTools removes previously-added native tools by name. Core tools and
// MCP tools are unaffected. Returns an error naming any tool that wasn't found.
func (m *Kit) RemoveTools(names ...string) error {
    m.promptOptsMu.Lock()
    defer m.promptOptsMu.Unlock()
    cur := m.agent.GetExtraTools()
    drop := toSet(names)
    kept := cur[:0]
    for _, t := range cur {
        if !drop[t.Info().Name] { kept = append(kept, t) }
    }
    m.agent.SetExtraTools(kept)
    return missingNames(cur, names) // nil when all matched
}

// SetExtraTools replaces the entire extra-tool set in one call.
func (m *Kit) SetExtraTools(tools ...Tool) {
    m.promptOptsMu.Lock(); defer m.promptOptsMu.Unlock()
    m.agent.SetExtraTools(tools)
}

// GetExtraTools returns the currently-registered extra tools (read-only).
func (m *Kit) GetExtraTools() []Tool { return m.agent.GetExtraTools() }

Design notes / open questions for the maintainer:

  • Scope = extraTools only. These methods manage the extraTools slice, never coreTools or MCP tools, mirroring the existing per-call semantics. Options.Tools (full override) stays a construction-time concept.
  • Thread-safety. Reuse the existing promptOptsMu (or a dedicated toolsMu) so a mutation can't race a per-call applyPromptOptions override or a mid-generation rebuildFantasyAgent. Worth documenting whether mutation during an in-flight Prompt takes effect on the current turn or the next.
  • Interaction with per-call PromptOptions.ExtraTools. Since both write extraTools, the per-call restore must snapshot/restore around the current persistent set (it already does via GetExtraTools()), so the two compose cleanly — but this should be called out in docs.
  • Name collisions. Last-write-wins on Info().Name seems least-surprising; alternatively reject duplicates with an error. Either is fine — just needs to be specified.
  • Session persistence. Native tool definitions aren't serialized into session state today (only MCP server configs / skills are re-resolvable). Re-adding tools on session resume would remain the embedder's responsibility; that's acceptable and matches how ExtraTools already behaves.

This unblocks native-tool progressive disclosure without forcing embedders onto the in-process-MCP detour, and it closes the asymmetry where MCP and Skills are runtime-mutable but native Go tools are not.

Alternatives Considered

  • I've considered workarounds or alternative approaches (per-call PromptOptions.ExtraTools, full host rebuild, and wrapping toolsets as in-process MCP servers — all detailed above).

Checklist


Environment: github.com/mark3labs/kit v0.80.0. Code references are line-accurate against that tag (internal/agent/agent.go SetExtraTools/GetExtraTools/rebuildFantasyAgent; pkg/kit/kit.go applyPromptOptions, GetTools, GetToolNames, AddMCPServer/RemoveMCPServer/AddInProcessMCPServer).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    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