21 Commits

Author SHA1 Message Date
Ed Zynda 49f8b485be feat(extensions): add OnLLMUsage, SetState, enriched AgentEndEvent (#53) (#54)
* feat(extensions): add OnLLMUsage, SetState, enriched AgentEndEvent (#53)

Three additive primitives to the extension API:

- OnLLMUsage event: per-LLM-call token + cost deltas attributed to the
  specific model/provider used for each round-trip. Derived from the SDK
  StepFinishEvent in the extension bridge. Enables accurate budget
  enforcement between calls instead of only at turn boundaries.

- ctx.SetState / GetState / DeleteState / ListState: session-scoped,
  last-write-wins key-value store backed by a sidecar file
  (<session>.ext-state.json) outside the conversation tree. Reads are
  O(1), writes don't grow the JSONL, and the store is not duplicated on
  fork. State is preserved across hot-reloads.

- Enriched AgentEndEvent: ToolCallCount, ToolNames, LLMCallCount, token
  deltas (input/output/cache-read/cache-write), CostDelta, and
  DurationMs populated by a per-turn aggregator. Existing handlers
  reading only Response/StopReason are unaffected.

Includes unit tests for the state store, LLMUsage registration,
enriched AgentEndEvent, turn aggregator, llmUsageMeta, and sidecar path
derivation. Adds examples/extensions/usage-budget.go demoing all three
primitives together. Documents the additions in README, the docs site
(extensions overview, capabilities, examples), and the kit-extensions
and kit-sdk skill guides.

Fixes #53

* fix(extensions): address review feedback on state store and llmUsageMeta

- Serialize SetState/DeleteState saver invocations through a new saverMu
  so overlapping atomic-rename writes can no longer race on the shared
  .tmp file and persist an older snapshot after a newer one.
- LoadStateFromFile now clears the in-memory store when the sidecar is
  missing or empty, matching the documented "replace … with its
  contents" contract. This makes session-switching safe by preventing
  keys from a prior session leaking into a new one. Tests updated to
  cover both the missing-file and empty-file cases.
- llmUsageMeta now detects Anthropic OAuth credentials and returns
  Cost=0, matching the comment and the existing usage_tracker behavior
  for OAuth users. Mirrors the OAuth detection already used in
  cmd/extension_context.go.
- Document the single-in-flight-turn assumption baked into the
  per-turn aggregator with a clear migration path (per-turn ID) for if
  concurrent turns ever become a supported use case.

* fix(extensions): release saverMu on panic in state store

Extract a runSaver helper that locks saverMu and defers Unlock before
invoking the persistence callback. Without the deferred Unlock, a panic
inside the saver (e.g. disk full mid-write) would leave saverMu held
forever and deadlock the next SetState/DeleteState. Both SetState and
DeleteState now route through the helper. New TestRunner_State_Saver
PanicReleasesSaverMu reproduces the deadlock window with a 2s deadline
and proves the mutex is released after a panic.
2026-06-09 16:18:10 +03:00
Ed Zynda 558fb5214f feat(sdk): expose remaining Fantasy lifecycle callbacks as events and hooks
Closes #19.

SDK events (pkg/kit):
- Add 10 new event types: StepStart, StepFinish, TextStart, TextEnd,
  ReasoningStart, Warnings, Source, StreamFinish, Error, Retry
- Add typed convenience subscribers for all 31 event types (20 previously
  required raw Subscribe + type assertion)
- Add OnPrepareStep hook for intercepting/replacing messages between
  steps within a multi-step turn (composes with existing steering)
- Rename OnStreaming to OnMessageUpdate (deprecated alias kept)

Agent internals (internal/agent):
- Add GenerateCallbacks struct replacing 16 positional callback params
- Add GenerateWithCallbacks method; deprecate GenerateWithLoopAndStreaming
- Wire all Fantasy stream callbacks: OnStepStart, OnTextStart/End,
  OnReasoningStart, OnWarnings, OnSource, OnStreamFinish, OnError,
  OnRetry, OnStepFinish (unified step event)
- Compose PrepareStep with steering channel + consumer hook

Extension system (internal/extensions):
- Add 8 new extension events: StepStart, StepFinish, ReasoningStart,
  Warnings, Source, Error, Retry, PrepareStep
- Bridge SDK events to extension runner with Yaegi-safe types (string
  errors, plain int64 token fields, ContextMessage for PrepareStep)

Docs: update README, SDK skill, www/sdk/callbacks, www/sdk/overview
2026-04-22 20:25:06 +03:00
Ed Zynda 4ba9d6fab3 feat(events): mirror Fantasy tool input streaming callbacks as Kit events
- Add ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent to SDK
- Wire Fantasy OnToolInputStart/Delta/End through agent to EventBus
- Add typed convenience subscribers: OnToolCallStart/Delta/End on Kit
- Bridge new events to TUI via ToolCallInputStart/Delta/End app events
- Extend extension system with OnToolCallInputStart/Delta/End handlers
- Add extension event types, API methods, loader wiring, Yaegi symbols
- Update docs: README, SDK skill, extensions skill, www/sdk, www/extensions

Closes #16
2026-04-21 23:28:13 +03:00
Ed Zynda 13d177e5d0 fix(extensions): use structured logging that respects log levels
Switch from standard log.Printf to charmbracelet/log for extension loading
messages. This ensures DEBUG output only appears when explicitly enabled.

- Remove unconditional WARN log for failed extension loads
- Convert DEBUG loaded extension message to structured log.Debug call
2026-04-08 00:39:21 +03:00
Ed Zynda b2bd016135 fix(tui): redirect log output to file to prevent TUI corruption
- Add tea.LogToFile in runInteractiveModeBubbleTea to send stdlib log
  output to /tmp/kit/kit.log instead of stderr
- Replace charmbracelet/log with stdlib log in extensions loader,
  runner, watcher, prompts loader, and pkg/kit so all log calls go
  through the redirected stdlib logger
- Leave charmbracelet/log in CLI-only commands (install, acp) and
  acpserver where stderr logging is correct
2026-04-07 21:20:04 +03:00
Ed Zynda 265fd2ec0c fix(extensions): skip _test.go files and non-extension examples/ subdirs
- Filter out _test.go files in findExtensionsInDir, findExtensionsInRepo,
  and ScanForExtensions to prevent Yaegi from loading test files
- Narrow examples/ traversal so only recognized extension directories
  (extensions/, ext/, *-ext/, *-extensions/) are scanned, not arbitrary
  subdirs like examples/sdk/ that import pkg/kit
2026-04-04 16:44:13 +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 ab3ce260c8 feat: add subagent monitoring extension with horizontal widget layout
Add new extension API hooks for tracking spawned subagents:
- OnSubagentStart, OnSubagentChunk, OnSubagentEnd events
- Extensions bridge for forwarding child subagent events

Create subagent-monitor.go extension:
- Displays horizontally-stacked widgets above input box
- Shows real-time scrolling output from each subagent
- Yaegi-safe: no sync.Mutex, no goroutines, nil-guarded context calls
- Race-free design with on-demand elapsed time calculation

Add comprehensive tests:
- SessionStart, SubagentLifecycle, MultipleSubagents, SessionShutdown

Update symbols.go to export new event types for Yaegi interpreter.
2026-03-26 13:38:06 +03:00
Ed Zynda 3e1c19442b feat(extensions): expose ToolOutputEvent to extensions API
Extensions can now subscribe to streaming tool output events using
OnToolOutput(), giving them the same power as the internal TUI to
observe and react to tool execution in real-time.

Changes:
- Add ToolOutputEvent struct to extensions API
- Add ToolOutput constant to EventType
- Add OnToolOutput() handler registration method
- Add event bridging from kit to extensions runner
- Export ToolOutputEvent in Yaegi symbols
- Add OnToolOutput() to public SDK (pkg/kit)

Example usage in an extension:
  api.OnToolOutput(func(e ext.ToolOutputEvent, ctx ext.Context) {
    ctx.PrintInfo(fmt.Sprintf("%s: %s", e.ToolName, e.Chunk))
  })
2026-03-22 20:28:30 +03:00
Ed Zynda 5b214b9fdf refactor: Use huh for CLI prompts, fix extension discovery in mixed repos
- Replace custom multi-select with huh.NewMultiSelect for kit install --select
- Replace raw bufio prompts in cmd/auth.go with huh.NewConfirm and huh.NewInput
- Fix extension discovery to use opinionated conventions (only scan root,
  extensions/, ext/, examples/extensions/ directories, skip cmd/internal/pkg/)
- Fix loader to use same convention-based scanning for installed git repos
- Fix errcheck lint warning in loader.go
2026-03-18 16:49:48 +03:00
Ed Zynda c5e6ca6e4d feat: Add kit install command for git-based extension distribution
Add comprehensive extension installation system for Kit:

Features:
- kit install <git-url> - Install extensions from git repos
- kit install <url> --local - Install to project .kit/git/ directory
- kit install <url> --select - Interactive selection for multi-extension repos
- kit install <url> --update - Update installed extensions
- kit install <url> --uninstall - Remove installed extensions
- Version pinning via @ref (tags, branches, commits)
- Support multiple URL formats (shorthand, git:, https, ssh)

Implementation:
- internal/extensions/installer.go - Git clone, checkout, validation
- internal/extensions/manifest.go - Package tracking with Include filtering
- internal/extensions/loader.go - Respect Include field when loading
- cmd/install.go - Cobra command with interactive prompts
- PromptMultiSelectConfig API - Multi-select prompts for extensions

Storage:
- Global: ~/.local/share/kit/git/<host>/<owner>/<repo>/
- Project: .kit/git/<host>/<owner>/<repo>/
- Manifests: packages.json tracking installed packages

Examples:
- Reorganized examples/extensions/ with README.md
- Added status-tools/ multi-file extension example
- Created comprehensive install guide in SKILL.md

Testing:
- Added installer_test.go with 15+ test cases
- All tests pass, build clean

Closes #extension-distribution
2026-03-18 16:21:31 +03:00
Ed Zynda aede76d807 feat: add TUI suspend, custom message rendering, and extension hot-reload
- ctx.SuspendTUI(callback): releases terminal for interactive subprocesses
  (vim, shell, htop), automatically restores TUI when callback returns.
  Uses BubbleTea v2 ReleaseTerminal/RestoreTerminal.

- api.RegisterMessageRenderer(config) + ctx.RenderMessage(name, content):
  named render functions for branded/styled extension output. Renderers
  receive content and terminal width, return ANSI-styled strings.

- ctx.ReloadExtensions(): hot-reloads all extensions from disk. Emits
  SessionShutdown to old extensions, reloads source, emits SessionStart
  to new. Event handlers, commands, renderers, shortcuts update immediately.
  TUI command list refreshes via WidgetUpdateEvent. Extension tools are
  NOT updated (baked into agent at creation, documented limitation).

New example extensions: interactive-shell.go, branded-output.go, dev-reload.go
2026-03-02 19:32:19 +03:00
Ed Zynda 9e1df38836 feat: add keyboard shortcuts, tool context, and ToolCallEvent source field
- RegisterShortcut(ShortcutDef, handler) for global keyboard shortcuts
  that fire across all non-modal app states (after ctrl+c, before
  component dispatch). Handlers run in goroutines for safe blocking calls.
- ToolContext with IsCancelled/OnProgress for rich tool execution;
  ExecuteWithContext on ToolDef takes priority over simple Execute.
- Source field on ToolCallEvent (currently "llm", forward-compatible
  with future user-initiated tool calls).
- Fix missing //go:build ignore on context-inject.go.
- Update plan-mode.go to register ctrl+alt+p shortcut.
2026-03-02 19:04:37 +03:00
Ed Zynda 8f5efee837 feat: add session before-hooks (OnBeforeFork, OnBeforeSessionSwitch) and compaction event (OnBeforeCompact)
Add three new extension events that allow extensions to gate destructive
session operations and compaction:

- OnBeforeFork: fires before branching in the tree selector; handler can
  cancel with reason (e.g. dirty-repo guard)
- OnBeforeSessionSwitch: fires before /new resets the session branch;
  handler can cancel with reason
- OnBeforeCompact: fires before context compaction (auto or manual);
  handler receives token stats and IsAutomatic flag, can cancel

Includes SDK hook registry (beforeCompact), extension bridge, UI
callbacks threaded through AppModelOptions, and two example extensions:
- confirm-destructive.go: git dirty check + fork confirmation
- compact-notify.go: compaction notification + auto-compact gating
2026-03-02 16:35:00 +03:00
Ed Zynda a392d3e572 feat: add OnContextPrepare event for context window filtering and injection
Extensions can now register an OnContextPrepare handler that fires after
the context window is built from the session tree and before messages are
sent to the LLM. Handlers receive ContextMessage entries with positional
indices and can filter, reorder, or inject messages. Original messages
referenced by index preserve tool calls, reasoning, and other complex
parts. New context-inject example extension demonstrates injecting a
local .kit/context.md file as an ephemeral system message every turn.
2026-03-02 15:56:08 +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 23c16bb197 feat: add tool mgmt, model mgmt, options, event bus, LLM completion, steer mode, and 10 example extensions
Phase 2+3 extension API additions:
- Tool management: GetAllTools, SetActiveTools (plan-mode support)
- Model management: SetModel, GetAvailableModels, ModelChangedEvent
- Extension options: RegisterOption, GetOption, SetOption (env/config/default)
- Inter-extension event bus: OnCustomEvent, EmitCustomEvent
- Direct LLM completion: ctx.Complete with streaming/blocking modes
- Steer delivery mode: CancelAndSend for interrupt-and-redirect

New example extensions (10):
- plan-mode.go: read-only exploration with /plan toggle
- summarize.go: conversation summarization via ctx.Complete
- bookmark.go: persistent bookmarks via AppendEntry/GetEntries
- auto-commit.go: auto-commit on exit using last assistant message
- permission-gate.go: confirm dangerous bash commands
- protected-paths.go: block writes to .env, .git/, secrets/
- notify.go: desktop notifications on agent completion
- inline-bash.go: !{cmd} expansion in prompts
- pirate.go: system prompt persona injection
- project-rules.go: load .kit/rules/*.md into system prompt

Always-wrap tools through runner for SetActiveTools disabled-tool checking.
Removed phase1/phase2 test extensions from examples.
2026-03-02 14:31:35 +03:00
Ed Zynda 98efaae960 feat: add custom tool rendering for extensions
Extensions can now override how tool calls are displayed in the TUI via
API.RegisterToolRenderer(). Supports custom display name, border color,
background color, header parameter formatting, body rendering, and
optional markdown processing of custom body output.
2026-02-28 16:23:45 +03:00
Ed Zynda fb3608326f add unrestricted stdlib symbols to Yaegi interpreter
os/exec lives in yaegi's stdlib/unrestricted package, not the default
stdlib.Symbols. Without it, extensions importing os/exec fail with
'unable to find source'. This is needed for the /run command example
and any extension that spawns subprocesses.
2026-02-27 00:43:31 +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 f42d487214 add Yaegi-based in-process extension system with event handlers, tool/command registration, and styled output
Implement a Pi-style extension system where plain .go files are loaded at
runtime via Yaegi. Extensions register typed event handlers against 13
lifecycle events (tool_call, tool_result, input, before_agent_start, etc.)
using concrete-type-only API methods to avoid Yaegi interface panics.

Key capabilities:
- Tool interception: block calls, modify results (wrapper pattern)
- Input handling: transform or fully handle user input (skip agent)
- System prompt injection via BeforeAgentStartResult
- Custom tool and slash command registration
- Styled output: ctx.Print, PrintInfo, PrintError, PrintBlock
- Legacy hooks.yml compatibility via adapter
- Auto-discovery from ~/.config/kit/extensions/ and .kit/extensions/
- CLI: kit extensions list|validate|init, --no-extensions, -e flags
- 58 unit tests covering runner, loader (Yaegi), wrapper, events
2026-02-27 00:08:48 +03:00