Documentation
¶
Overview ¶
Package agent builds ADK agents from baifo's high-level Spec.
The Builder is the SINGLE place where agent.Agent instances are constructed. By going through Build, every agent automatically gets the secrets pipeline, audit logging, and (in later phases) the context guard plugin attached in the right order. Code outside this package must not call llmagent.New directly.
This file implements the context-trim BeforeModel plugin, wired via guardrails.trim_oversized_user_text in baifo.yaml.
Background: when a session is resumed under a different root agent name, the ADK's ConvertForeignEvent (internal/llminternal/contents_processor.go) rewrites the entire old transcript — including unbounded tool results — into user-role text parts prefixed with "For context: agent `exec` tool returned result: ...". Megabytes of binary data can enter the parent's context window and instantly trigger context-guard compaction, which distorts the token accounting the guard relies on.
Fix: this plugin runs its BeforeModelCallback before every model call and truncates any user-role text part whose byte length exceeds maxChars. The LLMRequest is rebuilt from session events each turn, so the mutation is purely ephemeral — the stored session keeps full fidelity.
Ordering matters: this plugin must be prepended to the runner's plugin list (before the contextguard plugin) so the guard's BeforeModel token counting sees the already-trimmed request.
Index ¶
- Constants
- Variables
- func BuildContextGuardConfig(summariserLLM model.LLM, rows []AgentGuardSpec) runner.PluginConfig
- func BuildContextTrimPlugin(maxChars int) *plugin.Plugin
- func GuardContextWindow(modelID string) int
- func GuardThreshold(window int) int
- func NormalizeReasoning(s string) string
- func NormalizeReasoningAPI(s string) string
- func ResolveMCPs(configured []string, allRegistered []string) []string
- func ResolveSkills(configured []string, allAvailable []string) []string
- func RunConfigForStreaming(streaming bool) agent.RunConfig
- func ValidReasoning(s string) bool
- func ValidReasoningAPI(s string) bool
- func WithContextTrim(cfg runner.PluginConfig, trim *plugin.Plugin) runner.PluginConfig
- type AgentGuardSpec
- type Builder
- type Instance
- type ReasoningConfig
- type Spec
Constants ¶
const ( // GuardStateRealTokensPrefix holds the last real prompt-token count // reported by the provider for this agent. Reset to 0 right after a // threshold compaction, so it doubles as the "used" gauge value. GuardStateRealTokensPrefix = "__context_guard_real_tokens_" // GuardStateSummaryPrefix holds the running conversation summary. // Non-empty once at least one compaction has happened. GuardStateSummaryPrefix = "__context_guard_summary_" // GuardStateSummarizedAtPrefix holds the token count recorded at the // last compaction. Used together with the summary length to build a // fingerprint the TUI diffs across turns to detect a fresh compaction. GuardStateSummarizedAtPrefix = "__context_guard_summarized_at_" // GuardStateContentsAtPrefix holds the Content count recorded at the // last sliding-window compaction. Used to compute turns since then. GuardStateContentsAtPrefix = "__context_guard_contents_at_compaction_" )
Session-state key prefixes mirror the (unexported) constants the contextguard plugin writes per agent. baifo reads them back to drive the TUI footer gauge and the "context compacted" notice. They are part of the plugin's observable contract; keep them in sync with adk-utils-go/plugin/contextguard. The agent name is appended to each prefix to form the full key.
const ( ReasoningOff = "off" ReasoningMinimal = "minimal" ReasoningLow = "low" ReasoningMedium = "medium" ReasoningHigh = "high" )
ReasoningEffort constants — the accepted values of the `reasoning` field. Empty string is the implicit "unset / model default".
const ( ReasoningAPIEnabled = "enabled" ReasoningAPIAdaptive = "adaptive" )
Anthropic reasoning API identifiers. These mirror the adk-utils anthropic adapter's ThinkingMode values and are what the builder forwards through providers.ModelOptions.ThinkingMode.
const GuardDefaultMaxTurns = 30
GuardDefaultMaxTurns is the sliding-window turn limit baifo applies when a context_guard block selects sliding_window without max_turns. Mirrored by both agentOptionsFor (what the plugin actually uses) and the TUI gauge so the displayed denominator matches reality.
Variables ¶
var ErrInvalidSpec = errors.New("invalid agent spec")
ErrInvalidSpec is returned when the Spec is missing required fields.
Functions ¶
func BuildContextGuardConfig ¶
func BuildContextGuardConfig(summariserLLM model.LLM, rows []AgentGuardSpec) runner.PluginConfig
BuildContextGuardConfig returns a runner.PluginConfig that, when attached to a runner, summarises any registered agent's conversation once it approaches its context-window threshold. Returns the zero value (no plugins) when no row asks for guarding, which lets runner.New treat it as a no-op.
summariserLLM is the model the plugin invokes to produce the summary. We use the ROOT LLM in baifo even for static-template agents because instantiating every worker at boot just to grab its LLM pointer would be heavy, and a single capable summariser is fine for what's essentially a "compress this transcript" job.
We use catwalk's embedded model database (via CrushRegistry) so each model's real context window is known without hitting the network at boot. Unknown models fall back to a conservative default (128k tokens / 4k output) inside the registry.
func BuildContextTrimPlugin ¶
BuildContextTrimPlugin returns an ADK plugin whose BeforeModelCallback truncates every user-role text part of the outgoing LLMRequest that exceeds maxChars bytes. Returns nil when maxChars <= 0 (trimming disabled), so callers can test for nil to skip wiring.
func GuardContextWindow ¶
GuardContextWindow returns the context window (in tokens) catwalk records for modelID, via the same registry the plugin consults. Unknown models fall back to the registry's conservative default.
func GuardThreshold ¶
GuardThreshold mirrors the plugin's computeBuffer logic: large windows (>= 200k) reserve a fixed 20k-token buffer, smaller ones reserve 20%. The threshold is the token count at which a threshold-strategy compaction fires, so it is the natural denominator for the gauge.
func NormalizeReasoning ¶
NormalizeReasoning lowercases and trims a reasoning value and maps the "off" alias (and "none") to the empty string, which the rest of the pipeline treats as "do not set". Returns the canonical effort or "".
func NormalizeReasoningAPI ¶
NormalizeReasoningAPI lowercases and trims an explicit reasoning-API override and maps it to a canonical value. Empty (and unrecognised input) returns "" meaning "no override, use the heuristic".
func ResolveMCPs ¶
ResolveMCPs implements the "empty list means all" rule for the root agent's MCP catalogue.
configured == nil or empty → return every MCP currently registered. The root is the coordinator of the whole system; it sees what the user has connected without having to maintain a redundant allowlist in baifo.yaml.
configured non-empty → respect it verbatim. Power users that want to hide a particular MCP from the root (e.g. a destructive one that should only be reachable through a specific static worker) still have a way to opt out by listing only the MCPs they want.
The function does NOT validate that configured names exist in the registry; the agent builder reports unknown names with a clearer error later, and that path is exercised by an existing test.
func ResolveSkills ¶
ResolveSkills mirrors ResolveMCPs for skills: empty list in the config means "load every skill baifo knows about", a non-empty list is a hard restriction.
Skills are non-mutating instructions (markdown files), so giving the root access to all of them is low-risk. Static workers keep their own narrow skill list because their job is narrower by design.
func RunConfigForStreaming ¶
RunConfigForStreaming maps a per-provider streaming preference onto the ADK RunConfig the executors and runners consume. Streaming (SSE) is the default and is required for long Anthropic turns; non-streaming is reserved for OpenAI-compatible endpoints that do not implement SSE. Centralised here so every call site agrees on the mapping.
func ValidReasoning ¶
ValidReasoning reports whether s is a recognised effort (after
func ValidReasoningAPI ¶
ValidReasoningAPI reports whether s is a recognised override value. Empty is valid: it means "no override".
func WithContextTrim ¶
func WithContextTrim(cfg runner.PluginConfig, trim *plugin.Plugin) runner.PluginConfig
WithContextTrim prepends the context-trim plugin to cfg.Plugins when trim is non-nil. Prepending matters: the contextguard's BeforeModel token counting must see the already-trimmed request.
Types ¶
type AgentGuardSpec ¶
type AgentGuardSpec struct {
// Name is the agent identifier the contextguard plugin keys
// summaries by. For the root, this is cfg.Root.Name. For
// static templates, this is the template's Name field.
Name string
// Config is the user-facing ContextGuardConfig from baifo.yaml
// for this agent. An Enabled=false config disables the row
// silently; the builder skips it.
Config config.ContextGuardConfig
}
AgentGuardSpec is one row in the BuildContextGuardConfig input. Each row registers the named agent with the contextguard plugin so it can summarise that agent's conversation when the configured threshold is hit. Used by both the root spec and every static template that opts into the plugin.
type Builder ¶
type Builder struct {
Providers *providers.Registry
Secrets *secrets.Store
Audit *audit.Recorder
// MCPs is optional. When nil, Spec.MCPs must be empty or Build
// returns an error. Provided as a field (not a parameter) so the
// rest of the wiring stays compact.
MCPs *mcps.Registry
// ModelWrapper, when non-nil, is given the LLM the providers
// registry produced and returns the one the agent will actually
// use. Used to install diagnostic wrappers (request dumpers,
// rate-limiters, ...) without sprinkling conditionals through the
// build path. A nil wrapper is a no-op (identity).
ModelWrapper func(model.LLM) model.LLM
// OpaqueTools lists tool names whose arguments must NOT be walked
// by the secrets expander. The canonical population is the spawn
// tool names (see internal/tools/spawn.OpaqueToolNames), which
// take agent specs as input. If the expander ran on them, a
// ${secret:NAME} placeholder embedded in the child's prompt or
// initial_message would be substituted eagerly with the raw value
// and baked into the child agent's prompt, bypassing the child's
// own allowlist. Leaving the placeholders intact lets the child's
// own BeforeToolCallback decide, at its own tool boundary, whether
// to expand (using its own, typically narrower, allower).
//
// nil or empty disables the guard. Callers should pass the list
// verbatim; the Builder converts it into a set internally.
OpaqueTools []string
// ScrubAllResponses controls the AfterToolCallback's
// comprehensive-redaction pass: when true (the recommended
// default), every tool result is scanned for ANY raw value
// currently stored in `Secrets`, not just the values the
// BeforeToolCallback substituted in this call. This closes the
// "tools that emit secrets they were not given" gap (think of an
// MCP echoing a debug header that contains a token from a
// different secret in the store).
//
// Set to false to disable the comprehensive pass, useful only
// for debugging or when the operator has profiled and found the
// scan dominates a hot path. The targeted pass (scrubbing values
// expanded in this call) remains active regardless.
ScrubAllResponses bool
// MinScrubLength is the minimum length (in bytes) a stored
// secret value must have to be eligible for the comprehensive
// pass. Below this floor the value is skipped to avoid
// catastrophic false positives: e.g. a stored value of "1234"
// would otherwise redact every "1234" anywhere in any tool
// result.
//
// The targeted pass is unaffected: short values stay protected
// when the LLM explicitly references them via ${secret:NAME}.
// Recommended floor is 8 bytes (real API keys are far longer);
// 0 or negative disables the filter entirely (every length goes
// through), which is documented as dangerous in CONFIG.md and
// SECRETS.md.
MinScrubLength int
}
Builder turns a Spec into a fully-wired Instance. The collaborators it depends on are fields rather than parameters of Build so the same builder can produce many agents during the life of the process.
type Instance ¶
type Instance struct {
ID string
Spec Spec
Agent agent.Agent
// LLM is the model the agent was built with. Exposed so the
// context-guard plugin can hand it to the contextguard library
// for summarisation. nil only when the agent has no LLM, which
// is currently impossible (Build returns ErrNoRoot in that case)
// but kept nullable to match agent.Agent's contract.
LLM model.LLM
}
Instance is the result of building an agent: the ADK Agent plus enough context for callers to identify it in audit records and TUI listings without crossing the ADK boundary.
type ReasoningConfig ¶
type ReasoningConfig struct {
// Request-level config (consumed by Gemini and OpenAI).
GenerateContentConfig *genai.GenerateContentConfig
// Construction-time config (consumed by Anthropic).
ModelOptions *providers.ModelOptions
}
ReasoningConfig encapsulates all the backend-specific reasoning settings generated from a single baifo effort string. The builder consumes this without having to know the provider-specific plumbing.
func BuildReasoningConfig ¶
func BuildReasoningConfig(modelID, effort, apiOverride string) ReasoningConfig
BuildReasoningConfig turns the raw effort and api override strings into concrete provider configs. Returns an empty ReasoningConfig if no effort was requested.
type Spec ¶
type Spec struct {
Name string
Description string
Prompt string
// Provider + Model resolve through the providers.Registry.
Provider string
Model string
// AllowedSecrets is the whitelist for the secret expander. The
// length encodes the policy:
//
// - nil or empty → the agent cannot dereference any secret
// (least-privilege default). Built sub-agents land here when
// the operator declared no allowed_secrets on a template, or
// when a dynamic spawn omitted the field.
// - non-empty → exactly those names are allowed.
//
// The ROOT agent does not go through this field. It is built
// with UnrestrictedSecrets=true and gets AllowAll instead, so it
// can dereference every secret the operator stored. See
// SECRETS.md for the threat-model rationale.
AllowedSecrets []string
// UnrestrictedSecrets, when true, replaces the per-name allowlist
// with AllowAll. Reserved for the root agent. Sub-agents must
// leave this false and use AllowedSecrets to enumerate exactly
// what they can see.
UnrestrictedSecrets bool
// MCPs lists the MCP names (as declared in baifo.yaml) whose tools
// should be exposed to this agent. The Builder resolves them via
// mcps.Registry.Tools.
MCPs []string
// Skills lists the skill slugs that should be available to this
// agent. Skills do not contribute tool.Tool entries yet (see the
// SKILL TOOLSET note in memory); reserved here so callers can
// already wire the field from baifo.yaml.
Skills []string
// Reasoning is the optional reasoning-effort knob: one of
// "minimal" / "low" / "medium" / "high" (empty = leave the model's
// own default). The builder turns it into a request-level
// GenerateContentConfig.ThinkingConfig (honoured by openai and
// gemini) AND, for anthropic, a construction-time thinking budget
// passed through providers.ModelOptions. Validation of the value
// lives at the config / spawn boundary; an unrecognised value here
// is treated as "unset" so a bad string never breaks agent
// construction.
Reasoning string
// ReasoningAPI optionally overrides which Anthropic reasoning API the
// model speaks: "enabled" (classic budget-based) or "adaptive"
// (effort-based, Opus 4.5+). Empty lets the builder auto-detect from
// the catwalk catalogue. Ignored by non-anthropic providers. An
// unrecognised value is treated as "unset".
ReasoningAPI string
// ExtraTools are additional tools to attach beyond what MCPs and
// Skills provide. Mostly useful for tests and for spawn/supervise
// machinery in future phases.
ExtraTools []tool.Tool
}
Spec is baifo's description of an agent, decoupled from ADK types so callers (config loader, spawn tools, ...) can build agents without importing the ADK directly.