From 63a2d50a1a991a6e8b9f61ed0bdb980b2fe97de9 Mon Sep 17 00:00:00 2001 From: Kevin Castro Date: Mon, 6 Apr 2026 21:28:28 -0500 Subject: [PATCH] Enforce deps type to be encodable as JSON --- README.md | 10 +- agent.go | 6 +- agent_internal_test.go | 6 +- agent_options.go | 10 +- agent_options_test.go | 22 ++--- agent_runtime_test.go | 40 ++++---- agent_test.go | 14 +-- aliases.go | 4 + aliases_test.go | 6 +- e2e/e2e_test.go | 172 +++++++++++++++++------------------ end_strategy_test.go | 18 ++-- examples/basic/main.go | 2 +- examples/structured/main.go | 2 +- examples/tools/main.go | 2 +- history_test.go | 4 +- internal/core/context.go | 5 +- internal/core/deps.go | 14 +++ internal/core/deps_test.go | 24 +++++ output.go | 2 +- output_mode_test.go | 10 +- output_test.go | 8 +- run_loop_internal_test.go | 12 +-- stream_internal_test.go | 52 +++++------ stream_test.go | 8 +- system_prompt.go | 2 +- system_prompt_test.go | 46 +++++----- thinking_test.go | 6 +- tool/builder_test.go | 6 +- tool/deps.go | 4 + tool_prepare_test.go | 20 ++-- tool_retry_test.go | 16 ++-- typed_agent.go | 8 +- typed_agent_internal_test.go | 26 +++--- typed_agent_test.go | 16 ++-- usage_limits_test.go | 34 +++---- validator.go | 2 +- validator_internal_test.go | 4 +- validator_test.go | 34 +++---- 38 files changed, 362 insertions(+), 315 deletions(-) create mode 100644 internal/core/deps.go create mode 100644 internal/core/deps_test.go diff --git a/README.md b/README.md index 5ecf839..235c8f6 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ import ( func main() { model, _ := openai.New("gpt-4o") - agent := agentic.NewAgent[any]("You are a helpful assistant.", model) + agent := agentic.NewAgent[agentic.NoDeps]("You are a helpful assistant.", model) result, err := agent.Run(context.Background(), "Hello!", nil) if err != nil { @@ -88,7 +88,7 @@ type Summary struct { Points []string `json:"points" validate:"required,min=1"` } -agent := agentic.NewTypedAgent[any, Summary]( +agent := agentic.NewTypedAgent[agentic.NoDeps, Summary]( "Summarize the input as structured data.", model, "Return a structured summary.", @@ -158,12 +158,12 @@ for event := range stream.Events { Manage context window with built-in processors: ```go -agent := agentic.NewAgent[any]("You are helpful.", model, - agentic.WithHistoryProcessor[any](agentic.TruncateHistory(20)), +agent := agentic.NewAgent[agentic.NoDeps]("You are helpful.", model, + agentic.WithHistoryProcessor[agentic.NoDeps](agentic.TruncateHistory(20)), ) // Or chain multiple processors -agentic.WithHistoryProcessor[any](agentic.ChainProcessors( +agentic.WithHistoryProcessor[agentic.NoDeps](agentic.ChainProcessors( agentic.SlidingWindowHistory(4000, tokenCounter), agentic.TruncateHistory(50), )) diff --git a/agent.go b/agent.go index f91822a..20d6df9 100644 --- a/agent.go +++ b/agent.go @@ -6,8 +6,8 @@ import ( ) // Agent orchestrates LLM interactions with tools and structured output. -// DepsT is the type of user-provided dependencies accessible in tool handlers. -// Use any as DepsT if you don't need dependencies. +// DepsT is the type of user-provided dependencies accessible in tool handlers +// and dynamic prompts. Use [NoDeps] when you do not inject dependencies. type Agent[DepsT any] struct { model Model systemPrompt string @@ -23,7 +23,7 @@ type Agent[DepsT any] struct { // // Example: // -// agent := agentic.NewAgent[any]("You are a helpful assistant", model) +// agent := agentic.NewAgent[NoDeps]("You are a helpful assistant", model) func NewAgent[DepsT any](systemPrompt string, model Model, opts ...AgentOption[DepsT]) *Agent[DepsT] { config := defaultAgentConfig[DepsT]() for _, opt := range opts { diff --git a/agent_internal_test.go b/agent_internal_test.go index e74bf55..7d61f8a 100644 --- a/agent_internal_test.go +++ b/agent_internal_test.go @@ -27,7 +27,7 @@ func TestNewAgentDynamicRegistersHandoffs(t *testing.T) { func TestAgentRunAdditionalErrorPaths(t *testing.T) { t.Run("no choices in response", func(t *testing.T) { - agent := NewAgent[any]("system", &testutil.StubModel{ + agent := NewAgent[NoDeps]("system", &testutil.StubModel{ NameValue: "empty-model", Response: &ChatResponse{}, }) @@ -39,7 +39,7 @@ func TestAgentRunAdditionalErrorPaths(t *testing.T) { }) t.Run("tool call without registered tools", func(t *testing.T) { - agent := NewAgent[any]("system", &testutil.StubModel{ + agent := NewAgent[NoDeps]("system", &testutil.StubModel{ NameValue: "tool-model", Response: &ChatResponse{ Choices: []Choice{{ @@ -60,7 +60,7 @@ func TestAgentRunAdditionalErrorPaths(t *testing.T) { }) t.Run("model error is wrapped", func(t *testing.T) { - agent := NewAgent[any]("system", &testutil.StubModel{ + agent := NewAgent[NoDeps]("system", &testutil.StubModel{ NameValue: "error-model", Err: errors.New("boom"), }) diff --git a/agent_options.go b/agent_options.go index 94fc241..4f26a23 100644 --- a/agent_options.go +++ b/agent_options.go @@ -109,7 +109,7 @@ func WithOutputValidator[DepsT any](v OutputValidator[DepsT]) AgentOption[DepsT] // // Example: // -// agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { +// agentic.WithOutputValidatorFunc[NoDeps](func(ctx agentic.RunContext[NoDeps], output string) error { // if !strings.Contains(output, "answer") { // return agentic.NewValidationError("Response must contain the word 'answer'") // } @@ -135,8 +135,8 @@ func WithMaxValidationRetries[DepsT any](n int) AgentOption[DepsT] { // // Example: // -// agent := agentic.NewAgent[any]("prompt", model, -// agentic.WithThinking[any](agentic.ThinkingConfig{ +// agent := agentic.NewAgent[NoDeps]("prompt", model, +// agentic.WithThinking[NoDeps](agentic.ThinkingConfig{ // Enabled: true, // BudgetTokens: 10000, // }), @@ -153,8 +153,8 @@ func WithThinking[DepsT any](config ThinkingConfig) AgentOption[DepsT] { // // Example: // -// agent := agentic.NewAgent[any]("prompt", model, -// agentic.WithUsageLimits[any](agentic.UsageLimits{ +// agent := agentic.NewAgent[NoDeps]("prompt", model, +// agentic.WithUsageLimits[NoDeps](agentic.UsageLimits{ // MaxRequests: intPtr(10), // MaxTotalTokens: intPtr(50000), // }), diff --git a/agent_options_test.go b/agent_options_test.go index 83191d4..61478bf 100644 --- a/agent_options_test.go +++ b/agent_options_test.go @@ -6,32 +6,32 @@ import ( ) func TestWithTemperature(t *testing.T) { - cfg := defaultAgentConfig[any]() - WithTemperature[any](0.7)(&cfg) + cfg := defaultAgentConfig[NoDeps]() + WithTemperature[NoDeps](0.7)(&cfg) if cfg.temperature == nil || *cfg.temperature != 0.7 { t.Errorf("expected temperature 0.7, got %v", cfg.temperature) } } func TestWithMaxTokens(t *testing.T) { - cfg := defaultAgentConfig[any]() - WithMaxTokens[any](500)(&cfg) + cfg := defaultAgentConfig[NoDeps]() + WithMaxTokens[NoDeps](500)(&cfg) if cfg.maxTokens == nil || *cfg.maxTokens != 500 { t.Errorf("expected max tokens 500, got %v", cfg.maxTokens) } } func TestWithTopP(t *testing.T) { - cfg := defaultAgentConfig[any]() - WithTopP[any](0.9)(&cfg) + cfg := defaultAgentConfig[NoDeps]() + WithTopP[NoDeps](0.9)(&cfg) if cfg.topP == nil || *cfg.topP != 0.9 { t.Errorf("expected topP 0.9, got %v", cfg.topP) } } func TestWithToolChoice(t *testing.T) { - cfg := defaultAgentConfig[any]() - WithToolChoice[any](ToolChoiceRequired)(&cfg) + cfg := defaultAgentConfig[NoDeps]() + WithToolChoice[NoDeps](ToolChoiceRequired)(&cfg) if cfg.toolChoice == nil || *cfg.toolChoice != ToolChoiceRequired { t.Errorf("expected tool choice required, got %v", cfg.toolChoice) } @@ -74,7 +74,7 @@ func TestWithRunMaxIterations(t *testing.T) { } func TestDefaultAgentConfig(t *testing.T) { - cfg := defaultAgentConfig[any]() + cfg := defaultAgentConfig[NoDeps]() if cfg.maxIterations != 10 { t.Errorf("expected default maxIterations 10, got %d", cfg.maxIterations) } @@ -88,8 +88,8 @@ func TestHistoryProcessorOptions(t *testing.T) { return append(messages, NewTextMessage(RoleAssistant, "processed")), nil }) - cfg := defaultAgentConfig[any]() - WithHistoryProcessor[any](proc)(&cfg) + cfg := defaultAgentConfig[NoDeps]() + WithHistoryProcessor[NoDeps](proc)(&cfg) if cfg.historyProcessor == nil { t.Fatal("expected agent history processor to be set") } diff --git a/agent_runtime_test.go b/agent_runtime_test.go index e388a4f..fb18783 100644 --- a/agent_runtime_test.go +++ b/agent_runtime_test.go @@ -13,7 +13,7 @@ import ( func TestAgentSetRegistry(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) reg := agentic.NewRegistry() agent.SetRegistry(reg) @@ -30,8 +30,8 @@ func TestAgentSetRegistry(t *testing.T) { func TestAgentDynamicPromptError(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgentDynamic[any]( - func(ctx agentic.RunContext[any]) (string, error) { + agent := agentic.NewAgentDynamic[agentic.NoDeps]( + func(ctx agentic.RunContext[agentic.NoDeps]) (string, error) { return "", fmt.Errorf("prompt generation failed") }, model, @@ -56,7 +56,7 @@ func TestAgentNoToolsButToolCallRequested(t *testing.T) { }, ) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) _, err := agent.Run(context.Background(), "go", nil) if err == nil { @@ -66,7 +66,7 @@ func TestAgentNoToolsButToolCallRequested(t *testing.T) { func TestAgentWithRunOptions(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) result, err := agent.Run(context.Background(), "hello", nil, agentic.WithRunTemperature(0.5), @@ -84,7 +84,7 @@ func TestAgentWithRunOptions(t *testing.T) { func TestAgentWithMessagesContainingSystemPrompt(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) // If messages already contain a system prompt, don't add another result, err := agent.Run(context.Background(), "hello", nil, @@ -115,7 +115,7 @@ func TestAgentWithMessagesContainingSystemPrompt(t *testing.T) { func TestAgentModelRequestError(t *testing.T) { model := &testutil.StubModel{NameValue: "error-model", Err: fmt.Errorf("api error")} - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) _, err := agent.Run(context.Background(), "hello", nil) if err == nil { @@ -131,7 +131,7 @@ func TestAgentNoChoicesInResponse(t *testing.T) { NameValue: "empty-model", Response: &agentic.ChatResponse{Choices: nil}, } - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) _, err := agent.Run(context.Background(), "hello", nil) if err == nil { @@ -144,11 +144,11 @@ func TestAgentNoChoicesInResponse(t *testing.T) { func TestAgentWithTemperatureAndTopP(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("test", model, - agentic.WithTemperature[any](0.8), - agentic.WithMaxTokens[any](500), - agentic.WithTopP[any](0.9), - agentic.WithToolChoice[any](agentic.ToolChoiceAuto), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithTemperature[agentic.NoDeps](0.8), + agentic.WithMaxTokens[agentic.NoDeps](500), + agentic.WithTopP[agentic.NoDeps](0.9), + agentic.WithToolChoice[agentic.NoDeps](agentic.ToolChoiceAuto), ) result, err := agent.Run(context.Background(), "hello", nil) @@ -172,7 +172,7 @@ func TestAgentAddToolPanicsOnDuplicate(t *testing.T) { type Out struct{} tool, handler := agentic.MustToolPlain("dup", "duplicate", func(in In) (Out, error) { return Out{}, nil }) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) agent.AddTool(tool, handler) agent.AddTool(tool, handler) // should panic } @@ -181,8 +181,8 @@ func TestFirstNonNil(t *testing.T) { // Tested indirectly through agent options, but let's verify behavior // by using run options that override agent options model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("test", model, - agentic.WithTemperature[any](0.3), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithTemperature[agentic.NoDeps](0.3), ) // Run with temperature override @@ -208,13 +208,13 @@ func TestFirstNonNil(t *testing.T) { func TestAgentNewAgentDynamicWithOptions(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgentDynamic[any]( - func(ctx agentic.RunContext[any]) (string, error) { + agent := agentic.NewAgentDynamic[agentic.NoDeps]( + func(ctx agentic.RunContext[agentic.NoDeps]) (string, error) { return "dynamic prompt", nil }, model, - agentic.WithMaxIterations[any](5), - agentic.WithRetries[any](agentic.RetryConfig{MaxRetries: 3}), + agentic.WithMaxIterations[agentic.NoDeps](5), + agentic.WithRetries[agentic.NoDeps](agentic.RetryConfig{MaxRetries: 3}), ) result, err := agent.Run(context.Background(), "hello", nil) diff --git a/agent_test.go b/agent_test.go index 83b9ba5..5b2fa72 100644 --- a/agent_test.go +++ b/agent_test.go @@ -10,7 +10,7 @@ import ( func TestAgentSimpleRun(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "Hello!"}) - agent := agentic.NewAgent[any]("You are helpful", model) + agent := agentic.NewAgent[agentic.NoDeps]("You are helpful", model) result, err := agent.Run(context.Background(), "Hi", nil) if err != nil { @@ -49,7 +49,7 @@ func TestAgentWithTools(t *testing.T) { test.ModelResponse{Text: "The weather in Tokyo is 72F"}, ) - agent := agentic.NewAgent[any]("You are a weather assistant", model). + agent := agentic.NewAgent[agentic.NoDeps]("You are a weather assistant", model). AddTool(tool, handler) result, err := agent.Run(context.Background(), "What's the weather in Tokyo?", nil) @@ -127,8 +127,8 @@ func TestAgentMaxIterations(t *testing.T) { return NoopOutput{OK: true}, nil }) - agent := agentic.NewAgent[any]("test", model, - agentic.WithMaxIterations[any](3), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithMaxIterations[agentic.NoDeps](3), ).AddTool(tool, handler) result, err := agent.Run(context.Background(), "loop forever", nil) @@ -230,8 +230,8 @@ func TestAgentModelRetry(t *testing.T) { test.ModelResponse{Text: "Found the answer!"}, ) - agent := agentic.NewAgent[any]("search assistant", model, - agentic.WithRetries[any](agentic.RetryConfig{MaxRetries: 2}), + agent := agentic.NewAgent[agentic.NoDeps]("search assistant", model, + agentic.WithRetries[agentic.NoDeps](agentic.RetryConfig{MaxRetries: 2}), ).AddTool(tool, handler) result, err := agent.Run(context.Background(), "Find something", nil) @@ -263,7 +263,7 @@ func TestAgentUsageAccumulation(t *testing.T) { return NoopOutput{}, nil }) - agent := agentic.NewAgent[any]("test", model).AddTool(tool, handler) + agent := agentic.NewAgent[agentic.NoDeps]("test", model).AddTool(tool, handler) result, err := agent.Run(context.Background(), "go", nil) if err != nil { diff --git a/aliases.go b/aliases.go index b8af482..aa8b7a1 100644 --- a/aliases.go +++ b/aliases.go @@ -33,6 +33,10 @@ type ToolResult = core.ToolResult type Model = core.Model type StreamModel = core.StreamModel type RunContext[DepsT any] = core.RunContext[DepsT] + +// NoDeps is the empty dependency type for agents and tools without dependency injection. +// It implements json.Marshaler (always JSON null). See package core. +type NoDeps = core.NoDeps type ToolHandler = core.ToolHandler type ToolExecutionResult = core.ToolExecutionResult type ToolConfig = core.ToolConfig diff --git a/aliases_test.go b/aliases_test.go index b273464..115fa0e 100644 --- a/aliases_test.go +++ b/aliases_test.go @@ -45,7 +45,7 @@ func TestAddToolset(t *testing.T) { }, } - agent := NewAgent[any]("test", model).AddToolset(ts) + agent := NewAgent[NoDeps]("test", model).AddToolset(ts) if agent.registry == nil { t.Fatal("expected registry to be created") @@ -150,7 +150,7 @@ func TestAliasAgentAndDeferredWrappers(t *testing.T) { }, testprovider.ModelResponse{Text: "done"}, ) - plainAgent := AddTool(NewAgent[any]("system", plainModel), func(input aliasInput) (aliasOutput, error) { + plainAgent := AddTool(NewAgent[NoDeps]("system", plainModel), func(input aliasInput) (aliasOutput, error) { return aliasOutput{Greeting: "hello " + input.Name}, nil }) if _, err := plainAgent.Run(context.Background(), "run tool", nil); err != nil { @@ -174,7 +174,7 @@ func TestAliasAgentAndDeferredWrappers(t *testing.T) { t.Fatalf("AddToolWithDeps run: %v", err) } - typed := NewTypedAgent[any, aliasTypedOutput]( + typed := NewTypedAgent[NoDeps, aliasTypedOutput]( "system", testprovider.NewTestModel(testprovider.ModelResponse{Text: `{"result":"ok"}`}), "Return typed output", diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 14809aa..a91b556 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -176,10 +176,10 @@ func TestE2E_Anthropic_StaticPrompt(t *testing.T) { ctx := ctxWithTimeout(t) model := newAnthropicModel(t) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a pirate. Always respond with pirate language including 'Arrr'. Keep responses under 50 words.", model, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) result, err := agent.Run(ctx, "Say hello", nil) @@ -198,12 +198,12 @@ func TestE2E_Anthropic_MultipleSystemPrompts(t *testing.T) { ctx := ctxWithTimeout(t) model := newAnthropicModel(t) - agent := agentic.NewAgent[any]("", model, - agentic.WithSystemPrompts[any]( - agentic.StaticPrompt[any]("You are a helpful assistant that always responds in uppercase only."), - agentic.StaticPrompt[any]("Keep your responses to exactly one sentence."), + agent := agentic.NewAgent[agentic.NoDeps]("", model, + agentic.WithSystemPrompts[agentic.NoDeps]( + agentic.StaticPrompt[agentic.NoDeps]("You are a helpful assistant that always responds in uppercase only."), + agentic.StaticPrompt[agentic.NoDeps]("Keep your responses to exactly one sentence."), ), - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) result, err := agent.Run(ctx, "What is 2+2?", nil) @@ -306,12 +306,12 @@ func TestE2E_OpenAI_MultipleSystemPrompts(t *testing.T) { ctx := ctxWithTimeout(t) model := newOpenAIModel(t) - agent := agentic.NewAgent[any]("", model, - agentic.WithSystemPrompts[any]( - agentic.StaticPrompt[any]("You always respond as a medieval knight."), - agentic.StaticPrompt[any]("End every response with 'For honor and glory!'"), + agent := agentic.NewAgent[agentic.NoDeps]("", model, + agentic.WithSystemPrompts[agentic.NoDeps]( + agentic.StaticPrompt[agentic.NoDeps]("You always respond as a medieval knight."), + agentic.StaticPrompt[agentic.NoDeps]("End every response with 'For honor and glory!'"), ), - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) result, err := agent.Run(ctx, "Greet me", nil) @@ -336,11 +336,11 @@ func TestE2E_Anthropic_OutputValidator_PassesFirstTry(t *testing.T) { model := newAnthropicModel(t) validatorCalled := false - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a helpful assistant. Respond concisely.", model, - agentic.WithMaxTokens[any](100), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithMaxTokens[agentic.NoDeps](100), + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { validatorCalled = true if len(output) == 0 { return agentic.NewValidationError("Response cannot be empty") @@ -369,11 +369,11 @@ func TestE2E_Anthropic_OutputValidator_RetryOnce(t *testing.T) { model := newAnthropicModel(t) attempt := 0 - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You answer math questions. Always include the numeric result in your response.", model, - agentic.WithMaxTokens[any](200), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithMaxTokens[agentic.NoDeps](200), + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { attempt++ if attempt == 1 { // Reject the first response to force a retry @@ -381,7 +381,7 @@ func TestE2E_Anthropic_OutputValidator_RetryOnce(t *testing.T) { } return nil }), - agentic.WithMaxValidationRetries[any](3), + agentic.WithMaxValidationRetries[agentic.NoDeps](3), ) result, err := agent.Run(ctx, "What is 15 * 7?", nil) @@ -400,18 +400,18 @@ func TestE2E_Anthropic_OutputValidator_RequiresJSON(t *testing.T) { defer cancel() model := newAnthropicModel(t) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( `You answer questions. You MUST respond with valid JSON in the format: {"answer": "your answer here"}. Nothing else.`, model, - agentic.WithMaxTokens[any](200), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithMaxTokens[agentic.NoDeps](200), + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { trimmed := strings.TrimSpace(output) if !strings.HasPrefix(trimmed, "{") || !strings.HasSuffix(trimmed, "}") { return agentic.NewValidationError("Response must be valid JSON starting with { and ending with }. Do not include any other text.") } return nil }), - agentic.WithMaxValidationRetries[any](3), + agentic.WithMaxValidationRetries[agentic.NoDeps](3), ) result, err := agent.Run(ctx, "What is the capital of France?", nil) @@ -434,18 +434,18 @@ func TestE2E_Anthropic_OutputValidator_MultipleValidators(t *testing.T) { v1Called := false v2Called := false - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a helpful assistant. Keep responses brief.", model, - agentic.WithMaxTokens[any](100), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithMaxTokens[agentic.NoDeps](100), + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { v1Called = true if len(output) == 0 { return agentic.NewValidationError("cannot be empty") } return nil }), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { v2Called = true return nil }), @@ -470,18 +470,18 @@ func TestE2E_OpenAI_OutputValidator_RetryOnce(t *testing.T) { model := newOpenAIModel(t) attempt := 0 - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You answer questions concisely.", model, - agentic.WithMaxTokens[any](200), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithMaxTokens[agentic.NoDeps](200), + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { attempt++ if attempt == 1 { return agentic.NewValidationError("Please start your response with 'Answer:'") } return nil }), - agentic.WithMaxValidationRetries[any](3), + agentic.WithMaxValidationRetries[agentic.NoDeps](3), ) result, err := agent.Run(ctx, "What is 3+3?", nil) @@ -550,11 +550,11 @@ func TestE2E_Anthropic_Stream_DynamicPrompts(t *testing.T) { t.Fatalf("failed to create model: %v", err) } - agent := agentic.NewAgent[any]("", m, - agentic.WithSystemPrompts[any]( - agentic.StaticPrompt[any]("You are a pirate. Always say 'Arrr'. Keep responses short (under 30 words)."), + agent := agentic.NewAgent[agentic.NoDeps]("", m, + agentic.WithSystemPrompts[agentic.NoDeps]( + agentic.StaticPrompt[agentic.NoDeps]("You are a pirate. Always say 'Arrr'. Keep responses short (under 30 words)."), ), - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) sr, err := agent.RunStream(ctx, "Greet me", nil) @@ -593,17 +593,17 @@ func TestE2E_Anthropic_WithToolsAndValidator(t *testing.T) { return WeatherOutput{Temperature: 72, Condition: "sunny"}, nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a weather assistant. When asked about weather, use the get_weather tool. Always include the temperature in your response.", model, - agentic.WithMaxTokens[any](200), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithMaxTokens[agentic.NoDeps](200), + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { if !strings.Contains(output, "72") { return agentic.NewValidationError("Response must include the temperature (72)") } return nil }), - agentic.WithMaxValidationRetries[any](3), + agentic.WithMaxValidationRetries[agentic.NoDeps](3), ).AddTool(tool, handler) result, err := agent.Run(ctx, "What's the weather in San Francisco?", nil) @@ -637,13 +637,13 @@ func TestE2E_Anthropic_StructTagValidation(t *testing.T) { } outputSpec := agentic.NewToolOutput[MovieReview]("Provide a movie review") - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a movie critic. Review the movie as requested.", model, - agentic.WithMaxTokens[any](300), + agentic.WithMaxTokens[agentic.NoDeps](300), ) - result, err := agentic.RunOutput[any, MovieReview]( //nolint:staticcheck + result, err := agentic.RunOutput[agentic.NoDeps, MovieReview]( //nolint:staticcheck ctx, agent, "Review The Matrix", nil, outputSpec, ) if err != nil { @@ -677,13 +677,13 @@ func TestE2E_OpenAI_StructTagValidation(t *testing.T) { } outputSpec := agentic.NewToolOutput[CityInfo]("Provide city information") - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You provide factual information about cities.", model, - agentic.WithMaxTokens[any](300), + agentic.WithMaxTokens[agentic.NoDeps](300), ) - result, err := agentic.RunOutput[any, CityInfo]( //nolint:staticcheck + result, err := agentic.RunOutput[agentic.NoDeps, CityInfo]( //nolint:staticcheck ctx, agent, "Tell me about Tokyo", nil, outputSpec, ) if err != nil { @@ -746,10 +746,10 @@ func TestAutoTool_Anthropic(t *testing.T) { t.Errorf("expected description from tool tag, got %q", tool.Function.Description) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a calculator assistant. Use the calculate tool for math questions.", model, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ).AddAutoTool(tool, handler) result, err := agent.Run(ctx, "What is 6 times 7?", nil) @@ -785,10 +785,10 @@ func TestAutoTool_OpenAI(t *testing.T) { t.Errorf("expected name %q, got %q", "translate_word", tool.Function.Name) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a translation assistant. Use the translate_word tool to translate words.", model, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ).AddAutoTool(tool, handler) result, err := agent.Run(ctx, "Translate 'hello' to Spanish", nil) @@ -820,10 +820,10 @@ func TestAutoTool_MultipleTools_Anthropic(t *testing.T) { return TranslateWordOutput{Translation: "quince"}, nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a multi-purpose assistant. Use the appropriate tool for each task.", model, - agentic.WithMaxTokens[any](300), + agentic.WithMaxTokens[agentic.NoDeps](300), ).AddAutoTool(calcTool, calcHandler).AddAutoTool(translateTool, translateHandler) result, err := agent.Run(ctx, "What is 5 + 10?", nil) @@ -855,10 +855,10 @@ func TestE2E_OpenAI_Responses_BasicRun(t *testing.T) { ctx := ctxWithTimeout(t) model := newOpenAIResponsesModel(t) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a concise assistant. Keep answers to one sentence.", model, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) result, err := agent.Run(ctx, "What is 2+2?", nil) @@ -880,10 +880,10 @@ func TestE2E_OpenAI_Responses_Streaming(t *testing.T) { ctx := ctxWithTimeout(t) model := newOpenAIResponsesModel(t) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a pirate. Always say 'Arrr'. Keep responses under 30 words.", model, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) sr, err := agent.RunStream(ctx, "Greet me", nil) @@ -921,10 +921,10 @@ func TestE2E_OpenAI_Responses_WithTools(t *testing.T) { return WeatherOutput{Temperature: 72, Condition: "sunny"}, nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a weather assistant. When asked about weather, use the get_weather tool. Always include the temperature in your response.", model, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ).AddTool(tool, handler) result, err := agent.Run(ctx, "What's the weather in San Francisco?", nil) @@ -955,13 +955,13 @@ func TestE2E_OpenAI_Responses_StructuredOutput(t *testing.T) { } outputSpec := agentic.NewToolOutput[CityInfo]("Provide city information") - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You provide factual information about cities.", model, - agentic.WithMaxTokens[any](300), + agentic.WithMaxTokens[agentic.NoDeps](300), ) - result, err := agentic.RunOutput[any, CityInfo]( //nolint:staticcheck + result, err := agentic.RunOutput[agentic.NoDeps, CityInfo]( //nolint:staticcheck ctx, agent, "Tell me about Tokyo", nil, outputSpec, ) if err != nil { @@ -991,10 +991,10 @@ func TestE2E_OpenAI_Responses_StreamWithTools(t *testing.T) { return "The population of Paris is approximately 2.1 million.", nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a research assistant. Use the lookup tool to answer questions. Include the facts from the tool in your response.", model, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ).AddTool(tool, handler) sr, err := agent.RunStream(ctx, "What is the population of Paris?", nil) @@ -1022,10 +1022,10 @@ func TestE2E_Together_BasicRun(t *testing.T) { ctx := ctxWithTimeout(t) model := newTogetherModel(t) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a concise assistant. Keep answers to one sentence.", model, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) result, err := agent.Run(ctx, "What is 2+2?", nil) @@ -1047,10 +1047,10 @@ func TestE2E_Together_Streaming(t *testing.T) { ctx := ctxWithTimeout(t) model := newTogetherModel(t) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a pirate. Always say 'Arrr'. Keep responses under 30 words.", model, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) sr, err := agent.RunStream(ctx, "Greet me", nil) @@ -1087,10 +1087,10 @@ func TestE2E_Together_WithTools(t *testing.T) { return WeatherOutput{Temperature: 72, Condition: "sunny"}, nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a weather assistant. When asked about weather, use the get_weather tool. Always include the temperature in your response.", model, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ).AddTool(tool, handler) result, err := agent.Run(ctx, "What's the weather in San Francisco?", nil) @@ -1121,10 +1121,10 @@ func TestE2E_Grok_BasicRun(t *testing.T) { t.Fatalf("failed to create grok model: %v", err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a concise assistant. Keep answers to one sentence.", m, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) result, err := agent.Run(ctx, "What is 2+2?", nil) @@ -1150,10 +1150,10 @@ func TestE2E_Grok_Streaming(t *testing.T) { t.Fatalf("failed to create grok model: %v", err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a pirate. Always say 'Arrr'. Keep responses under 30 words.", m, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) sr, err := agent.RunStream(ctx, "Greet me", nil) @@ -1194,10 +1194,10 @@ func TestE2E_Grok_WithTools(t *testing.T) { return WeatherOutput{Temperature: 72, Condition: "sunny"}, nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a weather assistant. When asked about weather, use the get_weather tool. Always include the temperature in your response.", m, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ).AddTool(tool, handler) result, err := agent.Run(ctx, "What's the weather in San Francisco?", nil) @@ -1228,10 +1228,10 @@ func TestE2E_OpenRouter_BasicRun(t *testing.T) { t.Fatalf("failed to create openrouter model: %v", err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a concise assistant. Keep answers to one sentence.", m, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) result, err := agent.Run(ctx, "What is 2+2?", nil) @@ -1257,10 +1257,10 @@ func TestE2E_OpenRouter_Streaming(t *testing.T) { t.Fatalf("failed to create openrouter model: %v", err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a pirate. Always say 'Arrr'. Keep responses under 30 words.", m, - agentic.WithMaxTokens[any](100), + agentic.WithMaxTokens[agentic.NoDeps](100), ) sr, err := agent.RunStream(ctx, "Greet me", nil) @@ -1301,10 +1301,10 @@ func TestE2E_OpenRouter_WithTools(t *testing.T) { return ORWeatherOutput{Temperature: 72, Condition: "sunny"}, nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a weather assistant. When asked about weather, use the get_weather tool. Always include the temperature in your response.", m, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ).AddTool(tool, handler) result, err := agent.Run(ctx, "What's the weather in San Francisco?", nil) @@ -1336,10 +1336,10 @@ func TestE2E_Ollama_BasicRun(t *testing.T) { t.Fatalf("failed to create ollama model: %v", err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a concise assistant. Keep answers to one sentence. Do not think out loud.", m, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ) result, err := agent.Run(ctx, "What is 2+2? Reply with just the answer.", nil) @@ -1366,10 +1366,10 @@ func TestE2E_Ollama_Streaming(t *testing.T) { t.Fatalf("failed to create ollama model: %v", err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a concise assistant. Keep responses under 30 words. Do not think out loud.", m, - agentic.WithMaxTokens[any](200), + agentic.WithMaxTokens[agentic.NoDeps](200), ) sr, err := agent.RunStream(ctx, "Say hello in one sentence.", nil) @@ -1410,10 +1410,10 @@ func TestE2E_Ollama_WithTools(t *testing.T) { return OllamaWeatherOutput{Temperature: 72, Condition: "sunny"}, nil }) - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a weather assistant. When asked about weather, use the get_weather tool. Always include the temperature number in your response. Do not think out loud.", m, - agentic.WithMaxTokens[any](300), + agentic.WithMaxTokens[agentic.NoDeps](300), ).AddTool(tool, handler) result, err := agent.Run(ctx, "What's the weather in San Francisco?", nil) diff --git a/end_strategy_test.go b/end_strategy_test.go index c1f9266..db80098 100644 --- a/end_strategy_test.go +++ b/end_strategy_test.go @@ -35,8 +35,8 @@ func TestEndStrategyExhaustive_AllToolsExecute(t *testing.T) { test.ModelResponse{Text: "Done with both"}, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithEndStrategy[any](agentic.EndStrategyExhaustive), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithEndStrategy[agentic.NoDeps](agentic.EndStrategyExhaustive), ).AddTool(toolA, handlerA).AddTool(toolB, handlerB) result, err := agent.Run(context.Background(), "go", nil) @@ -77,7 +77,7 @@ func TestEndStrategyExhaustive_IsDefault(t *testing.T) { test.ModelResponse{Text: "done"}, ) - agent := agentic.NewAgent[any]("test", model).AddTool(tool, handler) + agent := agentic.NewAgent[agentic.NoDeps]("test", model).AddTool(tool, handler) _, err := agent.Run(context.Background(), "go", nil) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -111,8 +111,8 @@ func TestEndStrategyEarly_StopsAtOutputTool(t *testing.T) { }, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithEndStrategy[any](agentic.EndStrategyEarly), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithEndStrategy[agentic.NoDeps](agentic.EndStrategyEarly), ).AddTool(tool, handler) // Manually mark "final_result" as an output tool @@ -156,8 +156,8 @@ func TestEndStrategyEarly_NoOutputTool_ExecutesAll(t *testing.T) { test.ModelResponse{Text: "done"}, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithEndStrategy[any](agentic.EndStrategyEarly), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithEndStrategy[agentic.NoDeps](agentic.EndStrategyEarly), ).AddTool(tool, handler) _, err := agent.Run(context.Background(), "go", nil) @@ -190,8 +190,8 @@ func TestWithRunEndStrategy_OverridesAgent(t *testing.T) { ) // Agent default is Exhaustive, but run overrides to Early - agent := agentic.NewAgent[any]("test", model, - agentic.WithEndStrategy[any](agentic.EndStrategyExhaustive), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithEndStrategy[agentic.NoDeps](agentic.EndStrategyExhaustive), ).AddTool(tool, handler) agent.SetOutputToolNames(map[string]bool{"output_tool": true}) diff --git a/examples/basic/main.go b/examples/basic/main.go index 6595919..60ff42a 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -21,7 +21,7 @@ func main() { log.Fatal(err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a concise assistant. Keep answers short.", model, ) diff --git a/examples/structured/main.go b/examples/structured/main.go index 0d7279f..158b0a1 100644 --- a/examples/structured/main.go +++ b/examples/structured/main.go @@ -31,7 +31,7 @@ func main() { log.Fatal(err) } - agent := agentic.NewTypedAgent[any, RepoSummary]( + agent := agentic.NewTypedAgent[agentic.NoDeps, RepoSummary]( "You summarize repositories as structured data.", model, "Return the final answer as a structured repository summary.", diff --git a/examples/tools/main.go b/examples/tools/main.go index a2f24c4..0850c29 100644 --- a/examples/tools/main.go +++ b/examples/tools/main.go @@ -54,7 +54,7 @@ func main() { log.Fatal(err) } - agent := agentic.NewAgent[any]( + agent := agentic.NewAgent[agentic.NoDeps]( "You are a helpful travel assistant.", model, ) diff --git a/history_test.go b/history_test.go index 9430793..a4fd253 100644 --- a/history_test.go +++ b/history_test.go @@ -122,7 +122,7 @@ func TestRunResult_HistoryMethods(t *testing.T) { test.ModelResponse{Text: "response"}, ) - agent := agentic.NewAgent[any]("system prompt", model) + agent := agentic.NewAgent[agentic.NoDeps]("system prompt", model) // Run with history historyMsgs := []agentic.Message{ @@ -162,7 +162,7 @@ func TestRunResult_ResumeConversation(t *testing.T) { test.ModelResponse{Text: "second response"}, ) - agent := agentic.NewAgent[any]("system prompt", model) + agent := agentic.NewAgent[agentic.NoDeps]("system prompt", model) // First run result1, err := agent.Run(context.Background(), "hello", nil) diff --git a/internal/core/context.go b/internal/core/context.go index 192d7b8..352c551 100644 --- a/internal/core/context.go +++ b/internal/core/context.go @@ -4,8 +4,9 @@ import "context" // RunContext provides context and dependencies to tool handlers. // -// DepsT is the type of user-provided dependencies. Use any struct -// that combines your state and resources: +// DepsT is the type of user-provided dependencies. Use [NoDeps] when you do +// not inject dependencies; otherwise use any struct that combines your state +// and resources: // // type MyDeps struct { // DB *sql.DB diff --git a/internal/core/deps.go b/internal/core/deps.go new file mode 100644 index 0000000..1756172 --- /dev/null +++ b/internal/core/deps.go @@ -0,0 +1,14 @@ +package core + +import "encoding/json" + +// NoDeps is the dependency type for agents and tools that do not inject dependencies. +// It implements [json.Marshaler] and always encodes as JSON null. +type NoDeps struct{} + +// MarshalJSON implements [json.Marshaler]. +func (NoDeps) MarshalJSON() ([]byte, error) { + return []byte("null"), nil +} + +var _ json.Marshaler = NoDeps{} diff --git a/internal/core/deps_test.go b/internal/core/deps_test.go new file mode 100644 index 0000000..7c95cdf --- /dev/null +++ b/internal/core/deps_test.go @@ -0,0 +1,24 @@ +package core + +import ( + "encoding/json" + "testing" +) + +func TestNoDepsMarshalJSON(t *testing.T) { + b, err := json.Marshal(NoDeps{}) + if err != nil { + t.Fatal(err) + } + if string(b) != "null" { + t.Fatalf("got %s, want null", b) + } + var p *NoDeps + b, err = json.Marshal(p) + if err != nil { + t.Fatal(err) + } + if string(b) != "null" { + t.Fatalf("nil pointer: got %s, want null", b) + } +} diff --git a/output.go b/output.go index fa88ff3..5f750ee 100644 --- a/output.go +++ b/output.go @@ -111,7 +111,7 @@ func (s *ToolOutputSpec[T]) Parse(msg Message) (any, error) { // // Deprecated: Use NewTypedAgent instead: // -// agent := agentic.NewTypedAgent[any, MovieReview]("prompt", model, "output description") +// agent := agentic.NewTypedAgent[NoDeps, MovieReview]("prompt", model, "output description") // result, err := agent.Run(ctx, "Review The Matrix", nil) func RunOutput[DepsT any, OutputT any]( ctx context.Context, diff --git a/output_mode_test.go b/output_mode_test.go index 37c3641..919a736 100644 --- a/output_mode_test.go +++ b/output_mode_test.go @@ -310,7 +310,7 @@ func TestTypedAgentWithNativeOutput(t *testing.T) { test.ModelResponse{Text: string(jsonOutput)}, ) - agent := agentic.NewTypedAgentWithMode[any, Review]( + agent := agentic.NewTypedAgentWithMode[agentic.NoDeps, Review]( "You review movies.", model, agentic.NewNativeOutput[Review]("movie_review", "A structured movie review"), @@ -351,7 +351,7 @@ func TestTypedAgentWithPromptedOutput(t *testing.T) { test.ModelResponse{Text: string(jsonOutput)}, ) - agent := agentic.NewTypedAgentWithMode[any, Review]( + agent := agentic.NewTypedAgentWithMode[agentic.NoDeps, Review]( "You review movies.", model, agentic.NewPromptedOutput[Review](), @@ -393,7 +393,7 @@ func TestTypedAgentWithTextProcessor(t *testing.T) { test.ModelResponse{Text: " 42 "}, ) - agent := agentic.NewTypedAgentWithMode[any, int]( + agent := agentic.NewTypedAgentWithMode[agentic.NoDeps, int]( "You return numbers.", model, agentic.NewTextProcessorOutput(func(text string) (int, error) { @@ -429,7 +429,7 @@ func TestTypedAgentWithToolOutput_BackwardCompat(t *testing.T) { }, ) - agent := agentic.NewTypedAgent[any, Review]( + agent := agentic.NewTypedAgent[agentic.NoDeps, Review]( "You review movies.", model, "Provide a structured movie review", @@ -456,7 +456,7 @@ func TestTypedAgentWithNativeOutput_ValidationRetry(t *testing.T) { test.ModelResponse{Text: `{"name":"Alice"}`}, ) - agent := agentic.NewTypedAgentWithMode[any, Validated]( + agent := agentic.NewTypedAgentWithMode[agentic.NoDeps, Validated]( "Return names.", model, agentic.NewNativeOutput[Validated]("name", "A name"), diff --git a/output_test.go b/output_test.go index 6a4286a..d2a51cf 100644 --- a/output_test.go +++ b/output_test.go @@ -34,9 +34,9 @@ func TestRunOutputStructured(t *testing.T) { }, ) - agent := agentic.NewAgent[any]("You review movies", model) + agent := agentic.NewAgent[agentic.NoDeps]("You review movies", model) - result, err := agentic.RunOutput[any, MovieReview]( + result, err := agentic.RunOutput[agentic.NoDeps, MovieReview]( context.Background(), agent, "Review The Matrix", nil, outputSpec, ) if err != nil { @@ -77,9 +77,9 @@ func TestRunOutputStructuredFull(t *testing.T) { }, ) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) - result, err := agentic.RunOutput[any, Result]( + result, err := agentic.RunOutput[agentic.NoDeps, Result]( context.Background(), agent, "go", nil, outputSpec, ) if err != nil { diff --git a/run_loop_internal_test.go b/run_loop_internal_test.go index acd88ef..77cc7d4 100644 --- a/run_loop_internal_test.go +++ b/run_loop_internal_test.go @@ -22,11 +22,11 @@ func TestPrepareLoopAndBuildRequestAppliesOverrides(t *testing.T) { seenToolCount := 0 limits := UsageLimits{MaxRequests: IntPtr(2)} - agent := NewAgent[any]( + agent := NewAgent[NoDeps]( "base system", &testutil.StubModel{NameValue: "coverage-model"}, - WithUsageLimits[any](limits), - WithToolPrepare[any](func(ctx RunContext[any], tools []Tool) ([]Tool, error) { + WithUsageLimits[NoDeps](limits), + WithToolPrepare[NoDeps](func(ctx RunContext[NoDeps], tools []Tool) ([]Tool, error) { seenToolCount = len(tools) return tools[:1], nil }), @@ -105,7 +105,7 @@ func TestPrepareLoopAndBuildRequestAppliesOverrides(t *testing.T) { func TestBuildRequestWrapsProcessorAndToolPrepareErrors(t *testing.T) { t.Run("history processor error", func(t *testing.T) { - agent := NewAgent[any]("system", &testutil.StubModel{NameValue: "coverage-model"}) + agent := NewAgent[NoDeps]("system", &testutil.StubModel{NameValue: "coverage-model"}) ls, err := agent.prepareLoop( context.Background(), "prompt", @@ -132,10 +132,10 @@ func TestBuildRequestWrapsProcessorAndToolPrepareErrors(t *testing.T) { toolDef, handler := MustToolPlain("tool_a", "tool a", func(input input) (string, error) { return "ok", nil }) - agent := NewAgent[any]( + agent := NewAgent[NoDeps]( "system", &testutil.StubModel{NameValue: "coverage-model"}, - WithToolPrepare[any](func(ctx RunContext[any], tools []Tool) ([]Tool, error) { + WithToolPrepare[NoDeps](func(ctx RunContext[NoDeps], tools []Tool) ([]Tool, error) { return nil, errors.New("no tools today") }), ).AddTool(toolDef, handler) diff --git a/stream_internal_test.go b/stream_internal_test.go index 9fc7c6c..8d6602f 100644 --- a/stream_internal_test.go +++ b/stream_internal_test.go @@ -41,7 +41,7 @@ func TestRunStreamTrueText(t *testing.T) { }}, } - agent := NewAgent[any]("system", model) + agent := NewAgent[NoDeps]("system", model) stream, err := agent.RunStream(context.Background(), "say hello", nil) if err != nil { t.Fatalf("RunStream: %v", err) @@ -92,7 +92,7 @@ func TestRunStreamTrueWithToolCall(t *testing.T) { }, } - agent := NewAgent[any]("system", model).AddTool(tool, handler) + agent := NewAgent[NoDeps]("system", model).AddTool(tool, handler) stream, err := agent.RunStream(context.Background(), "double 5", nil) if err != nil { t.Fatalf("RunStream: %v", err) @@ -129,7 +129,7 @@ func TestRunStreamTrueErrorsWhenToolsAreMissing(t *testing.T) { }}, } - agent := NewAgent[any]("system", model) + agent := NewAgent[NoDeps]("system", model) stream, err := agent.RunStream(context.Background(), "run missing tool", nil) if err != nil { t.Fatalf("RunStream: %v", err) @@ -149,13 +149,13 @@ func TestRunStreamTrueValidationErrorStopsStream(t *testing.T) { }}, } - agent := NewAgent[any]( + agent := NewAgent[NoDeps]( "system", model, - WithOutputValidatorFunc[any](func(ctx RunContext[any], output string) error { + WithOutputValidatorFunc[NoDeps](func(ctx RunContext[NoDeps], output string) error { return errors.New("output rejected") }), - WithMaxValidationRetries[any](0), + WithMaxValidationRetries[NoDeps](0), ) stream, err := agent.RunStream(context.Background(), "validate this", nil) @@ -184,16 +184,16 @@ func TestRunStreamTrueValidationRetryAndOutputTool(t *testing.T) { }, } - agent := NewAgent[any]( + agent := NewAgent[NoDeps]( "system", model, - WithOutputValidatorFunc[any](func(ctx RunContext[any], output string) error { + WithOutputValidatorFunc[NoDeps](func(ctx RunContext[NoDeps], output string) error { if output == "bad" { return NewValidationError("retry please") } return nil }), - WithMaxValidationRetries[any](1), + WithMaxValidationRetries[NoDeps](1), ) stream, err := agent.RunStream(context.Background(), "prompt", nil) @@ -238,7 +238,7 @@ func TestRunStreamTrueValidationRetryAndOutputTool(t *testing.T) { }}, } - agent := NewAgent[any]("system", model).SetOutputToolNames(map[string]bool{"__output__": true}) + agent := NewAgent[NoDeps]("system", model).SetOutputToolNames(map[string]bool{"__output__": true}) stream, err := agent.RunStream(context.Background(), "prompt", nil) if err != nil { @@ -264,8 +264,8 @@ func TestRunStreamTrueValidationRetryAndOutputTool(t *testing.T) { func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { t.Run("prepare loop error is returned directly", func(t *testing.T) { - agent := NewAgentDynamic[any]( - func(ctx RunContext[any]) (string, error) { + agent := NewAgentDynamic[NoDeps]( + func(ctx RunContext[NoDeps]) (string, error) { return "", errors.New("prompt failed") }, &testutil.ScriptedStreamModel{NameValue: "stream-model"}, @@ -279,10 +279,10 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { t.Run("pre request usage limit stops immediately", func(t *testing.T) { model := &testutil.ScriptedStreamModel{NameValue: "stream-model"} - agent := NewAgent[any]( + agent := NewAgent[NoDeps]( "system", model, - WithUsageLimits[any](UsageLimits{MaxRequests: IntPtr(0)}), + WithUsageLimits[NoDeps](UsageLimits{MaxRequests: IntPtr(0)}), ) stream, err := agent.RunStream(context.Background(), "prompt", nil) @@ -301,7 +301,7 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { t.Run("build request error is forwarded", func(t *testing.T) { model := &testutil.ScriptedStreamModel{NameValue: "stream-model"} - agent := NewAgent[any]("system", model) + agent := NewAgent[NoDeps]("system", model) stream, err := agent.RunStream( context.Background(), @@ -323,7 +323,7 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { t.Run("request stream error is wrapped", func(t *testing.T) { model := &testutil.ScriptedStreamModel{NameValue: "stream-model"} - agent := NewAgent[any]("system", model) + agent := NewAgent[NoDeps]("system", model) stream, err := agent.RunStream(context.Background(), "prompt", nil) if err != nil { @@ -344,7 +344,7 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { {Type: StreamEventError, Error: expected}, }}, } - agent := NewAgent[any]("system", model) + agent := NewAgent[NoDeps]("system", model) stream, err := agent.RunStream(context.Background(), "prompt", nil) if err != nil { @@ -365,10 +365,10 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { {Type: StreamEventDone, Usage: &Usage{TotalTokens: 1}}, }}, } - agent := NewAgent[any]( + agent := NewAgent[NoDeps]( "system", model, - WithUsageLimits[any](UsageLimits{MaxTotalTokens: IntPtr(0)}), + WithUsageLimits[NoDeps](UsageLimits{MaxTotalTokens: IntPtr(0)}), ) stream, err := agent.RunStream(context.Background(), "prompt", nil) @@ -398,10 +398,10 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { {Type: StreamEventDone, Usage: &Usage{TotalTokens: 1}}, }}, } - agent := NewAgent[any]( + agent := NewAgent[NoDeps]( "system", model, - WithUsageLimits[any](UsageLimits{MaxToolCalls: IntPtr(0)}), + WithUsageLimits[NoDeps](UsageLimits{MaxToolCalls: IntPtr(0)}), ).AddTool(tool, handler) stream, err := agent.RunStream(context.Background(), "prompt", nil) @@ -424,7 +424,7 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { {Type: StreamEventDone, Usage: &Usage{TotalTokens: 1}}, }}, } - agent := NewAgent[any]("system", model) + agent := NewAgent[NoDeps]("system", model) agent.registry = &streamRegistryStub{executeErr: errors.New("registry failed")} stream, err := agent.RunStream(context.Background(), "prompt", nil) @@ -460,7 +460,7 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { }, }, } - agent := NewAgent[any]("system", model, WithRetries[any](RetryConfig{MaxRetries: 1})).AddTool(tool, handler) + agent := NewAgent[NoDeps]("system", model, WithRetries[NoDeps](RetryConfig{MaxRetries: 1})).AddTool(tool, handler) stream, err := agent.RunStream(context.Background(), "prompt", nil) if err != nil { @@ -508,7 +508,7 @@ func TestRunStreamTrueAdditionalErrorPaths(t *testing.T) { {Type: StreamEventDone, Usage: &Usage{TotalTokens: 1}}, }}, } - agent := NewAgent[any]("system", model, WithMaxIterations[any](1)).AddTool(tool, handler) + agent := NewAgent[NoDeps]("system", model, WithMaxIterations[NoDeps](1)).AddTool(tool, handler) stream, err := agent.RunStream(context.Background(), "prompt", nil) if err != nil { @@ -535,7 +535,7 @@ func TestConsumeAndForward(t *testing.T) { StreamEvent{Type: StreamEventDone, Usage: &Usage{TotalTokens: 7}}, ) - msg, usage, err := (&Agent[any]{}).consumeAndForward(stream, out) + msg, usage, err := (&Agent[NoDeps]{}).consumeAndForward(stream, out) if err != nil { t.Fatalf("consumeAndForward: %v", err) } @@ -562,7 +562,7 @@ func TestConsumeAndForward(t *testing.T) { out := make(chan StreamEvent, 1) stream := testutil.NewScriptedStream(StreamEvent{Type: StreamEventError, Error: expected}) - _, _, err := (&Agent[any]{}).consumeAndForward(stream, out) + _, _, err := (&Agent[NoDeps]{}).consumeAndForward(stream, out) if !errors.Is(err, expected) { t.Fatalf("expected %v, got %v", expected, err) } diff --git a/stream_test.go b/stream_test.go index a7d210f..e96061e 100644 --- a/stream_test.go +++ b/stream_test.go @@ -10,7 +10,7 @@ import ( func TestRunStreamEvents(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "Hello streaming!"}) - agent := agentic.NewAgent[any]("You are helpful", model) + agent := agentic.NewAgent[agentic.NoDeps]("You are helpful", model) stream, err := agent.RunStream(context.Background(), "Hi", nil) if err != nil { @@ -45,7 +45,7 @@ func TestRunStreamEvents(t *testing.T) { func TestRunStreamText(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "Hello streaming!"}) - agent := agentic.NewAgent[any]("You are helpful", model) + agent := agentic.NewAgent[agentic.NoDeps]("You are helpful", model) stream, err := agent.RunStream(context.Background(), "Hi", nil) if err != nil { @@ -83,7 +83,7 @@ func TestRunStreamWithTools(t *testing.T) { test.ModelResponse{Text: "Result is 10"}, ) - agent := agentic.NewAgent[any]("math helper", model).AddTool(tool, handler) + agent := agentic.NewAgent[agentic.NoDeps]("math helper", model).AddTool(tool, handler) stream, err := agent.RunStream(context.Background(), "double 5", nil) if err != nil { @@ -107,7 +107,7 @@ func TestRunStreamError(t *testing.T) { }, }) - agent := agentic.NewAgent[any]("test", model, agentic.WithMaxIterations[any](2)) + agent := agentic.NewAgent[agentic.NoDeps]("test", model, agentic.WithMaxIterations[agentic.NoDeps](2)) stream, err := agent.RunStream(context.Background(), "go", nil) if err != nil { diff --git a/system_prompt.go b/system_prompt.go index 8d84666..eb1cb71 100644 --- a/system_prompt.go +++ b/system_prompt.go @@ -14,7 +14,7 @@ type SystemPrompt[DepsT any] struct { // // Example: // -// prompt := agentic.StaticPrompt[any]("You are a helpful assistant") +// prompt := agentic.StaticPrompt[NoDeps]("You are a helpful assistant") func StaticPrompt[DepsT any](text string) SystemPrompt[DepsT] { return SystemPrompt[DepsT]{static: text} } diff --git a/system_prompt_test.go b/system_prompt_test.go index f75e19a..8cc4177 100644 --- a/system_prompt_test.go +++ b/system_prompt_test.go @@ -11,8 +11,8 @@ import ( ) func TestStaticPromptResolve(t *testing.T) { - sp := agentic.StaticPrompt[any]("Hello world") - ctx := agentic.RunContext[any]{Ctx: context.Background()} + sp := agentic.StaticPrompt[agentic.NoDeps]("Hello world") + ctx := agentic.RunContext[agentic.NoDeps]{Ctx: context.Background()} text, err := sp.Resolve(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -42,10 +42,10 @@ func TestDynamicPromptResolve(t *testing.T) { } func TestDynamicPromptError(t *testing.T) { - sp := agentic.DynamicPrompt[any](func(ctx agentic.RunContext[any]) (string, error) { + sp := agentic.DynamicPrompt[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps]) (string, error) { return "", fmt.Errorf("boom") }) - ctx := agentic.RunContext[any]{Ctx: context.Background()} + ctx := agentic.RunContext[agentic.NoDeps]{Ctx: context.Background()} _, err := sp.Resolve(ctx) if err == nil { t.Fatal("expected error") @@ -55,10 +55,10 @@ func TestDynamicPromptError(t *testing.T) { func TestMultipleStaticPromptsConcatenated(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("", model, - agentic.WithSystemPrompts[any]( - agentic.StaticPrompt[any]("You are a helpful assistant."), - agentic.StaticPrompt[any]("Always respond in JSON."), + agent := agentic.NewAgent[agentic.NoDeps]("", model, + agentic.WithSystemPrompts[agentic.NoDeps]( + agentic.StaticPrompt[agentic.NoDeps]("You are a helpful assistant."), + agentic.StaticPrompt[agentic.NoDeps]("Always respond in JSON."), ), ) @@ -117,10 +117,10 @@ func TestMixedStaticDynamicPrompts(t *testing.T) { func TestDynamicPromptErrorPropagation(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("", model, - agentic.WithSystemPrompts[any]( - agentic.StaticPrompt[any]("ok"), - agentic.DynamicPrompt[any](func(ctx agentic.RunContext[any]) (string, error) { + agent := agentic.NewAgent[agentic.NoDeps]("", model, + agentic.WithSystemPrompts[agentic.NoDeps]( + agentic.StaticPrompt[agentic.NoDeps]("ok"), + agentic.DynamicPrompt[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps]) (string, error) { return "", fmt.Errorf("dynamic prompt failed") }), ), @@ -140,9 +140,9 @@ func TestWithSystemPromptsOverridesLegacyPrompt(t *testing.T) { // Create agent with legacy static prompt, but also WithSystemPrompts. // WithSystemPrompts should take priority. - agent := agentic.NewAgent[any]("LEGACY PROMPT", model, - agentic.WithSystemPrompts[any]( - agentic.StaticPrompt[any]("NEW PROMPT"), + agent := agentic.NewAgent[agentic.NoDeps]("LEGACY PROMPT", model, + agentic.WithSystemPrompts[agentic.NoDeps]( + agentic.StaticPrompt[agentic.NoDeps]("NEW PROMPT"), ), ) @@ -160,7 +160,7 @@ func TestWithSystemPromptsOverridesLegacyPrompt(t *testing.T) { func TestBackwardCompatStaticPrompt(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("Legacy static", model) + agent := agentic.NewAgent[agentic.NoDeps]("Legacy static", model) _, err := agent.Run(context.Background(), "hi", nil) if err != nil { @@ -176,8 +176,8 @@ func TestBackwardCompatStaticPrompt(t *testing.T) { func TestBackwardCompatDynamicPrompt(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgentDynamic[any]( - func(ctx agentic.RunContext[any]) (string, error) { + agent := agentic.NewAgentDynamic[agentic.NoDeps]( + func(ctx agentic.RunContext[agentic.NoDeps]) (string, error) { return "Legacy dynamic", nil }, model, @@ -198,11 +198,11 @@ func TestBackwardCompatDynamicPrompt(t *testing.T) { func TestEmptyPromptSegmentsSkipped(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("", model, - agentic.WithSystemPrompts[any]( - agentic.StaticPrompt[any]("Hello"), - agentic.StaticPrompt[any](""), // empty, should be skipped - agentic.StaticPrompt[any]("World"), + agent := agentic.NewAgent[agentic.NoDeps]("", model, + agentic.WithSystemPrompts[agentic.NoDeps]( + agentic.StaticPrompt[agentic.NoDeps]("Hello"), + agentic.StaticPrompt[agentic.NoDeps](""), // empty, should be skipped + agentic.StaticPrompt[agentic.NoDeps]("World"), ), ) diff --git a/thinking_test.go b/thinking_test.go index c55d476..9c15228 100644 --- a/thinking_test.go +++ b/thinking_test.go @@ -13,8 +13,8 @@ func TestThinkingConfig_AgentOption(t *testing.T) { test.ModelResponse{Text: "answer"}, ) - agent := agentic.NewAgent[any]("prompt", model, - agentic.WithThinking[any](agentic.ThinkingConfig{ + agent := agentic.NewAgent[agentic.NoDeps]("prompt", model, + agentic.WithThinking[agentic.NoDeps](agentic.ThinkingConfig{ Enabled: true, BudgetTokens: 10000, }), @@ -49,7 +49,7 @@ func TestThinkingConfig_NotSet(t *testing.T) { test.ModelResponse{Text: "answer"}, ) - agent := agentic.NewAgent[any]("prompt", model) + agent := agentic.NewAgent[agentic.NoDeps]("prompt", model) _, err := agent.Run(context.Background(), "question", nil) if err != nil { diff --git a/tool/builder_test.go b/tool/builder_test.go index cac26b4..1d81d23 100644 --- a/tool/builder_test.go +++ b/tool/builder_test.go @@ -46,7 +46,7 @@ func TestMustToolWithDepsPanics(t *testing.T) { } }() type Input struct{} - MustToolWithDeps[Input, string, any]("", "desc", func(ctx core.RunContext[any], in Input) (string, error) { return "", nil }) + MustToolWithDeps[Input, string, NoDeps]("", "desc", func(ctx core.RunContext[NoDeps], in Input) (string, error) { return "", nil }) } func TestToolPlainError(t *testing.T) { @@ -59,7 +59,7 @@ func TestToolPlainError(t *testing.T) { func TestToolWithDepsError(t *testing.T) { type Input struct{} - _, _, err := ToolWithDeps[Input, string, any]("", "desc", func(ctx core.RunContext[any], in Input) (string, error) { return "", nil }) + _, _, err := ToolWithDeps[Input, string, NoDeps]("", "desc", func(ctx core.RunContext[NoDeps], in Input) (string, error) { return "", nil }) if err == nil { t.Error("expected error for empty name") } @@ -98,7 +98,7 @@ func TestDepsToolHandlerMarshalError(t *testing.T) { X int `json:"x"` } - _, handler := MustToolWithDeps("test", "test", func(ctx core.RunContext[any], in In) (string, error) { + _, handler := MustToolWithDeps("test", "test", func(ctx core.RunContext[NoDeps], in In) (string, error) { return "ok", nil }) diff --git a/tool/deps.go b/tool/deps.go index cbdb209..ba82234 100644 --- a/tool/deps.go +++ b/tool/deps.go @@ -8,6 +8,10 @@ import ( "github.com/regularkevvv/agentic-go/internal/core" ) +// NoDeps is the empty dependency type for tools that do not use RunContext dependencies. +// It matches [core.NoDeps] and the root [agentic.NoDeps] alias. +type NoDeps = core.NoDeps + // DepsToolHandler implements ToolHandler for tools that need access to agent dependencies. type DepsToolHandler[TInput any, TOutput any, DepsT any] struct { name string diff --git a/tool_prepare_test.go b/tool_prepare_test.go index 0700e87..c141f16 100644 --- a/tool_prepare_test.go +++ b/tool_prepare_test.go @@ -27,8 +27,8 @@ func TestToolPrepare_FilterTools(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) // Filter out tool_b - agent := agentic.NewAgent[any]("test", model, - agentic.WithToolPrepare[any](func(ctx agentic.RunContext[any], tools []agentic.Tool) ([]agentic.Tool, error) { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithToolPrepare[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], tools []agentic.Tool) ([]agentic.Tool, error) { var filtered []agentic.Tool for _, tool := range tools { if tool.Function.Name != "tool_b" { @@ -68,8 +68,8 @@ func TestToolPrepare_EmptyToolList(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "no tools available"}) // Return empty tool list - agent := agentic.NewAgent[any]("test", model, - agentic.WithToolPrepare[any](func(ctx agentic.RunContext[any], tools []agentic.Tool) ([]agentic.Tool, error) { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithToolPrepare[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], tools []agentic.Tool) ([]agentic.Tool, error) { return nil, nil }), ).AddTool(tool, handler) @@ -99,8 +99,8 @@ func TestToolPrepare_ErrorPropagation(t *testing.T) { return "", nil }) - agent := agentic.NewAgent[any]("test", model, - agentic.WithToolPrepare[any](func(ctx agentic.RunContext[any], tools []agentic.Tool) ([]agentic.Tool, error) { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithToolPrepare[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], tools []agentic.Tool) ([]agentic.Tool, error) { return nil, fmt.Errorf("permission denied") }), ).AddTool(tool, handler) @@ -124,8 +124,8 @@ func TestToolPrepare_ModifyDescription(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("test", model, - agentic.WithToolPrepare[any](func(ctx agentic.RunContext[any], tools []agentic.Tool) ([]agentic.Tool, error) { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithToolPrepare[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], tools []agentic.Tool) ([]agentic.Tool, error) { modified := make([]agentic.Tool, len(tools)) for i, t := range tools { t.Function.Description = "Modified: " + t.Function.Description @@ -222,8 +222,8 @@ func TestToolPrepare_CalledEachIteration(t *testing.T) { ) prepareCallCount := 0 - agent := agentic.NewAgent[any]("test", model, - agentic.WithToolPrepare[any](func(ctx agentic.RunContext[any], tools []agentic.Tool) ([]agentic.Tool, error) { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithToolPrepare[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], tools []agentic.Tool) ([]agentic.Tool, error) { prepareCallCount++ return tools, nil }), diff --git a/tool_retry_test.go b/tool_retry_test.go index 43ef4d9..90f249f 100644 --- a/tool_retry_test.go +++ b/tool_retry_test.go @@ -51,8 +51,8 @@ func TestPerToolRetry_OverridesGlobal(t *testing.T) { test.ModelResponse{Text: "done"}, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithRetries[any](agentic.RetryConfig{MaxRetries: 1}), // global = 1 + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithRetries[agentic.NoDeps](agentic.RetryConfig{MaxRetries: 1}), // global = 1 ).AddTool(tool, handler) result, err := agent.Run(context.Background(), "go", nil) @@ -89,8 +89,8 @@ func TestPerToolRetry_ZeroMeansNoRetries(t *testing.T) { test.ModelResponse{Text: "gave up"}, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithRetries[any](agentic.RetryConfig{MaxRetries: 5}), // global allows 5 + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithRetries[agentic.NoDeps](agentic.RetryConfig{MaxRetries: 5}), // global allows 5 ).AddTool(tool, handler) result, err := agent.Run(context.Background(), "go", nil) @@ -141,8 +141,8 @@ func TestPerToolRetry_NoConfig_FallsBackToGlobal(t *testing.T) { test.ModelResponse{Text: "done"}, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithRetries[any](agentic.RetryConfig{MaxRetries: 2}), // global = 2 + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithRetries[agentic.NoDeps](agentic.RetryConfig{MaxRetries: 2}), // global = 2 ).AddTool(tool, handler) result, err := agent.Run(context.Background(), "go", nil) @@ -186,8 +186,8 @@ func TestPerToolRetry_DifferentToolsDifferentLimits(t *testing.T) { test.ModelResponse{Text: "done"}, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithRetries[any](agentic.RetryConfig{MaxRetries: 1}), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithRetries[agentic.NoDeps](agentic.RetryConfig{MaxRetries: 1}), ).AddTool(toolFlaky, handlerFlaky).AddTool(toolStrict, handlerStrict) result, err := agent.Run(context.Background(), "go", nil) diff --git a/typed_agent.go b/typed_agent.go index 519bcd1..23f802c 100644 --- a/typed_agent.go +++ b/typed_agent.go @@ -20,7 +20,7 @@ import ( // Summary string `json:"summary" validate:"required,min=10" description:"Brief review"` // } // -// agent := agentic.NewTypedAgent[any, MovieReview]( +// agent := agentic.NewTypedAgent[NoDeps, MovieReview]( // "You review movies.", // model, // "Provide a structured movie review", @@ -37,11 +37,11 @@ type TypedAgent[DepsT any, OutputT any] struct { // // Example: // -// agent := agentic.NewTypedAgent[any, MovieReview]( +// agent := agentic.NewTypedAgent[NoDeps, MovieReview]( // "You are a movie critic.", // model, // "Provide a structured movie review", -// agentic.WithMaxTokens[any](500), +// agentic.WithMaxTokens[NoDeps](500), // ) func NewTypedAgent[DepsT any, OutputT any]( systemPrompt string, @@ -72,7 +72,7 @@ func NewTypedAgent[DepsT any, OutputT any]( // // Example with native output: // -// agent := agentic.NewTypedAgentWithMode[any, MovieReview]( +// agent := agentic.NewTypedAgentWithMode[NoDeps, MovieReview]( // "You are a movie critic.", // model, // agentic.NewNativeOutput[MovieReview]("movie_review", "A structured movie review"), diff --git a/typed_agent_internal_test.go b/typed_agent_internal_test.go index 1998958..4219ee7 100644 --- a/typed_agent_internal_test.go +++ b/typed_agent_internal_test.go @@ -72,7 +72,7 @@ func TestTypedAgentHelpersAndTextProcessorError(t *testing.T) { return "ok", nil }) - ta := NewTypedAgentWithMode[any, typedAgentCoverageValue]( + ta := NewTypedAgentWithMode[NoDeps, typedAgentCoverageValue]( "system", &testutil.StubModel{NameValue: "typed-model"}, NewToolOutput[typedAgentCoverageValue]("desc"), @@ -112,8 +112,8 @@ func TestTypedAgentHelpersAndTextProcessorError(t *testing.T) { t.Fatalf("expected missing assistant message error, got %v", err) } - wrong := &TypedAgent[any, typedAgentCoverageValue]{ - agent: NewAgent[any]("system", &testutil.StubModel{NameValue: "typed-model"}), + wrong := &TypedAgent[NoDeps, typedAgentCoverageValue]{ + agent: NewAgent[NoDeps]("system", &testutil.StubModel{NameValue: "typed-model"}), outputSpec: wrongTypeOutputSpec{}, } _, err := wrong.parseResult(&RunResult{Messages: []Message{NewTextMessage(RoleAssistant, "answer")}}) @@ -130,7 +130,7 @@ func TestTypedAgentHelpersAndTextProcessorError(t *testing.T) { }}, }, } - textAgent := NewTypedAgentWithMode[any, int]( + textAgent := NewTypedAgentWithMode[NoDeps, int]( "system", model, NewTextProcessorOutput(func(text string) (int, error) { @@ -165,10 +165,10 @@ func TestTypedAgentRunFallsBackToToolModeForUnknownOutputMode(t *testing.T) { }, } - agent := NewAgent[any]("system", model) + agent := NewAgent[NoDeps]("system", model) registerOutputTools(agent, spec.ToolOutputSpec) - ta := &TypedAgent[any, typedAgentCoverageValue]{ + ta := &TypedAgent[NoDeps, typedAgentCoverageValue]{ agent: agent, outputSpec: spec, } @@ -184,8 +184,8 @@ func TestTypedAgentRunFallsBackToToolModeForUnknownOutputMode(t *testing.T) { func TestTypedAgentRunAdditionalErrorPaths(t *testing.T) { t.Run("tool output parse error is wrapped", func(t *testing.T) { - ta := &TypedAgent[any, typedAgentCoverageValue]{ - agent: NewAgent[any]("system", &testutil.StubModel{ + ta := &TypedAgent[NoDeps, typedAgentCoverageValue]{ + agent: NewAgent[NoDeps]("system", &testutil.StubModel{ NameValue: "typed-model", Response: &ChatResponse{ Choices: []Choice{{ @@ -204,8 +204,8 @@ func TestTypedAgentRunAdditionalErrorPaths(t *testing.T) { }) t.Run("response format parse error is wrapped", func(t *testing.T) { - ta := &TypedAgent[any, typedAgentCoverageValue]{ - agent: NewAgent[any]("system", &testutil.StubModel{ + ta := &TypedAgent[NoDeps, typedAgentCoverageValue]{ + agent: NewAgent[NoDeps]("system", &testutil.StubModel{ NameValue: "typed-model", Response: &ChatResponse{ Choices: []Choice{{ @@ -224,8 +224,8 @@ func TestTypedAgentRunAdditionalErrorPaths(t *testing.T) { }) t.Run("response format model error is returned", func(t *testing.T) { - ta := &TypedAgent[any, typedAgentCoverageValue]{ - agent: NewAgent[any]("system", &testutil.StubModel{ + ta := &TypedAgent[NoDeps, typedAgentCoverageValue]{ + agent: NewAgent[NoDeps]("system", &testutil.StubModel{ NameValue: "typed-model", Err: errors.New("boom"), }), @@ -239,7 +239,7 @@ func TestTypedAgentRunAdditionalErrorPaths(t *testing.T) { }) t.Run("text processor model error is returned", func(t *testing.T) { - ta := NewTypedAgentWithMode[any, int]( + ta := NewTypedAgentWithMode[NoDeps, int]( "system", &testutil.StubModel{ NameValue: "typed-model", diff --git a/typed_agent_test.go b/typed_agent_test.go index cacaff7..e30c40b 100644 --- a/typed_agent_test.go +++ b/typed_agent_test.go @@ -33,7 +33,7 @@ func TestTypedAgentBasic(t *testing.T) { }, ) - agent := agentic.NewTypedAgent[any, MovieReview]( + agent := agentic.NewTypedAgent[agentic.NoDeps, MovieReview]( "You review movies.", model, "Provide a structured movie review", @@ -93,7 +93,7 @@ func TestTypedAgentWithTools(t *testing.T) { }, ) - agent := agentic.NewTypedAgent[any, Summary]( + agent := agentic.NewTypedAgent[agentic.NoDeps, Summary]( "You answer questions.", model, "Provide a structured summary", @@ -188,7 +188,7 @@ func TestTypedAgentValidationRetry(t *testing.T) { }, ) - agent := agentic.NewTypedAgent[any, ValidatedOutput]( + agent := agentic.NewTypedAgent[agentic.NoDeps, ValidatedOutput]( "You provide scores.", model, "Provide a name and score", @@ -214,11 +214,11 @@ func TestTypedAgentValidationExhausted(t *testing.T) { }, ) - agent := agentic.NewTypedAgent[any, ValidatedOutput]( + agent := agentic.NewTypedAgent[agentic.NoDeps, ValidatedOutput]( "You provide scores.", model, "Provide a name and score", - agentic.WithMaxValidationRetries[any](1), + agentic.WithMaxValidationRetries[agentic.NoDeps](1), ) _, err := agent.Run(context.Background(), "Score this", nil) @@ -259,7 +259,7 @@ func TestTypedAgentAddAutoTool(t *testing.T) { }, ) - agent := agentic.NewTypedAgent[any, Answer]( + agent := agentic.NewTypedAgent[agentic.NoDeps, Answer]( "You are a calculator.", model, "Provide the answer", @@ -289,10 +289,10 @@ func TestRunOutputStillWorks(t *testing.T) { }, ) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) outputSpec := agentic.NewToolOutput[Result]("Provide result") - result, err := agentic.RunOutput[any, Result]( + result, err := agentic.RunOutput[agentic.NoDeps, Result]( context.Background(), agent, "go", nil, outputSpec, ) if err != nil { diff --git a/usage_limits_test.go b/usage_limits_test.go index 254f277..3774635 100644 --- a/usage_limits_test.go +++ b/usage_limits_test.go @@ -25,11 +25,11 @@ func TestUsageLimitsRequestLimit(t *testing.T) { return "sunny", nil }) - agent := agentic.NewAgent[any]("test", model, - agentic.WithUsageLimits[any](agentic.UsageLimits{ + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithUsageLimits[agentic.NoDeps](agentic.UsageLimits{ MaxRequests: agentic.IntPtr(3), }), - agentic.WithMaxIterations[any](100), + agentic.WithMaxIterations[agentic.NoDeps](100), ) agent.AddTool(tool, handler) @@ -70,8 +70,8 @@ func TestUsageLimitsResponseTokenLimit(t *testing.T) { }, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithUsageLimits[any](agentic.UsageLimits{ + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithUsageLimits[agentic.NoDeps](agentic.UsageLimits{ MaxResponseTokens: agentic.IntPtr(50), // Lower than the 100 tokens the model uses }), ) @@ -116,8 +116,8 @@ func TestUsageLimitsTotalTokenLimit(t *testing.T) { return "pong", nil }) - agent := agentic.NewAgent[any]("test", model, - agentic.WithUsageLimits[any](agentic.UsageLimits{ + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithUsageLimits[agentic.NoDeps](agentic.UsageLimits{ MaxTotalTokens: agentic.IntPtr(200), // Exceeded after second request (150+250=400) }), ) @@ -152,11 +152,11 @@ func TestUsageLimitsToolCallLimit(t *testing.T) { return "pong", nil }) - agent := agentic.NewAgent[any]("test", model, - agentic.WithUsageLimits[any](agentic.UsageLimits{ + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithUsageLimits[agentic.NoDeps](agentic.UsageLimits{ MaxToolCalls: agentic.IntPtr(2), // 3 tool calls requested, limit is 2 }), - agentic.WithMaxIterations[any](100), + agentic.WithMaxIterations[agentic.NoDeps](100), ) agent.AddTool(tool, handler) @@ -176,7 +176,7 @@ func TestUsageLimitsToolCallLimit(t *testing.T) { func TestUsageLimitsNoLimitNoError(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "hello"}) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) result, err := agent.Run(context.Background(), "hi", nil) if err != nil { @@ -204,11 +204,11 @@ func TestUsageLimitsRunOptionOverridesAgent(t *testing.T) { }) // Agent has generous limit - agent := agentic.NewAgent[any]("test", model, - agentic.WithUsageLimits[any](agentic.UsageLimits{ + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithUsageLimits[agentic.NoDeps](agentic.UsageLimits{ MaxRequests: agentic.IntPtr(100), }), - agentic.WithMaxIterations[any](100), + agentic.WithMaxIterations[agentic.NoDeps](100), ) agent.AddTool(tool, handler) @@ -235,8 +235,8 @@ func TestUsageLimitsRequestTokenLimit(t *testing.T) { }, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithUsageLimits[any](agentic.UsageLimits{ + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithUsageLimits[agentic.NoDeps](agentic.UsageLimits{ MaxRequestTokens: agentic.IntPtr(100), // Lower than 500 prompt tokens }), ) @@ -281,7 +281,7 @@ func TestUsageTrackingAccumulation(t *testing.T) { return "pong", nil }) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) agent.AddTool(tool, handler) result, err := agent.Run(context.Background(), "go", nil) diff --git a/validator.go b/validator.go index f05d5b8..c2f2c80 100644 --- a/validator.go +++ b/validator.go @@ -123,7 +123,7 @@ type OutputValidator[DepsT any] interface { // // Example: // -// agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { +// agentic.WithOutputValidatorFunc[NoDeps](func(ctx agentic.RunContext[NoDeps], output string) error { // if !strings.Contains(output, "ANSWER:") { // return agentic.NewValidationError("Response must contain 'ANSWER:'") // } diff --git a/validator_internal_test.go b/validator_internal_test.go index 593d681..ce0a03e 100644 --- a/validator_internal_test.go +++ b/validator_internal_test.go @@ -91,13 +91,13 @@ func TestValidateStructAdditionalTagsAndTypedValidator(t *testing.T) { }) } - validator := TypedOutputValidatorFunc[any, validatorCoverageValue](func(ctx RunContext[any], output validatorCoverageValue) error { + validator := TypedOutputValidatorFunc[NoDeps, validatorCoverageValue](func(ctx RunContext[NoDeps], output validatorCoverageValue) error { if output.Value != "ok" { return errors.New("unexpected output") } return nil }) - if err := validator.ValidateTyped(RunContext[any]{Ctx: context.Background()}, validatorCoverageValue{Value: "ok"}); err != nil { + if err := validator.ValidateTyped(RunContext[NoDeps]{Ctx: context.Background()}, validatorCoverageValue{Value: "ok"}); err != nil { t.Fatalf("expected typed validator to pass, got %v", err) } } diff --git a/validator_test.go b/validator_test.go index ec8b001..94c8115 100644 --- a/validator_test.go +++ b/validator_test.go @@ -12,8 +12,8 @@ import ( func TestValidatorAlwaysPasses(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "Hello!"}) - agent := agentic.NewAgent[any]("test", model, - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { return nil // always pass }), ) @@ -37,8 +37,8 @@ func TestValidatorRejectsFirstAttemptPassesSecond(t *testing.T) { test.ModelResponse{Text: "good answer response"}, // second attempt after validation failure ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { callCount++ if !strings.Contains(output, "good") { return agentic.NewValidationError("Response must contain 'good'") @@ -83,12 +83,12 @@ func TestMultipleValidatorsAllMustPass(t *testing.T) { v1Called := false v2Called := false - agent := agentic.NewAgent[any]("test", model, - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { v1Called = true return nil }), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { v2Called = true return nil }), @@ -111,14 +111,14 @@ func TestMultipleValidatorsFirstFails(t *testing.T) { v2CallCount := 0 - agent := agentic.NewAgent[any]("test", model, - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { if output == "bad" { return agentic.NewValidationError("rejected") } return nil }), - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { v2CallCount++ return nil }), @@ -143,11 +143,11 @@ func TestMaxValidationRetriesExceeded(t *testing.T) { test.ModelResponse{Text: "bad"}, ) - agent := agentic.NewAgent[any]("test", model, - agentic.WithOutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithOutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { return agentic.NewValidationError("always fails") }), - agentic.WithMaxValidationRetries[any](2), + agentic.WithMaxValidationRetries[agentic.NoDeps](2), ) _, err := agent.Run(context.Background(), "hi", nil) @@ -162,12 +162,12 @@ func TestMaxValidationRetriesExceeded(t *testing.T) { func TestValidatorWithInterface(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "response"}) - validator := agentic.OutputValidatorFunc[any](func(ctx agentic.RunContext[any], output string) error { + validator := agentic.OutputValidatorFunc[agentic.NoDeps](func(ctx agentic.RunContext[agentic.NoDeps], output string) error { return nil }) - agent := agentic.NewAgent[any]("test", model, - agentic.WithOutputValidator[any](validator), + agent := agentic.NewAgent[agentic.NoDeps]("test", model, + agentic.WithOutputValidator[agentic.NoDeps](validator), ) result, err := agent.Run(context.Background(), "hi", nil) @@ -232,7 +232,7 @@ func TestValidatorWithDeps(t *testing.T) { func TestNoValidatorsDoesNothing(t *testing.T) { model := test.NewTestModel(test.ModelResponse{Text: "ok"}) - agent := agentic.NewAgent[any]("test", model) + agent := agentic.NewAgent[agentic.NoDeps]("test", model) result, err := agent.Run(context.Background(), "hi", nil) if err != nil {