Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 58 additions & 47 deletions agent/harness/agentmode/agentmode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity finding: Upstream AgentModeProvider.GetModeAsync(AgentSession session, ...) (dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs, lines ~180-194) calls Throw.IfNull(session) and throws ArgumentNullException when the session is null — the upstream port explicitly made the session parameter non-nullable/required for both GetModeAsync and SetModeAsync as part of graduating this API (microsoft/agent-framework#7052).

Go's new GetModeForSession(session *agent.Session) silently accepts a nil session and falls back to the configured default mode (via getSessionLockForSession/loadStateForSession returning the null-session lock and default state) instead of returning an error. This is inconsistent with the sibling SetModeForSession, which does return fmt.Errorf("agentmode: no session available") for a nil session, and it diverges from the upstream contract that requires a non-null session for both getters and setters.

Suggested resolution: either (a) have GetModeForSession return an error (or panic, consistent with Go idioms for programmer errors) when session == nil, mirroring SetModeForSession and the upstream non-nullable contract, or (b) if the permissive nil-session default-mode fallback is an intentional Go-specific ergonomic choice, document that divergence explicitly in the doc comment so it is not mistaken for an oversight.

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)
}
64 changes: 56 additions & 8 deletions agent/harness/agentmode/agentmode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}

Expand All @@ -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...)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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}"),
Expand Down
4 changes: 2 additions & 2 deletions docs/dotnet-go-sdk-feature-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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. |
Expand Down
Loading