Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .github/workflows/nightly-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 != '' }}

Expand Down
13 changes: 8 additions & 5 deletions cmd/root/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
21 changes: 21 additions & 0 deletions examples/new_agent.yaml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions examples/scaffolded_default_agent.yaml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 8 additions & 3 deletions pkg/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -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.).
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions pkg/session/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion pkg/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
Expand All @@ -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}
Expand Down