diff --git a/.github/workflows/nightly-scan.yml b/.github/workflows/nightly-scan.yml index d4740f36f..dcb88404c 100644 --- a/.github/workflows/nightly-scan.yml +++ b/.github/workflows/nightly-scan.yml @@ -23,6 +23,11 @@ jobs: scan: runs-on: ubuntu-latest timeout-minutes: 30 + # Skip if no API keys are configured + if: | + secrets.ANTHROPIC_API_KEY != '' || + secrets.OPENAI_API_KEY != '' || + secrets.GEMINI_API_KEY != '' env: HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }} diff --git a/cmd/root/otel.go b/cmd/root/otel.go index 1d29573db..4e036f5d3 100644 --- a/cmd/root/otel.go +++ b/cmd/root/otel.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "os" + "strings" "time" + "github.com/docker/cagent/pkg/version" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" @@ -22,7 +24,7 @@ func initOTelSDK(ctx context.Context) (err error) { resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceName(AppName), - semconv.ServiceVersion("dev"), // TODO: use actual version + semconv.ServiceVersion(version.Version), ), ) if err != nil { @@ -34,10 +36,11 @@ func initOTelSDK(ctx context.Context) (err error) { // Only initialize if endpoint is configured if endpoint != "" { - traceExporter, err = otlptracehttp.New(ctx, - otlptracehttp.WithEndpoint(endpoint), - otlptracehttp.WithInsecure(), // TODO: make configurable - ) + opts := []otlptracehttp.Option{otlptracehttp.WithEndpoint(endpoint)} + if strings.HasPrefix(endpoint, "http://") { + opts = append(opts, otlptracehttp.WithInsecure()) + } + traceExporter, err = otlptracehttp.New(ctx, opts...) if err != nil { return fmt.Errorf("failed to create trace exporter: %w", err) } diff --git a/examples/new_agent.yaml b/examples/new_agent.yaml new file mode 100644 index 000000000..7c65f7944 --- /dev/null +++ b/examples/new_agent.yaml @@ -0,0 +1,21 @@ +#!/usr/bin/env cagent run +## Generated by GitHub Copilot on 2026-02-05 +agents: + new_agent: + model: openai/gpt-4o + description: "Scaffolded agent for concise help, code, and file tasks." + instruction: | + You are the `new_agent`. Follow these rules: + - Be concise, helpful, and actionable. + - Prefer short examples; provide runnable snippets when applicable. + - Ask a clarifying question if the user's intent is ambiguous. + toolsets: + - type: filesystem + - type: shell + - type: memory + path: ./examples/new_agent_memory.db + - type: think + max_iterations: 10 + num_history_items: 20 + add_date: true + code_mode_tools: true diff --git a/examples/scaffolded_default_agent.yaml b/examples/scaffolded_default_agent.yaml new file mode 100644 index 000000000..b18064049 --- /dev/null +++ b/examples/scaffolded_default_agent.yaml @@ -0,0 +1,9 @@ +agents: + scaffolded_default: + model: openai/gpt-5-mini + description: "Scaffolded default agent" + instruction: | + You are a helpful assistant. Respond concisely and ask clarifying questions when necessary. + toolsets: + - type: think + - type: todo diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 18ad4574f..b04364524 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -1614,7 +1614,7 @@ func (r *LocalRuntime) executeToolWithHandler( a *agent.Agent, spanName string, execute func(ctx context.Context) (*tools.ToolCallResult, time.Duration, error), -) { +) *tools.ToolCallResult { ctx, span := r.startSpan(ctx, spanName, trace.WithAttributes( attribute.String("tool.name", toolCall.Function.Name), attribute.String("agent", a.Name()), @@ -1660,6 +1660,7 @@ func (r *LocalRuntime) executeToolWithHandler( CreatedAt: time.Now().Format(time.RFC3339), } addAgentMessage(sess, a, &toolResponseMsg, events) + return res } // runTool executes agent tools from toolsets (MCP, filesystem, etc.). @@ -1693,7 +1694,7 @@ func (r *LocalRuntime) runTool(ctx context.Context, tool tools.Tool, toolCall to } } - r.executeToolWithHandler(ctx, toolCall, tool, events, sess, a, "runtime.tool.handler", + toolResult := r.executeToolWithHandler(ctx, toolCall, tool, events, sess, a, "runtime.tool.handler", func(ctx context.Context) (*tools.ToolCallResult, time.Duration, error) { res, err := tool.Handler(ctx, toolCall) return res, 0, err @@ -1702,13 +1703,17 @@ func (r *LocalRuntime) runTool(ctx context.Context, tool tools.Tool, toolCall to // Execute post-tool hooks if configured if hooksExec != nil && hooksExec.HasPostToolUseHooks() { toolInput := parseToolInput(toolCall.Function.Arguments) + var toolResponse any + if toolResult != nil { + toolResponse = toolResult.Output + } input := &hooks.Input{ SessionID: sess.ID, Cwd: r.workingDir, ToolName: toolCall.Function.Name, ToolUseID: toolCall.ID, ToolInput: toolInput, - ToolResponse: nil, // TODO: pass actual tool response if needed + ToolResponse: toolResponse, } result, err := hooksExec.ExecutePostToolUse(ctx, input) diff --git a/pkg/session/migrations.go b/pkg/session/migrations.go index 4a364e8b3..ce0c6442e 100644 --- a/pkg/session/migrations.go +++ b/pkg/session/migrations.go @@ -96,14 +96,15 @@ func (m *MigrationManager) isMigrationApplied(ctx context.Context, name string) } // applyMigration applies a single migration -func (m *MigrationManager) applyMigration(ctx context.Context, migration *Migration) error { +func (m *MigrationManager) applyMigration(ctx context.Context, migration *Migration) (retErr error) { tx, err := m.db.BeginTx(ctx, nil) if err != nil { return err } defer func() { - // TODO: handle error - _ = tx.Rollback() + if rbErr := tx.Rollback(); rbErr != nil && !errors.Is(rbErr, sql.ErrTxDone) { + retErr = errors.Join(retErr, fmt.Errorf("failed to rollback migration transaction: %w", rbErr)) + } }() // Execute SQL migration if present diff --git a/pkg/session/session.go b/pkg/session/session.go index 731901003..c584b0ff6 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -148,7 +148,7 @@ type PermissionsConfig struct { type Message struct { // ID is the database ID of the message (used for persistence tracking) ID int64 `json:"-"` - AgentName string `json:"agentName"` // TODO: rename to agent_name + AgentName string `json:"agent_name"` Message chat.Message `json:"message"` // Implicit is an optional field to indicate if the message shouldn't be shown to the user. It's needed for special situations // like when an agent transfers a task to another agent - new session is created with a default user message, but this shouldn't be shown to the user. diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index ced3b6566..ea22ea7ce 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -15,6 +15,7 @@ import ( "github.com/docker/cagent/pkg/app" "github.com/docker/cagent/pkg/audio/transcribe" + "github.com/docker/cagent/pkg/environment" "github.com/docker/cagent/pkg/runtime" "github.com/docker/cagent/pkg/tui/animation" "github.com/docker/cagent/pkg/tui/commands" @@ -117,6 +118,11 @@ func DefaultKeyMap() KeyMap { } } +func getEnvVar(key string) string { + val, _ := environment.NewOsEnvProvider().Get(context.Background(), key) + return val +} + // New creates and initializes a new TUI application model func New(ctx context.Context, a *app.App) tea.Model { sessionState := service.NewSessionState(a.Session()) @@ -131,7 +137,7 @@ func New(ctx context.Context, a *app.App) tea.Model { completions: completion.New(), application: a, sessionState: sessionState, - transcriber: transcribe.New(os.Getenv("OPENAI_API_KEY")), // TODO(dga): should use envProvider + transcriber: transcribe.New(getEnvVar("OPENAI_API_KEY")), // Set up theme subscription using the subscription package themeSubscription: subscription.NewChannelSubscription(themeEventCh, func(themeRef string) tea.Msg { return messages.ThemeFileChangedMsg{ThemeRef: themeRef}