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:
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.
- 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.
- 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
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).
Feature Description
There is no public API on
*Kitto add, remove, or replace native Go tools on a live host after construction. The native tool set is frozen at host-build time viaOptions.Tools/Options.ExtraTools, and the only post-construction lever for native tools is per-callPromptOptions.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:AddMCPServer,AddInProcessMCPServerRemoveMCPServerGetMCPToolCount,ListMCPServersAddSkill,LoadAndAddSkillRemoveSkill,DisableSkillGetSkillsPromptOptions.ExtraTools)GetTools,GetToolNames(read-only)I'd like persistent, session-level mutation of the native tool set on
*Kit, e.g.: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 asearch_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:
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.buildKitHostagain with a differentOptions.Tools) — throws away the session manager, live MCP servers, per-hostOnPrepareStephooks, and the frozen system-prompt/memory snapshot. Heavy and stateful-lossy.AddInProcessMCPServer/RemoveMCPServer) — works, but it's a large amount of plumbing to convert ~100 nativekit.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.goalready has:and
rebuildFantasyAgent()already recombinescoreTools + mcpTools + extraToolsand rebuilds the underlyingfantasy.Agent. Crucially,pkg/kit/kit.goalready calls these internally for the per-call path inapplyPromptOptions:So persistent session-level methods are essentially the same calls without the restore step, plus a name-keyed remove. Sketch:
Design notes / open questions for the maintainer:
extraToolsonly. These methods manage theextraToolsslice, nevercoreToolsor MCP tools, mirroring the existing per-call semantics.Options.Tools(full override) stays a construction-time concept.promptOptsMu(or a dedicatedtoolsMu) so a mutation can't race a per-callapplyPromptOptionsoverride or a mid-generationrebuildFantasyAgent. Worth documenting whether mutation during an in-flightPrompttakes effect on the current turn or the next.PromptOptions.ExtraTools. Since both writeextraTools, the per-call restore must snapshot/restore around the current persistent set (it already does viaGetExtraTools()), so the two compose cleanly — but this should be called out in docs.Info().Nameseems least-surprising; alternatively reject duplicates with an error. Either is fine — just needs to be specified.ExtraToolsalready 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
PromptOptions.ExtraTools, full host rebuild, and wrapping toolsets as in-process MCP servers — all detailed above).Checklist
ExtraTools).Environment:
github.com/mark3labs/kit v0.80.0. Code references are line-accurate against that tag (internal/agent/agent.goSetExtraTools/GetExtraTools/rebuildFantasyAgent;pkg/kit/kit.goapplyPromptOptions,GetTools,GetToolNames,AddMCPServer/RemoveMCPServer/AddInProcessMCPServer).