Files
kit/internal/extensions/symbols.go
T
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

190 lines
8.7 KiB
Go

package extensions
import (
"reflect"
"github.com/traefik/yaegi/interp"
)
// Symbols returns the Yaegi export table that makes KIT's extension API
// available to interpreted Go code. Extensions import these types as:
//
// import "kit/ext"
//
// IMPORTANT: Only concrete types (structs, constants) are exported. Interfaces
// (Event, Result) and the HandlerFunc type are NOT exported because Yaegi
// cannot generate interface wrappers for them. Instead, extensions use
// event-specific methods like api.OnToolCall() which accept concrete function
// signatures.
func Symbols() interp.Exports {
return interp.Exports{
"kit/ext/ext": map[string]reflect.Value{
// Struct types (nil pointer trick for type registration)
"API": reflect.ValueOf((*API)(nil)),
"Context": reflect.ValueOf((*Context)(nil)),
"ToolDef": reflect.ValueOf((*ToolDef)(nil)),
"ToolContext": reflect.ValueOf((*ToolContext)(nil)),
"ShortcutDef": reflect.ValueOf((*ShortcutDef)(nil)),
"CommandDef": reflect.ValueOf((*CommandDef)(nil)),
"PrintBlockOpts": reflect.ValueOf((*PrintBlockOpts)(nil)),
// Session types
"SessionMessage": reflect.ValueOf((*SessionMessage)(nil)),
"ExtensionEntry": reflect.ValueOf((*ExtensionEntry)(nil)),
"SessionUsage": reflect.ValueOf((*SessionUsage)(nil)),
// Option types
"OptionDef": reflect.ValueOf((*OptionDef)(nil)),
// Model info types
"ModelInfoEntry": reflect.ValueOf((*ModelInfoEntry)(nil)),
// Tool info types
"ToolInfo": reflect.ValueOf((*ToolInfo)(nil)),
// LLM completion types
"CompleteRequest": reflect.ValueOf((*CompleteRequest)(nil)),
"CompleteResponse": reflect.ValueOf((*CompleteResponse)(nil)),
"CompactConfig": reflect.ValueOf((*CompactConfig)(nil)),
"FilePart": reflect.ValueOf((*FilePart)(nil)),
// Status bar types
"StatusBarEntry": reflect.ValueOf((*StatusBarEntry)(nil)),
// Widget types
"WidgetConfig": reflect.ValueOf((*WidgetConfig)(nil)),
"WidgetContent": reflect.ValueOf((*WidgetContent)(nil)),
"WidgetStyle": reflect.ValueOf((*WidgetStyle)(nil)),
"WidgetPlacement": reflect.ValueOf((*WidgetPlacement)(nil)),
"WidgetAbove": reflect.ValueOf(WidgetAbove),
"WidgetBelow": reflect.ValueOf(WidgetBelow),
// Header/Footer types
"HeaderFooterConfig": reflect.ValueOf((*HeaderFooterConfig)(nil)),
// UI visibility
"UIVisibility": reflect.ValueOf((*UIVisibility)(nil)),
// Context stats
"ContextStats": reflect.ValueOf((*ContextStats)(nil)),
// Overlay types
"OverlayAnchor": reflect.ValueOf((*OverlayAnchor)(nil)),
"OverlayCenter": reflect.ValueOf(OverlayCenter),
"OverlayTopCenter": reflect.ValueOf(OverlayTopCenter),
"OverlayBottomCenter": reflect.ValueOf(OverlayBottomCenter),
"OverlayStyle": reflect.ValueOf((*OverlayStyle)(nil)),
"OverlayConfig": reflect.ValueOf((*OverlayConfig)(nil)),
"OverlayResult": reflect.ValueOf((*OverlayResult)(nil)),
// Tool renderer types
"ToolRenderConfig": reflect.ValueOf((*ToolRenderConfig)(nil)),
// Message renderer types
"MessageRendererConfig": reflect.ValueOf((*MessageRendererConfig)(nil)),
// Editor interceptor types
"EditorKeyActionType": reflect.ValueOf((*EditorKeyActionType)(nil)),
"EditorKeyPassthrough": reflect.ValueOf(EditorKeyPassthrough),
"EditorKeyConsumed": reflect.ValueOf(EditorKeyConsumed),
"EditorKeyRemap": reflect.ValueOf(EditorKeyRemap),
"EditorKeySubmit": reflect.ValueOf(EditorKeySubmit),
"EditorKeyAction": reflect.ValueOf((*EditorKeyAction)(nil)),
"EditorConfig": reflect.ValueOf((*EditorConfig)(nil)),
// Prompt types
"PromptSelectConfig": reflect.ValueOf((*PromptSelectConfig)(nil)),
"PromptSelectResult": reflect.ValueOf((*PromptSelectResult)(nil)),
"PromptConfirmConfig": reflect.ValueOf((*PromptConfirmConfig)(nil)),
"PromptConfirmResult": reflect.ValueOf((*PromptConfirmResult)(nil)),
"PromptInputConfig": reflect.ValueOf((*PromptInputConfig)(nil)),
"PromptInputResult": reflect.ValueOf((*PromptInputResult)(nil)),
"PromptMultiSelectConfig": reflect.ValueOf((*PromptMultiSelectConfig)(nil)),
"PromptMultiSelectResult": reflect.ValueOf((*PromptMultiSelectResult)(nil)),
// Context filtering types
"ContextMessage": reflect.ValueOf((*ContextMessage)(nil)),
"ContextPrepareEvent": reflect.ValueOf((*ContextPrepareEvent)(nil)),
"ContextPrepareResult": reflect.ValueOf((*ContextPrepareResult)(nil)),
// Session lifecycle types
"BeforeForkEvent": reflect.ValueOf((*BeforeForkEvent)(nil)),
"BeforeForkResult": reflect.ValueOf((*BeforeForkResult)(nil)),
"BeforeSessionSwitchEvent": reflect.ValueOf((*BeforeSessionSwitchEvent)(nil)),
"BeforeSessionSwitchResult": reflect.ValueOf((*BeforeSessionSwitchResult)(nil)),
"BeforeCompactEvent": reflect.ValueOf((*BeforeCompactEvent)(nil)),
"BeforeCompactResult": reflect.ValueOf((*BeforeCompactResult)(nil)),
// Subagent types
"SubagentConfig": reflect.ValueOf((*SubagentConfig)(nil)),
"SubagentResult": reflect.ValueOf((*SubagentResult)(nil)),
"SubagentUsage": reflect.ValueOf((*SubagentUsage)(nil)),
"SubagentHandle": reflect.ValueOf((*SubagentHandle)(nil)),
"SubagentEvent": reflect.ValueOf((*SubagentEvent)(nil)),
// Subagent lifecycle events
"SubagentStartEvent": reflect.ValueOf((*SubagentStartEvent)(nil)),
"SubagentChunkEvent": reflect.ValueOf((*SubagentChunkEvent)(nil)),
"SubagentEndEvent": reflect.ValueOf((*SubagentEndEvent)(nil)),
// Theme types
"ThemeColor": reflect.ValueOf((*ThemeColor)(nil)),
"ThemeColorConfig": reflect.ValueOf((*ThemeColorConfig)(nil)),
// Tree navigation types
"TreeNode": reflect.ValueOf((*TreeNode)(nil)),
"TreeNavigationResult": reflect.ValueOf((*TreeNavigationResult)(nil)),
// Skill types
"Skill": reflect.ValueOf((*Skill)(nil)),
"SkillLoadResult": reflect.ValueOf((*SkillLoadResult)(nil)),
// Template parsing types
"PromptTemplate": reflect.ValueOf((*PromptTemplate)(nil)),
"ArgumentPattern": reflect.ValueOf((*ArgumentPattern)(nil)),
"ParseResult": reflect.ValueOf((*ParseResult)(nil)),
"ModelConditional": reflect.ValueOf((*ModelConditional)(nil)),
// Model resolution types
"ModelCapabilities": reflect.ValueOf((*ModelCapabilities)(nil)),
"ModelResolutionResult": reflect.ValueOf((*ModelResolutionResult)(nil)),
// Event structs
"ToolCallEvent": reflect.ValueOf((*ToolCallEvent)(nil)),
"ToolCallResult": reflect.ValueOf((*ToolCallResult)(nil)),
"ToolCallInputStartEvent": reflect.ValueOf((*ToolCallInputStartEvent)(nil)),
"ToolCallInputDeltaEvent": reflect.ValueOf((*ToolCallInputDeltaEvent)(nil)),
"ToolCallInputEndEvent": reflect.ValueOf((*ToolCallInputEndEvent)(nil)),
"ToolExecutionStartEvent": reflect.ValueOf((*ToolExecutionStartEvent)(nil)),
"ToolExecutionEndEvent": reflect.ValueOf((*ToolExecutionEndEvent)(nil)),
"ToolOutputEvent": reflect.ValueOf((*ToolOutputEvent)(nil)),
"ToolResultEvent": reflect.ValueOf((*ToolResultEvent)(nil)),
"ToolResultResult": reflect.ValueOf((*ToolResultResult)(nil)),
"InputEvent": reflect.ValueOf((*InputEvent)(nil)),
"InputResult": reflect.ValueOf((*InputResult)(nil)),
"BeforeAgentStartEvent": reflect.ValueOf((*BeforeAgentStartEvent)(nil)),
"BeforeAgentStartResult": reflect.ValueOf((*BeforeAgentStartResult)(nil)),
"AgentStartEvent": reflect.ValueOf((*AgentStartEvent)(nil)),
"AgentEndEvent": reflect.ValueOf((*AgentEndEvent)(nil)),
"MessageStartEvent": reflect.ValueOf((*MessageStartEvent)(nil)),
"MessageUpdateEvent": reflect.ValueOf((*MessageUpdateEvent)(nil)),
"MessageEndEvent": reflect.ValueOf((*MessageEndEvent)(nil)),
"SessionStartEvent": reflect.ValueOf((*SessionStartEvent)(nil)),
"SessionShutdownEvent": reflect.ValueOf((*SessionShutdownEvent)(nil)),
"ModelChangeEvent": reflect.ValueOf((*ModelChangeEvent)(nil)),
// Step lifecycle events
"StepStartEvent": reflect.ValueOf((*StepStartEvent)(nil)),
"StepFinishEvent": reflect.ValueOf((*StepFinishEvent)(nil)),
"ReasoningStartEvent": reflect.ValueOf((*ReasoningStartEvent)(nil)),
"WarningsEvent": reflect.ValueOf((*WarningsEvent)(nil)),
"SourceEvent": reflect.ValueOf((*SourceEvent)(nil)),
"ErrorEvent": reflect.ValueOf((*ErrorEvent)(nil)),
"RetryEvent": reflect.ValueOf((*RetryEvent)(nil)),
"PrepareStepEvent": reflect.ValueOf((*PrepareStepEvent)(nil)),
"PrepareStepResult": reflect.ValueOf((*PrepareStepResult)(nil)),
"LLMUsageEvent": reflect.ValueOf((*LLMUsageEvent)(nil)),
},
}
}