effects

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package effects implements the recorded-effect outbox (docs/durable-execution-design.md, "Recorded-effect outbox"): the runtime guard that makes mutating-tool re-execution ambiguity visible and blocking instead of silent, under mast's declared at-least-once contract.

The session event log IS the outbox — a durable FunctionCall event is the intent record, its paired FunctionResponse the completion record, both keyed by (session, invocation, function-call ID). This package adds no storage; it only reads history and refuses or replays calls.

The guard ships as an ADK runner plugin so it sits at the one seam every tool execution crosses (Flow.callTool wraps MCP, builtin, and federation tools alike). All history reads happen once per turn in BeforeRun, off the invocation context's session — the per-call tool context structurally has no session access (its Session() is unconditionally nil in ADK v2.1.0), so the per-call checks consult the turn-start snapshot instead. The permission gate's runtime wiring will share this layer, with the outbox check running first (a replayed result performs no new effect and needs no fresh approval).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CheckNameCollisions

func CheckNameCollisions(subAgents map[string]bool, pred Predicate, policies []ToolPolicy) []string

CheckNameCollisions reports composed sub-agent names that also name a mutating- or spawning-class tool. Such a name is ambiguous in the session log: ADK emits both a task delegation and a genuine tool call as a FunctionCall bearing the name, and the dangling scan excludes every FunctionCall named after a sub-agent (delegations are engine control flow, not effects — see Config.SubAgentNames). A real mutating tool sharing a sub-agent's name is therefore invisible to the outbox: a fail-open durability hole (gate finding N2). No scan-time heuristic can resolve the ambiguity — by name alone a delegation and a tool call are indistinguishable — so the collision must be fixed in the composition: rename the specialist or the tool. Both cmd/mast and the mast library entrypoints call this at construction and refuse on a non-empty result; a caller wiring its own root and outbox should do the same before New.

Only mutating/spawning tools matter: a read-only tool named after a sub-agent never dangles (scanHistory keeps only mutating/spawning unpaired calls), so a collision with one is harmless and is not reported.

Coverage is bounded by what is known by name at construction: mast's builtins, and the tool names an operator declared in tool_catalog.tools (classified through pred, so a nil-override entry still counts mutating by default-deny). tool_catalog.tools is an OVERRIDE list, not a tool inventory — its idiomatic use is to un-gate read-only tools, so a mutating tool an operator never listed there (the common case for MCP verbs) is not enumerable here and its collision is NOT caught. This reliably guards builtins and explicitly-declared tools; for everything else the authoring rule stands: do not name a specialist after a mutating tool. The returned names are sorted for a stable message.

func New

func New(cfg Config) (*plugin.Plugin, error)

New builds the outbox as an ADK runner plugin. Attach it via runner.Config.PluginConfig at every runner construction site.

func Overrides

func Overrides(logger *slog.Logger, policies []ToolPolicy) map[string]bool

Overrides flattens workload tool_catalog per-tool policies into the override map NewPredicate consumes, logging each applied override (the audit-logged requirement from the mutation predicate's definition). Later entries win on duplicate names; the workload loader rejects duplicates so that only matters for hand-built maps.

func SubAgentNames

func SubAgentNames(root agent.Agent) map[string]bool

SubAgentNames walks the agent tree from root and returns every composed agent's name — the delegation-call exclusion set for Config.SubAgentNames. The root's own name is included (harmless: a FunctionCall can never be named after the agent issuing it).

Types

type Class

type Class int

Class is a tool's effect classification under the mutation predicate (docs/orchestration-design.md, hitl_policy.on_mutation).

const (
	// ClassReadOnly tools never pay the outbox check.
	ClassReadOnly Class = iota

	// ClassMutating tools get intent/completion records in the log and
	// are refused in ambiguous-effect mode. Default for unknown tools:
	// MCP annotations are advisory and ADK v2.1.0's mcptoolset drops
	// them entirely (convertTool copies name/description/schemas only),
	// so default-deny-unknown is both the designed and the only
	// implementable stance; operators un-gate known-safe tools via the
	// workload tool_catalog override.
	ClassMutating

	// ClassSpawning tools start sub-runs whose inner tool calls this
	// process cannot individually guard from the spawn site (the
	// planner dispatch runner is a separate in-memory-session runner;
	// run_shape_* likewise when implemented). They carry no records of
	// their own but are refused in ambiguous-effect mode when they
	// arrive through the tool-execution seam. Note the containment
	// boundary: ADK's coordinator re-dispatch of an already-recorded
	// task delegation bypasses that seam — there, containment holds at
	// the inner-call level instead (the sub-run inherits the invocation
	// ID, so its own mutating tool calls are refused individually).
	ClassSpawning
)

type Config

type Config struct {
	// Predicate classifies tools; required.
	Predicate Predicate

	// SubAgentNames is the set of agent names composed under the
	// runner's root (see SubAgentNames). ADK's coordinator emits task
	// delegations as FunctionCalls NAMED AFTER THE SUB-AGENT, and
	// deliberately leaves them unresolved across user turns (a
	// specialist asking a clarifying question, a HITL pause inside a
	// node) — engine control flow, not effects. Without this exclusion
	// the scan wedges mast's default composition on its happy path.
	SubAgentNames map[string]bool

	// AckedAt returns the operator's effects-acknowledgement watermark
	// for a session, if one exists (pkg/transcript reads it from the
	// companion ops row). Dangling intents at or before the watermark
	// are considered operator-acknowledged and do not trip
	// ambiguous-effect mode. Optional; nil means no acks.
	AckedAt func(ctx context.Context, sessionID string) (time.Time, bool)

	// Logger for refusals, replays, and mode transitions. Optional.
	Logger *slog.Logger
}

Config configures the outbox plugin.

type DanglingIntent

type DanglingIntent struct {
	ToolName     string
	CallID       string
	InvocationID string
	Timestamp    time.Time
	// EventIndex is the 0-based position of the carrying event among log
	// events with non-nil Content, in log order. It lets a consumer group
	// dangling calls by the event that raised them — the auto-resume
	// repair path (cmd/mast, #41) answers only the calls of a single
	// (the last) function-call event, ADK's single-call-event validation
	// constraint. The outbox itself ignores it.
	EventIndex int
}

DanglingIntent is a durable mutating (or spawning) FunctionCall from a prior attempt with no completion in the log — the call may or may not have executed, and the window between an external effect committing and its completion event persisting cannot be closed, only detected.

type DanglingScan

type DanglingScan struct {
	// Mutating are unpaired mutating- or spawning-class calls: their
	// effect may or may not have committed, so a session with ANY of them
	// is ineligible for auto-resume and is left for an operator ack
	// (regardless of the effects ack watermark — an ack suppresses the
	// outbox refusal but does not pair the call, and synthesizing a
	// response would falsely assert the effect did not happen).
	Mutating []DanglingIntent
	// Repairable are unpaired ordinary read-only calls (a read-only tool
	// cut off mid-execution). They carry no external effect, so the
	// daemon may answer them with a synthetic error FunctionResponse to
	// make the history provider-valid before re-running.
	Repairable []DanglingIntent
	// Deferred are unpaired excluded calls (sub-agent task delegations,
	// and defensively any control/long-running call): engine-reconstruct
	// or operator territory. The daemon must not synthesize responses for
	// these; a candidate carrying any is skipped in slice-1.
	Deferred []DanglingIntent
	// LastCallEventIndex is the EventIndex of the last event carrying any
	// FunctionCall (paired or not), or -1 if none. Repair is only clean
	// when every Repairable call sits in this event (ADK validates a
	// repair message against the latest function-call event).
	LastCallEventIndex int
}

DanglingScan splits a session's unpaired FunctionCalls into the three buckets the boot-time auto-resume pass acts on (cmd/mast, #41). It is the once-and-only-once eligibility gate's single source of truth.

func ScanDangling

func ScanDangling(events session.Events, pred Predicate, subAgents map[string]bool) DanglingScan

ScanDangling classifies a session's unpaired FunctionCalls for the auto-resume eligibility and repair decisions. It shares pairScan with the outbox, so the two can never drift on what "dangling" means.

type Predicate

type Predicate func(toolName string) Class

Predicate classifies a tool by name. See NewPredicate.

func NewPredicate

func NewPredicate(overrides map[string]bool) Predicate

NewPredicate builds the mutation predicate: control surfaces are read-only, mast builtins use their registered class, per-tool overrides from the workload bundle apply next, and everything else — MCP tools included — defaults to mutating (default-deny-unknown).

type ToolPolicy

type ToolPolicy struct {
	Name     string
	Mutating *bool
}

ToolPolicy mirrors workload.ToolPolicy without importing pkg/workload (which would drag the YAML loader into every library embed that only wants the guard). The daemon and library root convert.

Jump to

Keyboard shortcuts

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