mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
49f8b485be
* 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.
190 lines
8.7 KiB
Go
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)),
|
|
},
|
|
}
|
|
}
|