Commit Graph

24 Commits

Author SHA1 Message Date
Ed Zynda ead4afbfe6 fix(subagent): prevent instant failure from already-dead parent contexts
- 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
2026-04-02 15:54:47 +03:00
Ed Zynda 1cf24ee5de fix(core): return error when read tool is used on a directory
- Return an error response guiding the agent to use ls instead
- Remove unused readDirectory helper function
2026-04-02 14:45:33 +03:00
Ed Zynda 9c90563765 refactor: simplify code patterns and reduce duplication
- Extract isShellTool() helper in tool_renderers.go to eliminate
  duplicated shell tool matching logic
- Replace bannedCommands slice with compiled regex in bash.go for
  cleaner security validation
- Extract pathSet helper type in loader.go for reusable path
  deduplication
- Consolidate ac()/acOr() helpers in themes.go for better organization
- Total reduction: ~34 lines across 4 files

All tests pass (go test -race ./...) and build succeeds.
2026-03-29 01:18:27 +03:00
Ed Zynda f36166bee5 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
2026-03-29 00:24:18 +03:00
Ed Zynda 8b1665a4ce feat: add multi-edit support to edit tool
Implement multi-edit functionality matching Pi's approach:
- Add 'edits' array parameter for multiple disjoint replacements
- All edits matched against original content (non-incremental)
- Overlap detection prevents conflicting edits
- Duplicate detection ensures unique matches
- Atomic operations: all succeed or none applied
- Detailed error messages with edit indices (edits[0], etc.)
- Fuzzy matching works with multi-edit mode
- Backward compatible with single-edit mode (old_text/new_text)

Changes:
- internal/core/edit.go: Multi-edit logic, validation, overlap detection
- internal/ui/messages.go: Add 'edits' to body keys
- internal/ui/tool_renderers.go: Render multi-edit diffs
- internal/core/edit_test.go: 9 comprehensive multi-edit tests
2026-03-27 10:34:43 +03:00
Ed Zynda d1cffb85ef fix: prevent bash tool from hanging on long-running/background processes
- Use process group isolation (Setpgid) so the entire process tree is
  killed on timeout/cancellation, not just the direct child
- Set cmd.Cancel to kill the process group (-pgid) with SIGKILL
- Set cmd.WaitDelay (500ms grace period) to force-close pipes when
  grandchild processes hold them open after the direct child exits
- Convert buffered path from cmd.Run() to explicit pipes + cmd.Start()
  + cmd.Wait() so WaitDelay can properly force-close pipe handles
- Reorder streaming path: cmd.Wait() before wg.Wait() so the WaitDelay
  timer starts when the child exits, not after pipes close
- Add mutex for thread-safe chunk collection in streaming mode
- Add comprehensive tests for timeout, background processes, context
  cancellation, and both buffered/streaming paths
2026-03-24 15:13:35 +03:00
Ed Zynda 3fc0ad906e feat(ui): streaming bash output in TUI
Display streaming bash output in the TUI stream region as it arrives.

Changes:
- Add streaming bash output rendering to renderStream()
- Style stdout with CodeBg, stderr with Error color
- Add streamingMu mutex for thread-safe buffer access
- Clear buffers on ToolResultEvent
- Add ToolOutputEvent to event system (pkg/kit, internal/app)
- Add ToolOutputHandler callback in agent
- Implement streaming mode in bash tool with pipes
- Add tests for accumulation and clearing

The streaming output appears in real-time below the LLM streaming text
while bash commands are executing, with proper synchronization to
prevent race conditions between Update and Render methods.
2026-03-22 20:23:19 +03:00
Ed Zynda b99aafaeaa fix: correct fuzzy match position mapping and diff generation in edit tool
Two bugs fixed in internal/core/edit.go:

1. fuzzyMatch/mapFuzzyIndex returned wrong byte positions when trailing
   whitespace was stripped during normalization. The old rune-counting
   approach assumed 1:1 mapping between original and normalized strings,
   but whitespace trimming changes string length. This caused the
   replacement splice to cut at wrong boundaries, corrupting files.

   Fix: replaced with normalizeWithMap() that builds an explicit
   byte-position mapping during normalization. Also added ambiguity
   guard — multiple fuzzy matches now return no-match instead of
   silently picking the wrong one.

2. generateDiff marked the entire rest of the file as changed.
   The old code used countNewlines(old[changeIdx:]) to compute the
   diff range, which counted ALL newlines from the change point to EOF.

   Fix: replaced hand-rolled diff with udiff.Unified() (already a
   dependency) for correct standard unified diff output.

Added internal/core/edit_test.go with 33 tests covering fuzzyMatch
position mapping, normalizeWithMap correctness, generateDiff output,
and end-to-end executeEdit scenarios including corruption regression
tests.
2026-03-22 18:02:07 +03:00
Ed Zynda a55f6d3d9a feat: improved compaction with split-turn handling, file tracking, and non-destructive persistence
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
2026-03-22 17:14:50 +03:00
Ed Zynda 027c2de849 fix: detach subagent context from parent deadline
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.
2026-03-22 17:12:40 +03:00
Ed Zynda 25f17a104d fix: truncate long individual lines to prevent TUI blow-up
A single very long line (e.g. minified JSON, base64 blob) could wrap
into hundreds of visual rows in the TUI even when within the line-count
and byte-count limits.

Core layer (truncate.go):
- Add defaultMaxLineLen (2000 chars) per-line cap
- Apply truncateLongLines() in both TruncateTail and truncateHead
  before line/byte truncation
- Append '[N chars truncated]' marker to capped lines

UI layer:
- Cap lines in renderBashBody() to width*3 chars before rendering
- Cap lines in shell display handler (model.go) similarly

Add comprehensive tests in truncate_test.go.
2026-03-22 13:31:25 +03:00
Ed Zynda a1decf9cff feat: add SubscribeSubagent API for per-tool-call event streaming
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)
2026-03-21 20:48:40 +03:00
Ed Zynda a95117686e fix: override SHELL env var to bash in command execution
When the user's login shell is nushell, fish, or another non-bash shell,
the SHELL environment variable leaks through to child processes. This
causes tools like tmux to use the wrong shell for pane commands, leading
to failures (e.g. nushell rejects 'sleep 30' because it requires
'sleep 30sec').

Override SHELL to point to the resolved bash binary path in both the
bash tool (internal/core/bash.go) and the TUI shell command handler
(internal/ui/model.go) so child processes always use bash.
2026-03-21 18:47:16 +03:00
Ed Zynda 8831b49b51 feat: in-process subagents replace subprocess spawning
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)
2026-03-16 11:39:59 +03:00
Ed Zynda c94edc929b feat: add rich tool metadata to SDK and extension events (Gaps 1-8)
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.
2026-03-16 11:10:05 +03:00
Ed Zynda 424847f0db feat: enable parallel tool execution with multi-tool status display
- 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).
2026-03-14 17:24:20 +03:00
Ed Zynda bbd8975ca0 feat: add first-class subagent support for task delegation
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
2026-03-09 23:07:27 +03:00
Ed Zynda 393074447b fix: truncate shell command output in TUI using same limits as core bash tool 2026-03-05 19:24:49 +03:00
Ed Zynda 37e82781b1 feat: add OnModelChange event and ctx.Exit(); remove Gap/Pi references from comments 2026-03-02 14:49:51 +03:00
Ed Zynda c925a69aec fix: remaining staticcheck QF1012 WriteString(Sprintf) violations 2026-02-27 18:42:45 +03:00
Ed Zynda 25a3523c56 fix: use fmt.Fprintf instead of WriteString(Sprintf) (staticcheck QF1012) 2026-02-27 18:40:11 +03:00
Ed Zynda d8f40039fe export tools and tool factories with WithWorkDir option (Plan 01)
- Add ToolOption/WithWorkDir functional options pattern to internal/core
- Update all 7 tool constructors to accept ...ToolOption and resolve
  paths relative to the configured working directory
- Create pkg/kit/tools.go with public exports: individual constructors,
  bundles (AllTools, CodingTools, ReadOnlyTools), and WithWorkDir
- Add CoreTools field to AgentConfig/AgentCreationOptions so callers
  can inject custom tool sets instead of hardcoding core.AllTools()
- Add Tools field to kit.Options and GetTools() to kit.Kit
- Fully backward compatible: no-arg calls use os.Getwd() as before
2026-02-27 11:37:46 +03:00
Ed Zynda 6ac8d3983a add SendMessage to extension Context and fix all golangci-lint issues
SendMessage lets extensions inject messages into the conversation and
trigger new agent turns, enabling async patterns like background
subagent execution. It delegates to app.Run() which handles queueing.

CommandDef.Execute now receives Context so commands can access
SendMessage, Print*, and session metadata. The UI layer wraps the
call via runner.GetContext() at the boundary.

Also fixes all 20+ golangci-lint issues across the codebase:
errcheck, modernize (min/max/slices.Contains/SplitSeq/range-over-int),
staticcheck (error string casing), and unused code removal.
2026-02-27 00:41:48 +03:00
Ed Zynda 3f2a399e47 replace builtin MCP tools with native core tools and custom content blocks
Remove the entire internal/builtin package (bash, fetch, todo, http, fs
servers) and all inprocess/builtin transport support from config and
connection pool.

Add internal/core package with 7 direct fantasy.AgentTool implementations
matching pi's coding agent: bash, read, write, edit, grep, find, ls.
These execute in-process with zero MCP/JSON serialization overhead.

Add internal/message package with crush-inspired custom content blocks:
ContentPart interface with TextContent, ReasoningContent, ToolCall,
ToolResult, and Finish types. Messages carry heterogeneous Parts slices
with type-tagged JSON serialization for persistence and a ToFantasyMessages
bridge for LLM provider integration.

Core tools are always registered on the agent. External MCP servers remain
supported for additional tools, but MCP loading failures are now non-fatal
since core tools guarantee a working baseline.
2026-02-26 17:41:02 +03:00