diff --git a/backend/internal/adapters/agent/pi/pi.go b/backend/internal/adapters/agent/pi/pi.go index bd3e3770d2..66f8f5baaf 100644 --- a/backend/internal/adapters/agent/pi/pi.go +++ b/backend/internal/adapters/agent/pi/pi.go @@ -74,9 +74,25 @@ func (p *Plugin) Manifest() adapters.Manifest { } } +// GetConfigSpec reports the per-project agent config keys Pi understands. +func (p *Plugin) GetConfigSpec(ctx context.Context) (ports.ConfigSpec, error) { + if err := ctx.Err(); err != nil { + return ports.ConfigSpec{}, err + } + return ports.ConfigSpec{ + Fields: []ports.ConfigField{ + { + Key: "model", + Type: ports.ConfigFieldString, + Description: "Model override passed to `pi --model`.", + }, + }, + }, nil +} + // GetLaunchCommand builds the argv to start a new interactive Pi session: // -// pi [--append-system-prompt ] [] +// pi [--append-system-prompt ] [--model ] [] // // The prompt is delivered in-command as a trailing positional message. Pi does // not honor a `--` options terminator, so the prompt must not begin with "-". @@ -97,6 +113,7 @@ func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) ( } cmd = append(cmd, "--append-system-prompt", string(data)) } + appendModelFlag(&cmd, cfg.Config) if cfg.Prompt != "" { cmd = append(cmd, cfg.Prompt) } @@ -130,10 +147,17 @@ func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig) } cmd = append(cmd, "--append-system-prompt", string(data)) } + appendModelFlag(&cmd, cfg.Config) cmd = append(cmd, "--session", agentSessionID) return cmd, true, nil } +func appendModelFlag(cmd *[]string, cfg ports.AgentConfig) { + if model := strings.TrimSpace(cfg.Model); model != "" { + *cmd = append(*cmd, "--model", model) + } +} + var piBinarySpec = binaryutil.BinarySpec{ Label: "pi", Names: []string{"pi"}, diff --git a/backend/internal/adapters/agent/pi/pi_test.go b/backend/internal/adapters/agent/pi/pi_test.go index 9ab70eaff0..54428a8a0e 100644 --- a/backend/internal/adapters/agent/pi/pi_test.go +++ b/backend/internal/adapters/agent/pi/pi_test.go @@ -32,13 +32,20 @@ func TestManifest(t *testing.T) { } } -func TestGetConfigSpecEmpty(t *testing.T) { +func TestGetConfigSpecReportsModelField(t *testing.T) { spec, err := (&Plugin{}).GetConfigSpec(context.Background()) if err != nil { t.Fatalf("err: %v", err) } - if len(spec.Fields) != 0 { - t.Fatalf("expected no fields, got %d", len(spec.Fields)) + want := []ports.ConfigField{ + { + Key: "model", + Type: ports.ConfigFieldString, + Description: "Model override passed to `pi --model`.", + }, + } + if !reflect.DeepEqual(spec.Fields, want) { + t.Fatalf("config fields\nwant: %#v\n got: %#v", want, spec.Fields) } } @@ -68,6 +75,38 @@ func TestGetLaunchCommandWorkerWithPromptIsInteractive(t *testing.T) { } } +func TestGetLaunchCommandAppendsConfiguredModelBeforePrompt(t *testing.T) { + p := &Plugin{resolvedBinary: "pi"} + cmd, err := p.GetLaunchCommand(context.Background(), ports.LaunchConfig{ + Config: ports.AgentConfig{Model: " openai/gpt-4o "}, + SystemPrompt: "follow repo rules", + Prompt: "add a health check", + }) + if err != nil { + t.Fatal(err) + } + + want := []string{"pi", "--append-system-prompt", "follow repo rules", "--model", "openai/gpt-4o", "add a health check"} + if !reflect.DeepEqual(cmd, want) { + t.Fatalf("cmd = %#v, want %#v", cmd, want) + } +} + +func TestGetLaunchCommandOmitsBlankConfiguredModel(t *testing.T) { + p := &Plugin{resolvedBinary: "pi"} + cmd, err := p.GetLaunchCommand(context.Background(), ports.LaunchConfig{ + Config: ports.AgentConfig{Model: " \t "}, + }) + if err != nil { + t.Fatal(err) + } + + want := []string{"pi"} + if !reflect.DeepEqual(cmd, want) { + t.Fatalf("cmd = %#v, want %#v", cmd, want) + } +} + func TestGetLaunchCommandOrchestratorAppendsSystemPromptInteractively(t *testing.T) { p := &Plugin{resolvedBinary: "pi"} cmd, err := p.GetLaunchCommand(context.Background(), ports.LaunchConfig{ @@ -187,6 +226,28 @@ func TestGetRestoreCommand(t *testing.T) { } } +func TestGetRestoreCommandAppendsConfiguredModelBeforeSession(t *testing.T) { + p := &Plugin{resolvedBinary: "pi"} + cmd, ok, err := p.GetRestoreCommand(context.Background(), ports.RestoreConfig{ + Config: ports.AgentConfig{Model: " openai/gpt-4o "}, + Session: ports.SessionRef{ + Metadata: map[string]string{ports.MetadataKeyAgentSessionID: "019e950e-52e0-7411-961b-d380ca7e610f"}, + }, + SystemPrompt: "follow repo rules", + }) + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("ok=false, want true") + } + + want := []string{"pi", "--append-system-prompt", "follow repo rules", "--model", "openai/gpt-4o", "--session", "019e950e-52e0-7411-961b-d380ca7e610f"} + if !reflect.DeepEqual(cmd, want) { + t.Fatalf("cmd = %#v, want %#v", cmd, want) + } +} + func TestGetRestoreCommandReappendsSystemPromptInteractively(t *testing.T) { p := &Plugin{resolvedBinary: "pi"} cmd, ok, err := p.GetRestoreCommand(context.Background(), ports.RestoreConfig{