mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
* feat(agent): add custom CLI arguments support Allow users to configure custom CLI arguments per agent that get appended to the agent subprocess command at launch time. This enables use cases like specifying different models (--model o3), max turns, or other provider-specific flags without needing separate runtimes. Changes: - Add custom_args JSONB column to agent table (migration 041) - Update API handler to accept/return custom_args in create/update - Pass custom_args through claim endpoint to daemon - Append custom_args to CLI commands for all agent backends - Add ExecOptions.CustomArgs field in agent package - Add Custom Args tab in agent detail UI - Add --custom-args flag to CLI agent create/update commands Closes MUL-802 * fix(agent): filter protocol-critical flags from custom_args Add per-backend filtering of custom_args to prevent users from accidentally overriding flags that the daemon hardcodes for its communication protocol (e.g. --output-format, --input-format, --permission-mode for Claude). This follows the same pattern as custom_env's isBlockedEnvKey: we only block the small, stable set of flags that would break the daemon↔agent protocol — not every possible dangerous flag. Workspace members are trusted for everything else. Each backend defines its own blocked set: - Claude: -p, --output-format, --input-format, --permission-mode - Gemini: -p, --yolo, -o - Codex: --listen - OpenCode: --format - OpenClaw: --local, --json, --session-id, --message - Hermes: none (ACP is positional) Includes unit tests for the filtering logic. * fix(agent): address code review nits for custom_args - Replace module-level `nextArgId` counter with `crypto.randomUUID()` in custom-args-tab.tsx to avoid SSR ID conflicts - Add unit tests for custom args passthrough and blocked-arg filtering in both Claude and Gemini arg builders
120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
// Package agent provides a unified interface for executing prompts via
|
|
// coding agents (Claude Code, Codex, OpenCode, OpenClaw, Hermes). It mirrors the happy-cli AgentBackend
|
|
// pattern, translated to idiomatic Go.
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
)
|
|
|
|
// Backend is the unified interface for executing prompts via coding agents.
|
|
type Backend interface {
|
|
// Execute runs a prompt and returns a Session for streaming results.
|
|
// The caller should read from Session.Messages (optional) and wait on
|
|
// Session.Result for the final outcome.
|
|
Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error)
|
|
}
|
|
|
|
// ExecOptions configures a single execution.
|
|
type ExecOptions struct {
|
|
Cwd string
|
|
Model string
|
|
SystemPrompt string
|
|
MaxTurns int
|
|
Timeout time.Duration
|
|
ResumeSessionID string // if non-empty, resume a previous agent session
|
|
CustomArgs []string // additional CLI arguments appended to the agent command
|
|
}
|
|
|
|
// Session represents a running agent execution.
|
|
type Session struct {
|
|
// Messages streams events as the agent works. The channel is closed
|
|
// when the agent finishes (before Result is sent).
|
|
Messages <-chan Message
|
|
// Result receives exactly one value — the final outcome — then closes.
|
|
Result <-chan Result
|
|
}
|
|
|
|
// MessageType identifies the kind of Message.
|
|
type MessageType string
|
|
|
|
const (
|
|
MessageText MessageType = "text"
|
|
MessageThinking MessageType = "thinking"
|
|
MessageToolUse MessageType = "tool-use"
|
|
MessageToolResult MessageType = "tool-result"
|
|
MessageStatus MessageType = "status"
|
|
MessageError MessageType = "error"
|
|
MessageLog MessageType = "log"
|
|
)
|
|
|
|
// Message is a unified event emitted by an agent during execution.
|
|
type Message struct {
|
|
Type MessageType
|
|
Content string // text content (Text, Error, Log)
|
|
Tool string // tool name (ToolUse, ToolResult)
|
|
CallID string // tool call ID (ToolUse, ToolResult)
|
|
Input map[string]any // tool input (ToolUse)
|
|
Output string // tool output (ToolResult)
|
|
Status string // agent status string (Status)
|
|
Level string // log level (Log)
|
|
}
|
|
|
|
// TokenUsage tracks token consumption for a single model.
|
|
type TokenUsage struct {
|
|
InputTokens int64
|
|
OutputTokens int64
|
|
CacheReadTokens int64
|
|
CacheWriteTokens int64
|
|
}
|
|
|
|
// Result is the final outcome after an agent session completes.
|
|
type Result struct {
|
|
Status string // "completed", "failed", "aborted", "timeout"
|
|
Output string // accumulated text output
|
|
Error string // error message if failed
|
|
DurationMs int64
|
|
SessionID string
|
|
Usage map[string]TokenUsage // keyed by model name
|
|
}
|
|
|
|
// Config configures a Backend instance.
|
|
type Config struct {
|
|
ExecutablePath string // path to CLI binary (claude, codex, opencode, openclaw, hermes, or gemini)
|
|
Env map[string]string // extra environment variables
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// New creates a Backend for the given agent type.
|
|
// Supported types: "claude", "codex", "opencode", "openclaw", "hermes", "gemini".
|
|
func New(agentType string, cfg Config) (Backend, error) {
|
|
if cfg.Logger == nil {
|
|
cfg.Logger = slog.Default()
|
|
}
|
|
|
|
switch agentType {
|
|
case "claude":
|
|
return &claudeBackend{cfg: cfg}, nil
|
|
case "codex":
|
|
return &codexBackend{cfg: cfg}, nil
|
|
case "opencode":
|
|
return &opencodeBackend{cfg: cfg}, nil
|
|
case "openclaw":
|
|
return &openclawBackend{cfg: cfg}, nil
|
|
case "hermes":
|
|
return &hermesBackend{cfg: cfg}, nil
|
|
case "gemini":
|
|
return &geminiBackend{cfg: cfg}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unknown agent type: %q (supported: claude, codex, opencode, openclaw, hermes, gemini)", agentType)
|
|
}
|
|
}
|
|
|
|
// DetectVersion runs the agent CLI with --version and returns the output.
|
|
func DetectVersion(ctx context.Context, executablePath string) (string, error) {
|
|
return detectCLIVersion(ctx, executablePath)
|
|
}
|