Commit Graph

93 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 685aaf207f feat(extensions): add hot-reload with file watching and /reload-ext command
- Add fsnotify-based file watcher that auto-reloads extensions on .go
  file changes in autoloaded dirs with 300ms debounce
- Add /reload-ext built-in command (alias /re) for manual reload
- Add Agent.SetExtraTools() so extension tools update on reload
  instead of being baked in at agent creation time
- Run reload async via tea.Cmd to avoid prog.Send() deadlock when
  extension handlers call ctx.Print() during SessionStart/Shutdown
- Wire watcher lifecycle into cmd/root.go with graceful shutdown
2026-04-02 15:41:54 +03:00
Ed Zynda c9637090fa feat(subagent): return early error for invalid model instead of silent fallback
- Add ValidateModelString() to ModelsRegistry for format, provider,
  and model name validation with typo suggestions
- Validate model in Kit.Subagent() before expensive Kit.New() setup
- Remove silent fallback to parent model on creation failure
- Error propagates as tool result so calling agent can self-correct
- Add registry_test.go covering format, provider, and suggestion cases
2026-04-02 14:45:03 +03:00
Ed Zynda 7d2f078111 fix(ui): freeze reasoning counter when last token is processed
- Wire fantasy's OnReasoningEnd callback through the full event chain:
  agent → SDK (ReasoningCompleteEvent) → app → TUI
- Freeze reasoning duration in both StreamComponent and
  StreamingMessageItem as soon as reasoning ends, not when the
  next assistant text chunk arrives
- Fix accent color on duration label in render.ReasoningBlock to
  match the live streaming style (VeryMuted prefix + Accent duration)
2026-04-02 14:18:42 +03:00
Ed Zynda cbb7387a72 fix(test): add return after t.Fatal to silence SA5011 nil-deref warnings
- internal/ui/model_test.go: bashItem nil check
- pkg/extensions/test/harness_test.go: footer and result nil checks
2026-04-01 21:24:02 +03:00
Ed Zynda 6d4e8bcec5 feat: add streaming support for compaction summaries
- Add StreamCallback parameter to compaction.Compact() for streaming text deltas
- Update generateSummary() to use fantasy.Agent.Stream() when callback provided
- Fix compactSplitTurn() to stream both history and turn prefix summaries
- Add SDK event subscription in CompactConversation() goroutine
- Update UI to handle streaming compaction like regular assistant messages
- Compaction summaries now stream word-by-word instead of appearing all at once

Fixes issue where compaction would show incomplete context (e.g. only 'nce')
by ensuring both history summary and turn prefix are streamed to the UI.
2026-03-31 17:33:51 +03:00
Ed Zynda d79eb1f0fa refactor(pkg/kit): use fantasy type aliases for LLM types with clean SDK names
Replace concrete LLMMessage/LLMUsage/LLMResponse/LLMFilePart structs with
type aliases to charm.land/fantasy types, exposing them under clean
LLM-prefixed names. This gives SDK consumers full access to rich message
parts (tool calls, reasoning, tool results) without importing fantasy
directly.

Key changes:
- LLM types are now aliases: LLMMessage=fantasy.Message, etc.
- Added aliases for all part types: LLMTextPart, LLMToolCallPart, etc.
- Re-exported constructors: NewLLMUserMessage, NewLLMSystemMessage
- Removed lossy conversion helpers (llm_convert.go, fantasyMsgsToKit)
- Updated all internal packages to use aliases consistently
- Added ACP smoke test script and prompt template
- Fixed lint issues: unused vars, modernize min() usage
2026-03-31 14:26:49 +03:00
Ed Zynda ac8ee6525d refactor(pkg/kit): replace fantasy type aliases with concrete LLM* structs
Remove charm.land/fantasy from the public API surface of pkg/kit by
replacing the four type aliases with concrete Kit-owned structs:

- LLMMessage  {Role LLMMessageRole, Content string}
- LLMUsage    {InputTokens, OutputTokens, TotalTokens, ...}
- LLMResponse {Content, FinishReason, Usage}
- LLMFilePart {Filename, Data []byte, MediaType}

Add LLMMessageRole type with user/assistant/system/tool constants.

Introduce pkg/kit/llm_convert.go as the single boundary layer where
Kit types convert to/from fantasy types internally. All callers in
pkg/kit, pkg/kit/compaction.go, pkg/kit/extensions_bridge.go, and
internal/app/app.go cross through this layer.

ContextPrepareHook.Messages and ContextPrepareResult.Messages change
from []fantasy.Message to []LLMMessage. extensions_bridge.go drops
its fantasy and strings imports entirely.

internal/app/app_test.go switches &fantasy.Usage{} to &kit.LLMUsage{}.

Add seven new tests in types_test.go covering concrete construction,
role constants, JSON snake_case tags, and round-trip conversion.
2026-03-31 13:44:05 +03:00
Ed Zynda 5aa6c9e116 chore: fix all golangci-lint v2 issues
- Fix gofmt formatting issues in 7 files
- Replace atomic.AddUint64 with atomic.Uint64 type (modernize)
- Replace for i := 0; i < count; i++ with for i := range count (modernize)
- Replace strings.Split with strings.SplitSeq (modernize)
- Replace deprecated GetFantasyProviders with GetLLMProviders
- Replace deprecated GetFantasyMessages with GetLLMMessages
- Replace deprecated ConvertFromFantasyMessage with ConvertFromLLMMessage
- Replace deprecated FromFantasyMessage with FromLLMMessage
- Replace deprecated ToFantasyMessages with ToLLMMessages
- Remove 2 unused formatToolArgs functions
2026-03-29 14:36:03 +03:00
Ed Zynda fd6f200659 refactor: clean up self-referential comments in caching code
Remove internal monologue comments that don't add value for readers:
- Remove lengthy explanations of type conflicts that are now resolved
- Remove 'NOTE:' and 'TODO:' comments documenting implementation history
- Remove obvious test comments that just restate what the code does
- Keep only meaningful comments that explain design intent

The code is now cleaner and easier to read without the self-referential
commentary that was useful during development but not for maintenance.
2026-03-29 14:28:29 +03:00
Ed Zynda b295a25946 feat: automatic prompt caching for cost reduction
Implements automatic prompt caching to reduce API costs by 60-90% for
repeated prompts with the same context.

Architecture:
- Provider-level caching for OpenAI (PromptCacheKey)
- Message-level caching for Anthropic (avoids type conflicts)
- Model family detection enables caching regardless of provider

Key Changes:
- Add ModelInfo.Family with SupportsCaching() and CacheType() methods
- Add ProviderConfig.DisableCaching for opt-out
- Implement message-level cache control in agent (like Crush)
  - Last system message gets cache control
  - Last 2 messages get cache control
  - Last tool gets cache control
- Auto-disable caching when thinking is enabled (type conflict avoidance)
- Add KIT_DISABLE_CACHE environment variable for global opt-out

Tested with opencode/claude-sonnet-4-6 showing cacheRead/cacheWrite
tokens in debug output, confirming 60-90% cost savings.

Closes cost optimization for multi-turn conversations.
2026-03-29 14:24:07 +03:00
Ed Zynda f0e4e2f757 refactor: remove Fantasy dependency name leakage from public SDK and docs
Rename public SDK symbols to use generic LLM terminology instead of
exposing the internal dependency name (charm.land/fantasy):

Public API renames (with deprecated wrappers for backward compat):
- ConvertToFantasyMessages() → ConvertToLLMMessages()
- ConvertFromFantasyMessage() → ConvertFromLLMMessage()
- GetFantasyProviders() → GetLLMProviders()

New type alias:
- LLMFilePart = fantasy.FilePart (eliminates need for direct fantasy import)
- PromptResultWithFiles() signature now uses LLMFilePart

Internal renames (with deprecated wrappers):
- ModelsRegistry.GetFantasyProviders() → GetLLMProviders()
- TreeManager.GetFantasyMessages() → GetLLMMessages()
- TreeManager.AppendFantasyMessage() → AppendLLMMessage()
- TreeManager.AddFantasyMessages() → AddLLMMessages()
- Message.ToFantasyMessages() → ToLLMMessages()
- FromFantasyMessage() → FromLLMMessage()
- npmToFantasyProvider → npmToLLMProvider
- isProviderFantasySupported() → isProviderLLMSupported()

All internal callers migrated to new names. ~30 comments updated
to remove Fantasy references across pkg/kit/, internal/agent/,
internal/models/, internal/message/, internal/session/.

Documentation updates:
- AGENTS.md: added Public SDK rules section (no dependency leakage,
  naming conventions, deprecation pattern)
- README.md: removed Fantasy references
- pkg/kit/README.md: full rewrite with current API surface
- skills/kit-sdk/SKILL.md: updated examples and type references
- www/pages/providers.md, www/pages/cli/commands.md: updated
2026-03-29 14:01:57 +03:00
Ed Zynda 971521f534 Group Extension* methods behind ExtensionAPI interface
- Create ExtensionAPI interface with all extension-related methods
- Add extensionAPI type that wraps *Kit and implements the interface
- Add Kit.Extensions() method to access the ExtensionAPI
- Remove ~30 Extension* methods from Kit (breaking SDK change)
- Update all internal callers (cmd/, internal/acpserver/) to use Extensions().Method()
- Extensions themselves unaffected (use kit/ext API via Yaegi)

This cleans up the Kit API surface while maintaining full extension functionality.
2026-03-29 13:19:51 +03:00
Ed Zynda 8c00682367 Rename Fantasy* types to LLM* and remove GenerateResult alias
- FantasyMessage -> LLMMessage
- FantasyUsage -> LLMUsage
- FantasyResponse -> LLMResponse
- Remove confusing GenerateResult = TurnResult alias
- Update documentation in SKILL.md
2026-03-29 13:11:55 +03:00
Ed Zynda 58caf155c1 pkg/: internal cleanups - shared iterator, per-instance skill cache, exported EntryID
kit.go
- Extract iterBranchMessages helper to eliminate ~15 lines of duplicated
  branch-fetch/type-assert boilerplate between GetSessionMessages and
  GetStructuredMessages
- Move skillCache from package-level global to per-Kit field; avoids
  cross-contamination when multiple Kit instances exist in same process

skills.go
- Remove globalSkillCache var and skillCache type definition
- Update DiscoverSkillsForExtension and ClearSkillCache to use m.skillCache
- Remove unused sync import

sessions.go
- Use m.treeSession.EntryID instead of local getEntryID duplicate
- Remove local getEntryID function (was missing LabelEntry, SessionInfoEntry,
  CompactionEntry types that internal/session.TreeManager.EntryID handles)

internal/session/tree_manager.go
- Export entryID -> EntryID so pkg/kit can use it directly
- Update all internal callers to use EntryID

config.go
- Add sync comment for defaultSystemPrompt noting it should be kept in sync
  with CLI default in cmd/root.go

hooks_test.go
- Add newEmptyHookedTool helper for tests that need hookedTool with empty
  hook registries
- Update TestHookedTool_Passthrough and TestHookedTool_InfoDelegates to use
  helper (saves ~6 lines of boilerplate each)
- Merge TestHookRegistry_HasHooks into TestHookRegistry_Unregister (was
  testing same behavior, now just one initial state assertion added)

All changes tested with opencode/kimi-k2.5 exploring the repo in tmux.
6 files changed, 69 insertions(+), 98 deletions(-)
2026-03-29 13:00:33 +03:00
Ed Zynda 3f08bf2424 pkg/kit: SDK quality-of-life improvements
Replace var function aliases with proper func wrappers (types.go)
- ParseModelString, CreateProvider, GetGlobalRegistry, LoadSystemPrompt
  were package-level vars, making them reassignable and rendering oddly
  in go doc. Now plain func wrappers with matching signatures.

Fix Subagent() double-error return convention
- Was returning both (*SubagentResult{Error: err}, err) simultaneously.
  Now returns (nil, err) on failure, consistent with Go conventions.
- Removed SubagentResult.Error field; errors come from the error return.
- Updated all call sites in cmd/root.go, internal/acpserver, and kit.go.

Fix NavigateTo/SummarizeBranch/CollapseBranch string-encoded errors
- All three returned "" or an error string instead of error values,
  making it impossible to distinguish success from failure in SummarizeBranch
  (empty string meant both "no content" and "LLM failed").
- NavigateTo: string -> error
- SummarizeBranch: string -> (string, error)
- CollapseBranch: string -> error
- Updated cmd/root.go bridge closures to use err != nil and err.Error().

Remove duplicate GetSessionFilePath (use GetSessionPath)
- GetSessionPath (sessions.go) and GetSessionFilePath (kit.go) were
  identical. Removed GetSessionFilePath; updated cmd/root.go and
  internal/acpserver to call GetSessionPath directly.
2026-03-29 12:51:04 +03:00
Ed Zynda 9fbbab05f6 pkg/: simplify code without altering public API
events.go
- Delete subagentListenerSet (verbatim duplicate of eventBus); reuse
  *eventBus in SubscribeSubagent and getSubagentListenerSet

hooks.go
- Add early-exit in run() when hooks slice is empty, making all
  hasHooks() guard call sites in kit.go and compaction.go redundant

kit.go
- Remove four if m.X.hasHooks() { m.X.run(...) } outer guards
  (beforeTurn, contextPrepare, afterTurn x2); run() now short-circuits
- Replace goto drained with an idiomatic return inside default: branch
- Replace stdlib log.Printf with charmlog.Debug (charmbracelet/log),
  consistent with the rest of the codebase; remove "log" import

config.go
- Collapse single-element configNames := []string{".kit"} loop into a
  direct viper.SetConfigName call (removes slice, for, break, flag)

auth.go
- Fix GetOpenAIAPIKey: it documented OPENAI_API_KEY env var fallback but
  never called os.Getenv; now it does

compaction.go
- Extract persistAndEmitCompaction helper; eliminates duplicated
  AppendCompaction + events.emit block in compactInternal and
  applyCustomCompaction
- Replace fmt.Errorf("%s", reason) with errors.New(reason)
- Name the 16384 magic number as const defaultReserveTokens

skills.go
- Fix broken double-checked lock in DiscoverSkillsForExtension: the
  read-unlock -> write-lock gap had a TOCTOU race; replaced with a
  single write-lock covering the check and load
- Remove dead nil guard in convertSkills (convertSkill never returns nil)
- Rename convertSkills parameter skills->skillList to avoid shadowing
  the skills package import

extensions_bridge.go
- Delete taskMutex struct (sync.Mutex wrapper with map passed as param);
  replace with inline var taskMu sync.Mutex at the use site
- Simplify AgentEnd double-if into a single combined := declaration

template_bridge.go
- Fix RenderTemplate: use varRegex.ReplaceAllStringFunc instead of
  two-pass strings.ReplaceAll; handles arbitrary whitespace in {{var}}
- Remove dead isFlag function and simplify ParseArguments guard
  (the outer !HasPrefix guard made isFlag always return false)
- Cache matchModelPattern compiled regexps in a sync.Map to avoid
  repeated regexp.Compile on hot streaming paths

pkg/extensions/test/mock.go
- Remove dead local StatusBarEntry type (duplicate of extensions type,
  never referenced)
- Change make([]T, 0) to nil for nine slice fields in NewMockContext

pkg/extensions/test/harness.go
- Remove MustLoad (no callers outside the package)
- Remove extPath field (assigned but never read)
- Remove redundant os.Stat in LoadFile (os.ReadFile already errors)

events_test.go
- Add five missing event types to TestEventTypes table
  (Compaction, ReasoningDelta, ToolOutput, StepUsage, SteerConsumed)
- Expand TestEventOrdering from 11 to 16 events with the same types
- Add a got < 0 assertion to TestEventBusConcurrentSubscribeEmit so the
  test can actually fail rather than only logging
2026-03-29 12:39:19 +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 879e81f9b5 remove deprecated API methods: GetExtRunner, GetBufferedLogger, GetAgent, PromptWithCallbacks
These methods have been deprecated since the narrow-accessor and event-
subscriber APIs were introduced. No callers exist in this repository.

- pkg/kit/kit.go: remove GetExtRunner(), GetBufferedLogger(), GetAgent(),
  and PromptWithCallbacks(); update Subscribe() doc comment which still
  mentioned PromptWithCallbacks; tighten section header comment
- pkg/kit/README.md: replace PromptWithCallbacks example with the
  OnToolCall/OnToolResult/OnStreaming subscriber pattern; remove method
  from the quick-reference list
- README.md: same example migration in the SDK section
- www/pages/sdk/callbacks.md: remove the PromptWithCallbacks section
  entirely; the event-based monitoring section that followed it is now
  the lead content
- www/pages/sdk/overview.md: remove PromptWithCallbacks row from the
  prompt-variant table
- skills/kit-sdk/SKILL.md: remove the deprecated legacy callback snippet
2026-03-29 00:05:09 +03:00
Ed Zynda dcfebafcc5 fix: correct token usage and cost tracking for multi-step tool calls
This commit fixes several issues with token usage tracking:

1. Fix InputTokens-only validation bug - now checks any token field > 0
   to handle OpenAI-compatible providers where cached prompts result in
   InputTokens=0 while OutputTokens>0

2. Remove per-step context token updates from recordStepUsage() - context
   fill is now set once at turn completion via updateUsageFromTurnResult
   using FinalUsage.InputTokens, preventing display jumps during multi-step
   tool calls

3. Track maximum context seen in SetContextTokens() - prevents the status
   bar from showing decreasing token counts when FinalUsage.InputTokens
   reflects only the last step's input

4. Add comprehensive debug logging for token tracking at key points:
   - StepUsageEvent emission
   - recordStepUsage processing
   - updateUsageFromTurnResult processing

5. Update tests to reflect new behavior:
   - TestRecordStepUsage_updatesTracker: no longer expects context updates
   - TestUpdateUsageFromTurnResult_contextTokensUsesInputOnly: verifies
     InputTokens-only tracking

All tests pass. Token tracking now correctly accumulates costs and shows
monotonically increasing context size.
2026-03-28 17:49:31 +03:00
Ed Zynda 4caa8ba3dc Bridge SDK features to extension system: tree navigation, skills, templates, model resolution
This commit bridges 4 categories of internal SDK capabilities to the extension
system, enabling extensions like pi-prompt-template-model to be built with
minimal custom code.

New Extension APIs:

Tree Navigation (Phase 1):
- GetTreeNode, GetCurrentBranch, GetChildren - Navigate conversation tree
- NavigateTo - Branch/fork to specific entries
- SummarizeBranch - LLM-based branch summarization
- CollapseBranch - Fresh context primitive for context management

Skill Loading (Phase 2):
- LoadSkill, LoadSkillsFromDir - Load skill files with YAML frontmatter
- DiscoverSkills - Auto-discover from standard locations
- InjectSkillAsContext, InjectRawSkillAsContext - Pre-load skills

Template Parsing (Phase 3):
- ParseTemplate, RenderTemplate - {{variable}} substitution
- ParseArguments, SimpleParseArguments - CLI-style arg parsing (, , )
- EvaluateModelConditional, RenderWithModelConditionals - Model conditionals

Model Resolution (Phase 4):
- ResolveModelChain - Fallback chain resolution
- GetModelCapabilities - Query model specs
- CheckModelAvailable, GetCurrentProvider, GetCurrentModelID

Files Modified:
- internal/extensions/api.go - New types and Context methods
- internal/extensions/symbols.go - Export to Yaegi
- internal/extensions/runner.go - No-op stubs
- pkg/kit/sessions.go - Tree navigation bridge
- pkg/kit/skills.go - Skill loading bridge
- pkg/kit/template_bridge.go - NEW - Template & model resolution
- cmd/root.go - Wire to extension Context

Examples Added:
- conversation-manager.go - Tree nav, branch collapse, fresh context loops
- prompt-templates.go - Frontmatter templates with model switching
- bridge_demo.go - All new APIs demonstration

Documentation Updated:
- README.md - New capabilities and examples
- www/pages/extensions/capabilities.md - Full API docs
- www/pages/extensions/examples.md - New example category
- skills/kit-extensions/SKILL.md - Extension developer docs
2026-03-28 12:00:19 +03:00
Ed Zynda d9326fcf21 fix: auto-initialize extension context in kit.New()
Extensions were being loaded automatically by SetupAgent but the context
was never initialized unless the SDK user explicitly called
SetExtensionContext. This left extensions with a zero-value Context where
all function fields are nil.

Now kit.New() automatically calls SetExtensionContext with minimal defaults
(CWD, Model, Interactive=false) when extensions are loaded. SDK users can
still call SetExtensionContext to override with richer implementations
(TUI callbacks, prompts, etc.).

Combined with the normalizeContext() safety net in the runner, extensions
are now guaranteed to work in SDK mode without explicit context wiring.
2026-03-27 15:54:56 +03:00
Ed Zynda 3cb3e5dba1 Fix missing system prompt when switching models in interactive mode
When using /model command to switch models, the system prompt was not being
passed to the new provider config. This caused OpenAI Codex to fail with
"Instructions are required" error.

- Load system prompt using config.LoadSystemPrompt() in SetModel
- Pass SystemPrompt to ProviderConfig when building model config
- This ensures Codex OAuth gets the instructions field it requires
2026-03-26 15:06:32 +03:00
Ed Zynda bbf1106e27 Add OpenAI ChatGPT/Codex OAuth authentication alongside Anthropic auth
Implements OAuth authentication for OpenAI ChatGPT Plus/Pro (Codex) similar to pi:

- Add OpenAICredentials type with OAuth and API key support
- Add OpenAI OAuth client with correct endpoints (auth.openai.com)
- Implement PKCE-based OAuth flow with local callback server on :1455
- Add login/logout/status commands for openai provider
- Support both ChatGPT/Codex OAuth tokens (chatgpt.com/backend-api) and
  regular OpenAI API keys (api.openai.com)
- Extract and store ChatGPT account ID from JWT token
- Add custom HTTP transport with required Codex headers:
  - chatgpt-account-id, originator, OpenAI-Beta: responses=experimental
- Update provider selection to use correct endpoint based on auth type

Usage:
  kit auth login openai    # OAuth with ChatGPT account
  kit auth logout openai
  kit auth status

The implementation follows the same patterns as the existing Anthropic OAuth
support, with automatic token refresh and secure credential storage in
~/.config/.kit/credentials.json
2026-03-26 14:50:15 +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 be55bc03f1 Add mid-turn steering with Ctrl+S 2026-03-26 12:10:14 +03:00
Ed Zynda 09919b6307 feat: update token usage after each step in multi-step turns
Previously, token usage and costs were only updated at the end of a complete
turn. For long-running multi-step tool-calling conversations, this meant the
status bar showed stale (or zero) costs during the entire interaction.

Now, after each complete step (tool call + result), the usage tracker is
updated with the actual token counts from that step. This provides real-time
cost accumulation visible in the status bar.

Changes:
- Add StepUsageHandler type and onStepUsage parameter to agent
- Emit StepUsageEvent from kit layer after each step completes
- Handle StepUsageEvent in app layer to update UsageTracker
- Add EventStepUsage constant and StepUsageEvent struct to events

The step usage is additive - each step's tokens are added to the running
session totals, just like the final turn usage was before.
2026-03-25 18:17:48 +03:00
Ed Zynda 329cd4ea4a feat: Add custom models via config file
Allow users to define custom models in ~/.kit.yml under the customModels
section. These models are automatically merged into the custom provider.

Example config:
  customModels:
    my-model:
      name: "My Custom Model"
      reasoning: true
      temperature: true
      cost:
        input: 0.002
        output: 0.004
      limit:
        context: 128000
        output: 32000

Usage:
  kit --model custom/my-model "Hello"
  kit --provider-url "http://localhost:8080" --model custom/my-model "Hello"

Note: When --provider-url is specified without --model, kit defaults to
custom/custom. When --provider-url is specified WITH a custom model from
config, that model is used.

Bug fixes:
- Fixed kit.New() re-loading config file and overriding CLI-specified config
- Fixed models command to reload registry for custom models
2026-03-24 14:19:49 +03:00
Ed Zynda 1e2a3e2589 fix: preserve completed messages in session on ESC cancellation
Previously, pressing ESC twice to cancel rolled back the entire tree
session to the pre-turn state, discarding the user message, completed
tool call/result pairs, and any streamed response. Content that had
already rendered in the TUI would vanish from the session history.

Now the cancellation path uses the same logic as the non-cancellation
error path: the user message (already persisted before generation) and
any completed step messages (fully-paired tool_use + tool_result from
OnStepFinish) are preserved. Only the in-progress pending message or
tool call is discarded.

This ensures that if a message has rendered in the TUI, it stays in
the history and session.
2026-03-23 17:51:22 +03:00
Ed Zynda 82cbf1d457 move SDK examples from pkg/kit/examples to examples/sdk
Relocate SDK usage examples to the top-level examples directory alongside
extension examples for better discoverability. Add README for the SDK
examples directory.
2026-03-23 16:02:54 +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 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 2d14b3461f feat: cancel tool calls with double-ESC without breaking API pairs
When the user presses ESC twice to cancel during a tool call, the entire
turn is now rolled back instead of persisting partial progress. This
ensures that tool_use and tool_result messages are always sent to the API
as matched pairs, avoiding errors from orphaned tool calls.

Changes:
- Save pre-turn leaf ID before appending user messages
- On context cancellation (double-ESC), branch back to pre-turn leaf
- On other errors (API failures), still persist partial progress
- Update app.go comments to reflect new behavior
2026-03-22 18:44:37 +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 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 ec4ac64343 fix: stop re-emitting subagent events onto parent event bus
The core tool spawner in generate() was unconditionally setting OnEvent
to re-emit every child event onto the parent Kit's event bus. This caused
subagent tool calls, streaming text, reasoning, and responses to surface
in the TUI as if they were the parent's own events.

Remove the OnEvent callback from the core tool spawner. The spawn_subagent
tool is a blocking call that returns a summary result — it doesn't need
real-time event streaming. SDK consumers who need real-time subagent events
can call Kit.Subagent() directly with their own OnEvent callback. The
extension and ACP paths are unaffected as they bridge to their own
callbacks independently.
2026-03-21 20:41:28 +03:00
Ed Zynda 4e66c0b4f7 feat: add session management features (picker, history, /name, /export, /import)
Fill session management gaps compared to pi:
- TUI session picker (/resume, --resume flag) with search, scope/filter
  toggles, delete, right-aligned metadata, and background-highlighted cursor
- Prompt history navigation via up/down arrows (100-entry ring buffer)
- /name <name> command now functional (was stubbed as not implemented)
- /export [path] exports session JSONL to file
- /import <path> loads session from JSONL file
- Remove deprecated unused App.runQueueItem method
2026-03-20 18:08:48 +03:00
Ed Zynda 131ce8f2cc Fix message batching and cancellation persistence
1. Batch queued messages into single agent turn
   - Add PromptResultWithMessages() to SDK for batch submission
   - Rewrite drainQueue() to collect all queued items and submit together
   - This prevents the agent from processing queued messages sequentially

2. Persist tool messages on cancellation
   - When generation is cancelled (double-ESC), persist any completed
     tool calls and results to the session before returning
   - Prevents the agent from re-doing work when user continues

Both issues caused the agent to lose context:
- Batching: Multiple queued messages now submitted as one turn
- Cancellation: Tool results from cancelled turns are preserved
2026-03-20 17:18:07 +03:00
Ed Zynda 4ae03aab7c refactor: move extension testing package to pkg/
Move the extension testing package from internal/extensions/test to
pkg/extensions/test to make it publicly importable by external extension
authors.

Changes:
- Moved test package files to pkg/extensions/test/
- Updated all imports from internal/ to pkg/ path:
  - README.md
  - examples/extensions/tool-logger_test.go
  - examples/extensions/extension_test_template.go
  - skills/kit-extensions/SKILL.md
  - www/pages/extensions/testing.md
  - pkg/extensions/test/README.md
  - pkg/extensions/test/harness.go

The test package is now available for external import as:
  github.com/mark3labs/kit/pkg/extensions/test

All tests pass with race detector.
2026-03-20 13:40:15 +03:00
Ed Zynda ef8628eecc fix: forward subagent events to parent event bus in core spawn_subagent tool
The spawner closure in generate() called m.Subagent() without setting
OnEvent, so child events (tool calls, text streaming, reasoning deltas)
were silently discarded. Wire OnEvent to re-emit on the parent's bus,
matching the behavior already present in the extension SpawnSubagent path.
2026-03-17 13:03:41 +03:00
Ed Zynda 3167222b72 fix: gracefully recover from bad model names in subagents
If the requested model fails (bad name, unsupported provider), fall
back to the parent's model instead of returning a hard error. The
original prompt is prepended with a note so the agent knows which
model is actually running and can adjust future calls.
2026-03-16 13:43:52 +03:00
Ed Zynda e3b37191b1 fix: inherit parent provider for bare model names in subagents
When spawn_subagent is called with a model name like 'claude-haiku'
(no provider prefix), prepend the parent's provider instead of letting
ParseModelString guess. Only full 'provider/model' strings bypass this.
2026-03-16 13:41:02 +03:00
Ed Zynda 3ad0b3616d fix: surface SubagentSessionID in ToolResultMetadata
The subagent_session_id was already attached to the fantasy response
metadata by internal/core/subagent.go but ToolResultMetadata had no
field for it, so json.Unmarshal silently dropped it. Add the field
so SDK consumers can detect subagent tools and load their sessions.
2026-03-16 13:01:34 +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 a05da5f3ab fix(auth): support OAuth credentials in ACP mode and auto-refresh tokens
Remove the early ValidateEnvironment gate from CreateProvider that only
checked env vars and --provider-api-key, blocking stored OAuth credentials
from working. Each provider creation function already handles its own auth
resolution with clear error messages.

Update ValidateEnvironment to also check stored Anthropic credentials so
the model selector UI correctly shows Anthropic models for OAuth users.

Add automatic token refresh in oauthTransport so long-lived ACP sessions
survive token renewals. Surface actionable auth error messages in ACP
session creation.

Fix pre-existing staticcheck SA5011 warnings in test files.
2026-03-15 12:38:23 +03:00
Ed Zynda 4c126ca41b feat(ui): show clean summary for subagent results instead of raw output
- Add custom renderer for spawn_subagent tool showing status + 3-line preview
- Pass toolArgs through ToolExecutionEvent to show task in spinner
- Display 'Subagent: <task>' during execution instead of generic message
- Compact mode shows concise one-line status summary
2026-03-14 17:04:50 +03:00
Ed Zynda c46687fc44 fix: pass image file parts through Fantasy agent's Files field
splitPromptAndHistory was extracting only text from the last user
message, discarding FilePart data (clipboard images). The fix extracts
both text and file parts, passing files via AgentStreamCall.Files and
AgentCall.Files so Fantasy includes them in the API request.

Also preserves file parts when BeforeTurn hooks or skill expansion
replace the user message text in runTurn.
2026-03-09 10:26:31 +03:00
Ed Zynda 4966c0ca2a feat: add clipboard image paste support (Ctrl+V)
Add multimodal image support so users can paste clipboard images into
prompts alongside text. Images are read from the system clipboard via
platform-specific tools and sent as fantasy.FilePart to the LLM API.

- New internal/clipboard package with platform-specific image readers:
  Linux: xclip (X11) with wl-paste (Wayland) fallback
  macOS: osascript with AppKit NSPasteboard
  Magic byte detection for PNG/JPEG/GIF/WebP/BMP/TIFF
- New ImageContent type in message model with full serialization and
  Fantasy bridge support (ImageContent <-> fantasy.FilePart)
- InputComponent handles Ctrl+V (paste image), Ctrl+U (clear images),
  shows attachment indicator, and carries images through submitMsg
- App layer queue upgraded from []string to []queueItem to carry files
  alongside prompts through the drain loop
- Kit SDK gains PromptResultWithFiles() for multimodal user messages
- AppController interface extended with RunWithFiles()
2026-03-08 11:37:21 +03:00
Ed Zynda f3ea18ae3a feat: add thinking model support with configurable reasoning levels
Add extended thinking/reasoning support for Anthropic and OpenAI models:

- ThinkingLevel type (off/minimal/low/medium/high) with token budgets
- Stream reasoning deltas via OnReasoningDelta through SDK→TUI event pipeline
- Render thinking blocks in StreamComponent (muted italic, collapsible)
- ctrl+t toggles thinking visibility, shift+tab cycles thinking level
- /thinking slash command with tab-completion for level names
- --thinking-level CLI flag and config file support
- Map ThinkingLevel to OpenAI ReasoningEffort for Responses API
- Auto-bump Anthropic max_tokens when thinking budget exceeds it
- Fix ResponseCompleteEvent prematurely resetting stream in streaming mode
- Status bar displays current thinking level
2026-03-07 21:27:46 +03:00