compose

package
v2.8.0-dev.3 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package compose lifts reusable substrate wiring out of cmd/core-agent so library consumers can build the same agent stack the bundled binary does, per docs/compose-extraction-design.md (#386): substrate builders (compactor, agentic tools, MCP digest LLM fallback, context cache), operator-visible formatters, pricing operations, grant persistence, and multi-session construction (session factory, resumer, and authn wiring). Flag parsing and run() orchestration stay in the binary.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppendBuiltinAllowExtra

func AppendBuiltinAllowExtra(agentsDir, name string) error

AppendBuiltinAllowExtra adds name to .agents/config.json's permissions.builtin_allow_extras list. Idempotent — re-enabling a bundle that's already on is a no-op. Validation against the bundle catalog (permissions.KnownBundles) happens in the TUI before this is called, so an invalid name never reaches disk.

func AppendPathScope

func AppendPathScope(agentsDir, pattern string) error

AppendPathScope adds pattern to .agents/config.json's path_scope.allow list and rewrites the file atomically. If the file doesn't exist yet it is created with defaults so the addition has somewhere to live.

func AppendPathScopeEntry

func AppendPathScopeEntry(agentsDir, path, access string) error

AppendPathScopeEntry adds a typed entry to .agents/config.json's path_scope.allow_paths list and rewrites the file atomically. access is "r" / "w" / "rw" (permissions.ParseAccess grammar). Idempotent: an existing entry for the same path with the same access is a no-op; the same path with a different access is widened to the union of the two (a later rw grant upgrades an earlier r entry in place rather than appending a duplicate).

func AppendPermissionsAllow

func AppendPermissionsAllow(agentsDir string, patterns []string) error

AppendPermissionsAllow adds one or more patterns to .agents/config.json's permissions.allow list. Idempotent — duplicate patterns are skipped silently so /permissions can be re-run without growing the config file.

func AppendPermissionsDeny

func AppendPermissionsDeny(agentsDir string, patterns []string) error

AppendPermissionsDeny mirrors AppendPermissionsAllow for the deny list. Idempotent.

func BuildAgenticTools

func BuildAgenticTools(
	builtinTools []adktool.Tool,
	agentGetter func() *agent.Agent,
	provider models.Provider,
	smallModelID string,
) ([]adktool.Tool, error)

BuildAgenticTools constructs the agentic_* tool wrappers from docs/context-management-design.md Mechanism B. The wrappers call Agent.RunSubtask so raw tool output stays in the subtask and only the digest reaches the parent's context.

builtinTools is the already-constructed list (from tools.Build) — we pick out the canonical inner tools (read_file, fetch_url, grep, list_dir, glob) by name so the subtask shares the parent agent's permission gate and per-tool output caps. Wrappers whose required inner tool isn't in builtinTools are silently skipped (e.g. --no-builtin-tools, --disable-tools=read_file, or fetch_url disabled because url_scope.allow is empty).

agentGetter is the late-binding closure that resolves *Agent once agent.New finishes. provider+smallModelID is the cost- efficiency lever: when smallModelID is non-empty, the wrappers route subtasks through that cheaper model. Empty smallModelID means subtasks inherit the parent's model — the wrappers still work, just without the cost win.

func BuildCompactor

func BuildCompactor(cfg config.CompactionConfig) agent.Compactor

BuildCompactor constructs the auto-compaction trigger that the post-turn hook consults. Starts from the substrate's per-tier defaults (modeltier.DefaultCompactionThresholds) and the historical 0.85 fallback, then layers operator config overrides on top.

Resolution precedence for any one threshold lookup:

  1. cfg.ThresholdByTier[currentModelTier], when present
  2. The substrate per-tier default for that tier
  3. cfg.Threshold (single fallback), when set
  4. agent.DefaultCompactionThreshold

Operators who want to leave defaults alone provide an empty CompactionConfig — same behavior as agent.NewDefaultCompactor() returns directly.

func BuildMCPDigestLLMFallback

func BuildMCPDigestLLMFallback(
	agentRef **agent.Agent,
	provider models.Provider,
	modelID string,
) func(ctx context.Context, raw []byte) (mcp.LLMFallbackResult, error)

BuildMCPDigestLLMFallback constructs the closure that pkg/mcp invokes on responses the structural pruner can't reduce below threshold. Returns nil when the operator hasn't opted in — callers nil-check before setting DigestOptions.LLMFallback so the wrap stays on the shipped structural-only default.

Late binding via **agent.Agent: mcp.Build runs before agent.New (the toolsets need to be constructed to pass into the agent's options), so agentRef is populated by the WithPostConstruct hook after mcp.Build has already recorded the closure into DigestOptions.LLMFallback. The model can't invoke an MCP tool through the wrap before its first turn anyway, so the pointer is non-nil by the time the closure fires in practice.

modelID is resolved up-front so it appears in the startup log; the closure caches the adkmodel.LLM on first invocation and reuses it. Empty modelID → subagent inherits the parent's model (functionally correct, no cost win). SubagentModel on the returned result mirrors modelID verbatim so display-side pricing lookup uses the resolved tier, not the parent.

func BuildMultiSessionAuthn

func BuildMultiSessionAuthn(cfg config.MultiSessionConfig) (auth.Authenticator, auth.Caller, error)

BuildMultiSessionAuthn translates the operator's attach.multi_session config block into the pkg/auth Authenticator that the attach listener consults per-request. Returns:

  • authn: the resolved Authenticator (or nil for single-user mode)
  • fallback: the Caller stamped on requests that don't authenticate (used by the host's caller middleware as the no-cred default)
  • err: a fatal startup error if the config is internally inconsistent OR a referenced file can't be loaded

In single-user mode (multi_session.enabled = false), returns (nil, zero-Caller, nil) — the attach server defaults its own AnonymousAuth and the wiring is a no-op.

func BuildSessionFactory

func BuildSessionFactory(deps SessionFactoryDeps) attach.SessionFactory

BuildSessionFactory returns an attach.SessionFactory closure that constructs a fresh *agent.Agent per POST /sessions request. The closure captures the deps by value (slices + pointers); per-call it generates a unique sessionID, derives a per-session sub-gate + prompter, loads the per-caller instruction overlay, and assembles a minimal-but-functional agent.

The handler is responsible for calling RegisterOwned on the returned Registrant with the originating Caller.Identity — this factory deliberately does NOT register with the session registry itself, because that would self-register via the legacy Register() (no Owner stamp), losing the ACL ownership that's the whole point.

func BuildSessionResumer

func BuildSessionResumer(deps SessionFactoryDeps) attach.SessionResumer

BuildSessionResumer wires the attach server's SessionResumer. Reads the persisted ACL row from deps.ACLStore; materializes the original Caller from row.Owner; reconstructs the agent via ReproduceAgent with the EXPLICIT sessionID so ADK's session.Service reattaches the prior conversation history from the eventlog.

Returns nil when deps.ACLStore is nil — session-resume is opt-in. The attach server's Options.Resumer being nil leaves the legacy "Lookup miss = 404" behavior in place, no behavior change for pre-v2.5 deployments.

Resumer failures propagate to the registry; the registry's resumeAndRegister handles ErrSessionACLNotFound → ErrSessionNotFound translation. Other errors surface as 500 with the underlying cause (per docs/session-resume-design.md OQ #2).

func CfgToCatalogOverride

func CfgToCatalogOverride(m config.PricingMap) map[string]pricing.ModelRates

CfgToCatalogOverride translates config.PricingMap (the JSON-tagged per-model rate map operators put under model.pricing) into the internal/pricing wire shape. nil-safe; an empty map means "no cfg override, fall through to the file + builtin layers".

func DescribeRefresh

func DescribeRefresh(w io.Writer, out pricing.RefreshOutcome)

DescribeRefresh renders a one-line summary of a pricing-refresh outcome to w. Surfaces the four distinct shapes operators care about: fresh write (model count), 304-not-modified, skipped (cache still within MinInterval), network failure (cache age + error so the operator knows to expect stale rates).

func FormatStartupSummary

func FormatStartupSummary(in StartupSummaryInputs) []string

FormatStartupSummary produces the config-summary block emitted at daemon startup right after the config / instruction / MCP / skills resolution completes. Six lines, one per topic, in the standard core-agent: <topic>: <detail> shape. Callers wrap each returned line with the `send` helper defined in run().

Kept pure (no I/O beyond os.Getenv for the Vertex env-var summary) so it can be unit-tested by table-driven fixtures. Operators reading the daemon log see these lines FIRST (before "attach listener on" and the other established lines) — this is the "what did the daemon actually load" answer that was silent before #212.

func MaybeWireContextCache

func MaybeWireContextCache(
	ctx context.Context,
	provider models.Provider,
	cfg *config.Config,
	noContextCache bool,
	send func(string),
) *vertexcache.Manager

MaybeWireContextCache builds a vertexcache.Manager and installs its hooks on the provider when the following are all true:

  1. The provider is *gemini.Provider (concrete type — cache hooks live on that struct).
  2. Backend is Vertex (cfg.Model.Provider == "vertex").
  3. Caching is enabled in config (default ON; explicit enabled=false in cfg.Model.Vertex.ContextCache disables).
  4. The --no-context-cache CLI kill switch was NOT set.

Returns the manager on success (caller wires deferred Delete) or nil when caching was skipped for any reason. Never fails hard: if constructing the sibling genai.Client fails, the helper logs and returns nil — the agent still starts, just without caching.

func NewFilteredLogWriter

func NewFilteredLogWriter(w io.Writer) io.Writer

NewFilteredLogWriter wraps w in a writer that drops noisy log lines from genai/ADK that embedding surfaces don't want exposed. Today the only filtered lines are the `Error context canceled` / `Error context deadline exceeded` messages from genai's SSE scanner, which fire every time a turn is interrupted mid-stream (genai/api_client.go:484 log.Printf's it unconditionally).

Anything that isn't filtered passes through to w unchanged, so consumer-supplied log lines still appear. Typical use:

log.SetOutput(compose.NewFilteredLogWriter(os.Stderr))

func PersistModelChoice

func PersistModelChoice(agentsDir, modelID string) error

PersistModelChoice writes the new model name to .agents/config.json so /model survives across runs. Caller is responsible for first invoking the in-memory rebuild via tui.Options.RebuildAgent — this is purely the disk side.

func PersistThemeChoice

func PersistThemeChoice(agentsDir, themeName string) error

PersistThemeChoice writes the picker's selection to .agents/config.json so /theme survives across runs. Validates before save so a bad name surfaces as a picker error instead of silently corrupting the file (config.Save itself does not validate).

func RebuildPricingCatalog

func RebuildPricingCatalog(cfg *config.Config, agentsDir, coreHome string) error

RebuildPricingCatalog re-reads every pricing source and installs the fresh catalog into usage.SetCatalog. Called after /pricing refresh + /pricing set so subsequent cost lookups see the new rates without a process restart.

func RefreshPricing

func RefreshPricing(ctx context.Context, cfg *config.Config, agentsDir, coreHome string) (string, error)

RefreshPricing is the /pricing refresh slash callback. Forces an out-of-cycle fetch (MinInterval: -1s) regardless of how recently the daily refresh ran, rebuilds the catalog, and returns a summary line for the chat scrollback.

func RenderContextStats

func RenderContextStats(s agent.ContextStats, parentInputRate float64) string

RenderContextStats formats Agent.ContextStats as a multi-line SystemMessage for the /context (alias /boundaries) slash. Empty sections collapse to a one-line "no X yet" so a fresh session still gets a meaningful response. Format intentionally mirrors /stats' two-column key-value layout for visual parity.

parentInputRate is the parent model's per-million-token input rate (in USD), used to compute the "savings vs. no-digest baseline" dollar figure for the Digest savings block. Zero means "unknown" (fresh session, provider-less pricing, or an unpriced model) — the block still renders but omits the dollar figures.

func ReproduceAgent

func ReproduceAgent(deps SessionFactoryDeps, caller auth.Caller, sid string, origin string) (*attachadapter.Adapter, context.CancelFunc, error)

ReproduceAgent constructs an *agent.Agent under (caller, sid) using the shared SessionFactoryDeps shape. Used both by the on-demand session factory (sid is freshly minted) and by the resumer (sid comes from the persisted ACL row — ADK's session.Service reattaches the prior conversation history when the same triple opens the eventlog).

origin is "created" (factory path) or "resumed" (resumer path) and flows into the operator-visible stderr log line so the daemon log distinguishes the two.

Returns the constructed agent wrapped in its attach adapter (the registry entry the handler registers via RegisterOwnedWithCancel / registerResumed — recover the agent with Adapter.Agent()) + a CancelFunc that stops the per-session wake-loop goroutine. The caller hands the cancel to the registry so eviction terminates the loop cleanly instead of leaking it past the session's lifetime. The wake loop's ctx is derived from deps.DaemonCtx — either source of cancellation (daemon shutdown or per-session evict) closes ctx.Done and the loop exits.

func SetPricing

func SetPricing(cfg *config.Config, agentsDir, coreHome, model string, inputPerMTok, outputPerMTok float64) (string, error)

SetPricing is the /pricing set slash callback. Reads the user file, writes/updates the manual entry, saves atomically, then rebuilds the catalog so the rate takes effect immediately.

Types

type AttachOptions

type AttachOptions struct {
	Listen           string
	UnixSocket       string
	TLSCert          string
	TLSKey           string
	ClientCA         string
	TokenEnv         string
	ReadOnly         bool
	PeerHub          bool
	RegisterTo       string
	RegisterName     string
	RegisterEndpoint string
	// UI enables the /ui/* route on the attach listener serving the
	// mast-web operator UI. Uses the embedded bundle from
	// internal/webui (populated by dev/tools/fetch-mast-web at build
	// time) unless UIDir overrides with a local directory. CLI-only
	// in the bundled binary — the config file's attach block has no
	// counterpart, so BuildAttachOptions leaves both zero.
	UI    bool
	UIDir string
}

AttachOptions is the resolved attach-listener configuration a host feeds into its attach.Options / registration wiring. One value bundles what the bundled CLI exposes as eleven `--attach-*` flags plus the config file's `attach` block; library consumers usually fill it straight from BuildAttachOptions and overlay their own flag/env precedence on top (the bundled CLI keeps its CLI-beats- config overlay in package main, where flag.Visit lives).

func BuildAttachOptions

func BuildAttachOptions(cfg config.AttachConfig) AttachOptions

BuildAttachOptions translates the config file's `attach` block into an AttachOptions value, expanding ${ENV_VAR} references in every string field so paths and addresses can be parameterized per deployment ("$RUNTIME_DIR/agent.sock"). This is the config half of the bundled CLI's flag-vs-config merge; hosts with their own flag surface apply whatever precedence they want on top.

type ConfigGrantStore

type ConfigGrantStore struct {
	AgentsDir string
}

ConfigGrantStore persists "allow always" grants into .agents/config.json (permissions.allow / path_scope.allow_paths), atomically, via config.Load + config.Save. It is the reference permissions.GrantStore implementation — wire it with permissions.Options.GrantStore or Gate.SetGrantStore and the gate's DecisionAllowAlways path persists through it for every prompter (TUI, stdin, HTTP broker), closing the pre-v2.8 gap where only the bundled TUI's callbacks implemented the persistent half of the contract.

An empty AgentsDir makes Persist a no-op, matching the TUI's historical "no .agents dir resolved ⇒ fall back to allow-session" behavior. A present-but-unwritable dir errors — the operator asked to persist and it failed; the gate surfaces that to the call.

func (*ConfigGrantStore) Persist

Persist implements permissions.GrantStore. Path-scope grants land as typed path_scope.allow_paths entries carrying the grant's resolved access ("r" / "rw" after the gate's read→r / write→rw promotion) — NOT the legacy bare path_scope.allow list, whose entries reload as rw and would silently broaden a read-only grant on the next restart. Everything else lands in permissions.allow with the gate's fully-expanded "<tool>:<key>" pattern.

type SessionFactoryDeps

type SessionFactoryDeps struct {
	// DaemonCtx is the daemon's lifetime context — every per-session
	// wake loop spawned by the factory uses it as the cancellation
	// signal so SIGTERM / Ctrl-C ends them cleanly. Required.
	DaemonCtx context.Context

	Model          adkmodel.LLM
	Template       *permissions.Gate
	BuiltinTools   []adktool.Tool
	Toolsets       []adktool.Toolset
	EventlogHandle *eventlog.Handle
	PricingRate    usage.Pricing
	ProjectRoot    string
	UserRoot       string
	HomeAgentsDir  string
	AgentsDir      string
	UsersDir       string
	// EnvInterp is the ${env:VAR} interpolator wired from the daemon's
	// env manifest (see pkg/agentenv, #322). May be nil when the
	// bundle doesn't ship an env.yaml / env.json — loaders treat nil
	// as "no interpolation."
	EnvInterp func(string) string
	Registry  *attach.SessionRegistry
	// Cfg + MCPServers feed the read-only AttachXProvider closures
	// (memory / skills / mcp / pricing) so the per-session /memory,
	// /skills, /mcp, /pricing slash commands return real data
	// instead of "no servers configured" for on-demand sessions.
	Cfg        *config.Config
	MCPServers []*mcp.Server
	// ACLStore is the persistent ACL backing for session-resume
	// (Phase 2 of docs/session-resume-design.md). The factory
	// writes through it via RegisterOwned at session-creation time
	// (handled by the registry, not directly); the resumer reads
	// from it on Lookup miss to reconstruct evicted sessions. Nil
	// disables resume — the registry behaves as pre-v2.5.
	ACLStore attach.SessionACLStore
	// NoCompact / NoCheckpoint mirror the --no-compact /
	// --no-checkpoint CLI flags. When false (the default),
	// ReproduceAgent wires WithCompactor / WithCheckpointer so
	// /compact and /done work against session-created agents; when
	// true, the corresponding option is skipped so the disable flag
	// applies uniformly to the main agent AND every session-created
	// agent under it.
	NoCompact    bool
	NoCheckpoint bool
}

SessionFactoryDeps bundles the daemon-wide configuration the per-session SessionFactory closure needs to capture. Constructed once at daemon startup; the resulting factory builds fresh *agent.Agent values for each POST /sessions request.

Substrate + config-only operator features are wired per-session: tools, eventlog, per-session sub-gate, per-caller instruction overlay, per-session prompter, plus Compactor / Checkpointer / CostCeiling (these are pure config, so per-session reconstruction is trivial and the alternative was /compact and /done erroring on every session-created agent). Features that need per-session scoping decisions the daemon doesn't yet make (BackgroundManager sharing, Watchdog alert-sink routing, agentic tool wrappers, MCP custom auth) remain deferred — sessions created via POST /sessions see the substrate without them.

type StartupSummaryInputs

type StartupSummaryInputs struct {
	// CfgPath is the value of the -c / --config flag as passed on the
	// CLI. Empty means the daemon fell through to config discovery
	// (walk-up from cwd looking for .agents/config.json).
	CfgPath string
	// Cfg is the fully-resolved config (post CLI overrides, post
	// task-class tier fills).
	Cfg *config.Config
	// AgentsDir is the resolved .agents/ directory (from
	// filepath.Dir(cfgPath) when -c is set, else from config
	// discovery). Empty means "no agentsDir was found" — the daemon
	// still runs; MCP + skills + record_plan just have nowhere to
	// live.
	AgentsDir string
	// ProviderName is the concrete provider name after resolution
	// (vertex / gemini / anthropic / anthropic-vertex / echo /
	// scripted). Comes from provider.Name() at the call site.
	ProviderName string
	// MCPServers describes every MCP server the daemon successfully
	// or unsuccessfully started. mcp.Server carries the name +
	// Status + Err — this summary calls the ones with a nil Err
	// "ok" and the ones with Status != "" but Err != nil "failed".
	MCPServers []*mcp.Server
	// LoadedSkills describes the discovered skills — count + names
	// via LoadedSkills.Infos.
	LoadedSkills skills.Skills
}

StartupSummaryInputs bundles everything FormatStartupSummary needs. Keeping the input surface explicit (vs pulling from package-level state) is what makes the formatter unit-testable.

Jump to

Keyboard shortcuts

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