rename spawn_subagent tool to subagent; remove redundant toolDisplayNames map

Tool rename (breaking change for ToolName string comparisons in event handlers):
- internal/core/subagent.go: Name field 'spawn_subagent' → 'subagent'
- internal/extensions/wrapper.go: update coreToolKinds map key
- pkg/kit/events.go: update coreToolKinds map key and ToolKindSubagent comment
- pkg/kit/extensions_bridge.go: update three ToolName == ... guards
- internal/ui/tool_renderers.go: update two toolName == ... case guards
- internal/ui/stream.go: remove special-case branch (toolName is now already
  'subagent', so the title-case fallback produces 'Subagent' naturally)

Comments/docs updated everywhere (no logic changes):
- internal/core/tools.go, internal/extensions/api.go, events.go
- pkg/kit/kit.go, tools.go
- examples/extensions/subagent-test.go, kit-telegram/main.go
- README.md, skills/kit-sdk/SKILL.md
- www/pages/advanced/subagents.md, extensions/capabilities.md
- www/pages/index.md, sdk/callbacks.md
- www/public/session/index.html (tracked UI asset)

Redundant toolDisplayNames map removed (item #14):
- internal/ui/messages.go: delete the 7-entry map whose every value was
  identical to what the title-case fallback already produced; simplify
  toolDisplayName() to just the fallback
This commit is contained in:
Ed Zynda
2026-03-29 00:24:18 +03:00
parent 879e81f9b5
commit f36166bee5
21 changed files with 51 additions and 68 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in
## Features
- **Multi-Provider LLM Support**: Anthropic, OpenAI, Google Gemini, Ollama, Azure OpenAI, AWS Bedrock, OpenRouter, and more
- **Built-in Core Tools**: bash, read, write, edit, grep, find, ls, spawn_subagent - no MCP overhead
- **Built-in Core Tools**: bash, read, write, edit, grep, find, ls, subagent - no MCP overhead
- **MCP Integration**: Connect external MCP servers for expanded capabilities
- **Extension System**: Write custom tools, commands, widgets, and UI modifications in Go
- **Theming**: 22 built-in color themes (KITT, Catppuccin, Dracula, Nord, etc.) with runtime switching, persistence, and custom theme files
+1 -1
View File
@@ -908,7 +908,7 @@ func summarizeToolAction(toolName string, inputJSON string) string {
return "searching " + getStr("pattern", "text")
case "ls":
return "listing " + getStr("path", "directory")
case "spawn_subagent":
case "subagent":
return "spawning subagent"
default:
return "using " + toolName
+1 -1
View File
@@ -37,7 +37,7 @@ func Init(api ext.API) {
"Subagent Test Extension loaded\n\n" +
"/subtest <task> Spawn blocking subagent\n" +
"/subbg <task> Spawn background subagent\n\n" +
"The LLM can also use the spawn_subagent tool.")
"The LLM can also use the subagent tool.")
})
api.OnAgentEnd(func(_ ext.AgentEndEvent, ctx ext.Context) {
+5 -5
View File
@@ -28,14 +28,14 @@ type SubagentSpawnResult struct {
// SubagentSpawnFunc is a callback that spawns an in-process subagent. The
// parent Kit instance injects this into the context so the core tool can
// call back without importing pkg/kit (which would create a cycle).
// The toolCallID parameter is the LLM-assigned ID of the spawn_subagent
// The toolCallID parameter is the LLM-assigned ID of the subagent
// tool call, enabling the parent to correlate subagent events.
type SubagentSpawnFunc func(ctx context.Context, toolCallID, prompt, model, systemPrompt string, timeout time.Duration) (*SubagentSpawnResult, error)
type subagentCtxKey struct{}
// WithSubagentSpawner stores a spawn function in the context so that the
// spawn_subagent core tool can create in-process subagents.
// subagent core tool can create in-process subagents.
func WithSubagentSpawner(ctx context.Context, fn SubagentSpawnFunc) context.Context {
return context.WithValue(ctx, subagentCtxKey{}, fn)
}
@@ -49,7 +49,7 @@ func getSubagentSpawner(ctx context.Context) SubagentSpawnFunc {
}
// ---------------------------------------------------------------------------
// spawn_subagent tool
// subagent tool
// ---------------------------------------------------------------------------
type subagentArgs struct {
@@ -59,11 +59,11 @@ type subagentArgs struct {
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
}
// NewSubagentTool creates the spawn_subagent core tool.
// NewSubagentTool creates the subagent core tool.
func NewSubagentTool(opts ...ToolOption) fantasy.AgentTool {
return &coreTool{
info: fantasy.ToolInfo{
Name: "spawn_subagent",
Name: "subagent",
Description: `Spawn a subagent to perform a task autonomously.
The subagent runs as a separate in-process Kit instance with full tool access
+1 -1
View File
@@ -86,7 +86,7 @@ func ReadOnlyTools(opts ...ToolOption) []fantasy.AgentTool {
}
}
// SubagentTools returns all core tools except spawn_subagent. This prevents
// SubagentTools returns all core tools except subagent. This prevents
// infinite recursion when a subagent is itself a Kit instance.
func SubagentTools(opts ...ToolOption) []fantasy.AgentTool {
return []fantasy.AgentTool{
+5 -5
View File
@@ -1022,7 +1022,7 @@ func (a *API) OnToolResult(handler func(ToolResultEvent, Context) *ToolResultRes
a.onToolResult(handler)
}
// OnSubagentStart registers a handler that fires when a spawn_subagent tool
// OnSubagentStart registers a handler that fires when a subagent tool
// call begins executing. Use the ToolCallID to correlate with subsequent
// OnSubagentChunk and OnSubagentEnd events for the same subagent.
func (a *API) OnSubagentStart(handler func(SubagentStartEvent, Context)) {
@@ -1037,7 +1037,7 @@ func (a *API) OnSubagentChunk(handler func(SubagentChunkEvent, Context)) {
a.onSubagentChunk(handler)
}
// OnSubagentEnd registers a handler that fires when a spawn_subagent call
// OnSubagentEnd registers a handler that fires when a subagent call
// completes. ErrorMsg is non-empty when the subagent failed.
func (a *API) OnSubagentEnd(handler func(SubagentEndEvent, Context)) {
a.onSubagentEnd(handler)
@@ -2046,9 +2046,9 @@ func (BeforeCompactResult) isResult() {}
// Subagent lifecycle events (exposed to Yaegi — concrete structs)
// ---------------------------------------------------------------------------
// SubagentStartEvent fires when a spawn_subagent tool call begins executing.
// SubagentStartEvent fires when a subagent tool call begins executing.
type SubagentStartEvent struct {
// ToolCallID is the LLM-assigned ID of the spawn_subagent tool call.
// ToolCallID is the LLM-assigned ID of the subagent tool call.
// Use this to correlate SubagentChunkEvent and SubagentEndEvent.
ToolCallID string
// Task is the task description passed to the subagent.
@@ -2088,7 +2088,7 @@ type SubagentChunkEvent struct {
func (e SubagentChunkEvent) Type() EventType { return SubagentChunk }
// SubagentEndEvent fires when a spawn_subagent tool call completes.
// SubagentEndEvent fires when a subagent tool call completes.
type SubagentEndEvent struct {
// ToolCallID matches the SubagentStartEvent.ToolCallID for this subagent.
ToolCallID string
+2 -2
View File
@@ -72,7 +72,7 @@ const (
// cancel compaction by returning Cancel=true.
BeforeCompact EventType = "before_compact"
// SubagentStart fires when a spawn_subagent tool call begins executing.
// SubagentStart fires when a subagent tool call begins executing.
// Carries the tool call ID and the task description.
SubagentStart EventType = "subagent_start"
@@ -80,7 +80,7 @@ const (
// subagent: text chunks, tool calls, tool results, etc.
SubagentChunk EventType = "subagent_chunk"
// SubagentEnd fires when a spawn_subagent tool call completes (success
// SubagentEnd fires when a subagent tool call completes (success
// or error). Carries the final response and any error message.
SubagentEnd EventType = "subagent_end"
)
+1 -1
View File
@@ -49,7 +49,7 @@ var coreToolKinds = map[string]string{
"ls": "read",
"grep": "search",
"find": "search",
"spawn_subagent": "agent",
"subagent": "agent",
}
// toolKindFor returns the ToolKind for a given tool name, defaulting to
+2 -15
View File
@@ -41,27 +41,14 @@ type UIMessage struct {
Streaming bool
}
// toolDisplayNames maps raw tool names to human-friendly display names.
var toolDisplayNames = map[string]string{
"bash": "Bash",
"read": "Read",
"write": "Write",
"edit": "Edit",
"grep": "Grep",
"find": "Find",
"ls": "Ls",
}
// getTheme returns the current theme (helper for compact_renderer.go)
func getTheme() Theme {
return GetTheme()
}
// toolDisplayName returns a human-friendly display name for a tool.
// toolDisplayName returns a human-friendly display name for a tool,
// title-casing the first letter of the raw name.
func toolDisplayName(rawName string) string {
if display, ok := toolDisplayNames[rawName]; ok {
return display
}
if rawName != "" {
return strings.ToUpper(rawName[:1]) + rawName[1:]
}
-4
View File
@@ -650,10 +650,6 @@ func removeToolID(ids []string, id string) []string {
}
// formatToolExecutionMessage creates a descriptive spinner message for tool execution.
// For spawn_subagent, it shows simply as "Subagent".
func formatToolExecutionMessage(toolName string) string {
if toolName == "spawn_subagent" {
return "Subagent"
}
return toolName
}
+2 -2
View File
@@ -51,7 +51,7 @@ func renderToolBody(toolName, toolArgs, toolResult string, width int) string {
if body := renderBashBody(toolResult, width); body != "" {
return body
}
case toolName == "spawn_subagent":
case toolName == "subagent":
if body := renderSubagentBody(toolResult, width); body != "" {
return body
}
@@ -780,7 +780,7 @@ func renderToolBodyCompact(toolName, toolArgs, toolResult string, width int) str
case toolName == "bash" || toolName == "grep" || toolName == "find" ||
strings.Contains(toolName, "shell") || strings.Contains(toolName, "command"):
return renderBashCompact(toolResult, width)
case toolName == "spawn_subagent":
case toolName == "subagent":
return renderSubagentCompact(toolResult)
}
return ""
+5 -5
View File
@@ -70,7 +70,7 @@ const (
ToolKindEdit = "edit" // File modification (edit, write)
ToolKindRead = "read" // File reading (read, ls)
ToolKindSearch = "search" // Content/file search (grep, find)
ToolKindSubagent = "agent" // Subagent spawning (spawn_subagent)
ToolKindSubagent = "agent" // Subagent spawning (subagent)
)
// coreToolKinds maps built-in tool names to their kind. MCP and extension
@@ -83,7 +83,7 @@ var coreToolKinds = map[string]string{
"ls": ToolKindRead,
"grep": ToolKindSearch,
"find": ToolKindSearch,
"spawn_subagent": ToolKindSubagent,
"subagent": ToolKindSubagent,
}
// toolKindFor returns the ToolKind for a given tool name, defaulting to
@@ -216,7 +216,7 @@ type ToolResultEvent struct {
// ToolResultMetadata carries structured data from tool executions.
type ToolResultMetadata struct {
FileDiffs []FileDiffInfo `json:"file_diffs,omitempty"` // Present for edit/write tools
SubagentSessionID string `json:"subagent_session_id,omitempty"` // Present for spawn_subagent tool
SubagentSessionID string `json:"subagent_session_id,omitempty"` // Present for subagent tool
}
// FileDiffInfo describes a file modification from an edit or write tool.
@@ -457,13 +457,13 @@ func (s *subagentListenerSet) emit(event Event) {
//
// The listener receives the same event types as Subscribe() (ToolCallEvent,
// MessageUpdateEvent, etc.) but scoped to the child agent's activity. If the
// tool call ID doesn't correspond to an active or future spawn_subagent call,
// tool call ID doesn't correspond to an active or future subagent call,
// the listener simply never fires.
//
// Typical usage — register inside an OnToolCall handler:
//
// kit.OnToolCall(func(e kit.ToolCallEvent) {
// if e.ToolName == "spawn_subagent" {
// if e.ToolName == "subagent" {
// kit.SubscribeSubagent(e.ToolCallID, func(child kit.Event) {
// // real-time subagent events
// })
+5 -5
View File
@@ -126,9 +126,9 @@ func (m *Kit) bridgeExtensions(runner *extensions.Runner) {
// extension runner.
//
// Flow:
// ToolExecutionStartEvent(spawn_subagent) → emit SubagentStartEvent
// ToolExecutionStartEvent(subagent) → emit SubagentStartEvent
// → SubscribeSubagent → emit SubagentChunkEvents
// ToolResultEvent(spawn_subagent) → emit SubagentEndEvent
// ToolResultEvent(subagent) → emit SubagentEndEvent
//
// We use ToolExecutionStart (not ToolCall) for SubagentStart because that
// is when the subagent actually begins running. We use ToolResult for
@@ -146,7 +146,7 @@ func (m *Kit) bridgeExtensions(runner *extensions.Runner) {
// Intercept ToolCall to capture the task and subscribe to child events.
m.Subscribe(func(e Event) {
ev, ok := e.(ToolCallEvent)
if !ok || ev.ToolName != "spawn_subagent" {
if !ok || ev.ToolName != "subagent" {
return
}
@@ -201,7 +201,7 @@ func (m *Kit) bridgeExtensions(runner *extensions.Runner) {
if runner.HasHandlers(extensions.SubagentStart) {
m.Subscribe(func(e Event) {
ev, ok := e.(ToolExecutionStartEvent)
if !ok || ev.ToolName != "spawn_subagent" {
if !ok || ev.ToolName != "subagent" {
return
}
task := taskMu.get(taskByCallID, ev.ToolCallID)
@@ -216,7 +216,7 @@ func (m *Kit) bridgeExtensions(runner *extensions.Runner) {
if runner.HasHandlers(extensions.SubagentEnd) {
m.Subscribe(func(e Event) {
ev, ok := e.(ToolResultEvent)
if !ok || ev.ToolName != "spawn_subagent" {
if !ok || ev.ToolName != "subagent" {
return
}
task := taskMu.get(taskByCallID, ev.ToolCallID)
+3 -3
View File
@@ -1256,7 +1256,7 @@ type SubagentConfig struct {
SystemPrompt string
// Tools overrides the tool set. If nil, SubagentTools() is used (all
// core tools except spawn_subagent, preventing infinite recursion).
// core tools except subagent, preventing infinite recursion).
Tools []Tool
// NoSession, when true, uses an in-memory ephemeral session. When false
@@ -1330,7 +1330,7 @@ func (m *Kit) Subagent(ctx context.Context, cfg SubagentConfig) (*SubagentResult
systemPrompt = "You are a helpful coding assistant. Complete the task efficiently and thoroughly."
}
// Default tools: everything except spawn_subagent.
// Default tools: everything except subagent.
tools := cfg.Tools
if tools == nil {
tools = SubagentTools()
@@ -1438,7 +1438,7 @@ func (m *Kit) generate(ctx context.Context, messages []fantasy.Message) (*agent.
})
// Inject the in-process subagent spawner into the context so the
// spawn_subagent core tool can create child Kit instances without
// subagent core tool can create child Kit instances without
// importing pkg/kit (which would create an import cycle).
ctx = core.WithSubagentSpawner(ctx, func(
spawnCtx context.Context, toolCallID, prompt, model, systemPrompt string, timeout time.Duration,
+1 -1
View File
@@ -52,7 +52,7 @@ func CodingTools(opts ...ToolOption) []Tool { return core.CodingTools(opts...) }
// read, grep, find, ls.
func ReadOnlyTools(opts ...ToolOption) []Tool { return core.ReadOnlyTools(opts...) }
// SubagentTools returns all core tools except spawn_subagent. Use this when
// SubagentTools returns all core tools except subagent. Use this when
// creating child Kit instances (in-process subagents) to prevent infinite
// recursion.
func SubagentTools(opts ...ToolOption) []Tool { return core.SubagentTools(opts...) }
+4 -4
View File
@@ -251,7 +251,7 @@ Tools are classified by kind for UI rendering:
- `ToolKindEdit` = `"edit"` — edit, write
- `ToolKindRead` = `"read"` — read, ls
- `ToolKindSearch` = `"search"` — grep, find
- `ToolKindSubagent` = `"agent"`spawn_subagent
- `ToolKindSubagent` = `"agent"` — subagent
---
@@ -358,7 +358,7 @@ kit.NewLsTool(opts...) // directory listing
kit.AllTools(opts...) // all 7 core tools
kit.CodingTools(opts...) // bash, read, write, edit
kit.ReadOnlyTools(opts...) // read, grep, find, ls
kit.SubagentTools(opts...) // all except spawn_subagent (prevents recursion)
kit.SubagentTools(opts...) // all except subagent (prevents recursion)
```
### Tool options
@@ -514,7 +514,7 @@ result, err := host.Subagent(ctx, kit.SubagentConfig{
Prompt: "Analyze the test files and summarize coverage",
Model: "anthropic/claude-haiku-3-5-20241022", // empty = parent's model
SystemPrompt: "You are a test analysis expert.",
Tools: nil, // nil = SubagentTools() (all except spawn_subagent)
Tools: nil, // nil = SubagentTools() (all except subagent)
NoSession: true, // ephemeral
Timeout: 2 * time.Minute, // 0 = 5 minute default
OnEvent: func(e kit.Event) {
@@ -532,7 +532,7 @@ result, err := host.Subagent(ctx, kit.SubagentConfig{
```go
host.OnToolCall(func(e kit.ToolCallEvent) {
if e.ToolName == "spawn_subagent" {
if e.ToolName == "subagent" {
host.SubscribeSubagent(e.ToolCallID, func(child kit.Event) {
// Real-time events scoped to this subagent
})
+6 -6
View File
@@ -32,12 +32,12 @@ Key flags for subprocess usage:
Positional arguments are the prompt. `@file` arguments attach file content as context.
## Built-in spawn_subagent tool
## Built-in subagent tool
Kit includes a built-in `spawn_subagent` tool that the LLM can use to delegate tasks to independent child agents:
Kit includes a built-in `subagent` tool that the LLM can use to delegate tasks to independent child agents:
```
spawn_subagent(
subagent(
task: "Analyze the test files and summarize coverage",
model: "anthropic/claude-haiku-latest", // optional
system_prompt: "You are a test analysis expert.", // optional
@@ -61,7 +61,7 @@ result := ctx.SpawnSubagent(ext.SubagentConfig{
### Monitoring subagents from extensions
When the LLM (not the extension itself) spawns a subagent using the `spawn_subagent` tool, extensions can monitor its activity in real-time using three lifecycle event handlers:
When the LLM (not the extension itself) spawns a subagent using the `subagent` tool, extensions can monitor its activity in real-time using three lifecycle event handlers:
```go
// Track active subagents and display their output
@@ -147,11 +147,11 @@ result, err := host.Subagent(ctx, kit.SubagentConfig{
### Real-time subagent events
Use `SubscribeSubagent` to receive real-time events from LLM-initiated subagents (i.e., when the model uses the `spawn_subagent` tool). Register inside an `OnToolCall` handler using the tool call ID:
Use `SubscribeSubagent` to receive real-time events from LLM-initiated subagents (i.e., when the model uses the `subagent` tool). Register inside an `OnToolCall` handler using the tool call ID:
```go
host.OnToolCall(func(e kit.ToolCallEvent) {
if e.ToolName == "spawn_subagent" {
if e.ToolName == "subagent" {
host.SubscribeSubagent(e.ToolCallID, func(event kit.Event) {
switch ev := event.(type) {
case kit.MessageUpdateEvent:
+1 -1
View File
@@ -239,7 +239,7 @@ result := ctx.SpawnSubagent(ext.SubagentConfig{
### Monitoring subagents spawned by the main agent
When the LLM uses the built-in `spawn_subagent` tool, extensions can monitor the subagent's activity in real-time using three lifecycle events:
When the LLM uses the built-in `subagent` tool, extensions can monitor the subagent's activity in real-time using three lifecycle events:
```go
// Subagent started
+1 -1
View File
@@ -13,7 +13,7 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in
## Features
- **Multi-Provider LLM Support** — Anthropic, OpenAI, Google Gemini, Ollama, Azure OpenAI, AWS Bedrock, OpenRouter, and more
- **Built-in Core Tools** — bash, read, write, edit, grep, find, ls, spawn_subagent with no MCP overhead
- **Built-in Core Tools** — bash, read, write, edit, grep, find, ls, subagent with no MCP overhead
- **MCP Integration** — Connect external MCP servers for expanded capabilities
- **Extension System** — Write custom tools, commands, widgets, and UI modifications in Go
- **Interactive TUI** — Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering
+2 -2
View File
@@ -74,11 +74,11 @@ The first argument is a priority (lower = runs first).
## Subagent event monitoring
Monitor real-time events from LLM-initiated subagents (when the model uses the `spawn_subagent` tool):
Monitor real-time events from LLM-initiated subagents (when the model uses the `subagent` tool):
```go
host.OnToolCall(func(e kit.ToolCallEvent) {
if e.ToolName == "spawn_subagent" {
if e.ToolName == "subagent" {
host.SubscribeSubagent(e.ToolCallID, func(event kit.Event) {
// Receives the same event types as Subscribe(), scoped to the child agent
switch ev := event.(type) {
+2 -2
View File
@@ -1566,7 +1566,7 @@ a:hover { text-decoration: underline; }
'grep': '🔍',
'find': '📁',
'ls': '📂',
'spawn_subagent': '🤖',
'subagent': '🤖',
'fetch': '🌐',
'todo': '✅'
};
@@ -1612,7 +1612,7 @@ a:hover { text-decoration: underline; }
headerLabel = formatLsHeader(input);
bodyHtml = renderGenericBody(input, result);
break;
case 'spawn_subagent':
case 'subagent':
headerLabel = formatSubagentHeader(input);
bodyHtml = renderSubagentBody(input, result);
break;