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 ¶
- func AppendBuiltinAllowExtra(agentsDir, name string) error
- func AppendPathScope(agentsDir, pattern string) error
- func AppendPathScopeEntry(agentsDir, path, access string) error
- func AppendPermissionsAllow(agentsDir string, patterns []string) error
- func AppendPermissionsDeny(agentsDir string, patterns []string) error
- func BuildAgenticTools(builtinTools []adktool.Tool, agentGetter func() *agent.Agent, ...) ([]adktool.Tool, error)
- func BuildCompactor(cfg config.CompactionConfig) agent.Compactor
- func BuildMCPDigestLLMFallback(agentRef **agent.Agent, provider models.Provider, modelID string) func(ctx context.Context, raw []byte) (mcp.LLMFallbackResult, error)
- func BuildMultiSessionAuthn(cfg config.MultiSessionConfig) (auth.Authenticator, auth.Caller, error)
- func BuildSessionFactory(deps SessionFactoryDeps) attach.SessionFactory
- func BuildSessionResumer(deps SessionFactoryDeps) attach.SessionResumer
- func CfgToCatalogOverride(m config.PricingMap) map[string]pricing.ModelRates
- func DescribeRefresh(w io.Writer, out pricing.RefreshOutcome)
- func FormatStartupSummary(in StartupSummaryInputs) []string
- func MaybeWireContextCache(ctx context.Context, provider models.Provider, cfg *config.Config, ...) *vertexcache.Manager
- func NewFilteredLogWriter(w io.Writer) io.Writer
- func PersistModelChoice(agentsDir, modelID string) error
- func PersistThemeChoice(agentsDir, themeName string) error
- func RebuildPricingCatalog(cfg *config.Config, agentsDir, coreHome string) error
- func RefreshPricing(ctx context.Context, cfg *config.Config, agentsDir, coreHome string) (string, error)
- func RenderContextStats(s agent.ContextStats, parentInputRate float64) string
- func ReproduceAgent(deps SessionFactoryDeps, caller auth.Caller, sid string, origin string) (*attachadapter.Adapter, context.CancelFunc, error)
- func SetPricing(cfg *config.Config, agentsDir, coreHome, model string, ...) (string, error)
- type AttachOptions
- type ConfigGrantStore
- type SessionFactoryDeps
- type StartupSummaryInputs
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AppendBuiltinAllowExtra ¶
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 ¶
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 ¶
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 ¶
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 ¶
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:
- cfg.ThresholdByTier[currentModelTier], when present
- The substrate per-tier default for that tier
- cfg.Threshold (single fallback), when set
- 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:
- The provider is *gemini.Provider (concrete type — cache hooks live on that struct).
- Backend is Vertex (cfg.Model.Provider == "vertex").
- Caching is enabled in config (default ON; explicit enabled=false in cfg.Model.Vertex.ContextCache disables).
- 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
func (s *ConfigGrantStore) Persist(_ context.Context, g permissions.Grant) error
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.