From 5ec90375ea2d51e30f46a92cf25fd6e6ef777b86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:37:38 +0000 Subject: [PATCH] [dotnet-port-api] Align agentmode session helpers Port the AgentMode session-helper parity from microsoft/agent-framework#7052 by adding explicit session-based helper methods while preserving the existing option-based helpers as compatibility wrappers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/harness/agentmode/agentmode.go | 102 ++++++++++++---------- agent/harness/agentmode/agentmode_test.go | 55 ++++++++++-- docs/dotnet-go-sdk-feature-comparison.md | 4 +- 3 files changed, 107 insertions(+), 54 deletions(-) diff --git a/agent/harness/agentmode/agentmode.go b/agent/harness/agentmode/agentmode.go index 1496d954..3b7dc1f0 100644 --- a/agent/harness/agentmode/agentmode.go +++ b/agent/harness/agentmode/agentmode.go @@ -184,8 +184,12 @@ type Provider struct { // own, so a runtime cleanup deletes the entry once the session is collected, // keeping the registry from growing unbounded. func (p *Provider) getSessionLock(opts []agent.Option) *sync.Mutex { - session, ok := agent.GetOption(opts, agent.WithSession) - if !ok || session == nil { + session, _ := agent.GetOption(opts, agent.WithSession) + return p.getSessionLockForSession(session) +} + +func (p *Provider) getSessionLockForSession(session *agent.Session) *sync.Mutex { + if session == nil { return &p.nullSessionLock } key := weak.Make(session) @@ -204,17 +208,8 @@ func (p *Provider) getSessionLock(opts []agent.Option) *sync.Mutex { return actual.(*sync.Mutex) } -func (p *Provider) Invoking(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, []agent.Option, error) { - return p.provider.Invoking(ctx, invoking) -} - -func (p *Provider) Invoked(ctx context.Context, invoked agent.InvokedContext) error { - return p.provider.Invoked(ctx, invoked) -} - -func (p *Provider) loadState(opts []agent.Option) *state { - session, ok := agent.GetOption(opts, agent.WithSession) - if !ok { +func (p *Provider) loadStateForSession(session *agent.Session) *state { + if session == nil { return &state{CurrentMode: p.defaultMode} } var s state @@ -224,14 +219,31 @@ func (p *Provider) loadState(opts []agent.Option) *state { return &state{CurrentMode: p.defaultMode} } -func (p *Provider) saveState(opts []agent.Option, s *state) { - session, ok := agent.GetOption(opts, agent.WithSession) - if !ok || s == nil { +func (p *Provider) saveStateForSession(session *agent.Session, s *state) { + if session == nil || s == nil { return } session.Set(stateKey, *s) } +func (p *Provider) Invoking(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, []agent.Option, error) { + return p.provider.Invoking(ctx, invoking) +} + +func (p *Provider) Invoked(ctx context.Context, invoked agent.InvokedContext) error { + return p.provider.Invoked(ctx, invoked) +} + +func (p *Provider) loadState(opts []agent.Option) *state { + session, _ := agent.GetOption(opts, agent.WithSession) + return p.loadStateForSession(session) +} + +func (p *Provider) saveState(opts []agent.Option, s *state) { + session, _ := agent.GetOption(opts, agent.WithSession) + p.saveStateForSession(session, s) +} + func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, []agent.Option, error) { opts := invoking.Options @@ -324,46 +336,48 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { return []tool.FuncTool{setTool, getTool} } -// GetMode returns the current operating mode from the session. +// GetModeForSession returns the current operating mode from session state. // If no state has been persisted yet, it returns the configured default mode. -func (p *Provider) GetMode(opts ...agent.Option) string { - mu := p.getSessionLock(opts) +func (p *Provider) GetModeForSession(session *agent.Session) string { + mu := p.getSessionLockForSession(session) mu.Lock() defer mu.Unlock() - session, ok := agent.GetOption(opts, agent.WithSession) - if !ok { - return p.defaultMode - } - var s state - if found, _ := session.Get(stateKey, &s); found { - return s.CurrentMode - } - return p.defaultMode + return p.loadStateForSession(session).CurrentMode } -// SetMode sets the operating mode in the session, validating it against -// the provider's configured modes. Returns an error if the mode is invalid -// or no session is available. -func (p *Provider) SetMode(mode string, opts ...agent.Option) error { +// GetMode returns the current operating mode from the session option. +// If no state has been persisted yet, it returns the configured default mode. +func (p *Provider) GetMode(opts ...agent.Option) string { + session, _ := agent.GetOption(opts, agent.WithSession) + return p.GetModeForSession(session) +} + +// 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 { if _, ok := p.validModes[mode]; !ok { return fmt.Errorf("agentmode: invalid mode %q", mode) } - mu := p.getSessionLock(opts) + mu := p.getSessionLockForSession(session) mu.Lock() defer mu.Unlock() - session, ok := agent.GetOption(opts, agent.WithSession) - if !ok { + if session == nil { return fmt.Errorf("agentmode: no session available") } - var s state - if found, _ := session.Get(stateKey, &s); found { - if s.CurrentMode != mode { - s.PreviousMode = s.CurrentMode - s.CurrentMode = mode - } - } else { - s = state{CurrentMode: mode} + s := p.loadStateForSession(session) + if s.CurrentMode != mode { + s.PreviousMode = s.CurrentMode + s.CurrentMode = mode } - session.Set(stateKey, s) + p.saveStateForSession(session, s) return nil } + +// SetMode sets the operating mode in the session option, validating it against +// the provider's configured modes. Returns an error if the mode is invalid or +// no session is available. +func (p *Provider) SetMode(mode string, opts ...agent.Option) error { + session, _ := agent.GetOption(opts, agent.WithSession) + return p.SetModeForSession(session, mode) +} diff --git a/agent/harness/agentmode/agentmode_test.go b/agent/harness/agentmode/agentmode_test.go index 430d1caf..99fe2789 100644 --- a/agent/harness/agentmode/agentmode_test.go +++ b/agent/harness/agentmode/agentmode_test.go @@ -293,6 +293,7 @@ func TestDuplicateModeNames_Panics(t *testing.T) { func TestExternalModeChange_InjectsNotification(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() + session, _ := agent.GetOption(opts, agent.WithSession) msgs := newMessages("hi") // Initialize state. @@ -302,7 +303,7 @@ func TestExternalModeChange_InjectsNotification(t *testing.T) { } // Change mode externally. - if err := p.SetMode("execute", opts...); err != nil { + if err := p.SetModeForSession(session, "execute"); err != nil { t.Fatal(err) } @@ -328,10 +329,11 @@ func TestExternalModeChange_InjectsNotification(t *testing.T) { func TestExternalModeChange_NotificationClearedAfterFirstRead(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() + session, _ := agent.GetOption(opts, agent.WithSession) msgs := newMessages("hi") _, _, _ = invokeProvider(p, context.Background(), msgs, opts...) - _ = p.SetMode("execute", opts...) + _ = p.SetModeForSession(session, "execute") // First read: should have notification. outMessages, _, _ := invokeProvider(p, context.Background(), msgs, opts...) @@ -359,12 +361,13 @@ func TestExternalModeChange_NotificationClearedAfterFirstRead(t *testing.T) { func TestExternalModeChange_SameMode_NoNotification(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() + session, _ := agent.GetOption(opts, agent.WithSession) msgs := newMessages("hi") _, _, _ = invokeProvider(p, context.Background(), msgs, opts...) // Set to same mode. - _ = p.SetMode("plan", opts...) + _ = p.SetModeForSession(session, "plan") outMessages, _, _ := invokeProvider(p, context.Background(), msgs, opts...) for _, msg := range outMessages { @@ -455,7 +458,43 @@ func TestPublicGetMode_ReturnsDefaultMode(t *testing.T) { } } -// 20. PublicSetMode_ChangesMode +// 20. PublicGetModeForSession_ReturnsDefaultMode +func TestPublicGetModeForSession_ReturnsDefaultMode(t *testing.T) { + p := agentmode.New(agentmode.Config{}) + session := agenttest.CreateSession() + + if mode := p.GetModeForSession(session); mode != "plan" { + t.Errorf("expected 'plan', got %q", mode) + } +} + +// 21. PublicSetModeForSession_ChangesMode +func TestPublicSetModeForSession_ChangesMode(t *testing.T) { + p := agentmode.New(agentmode.Config{}) + session := agenttest.CreateSession() + + if err := p.SetModeForSession(session, "execute"); err != nil { + t.Fatal(err) + } + if mode := p.GetModeForSession(session); mode != "execute" { + t.Errorf("expected 'execute', got %q", mode) + } +} + +// 22. PublicSetModeForSession_NoSession_ReturnsError +func TestPublicSetModeForSession_NoSession_ReturnsError(t *testing.T) { + p := agentmode.New(agentmode.Config{}) + + err := p.SetModeForSession(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. PublicSetMode_ChangesMode func TestPublicSetMode_ChangesMode(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() @@ -468,7 +507,7 @@ func TestPublicSetMode_ChangesMode(t *testing.T) { } } -// 21. PublicSetMode_InvalidMode_Throws +// 24. PublicSetMode_InvalidMode_Throws func TestPublicSetMode_InvalidMode_ReturnsError(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() @@ -479,7 +518,7 @@ func TestPublicSetMode_InvalidMode_ReturnsError(t *testing.T) { } } -// 22. PublicSetMode_ReflectedInToolResults +// 25. PublicSetMode_ReflectedInToolResults func TestPublicSetMode_ReflectedInInstructions(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() @@ -497,7 +536,7 @@ func TestPublicSetMode_ReflectedInInstructions(t *testing.T) { } } -// 23. State_PersistsAcrossInvocations +// 26. State_PersistsAcrossInvocations func TestState_PersistsAcrossInvocations(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() @@ -521,7 +560,7 @@ func TestState_PersistsAcrossInvocations(t *testing.T) { } } -// 24. Options_CustomInstructions_OverridesDefault +// 27. Options_CustomInstructions_OverridesDefault func TestCustomInstructions_OverridesDefault(t *testing.T) { p := agentmode.New(agentmode.Config{ Instructions: "Custom instructions for mode {current_mode}", diff --git a/docs/dotnet-go-sdk-feature-comparison.md b/docs/dotnet-go-sdk-feature-comparison.md index 22f52a52..6516967e 100644 --- a/docs/dotnet-go-sdk-feature-comparison.md +++ b/docs/dotnet-go-sdk-feature-comparison.md @@ -68,7 +68,7 @@ Within overlapping features, the main misalignments are API shape and ecosystem | Logging | Microsoft.Extensions.Logging source-generated logs. | `slog` logger support through `agent.Config.Logger`, automatic agent run logs, and provider/middleware diagnostics. | Partial | Logging ecosystems differ. | | OpenTelemetry for agents | Agent/workflow observability samples and OpenTelemetry workflow builder extension. | `provider/otelprovider`, `workflow/observability/opentelemetry`, workflow builder instrumentation via `WithTelemetry`, trace context propagation in workflow context. | Aligned | API shape differs: Go passes a tracer from the OpenTelemetry adapter separately from `TelemetryOptions` and keeps workflow observability internals unexported. | | Evaluation | Agent evaluation extensions, eval checks, local/function evaluators, conversation splitters, workflow evaluation samples, Foundry quality samples. | No evaluation package. | .NET only | No Go equivalent found. | -| Harness utilities | Agent mode, file access, file memory, file store, subagents, todo, tool approval harness providers, loop harness. | `agent/harness/agentmode`, `agent/harness/todo`, `agent/harness/toolapproval`, `agent/harness/toolautocall`, `agent/harness/loop`. | Partial | Go now has packaged harness support for agent mode, todo tracking, tool approval, tool auto-call, and loop reinvocation with delegate/completion-marker evaluators. It still lacks file access, file memory, file store, subagent harness utilities, and the .NET AI-judge loop evaluator. Agent mode tool names (`mode_set`/`mode_get`), default instructions, and mode descriptions are aligned with .NET (#6071). | +| Harness utilities | Agent mode, file access, file memory, file store, subagents, todo, tool approval harness providers, loop harness. | `agent/harness/agentmode`, `agent/harness/todo`, `agent/harness/toolapproval`, `agent/harness/toolautocall`, `agent/harness/loop`. | Partial | Go now has packaged harness support for agent mode, todo tracking, tool approval, tool auto-call, and loop reinvocation with delegate/completion-marker evaluators. It still lacks file access, file memory, file store, subagent harness utilities, and the .NET AI-judge loop evaluator. Agent mode tool names (`mode_set`/`mode_get`), default instructions, mode descriptions, and session helper APIs are aligned with .NET (#6071, #7052). | | RAG | Basic text RAG, custom vector store RAG, custom data source RAG, Foundry service RAG, Neo4j graph RAG samples. | No RAG package or sample found. | .NET only | Go has data/file/vector content types but no RAG workflow package or samples. | | Purview | `Microsoft.Agents.AI.Purview` models and end-to-end sample. | No equivalent package. | .NET only | No Go governance/Purview integration. | | Cosmos DB storage | Cosmos chat history provider and workflow checkpoint store. | No built-in Cosmos package. | .NET only | Go has public in-memory and JSON/file workflow checkpoint stores plus a custom store interface, but no Cosmos DB provider. | @@ -177,7 +177,7 @@ The following Go packages and sample groups were present and accounted for in th | `agent` | Core agent runtime, options, sessions, history, context providers, middleware, responses. | | `agent/compaction` | Compaction provider, triggers, strategies, message indexing. | | `agent/format/jsonformat` | JSON response formats and schema helpers. | -| `agent/harness/agentmode` | Agent operating-mode context provider and mode-switching tools. | +| `agent/harness/agentmode` | Agent operating-mode context provider, session helpers, and mode-switching tools. | | `agent/harness/todo` | Todo-list context provider and todo management tools. | | `agent/harness/toolapproval` | Human-in-the-loop tool approval middleware with standing approval rules. | | `agent/harness/toolautocall` | Function-tool auto-calling and approval handling. |