- Replace detachedWithCancel (goroutine-based) with context.WithoutCancel
+ valuesContext; the old goroutine would fire immediately if the parent
was already cancelled/deadline-exceeded, causing 'failed after 0s'
- Kit.Subagent() pre-flight: if the incoming ctx is already done, reset
to context.Background() before applying the subagent timeout
- Both Subagent() error paths now return a non-nil *SubagentResult with
Elapsed set, so the tool response always shows accurate timing
- Narrow viperInitMu scope in Kit.New(): snapshot viper state + call
BuildProviderConfig under the lock, then release before SetupAgent /
MCP loading; parallel subagent spawns no longer serialise on viper I/O
- AgentSetupOptions gains ProviderConfig + scalar fields so SetupAgent
can skip viper reads when a pre-built config is supplied
- Add subagent_test.go covering the fixed context detachment behaviour
Rework the compaction system with several improvements modelled after
pi's approach:
Compaction engine (internal/compaction):
- Tool result truncation: cap tool result text at 2000 chars during
serialisation to keep summarisation requests within token budgets
- Serialize tool calls and reasoning parts (previously only text)
- Split turn handling: when a single turn exceeds the keep budget,
summarise the turn prefix separately and merge with history summary
- Cumulative file tracking: extract read/modified files from tool calls
(read, write, edit, grep, find, ls) and carry forward across
compactions via PreviousCompaction parameter
- Add IsSplitTurn, findTurnStart helpers and CutPoint, ReadFiles,
ModifiedFiles fields to CompactionResult
Session tree (internal/session):
- New CompactionEntry type records summary, first-kept-entry-id, token
stats, and file lists without deleting old messages
- BuildContext skips entries before the compaction boundary and injects
the summary as a system message
- GetContextEntryIDs maps fantasy message indices to entry IDs for
cut-point resolution
- GetLastCompaction retrieves prior file tracking state
Non-destructive compaction (pkg/kit):
- Compact now appends a CompactionEntry instead of clearing and
rewriting the session — old messages remain on disk for history
- Extension hook (BeforeCompact) can now provide a custom Summary that
replaces the LLM-generated one, in addition to cancelling
UI (internal/ui):
- Tree selector renders CompactionEntry nodes with info styling
Events & hooks (pkg/kit):
- CompactionEvent includes ReadFiles and ModifiedFiles
- BeforeCompactResult gains Summary field for extension-provided summaries
- Bridge updated to forward custom summaries from extensions
The spawn_subagent tool was inheriting the parent's context deadline,
causing subagents to be killed prematurely (e.g. after ~120s instead of
the intended 5-minute default).
The parent LLM generation loop's context carries its own deadline which,
via Go's context.WithTimeout semantics (takes the minimum of parent
deadline and new timeout), would always win over the subagent's longer
timeout.
Add a detachedContext type that preserves context values (spawner func,
etc.) and propagates parent cancellation (Ctrl-C) but strips the
deadline. Applied only in the internal tool handler (executeSubagent) so
the public Kit.Subagent() SDK method continues to honor caller-provided
context deadlines.
Add Kit.SubscribeSubagent(toolCallID, listener) which lets SDK consumers
opt into real-time events from LLM-initiated subagents. Listeners are
keyed by the spawn_subagent tool call ID, which is available in the
ToolCallEvent before the subagent starts.
The typical pattern is:
kit.OnToolCall(func(e kit.ToolCallEvent) {
if e.ToolName == "spawn_subagent" {
kit.SubscribeSubagent(e.ToolCallID, func(child kit.Event) {
// real-time subagent events
})
}
})
Implementation:
- Thread toolCallID through SubagentSpawnFunc so generate() knows which
tool call triggered the spawn
- Add subagentListenerSet (per-tool-call event bus) stored in a sync.Map
on the Kit struct, keyed by toolCallID
- In generate(), wire OnEvent to dispatch to registered listeners only
when SubscribeSubagent has been called for that tool call
- Listeners are cleaned up automatically when the subagent completes
- No listeners registered = no OnEvent callback = no overhead (the
default TUI path)
Subagents now run as child Kit instances in the same process instead of
spawning a kit binary subprocess. This removes the binary dependency,
eliminates JSON serialization overhead, and enables SDK-only consumers
to use subagents without installing the kit CLI.
- Add Kit.Subagent() method for in-process subagent execution
- Add SubagentConfig/SubagentResult types to the SDK
- Add context-based SubagentSpawnFunc injection so core spawn_subagent
tool calls back to Kit.Subagent() without an import cycle
- Add SubagentTools() bundle (all core tools minus spawn_subagent)
- Add viperInitMu for thread-safe concurrent kit.New() calls
- Wire extension ctx.SpawnSubagent and ACP server to use in-process
- Child Kit gets parent's model as fallback, in-memory or persisted
session, and no extensions (preventing recursive loading)
Thread ToolCallID, ToolKind, ParsedArgs, FileDiff metadata, StopReason,
SessionID, and StructuredMessages across the SDK event bus, extension
wrapper, app bridge, hooks, and ACP server layers.
- Gap 1: ToolCallID from Fantasy's ToolCallContent threaded end-to-end
- Gap 2: ToolKind via static lookup (execute/edit/read/search/agent)
- Gap 3+4: FileDiffInfo with DiffBlocks via fantasy.ToolResponse.Metadata
- Gap 5: StopReason from Fantasy FinishReason on TurnEndEvent/TurnResult
- Gap 6: Subagent sessions now opt-out (NoSession); SessionID in JSON output
- Gap 7: GetStructuredMessages() returns typed ContentParts
- Gap 8: ParsedArgs map[string]any on tool events for convenience
Edit/write tools attach structured diff metadata. ACP server uses real
ToolCallIDs. Extension and SDK events kept in sync with matching fields.
- Mark read-only core tools as parallel-safe (read, grep, find, ls)
- Mark spawn_subagent as parallel-safe for concurrent task delegation
- Update UI to track multiple active tools during parallel execution
- Display 'Running: tool1, tool2, ...' in spinner for concurrent tools
- Add test for parallel tool execution scenarios
Fantasy already supports parallel execution via ToolInfo.Parallel field.
Tools marked parallel run concurrently (up to 5 at a time).
Implement 4-phase subagent system enabling LLM and extensions to spawn,
manage, and orchestrate child Kit instances for parallel task execution.
- Phase 1: SDK API with SpawnSubagent() for extensions
- Phase 2: spawn_subagent core tool for LLM usage
- Phase 3: Session hierarchy with ParentSessionID tracking
- Phase 4: Provider pooling for concurrent model access
New files:
- internal/extensions/subagent.go: SpawnSubagent implementation
- internal/core/subagent.go: Core tool definition
- internal/models/pool.go: Provider pool for concurrency
- examples/extensions/subagent-test.go: Test extension
- openspec/subagent-support.md: Design specification