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
104 changes: 104 additions & 0 deletions agent/harness/harness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.

// Package harness provides a composed, cross-provider harness configuration
// aligned with the upstream .NET HarnessAgent surface.
package harness

import (
"slices"

"github.com/microsoft/agent-framework-go/agent"
"github.com/microsoft/agent-framework-go/agent/harness/agentmode"
"github.com/microsoft/agent-framework-go/agent/harness/loop"
"github.com/microsoft/agent-framework-go/agent/harness/todo"
"github.com/microsoft/agent-framework-go/agent/harness/toolapproval"
)

// DefaultInstructions are the built-in harness-level instructions applied when
// Config.DisableInstructions is false and Config.Instructions is empty.
const DefaultInstructions = `You are a helpful AI assistant that uses tools to complete tasks.

## General guidelines

- Think through the task before acting. Break complex work into clear steps.
- Use the tools available to you to gather information, perform actions, and verify results.
- Explain your reasoning and thought process as you work through tasks.
- Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.
- Avoid making more than 4 tool calls in a row without explaining what you are doing.
- If a tool call fails or returns unexpected results, adapt your approach rather than repeating the same call.
- When you have completed the task, present a clear and concise summary of what you did and what you found.`

// Config configures the composed harness behavior applied by [Configure].
type Config struct {
// Instructions overrides DefaultInstructions when non-empty.
Instructions string

// DisableInstructions omits harness-level instructions entirely.
DisableInstructions bool

// DisableTodoProvider omits the default todo tracking context provider.
DisableTodoProvider bool

// TodoOptions customizes the default todo provider when it is enabled.
TodoOptions *todo.Options

// DisableAgentModeProvider omits the default agent-mode context provider.
DisableAgentModeProvider bool

// AgentModeConfig customizes the default agent-mode provider when it is enabled.
AgentModeConfig agentmode.Config

// DisableToolApproval omits the default tool-approval middleware.
DisableToolApproval bool

// ToolApprovalConfig customizes the default tool-approval middleware when it is enabled.
ToolApprovalConfig toolapproval.Config

// LoopConfig enables loop middleware when non-nil and its Evaluators slice is
// non-empty. Evaluators must be non-nil; loop.New rejects nil evaluators at
// runtime. The loop middleware is appended before tool-approval middleware so
// loop reinvocation remains outside approval handling, mirroring the upstream
// .NET HarnessAgent ordering.
LoopConfig *loop.Config
}

// Configure returns a copy of cfg with the standard harness instructions,
// context providers, and middlewares applied.
//
// It is intended for composing provider-specific agent configs, for example:
//
// openaiprovider.AgentConfig{
// Config: harness.Configure(agent.Config{Name: "Research"}, harness.Config{}),
// Model: "gpt-5",
// }
func Configure(cfg agent.Config, harnessCfg Config) agent.Config {
out := cfg
out.RunOptions = slices.Clone(cfg.RunOptions)
out.ContextProviders = slices.Clone(cfg.ContextProviders)
out.Middlewares = slices.Clone(cfg.Middlewares)

if !harnessCfg.DisableInstructions {
instructions := DefaultInstructions
if harnessCfg.Instructions != "" {
instructions = harnessCfg.Instructions
}
out.RunOptions = append([]agent.Option{agent.WithInstructions(instructions)}, out.RunOptions...)
}

if !harnessCfg.DisableTodoProvider {
out.ContextProviders = append(out.ContextProviders, todo.New(harnessCfg.TodoOptions))
}
if !harnessCfg.DisableAgentModeProvider {
out.ContextProviders = append(out.ContextProviders, agentmode.New(harnessCfg.AgentModeConfig))
}
if harnessCfg.LoopConfig != nil && len(harnessCfg.LoopConfig.Evaluators) > 0 {
loopCfg := *harnessCfg.LoopConfig
loopCfg.Evaluators = slices.Clone(loopCfg.Evaluators)
out.Middlewares = append(out.Middlewares, loop.New(loopCfg))
}
Comment thread
michelle-clayton-work marked this conversation as resolved.
if !harnessCfg.DisableToolApproval {
out.Middlewares = append(out.Middlewares, toolapproval.New(harnessCfg.ToolApprovalConfig))
}

return out
}
17 changes: 17 additions & 0 deletions agent/harness/harness_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.

package harness_test

import (
"fmt"

"github.com/microsoft/agent-framework-go/agent"
"github.com/microsoft/agent-framework-go/agent/harness"
)

func ExampleConfigure() {
cfg := harness.Configure(agent.Config{Name: "ResearchAgent"}, harness.Config{})

fmt.Println(len(cfg.ContextProviders), len(cfg.Middlewares), len(cfg.RunOptions))
// Output: 2 1 1
}
186 changes: 186 additions & 0 deletions agent/harness/harness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright (c) Microsoft. All rights reserved.

package harness_test

import (
"context"
"iter"
"slices"
"testing"

"github.com/microsoft/agent-framework-go/agent"
"github.com/microsoft/agent-framework-go/agent/harness"
"github.com/microsoft/agent-framework-go/agent/harness/loop"
"github.com/microsoft/agent-framework-go/internal/agenttest"
"github.com/microsoft/agent-framework-go/message"
"github.com/microsoft/agent-framework-go/tool"
)

func TestConfigure_DefaultsApplyHarnessSurface(t *testing.T) {
var capturedMessages []*message.Message
var capturedInstructions []string
var capturedTools []tool.Tool

a := agent.New(agent.ProviderConfig{
ProviderName: "test",
Run: func(_ context.Context, msgs []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] {
capturedMessages = msgs
capturedInstructions = slices.Collect(agent.AllOptions(opts, agent.WithInstructions))
capturedTools = slices.Collect(agent.AllOptions(opts, agent.WithTool))
return func(yield func(*agent.ResponseUpdate, error) bool) {
yield(&agent.ResponseUpdate{
Role: message.RoleAssistant,
Contents: []message.Content{&message.TextContent{Text: "done"}},
}, nil)
}
},
}, harness.Configure(agent.Config{}, harness.Config{}))

_, err := a.RunText(t.Context(), "hello", agent.WithSession(agenttest.CreateSession())).Collect()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if got := textMessages(capturedMessages); !slices.Equal(got, []string{"hello", "### Current todo list\n- none yet"}) {
t.Fatalf("messages = %q, want hello plus todo summary", got)
}

if len(capturedTools) != 7 {
t.Fatalf("expected 7 harness tools, got %d", len(capturedTools))
}
for _, name := range []string{
"todos_add",
"todos_complete",
"todos_remove",
"todos_get_remaining",
"todos_get_all",
"mode_set",
"mode_get",
} {
if !hasToolNamed(capturedTools, name) {
t.Fatalf("expected tool %q to be configured", name)
}
}

if len(capturedInstructions) < 3 {
t.Fatalf("expected harness instructions plus provider instructions, got %d entries", len(capturedInstructions))
}
if capturedInstructions[0] != harness.DefaultInstructions {
t.Fatalf("first instructions = %q, want harness default instructions", capturedInstructions[0])
}
}

func TestConfigure_DisableFlagsOmitHarnessDefaults(t *testing.T) {
cfg := harness.Configure(agent.Config{}, harness.Config{
DisableInstructions: true,
DisableTodoProvider: true,
DisableAgentModeProvider: true,
DisableToolApproval: true,
LoopConfig: &loop.Config{},
})

if got := slices.Collect(agent.AllOptions(cfg.RunOptions, agent.WithInstructions)); len(got) != 0 {
t.Fatalf("expected no harness instructions, got %q", got)
}
if len(cfg.ContextProviders) != 0 {
t.Fatalf("expected no context providers, got %d", len(cfg.ContextProviders))
}
if len(cfg.Middlewares) != 0 {
t.Fatalf("expected no middlewares, got %d", len(cfg.Middlewares))
}
}

func TestConfigure_PrependsInstructionsAndAppendsToExistingConfig(t *testing.T) {
customProvider := agent.NewContextProvider(agent.ContextProviderConfig{SourceID: "custom"})
customMiddleware := agent.MiddlewareFunc(func(next agent.RunFunc, ctx context.Context, messages []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] {
return next(ctx, messages, opts...)
})

cfg := harness.Configure(agent.Config{
RunOptions: []agent.Option{agent.WithInstructions("custom instructions")},
ContextProviders: []agent.ContextProvider{customProvider},
Middlewares: []agent.Middleware{customMiddleware},
}, harness.Config{
Instructions: "harness instructions",
LoopConfig: &loop.Config{
Evaluators: []loop.Evaluator{
loop.EvaluatorFunc(func(context.Context, *loop.Context) (loop.Evaluation, error) {
return loop.Stop(), nil
}),
},
},
})

gotInstructions := slices.Collect(agent.AllOptions(cfg.RunOptions, agent.WithInstructions))
if !slices.Equal(gotInstructions[:2], []string{"harness instructions", "custom instructions"}) {
t.Fatalf("instructions = %q, want harness instructions before custom instructions", gotInstructions)
}
if len(cfg.ContextProviders) != 3 {
t.Fatalf("expected existing provider plus 2 harness providers, got %d", len(cfg.ContextProviders))
}
if len(cfg.Middlewares) != 3 {
t.Fatalf("expected existing middleware plus loop and tool-approval middleware, got %d", len(cfg.Middlewares))
}
}

func TestConfigure_LoopConfigReinvokesAgent(t *testing.T) {
var calls int
a := agent.New(agent.ProviderConfig{
ProviderName: "test",
Run: func(_ context.Context, _ []*message.Message, _ ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] {
calls++
text := "first"
if calls > 1 {
text = "second"
}
return func(yield func(*agent.ResponseUpdate, error) bool) {
yield(&agent.ResponseUpdate{
Role: message.RoleAssistant,
Contents: []message.Content{&message.TextContent{Text: text}},
}, nil)
}
},
}, harness.Configure(agent.Config{}, harness.Config{
DisableInstructions: true,
DisableTodoProvider: true,
DisableAgentModeProvider: true,
DisableToolApproval: true,
LoopConfig: &loop.Config{
Evaluators: []loop.Evaluator{
loop.EvaluatorFunc(func(_ context.Context, lc *loop.Context) (loop.Evaluation, error) {
if lc.Iteration == 1 {
return loop.Continue("try again"), nil
}
return loop.Stop(), nil
}),
},
},
}))

_, err := a.RunText(t.Context(), "hello", agent.WithSession(agenttest.CreateSession())).Collect()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if calls != 2 {
t.Fatalf("expected loop middleware to re-invoke the agent once, got %d calls", calls)
}
}

func textMessages(messages []*message.Message) []string {
var out []string
for _, msg := range messages {
if msg == nil || len(msg.Contents) == 0 {
continue
}
if text, ok := msg.Contents[0].(*message.TextContent); ok {
out = append(out, text.Text)
}
}
return out
}

func hasToolNamed(tools []tool.Tool, name string) bool {
return slices.ContainsFunc(tools, func(t tool.Tool) bool {
return t != nil && t.Name() == name
})
}
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`, `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 a top-level harness configurator plus packaged 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). |
| 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