Help us shape the future of Go! Take the 2026 Go Developer Survey

maf

package module
v0.1.0-preview.17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package maf wires AgentExperience into Microsoft Agent Framework for Go (github.com/microsoft/agent-framework-go): capture, verify, store and inject lessons, with one call on the agent's configuration.

store, err := inmemory.New()
if err != nil {
	return err
}
identity, err := ae.DevelopmentIdentity(ae.DevelopmentOptions{}) // development and tests only
if err != nil {
	return err
}
config := agent.Config{Name: "triage-ticket", Tools: []tool.Tool{runTests}}
if err := maf.UseAgentExperience(&config, maf.Options{Store: store, Identity: identity,
	Verify: ae.ToolSucceeded("run_tests"), ClassifyToolResult: ae.ExitCode("run_tests")}); err != nil {
	return err
}
triage := openaiprovider.NewChatCompletionsAgent(client, openaiprovider.AgentConfig{Config: config, Model: model})

(the package's Example compiles this).

The Go SDK builds an agent from its configuration once (agent.New, or a provider's constructor), so the one call changes the configuration before the agent is built. What it adds:

  • Capture, as agent middleware (outermost, so it wraps the history and context providers) plus function invocation middleware (outermost). Each invocation is one run of one attempt, streaming or not: every tool call with its arguments, its result and its error (an error the tool returned, recorded by its type name only; a panic, recorded by its value's type and re-raised; or a returned call the classifier named an error class for). When the invocation ends, Verify decides from the host's own facts; a verdict is finalized through the core (the deterministic lesson, the derived IDs) and stored. Without Verify nothing is stored.

  • Injection, as a context provider. It derives the task text from the invocation's messages (the contract's rule), searches the store in the identity's scope, ranks with the contract's weights, renders the Historical Reference block with BlockOptions, and adds it as one user-role message (MessageRole system sends it as a system message) just before the new input, stamped AdditionalProperties[HistoricalReferenceKey] = true.

  • Injection tracking (contract 1.4). Once the block is handed over, a store that records injections (an ae.InjectionRecorder: the PostgreSQL store, the in-memory store) is told which records it delivered, in a goroutine bounded by InjectionRecordTimeout, so the .NET lessons view shows when each lesson was last injected. It never delays or fails the invocation; the outcome is reported through OnInjectionRecorded, and ContextProviderOf(config).Close awaits the records still running, for a host shutting down.

  • Semantic retrieval (contract 1.6), with Options.Embedder. Injection then runs the text and the vector channel together, merges them as the contract says and ranks as before; every stored record's summary is embedded in the background (a failure is reported as StageIndex and never fails the capture). Without an embedder, or when the vector channel fails, the injection answers from text alone and says why (OnVectorFallback). To register one, implement ae.Embedder (ModelID, Dimension, Embed) over your provider's client and set Options.Embedder; no embedding model is a dependency of this module.

  • The injection holdout (contract 1.8), with Options.Holdout. On a deterministic share (Rate) of captured runs whose block names a record, the block is built and then withheld: no message, no exposure, no injection record, and OnWithheld is told what was withheld. Every assigned run, either arm, is recorded in the background through a store that is an ae.HoldoutRecorder (the PostgreSQL store, the in-memory store), so the .NET benefit estimate compares the runs of every language. Off by default (rate 0): nothing then changes.

  • The lessons view (contract 1.7), read-only. LessonsOf(&config) (or the provider's Lessons method, or NewLessons for the options alone) lists what has been learned in a scope: each lesson's status, confidence and checks, the block text an agent would be shown for it, why retrieval would not use it, and when it was last injected, from the same store, block options and model-authored flag the injection uses. It reads only, in the caller's own scope and authorization, and renders no HTML.

Nothing here returns an error or panics into the agent: a store, identity, classifier or verifier failure means no block, or no stored record, and is reported through OnInjectionFailure or OnCaptureFailure. Only the invocation's own errors and cancellation, and the tool's own errors and panics, reach the caller, unchanged. A panic through the invocation (the provider's, a middleware's, or one raised in the caller's own loop over the updates, which runs inside the iterator) ends its run Failed, recorded as panic(<type>), and goes on unchanged.

Example

The quick start with an OpenAI model: compiled, not run, as it needs a key. examples/quickstart runs the same setup with a scripted model and checks its output.

package main

import (
	"context"

	"github.com/microsoft/agent-framework-go/agent"
	"github.com/microsoft/agent-framework-go/provider/openaiprovider"
	"github.com/microsoft/agent-framework-go/tool"
	"github.com/openai/openai-go/v3"

	ae "github.com/fabbrik/AgentExperience.NET/go"
	"github.com/fabbrik/AgentExperience.NET/go/inmemory"
	"github.com/fabbrik/AgentExperience.NET/go/maf"
)

// quickStart is the README's quick start, word for word between its first and last line, so it keeps compiling.
func quickStart(ctx context.Context, client openai.Client, model string, runTests tool.FuncTool) error {
	store, err := inmemory.New()
	if err != nil {
		return err
	}
	identity, err := ae.DevelopmentIdentity(ae.DevelopmentOptions{}) // one local identity: development and tests only
	if err != nil {
		return err
	}
	config := agent.Config{Name: "triage-ticket", Tools: []tool.Tool{runTests}}
	if err := maf.UseAgentExperience(&config, maf.Options{Store: store, Identity: identity,
		Verify: ae.ToolSucceeded("run_tests"), ClassifyToolResult: ae.ExitCode("run_tests")}); err != nil { // your check, never the model
		return err
	}
	triage := openaiprovider.NewChatCompletionsAgent(client, openaiprovider.AgentConfig{Config: config, Model: model})
	if _, err := triage.RunText(ctx, "Ticket #4812: a refund is stuck on a lock. Triage it.").Collect(); err != nil {
		return err
	}
	_, err = triage.RunText(ctx, "Ticket #4977: a refund is stuck on a lock again. Triage it.").Collect() // gets the lesson
	return err
}

// The quick start with an OpenAI model: compiled, not run, as it needs a key. examples/quickstart runs the same setup
// with a scripted model and checks its output.
func main() {
	_ = quickStart
}

Index

Examples

Constants

View Source
const (
	// CanceledError is recorded when the caller's context was canceled.
	CanceledError = "context.Canceled"
	// DeadlineExceededError is recorded when the caller's context passed its deadline.
	DeadlineExceededError = "context.DeadlineExceeded"
	// StoppedError is recorded for an invocation whose caller stopped reading its updates before it ended.
	StoppedError = "agent.RunStopped"
)

The errors recorded for an invocation or a tool call that did not end on its own.

View Source
const DefaultFinalizationTimeout = 5 * time.Second

DefaultFinalizationTimeout is how long an invocation's end waits for finalization by default: .NET's FinalizationTimeout.

View Source
const DefaultInjectionRecordTimeout = 5 * time.Second

DefaultInjectionRecordTimeout is how long recording one injected block may take by default, all chunks together: .NET's.

View Source
const DefaultTaskID = "default"

DefaultTaskID is the task ID runs are captured under when none is given and the agent's name keeps nothing.

View Source
const DefaultVectorTimeout = 500 * time.Millisecond

DefaultVectorTimeout is how long the vector channel of one injection (embedding the task text, then searching the stored vectors) may take by default; past it the injection answers from text alone.

View Source
const HistoricalReferenceKey = "AgentExperience.HistoricalReference"

HistoricalReferenceKey is the AdditionalProperties key that stamps the injected message.

View Source
const MaxAbandonedInjectionRecords = 16

MaxAbandonedInjectionRecords is how many injection writes that outlived their timeout may still be running before another is not started: .NET's ExperienceInjectionLimits.MaxAbandonedReads default.

View Source
const MaxVectorCalls = 16

MaxVectorCalls is how many embedder or vector-store calls may be in flight at once, per UseAgentExperience call, abandoned (timed-out) ones included: a goroutine cannot be stopped, so a provider that never returns holds its slot. Past it a call fails at once (ProviderUnavailable, VectorSearchFailed), so a hung provider can never starve the text search or pile up goroutines.

View Source
const ProvenanceSource = "agentexperience.maf"

ProvenanceSource is the provenance source every run captured here is recorded with.

View Source
const SourceID = "agentexperience"

SourceID is the context provider's source ID, which the injected message's Source names.

Variables

View Source
var ErrAlreadyWired = errors.New("maf: UseAgentExperience was already called on this agent configuration")

ErrAlreadyWired is UseAgentExperience called a second time on one configuration.

View Source
var ErrLessonsNotAvailable = errors.New("maf: the lessons view needs a store that is an ae.LessonsReader, " +
	"and a configuration UseAgentExperience was called on")

ErrLessonsNotAvailable is a setup whose store cannot answer the lessons view (it is not an ae.LessonsReader; the PostgreSQL store and the in-memory store both are), or a configuration UseAgentExperience was not called on.

Functions

func DefaultEnvironment

func DefaultEnvironment() ae.EnvironmentFingerprint

DefaultEnvironment is the environment a run is recorded under by default: this host, this Go runtime, this operating system and architecture.

func LessonsOf

func LessonsOf(config *agent.Config) (*ae.Lessons, error)

LessonsOf is the lessons view of the setup UseAgentExperience made on config (injection disabled or not), over its store, with its block options and model-authored flag. ErrLessonsNotAvailable when config was not set up, or its store cannot answer the view.

func NewLessons

func NewLessons(options Options) (*ae.Lessons, error)

NewLessons is the lessons view for the options' store, block options, model-authored flag and clock (ae.Now when nil). Nothing else of the options is used, or validated. The store must be an ae.LessonsReader.

func UseAgentExperience

func UseAgentExperience(config *agent.Config, options Options) error

UseAgentExperience wires capture and injection into an agent's configuration, before the agent is built from it (agent.New, or a provider's constructor, which takes an agent.Config).

It puts the capture middleware first in config.Middlewares and the tool middleware first in config.FunctionMiddlewares, so both wrap everything else, and appends the context provider to config.ContextProviders, unless the configuration already holds a *ContextProvider (any one, deliberately by type: the agent then gets exactly one block) or DisableInjection is set. Everything the configuration already holds is kept. A second call on the same configuration is ErrAlreadyWired; invalid options are an error, and so is a development identity outside a development or test environment.

func Version

func Version() string

Version is this module's version as the build recorded it (runtime/debug.ReadBuildInfo: the main module's, or the dependency's), else "0.0.0-dev". It is recorded as the provenance source version of every run captured here.

func WaitForIndexWrites

func WaitForIndexWrites(ctx context.Context, config *agent.Config) error

WaitForIndexWrites waits for every background embedding write the configuration's agent started (after a record was stored) to end, or for ctx to be done, whichever is first, and returns ctx's error in the second case. Each write is bounded by FinalizationTimeout. It does nothing for a configuration UseAgentExperience was not called on.

Types

type CaptureFailure

type CaptureFailure struct {
	Stage  CaptureStage
	Reason string
	// RunID is the run's ID, once it started.
	RunID *ae.UUID
	Err   error
}

CaptureFailure is a capture problem, reported once per stage per run. Reason never quotes record content; Err is the error (or the recovered panic, as an error), when there was one.

type CaptureStage

type CaptureStage string

CaptureStage names where capture failed.

const (
	StageResolveIdentity          CaptureStage = "ResolveIdentity"
	StageStartRun                 CaptureStage = "StartRun"
	StageRecordExposure           CaptureStage = "RecordExposure"
	StageToolCall                 CaptureStage = "ToolCall"
	StageToolResultClassification CaptureStage = "ToolResultClassification"
	StageAppendAttempt            CaptureStage = "AppendAttempt"
	StageCompleteRun              CaptureStage = "CompleteRun"
	StageVerify                   CaptureStage = "Verify"
	StageFinalize                 CaptureStage = "Finalize"
	StageStore                    CaptureStage = "Store"
	// StageIndex is the background embedding write after a record was stored (contract 1.6): the record stays
	// stored, only its vector is missing.
	StageIndex CaptureStage = "Index"
)

The capture stages.

type ContextProvider

type ContextProvider struct {
	// contains filtered or unexported fields
}

ContextProvider injects the lessons that apply to an invocation, before the model is called: the task text from the invocation's messages, the text search in the identity's scope, the contract's ranking, and the rendered block as one message just before the new input. It never returns an error or panics into the agent; a failure means no block and is reported through OnInjectionFailure.

The messages it sees are the agent's history (a history provider always runs before the context providers in the Go SDK) and then the new input; history counts as replayed history for the task text, so a short follow-up ("and retry") is searched joined after the previous request, as .NET's ChatClientAgent-level provider does. A block a history provider replays (the SDK persists context messages with the request) is dropped, so each request carries exactly one block, and the block never counts as a request.

Once a block is handed over, the records it delivered are recorded as injected (contract 1.4) in a goroutine; Close awaits the ones still running.

func ContextProviderOf

func ContextProviderOf(config *agent.Config) *ContextProvider

ContextProviderOf is the ContextProvider UseAgentExperience added to config (or one the configuration already held), or nil when it has none.

func NewContextProvider

func NewContextProvider(options Options) (*ContextProvider, error)

NewContextProvider is the injection alone, to wire by hand (UseAgentExperience adds one). Without the capture middleware of the same UseAgentExperience call it resolves the identity itself, and records no exposure.

func (*ContextProvider) AbandonedInjectionRecords

func (p *ContextProvider) AbandonedInjectionRecords() int

AbandonedInjectionRecords is how many injection writes that outlived their timeout are still running against the store.

func (*ContextProvider) Close

func (p *ContextProvider) Close(ctx context.Context) error

Close waits for every injection record the provider started, and every background embedding write its configuration started, to end, or for ctx to be done, whichever is first, and returns ctx's error in the second case. Each record is bounded by InjectionRecordTimeout, so a host shutting down can call it with a context of a few seconds. A store write that overran that bound was abandoned when its record ended and is not awaited: it may still be running (AbandonedInjectionRecords counts them). Close may run concurrently with invocations, and the provider stays usable; a record started after Close began may or may not be awaited.

func (*ContextProvider) Invoked

Invoked does nothing: capture is the agent middleware's.

func (*ContextProvider) Invoking

func (p *ContextProvider) Invoking(ctx context.Context, invoking agent.InvokingContext) (messages []*message.Message,
	options []agent.Option, err error)

Invoking adds the block, when one applies. It never returns an error.

func (*ContextProvider) Lessons

func (p *ContextProvider) Lessons() (*ae.Lessons, error)

Lessons is the lessons view over the provider's store, with its block options and model-authored flag. List a scope with Lessons.List(ctx, bounds, scope, query), or an identity's with Lessons.ListFor.

type HoldoutOptions

type HoldoutOptions struct {
	// Rate is the share of eligible runs whose block is withheld, from 0 (off, the default) to 1 (every one). NaN, a
	// negative rate or one above 1 is refused by UseAgentExperience.
	Rate float64
	// Salt is mixed into every assignment, so two experiments over the same runs draw different arms;
	// ae.DefaultHoldoutSalt when empty. (Go cannot tell an empty salt from an unset one, so empty means the default
	// here, where .NET and Python refuse an explicitly empty salt.) Changing it re-draws every run.
	Salt string
}

HoldoutOptions are the injection holdout (contract 1.8). The zero value is off.

Who takes part: only an invocation captured by the same UseAgentExperience call (whose run started), whose built block names at least one record. The arm is ae.AssignHoldout(run ID, Salt, Rate), a pure function, so every invocation of a run lands in the same arm, and the arm is the one the .NET and Python integrations assign. A held-out invocation gets no block (the block is built first, so what is withheld is exactly what would have been delivered): no message, no exposure on the run, no injection record; the inject outcome is Withheld and OnWithheld is told. A treatment invocation is injected exactly as without the holdout. Both arms are recorded in the background (the record_holdout operation, OnHoldoutRecorded), and never delay or fail the invocation. This port has no withdrawal notices, so a held-out invocation never owes any.

type HoldoutRecordResult

type HoldoutRecordResult struct {
	Outcome       InjectionRecordOutcome
	RunID         ae.UUID
	Arm           ae.HoldoutArm
	ExperienceIDs []ae.UUID
	Err           error
}

HoldoutRecordResult is how recording one holdout assignment ended (contract 1.8), with the outcomes .NET reports for its record_holdout operation (InjectionRecorded, InjectionNotTracked, InjectionNotRecorded, InjectionOutcomeUnknown): the run, its arm, the records its block named, and the error, when there was one (never record content).

type IdentityContext

type IdentityContext struct {
	Agent   *agent.Agent
	Session *agent.Session
}

IdentityContext is what an identity resolver is given: the agent and the caller's session. Never model output.

type InjectionFailure

type InjectionFailure struct {
	Reason string
	Err    error
}

InjectionFailure is why an invocation got no block: the reason (never record content), and the error, when there was one.

type InjectionRecordOutcome

type InjectionRecordOutcome string

InjectionRecordOutcome is how recording an injected block ended, as .NET reports its record_injection operation.

const (
	// InjectionRecorded is every chunk recorded.
	InjectionRecorded InjectionRecordOutcome = "Recorded"
	// InjectionNotTracked is a store that does not record injections (not an ae.InjectionRecorder).
	InjectionNotTracked InjectionRecordOutcome = "NotTracked"
	// InjectionNotRecorded is a store that refused or failed (a missing grant included), or too many earlier writes
	// still running.
	InjectionNotRecorded InjectionRecordOutcome = "NotRecorded"
	// InjectionOutcomeUnknown is a write that ran out of time or was cancelled; it may still commit.
	InjectionOutcomeUnknown InjectionRecordOutcome = "OutcomeUnknown"
)

type InjectionRecordResult

type InjectionRecordResult struct {
	Outcome       InjectionRecordOutcome
	ExperienceIDs []ae.UUID
	Err           error
}

InjectionRecordResult is how recording one injected block ended: the outcome, the records the block delivered, and the error, when there was one (never record content).

type InjectionResult

type InjectionResult struct {
	ExperienceIDs  []ae.UUID
	ByteCount      int
	Truncated      bool
	VectorFallback *ae.VectorFallback
	// WithheldExperienceIDs are the records a held-out invocation's block named and did not deliver (contract 1.8):
	// set only in what OnWithheld is told, where ExperienceIDs is empty and ByteCount 0.
	WithheldExperienceIDs []ae.UUID
}

InjectionResult is what one injection delivered: the records in the block, best first, the block's size in UTF-8 bytes, whether the search returned more candidates than the ranking considers, and, when the vector channel did not contribute (contract 1.6), why: nil when it did.

type Options

type Options struct {
	// Store is required: the in-memory store (the inmemory module) for development, or the PostgreSQL store.
	Store ae.Store
	// Identity is who every invocation runs for: ae.DevelopmentIdentity for development and tests, or one from your
	// own authentication. Exactly one of Identity and ResolveIdentity is given.
	Identity ae.Identity
	// ResolveIdentity resolves the identity per invocation from your own authentication; nil (with no error) leaves
	// the invocation neither captured nor injected. A development identity it returns is checked every time.
	ResolveIdentity func(ctx context.Context, identity IdentityContext) (*ae.Identity, error)
	// Verify decides from your own facts whether a run is stored (see ae.ToolSucceeded and the others); without it
	// nothing is stored.
	Verify ae.Verifier
	// ClassifyToolResult marks a returned tool call as failed (see ae.ExitCode and ae.HTTPStatus).
	ClassifyToolResult ae.ToolResultClassifier
	// TaskID is the task ID runs are captured under; when empty, the agent's name reduced to an identifier, else
	// DefaultTaskID.
	TaskID string
	// Sanitization is ae.DefaultSanitization() when zero: only allowlisted argument values are kept.
	Sanitization ae.Sanitization
	// BlockOptions are the block's layout and limits (the contract's defaults when zero).
	BlockOptions ae.BlockOptions
	// Environment is the environment runs are recorded under; DefaultEnvironment() when nil.
	Environment *ae.EnvironmentFingerprint
	// PreferredEnvironment is the metadata ranking prefers.
	PreferredEnvironment map[string]string
	// ExcludeModelAuthored leaves model-authored lessons out of the search and the ranking.
	ExcludeModelAuthored bool
	// MessageRole is the injected message's role: message.RoleUser (when empty) or message.RoleSystem.
	MessageRole message.Role
	// DisableInjection captures without injecting.
	DisableInjection bool
	// OnCaptureFailure, OnInjectionFailure, OnInjected, OnFinalized and OnInjectionRecorded are told what happened;
	// each is optional, and a panic in one is recovered and ignored. OnInjectionRecorded is called from the
	// recording's own goroutine.
	OnCaptureFailure    func(CaptureFailure)
	OnInjectionFailure  func(InjectionFailure)
	OnInjected          func(InjectionResult)
	OnFinalized         func(ae.FinalizedExperience)
	OnInjectionRecorded func(InjectionRecordResult)
	// Holdout is the injection holdout (contract 1.8); off when zero.
	Holdout HoldoutOptions
	// OnWithheld is told, for a held-out invocation, what its block withheld (InjectionResult.WithheldExperienceIDs);
	// OnHoldoutRecorded how recording each assignment ended, from the recording's own goroutine. Each is optional, and a
	// panic in one is recovered and ignored.
	OnWithheld        func(InjectionResult)
	OnHoldoutRecorded func(HoldoutRecordResult)
	// Clock is ae.Now when nil.
	Clock func() ae.Instant
	// NewID is ae.NewRandomUUID when nil.
	NewID func() ae.UUID
	// Embedder (contract 1.6) turns text into a vector, for hybrid retrieval and for embedding each stored record.
	// With one, injection runs the text and the vector channel together (a store that is an ae.EmbeddingIndex serves
	// it: the in-memory store, and the PostgreSQL store with pgvector), merges them and ranks as before, and every
	// stored record's summary is embedded in the background. Without one, or when the vector channel fails, the
	// injection answers from text alone and says why (OnVectorFallback, InjectionResult.VectorFallback). The
	// embedder's calls honour ctx, are bounded by VectorTimeout, and at most MaxVectorCalls run at once, abandoned
	// ones included.
	Embedder ae.Embedder
	// MinVectorRelevance, when set (0 to 1), drops vector candidates whose relevance is below it; text candidates
	// never are.
	MinVectorRelevance *float64
	// VectorTimeout bounds the vector channel of one injection in all (embedding the task text, then searching):
	// past it the injection answers from text alone and reports ProviderUnavailable or VectorSearchFailed.
	// DefaultVectorTimeout when 0.
	VectorTimeout time.Duration
	// OnVectorFallback is told, once per injection, why the vector channel did not contribute (NotConfigured,
	// ProviderUnavailable, ModelMismatch, DimensionMismatch, VectorSearchFailed); optional, and a panic in it is
	// recovered and ignored. Its detail never quotes record or vector content.
	OnVectorFallback func(ae.VectorFallback)
	// FinalizationTimeout bounds how long an invocation's end waits for finalization (append, verify, finalize,
	// store), on its own clock, never the caller's context: past it the work is abandoned and reported, and the
	// invocation returns. DefaultFinalizationTimeout when 0.
	FinalizationTimeout time.Duration
	// InjectionRecordTimeout bounds how long recording one injected block may take in the background, all chunks
	// together: past it the write is abandoned and reported InjectionOutcomeUnknown. The invocation never waits for
	// it. DefaultInjectionRecordTimeout when 0.
	InjectionRecordTimeout time.Duration
}

Options configure UseAgentExperience (and NewContextProvider).

Directories

Path Synopsis
examples
quickstart command
Command quickstart is the Go quick start: the README's setup, run twice, with a scripted stand-in for the model.
Command quickstart is the Go quick start: the README's setup, run twice, with a scripted stand-in for the model.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL