From 67dc1304241fd8c68f80ebada898e2f7aa76ebf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 10:58:46 +0000 Subject: [PATCH] feat(agentci): add model/runner fields for multi-backend support Tickets now carry model (sonnet/haiku/opus) and runner (claude/codex) fields. agent-runner.sh dispatches to the right backend. Defaults to claude with sonnet model for cost efficiency. Co-Authored-By: Claude Opus 4.6 --- pkg/agentci/config.go | 29 ++++++++++++++++++++++++----- pkg/jobrunner/handlers/dispatch.go | 6 ++++++ scripts/agent-runner.sh | 29 ++++++++++++++++++++++------- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/pkg/agentci/config.go b/pkg/agentci/config.go index bf3d13b3..27c654fe 100644 --- a/pkg/agentci/config.go +++ b/pkg/agentci/config.go @@ -10,10 +10,12 @@ import ( // AgentConfig represents a single agent machine in the config file. type AgentConfig struct { - Host string `yaml:"host" mapstructure:"host"` - QueueDir string `yaml:"queue_dir" mapstructure:"queue_dir"` + Host string `yaml:"host" mapstructure:"host"` + QueueDir string `yaml:"queue_dir" mapstructure:"queue_dir"` ForgejoUser string `yaml:"forgejo_user" mapstructure:"forgejo_user"` - Active bool `yaml:"active" mapstructure:"active"` + Model string `yaml:"model" mapstructure:"model"` // claude model: sonnet, haiku, opus (default: sonnet) + Runner string `yaml:"runner" mapstructure:"runner"` // runner binary: claude, codex (default: claude) + Active bool `yaml:"active" mapstructure:"active"` } // LoadAgents reads agent targets from config and returns a map suitable for the dispatch handler. @@ -37,9 +39,19 @@ func LoadAgents(cfg *config.Config) (map[string]handlers.AgentTarget, error) { if queueDir == "" { queueDir = "/home/claude/ai-work/queue" } + model := ac.Model + if model == "" { + model = "sonnet" + } + runner := ac.Runner + if runner == "" { + runner = "claude" + } targets[name] = handlers.AgentTarget{ Host: ac.Host, QueueDir: queueDir, + Model: model, + Runner: runner, } } @@ -49,12 +61,19 @@ func LoadAgents(cfg *config.Config) (map[string]handlers.AgentTarget, error) { // SaveAgent writes an agent config entry to the config file. func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error { key := fmt.Sprintf("agentci.agents.%s", name) - return cfg.Set(key, map[string]any{ + data := map[string]any{ "host": ac.Host, "queue_dir": ac.QueueDir, "forgejo_user": ac.ForgejoUser, "active": ac.Active, - }) + } + if ac.Model != "" { + data["model"] = ac.Model + } + if ac.Runner != "" { + data["runner"] = ac.Runner + } + return cfg.Set(key, data) } // RemoveAgent removes an agent from the config file. diff --git a/pkg/jobrunner/handlers/dispatch.go b/pkg/jobrunner/handlers/dispatch.go index 1fb99d8f..116e01cd 100644 --- a/pkg/jobrunner/handlers/dispatch.go +++ b/pkg/jobrunner/handlers/dispatch.go @@ -18,6 +18,8 @@ import ( type AgentTarget struct { Host string // SSH destination (e.g., "claude@192.168.0.201") QueueDir string // Remote queue directory (e.g., "~/ai-work/queue") + Model string // AI model: sonnet, haiku, opus (default: sonnet) + Runner string // Runner binary: claude, codex (default: claude) } // DispatchTicket is the JSON payload written to the agent's queue. @@ -33,6 +35,8 @@ type DispatchTicket struct { ForgeURL string `json:"forge_url"` ForgeToken string `json:"forge_token"` ForgeUser string `json:"forgejo_user"` + Model string `json:"model,omitempty"` + Runner string `json:"runner,omitempty"` CreatedAt string `json:"created_at"` } @@ -93,6 +97,8 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin ForgeURL: h.forgeURL, ForgeToken: h.token, ForgeUser: signal.Assignee, + Model: agent.Model, + Runner: agent.Runner, CreatedAt: time.Now().UTC().Format(time.RFC3339), } diff --git a/scripts/agent-runner.sh b/scripts/agent-runner.sh index 46a6ca4d..caf45a70 100755 --- a/scripts/agent-runner.sh +++ b/scripts/agent-runner.sh @@ -101,15 +101,30 @@ The repo is cloned at the current directory on branch '${TARGET_BRANCH}'. Create a feature branch from '${TARGET_BRANCH}', make minimal targeted changes, commit referencing #${ISSUE_NUM}, and push. Then create a PR targeting '${TARGET_BRANCH}' using the forgejo MCP tools or git push." -# --- 9. Run Claude --- +# --- 9. Run AI agent --- +MODEL=$(jq -r '.model // "sonnet"' "$TICKET_FILE") +RUNNER=$(jq -r '.runner // "claude"' "$TICKET_FILE") LOG_FILE="$LOG_DIR/${REPO_OWNER}-${REPO_NAME}-${ISSUE_NUM}.log" -echo "$(date -Iseconds) Running claude..." -echo "$PROMPT" | claude -p \ - --dangerously-skip-permissions \ - --output-format text \ - > "$LOG_FILE" 2>&1 + +echo "$(date -Iseconds) Running ${RUNNER} (model: ${MODEL})..." + +case "$RUNNER" in + codex) + codex --approval-mode full-auto \ + --quiet \ + "$PROMPT" \ + > "$LOG_FILE" 2>&1 + ;; + *) + echo "$PROMPT" | claude -p \ + --model "$MODEL" \ + --dangerously-skip-permissions \ + --output-format text \ + > "$LOG_FILE" 2>&1 + ;; +esac EXIT_CODE=$? -echo "$(date -Iseconds) Claude exited with code: $EXIT_CODE" +echo "$(date -Iseconds) ${RUNNER} exited with code: $EXIT_CODE" # --- 10. Move to done --- mv "$TICKET_FILE" "$DONE_DIR/"