From af64050315116d0943b1b878976abbaee1d1466c Mon Sep 17 00:00:00 2001 From: michelle-clayton-work Date: Tue, 22 Sep 2026 15:58:59 -0700 Subject: [PATCH 1/2] Align agent mode harness with .NET Add configurable mode tools, notification suppression, named tool arguments, and matching empty-mode validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/harness/agentmode/agentmode.go | 178 +++++++++++++-------- agent/harness/agentmode/agentmode_test.go | 182 ++++++++++++++++++++-- 2 files changed, 287 insertions(+), 73 deletions(-) diff --git a/agent/harness/agentmode/agentmode.go b/agent/harness/agentmode/agentmode.go index 32e1f696..edd57839 100644 --- a/agent/harness/agentmode/agentmode.go +++ b/agent/harness/agentmode/agentmode.go @@ -25,14 +25,17 @@ import ( const stateKey = "agentModeState" +const modeGetInstructions = "Use the mode_get tool to check your current operating mode.\n" + +const modeSetInstructions = "Use the mode_set tool to switch between modes as your work progresses. Only use mode_set if the user explicitly instructs/allows you to change modes.\n\n" + +const planModeTransition = "7. When approval is granted, always switch to execute mode (using the `mode_set` tool), and follow the steps for *Execute mode*." + const defaultInstructions = `## Agent Mode - You can operate in different modes. Depending on the mode you are in, you will be required to follow different processes. -Use the mode_get tool to check your current operating mode. -Use the mode_set tool to switch between modes as your work progresses. Only use mode_set if the user explicitly instructs/allows you to change modes. - -You are currently operating in the {current_mode} mode. +{mode_get_instructions}{mode_set_instructions}You are currently operating in the {current_mode} mode. ### Mandatory Mode based Workflow @@ -54,7 +57,8 @@ type state struct { // Config configures the agent mode provider. type Config struct { - // Modes is the set of available modes. If empty, defaults to "plan" and "execute". + // Modes is the set of available modes. If nil, defaults to "plan" and "execute". + // An empty, non-nil slice is invalid. Modes []Mode // DefaultMode is the initial mode. Must be one of the configured Modes. @@ -64,6 +68,14 @@ type Config struct { // Instructions overrides the default instruction template. // Use {available_modes} and {current_mode} as placeholders. Instructions *string + + // DisableModeSetTool omits the mode_set tool while retaining mode state + // and instructions. + DisableModeSetTool bool + + // DisableModeGetTool omits the mode_get tool while retaining mode state + // and instructions. + DisableModeGetTool bool } var defaultModes = []Mode{ @@ -82,7 +94,7 @@ Process to follow when in plan mode: 4. Do short exploratory research if it helps with being able to ask sensible clarifications from the user. 5. Write the plan to a memory file, so that it is retained even if compaction happens. Make sure to update the plan file if the user requests changes. 6. Present the plan to the user and ask for approval to switch to execute mode and process the plan. -7. When approval is granted, always switch to execute mode (using the ` + "`mode_set`" + ` tool), and follow the steps for *Execute mode*.`, +{plan_mode_transition}`, }, { Name: "execute", @@ -106,9 +118,9 @@ If 2. Work autonomously using your best judgment — do not ask the user questio // Panics if the configuration contains duplicate names, an empty mode name or // instructions, or a default mode that is not in the configured set. func New(cfg Config) *Provider { - modes := defaultModes - if len(cfg.Modes) > 0 { - modes = cfg.Modes + modes := cfg.Modes + if modes == nil { + modes = defaultModes } if len(modes) == 0 { panic("agentmode: at least one mode must be configured") @@ -119,6 +131,7 @@ func New(cfg Config) *Provider { defaultMode = *cfg.DefaultMode } instructions := defaultInstructions + usesDefaultInstructions := cfg.Instructions == nil if cfg.Instructions != nil { instructions = *cfg.Instructions } @@ -144,11 +157,15 @@ func New(cfg Config) *Provider { } p := &Provider{ - modes: modes, - defaultMode: defaultMode, - instructions: instructions, - validModes: validModes, - modeNamesDisplay: strings.Join(modeNames, "\", \""), + modes: modes, + defaultMode: defaultMode, + instructions: instructions, + validModes: validModes, + modeNamesDisplay: strings.Join(modeNames, "\", \""), + usesDefaultModes: cfg.Modes == nil, + usesDefaultInstructions: usesDefaultInstructions, + disableModeSetTool: cfg.DisableModeSetTool, + disableModeGetTool: cfg.DisableModeGetTool, } p.provider = agent.NewContextProvider(agent.ContextProviderConfig{ @@ -161,12 +178,16 @@ func New(cfg Config) *Provider { // Provider is an agent mode context provider. // Use [New] to create. Provider can be used directly in agent configuration. type Provider struct { - provider agent.ContextProvider - modes []Mode - defaultMode string - instructions string - validModes map[string]struct{} - modeNamesDisplay string + provider agent.ContextProvider + modes []Mode + defaultMode string + instructions string + validModes map[string]struct{} + modeNamesDisplay string + usesDefaultModes bool + usesDefaultInstructions bool + disableModeSetTool bool + disableModeGetTool bool sessionLocks sync.Map // map[weak.Pointer[agent.Session]]*sync.Mutex nullSessionLock sync.Mutex @@ -287,50 +308,79 @@ func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext) func (p *Provider) buildInstructions(currentMode string) string { var sb strings.Builder for _, m := range p.modes { - fmt.Fprintf(&sb, "#### %s\n\n%s\n\n", m.Name, strings.TrimRight(m.Instructions, "\n")) + modeInstructions := m.Instructions + if p.usesDefaultModes && m.Name == "plan" { + transition := planModeTransition + if p.disableModeSetTool { + transition = "" + } + modeInstructions = strings.ReplaceAll(modeInstructions, "{plan_mode_transition}", transition) + } + fmt.Fprintf(&sb, "#### %s\n\n%s\n\n", m.Name, strings.TrimRight(modeInstructions, "\n")) } modesText := strings.TrimRight(sb.String(), "\n") - result := strings.ReplaceAll(p.instructions, "{available_modes}", modesText) + result := p.instructions + if p.usesDefaultInstructions { + getInstructions := modeGetInstructions + if p.disableModeGetTool { + getInstructions = "" + } + setInstructions := modeSetInstructions + if p.disableModeSetTool { + setInstructions = "" + } + result = strings.ReplaceAll(result, "{mode_get_instructions}", getInstructions) + result = strings.ReplaceAll(result, "{mode_set_instructions}", setInstructions) + } + result = strings.ReplaceAll(result, "{available_modes}", modesText) result = strings.ReplaceAll(result, "{current_mode}", currentMode) return result } func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { - setTool := functool.MustNew( - functool.Config{ - Name: "mode_set", - Description: fmt.Sprintf("Switch the agent's operating mode. Supported modes: \"%s\".", p.modeNamesDisplay), - }, - func(ctx context.Context, mode string) (string, error) { - if _, ok := p.validModes[mode]; !ok { - return "", fmt.Errorf("invalid mode: %q. Supported modes: \"%s\"", mode, p.modeNamesDisplay) - } - mu := p.getSessionLock(opts) - mu.Lock() - defer mu.Unlock() - st := p.loadState(opts) - st.CurrentMode = mode - p.saveState(opts, st) - return fmt.Sprintf("Mode changed to %q.", mode), nil - }, - ) - - getTool := functool.MustNew( - functool.Config{ - Name: "mode_get", - Description: "Get the agent's current operating mode.", - }, - func(ctx context.Context, _ struct{}) (string, error) { - mu := p.getSessionLock(opts) - mu.Lock() - defer mu.Unlock() - st := p.loadState(opts) - return st.CurrentMode, nil - }, - ) - - return []tool.FuncTool{setTool, getTool} + tools := make([]tool.FuncTool, 0, 2) + if !p.disableModeSetTool { + type setModeInput struct { + Mode string `json:"mode" jsonschema:"The operating mode to switch to"` + } + tools = append(tools, functool.MustNew( + functool.Config{ + Name: "mode_set", + Description: fmt.Sprintf("Switch the agent's operating mode. Supported modes: \"%s\".", p.modeNamesDisplay), + }, + func(ctx context.Context, input setModeInput) (string, error) { + if _, ok := p.validModes[input.Mode]; !ok { + return "", fmt.Errorf("invalid mode: %q. Supported modes: \"%s\"", input.Mode, p.modeNamesDisplay) + } + mu := p.getSessionLock(opts) + mu.Lock() + defer mu.Unlock() + st := p.loadState(opts) + st.CurrentMode = input.Mode + p.saveState(opts, st) + return fmt.Sprintf("Mode changed to %q.", input.Mode), nil + }, + )) + } + + if !p.disableModeGetTool { + tools = append(tools, functool.MustNew( + functool.Config{ + Name: "mode_get", + Description: "Get the agent's current operating mode.", + }, + func(ctx context.Context, _ struct{}) (string, error) { + mu := p.getSessionLock(opts) + mu.Lock() + defer mu.Unlock() + st := p.loadState(opts) + return st.CurrentMode, nil + }, + )) + } + + return tools } // ModeForSession returns the current operating mode from session state. @@ -343,12 +393,16 @@ func (p *Provider) ModeForSession(session *agent.Session) string { } // SetModeForSession sets the operating mode in session state, validating it -// against the provider's configured modes. Returns an error if the mode is -// invalid or no session is available. -func (p *Provider) SetModeForSession(session *agent.Session, mode string) error { +// against the provider's configured modes. Pass true as disableNotification to +// suppress the next mode-change notification and clear any pending notification. +// At most one disableNotification value may be provided. +func (p *Provider) SetModeForSession(session *agent.Session, mode string, disableNotification ...bool) error { if _, ok := p.validModes[mode]; !ok { return fmt.Errorf("agentmode: invalid mode %q", mode) } + if len(disableNotification) > 1 { + return fmt.Errorf("agentmode: at most one disableNotification value may be provided") + } mu := p.getSessionLockForSession(session) mu.Lock() defer mu.Unlock() @@ -356,10 +410,12 @@ func (p *Provider) SetModeForSession(session *agent.Session, mode string) error return fmt.Errorf("agentmode: no session available") } s := p.loadStateForSession(session) - if s.CurrentMode != mode { + if len(disableNotification) == 1 && disableNotification[0] { + s.PreviousMode = "" + } else if s.CurrentMode != mode { s.PreviousMode = s.CurrentMode - s.CurrentMode = mode } + s.CurrentMode = mode p.saveStateForSession(session, s) return nil } diff --git a/agent/harness/agentmode/agentmode_test.go b/agent/harness/agentmode/agentmode_test.go index 6e1c7fc4..409e8249 100644 --- a/agent/harness/agentmode/agentmode_test.go +++ b/agent/harness/agentmode/agentmode_test.go @@ -4,6 +4,7 @@ package agentmode_test import ( "context" + "encoding/json" "slices" "strings" "sync" @@ -104,7 +105,7 @@ func TestConcurrentToolInvocations_NoDataRace(t *testing.T) { } go func(idx int, mode string) { defer wg.Done() - _, errs[idx] = setTool.Call(context.Background(), `{"Arg0":"`+mode+`"}`) + _, errs[idx] = setTool.Call(context.Background(), `{"mode":"`+mode+`"}`) }(i*2, mode) go func(idx int) { defer wg.Done() @@ -226,17 +227,14 @@ func TestInvalidDefaultMode_Panics(t *testing.T) { }) } -// 7. Options_EmptyModes_UsesDefaults -// In Go, an empty Modes slice is treated as "use defaults" (plan/execute). -func TestEmptyModes_UsesDefaults(t *testing.T) { - p := agentmode.New(agentmode.Config{ - Modes: []agentmode.Mode{}, - }) - opts := sessionOpts() - mode := p.ModeForSession(mustSession(t, opts)) - if mode != "plan" { - t.Errorf("expected default mode 'plan' for empty modes, got %q", mode) - } +// 7. Options_EmptyModes_Panics +func TestEmptyModes_Panics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("expected panic for empty modes") + } + }() + agentmode.New(agentmode.Config{Modes: []agentmode.Mode{}}) } // 8. Options_CustomModes_AppearInInstructions @@ -406,6 +404,52 @@ func TestExternalModeChange_SameMode_NoNotification(t *testing.T) { } } +func TestExternalModeChange_DisableNotification(t *testing.T) { + p := agentmode.New(agentmode.Config{}) + opts := sessionOpts() + session := mustSession(t, opts) + + if err := p.SetModeForSession(session, "execute", true); err != nil { + t.Fatal(err) + } + + outMessages, _, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + for _, msg := range outMessages { + if strings.Contains(msg.Contents.Text(), "Mode changed") { + t.Error("did not expect a mode-change notification") + } + } + if mode := p.ModeForSession(session); mode != "execute" { + t.Errorf("expected mode execute, got %q", mode) + } +} + +func TestExternalModeChange_DisableNotificationClearsPendingNotification(t *testing.T) { + p := agentmode.New(agentmode.Config{}) + opts := sessionOpts() + session := mustSession(t, opts) + + if err := p.SetModeForSession(session, "execute"); err != nil { + t.Fatal(err) + } + if err := p.SetModeForSession(session, "execute", true); err != nil { + t.Fatal(err) + } + + outMessages, _, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + for _, msg := range outMessages { + if strings.Contains(msg.Contents.Text(), "Mode changed") { + t.Error("expected the pending notification to be cleared") + } + } +} + // 14. SetModeForSession_ChangesMode func TestSetModeForSession_ChangesMode(t *testing.T) { p := agentmode.New(agentmode.Config{}) @@ -523,6 +567,18 @@ func TestPublicSetModeForSession_NoSession_ReturnsError(t *testing.T) { } } +func TestPublicSetModeForSession_MultipleNotificationValuesReturnsError(t *testing.T) { + p := agentmode.New(agentmode.Config{}) + + err := p.SetModeForSession(agenttest.CreateSession(), "execute", true, false) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "at most one disableNotification") { + t.Fatalf("expected notification argument error, got %v", err) + } +} + // 23. PublicSetModeForSession_ReflectedInToolResults func TestPublicSetModeForSession_ReflectedInInstructions(t *testing.T) { p := agentmode.New(agentmode.Config{}) @@ -610,6 +666,108 @@ func TestToolNames(t *testing.T) { } } +func TestModeSetTool_UsesNamedModeArgument(t *testing.T) { + p := agentmode.New(agentmode.Config{}) + opts := sessionOpts() + + _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + + for _, candidate := range collectTools(outOpts) { + if candidate.Name() != "mode_set" { + continue + } + modeSet, ok := candidate.(tool.FuncTool) + if !ok { + t.Fatalf("mode_set has type %T, want tool.FuncTool", candidate) + } + schema, err := json.Marshal(modeSet.Schema()) + if err != nil { + t.Fatalf("marshal mode_set schema: %v", err) + } + if !strings.Contains(string(schema), `"mode"`) { + t.Errorf("mode_set schema does not contain a named mode argument: %s", schema) + } + if strings.Contains(string(schema), `"Arg0"`) { + t.Errorf("mode_set schema contains the generic Arg0 argument: %s", schema) + } + if _, err := modeSet.Call(context.Background(), `{"mode":"execute"}`); err != nil { + t.Fatalf("mode_set rejected its named mode argument: %v", err) + } + if mode := p.ModeForSession(mustSession(t, opts)); mode != "execute" { + t.Errorf("expected execute mode, got %q", mode) + } + return + } + t.Fatal("mode_set tool not found") +} + +func TestDisableModeSetTool(t *testing.T) { + p := agentmode.New(agentmode.Config{DisableModeSetTool: true}) + opts := sessionOpts() + + _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + + for _, candidate := range collectTools(outOpts) { + if candidate.Name() == "mode_set" { + t.Error("mode_set should be disabled") + } + } + instructions := collectInstructions(outOpts) + if strings.Contains(instructions, "mode_set") { + t.Error("default instructions should not refer to a disabled mode_set tool") + } +} + +func TestDisableModeGetTool(t *testing.T) { + p := agentmode.New(agentmode.Config{DisableModeGetTool: true}) + opts := sessionOpts() + + _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + + for _, candidate := range collectTools(outOpts) { + if candidate.Name() == "mode_get" { + t.Error("mode_get should be disabled") + } + } + instructions := collectInstructions(outOpts) + if strings.Contains(instructions, "mode_get") { + t.Error("default instructions should not refer to a disabled mode_get tool") + } +} + +func TestDisableModeToolsRetainsModeStateAndInstructions(t *testing.T) { + p := agentmode.New(agentmode.Config{ + DisableModeSetTool: true, + DisableModeGetTool: true, + }) + opts := sessionOpts() + session := mustSession(t, opts) + + if err := p.SetModeForSession(session, "execute", true); err != nil { + t.Fatal(err) + } + _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + + if tools := collectTools(outOpts); len(tools) != 0 { + t.Fatalf("expected no mode tools, got %d", len(tools)) + } + if instructions := collectInstructions(outOpts); !strings.Contains(instructions, "execute") { + t.Error("expected mode instructions to remain enabled") + } +} + // Verify default instructions contain mode_set/mode_get tool references and mode-check guidance. func TestDefaultInstructions_ContainToolNamesAndModeCheckGuidance(t *testing.T) { p := agentmode.New(agentmode.Config{}) From 2fcca794942405d3ae1a63e9082476c87560bb6c Mon Sep 17 00:00:00 2001 From: michelle-clayton-work Date: Thu, 24 Sep 2026 10:48:41 -0700 Subject: [PATCH 2/2] Refine agent mode session API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/harness/agentmode/agentmode.go | 55 ++++----- agent/harness/agentmode/agentmode_test.go | 130 +++++++++------------- 2 files changed, 72 insertions(+), 113 deletions(-) diff --git a/agent/harness/agentmode/agentmode.go b/agent/harness/agentmode/agentmode.go index 1d1492eb..f2749026 100644 --- a/agent/harness/agentmode/agentmode.go +++ b/agent/harness/agentmode/agentmode.go @@ -208,10 +208,10 @@ type Provider struct { // keeping the registry from growing unbounded. func (p *Provider) getSessionLock(opts []agent.Option) *sync.Mutex { session, _ := agent.GetOption(opts, agent.WithSession) - return p.getSessionLockForSession(session) + return p.sessionLock(session) } -func (p *Provider) getSessionLockForSession(session *agent.Session) *sync.Mutex { +func (p *Provider) sessionLock(session *agent.Session) *sync.Mutex { if session == nil { return &p.nullSessionLock } @@ -231,7 +231,7 @@ func (p *Provider) getSessionLockForSession(session *agent.Session) *sync.Mutex return actual.(*sync.Mutex) } -func (p *Provider) loadStateForSession(session *agent.Session) *state { +func (p *Provider) loadSessionState(session *agent.Session) *state { if session == nil { return &state{CurrentMode: p.defaultMode} } @@ -242,7 +242,7 @@ func (p *Provider) loadStateForSession(session *agent.Session) *state { return &state{CurrentMode: p.defaultMode} } -func (p *Provider) saveStateForSession(session *agent.Session, s *state) { +func (p *Provider) saveSessionState(session *agent.Session, s *state) { if session == nil || s == nil { return } @@ -259,12 +259,12 @@ func (p *Provider) Invoked(ctx context.Context, invoked agent.InvokedContext) er func (p *Provider) loadState(opts []agent.Option) *state { session, _ := agent.GetOption(opts, agent.WithSession) - return p.loadStateForSession(session) + return p.loadSessionState(session) } func (p *Provider) saveState(opts []agent.Option, s *state) { session, _ := agent.GetOption(opts, agent.WithSession) - p.saveStateForSession(session, s) + p.saveSessionState(session, s) } func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, []agent.Option, error) { @@ -275,10 +275,10 @@ func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext) mu := p.getSessionLock(opts) mu.Lock() st := p.loadState(opts) - // Persist the initial state so SetModeForSession can read it. + // Persist the initial state so SetMode can read it. p.saveState(opts, st) - // If the mode was changed externally (e.g. via SetModeForSession), inject a notification + // If the mode was changed externally (e.g. via SetMode), inject a notification // so the agent clearly sees the change in conversation context. if st.PreviousMode != "" { outMessages = append(outMessages, message.NewText(fmt.Sprintf( @@ -386,46 +386,29 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { return tools } -// ModeForSession returns the current operating mode from session state. +// Mode returns the current operating mode from session state. // If no state has been persisted yet, it returns the configured default mode. -func (p *Provider) ModeForSession(session *agent.Session) string { - mu := p.getSessionLockForSession(session) +func (p *Provider) Mode(session *agent.Session) string { + mu := p.sessionLock(session) mu.Lock() defer mu.Unlock() - return p.loadStateForSession(session).CurrentMode + return p.loadSessionState(session).CurrentMode } -// SetModeForSession sets the operating mode in session state, validating it -// against the provider's configured modes. Returns an error if the mode is -// invalid or no session is available. Pass true as disableNotification to -// suppress the next mode-change notification and clear any pending notification. -// At most one disableNotification value may be provided. -func (p *Provider) SetModeForSession(session *agent.Session, mode string, disableNotification ...bool) error { - if len(disableNotification) > 1 { - return fmt.Errorf("agentmode: at most one disableNotification value may be provided") - } - disable := len(disableNotification) == 1 && disableNotification[0] - return p.setModeForSessionWithNotificationOption(session, mode, disable) -} - -// SetModeForSessionSilently sets the operating mode in session state without -// queuing the next-invocation mode-change notification. It also clears any -// pending mode-change notification for the session. -func (p *Provider) SetModeForSessionSilently(session *agent.Session, mode string) error { - return p.setModeForSessionWithNotificationOption(session, mode, true) -} - -func (p *Provider) setModeForSessionWithNotificationOption(session *agent.Session, mode string, disableNotification bool) error { +// SetMode sets the operating mode in session state. It returns an error if the +// mode is invalid or no session is available. Set disableNotification to true +// to suppress the next mode-change notification and clear any pending one. +func (p *Provider) SetMode(session *agent.Session, mode string, disableNotification bool) error { if _, ok := p.validModes[mode]; !ok { return fmt.Errorf("agentmode: invalid mode %q", mode) } - mu := p.getSessionLockForSession(session) + mu := p.sessionLock(session) mu.Lock() defer mu.Unlock() if session == nil { return fmt.Errorf("agentmode: no session available") } - s := p.loadStateForSession(session) + s := p.loadSessionState(session) if disableNotification { s.PreviousMode = "" } @@ -436,6 +419,6 @@ func (p *Provider) setModeForSessionWithNotificationOption(session *agent.Sessio } s.CurrentMode = mode } - p.saveStateForSession(session, s) + p.saveSessionState(session, s) return nil } diff --git a/agent/harness/agentmode/agentmode_test.go b/agent/harness/agentmode/agentmode_test.go index f544edc9..ae997e29 100644 --- a/agent/harness/agentmode/agentmode_test.go +++ b/agent/harness/agentmode/agentmode_test.go @@ -167,7 +167,7 @@ func TestCustomModes_AreUsed(t *testing.T) { }) opts := sessionOpts() - mode := p.ModeForSession(mustSession(t, opts)) + mode := p.Mode(mustSession(t, opts)) if mode != "draft" { t.Errorf("expected default mode 'draft', got %q", mode) } @@ -183,14 +183,14 @@ func TestCustomModes_SetModeValidatesAgainstList(t *testing.T) { }) opts := sessionOpts() - if err := p.SetModeForSession(mustSession(t, opts), "review"); err != nil { + if err := p.SetMode(mustSession(t, opts), "review", false); err != nil { t.Fatalf("expected valid mode 'review' to succeed: %v", err) } - if mode := p.ModeForSession(mustSession(t, opts)); mode != "review" { + if mode := p.Mode(mustSession(t, opts)); mode != "review" { t.Errorf("expected 'review', got %q", mode) } - if err := p.SetModeForSession(mustSession(t, opts), "invalid"); err == nil { + if err := p.SetMode(mustSession(t, opts), "invalid", false); err == nil { t.Fatal("expected error for invalid mode") } } @@ -206,7 +206,7 @@ func TestCustomDefaultMode_IsUsed(t *testing.T) { }) opts := sessionOpts() - mode := p.ModeForSession(mustSession(t, opts)) + mode := p.Mode(mustSession(t, opts)) if mode != "review" { t.Errorf("expected default mode 'review', got %q", mode) } @@ -324,7 +324,7 @@ func TestExternalModeChange_InjectsNotification(t *testing.T) { } // Change mode externally. - if err := p.SetModeForSession(session, "execute"); err != nil { + if err := p.SetMode(session, "execute", false); err != nil { t.Fatal(err) } @@ -357,7 +357,7 @@ func TestExternalModeChange_NotificationClearedAfterFirstRead(t *testing.T) { msgs := newMessages("hi") _, _, _ = invokeProvider(p, context.Background(), msgs, opts...) - _ = p.SetModeForSession(session, "execute") + _ = p.SetMode(session, "execute", false) // First read: should have notification. outMessages, _, _ := invokeProvider(p, context.Background(), msgs, opts...) @@ -394,7 +394,7 @@ func TestExternalModeChange_SameMode_NoNotification(t *testing.T) { _, _, _ = invokeProvider(p, context.Background(), msgs, opts...) // Set to same mode. - _ = p.SetModeForSession(session, "plan") + _ = p.SetMode(session, "plan", false) outMessages, _, _ := invokeProvider(p, context.Background(), msgs, opts...) for _, msg := range outMessages { @@ -404,28 +404,28 @@ func TestExternalModeChange_SameMode_NoNotification(t *testing.T) { } } -// 14. SetModeForSession_ChangesMode -func TestSetModeForSession_ChangesMode(t *testing.T) { +// 14. SetMode_ChangesMode +func TestSetMode_ChangesMode(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() _, _, _ = invokeProvider(p, context.Background(), newMessages("hi"), opts...) - if err := p.SetModeForSession(mustSession(t, opts), "execute"); err != nil { + if err := p.SetMode(mustSession(t, opts), "execute", false); err != nil { t.Fatal(err) } - if mode := p.ModeForSession(mustSession(t, opts)); mode != "execute" { + if mode := p.Mode(mustSession(t, opts)); mode != "execute" { t.Errorf("expected 'execute', got %q", mode) } } -// 15. SetModeForSession_ReturnsConfirmation — verified via instructions reflecting the new mode -func TestSetModeForSession_ReflectedInInstructions(t *testing.T) { +// 15. SetMode_ReturnsConfirmation — verified via instructions reflecting the new mode +func TestSetMode_ReflectedInInstructions(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() _, _, _ = invokeProvider(p, context.Background(), newMessages("hi"), opts...) - _ = p.SetModeForSession(mustSession(t, opts), "execute") + _ = p.SetMode(mustSession(t, opts), "execute", false) _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) if err != nil { @@ -438,12 +438,12 @@ func TestSetModeForSession_ReflectedInInstructions(t *testing.T) { } } -// 16. SetModeForSession_InvalidMode_Throws -func TestSetModeForSession_InvalidMode_ReturnsError(t *testing.T) { +// 16. SetMode_InvalidMode_Throws +func TestSetMode_InvalidMode_ReturnsError(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() - err := p.SetModeForSession(mustSession(t, opts), "nonexistent") + err := p.SetMode(mustSession(t, opts), "nonexistent", false) if err == nil { t.Fatal("expected error for invalid mode") } @@ -452,67 +452,67 @@ func TestSetModeForSession_InvalidMode_ReturnsError(t *testing.T) { } } -// 17. ModeForSession_ReturnsDefaultMode -func TestModeForSession_ReturnsDefaultMode(t *testing.T) { +// 17. Mode_ReturnsDefaultMode +func TestMode_ReturnsDefaultMode(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() - mode := p.ModeForSession(mustSession(t, opts)) + mode := p.Mode(mustSession(t, opts)) if mode != "plan" { t.Errorf("expected 'plan', got %q", mode) } } -// 18. ModeForSession_ReturnsUpdatedModeAfterSet -func TestModeForSession_ReturnsUpdatedModeAfterSet(t *testing.T) { +// 18. Mode_ReturnsUpdatedModeAfterSet +func TestMode_ReturnsUpdatedModeAfterSet(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() - _ = p.SetModeForSession(mustSession(t, opts), "execute") - mode := p.ModeForSession(mustSession(t, opts)) + _ = p.SetMode(mustSession(t, opts), "execute", false) + mode := p.Mode(mustSession(t, opts)) if mode != "execute" { t.Errorf("expected 'execute', got %q", mode) } } -// 19. ModeForSession_WithSessionOption_ReturnsDefaultMode -func TestModeForSession_WithSessionOption_ReturnsDefaultMode(t *testing.T) { +// 19. Mode_WithSessionOption_ReturnsDefaultMode +func TestMode_WithSessionOption_ReturnsDefaultMode(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() - if mode := p.ModeForSession(mustSession(t, opts)); mode != "plan" { + if mode := p.Mode(mustSession(t, opts)); mode != "plan" { t.Errorf("expected 'plan', got %q", mode) } } -// 20. PublicModeForSession_ReturnsDefaultMode -func TestPublicModeForSession_ReturnsDefaultMode(t *testing.T) { +// 20. PublicMode_ReturnsDefaultMode +func TestPublicMode_ReturnsDefaultMode(t *testing.T) { p := agentmode.New(agentmode.Config{}) session := agenttest.CreateSession() - if mode := p.ModeForSession(session); mode != "plan" { + if mode := p.Mode(session); mode != "plan" { t.Errorf("expected 'plan', got %q", mode) } } -// 21. PublicSetModeForSession_ChangesMode -func TestPublicSetModeForSession_ChangesMode(t *testing.T) { +// 21. PublicSetMode_ChangesMode +func TestPublicSetMode_ChangesMode(t *testing.T) { p := agentmode.New(agentmode.Config{}) session := agenttest.CreateSession() - if err := p.SetModeForSession(session, "execute"); err != nil { + if err := p.SetMode(session, "execute", false); err != nil { t.Fatal(err) } - if mode := p.ModeForSession(session); mode != "execute" { + if mode := p.Mode(session); mode != "execute" { t.Errorf("expected 'execute', got %q", mode) } } -// 22. PublicSetModeForSession_NoSession_ReturnsError -func TestPublicSetModeForSession_NoSession_ReturnsError(t *testing.T) { +// 22. PublicSetMode_NoSession_ReturnsError +func TestPublicSetMode_NoSession_ReturnsError(t *testing.T) { p := agentmode.New(agentmode.Config{}) - err := p.SetModeForSession(nil, "execute") + err := p.SetMode(nil, "execute", false) if err == nil { t.Fatal("expected error") } @@ -521,36 +521,12 @@ func TestPublicSetModeForSession_NoSession_ReturnsError(t *testing.T) { } } -func TestPublicSetModeForSession_MultipleNotificationValuesReturnsError(t *testing.T) { - p := agentmode.New(agentmode.Config{}) - - err := p.SetModeForSession(agenttest.CreateSession(), "execute", true, false) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "at most one disableNotification") { - t.Fatalf("expected notification argument error, got %v", err) - } -} - -func TestPublicSetModeForSessionSilently_NoSession_ReturnsError(t *testing.T) { - p := agentmode.New(agentmode.Config{}) - - err := p.SetModeForSessionSilently(nil, "execute") - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "no session available") { - t.Fatalf("expected no-session error, got %v", err) - } -} - -// 23. PublicSetModeForSession_ReflectedInToolResults -func TestPublicSetModeForSession_ReflectedInInstructions(t *testing.T) { +// 23. PublicSetMode_ReflectedInToolResults +func TestPublicSetMode_ReflectedInInstructions(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() - _ = p.SetModeForSession(mustSession(t, opts), "execute") + _ = p.SetMode(mustSession(t, opts), "execute", false) _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) if err != nil { @@ -569,7 +545,7 @@ func TestState_PersistsAcrossInvocations(t *testing.T) { opts := sessionOpts() _, _, _ = invokeProvider(p, context.Background(), newMessages("hi"), opts...) - _ = p.SetModeForSession(mustSession(t, opts), "execute") + _ = p.SetMode(mustSession(t, opts), "execute", false) // Second invocation — mode should persist. _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) @@ -582,12 +558,12 @@ func TestState_PersistsAcrossInvocations(t *testing.T) { t.Error("expected mode 'execute' to persist across invocations") } - if mode := p.ModeForSession(mustSession(t, opts)); mode != "execute" { + if mode := p.Mode(mustSession(t, opts)); mode != "execute" { t.Errorf("expected 'execute', got %q", mode) } } -func TestSetModeForSessionSilently_ChangesModeWithoutNotification(t *testing.T) { +func TestSetMode_DisableNotificationChangesModeWithoutNotification(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() session := mustSession(t, opts) @@ -597,10 +573,10 @@ func TestSetModeForSessionSilently_ChangesModeWithoutNotification(t *testing.T) t.Fatal(err) } - if err := p.SetModeForSessionSilently(session, "execute"); err != nil { + if err := p.SetMode(session, "execute", true); err != nil { t.Fatal(err) } - if mode := p.ModeForSession(session); mode != "execute" { + if mode := p.Mode(session); mode != "execute" { t.Fatalf("expected mode to be updated to execute, got %q", mode) } @@ -615,15 +591,15 @@ func TestSetModeForSessionSilently_ChangesModeWithoutNotification(t *testing.T) } } -func TestSetModeForSession_DisableNotification(t *testing.T) { +func TestSetMode_DisableNotification(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() session := mustSession(t, opts) - if err := p.SetModeForSession(session, "execute", true); err != nil { + if err := p.SetMode(session, "execute", true); err != nil { t.Fatal(err) } - if mode := p.ModeForSession(session); mode != "execute" { + if mode := p.Mode(session); mode != "execute" { t.Fatalf("expected mode to be updated to execute, got %q", mode) } @@ -638,7 +614,7 @@ func TestSetModeForSession_DisableNotification(t *testing.T) { } } -func TestSetModeForSessionSilently_ClearsPendingNotification(t *testing.T) { +func TestSetMode_DisableNotificationClearsPendingNotification(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() session := mustSession(t, opts) @@ -648,10 +624,10 @@ func TestSetModeForSessionSilently_ClearsPendingNotification(t *testing.T) { t.Fatal(err) } - if err := p.SetModeForSession(session, "execute"); err != nil { + if err := p.SetMode(session, "execute", false); err != nil { t.Fatal(err) } - if err := p.SetModeForSessionSilently(session, "plan"); err != nil { + if err := p.SetMode(session, "plan", true); err != nil { t.Fatal(err) } @@ -764,7 +740,7 @@ func TestModeSetTool_UsesNamedModeArgument(t *testing.T) { if _, err := modeSet.Call(context.Background(), `{"mode":"execute"}`); err != nil { t.Fatalf("mode_set rejected its named mode argument: %v", err) } - if mode := p.ModeForSession(mustSession(t, opts)); mode != "execute" { + if mode := p.Mode(mustSession(t, opts)); mode != "execute" { t.Errorf("expected execute mode, got %q", mode) } return