Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions agent/llmagent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import (
"github.com/sylumi/agentkit/tool"
)

const defaultMaxModelCalls = 10

// Config describes an LLM agent.
type Config struct {
Name string
Expand All @@ -29,7 +27,7 @@ type Config struct {
Toolsets []tool.Toolset
// GenerateConfig and its nested data must remain unchanged during Run.
GenerateConfig *model.GenerateConfig
// MaxModelCalls limits model calls per Run; zero defaults to 10.
// MaxModelCalls limits model calls per Run and must be positive.
MaxModelCalls int
}

Expand All @@ -52,11 +50,8 @@ func New(cfg Config) (agent.Agent, error) {
if cfg.Model == nil {
return nil, fmt.Errorf("llmagent: model is required")
}
if cfg.MaxModelCalls < 0 {
return nil, fmt.Errorf("llmagent: max model calls must not be negative")
}
if cfg.MaxModelCalls == 0 {
cfg.MaxModelCalls = defaultMaxModelCalls
if cfg.MaxModelCalls <= 0 {
return nil, fmt.Errorf("llmagent: max model calls must be positive")
}
return &llmAgent{
name: cfg.Name,
Expand Down
41 changes: 25 additions & 16 deletions agent/llmagent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestStreamingEvents(t *testing.T) {
}}}}, nil)
}
})
a, err := llmagent.New(llmagent.Config{Name: "streaming-agent", Model: llm})
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "streaming-agent", Model: llm})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -167,23 +167,24 @@ func TestNew(t *testing.T) {
return nil
})
for _, cfg := range []llmagent.Config{
{Model: llm}, {Name: " \t", Model: llm}, {Name: "chat"},
{Model: llm, MaxModelCalls: 1}, {Name: " \t", Model: llm, MaxModelCalls: 1}, {Name: "chat", MaxModelCalls: 1},
{Name: "chat", Model: llm, MaxModelCalls: 0},
{Name: "chat", Model: llm, MaxModelCalls: -1},
} {
if _, err := llmagent.New(cfg); err == nil {
t.Fatalf("accepted invalid config: %+v", cfg)
}
}
for _, description := range []string{"", "Answers questions."} {
a, err := llmagent.New(llmagent.Config{Name: "chat", Description: description, Model: llm})
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Description: description, Model: llm})
if err != nil {
t.Fatal(err)
}
if a.Name() != "chat" || a.Description() != description {
t.Fatalf("agent identity: %q, %q", a.Name(), a.Description())
}
}
for _, limit := range []int{0, 1, 20} {
for _, limit := range []int{1, 20} {
if _, err := llmagent.New(llmagent.Config{Name: "chat", Model: llm, MaxModelCalls: limit}); err != nil {
t.Fatalf("rejected model call limit %d: %v", limit, err)
}
Expand Down Expand Up @@ -227,7 +228,8 @@ func TestRunToolsAndGenerateConfig(t *testing.T) {
}
modelCalls := 0
a, err := llmagent.New(llmagent.Config{
Name: "chat", Instruction: wantRequest.Instructions,
MaxModelCalls: 10,
Name: "chat", Instruction: wantRequest.Instructions,
Tools: configuredTools, GenerateConfig: tc.config,
Model: modelFunc(func(_ context.Context, req model.Request, stream bool) iter.Seq2[model.Event, error] {
modelCalls++
Expand Down Expand Up @@ -290,7 +292,8 @@ func TestRunRejectsInvalidTools(t *testing.T) {
wantError = `duplicate tool name "weather"`
}
a, err := llmagent.New(llmagent.Config{
Name: "chat", Tools: configuredTools,
MaxModelCalls: 10,
Name: "chat", Tools: configuredTools,
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
t.Fatal("invalid tools reached the model")
return nil
Expand Down Expand Up @@ -356,7 +359,8 @@ func TestRunHistoryAndPersistence(t *testing.T) {
}
})
a, err := llmagent.New(llmagent.Config{
Name: "chat", Description: "Not a prompt.", Model: llm, Instruction: "Keep {placeholders} literal.",
MaxModelCalls: 10,
Name: "chat", Description: "Not a prompt.", Model: llm, Instruction: "Keep {placeholders} literal.",
})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -482,7 +486,7 @@ func TestRunModelErrors(t *testing.T) {
Partial: model.PartialOutput{Parts: []model.PartialPart{{Kind: model.PartText, Text: &partialText}}},
}
closed := false
a, err := llmagent.New(llmagent.Config{Name: "chat", Model: modelFunc(
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Model: modelFunc(
func(modelCtx context.Context, _ model.Request, _ bool) iter.Seq2[model.Event, error] {
return func(yield func(model.Event, error) bool) {
defer func() { closed = true }()
Expand Down Expand Up @@ -516,7 +520,7 @@ func TestRunModelErrors(t *testing.T) {
func TestRunCanceledBeforeConsumption(t *testing.T) {
_, invocation := newInvocation(t)
configuredTool := &stubTool{t: t, definition: model.ToolDefinition{Name: "weather"}}
a, err := llmagent.New(llmagent.Config{Name: "chat", Tools: []tool.Tool{configuredTool}, Model: modelFunc(
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Tools: []tool.Tool{configuredTool}, Model: modelFunc(
func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
t.Fatal("canceled invocation called the model")
return nil
Expand Down Expand Up @@ -546,7 +550,7 @@ func TestRunCanceledBeforeConsumption(t *testing.T) {
func TestRunEarlyExit(t *testing.T) {
_, invocation := newInvocation(t)
closed, stopped := false, false
a, err := llmagent.New(llmagent.Config{Name: "chat", Model: modelFunc(
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Model: modelFunc(
func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
return func(yield func(model.Event, error) bool) {
defer func() { closed = true }()
Expand Down Expand Up @@ -705,7 +709,8 @@ func TestRunMultipleToolsAndErrors(t *testing.T) {
}
modelCalls := 0
a, err := llmagent.New(llmagent.Config{
Name: "chat", Tools: []tool.Tool{second, first},
MaxModelCalls: 10,
Name: "chat", Tools: []tool.Tool{second, first},
Model: modelFunc(func(_ context.Context, req model.Request, _ bool) iter.Seq2[model.Event, error] {
modelCalls++
steps = append(steps, fmt.Sprintf("model-%d", modelCalls))
Expand Down Expand Up @@ -752,7 +757,7 @@ func TestRunModelCallLimit(t *testing.T) {
limit int
want int
}{
{name: "default", want: 10},
{name: "ten calls", limit: 10, want: 10},
{name: "one call", limit: 1, want: 1},
{name: "two calls", limit: 2, want: 2},
} {
Expand Down Expand Up @@ -828,7 +833,8 @@ func TestRunStopsBeforeFurtherWork(t *testing.T) {
return "done", nil
}}
a, err := llmagent.New(llmagent.Config{
Name: "chat", Tools: []tool.Tool{lookup},
MaxModelCalls: 10,
Name: "chat", Tools: []tool.Tool{lookup},
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
modelCalls++
if modelCalls != 1 {
Expand Down Expand Up @@ -900,7 +906,8 @@ func TestRunToolCancellation(t *testing.T) {
}}
modelCalls := 0
a, err := llmagent.New(llmagent.Config{
Name: "chat", Tools: []tool.Tool{lookup},
MaxModelCalls: 10,
Name: "chat", Tools: []tool.Tool{lookup},
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
modelCalls++
if modelCalls != 1 {
Expand Down Expand Up @@ -945,7 +952,8 @@ func TestRunPersistenceFailureStops(t *testing.T) {
return "done", nil
}}
a, err := llmagent.New(llmagent.Config{
Name: "chat", Tools: []tool.Tool{lookup},
MaxModelCalls: 10,
Name: "chat", Tools: []tool.Tool{lookup},
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
modelCalls++
if modelCalls != 1 {
Expand Down Expand Up @@ -999,7 +1007,8 @@ func TestRunModelErrorAfterTool(t *testing.T) {
}}
closed := false
a, err := llmagent.New(llmagent.Config{
Name: "chat", Tools: []tool.Tool{lookup},
MaxModelCalls: 10,
Name: "chat", Tools: []tool.Tool{lookup},
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
modelCalls++
if modelCalls == 1 {
Expand Down
14 changes: 8 additions & 6 deletions agent/llmagent/toolsets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ func TestToolsetsLifecycle(t *testing.T) {
sets := []tool.Toolset{first, plain, emptySet}
modelCalls := 0
a, err := llmagent.New(llmagent.Config{
Name: "chat", Instruction: "base", Tools: []tool.Tool{newTool("static")},
MaxModelCalls: 10,
Name: "chat", Instruction: "base", Tools: []tool.Tool{newTool("static")},
Toolsets: sets, GenerateConfig: &model.GenerateConfig{},
Model: modelFunc(func(_ context.Context, req model.Request, _ bool) iter.Seq2[model.Event, error] {
modelCalls++
Expand Down Expand Up @@ -154,7 +155,8 @@ func TestEmptyToolsetDiscoveredOnce(t *testing.T) {
store, invocation := newInvocation(t)
discoveries, modelCalls := 0, 0
a, err := llmagent.New(llmagent.Config{
Name: "empty", Toolsets: []tool.Toolset{toolsetFunc{"empty", func(context.Context) ([]tool.Tool, error) {
MaxModelCalls: 10,
Name: "empty", Toolsets: []tool.Toolset{toolsetFunc{"empty", func(context.Context) ([]tool.Tool, error) {
discoveries++
return empty, nil
}}},
Expand Down Expand Up @@ -204,7 +206,7 @@ func TestToolsetRegistrationFailures(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
_, invocation := newInvocation(t)
a, err := llmagent.New(llmagent.Config{Name: "chat", Tools: tc.tools, Toolsets: tc.sets,
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Tools: tc.tools, Toolsets: tc.sets,
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
t.Fatal("invalid registration reached model")
return nil
Expand Down Expand Up @@ -263,7 +265,7 @@ func TestToolsetFailureAndCancellation(t *testing.T) {
toolsetFunc: toolsetFunc{"later", func(context.Context) ([]tool.Tool, error) { laterDiscoveries++; return nil, nil }},
process: func(context.Context, *model.Request) error { t.Fatal("processing continued after failure"); return nil },
}
a, err := llmagent.New(llmagent.Config{Name: "chat", Toolsets: []tool.Toolset{first, later},
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Toolsets: []tool.Toolset{first, later},
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
t.Fatal("model called after failure")
return nil
Expand Down Expand Up @@ -318,7 +320,7 @@ func TestToolsetsConcurrentRuns(t *testing.T) {
return nil
},
}
a, err := llmagent.New(llmagent.Config{Name: "chat", Instruction: "base", Toolsets: []tool.Toolset{set},
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Instruction: "base", Toolsets: []tool.Toolset{set},
Model: modelFunc(func(ctx context.Context, req model.Request, _ bool) iter.Seq2[model.Event, error] {
id := ctx.Value(runKey{}).(string)
if req.Instructions != "base|"+id {
Expand Down Expand Up @@ -384,7 +386,7 @@ func TestToolsetProcessingStopsWithRun(t *testing.T) {
return cause
},
}
a, err := llmagent.New(llmagent.Config{Name: "chat", Toolsets: []tool.Toolset{set},
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Toolsets: []tool.Toolset{set},
Model: modelFunc(func(context.Context, model.Request, bool) iter.Seq2[model.Event, error] {
modelCalls++
return resultStream(toolCallResult(model.ToolCallPart{ID: "call", Name: "lookup", Arguments: json.RawMessage(`{}`)}))
Expand Down
2 changes: 1 addition & 1 deletion cmd/launcher/web/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func TestWebInfersPublicModelLabel(t *testing.T) {
if err != nil {
t.Fatal(err)
}
a, err := llmagent.New(llmagent.Config{Name: "chat", Model: llm})
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "chat", Model: llm})
if err != nil {
t.Fatal(err)
}
Expand Down
9 changes: 5 additions & 4 deletions examples/assistant/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ func main() {

thinking, maxOutput := true, int64(4096)
a, err := llmagent.New(llmagent.Config{
Name: "assistant",
Model: llm,
Instruction: "You are a helpful assistant. Answer in the user's language. Use get_current_time when you need the current date or time.",
Tools: []tool.Tool{currentTime},
MaxModelCalls: 10,
Name: "assistant",
Model: llm,
Instruction: "You are a helpful assistant. Answer in the user's language. Use get_current_time when you need the current date or time.",
Tools: []tool.Tool{currentTime},
GenerateConfig: &model.GenerateConfig{
MaxOutputTokens: &maxOutput,
Reasoning: &model.ReasoningConfig{Enabled: &thinking, Effort: "high"},
Expand Down
3 changes: 2 additions & 1 deletion runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ func TestRunToolLoopAndNextTurn(t *testing.T) {
}
var requests []model.Request
a, err := llmagent.New(llmagent.Config{
Name: "weather-agent", Tools: []tool.Tool{weather},
MaxModelCalls: 10,
Name: "weather-agent", Tools: []tool.Tool{weather},
Model: modelFunc(func(ctx context.Context, req model.Request, stream bool) iter.Seq2[model.Event, error] {
modelCalls++
if ctx != t.Context() || stream != streaming || modelCalls > 3 {
Expand Down
3 changes: 2 additions & 1 deletion runner/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ func TestRunSkillsToolset(t *testing.T) {
calls := 0
var firstInstructions string
a, err := llmagent.New(llmagent.Config{
Name: "skills", Instruction: "Help the user.", Toolsets: []tool.Toolset{skills},
MaxModelCalls: 10,
Name: "skills", Instruction: "Help the user.", Toolsets: []tool.Toolset{skills},
Model: modelFunc(func(_ context.Context, req model.Request, stream bool) iter.Seq2[model.Event, error] {
calls++
if stream != streaming {
Expand Down
4 changes: 2 additions & 2 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func TestLLMAgentToolsAcrossTurns(t *testing.T) {
yield(model.ResultEvent{Result: result}, nil)
}
})
a, err := llmagent.New(llmagent.Config{Name: "calculator", Model: llm, Tools: []tool.Tool{add}})
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "calculator", Model: llm, Tools: []tool.Tool{add}})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -418,7 +418,7 @@ func TestLiveStreamArrivesBeforeFinalCommit(t *testing.T) {
}}}}, nil)
}
})
a, err := llmagent.New(llmagent.Config{Name: "live", Model: llm})
a, err := llmagent.New(llmagent.Config{MaxModelCalls: 10, Name: "live", Model: llm})
if err != nil {
t.Fatal(err)
}
Expand Down
Loading