diff --git a/agent/harness/agentmode/agentmode.go b/agent/harness/agentmode/agentmode.go index a4a60239..7e2b1e02 100644 --- a/agent/harness/agentmode/agentmode.go +++ b/agent/harness/agentmode/agentmode.go @@ -187,8 +187,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) @@ -207,20 +211,8 @@ func (p *Provider) getSessionLock(opts []agent.Option) *sync.Mutex { return actual.(*sync.Mutex) } -// Invoking implements agent.ContextProvider by delegating to the wrapped provider, applying this provider's context/instructions to the invocation. -func (p *Provider) Invoking(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, []agent.Option, error) { - return p.provider.Invoking(ctx, invoking) -} - -// Invoked implements agent.ContextProvider by delegating to the wrapped provider. -// The wrapped provider is configured without a Store, so this is a no-op on success. -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 @@ -230,14 +222,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 +333,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 || session == nil { + 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 9b47d13d..1e7480d6 100644 --- a/agent/harness/agentmode/agentmode_test.go +++ b/agent/harness/agentmode/agentmode_test.go @@ -304,6 +304,10 @@ func TestDuplicateModeNames_Panics(t *testing.T) { func TestExternalModeChange_InjectsNotification(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() + session, ok := agent.GetOption(opts, agent.WithSession) + if !ok || session == nil { + t.Fatal("expected session option from sessionOpts()") + } msgs := newMessages("hi") // Initialize state. @@ -313,7 +317,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) } @@ -339,10 +343,14 @@ func TestExternalModeChange_InjectsNotification(t *testing.T) { func TestExternalModeChange_NotificationClearedAfterFirstRead(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() + session, ok := agent.GetOption(opts, agent.WithSession) + if !ok || session == nil { + t.Fatal("expected session option from sessionOpts()") + } 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...) @@ -370,12 +378,16 @@ func TestExternalModeChange_NotificationClearedAfterFirstRead(t *testing.T) { func TestExternalModeChange_SameMode_NoNotification(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() + session, ok := agent.GetOption(opts, agent.WithSession) + if !ok || session == nil { + t.Fatal("expected session option from sessionOpts()") + } 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 { @@ -466,7 +478,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() @@ -479,7 +527,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() @@ -499,7 +547,7 @@ func TestPublicSetMode_NilSession_ReturnsError(t *testing.T) { } } -// 22. PublicSetMode_ReflectedInToolResults +// 25. PublicSetMode_ReflectedInToolResults func TestPublicSetMode_ReflectedInInstructions(t *testing.T) { p := agentmode.New(agentmode.Config{}) opts := sessionOpts() @@ -517,7 +565,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() @@ -541,7 +589,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: new("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 97afef6b..a283f5cf 100644 --- a/docs/dotnet-go-sdk-feature-comparison.md +++ b/docs/dotnet-go-sdk-feature-comparison.md @@ -76,7 +76,7 @@ Intentional contract choices in this parity pass: | 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`; message injection is supplied through `agent.Config`. | Partial | Go now has packaged harness support for agent mode, todo tracking, tool approval, tool auto-call, message injection, 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`; message injection is supplied through `agent.Config`. | Partial | Go now has packaged harness support for agent mode, todo tracking, tool approval, tool auto-call, message injection, 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. | @@ -189,7 +189,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. |