From 65ba4148511a7a2ce5354660ef96048d95da5c8d Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Tue, 24 Mar 2026 11:54:21 -0400 Subject: [PATCH] refactor(agent): share clone helpers across adapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract shared clone-state helpers for command/model/reasoning/agentic/session fields across the command-backed agents and add coverage for session-bearing clone chains. Validation: - go fmt ./... - go vet ./... - go test ./... - roborev fix --open --list 🤖 Generated with [OpenAI Codex](https://openai.com/codex) Co-authored-by: OpenAI Codex --- internal/agent/agent_test.go | 99 ++++++++++++++++++++++++++++++++++ internal/agent/claude.go | 50 ++++++++--------- internal/agent/clone_config.go | 55 +++++++++++++++++++ internal/agent/codex.go | 44 +++++++-------- internal/agent/copilot.go | 38 ++++++------- internal/agent/cursor.go | 38 ++++++------- internal/agent/droid.go | 24 ++++++--- internal/agent/gemini.go | 38 ++++++------- internal/agent/kilo.go | 48 ++++++++--------- internal/agent/kiro.go | 20 ++++++- internal/agent/opencode.go | 50 ++++++++--------- internal/agent/pi.go | 66 +++++++++-------------- 12 files changed, 363 insertions(+), 207 deletions(-) create mode 100644 internal/agent/clone_config.go diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index db596d556..09fac654f 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -320,6 +320,105 @@ func TestCodexBuildArgsModelWithReasoning(t *testing.T) { assert.Contains(t, cmdLine, `-c model_reasoning_effort="high"`) } +func TestSessionAgentsPreserveStateAcrossCloneMethods(t *testing.T) { + tests := []struct { + name string + agent Agent + verify func(*testing.T, Agent) + }{ + { + name: "codex", + agent: NewCodexAgent("codex"). + WithSessionID("session-123"). + WithModel("o4-mini"). + WithReasoning(ReasoningThorough). + WithAgentic(true), + verify: func(t *testing.T, a Agent) { + codex, ok := a.(*CodexAgent) + require.True(t, ok) + assert.Equal(t, "session-123", codex.SessionID) + assert.Equal(t, "o4-mini", codex.Model) + assert.Equal(t, ReasoningThorough, codex.Reasoning) + assert.True(t, codex.Agentic) + }, + }, + { + name: "claude", + agent: NewClaudeAgent("claude"). + WithSessionID("session-123"). + WithModel("opus"). + WithReasoning(ReasoningThorough). + WithAgentic(true), + verify: func(t *testing.T, a Agent) { + claude, ok := a.(*ClaudeAgent) + require.True(t, ok) + assert.Equal(t, "session-123", claude.SessionID) + assert.Equal(t, "opus", claude.Model) + assert.Equal(t, ReasoningThorough, claude.Reasoning) + assert.True(t, claude.Agentic) + }, + }, + { + name: "opencode", + agent: NewOpenCodeAgent("opencode"). + WithSessionID("session-123"). + WithModel("anthropic/claude-sonnet-4"). + WithReasoning(ReasoningThorough). + WithAgentic(true), + verify: func(t *testing.T, a Agent) { + opencode, ok := a.(*OpenCodeAgent) + require.True(t, ok) + assert.Equal(t, "session-123", opencode.SessionID) + assert.Equal(t, "anthropic/claude-sonnet-4", opencode.Model) + assert.Equal(t, ReasoningThorough, opencode.Reasoning) + assert.True(t, opencode.Agentic) + }, + }, + { + name: "kilo", + agent: NewKiloAgent("kilo"). + WithSessionID("session-123"). + WithModel("anthropic/claude-sonnet-4"). + WithReasoning(ReasoningThorough). + WithAgentic(true), + verify: func(t *testing.T, a Agent) { + kilo, ok := a.(*KiloAgent) + require.True(t, ok) + assert.Equal(t, "session-123", kilo.SessionID) + assert.Equal(t, "anthropic/claude-sonnet-4", kilo.Model) + assert.Equal(t, ReasoningThorough, kilo.Reasoning) + assert.True(t, kilo.Agentic) + }, + }, + { + name: "pi", + agent: func() Agent { + pi := NewPiAgent("pi").WithProvider("anthropic").(*PiAgent) + return pi. + WithSessionID("session-123"). + WithModel("claude-sonnet-4"). + WithReasoning(ReasoningThorough). + WithAgentic(true) + }(), + verify: func(t *testing.T, a Agent) { + pi, ok := a.(*PiAgent) + require.True(t, ok) + assert.Equal(t, "anthropic", pi.Provider) + assert.Equal(t, "session-123", pi.SessionID) + assert.Equal(t, "claude-sonnet-4", pi.Model) + assert.Equal(t, ReasoningThorough, pi.Reasoning) + assert.True(t, pi.Agentic) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.verify(t, tt.agent) + }) + } +} + func assertArgsContain(t *testing.T, cmdLine, flag, value string) { t.Helper() tokens := strings.Fields(cmdLine) diff --git a/internal/agent/claude.go b/internal/agent/claude.go index 9b9343aa5..a8186008e 100644 --- a/internal/agent/claude.go +++ b/internal/agent/claude.go @@ -34,26 +34,32 @@ func NewClaudeAgent(command string) *ClaudeAgent { return &ClaudeAgent{Command: command, Reasoning: ReasoningStandard} } -// WithReasoning returns a copy of the agent with the specified reasoning level. -func (a *ClaudeAgent) WithReasoning(level ReasoningLevel) Agent { +func (a *ClaudeAgent) clone(opts ...agentCloneOption) *ClaudeAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + a.SessionID, + opts..., + ) return &ClaudeAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: level, - Agentic: a.Agentic, - SessionID: a.SessionID, + Command: cfg.Command, + Model: cfg.Model, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + SessionID: cfg.SessionID, } } +// WithReasoning returns a copy of the agent with the specified reasoning level. +func (a *ClaudeAgent) WithReasoning(level ReasoningLevel) Agent { + return a.clone(withClonedReasoning(level)) +} + // WithAgentic returns a copy of the agent configured for agentic mode. func (a *ClaudeAgent) WithAgentic(agentic bool) Agent { - return &ClaudeAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedAgentic(agentic)) } // WithModel returns a copy of the agent configured to use the specified model. @@ -61,24 +67,12 @@ func (a *ClaudeAgent) WithModel(model string) Agent { if model == "" { return a } - return &ClaudeAgent{ - Command: a.Command, - Model: model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedModel(model)) } // WithSessionID returns a copy of the agent configured to resume a prior session. func (a *ClaudeAgent) WithSessionID(sessionID string) Agent { - return &ClaudeAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: sanitizedResumeSessionID(sessionID), - } + return a.clone(withClonedSessionID(sessionID)) } // claudeEffort maps ReasoningLevel to Claude Code's --effort flag values diff --git a/internal/agent/clone_config.go b/internal/agent/clone_config.go new file mode 100644 index 000000000..5928c9b7f --- /dev/null +++ b/internal/agent/clone_config.go @@ -0,0 +1,55 @@ +package agent + +type agentCloneConfig struct { + Command string + Model string + Reasoning ReasoningLevel + Agentic bool + SessionID string +} + +type agentCloneOption func(*agentCloneConfig) + +func newAgentCloneConfig( + command, model string, + reasoning ReasoningLevel, + agentic bool, + sessionID string, + opts ...agentCloneOption, +) agentCloneConfig { + cfg := agentCloneConfig{ + Command: command, + Model: model, + Reasoning: reasoning, + Agentic: agentic, + SessionID: sessionID, + } + for _, opt := range opts { + opt(&cfg) + } + return cfg +} + +func withClonedReasoning(level ReasoningLevel) agentCloneOption { + return func(cfg *agentCloneConfig) { + cfg.Reasoning = level + } +} + +func withClonedAgentic(agentic bool) agentCloneOption { + return func(cfg *agentCloneConfig) { + cfg.Agentic = agentic + } +} + +func withClonedModel(model string) agentCloneOption { + return func(cfg *agentCloneConfig) { + cfg.Model = model + } +} + +func withClonedSessionID(sessionID string) agentCloneOption { + return func(cfg *agentCloneConfig) { + cfg.SessionID = sanitizedResumeSessionID(sessionID) + } +} diff --git a/internal/agent/codex.go b/internal/agent/codex.go index a429af3b8..c29c402e1 100644 --- a/internal/agent/codex.go +++ b/internal/agent/codex.go @@ -40,20 +40,32 @@ func NewCodexAgent(command string) *CodexAgent { return &CodexAgent{Command: command, Reasoning: ReasoningStandard} } +func (a *CodexAgent) clone(opts ...agentCloneOption) *CodexAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + a.SessionID, + opts..., + ) + return &CodexAgent{ + Command: cfg.Command, + Model: cfg.Model, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + SessionID: cfg.SessionID, + } +} + // WithReasoning returns a copy of the agent with the specified reasoning level func (a *CodexAgent) WithReasoning(level ReasoningLevel) Agent { - return &CodexAgent{Command: a.Command, Model: a.Model, Reasoning: level, Agentic: a.Agentic, SessionID: a.SessionID} + return a.clone(withClonedReasoning(level)) } // WithAgentic returns a copy of the agent configured for agentic mode. func (a *CodexAgent) WithAgentic(agentic bool) Agent { - return &CodexAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedAgentic(agentic)) } // WithModel returns a copy of the agent configured to use the specified model. @@ -61,24 +73,12 @@ func (a *CodexAgent) WithModel(model string) Agent { if model == "" { return a } - return &CodexAgent{ - Command: a.Command, - Model: model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedModel(model)) } // WithSessionID returns a copy of the agent configured to resume a prior session. func (a *CodexAgent) WithSessionID(sessionID string) Agent { - return &CodexAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: sanitizedResumeSessionID(sessionID), - } + return a.clone(withClonedSessionID(sessionID)) } // codexReasoningEffort maps ReasoningLevel to codex-specific effort values diff --git a/internal/agent/copilot.go b/internal/agent/copilot.go index ce48bdff8..9cb2c6904 100644 --- a/internal/agent/copilot.go +++ b/internal/agent/copilot.go @@ -78,26 +78,33 @@ func NewCopilotAgent(command string) *CopilotAgent { return &CopilotAgent{Command: command, Reasoning: ReasoningStandard} } -// WithReasoning returns a copy of the agent with the model preserved (reasoning not yet supported). -func (a *CopilotAgent) WithReasoning(level ReasoningLevel) Agent { +func (a *CopilotAgent) clone(opts ...agentCloneOption) *CopilotAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + "", + opts..., + ) return &CopilotAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: level, - Agentic: a.Agentic, + Command: cfg.Command, + Model: cfg.Model, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, } } +// WithReasoning returns a copy of the agent with the model preserved (reasoning not yet supported). +func (a *CopilotAgent) WithReasoning(level ReasoningLevel) Agent { + return a.clone(withClonedReasoning(level)) +} + // WithAgentic returns a copy of the agent configured for agentic mode. // In agentic mode, all tools are allowed without restriction. In review mode // (default), destructive tools are denied via --deny-tool flags. func (a *CopilotAgent) WithAgentic(agentic bool) Agent { - return &CopilotAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: agentic, - } + return a.clone(withClonedAgentic(agentic)) } // WithModel returns a copy of the agent configured to use the specified model. @@ -105,12 +112,7 @@ func (a *CopilotAgent) WithModel(model string) Agent { if model == "" { return a } - return &CopilotAgent{ - Command: a.Command, - Model: model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - } + return a.clone(withClonedModel(model)) } func (a *CopilotAgent) Name() string { diff --git a/internal/agent/cursor.go b/internal/agent/cursor.go index a0800008b..63719050a 100644 --- a/internal/agent/cursor.go +++ b/internal/agent/cursor.go @@ -23,36 +23,38 @@ func NewCursorAgent(command string) *CursorAgent { return &CursorAgent{Command: command, Reasoning: ReasoningStandard} } +func (a *CursorAgent) clone(opts ...agentCloneOption) *CursorAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + "", + opts..., + ) + return &CursorAgent{ + Command: cfg.Command, + Model: cfg.Model, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + } +} + // WithReasoning returns a copy with the reasoning level stored. // The agent CLI has no reasoning flag; callers can map reasoning to model selection instead. func (a *CursorAgent) WithReasoning(level ReasoningLevel) Agent { - return &CursorAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: level, - Agentic: a.Agentic, - } + return a.clone(withClonedReasoning(level)) } func (a *CursorAgent) WithAgentic(agentic bool) Agent { - return &CursorAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: agentic, - } + return a.clone(withClonedAgentic(agentic)) } func (a *CursorAgent) WithModel(model string) Agent { if model == "" { return a } - return &CursorAgent{ - Command: a.Command, - Model: model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - } + return a.clone(withClonedModel(model)) } func (a *CursorAgent) Name() string { diff --git a/internal/agent/droid.go b/internal/agent/droid.go index aeb75ff0e..615b81ad0 100644 --- a/internal/agent/droid.go +++ b/internal/agent/droid.go @@ -24,18 +24,30 @@ func NewDroidAgent(command string) *DroidAgent { return &DroidAgent{Command: command, Reasoning: ReasoningStandard} } +func (a *DroidAgent) clone(opts ...agentCloneOption) *DroidAgent { + cfg := newAgentCloneConfig( + a.Command, + "", + a.Reasoning, + a.Agentic, + "", + opts..., + ) + return &DroidAgent{ + Command: cfg.Command, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + } +} + // WithReasoning returns a copy of the agent with the specified reasoning level func (a *DroidAgent) WithReasoning(level ReasoningLevel) Agent { - return &DroidAgent{Command: a.Command, Reasoning: level, Agentic: a.Agentic} + return a.clone(withClonedReasoning(level)) } // WithAgentic returns a copy of the agent configured for agentic mode. func (a *DroidAgent) WithAgentic(agentic bool) Agent { - return &DroidAgent{ - Command: a.Command, - Reasoning: a.Reasoning, - Agentic: agentic, - } + return a.clone(withClonedAgentic(agentic)) } // WithModel returns the agent unchanged (model selection not supported for droid). diff --git a/internal/agent/gemini.go b/internal/agent/gemini.go index 976513579..8a03f91e6 100644 --- a/internal/agent/gemini.go +++ b/internal/agent/gemini.go @@ -45,24 +45,31 @@ func NewGeminiAgent(command string) *GeminiAgent { return &GeminiAgent{Command: command, Model: defaultGeminiModel, Reasoning: ReasoningStandard} } -// WithReasoning returns a copy of the agent with the model preserved (reasoning not yet supported). -func (a *GeminiAgent) WithReasoning(level ReasoningLevel) Agent { +func (a *GeminiAgent) clone(opts ...agentCloneOption) *GeminiAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + "", + opts..., + ) return &GeminiAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: level, - Agentic: a.Agentic, + Command: cfg.Command, + Model: cfg.Model, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, } } +// WithReasoning returns a copy of the agent with the model preserved (reasoning not yet supported). +func (a *GeminiAgent) WithReasoning(level ReasoningLevel) Agent { + return a.clone(withClonedReasoning(level)) +} + // WithAgentic returns a copy of the agent configured for agentic mode. func (a *GeminiAgent) WithAgentic(agentic bool) Agent { - return &GeminiAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: agentic, - } + return a.clone(withClonedAgentic(agentic)) } // WithModel returns a copy of the agent configured to use the specified model. @@ -70,12 +77,7 @@ func (a *GeminiAgent) WithModel(model string) Agent { if model == "" { return a } - return &GeminiAgent{ - Command: a.Command, - Model: model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - } + return a.clone(withClonedModel(model)) } func (a *GeminiAgent) Name() string { diff --git a/internal/agent/kilo.go b/internal/agent/kilo.go index f4ab042e0..238a11d0a 100644 --- a/internal/agent/kilo.go +++ b/internal/agent/kilo.go @@ -27,48 +27,42 @@ func NewKiloAgent(command string) *KiloAgent { return &KiloAgent{Command: command, Reasoning: ReasoningStandard} } -func (a *KiloAgent) WithReasoning(level ReasoningLevel) Agent { +func (a *KiloAgent) clone(opts ...agentCloneOption) *KiloAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + a.SessionID, + opts..., + ) return &KiloAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: level, - Agentic: a.Agentic, - SessionID: a.SessionID, + Command: cfg.Command, + Model: cfg.Model, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + SessionID: cfg.SessionID, } } +func (a *KiloAgent) WithReasoning(level ReasoningLevel) Agent { + return a.clone(withClonedReasoning(level)) +} + func (a *KiloAgent) WithAgentic(agentic bool) Agent { - return &KiloAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedAgentic(agentic)) } func (a *KiloAgent) WithModel(model string) Agent { if model == "" { return a } - return &KiloAgent{ - Command: a.Command, - Model: model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedModel(model)) } // WithSessionID returns a copy of the agent configured to resume a prior session. func (a *KiloAgent) WithSessionID(sessionID string) Agent { - return &KiloAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: sanitizedResumeSessionID(sessionID), - } + return a.clone(withClonedSessionID(sessionID)) } func (a *KiloAgent) Name() string { diff --git a/internal/agent/kiro.go b/internal/agent/kiro.go index 1fbc2b5b7..72c799e1c 100644 --- a/internal/agent/kiro.go +++ b/internal/agent/kiro.go @@ -91,16 +91,32 @@ func NewKiroAgent(command string) *KiroAgent { return &KiroAgent{Command: command, Reasoning: ReasoningStandard} } +func (a *KiroAgent) clone(opts ...agentCloneOption) *KiroAgent { + cfg := newAgentCloneConfig( + a.Command, + "", + a.Reasoning, + a.Agentic, + "", + opts..., + ) + return &KiroAgent{ + Command: cfg.Command, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + } +} + // WithReasoning returns a copy with the reasoning level stored. // kiro-cli has no reasoning flag; callers can map reasoning to agent selection instead. func (a *KiroAgent) WithReasoning(level ReasoningLevel) Agent { - return &KiroAgent{Command: a.Command, Reasoning: level, Agentic: a.Agentic} + return a.clone(withClonedReasoning(level)) } // WithAgentic returns a copy of the agent configured for agentic mode. // In agentic mode, --trust-all-tools is passed so kiro can use tools without confirmation. func (a *KiroAgent) WithAgentic(agentic bool) Agent { - return &KiroAgent{Command: a.Command, Reasoning: a.Reasoning, Agentic: agentic} + return a.clone(withClonedAgentic(agentic)) } // WithModel returns the agent unchanged; kiro-cli does not expose a --model CLI flag. diff --git a/internal/agent/opencode.go b/internal/agent/opencode.go index 9a7675e48..f73007205 100644 --- a/internal/agent/opencode.go +++ b/internal/agent/opencode.go @@ -26,28 +26,34 @@ func NewOpenCodeAgent(command string) *OpenCodeAgent { return &OpenCodeAgent{Command: command, Reasoning: ReasoningStandard} } -// WithReasoning returns a copy of the agent with the model preserved (reasoning not yet supported). -func (a *OpenCodeAgent) WithReasoning(level ReasoningLevel) Agent { +func (a *OpenCodeAgent) clone(opts ...agentCloneOption) *OpenCodeAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + a.SessionID, + opts..., + ) return &OpenCodeAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: level, - Agentic: a.Agentic, - SessionID: a.SessionID, + Command: cfg.Command, + Model: cfg.Model, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + SessionID: cfg.SessionID, } } +// WithReasoning returns a copy of the agent with the model preserved (reasoning not yet supported). +func (a *OpenCodeAgent) WithReasoning(level ReasoningLevel) Agent { + return a.clone(withClonedReasoning(level)) +} + // WithAgentic returns a copy of the agent configured for agentic mode. // Note: OpenCode's `run` command auto-approves all permissions in non-interactive mode, // so agentic mode is effectively always enabled when running through roborev. func (a *OpenCodeAgent) WithAgentic(agentic bool) Agent { - return &OpenCodeAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedAgentic(agentic)) } // WithModel returns a copy of the agent configured to use the specified model. @@ -55,24 +61,12 @@ func (a *OpenCodeAgent) WithModel(model string) Agent { if model == "" { return a } - return &OpenCodeAgent{ - Command: a.Command, - Model: model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedModel(model)) } // WithSessionID returns a copy of the agent configured to resume a prior session. func (a *OpenCodeAgent) WithSessionID(sessionID string) Agent { - return &OpenCodeAgent{ - Command: a.Command, - Model: a.Model, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: sanitizedResumeSessionID(sessionID), - } + return a.clone(withClonedSessionID(sessionID)) } func (a *OpenCodeAgent) Name() string { diff --git a/internal/agent/pi.go b/internal/agent/pi.go index 88f34c780..e9bbbbd6f 100644 --- a/internal/agent/pi.go +++ b/internal/agent/pi.go @@ -31,32 +31,37 @@ func NewPiAgent(command string) *PiAgent { return &PiAgent{Command: command, Reasoning: ReasoningStandard} } +func (a *PiAgent) clone(opts ...agentCloneOption) *PiAgent { + cfg := newAgentCloneConfig( + a.Command, + a.Model, + a.Reasoning, + a.Agentic, + a.SessionID, + opts..., + ) + return &PiAgent{ + Command: cfg.Command, + Model: cfg.Model, + Provider: a.Provider, + Reasoning: cfg.Reasoning, + Agentic: cfg.Agentic, + SessionID: cfg.SessionID, + } +} + func (a *PiAgent) Name() string { return "pi" } // WithReasoning returns a copy of the agent configured with the specified reasoning level. func (a *PiAgent) WithReasoning(level ReasoningLevel) Agent { - return &PiAgent{ - Command: a.Command, - Model: a.Model, - Provider: a.Provider, - Reasoning: level, - Agentic: a.Agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedReasoning(level)) } // WithAgentic returns a copy of the agent configured for agentic mode. func (a *PiAgent) WithAgentic(agentic bool) Agent { - return &PiAgent{ - Command: a.Command, - Model: a.Model, - Provider: a.Provider, - Reasoning: a.Reasoning, - Agentic: agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedAgentic(agentic)) } // WithModel returns a copy of the agent configured to use the specified model. @@ -64,14 +69,7 @@ func (a *PiAgent) WithModel(model string) Agent { if model == "" { return a } - return &PiAgent{ - Command: a.Command, - Model: model, - Provider: a.Provider, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: a.SessionID, - } + return a.clone(withClonedModel(model)) } // WithProvider returns a copy of the agent configured to use the specified provider. @@ -79,26 +77,14 @@ func (a *PiAgent) WithProvider(provider string) Agent { if provider == "" { return a } - return &PiAgent{ - Command: a.Command, - Model: a.Model, - Provider: provider, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: a.SessionID, - } + cloned := a.clone() + cloned.Provider = provider + return cloned } // WithSessionID returns a copy of the agent configured to resume a prior session. func (a *PiAgent) WithSessionID(sessionID string) Agent { - return &PiAgent{ - Command: a.Command, - Model: a.Model, - Provider: a.Provider, - Reasoning: a.Reasoning, - Agentic: a.Agentic, - SessionID: sanitizedResumeSessionID(sessionID), - } + return a.clone(withClonedSessionID(sessionID)) } func (a *PiAgent) CommandName() string {