Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
32045be
chore(deps): bump the all group with 5 updates (#190)
dependabot[bot] Mar 30, 2026
783847b
chore(deps): bump the all group with 3 updates (#193)
dependabot[bot] Apr 6, 2026
92dc3ae
chore(deps): bump github.com/ardanlabs/kronk in the kronk group (#194)
dependabot[bot] Apr 6, 2026
c8896d3
chore: add `CODEOWNERS` to ask review from me
andreynering Apr 10, 2026
06f2e8f
chore(deps): bump the all group with 2 updates (#199)
dependabot[bot] Apr 13, 2026
5f01141
chore(deps): bump github.com/ardanlabs/kronk in the kronk group (#200)
dependabot[bot] Apr 13, 2026
ce26050
ci: fix govulncheck by updating to go 1.26.2 (#201)
andreynering Apr 13, 2026
bbf53dc
fix(agent): buffer tool calls
meowgorithm Apr 13, 2026
15bced7
chore(agent): move tool execution after stream loop + add test
meowgorithm Apr 13, 2026
d9b6308
v0.17.2
meowgorithm Apr 13, 2026
61bc0b2
feat(agent): add the ability to stop a turn and end the agent loop
meowgorithm Apr 13, 2026
402a113
v0.18.0
meowgorithm Apr 15, 2026
f60d4fe
feat(anthropic): add `EffortXHigh` constant (#204)
DanielleMaywood Apr 16, 2026
b2c4b61
v0.19.0
andreynering Apr 16, 2026
12cec64
chore(deps): bump the all group with 4 updates (#206)
dependabot[bot] Apr 20, 2026
3bfd3c8
chore(deps): bump github.com/ardanlabs/kronk in the kronk group (#207)
dependabot[bot] Apr 20, 2026
471520c
fix(anthropic/openai/google): wrap `io.ErrUnexpectedEOF` as `Provider…
ljuti Apr 22, 2026
95dcd6e
feat(anthropic): add document support for pdf and text file content b…
Nic-vdwalt Apr 22, 2026
b852eff
v0.20.0
andreynering Apr 22, 2026
6d22b85
feat(openai): add gpt-5.5 responses models (#211)
ibetitsmike Apr 24, 2026
5975b48
fix(openaicompat): parse "reasoning" field and include reasoning_cont…
carsonfarmer Apr 24, 2026
d9ff008
v0.21.0
andreynering Apr 24, 2026
f8a3e2f
chore(deps): bump the all group with 2 updates (#215)
dependabot[bot] Apr 27, 2026
b7c17e0
ci: fix build running twice on pull requests
andreynering Apr 27, 2026
1720eac
fix(openrouter): don't clip thinking output on claude models
meowgorithm Apr 25, 2026
5f0d1e3
fix(openrouter): use xstrings helper
meowgorithm Apr 27, 2026
d8aafda
chore(deps): import strings package
andreynering Apr 27, 2026
43a2aa5
test: re-record test fixtures
andreynering Apr 27, 2026
ea03063
v0.21.1
andreynering Apr 27, 2026
505a5b8
chore(deps): upgrade kronk to v1.24.0 (#218)
ardan-bkennedy Apr 29, 2026
d73b308
fix(anthropic): preserve `tool_use` when `ToolCallPart.Input` is empt…
ljuti Apr 29, 2026
b2f7230
fix(openai,openaicompat): apply `WithName` to provider lookup and `Na…
fwang2002 Apr 29, 2026
966aa60
feat(agent): add `ToolChoice` to `AgentCall`, `AgentStreamCall`, and …
fwang2002 Apr 29, 2026
6dbd4f5
v0.22.0
andreynering Apr 29, 2026
ea8f291
fix(openai): handle media tool results (#221)
meowgorithm Apr 30, 2026
792d186
feat: add `ExtraBody` provider option to openai-compat (#220)
andreynering Apr 30, 2026
f9f1cf6
v0.23.0
andreynering Apr 30, 2026
54c9dd5
chore(deps): bump the all group with 4 updates (#225)
dependabot[bot] May 4, 2026
2c8825d
chore(deps): bump github.com/ardanlabs/kronk (#226)
dependabot[bot] May 4, 2026
ba2ee05
fix: guard against nil response in Generate to prevent panic (#228)
slzcdhd May 8, 2026
e0ddf87
fix(openai): yield tool calls with invalid JSON instead of silently d…
mkaaad May 8, 2026
c954320
chore: update go patch version to make govulncheck happy
andreynering May 8, 2026
e6ec395
v0.23.1
andreynering May 8, 2026
d9d0f63
chore(deps): bump github.com/ardanlabs/kronk in the kronk group (#231)
dependabot[bot] May 11, 2026
c03d606
chore(deps): bump google.golang.org/genai (#230)
dependabot[bot] May 11, 2026
5698022
chore(deps): update kronk (#233)
ardan-bkennedy May 12, 2026
d097d04
v0.23.2
andreynering May 12, 2026
26c3c41
fix(providers/anthropic): preserve web search error replay (#237)
ethanndickson May 14, 2026
53ec8a6
fix(providers/anthropic): preserve reasoning replay fidelity (#238)
ethanndickson May 14, 2026
11bf8ee
chore: handle partial stream endings to prevent silent truncation (#232)
meowgorithm May 14, 2026
d259d31
feat(responses): add WithResponsesAPIFunc to opt in specific models
taciturnaxolotl May 12, 2026
8be3606
v0.24.0
andreynering May 14, 2026
a7be611
feat(retry): also retry on network connection errors
processtrader May 17, 2026
4cf3a0f
refactor(retry): move functions to a better place
andreynering May 18, 2026
15e3da1
v0.25.0
andreynering May 18, 2026
54c29a3
chore(deps): bump the all group with 2 updates (#243)
dependabot[bot] May 18, 2026
c5fcd7c
chore(deps): bump github.com/ardanlabs/kronk (#244)
dependabot[bot] May 18, 2026
e277abd
fix(openai): require terminal stream events before finishing (#246)
ethanndickson May 20, 2026
9aa7447
fix(anthropic): require message_stop before finishing stream (#245)
ethanndickson May 20, 2026
98dc56e
v0.25.1
andreynering May 20, 2026
c8d4eb2
chore: add `.helix/ignore`
andreynering May 21, 2026
1b525a1
fix(bedrock): enforce `us-east-1` as region for bedrock (#248)
andreynering May 22, 2026
2569fb6
ci: fix govulncheck
andreynering May 22, 2026
dcabfc5
v0.25.2
andreynering May 22, 2026
470094b
chore(deps): bump the all group with 2 updates (#250)
dependabot[bot] May 25, 2026
e519b1b
chore(deps): bump github.com/ardanlabs/kronk (#251)
dependabot[bot] May 25, 2026
75704ee
feat(bedrock): add WithRegion option to configure AWS region (#252)
andreynering May 26, 2026
412a0a5
v0.26.0
andreynering May 26, 2026
f4434d7
ci(labeler): do not use custom token
andreynering May 26, 2026
1327dc1
feat(errors): support ionet context length error
taciturnaxolotl May 28, 2026
22785da
fix(bedrock): apply region if given for aws config as well (#255)
andreynering May 28, 2026
da2032f
v0.27.0
andreynering May 28, 2026
55563e4
feat(errors): add proper context window errors for alibaba
taciturnaxolotl May 28, 2026
8057236
v0.28.0
taciturnaxolotl May 29, 2026
7985930
chore(deps): bump github.com/ardanlabs/kronk in the kronk group (#263)
dependabot[bot] Jun 2, 2026
2d27a0e
chore(deps): bump github.com/kaptinlin/jsonschema (#264)
dependabot[bot] Jun 2, 2026
283ecba
feat(anthropic): add support for "extra body" (#266)
andreynering Jun 2, 2026
0e72001
v0.29.0
andreynering Jun 2, 2026
a79c920
fix(providers/anthropic): forward PDF filename to document title
ethanndickson Jun 3, 2026
c9afe3b
fix(providers/anthropic): forward text filename and warn on unsupport…
ethanndickson Jun 3, 2026
5876e91
ci: fix govulncheck
andreynering Jun 3, 2026
124d86a
v0.29.1
andreynering Jun 3, 2026
853fa9f
chore: merge main into coder_2_33
ibetitsmike Jun 4, 2026
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @andreynering
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: build
on: [push, pull_request]
on:
push:
branches: [main]
pull_request:

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:

permissions:
issues: write
pull-requests: write
contents: read

jobs:
Expand All @@ -26,5 +27,4 @@ jobs:
enable-versioned-regex: 0
include-title: 1
include-body: 0
repo-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
issue-number: ${{ github.event.inputs.issue-number || github.event.issue.number || github.event.pull_request.number }}
1 change: 1 addition & 0 deletions .helix/ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
providertests/testdata
166 changes: 106 additions & 60 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ type agentSettings struct {
providerDefinedTools []ProviderDefinedTool
executableProviderTools []ExecutableProviderTool
tools []AgentTool
toolChoice *ToolChoice
maxRetries *int

model LanguageModel
Expand All @@ -162,12 +163,13 @@ type AgentCall struct {
Files []FilePart `json:"files"`
Messages []Message `json:"messages"`
MaxOutputTokens *int64
Temperature *float64 `json:"temperature"`
TopP *float64 `json:"top_p"`
TopK *int64 `json:"top_k"`
PresencePenalty *float64 `json:"presence_penalty"`
FrequencyPenalty *float64 `json:"frequency_penalty"`
ActiveTools []string `json:"active_tools"`
Temperature *float64 `json:"temperature"`
TopP *float64 `json:"top_p"`
TopK *int64 `json:"top_k"`
PresencePenalty *float64 `json:"presence_penalty"`
FrequencyPenalty *float64 `json:"frequency_penalty"`
ActiveTools []string `json:"active_tools"`
ToolChoice *ToolChoice `json:"tool_choice"`
ProviderOptions ProviderOptions
OnRetry OnRetryCallback
MaxRetries *int
Expand Down Expand Up @@ -252,12 +254,13 @@ type AgentStreamCall struct {
Files []FilePart `json:"files"`
Messages []Message `json:"messages"`
MaxOutputTokens *int64
Temperature *float64 `json:"temperature"`
TopP *float64 `json:"top_p"`
TopK *int64 `json:"top_k"`
PresencePenalty *float64 `json:"presence_penalty"`
FrequencyPenalty *float64 `json:"frequency_penalty"`
ActiveTools []string `json:"active_tools"`
Temperature *float64 `json:"temperature"`
TopP *float64 `json:"top_p"`
TopK *int64 `json:"top_k"`
PresencePenalty *float64 `json:"presence_penalty"`
FrequencyPenalty *float64 `json:"frequency_penalty"`
ActiveTools []string `json:"active_tools"`
ToolChoice *ToolChoice `json:"tool_choice"`
Headers map[string]string
ProviderOptions ProviderOptions
OnRetry OnRetryCallback
Expand Down Expand Up @@ -335,6 +338,7 @@ func (a *agent) prepareCall(call AgentCall) AgentCall {
call.PresencePenalty = cmp.Or(call.PresencePenalty, a.settings.presencePenalty)
call.FrequencyPenalty = cmp.Or(call.FrequencyPenalty, a.settings.frequencyPenalty)
call.MaxRetries = cmp.Or(call.MaxRetries, a.settings.maxRetries)
call.ToolChoice = cmp.Or(call.ToolChoice, a.settings.toolChoice)

if len(call.StopWhen) == 0 && len(a.settings.stopWhen) > 0 {
call.StopWhen = a.settings.stopWhen
Expand Down Expand Up @@ -383,6 +387,9 @@ func (a *agent) Generate(ctx context.Context, opts AgentCall) (*AgentResult, err
stepSystemPrompt := a.settings.systemPrompt
stepActiveTools := opts.ActiveTools
stepToolChoice := ToolChoiceAuto
if opts.ToolChoice != nil {
stepToolChoice = *opts.ToolChoice
}
disableAllTools := false
stepTools := a.settings.tools
if opts.PrepareStep != nil {
Expand Down Expand Up @@ -485,7 +492,12 @@ func (a *agent) Generate(ctx context.Context, opts AgentCall) (*AgentResult, err

toolResults, err := a.executeTools(ctx, stepTools, stepExecProviderTools, stepToolCalls, nil)

// Build step content with validated tool calls and tool results. // Provider-executed tool calls are kept as-is.
// If any tool result requested a stop, deliver all results but don't
// request another completion from the model.
stopTurnRequested := hasStopTurn(toolResults)

// Build step content with validated tool calls and tool results.
// Provider-executed tool calls are kept as-is.
stepContent := []Content{}
toolCallIndex := 0
for _, content := range result.Content {
Expand Down Expand Up @@ -523,7 +535,7 @@ func (a *agent) Generate(ctx context.Context, opts AgentCall) (*AgentResult, err
steps = append(steps, stepResult)
shouldStop := isStopConditionMet(opts.StopWhen, steps)

if shouldStop || err != nil || len(stepToolCalls) == 0 || result.FinishReason != FinishReasonToolCalls {
if shouldStop || err != nil || stopTurnRequested || len(stepToolCalls) == 0 || result.FinishReason != FinishReasonToolCalls {
break
}
}
Expand Down Expand Up @@ -561,6 +573,15 @@ func isStopConditionMet(conditions []StopCondition, steps []StepResult) bool {
return false
}

func hasStopTurn(results []ToolResultContent) bool {
for _, r := range results {
if r.StopTurn {
return true
}
}
return false
}

func toResponseMessages(content []Content) []Message {
var assistantParts []MessagePart
var toolParts []MessagePart
Expand Down Expand Up @@ -729,13 +750,15 @@ func (a *agent) executeSingleTool(ctx context.Context, toolMap map[string]AgentT
Error: err,
}
result.ClientMetadata = toolResult.Metadata
result.StopTurn = toolResult.StopTurn
if toolResultCallback != nil {
_ = toolResultCallback(result)
}
return result, true
}

result.ClientMetadata = toolResult.Metadata
result.StopTurn = toolResult.StopTurn
if toolResult.IsError {
result.Result = ToolResultOutputContentError{
Error: errors.New(toolResult.Content),
Expand Down Expand Up @@ -771,6 +794,7 @@ func (a *agent) Stream(ctx context.Context, opts AgentStreamCall) (*AgentResult,
PresencePenalty: opts.PresencePenalty,
FrequencyPenalty: opts.FrequencyPenalty,
ActiveTools: opts.ActiveTools,
ToolChoice: opts.ToolChoice,
ProviderOptions: opts.ProviderOptions,
MaxRetries: opts.MaxRetries,
OnRetry: opts.OnRetry,
Expand Down Expand Up @@ -801,6 +825,9 @@ func (a *agent) Stream(ctx context.Context, opts AgentStreamCall) (*AgentResult,
stepSystemPrompt := a.settings.systemPrompt
stepActiveTools := call.ActiveTools
stepToolChoice := ToolChoiceAuto
if call.ToolChoice != nil {
stepToolChoice = *call.ToolChoice
}
disableAllTools := false
stepTools := a.settings.tools
// Apply step preparation if provided
Expand Down Expand Up @@ -1190,6 +1217,14 @@ func WithProviderDefinedTools(tools ...ProviderTool) AgentOption {
}
}

// WithToolChoice sets the default tool choice for the agent. It is overridden
// by the ToolChoice on a specific call, and by PrepareStep at the step level.
func WithToolChoice(choice ToolChoice) AgentOption {
return func(s *agentSettings) {
s.toolChoice = &choice
}
}

// WithStopConditions sets the stop conditions for the agent.
func WithStopConditions(conditions ...StopCondition) AgentOption {
return func(s *agentSettings) {
Expand Down Expand Up @@ -1247,11 +1282,7 @@ func (a *agent) processStepStream(ctx context.Context, stream StreamResponse, op
toolCall ToolCallContent
parallel bool
}
toolChan := make(chan toolExecutionRequest, 10)
var toolExecutionWg sync.WaitGroup
var toolStateMu sync.Mutex
toolResults := make([]ToolResultContent, 0)
var toolExecutionErr error
var pendingDispatches []toolExecutionRequest

// Create a map for quick tool lookup
toolMap := make(map[string]AgentTool)
Expand All @@ -1264,43 +1295,6 @@ func (a *agent) processStepStream(ctx context.Context, stream StreamResponse, op
execProviderToolMap[ept.GetName()] = ept
}

// Semaphores for controlling parallelism
parallelSem := make(chan struct{}, 5)
var sequentialMu sync.Mutex

// Single coordinator goroutine that dispatches tools
toolExecutionWg.Go(func() {
for req := range toolChan {
if req.parallel {
parallelSem <- struct{}{}
toolExecutionWg.Go(func() {
defer func() { <-parallelSem }()
result, isCriticalError := a.executeSingleTool(ctx, toolMap, execProviderToolMap, req.toolCall, opts.OnToolResult)
toolStateMu.Lock()
toolResults = append(toolResults, result)
if isCriticalError && toolExecutionErr == nil {
if errorResult, ok := result.Result.(ToolResultOutputContentError); ok && errorResult.Error != nil {
toolExecutionErr = errorResult.Error
}
}
toolStateMu.Unlock()
})
} else {
sequentialMu.Lock()
result, isCriticalError := a.executeSingleTool(ctx, toolMap, execProviderToolMap, req.toolCall, opts.OnToolResult)
toolStateMu.Lock()
toolResults = append(toolResults, result)
if isCriticalError && toolExecutionErr == nil {
if errorResult, ok := result.Result.(ToolResultOutputContentError); ok && errorResult.Error != nil {
toolExecutionErr = errorResult.Error
}
}
toolStateMu.Unlock()
sequentialMu.Unlock()
}
}
})

// Process stream parts
for part := range stream {
// Forward all parts to chunk callback
Expand Down Expand Up @@ -1475,8 +1469,9 @@ func (a *agent) processStepStream(ctx context.Context, stream StreamResponse, op
isParallel = tool.Info().Parallel
}

// Send tool call to execution channel
toolChan <- toolExecutionRequest{toolCall: validatedToolCall, parallel: isParallel}
// Buffer dispatch until stream is fully consumed so that all
// OnToolCall callbacks complete before any tool result is written.
pendingDispatches = append(pendingDispatches, toolExecutionRequest{toolCall: validatedToolCall, parallel: isParallel})

// Clean up active tool call
delete(activeToolCalls, part.ID)
Expand Down Expand Up @@ -1534,7 +1529,58 @@ func (a *agent) processStepStream(ctx context.Context, stream StreamResponse, op
}
}

// Close the tool execution channel and wait for all executions to complete
// All tool calls are now collected. Create the execution channel sized to
// avoid blocking during dispatch, start the coordinator, then flush the batch.
toolChan := make(chan toolExecutionRequest, len(pendingDispatches))
var toolExecutionWg sync.WaitGroup
var toolStateMu sync.Mutex
toolResults := make([]ToolResultContent, 0, len(pendingDispatches))
var toolExecutionErr error

// Semaphores for controlling parallelism.
parallelSem := make(chan struct{}, 5)
var sequentialMu sync.Mutex

// Single coordinator goroutine that dispatches tools.
toolExecutionWg.Go(func() {
for req := range toolChan {
if req.parallel {
parallelSem <- struct{}{}
toolExecutionWg.Go(func() {
defer func() { <-parallelSem }()
result, isCriticalError := a.executeSingleTool(ctx, toolMap, execProviderToolMap, req.toolCall, opts.OnToolResult)
toolStateMu.Lock()
toolResults = append(toolResults, result)
if isCriticalError && toolExecutionErr == nil {
if errorResult, ok := result.Result.(ToolResultOutputContentError); ok && errorResult.Error != nil {
toolExecutionErr = errorResult.Error
}
}
toolStateMu.Unlock()
})
} else {
sequentialMu.Lock()
result, isCriticalError := a.executeSingleTool(ctx, toolMap, execProviderToolMap, req.toolCall, opts.OnToolResult)
toolStateMu.Lock()
toolResults = append(toolResults, result)
if isCriticalError && toolExecutionErr == nil {
if errorResult, ok := result.Result.(ToolResultOutputContentError); ok && errorResult.Error != nil {
toolExecutionErr = errorResult.Error
}
}
toolStateMu.Unlock()
sequentialMu.Unlock()
}
}
})

// Dispatch all buffered tool calls now that every OnToolCall callback has
// been called, then close and wait.
for _, req := range pendingDispatches {
toolChan <- req
}

// Close the tool execution channel and wait for all executions to complete.
close(toolChan)
toolExecutionWg.Wait()

Expand Down Expand Up @@ -1562,7 +1608,7 @@ func (a *agent) processStepStream(ctx context.Context, stream StreamResponse, op
}

// Determine if we should continue (has tool calls and not stopped)
shouldContinue := len(stepToolCalls) > 0 && stepFinishReason == FinishReasonToolCalls
shouldContinue := len(stepToolCalls) > 0 && stepFinishReason == FinishReasonToolCalls && !hasStopTurn(toolResults)

return stepExecutionResult{
StepResult: stepResult,
Expand Down
Loading
Loading