From 988c7cbed597c2cdc5be343a924514e786f92218 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:25:08 +0000 Subject: [PATCH] [dotnet-port-api] Add TodoProvider session helpers Port the session-first TodoProvider read helpers from upstream .NET so Go exposes direct session-based access alongside the existing option-based helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/harness/todo/todo.go | 89 +++++++++++++----------- agent/harness/todo/todo_test.go | 55 ++++++++++++++- docs/dotnet-go-sdk-feature-comparison.md | 2 +- 3 files changed, 102 insertions(+), 44 deletions(-) diff --git a/agent/harness/todo/todo.go b/agent/harness/todo/todo.go index 73df2df0..d96bb70e 100644 --- a/agent/harness/todo/todo.go +++ b/agent/harness/todo/todo.go @@ -124,29 +124,31 @@ func (p *Provider) Invoked(ctx context.Context, invoked agent.InvokedContext) er // GetAllItems returns all todo items from the session state. func (p *Provider) GetAllItems(opts ...agent.Option) []Item { - mu := p.getSessionLock(opts) - mu.Lock() - defer mu.Unlock() - st := p.loadState(opts) - result := make([]Item, len(st.Items)) - copy(result, st.Items) - return result + return p.GetAllTodos(sessionFromOptions(opts)) } // GetRemainingItems returns only the incomplete todo items from the session state. func (p *Provider) GetRemainingItems(opts ...agent.Option) []Item { - mu := p.getSessionLock(opts) + return p.GetRemainingTodos(sessionFromOptions(opts)) +} + +// GetAllTodos returns all todo items stored in session. +func (p *Provider) GetAllTodos(session *agent.Session) []Item { + mu := p.getSessionLock(session) mu.Lock() defer mu.Unlock() - st := p.loadState(opts) - return remainingItems(st.Items) + return copyItems(p.loadState(session).Items) } -func (p *Provider) loadState(opts []agent.Option) *state { - session, ok := agent.GetOption(opts, agent.WithSession) - if !ok { - return &state{} - } +// GetRemainingTodos returns only the incomplete todo items stored in session. +func (p *Provider) GetRemainingTodos(session *agent.Session) []Item { + mu := p.getSessionLock(session) + mu.Lock() + defer mu.Unlock() + return remainingItems(p.loadState(session).Items) +} + +func (p *Provider) loadState(session *agent.Session) *state { var s state if found, _ := session.Get(stateKey, &s); found { return &s @@ -154,9 +156,8 @@ func (p *Provider) loadState(opts []agent.Option) *state { return &state{} } -func (p *Provider) saveState(opts []agent.Option, s *state) { - session, ok := agent.GetOption(opts, agent.WithSession) - if !ok || s == nil { +func (p *Provider) saveState(session *agent.Session, s *state) { + if session == nil || s == nil { return } session.Set(stateKey, *s) @@ -178,9 +179,8 @@ func (p *Provider) saveState(opts []agent.Option, s *state) { // Weak keys do not keep sessions alive and do not remove map entries on their // 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 { +func (p *Provider) getSessionLock(session *agent.Session) *sync.Mutex { + if session == nil { return &p.nullSessionLock } key := weak.Make(session) @@ -201,6 +201,7 @@ func (p *Provider) getSessionLock(opts []agent.Option) *sync.Mutex { func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, []agent.Option, error) { opts := invoking.Options + session := sessionFromOptions(opts) tools := p.createTools(opts) var outOpts []agent.Option @@ -215,9 +216,9 @@ func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext) // Inject current todo list summary so the agent sees outstanding work. if !p.suppressTodoMessage { - mu := p.getSessionLock(opts) + mu := p.getSessionLock(session) mu.Lock() - st := p.loadState(opts) + st := p.loadState(session) mu.Unlock() var todoMsg string @@ -233,16 +234,17 @@ func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext) } func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { + session := sessionFromOptions(opts) addTool := functool.MustNew( functool.Config{ Name: "todos_add", Description: "Add one or more todo items. Each item has a title and an optional description. Returns the list of created todo items.", }, func(ctx context.Context, input []ItemInput) ([]Item, error) { - mu := p.getSessionLock(opts) + mu := p.getSessionLock(session) mu.Lock() defer mu.Unlock() - st := p.loadState(opts) + st := p.loadState(session) var created []Item for _, in := range input { item := Item{ @@ -256,7 +258,7 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { st.Items = append(st.Items, item) created = append(created, item) } - p.saveState(opts, st) + p.saveState(session, st) return created, nil }, ) @@ -267,10 +269,10 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { Description: "Mark one or more todo items as complete. Each entry has an ID and a reason describing how/why the item was completed. Returns the number of items that were found and marked complete.", }, func(ctx context.Context, items []CompleteInput) (int, error) { - mu := p.getSessionLock(opts) + mu := p.getSessionLock(session) mu.Lock() defer mu.Unlock() - st := p.loadState(opts) + st := p.loadState(session) idSet := make(map[int]struct{}, len(items)) for _, item := range items { idSet[item.ID] = struct{}{} @@ -283,7 +285,7 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { } } if completed > 0 { - p.saveState(opts, st) + p.saveState(session, st) } return completed, nil }, @@ -295,10 +297,10 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { Description: "Remove one or more todo items by their IDs. Returns the number of items that were found and removed.", }, func(ctx context.Context, ids []int) (int, error) { - mu := p.getSessionLock(opts) + mu := p.getSessionLock(session) mu.Lock() defer mu.Unlock() - st := p.loadState(opts) + st := p.loadState(session) idSet := make(map[int]struct{}, len(ids)) for _, id := range ids { idSet[id] = struct{}{} @@ -314,7 +316,7 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { } if removed > 0 { st.Items = remaining - p.saveState(opts, st) + p.saveState(session, st) } return removed, nil }, @@ -326,11 +328,7 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { Description: "Retrieve the list of incomplete todo items.", }, func(ctx context.Context, _ struct{}) ([]Item, error) { - mu := p.getSessionLock(opts) - mu.Lock() - defer mu.Unlock() - st := p.loadState(opts) - return remainingItems(st.Items), nil + return p.GetRemainingTodos(session), nil }, ) @@ -340,11 +338,7 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool { Description: "Retrieve the full list of todo items, both complete and incomplete.", }, func(ctx context.Context, _ struct{}) ([]Item, error) { - mu := p.getSessionLock(opts) - mu.Lock() - defer mu.Unlock() - st := p.loadState(opts) - return st.Items, nil + return p.GetAllTodos(session), nil }, ) @@ -361,6 +355,17 @@ func remainingItems(items []Item) []Item { return remaining } +func copyItems(items []Item) []Item { + result := make([]Item, len(items)) + copy(result, items) + return result +} + +func sessionFromOptions(opts []agent.Option) *agent.Session { + session, _ := agent.GetOption(opts, agent.WithSession) + return session +} + func formatTodoListMessage(items []Item) string { if len(items) == 0 { return "### Current todo list\n- none yet" diff --git a/agent/harness/todo/todo_test.go b/agent/harness/todo/todo_test.go index 5209486a..8d77395b 100644 --- a/agent/harness/todo/todo_test.go +++ b/agent/harness/todo/todo_test.go @@ -362,6 +362,24 @@ func TestPublicGetAllTodos_ReturnsAllItems(t *testing.T) { } } +func TestPublicGetAllTodosFromSession_ReturnsAllItems(t *testing.T) { + p := todo.New(nil) + session := agenttest.CreateSession() + opts := []agent.Option{agent.WithSession(session)} + + _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + + callTool(t, outOpts, "todos_add", `{"Arg0":[{"title":"X"},{"title":"Y"}]}`) + + all := p.GetAllTodos(session) + if len(all) != 2 { + t.Fatalf("expected 2 items, got %d", len(all)) + } +} + // 14. PublicGetRemainingTodos_ReturnsOnlyIncomplete func TestPublicGetRemainingTodos_ReturnsOnlyIncomplete(t *testing.T) { p := todo.New(nil) @@ -385,6 +403,29 @@ func TestPublicGetRemainingTodos_ReturnsOnlyIncomplete(t *testing.T) { } } +func TestPublicGetRemainingTodosFromSession_ReturnsOnlyIncomplete(t *testing.T) { + p := todo.New(nil) + session := agenttest.CreateSession() + opts := []agent.Option{agent.WithSession(session)} + + _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) + if err != nil { + t.Fatal(err) + } + + callTool(t, outOpts, "todos_add", `{"Arg0":[{"title":"Done"},{"title":"Open"}]}`) + items := p.GetAllTodos(session) + callTool(t, outOpts, "todos_complete", fmt.Sprintf(`{"Arg0":[{"id":%d,"reason":"done"}]}`, items[0].ID)) + + remaining := p.GetRemainingTodos(session) + if len(remaining) != 1 { + t.Fatalf("expected 1 remaining, got %d", len(remaining)) + } + if remaining[0].Title != "Open" { + t.Errorf("expected 'Open', got %q", remaining[0].Title) + } +} + // 15. PublicGetAllTodos_ReturnsEmptyForNewSession func TestPublicGetAllTodos_ReturnsEmptyForNewSession(t *testing.T) { p := todo.New(nil) @@ -396,6 +437,15 @@ func TestPublicGetAllTodos_ReturnsEmptyForNewSession(t *testing.T) { } } +func TestPublicGetAllTodosFromNilSession_ReturnsEmpty(t *testing.T) { + p := todo.New(nil) + + items := p.GetAllTodos(nil) + if len(items) != 0 { + t.Fatalf("expected 0 items for nil session, got %d", len(items)) + } +} + // 16. Options_CustomInstructions_OverridesDefault func TestCustomInstructions_OverridesDefault(t *testing.T) { p := todo.New(&todo.Options{ @@ -682,7 +732,8 @@ func TestCompleteTodos_EmptyReasonIsAccepted(t *testing.T) { // state. Run under -race. func TestTodo_ConcurrentSessionAccess_NoDataRace(t *testing.T) { p := todo.New(nil) - opts := sessionOpts() + session := agenttest.CreateSession() + opts := []agent.Option{agent.WithSession(session)} _, outOpts, err := invokeProvider(p, context.Background(), newMessages("hi"), opts...) if err != nil { @@ -711,6 +762,8 @@ func TestTodo_ConcurrentSessionAccess_NoDataRace(t *testing.T) { defer wg.Done() _ = p.GetAllItems(opts...) _ = p.GetRemainingItems(opts...) + _ = p.GetAllTodos(session) + _ = p.GetRemainingTodos(session) }(i*2 + 1) } wg.Wait() diff --git a/docs/dotnet-go-sdk-feature-comparison.md b/docs/dotnet-go-sdk-feature-comparison.md index 22f52a52..1e45a63c 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, and mode descriptions are aligned with .NET (#6071), and the todo provider now exposes matching session-first todo read helpers (#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. |