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
88 changes: 47 additions & 41 deletions agent/harness/todo/todo.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"context"
"fmt"
"runtime"
"slices"
"strings"
"sync"
"weak"
Expand Down Expand Up @@ -133,37 +132,40 @@ 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)
return slices.Clone(st.Items)
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
}
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)
Expand All @@ -185,9 +187,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)
Expand All @@ -208,6 +209,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
Expand All @@ -222,9 +224,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
Expand All @@ -240,16 +242,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{
Expand All @@ -263,7 +266,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
},
)
Expand All @@ -274,10 +277,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{}{}
Expand All @@ -290,7 +293,7 @@ func (p *Provider) createTools(opts []agent.Option) []tool.FuncTool {
}
}
if completed > 0 {
p.saveState(opts, st)
p.saveState(session, st)
}
return completed, nil
},
Expand All @@ -302,10 +305,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{}{}
Expand All @@ -321,7 +324,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
},
Expand All @@ -333,11 +336,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
},
)

Expand All @@ -347,11 +346,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
},
)

Expand All @@ -368,6 +363,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"
Expand Down
55 changes: 54 additions & 1 deletion agent/harness/todo/todo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,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)
Expand All @@ -395,6 +413,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)
Expand All @@ -406,6 +447,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{
Expand Down Expand Up @@ -692,7 +742,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 {
Expand Down Expand Up @@ -721,6 +772,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()
Expand Down
2 changes: 1 addition & 1 deletion 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, 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. |
Expand Down
Loading