From e5a13e2e12d03ace709e220b82c6aa9c366906be Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 22 Apr 2026 21:05:04 +0300 Subject: [PATCH] feat(sdk): add missing LLM type aliases and remove fantasy dependency leakage - Add LLMToolResultOutputContentMedia alias (closes gap in tool result types) - Add LLMToolResultContentType enum and constants (Text, Error, Media) - Add LLMToolInfo, LLMProviderOptions, LLMProviderMetadata, LLMPrompt aliases - Replace all fantasy.* references in hooks.go and hooks_test.go with SDK-owned aliases, removing the charm.land/fantasy import from both - Fix gofmt alignment in internal/extensions/symbols.go - Update SDK skill doc with complete LLM type reference --- internal/extensions/symbols.go | 16 ++++----- pkg/kit/hooks.go | 20 +++++------ pkg/kit/hooks_test.go | 62 ++++++++++++++++------------------ pkg/kit/kit.go | 2 +- pkg/kit/types.go | 40 ++++++++++++++++++++++ skills/kit-sdk/SKILL.md | 17 +++++++++- 6 files changed, 104 insertions(+), 53 deletions(-) diff --git a/internal/extensions/symbols.go b/internal/extensions/symbols.go index 0092f61e..84f5a866 100644 --- a/internal/extensions/symbols.go +++ b/internal/extensions/symbols.go @@ -174,15 +174,15 @@ func Symbols() interp.Exports { "ModelChangeEvent": reflect.ValueOf((*ModelChangeEvent)(nil)), // Step lifecycle events - "StepStartEvent": reflect.ValueOf((*StepStartEvent)(nil)), - "StepFinishEvent": reflect.ValueOf((*StepFinishEvent)(nil)), + "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)), + "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)), }, } } diff --git a/pkg/kit/hooks.go b/pkg/kit/hooks.go index a22af13b..36df2422 100644 --- a/pkg/kit/hooks.go +++ b/pkg/kit/hooks.go @@ -5,8 +5,6 @@ import ( "fmt" "sort" "sync" - - "charm.land/fantasy" ) // --------------------------------------------------------------------------- @@ -295,16 +293,16 @@ func (m *Kit) OnPrepareStep(p HookPriority, h func(PrepareStepHook) *PrepareStep // AfterToolResult hooks around each execution. The registries are referenced // by pointer so hooks added after agent creation are still invoked. type hookedTool struct { - inner fantasy.AgentTool + inner Tool beforeToolCall *hookRegistry[BeforeToolCallHook, BeforeToolCallResult] afterToolResult *hookRegistry[AfterToolResultHook, AfterToolResultResult] } -func (h *hookedTool) Info() fantasy.ToolInfo { return h.inner.Info() } -func (h *hookedTool) ProviderOptions() fantasy.ProviderOptions { return h.inner.ProviderOptions() } -func (h *hookedTool) SetProviderOptions(o fantasy.ProviderOptions) { h.inner.SetProviderOptions(o) } +func (h *hookedTool) Info() LLMToolInfo { return h.inner.Info() } +func (h *hookedTool) ProviderOptions() LLMProviderOptions { return h.inner.ProviderOptions() } +func (h *hookedTool) SetProviderOptions(o LLMProviderOptions) { h.inner.SetProviderOptions(o) } -func (h *hookedTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) { +func (h *hookedTool) Run(ctx context.Context, call LLMToolCall) (LLMToolResponse, error) { toolName := h.inner.Info().Name // 1. BeforeToolCall — can block execution. @@ -318,7 +316,7 @@ func (h *hookedTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.To if reason == "" { reason = "blocked by hook" } - return fantasy.NewTextErrorResponse(fmt.Sprintf("Error: %s", reason)), + return newLLMTextErrorResponse(fmt.Sprintf("Error: %s", reason)), fmt.Errorf("tool blocked by hook: %s", reason) } } @@ -353,9 +351,9 @@ func (h *hookedTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.To func hookToolWrapper( beforeToolCall *hookRegistry[BeforeToolCallHook, BeforeToolCallResult], afterToolResult *hookRegistry[AfterToolResultHook, AfterToolResultResult], -) func([]fantasy.AgentTool) []fantasy.AgentTool { - return func(tools []fantasy.AgentTool) []fantasy.AgentTool { - wrapped := make([]fantasy.AgentTool, len(tools)) +) func([]Tool) []Tool { + return func(tools []Tool) []Tool { + wrapped := make([]Tool, len(tools)) for i, tool := range tools { wrapped[i] = &hookedTool{ inner: tool, diff --git a/pkg/kit/hooks_test.go b/pkg/kit/hooks_test.go index 26250e63..c7007703 100644 --- a/pkg/kit/hooks_test.go +++ b/pkg/kit/hooks_test.go @@ -5,8 +5,6 @@ import ( "fmt" "sync" "testing" - - "charm.land/fantasy" ) // --------------------------------------------------------------------------- @@ -177,20 +175,20 @@ func TestHookRegistry_ConcurrentAccess(t *testing.T) { // mockAgentTool implements the AgentTool interface for testing. type mockAgentTool struct { name string - runFn func(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) - popts fantasy.ProviderOptions + runFn func(ctx context.Context, call LLMToolCall) (LLMToolResponse, error) + popts LLMProviderOptions } -func (m *mockAgentTool) Info() fantasy.ToolInfo { - return fantasy.ToolInfo{Name: m.name, Description: "mock tool"} +func (m *mockAgentTool) Info() LLMToolInfo { + return LLMToolInfo{Name: m.name, Description: "mock tool"} } -func (m *mockAgentTool) ProviderOptions() fantasy.ProviderOptions { return m.popts } -func (m *mockAgentTool) SetProviderOptions(o fantasy.ProviderOptions) { m.popts = o } -func (m *mockAgentTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) { +func (m *mockAgentTool) ProviderOptions() LLMProviderOptions { return m.popts } +func (m *mockAgentTool) SetProviderOptions(o LLMProviderOptions) { m.popts = o } +func (m *mockAgentTool) Run(ctx context.Context, call LLMToolCall) (LLMToolResponse, error) { if m.runFn != nil { return m.runFn(ctx, call) } - return fantasy.NewTextResponse("default output"), nil + return newLLMTextResponse("default output"), nil } // newEmptyHookedTool creates a hookedTool with empty hook registries and the given mock tool. @@ -203,14 +201,14 @@ func newEmptyHookedTool(mock *mockAgentTool) *hookedTool { func TestHookedTool_Passthrough(t *testing.T) { mock := &mockAgentTool{ name: "test_tool", - runFn: func(_ context.Context, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { - return fantasy.NewTextResponse("hello world"), nil + runFn: func(_ context.Context, _ LLMToolCall) (LLMToolResponse, error) { + return newLLMTextResponse("hello world"), nil }, } ht := newEmptyHookedTool(mock) - resp, err := ht.Run(context.Background(), fantasy.ToolCall{Input: "{}"}) + resp, err := ht.Run(context.Background(), LLMToolCall{Input: "{}"}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -226,9 +224,9 @@ func TestHookedTool_BeforeToolCallBlock(t *testing.T) { toolRan := false mock := &mockAgentTool{ name: "dangerous_tool", - runFn: func(_ context.Context, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { + runFn: func(_ context.Context, _ LLMToolCall) (LLMToolResponse, error) { toolRan = true - return fantasy.NewTextResponse("should not run"), nil + return newLLMTextResponse("should not run"), nil }, } @@ -241,7 +239,7 @@ func TestHookedTool_BeforeToolCallBlock(t *testing.T) { ht := &hookedTool{inner: mock, beforeToolCall: before, afterToolResult: after} - resp, err := ht.Run(context.Background(), fantasy.ToolCall{Input: "{}"}) + resp, err := ht.Run(context.Background(), LLMToolCall{Input: "{}"}) if err == nil { t.Fatal("expected error from blocked tool") } @@ -263,7 +261,7 @@ func TestHookedTool_BeforeToolCallBlockDefaultReason(t *testing.T) { }) ht := &hookedTool{inner: mock, beforeToolCall: before, afterToolResult: after} - resp, _ := ht.Run(context.Background(), fantasy.ToolCall{}) + resp, _ := ht.Run(context.Background(), LLMToolCall{}) if resp.Content != "Error: blocked by hook" { t.Errorf("expected default block reason, got %q", resp.Content) } @@ -275,8 +273,8 @@ func TestHookedTool_AfterToolResultModify(t *testing.T) { mock := &mockAgentTool{ name: "tool", - runFn: func(_ context.Context, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { - return fantasy.NewTextResponse("secret data"), nil + runFn: func(_ context.Context, _ LLMToolCall) (LLMToolResponse, error) { + return newLLMTextResponse("secret data"), nil }, } @@ -286,7 +284,7 @@ func TestHookedTool_AfterToolResultModify(t *testing.T) { }) ht := &hookedTool{inner: mock, beforeToolCall: before, afterToolResult: after} - resp, err := ht.Run(context.Background(), fantasy.ToolCall{Input: "{}"}) + resp, err := ht.Run(context.Background(), LLMToolCall{Input: "{}"}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -301,8 +299,8 @@ func TestHookedTool_AfterToolResultModifyIsError(t *testing.T) { mock := &mockAgentTool{ name: "tool", - runFn: func(_ context.Context, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { - return fantasy.NewTextResponse("ok"), nil + runFn: func(_ context.Context, _ LLMToolCall) (LLMToolResponse, error) { + return newLLMTextResponse("ok"), nil }, } @@ -312,7 +310,7 @@ func TestHookedTool_AfterToolResultModifyIsError(t *testing.T) { }) ht := &hookedTool{inner: mock, beforeToolCall: before, afterToolResult: after} - resp, err := ht.Run(context.Background(), fantasy.ToolCall{}) + resp, err := ht.Run(context.Background(), LLMToolCall{}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -327,8 +325,8 @@ func TestHookedTool_HookReceivesToolInfo(t *testing.T) { mock := &mockAgentTool{ name: "my_tool", - runFn: func(_ context.Context, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { - return fantasy.NewTextResponse("result"), nil + runFn: func(_ context.Context, _ LLMToolCall) (LLMToolResponse, error) { + return newLLMTextResponse("result"), nil }, } @@ -345,7 +343,7 @@ func TestHookedTool_HookReceivesToolInfo(t *testing.T) { }) ht := &hookedTool{inner: mock, beforeToolCall: before, afterToolResult: after} - _, _ = ht.Run(context.Background(), fantasy.ToolCall{Input: `{"key":"value"}`}) + _, _ = ht.Run(context.Background(), LLMToolCall{Input: `{"key":"value"}`}) if capturedBefore.ToolName != "my_tool" { t.Errorf("BeforeToolCall: expected tool name 'my_tool', got %q", capturedBefore.ToolName) @@ -380,7 +378,7 @@ func TestHookToolWrapper(t *testing.T) { wrapper := hookToolWrapper(before, after) - tools := []fantasy.AgentTool{ + tools := []Tool{ &mockAgentTool{name: "tool_a"}, &mockAgentTool{name: "tool_b"}, } @@ -407,7 +405,7 @@ func TestHookToolWrapper(t *testing.T) { return &BeforeToolCallResult{Block: true, Reason: "late hook"} }) - _, err := wrapped[0].Run(context.Background(), fantasy.ToolCall{}) + _, err := wrapped[0].Run(context.Background(), LLMToolCall{}) if err == nil { t.Error("expected error from late-registered blocking hook") } @@ -548,7 +546,7 @@ func TestPrepareStepHookRegistry(t *testing.T) { if h.StepNumber == 0 { // On step 0, prepend a system message. newMsgs := make([]LLMMessage, 0, len(h.Messages)+1) - newMsgs = append(newMsgs, fantasy.NewSystemMessage("injected")) + newMsgs = append(newMsgs, NewLLMSystemMessage("injected")) newMsgs = append(newMsgs, h.Messages...) return &PrepareStepResult{Messages: newMsgs} } @@ -558,7 +556,7 @@ func TestPrepareStepHookRegistry(t *testing.T) { // Test step 0 — should modify messages. input := PrepareStepHook{ StepNumber: 0, - Messages: []LLMMessage{fantasy.NewUserMessage("hello")}, + Messages: []LLMMessage{NewLLMUserMessage("hello")}, } result := hr.run(input) if result == nil { @@ -567,7 +565,7 @@ func TestPrepareStepHookRegistry(t *testing.T) { if len(result.Messages) != 2 { t.Fatalf("expected 2 messages, got %d", len(result.Messages)) } - if result.Messages[0].Role != fantasy.MessageRoleSystem { + if result.Messages[0].Role != LLMRoleSystem { t.Errorf("expected system message first, got role %q", result.Messages[0].Role) } @@ -599,7 +597,7 @@ func TestPrepareStepHookPriority(t *testing.T) { input := PrepareStepHook{ StepNumber: 0, - Messages: []LLMMessage{fantasy.NewUserMessage("test")}, + Messages: []LLMMessage{NewLLMUserMessage("test")}, } result := hr.run(input) diff --git a/pkg/kit/kit.go b/pkg/kit/kit.go index 2ced985d..b60186f5 100644 --- a/pkg/kit/kit.go +++ b/pkg/kit/kit.go @@ -732,7 +732,7 @@ func (m *Kit) ExecuteCompletion(ctx context.Context, req extensions.CompleteRequ llmModel fantasy.LanguageModel closer func() usedModel string - providerOps fantasy.ProviderOptions + providerOps LLMProviderOptions ) if req.Model == "" { diff --git a/pkg/kit/types.go b/pkg/kit/types.go index d450c646..952038fe 100644 --- a/pkg/kit/types.go +++ b/pkg/kit/types.go @@ -184,6 +184,40 @@ type LLMToolResultOutputContentText = fantasy.ToolResultOutputContentText // LLMToolResultOutputContentError is an error-valued tool result output. type LLMToolResultOutputContentError = fantasy.ToolResultOutputContentError +// LLMToolResultOutputContentMedia is a media-valued tool result output +// (images, audio, etc.) carrying base64-encoded data and a MIME type. +type LLMToolResultOutputContentMedia = fantasy.ToolResultOutputContentMedia + +// LLMToolResultContentType classifies the kind of a tool result output +// ("text", "error", or "media"). +type LLMToolResultContentType = fantasy.ToolResultContentType + +// Tool result content type constants. +const ( + // LLMToolResultContentTypeText represents text output. + LLMToolResultContentTypeText = fantasy.ToolResultContentTypeText + // LLMToolResultContentTypeError represents error text output. + LLMToolResultContentTypeError = fantasy.ToolResultContentTypeError + // LLMToolResultContentTypeMedia represents media (binary) output. + LLMToolResultContentTypeMedia = fantasy.ToolResultContentTypeMedia +) + +// LLMToolInfo describes a tool's name, description, and JSON-Schema parameters. +type LLMToolInfo = fantasy.ToolInfo + +// LLMProviderOptions carries provider-specific key/value option maps, keyed +// by provider name (e.g. "anthropic"). Use this when configuring or +// inspecting provider-specific tool behaviour. +type LLMProviderOptions = fantasy.ProviderOptions + +// LLMProviderMetadata carries provider-specific metadata returned alongside +// LLM responses, keyed by provider name. +type LLMProviderMetadata = fantasy.ProviderMetadata + +// LLMPrompt is an ordered sequence of [LLMMessage] values forming a complete +// prompt for the LLM. +type LLMPrompt = fantasy.Prompt + // LLMMessageRole identifies the participant role in an LLM conversation. type LLMMessageRole = fantasy.MessageRole @@ -210,6 +244,12 @@ var NewLLMUserMessage = fantasy.NewUserMessage // prompt strings. var NewLLMSystemMessage = fantasy.NewSystemMessage +// newLLMTextErrorResponse creates a tool-error response (internal helper). +var newLLMTextErrorResponse = fantasy.NewTextErrorResponse + +// newLLMTextResponse creates a plain-text tool response (internal helper). +var newLLMTextResponse = fantasy.NewTextResponse + // ==== Compaction Types (internal/compaction/) ==== // CompactionResult contains statistics from a compaction operation. diff --git a/skills/kit-sdk/SKILL.md b/skills/kit-sdk/SKILL.md index a136b3d6..a46f1baf 100644 --- a/skills/kit-sdk/SKILL.md +++ b/skills/kit-sdk/SKILL.md @@ -1170,15 +1170,30 @@ kit.Config, kit.MCPServerConfig // Provider types kit.ProviderConfig, kit.ProviderResult, kit.ModelInfo, kit.ModelCost, kit.ModelLimit -// LLM types — concrete Kit-owned structs (no external library dependency) +// LLM types — clean aliases (no external library dependency in consumer code) kit.LLMMessage // {Role LLMMessageRole, Content string} +kit.LLMMessagePart // interface for message content parts kit.LLMMessageRole // "user" | "assistant" | "system" | "tool" kit.LLMUsage // {InputTokens, OutputTokens, TotalTokens, ReasoningTokens, // CacheCreationTokens, CacheReadTokens} kit.LLMResponse // {Content, FinishReason, Usage} kit.LLMFilePart // {Filename, Data []byte, MediaType} +kit.LLMTextPart // plain-text content part +kit.LLMReasoningPart // reasoning/chain-of-thought content part kit.LLMToolCall // {ID, Name, Input string} — execution-layer tool call (for Tool.Run) kit.LLMToolResponse // {Type, Content, Data, MediaType, IsError, ...} — raw tool response +kit.LLMToolCallPart // LLM-initiated tool invocation within a message +kit.LLMToolResultPart // tool result within a message +kit.LLMToolResultOutputContent // interface for tool result output +kit.LLMToolResultOutputContentText // text tool result +kit.LLMToolResultOutputContentError // error tool result +kit.LLMToolResultOutputContentMedia // media tool result {Data, MediaType, Text} +kit.LLMToolResultContentType // "text" | "error" | "media" +kit.LLMToolInfo // {Name, Description, Parameters, Required, Parallel} +kit.LLMProviderOptions // provider-specific option maps (keyed by provider name) +kit.LLMProviderMetadata // provider-specific response metadata +kit.LLMPrompt // []LLMMessage — ordered prompt sequence +kit.LLMFinishReason // "stop" | "length" | "tool-calls" | ... // Compaction types kit.CompactionResult, kit.CompactionOptions