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
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ jobs:
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: "1.25.5"
go-version: "1.26.0"
cache: true

- name: Lint
uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0
with:
version: v2.8
version: v2.9

- name: Lint GitHub Actions
uses: raven-actions/actionlint@e01d1ea33dd6a5ed517d95b4c0c357560ac6f518 # v2.1.1
Expand All @@ -48,7 +48,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: "1.25.5"
go-version: "1.26.0"
cache: true

- name: Install Task
Expand All @@ -68,7 +68,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: "1.25.5"
go-version: "1.26.0"
cache: true

- name: Install go-licences
Expand All @@ -87,7 +87,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: "1.25.5"
go-version: "1.26.0"
cache: true

- name: Install Task
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ task build # Should create ./bin/cagent
- Manual workflow dispatch

**Build Configuration:**
- Go version: 1.25.5
- Go version: 1.26.0
- Platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64
- Binary name: `cagent` (or `cagent.exe` on Windows)
- Version injection: Uses git tag and commit SHA via ldflags
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1

ARG GO_VERSION="1.25.5"
ARG GO_VERSION="1.26.0"
ARG ALPINE_VERSION="3.22"
ARG XX_VERSION="1.9.0"

Expand Down
12 changes: 4 additions & 8 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,18 @@ We collect anonymous usage data to help improve cagent. To disable:
rootCmd.SetErr(stderr)

if err := rootCmd.ExecuteContext(ctx); err != nil {
envErr := &environment.RequiredEnvError{}
runtimeErr := RuntimeError{}

switch {
case ctx.Err() != nil:
if ctx.Err() != nil {
return ctx.Err()
case errors.As(err, &envErr):
} else if envErr, ok := errors.AsType[*environment.RequiredEnvError](err); ok {
fmt.Fprintln(stderr, "The following environment variables must be set:")
for _, v := range envErr.Missing {
fmt.Fprintf(stderr, " - %s\n", v)
}
fmt.Fprintln(stderr, "\nEither:\n - Set those environment variables before running cagent\n - Run cagent with --env-from-file\n - Store those secrets using one of the built-in environment variable providers.")
case errors.As(err, &runtimeErr):
} else if _, ok := errors.AsType[RuntimeError](err); ok {
// Runtime errors have already been printed by the command itself
// Don't print them again or show usage
default:
} else {
// Command line usage errors - show the error and usage
fmt.Fprintln(stderr, err)
fmt.Fprintln(stderr)
Expand Down
3 changes: 1 addition & 2 deletions cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,7 @@ func (f *runExecFlags) handleExecMode(ctx context.Context, out *cli.Printer, rt
OutputJSON: f.outputJSON,
AutoApprove: f.autoApprove,
}, rt, sess, userMessages)
var cliErr cli.RuntimeError
if errors.As(err, &cliErr) {
if cliErr, ok := errors.AsType[cli.RuntimeError](err); ok {
return RuntimeError{Err: cliErr.Err}
}
return err
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/docker/cagent

go 1.25.5
go 1.26.0

replace github.com/charmbracelet/ultraviolet => github.com/dgageot/ultraviolet v0.0.0-20260106070720-e493364e381d

Expand Down
8 changes: 4 additions & 4 deletions pkg/acp/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ func (a *Agent) handleMaxIterationsReached(ctx context.Context, acpSess *Session
SessionId: acp.SessionId(acpSess.id),
ToolCall: acp.RequestPermissionToolCall{
ToolCallId: "max_iterations",
Title: acp.Ptr(fmt.Sprintf("Maximum iterations (%d) reached", e.MaxIterations)),
Title: new(fmt.Sprintf("Maximum iterations (%d) reached", e.MaxIterations)),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid syntax: new() called with fmt.Sprintf expression instead of a type

The expression new(fmt.Sprintf("Maximum iterations (%d) reached", e.MaxIterations)) is invalid Go syntax. The new() builtin accepts only a type expression (e.g., new(string)), not a value-producing function call. fmt.Sprintf() returns a string value, not a type. This will fail to compile.

The correct approach would be:

title := fmt.Sprintf("Maximum iterations (%d) reached", e.MaxIterations)
Title: &title,

Or use the acp.Ptr() helper function if available (as seen elsewhere in the codebase):

Title: acp.Ptr(fmt.Sprintf("Maximum iterations (%d) reached", e.MaxIterations)),

Looking at line 753 in the same file, you correctly use &title for string pointers.

Kind: acp.Ptr(acp.ToolKindExecute),
Status: acp.Ptr(acp.ToolCallStatusPending),
},
Expand Down Expand Up @@ -750,9 +750,9 @@ func buildToolCallUpdate(toolCall tools.ToolCall, tool tools.Tool, status acp.To

return acp.RequestPermissionToolCall{
ToolCallId: acp.ToolCallId(toolCall.ID),
Title: acp.Ptr(title),
Kind: acp.Ptr(kind),
Status: acp.Ptr(status),
Title: &title,
Kind: &kind,
Status: &status,
RawInput: parseToolCallArguments(toolCall.Function.Arguments),
}
}
Expand Down
7 changes: 3 additions & 4 deletions pkg/chat/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"unicode/utf8"

Expand Down Expand Up @@ -336,10 +337,8 @@ func isTextByContent(filePath string) bool {
data := buf[:n]

// Check for null bytes (strong binary indicator)
for _, b := range data {
if b == 0 {
return false
}
if slices.Contains(data, 0) {
return false
}

// Check if the content is valid UTF-8
Expand Down
6 changes: 1 addition & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func validateConfig(cfg *latest.Config) error {
for name := range cfg.Models {
if cfg.Models[name].ParallelToolCalls == nil {
m := cfg.Models[name]
m.ParallelToolCalls = boolPtr(true)
m.ParallelToolCalls = new(true)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid syntax: new() called with a value instead of a type

The expression new(true) is invalid Go syntax. The new() builtin function requires a type as its argument, but true is a boolean literal/value, not a type. This code will fail to compile with an error like "cannot use true (untyped bool constant) as type in argument to new".

The field ParallelToolCalls is of type *bool, so the correct approach would be:

t := true
m.ParallelToolCalls = &t

Or use a helper function that returns *bool. Even though Go 1.26 enhanced the new() builtin to accept non-constant expressions, those expressions must still be type expressions, not value literals.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahahahah

cfg.Models[name] = m
}
}
Expand Down Expand Up @@ -134,10 +134,6 @@ func validateConfig(cfg *latest.Config) error {
return nil
}

func boolPtr(b bool) *bool {
return &b
}

// providerAPITypes are the allowed values for api_type in provider configs
var providerAPITypes = map[string]bool{
"": true, // empty is allowed (defaults to openai_chatcompletions)
Expand Down
59 changes: 28 additions & 31 deletions pkg/config/latest/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package latest

import (
"encoding/json"
"maps"
"os"
"reflect"
"sort"
Expand Down Expand Up @@ -46,18 +47,14 @@ func (s jsonSchema) resolveRef(root jsonSchema) jsonSchema {
// excluded. It recurses into anonymous (embedded) struct fields so that
// promoted fields are included.
func structJSONFields(t reflect.Type) map[string]bool {
if t.Kind() == reflect.Ptr {
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
fields := make(map[string]bool)
for i := range t.NumField() {
f := t.Field(i)

for f := range t.Fields() {
// Recurse into anonymous (embedded) structs.
if f.Anonymous {
for k, v := range structJSONFields(f.Type) {
fields[k] = v
}
maps.Copy(fields, structJSONFields(f.Type))
continue
}

Expand Down Expand Up @@ -119,27 +116,27 @@ func TestSchemaMatchesGoTypes(t *testing.T) {

entries := []entry{
// Top-level Config
{reflect.TypeOf(Config{}), root, "Config (top-level)"},
{reflect.TypeFor[Config](), root, "Config (top-level)"},
}

// Definitions that map 1:1 to a Go struct.
definitionMap := map[string]reflect.Type{
"AgentConfig": reflect.TypeOf(AgentConfig{}),
"FallbackConfig": reflect.TypeOf(FallbackConfig{}),
"ModelConfig": reflect.TypeOf(ModelConfig{}),
"Metadata": reflect.TypeOf(Metadata{}),
"ProviderConfig": reflect.TypeOf(ProviderConfig{}),
"Toolset": reflect.TypeOf(Toolset{}),
"Remote": reflect.TypeOf(Remote{}),
"SandboxConfig": reflect.TypeOf(SandboxConfig{}),
"ScriptShellToolConfig": reflect.TypeOf(ScriptShellToolConfig{}),
"PostEditConfig": reflect.TypeOf(PostEditConfig{}),
"PermissionsConfig": reflect.TypeOf(PermissionsConfig{}),
"HooksConfig": reflect.TypeOf(HooksConfig{}),
"HookMatcherConfig": reflect.TypeOf(HookMatcherConfig{}),
"HookDefinition": reflect.TypeOf(HookDefinition{}),
"RoutingRule": reflect.TypeOf(RoutingRule{}),
"ApiConfig": reflect.TypeOf(APIToolConfig{}),
"AgentConfig": reflect.TypeFor[AgentConfig](),
"FallbackConfig": reflect.TypeFor[FallbackConfig](),
"ModelConfig": reflect.TypeFor[ModelConfig](),
"Metadata": reflect.TypeFor[Metadata](),
"ProviderConfig": reflect.TypeFor[ProviderConfig](),
"Toolset": reflect.TypeFor[Toolset](),
"Remote": reflect.TypeFor[Remote](),
"SandboxConfig": reflect.TypeFor[SandboxConfig](),
"ScriptShellToolConfig": reflect.TypeFor[ScriptShellToolConfig](),
"PostEditConfig": reflect.TypeFor[PostEditConfig](),
"PermissionsConfig": reflect.TypeFor[PermissionsConfig](),
"HooksConfig": reflect.TypeFor[HooksConfig](),
"HookMatcherConfig": reflect.TypeFor[HookMatcherConfig](),
"HookDefinition": reflect.TypeFor[HookDefinition](),
"RoutingRule": reflect.TypeFor[RoutingRule](),
"ApiConfig": reflect.TypeFor[APIToolConfig](),
}

for name, goType := range definitionMap {
Expand All @@ -159,13 +156,13 @@ func TestSchemaMatchesGoTypes(t *testing.T) {
}

inlines := []inlineEntry{
{reflect.TypeOf(StructuredOutput{}), []string{"AgentConfig", "structured_output"}, "StructuredOutput (AgentConfig.structured_output)"},
{reflect.TypeOf(RAGConfig{}), []string{"RAGConfig"}, "RAGConfig"},
{reflect.TypeOf(RAGToolConfig{}), []string{"RAGConfig", "tool"}, "RAGToolConfig (RAGConfig.tool)"},
{reflect.TypeOf(RAGResultsConfig{}), []string{"RAGConfig", "results"}, "RAGResultsConfig (RAGConfig.results)"},
{reflect.TypeOf(RAGFusionConfig{}), []string{"RAGConfig", "results", "fusion"}, "RAGFusionConfig (RAGConfig.results.fusion)"},
{reflect.TypeOf(RAGRerankingConfig{}), []string{"RAGConfig", "results", "reranking"}, "RAGRerankingConfig (RAGConfig.results.reranking)"},
{reflect.TypeOf(RAGChunkingConfig{}), []string{"RAGConfig", "strategies", "*", "chunking"}, "RAGChunkingConfig (RAGConfig.strategies[].chunking)"},
{reflect.TypeFor[StructuredOutput](), []string{"AgentConfig", "structured_output"}, "StructuredOutput (AgentConfig.structured_output)"},
{reflect.TypeFor[RAGConfig](), []string{"RAGConfig"}, "RAGConfig"},
{reflect.TypeFor[RAGToolConfig](), []string{"RAGConfig", "tool"}, "RAGToolConfig (RAGConfig.tool)"},
{reflect.TypeFor[RAGResultsConfig](), []string{"RAGConfig", "results"}, "RAGResultsConfig (RAGConfig.results)"},
{reflect.TypeFor[RAGFusionConfig](), []string{"RAGConfig", "results", "fusion"}, "RAGFusionConfig (RAGConfig.results.fusion)"},
{reflect.TypeFor[RAGRerankingConfig](), []string{"RAGConfig", "results", "reranking"}, "RAGRerankingConfig (RAGConfig.results.reranking)"},
{reflect.TypeFor[RAGChunkingConfig](), []string{"RAGConfig", "strategies", "*", "chunking"}, "RAGChunkingConfig (RAGConfig.strategies[].chunking)"},
}

for _, il := range inlines {
Expand Down
33 changes: 16 additions & 17 deletions pkg/config/latest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package latest
import (
"encoding/json"
"fmt"
"maps"
"strings"
"time"

Expand All @@ -20,7 +21,7 @@ type Config struct {
Providers map[string]ProviderConfig `json:"providers,omitempty"`
Models map[string]ModelConfig `json:"models,omitempty"`
RAG map[string]RAGConfig `json:"rag,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Metadata Metadata `json:"metadata"`
Permissions *PermissionsConfig `json:"permissions,omitempty"`
}

Expand Down Expand Up @@ -125,7 +126,7 @@ type FallbackConfig struct {
// Cooldown is the duration to stick with a successful fallback model before
// retrying the primary. Only applies after a non-retryable error (e.g., 429).
// Default is 1 minute. Use Go duration format (e.g., "1m", "30s", "2m30s").
Cooldown Duration `json:"cooldown,omitempty"`
Cooldown Duration `json:"cooldown"`
}

// Duration is a wrapper around time.Duration that supports YAML/JSON unmarshaling
Expand Down Expand Up @@ -410,13 +411,13 @@ type Toolset struct {
Instruction string `json:"instruction,omitempty"`
Toon string `json:"toon,omitempty"`

Defer DeferConfig `json:"defer,omitempty" yaml:"defer,omitempty"`
Defer DeferConfig `json:"defer" yaml:"defer,omitempty"`

// For the `mcp` tool
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
Ref string `json:"ref,omitempty"`
Remote Remote `json:"remote,omitempty"`
Remote Remote `json:"remote"`
Config any `json:"config,omitempty"`

// For the `a2a` tool
Expand All @@ -441,7 +442,7 @@ type Toolset struct {
// For the `filesystem` tool - post-edit commands
PostEdit []PostEditConfig `json:"post_edit,omitempty"`

APIConfig APIToolConfig `json:"api_config,omitempty"`
APIConfig APIToolConfig `json:"api_config"`

// For the `filesystem` tool - VCS integration
IgnoreVCS *bool `json:"ignore_vcs,omitempty"`
Expand Down Expand Up @@ -569,11 +570,11 @@ func (t ThinkingBudget) MarshalYAML() ([]byte, error) {
func (t ThinkingBudget) MarshalJSON() ([]byte, error) {
// If Effort string is set (non-empty), marshal as string
if t.Effort != "" {
return []byte(fmt.Sprintf("%q", t.Effort)), nil
return fmt.Appendf(nil, "%q", t.Effort), nil
}

// Otherwise marshal as integer (includes 0, -1, and positive values)
return []byte(fmt.Sprintf("%d", t.Tokens)), nil
return fmt.Appendf(nil, "%d", t.Tokens), nil
}

// UnmarshalJSON implements custom unmarshaling to accept simple string or int format
Expand Down Expand Up @@ -618,11 +619,11 @@ type RAGToolConfig struct {
// RAGConfig represents a RAG (Retrieval-Augmented Generation) configuration
// Uses a unified strategies array for flexible, extensible configuration
type RAGConfig struct {
Tool RAGToolConfig `json:"tool,omitempty"` // Tool configuration
Tool RAGToolConfig `json:"tool"` // Tool configuration
Docs []string `json:"docs,omitempty"` // Shared documents across all strategies
RespectVCS *bool `json:"respect_vcs,omitempty"` // Whether to respect VCS ignore files like .gitignore (default: true)
Strategies []RAGStrategyConfig `json:"strategies,omitempty"` // Array of strategy configurations
Results RAGResultsConfig `json:"results,omitempty"`
Results RAGResultsConfig `json:"results"`
}

// GetRespectVCS returns whether VCS ignore files should be respected, defaulting to true
Expand All @@ -636,11 +637,11 @@ func (c *RAGConfig) GetRespectVCS() bool {
// RAGStrategyConfig represents a single retrieval strategy configuration
// Strategy-specific fields are stored in Params (validated by strategy implementation)
type RAGStrategyConfig struct { //nolint:recvcheck // Marshal methods must use value receiver for YAML/JSON slice encoding, Unmarshal must use pointer
Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc.
Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs)
Database RAGDatabaseConfig `json:"database,omitempty"` // Database configuration
Chunking RAGChunkingConfig `json:"chunking,omitempty"` // Chunking configuration
Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input)
Type string `json:"type"` // Strategy type: "chunked-embeddings", "bm25", etc.
Docs []string `json:"docs,omitempty"` // Strategy-specific documents (augments shared docs)
Database RAGDatabaseConfig `json:"database"` // Database configuration
Chunking RAGChunkingConfig `json:"chunking"` // Chunking configuration
Limit int `json:"limit,omitempty"` // Max results from this strategy (for fusion input)

// Strategy-specific parameters (arbitrary key-value pairs)
// Examples:
Expand Down Expand Up @@ -793,9 +794,7 @@ func (s RAGStrategyConfig) buildFlattenedMap() map[string]any {
}

// Flatten Params into the same level
for k, v := range s.Params {
result[k] = v
}
maps.Copy(result, s.Params)

return result
}
Expand Down
8 changes: 2 additions & 6 deletions pkg/config/sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/url"
"os"
"path/filepath"
"slices"
"strings"

"github.com/docker/cagent/pkg/content"
Expand Down Expand Up @@ -295,12 +296,7 @@ func isGitHubURL(urlStr string) bool {
if err != nil {
return false
}
for _, host := range githubHosts {
if u.Host == host {
return true
}
}
return false
return slices.Contains(githubHosts, u.Host)
}

// addGitHubAuth adds GitHub token authorization to the request if:
Expand Down
Loading