projection

package
v1.12.0 Latest Latest
Warning

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

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

Documentation

Overview

Package projection holds the run-start agent-config projection shared by every run-loop driver (the production dev driver and the harbortest devstack twin) and exercised directly by the control-plane integration test. Extracting it here keeps the projection logic in ONE place: the drivers live in separate binaries (cmd/harbor's package main cannot be imported by harbortest/devstack), so an inlined copy in each would drift (CLAUDE.md §17.6). The integration test calls the same function, so the test exercises the real projection rather than a test-local copy (CLAUDE.md §17.4).

Index

Constants

This section is empty.

Variables

View Source
var ErrReconcileRead = errors.New("agentcfg/projection: run-start reconcile active-revision read failed")

ErrReconcileRead wraps an active-revision read failure inside ReconcileConnections so the run-loop caller can distinguish a fail-loud read error (never a silent "detach nothing", CLAUDE.md §13) from a best-effort detach error.

View Source
var ErrSkillBodyMissing = errors.New("agentcfg/projection: agent-config pins a skill whose body is absent from the store")

ErrSkillBodyMissing is returned by ActiveSkillViews when an agent's active config pins an ADMIN skill-membership name whose body is absent from the store (e.g. the skill was hard-deleted, or a rollback landed on a revision referencing a since-deleted skill). An admin-pinned skill whose body is absent from the store is a LOUD failure at run-start projection — never a silent drop (CLAUDE.md §13). Session-personal skill names are exempt (a safe-subset add that may legitimately not be in the directory view).

Functions

func ActiveLLMOverrides

func ActiveLLMOverrides(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple) (*planner.LLMOverrides, error)

ActiveLLMOverrides resolves the PER-AGENT LLM-parameter override layer from the agent's active config revision at run start. It returns the per-agent sampling defaults (model / temperature / max-tokens / reasoning-effort) the agent has pinned, as a planner.LLMOverrides carrying ONLY those four dimensions — the layer the run loop folds BETWEEN the session override and the tenant-wide baseline (precedence session › per-agent › tenant-wide baseline › config default).

A nil registry, an empty agentID, an agent with no active revision, or an active revision with no LLM-params section returns (nil, nil) — the backward-compatible "no per-agent override" path. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to the tenant baseline on a read failure.

The active revision is read ONCE per run; the returned bundle is fresh (its pointers are copies), so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). `id` carries the run's identity; only the triple is used (the registry is identity-scoped, never keyed by run). This is sampling parameters only — ExtraInstructions / prompt layers are resolved elsewhere (the agent-config prompt-layer projection), so the per-agent LLM layer never carries prompt text.

func ActivePlannerCatalogView

func ActivePlannerCatalogView(ctx context.Context, reg agentcfg.Registry, ov sessionoverlay.Store, agentID string, id identity.Quadruple, cat tools.ToolCatalog, filter tools.CatalogFilter) (tools.PlannerCatalogView, error)

ActivePlannerCatalogView builds the run's planner-facing catalog view at run start, applying the agent's active-config tool exposure: a paused MCP server's tools and any individually-disabled tool are excluded from the view (next-turn projection — the live transport stays WARM). The exclusion set is the order-independent union of THREE narrow-only disable tiers — the admin baseline, the durable per-user disable set (which spans that user's sessions for the agent), and the ephemeral session overlay — so it can only ever grow: no tier can re-expose a tool a higher tier disabled. It always returns a usable view: a nil registry, an empty agentID, an agent with no active revision, or an active revision with no tool-exposure section (or an empty one) returns the plain tools.NewPlannerView over cat+filter — the backward-compatible "ungated" path. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to the unfiltered view on a read failure.

The active revision is read ONCE per run; the returned view is fresh, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

func ActivePromptLayers

func ActivePromptLayers(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple) (base string, user string, ok bool, err error)

ActivePromptLayers resolves the agent's active-config layered system prompt at run start. It returns the base + user layer text and whether the active revision carries a prompt-layer section (ok). A nil registry, an empty agentID, an agent with no active revision, or an active revision with no prompt-layer section returns ("", "", false, nil) — the backward-compatible "no durable prompt layers" path (the run keeps its configured base). A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through on a read failure.

An unset layer within a present section returns the empty string for that layer (the caller treats empty as "inherit the configured default" for the base, and "no user layer" for the user layer).

The active revision is read ONCE per run; the returned values are plain strings, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

func ActiveRunCompletionHook added in v1.10.0

func ActiveRunCompletionHook(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, yamlDefault *steering.CompletionHookSpec) (*steering.CompletionHookSpec, bool, error)

ActiveRunCompletionHook resolves the run-completion hook for a run at run start with next-run projection semantics. Resolution precedence is pinned here (and by a table test — CLAUDE.md §17.6): the agent-config `hooks` section (when PRESENT) over the static yaml default over no hook. The two run-loop drivers (the production dev driver and the harbortest devstack twin) call this ONE helper, so the precedence cannot drift between binaries.

A PRESENT hooks section is authoritative (mirrors the naming section): a run-completion hook with a non-empty tool pins it, while a present section with no/empty run-completion tool is an explicit per-agent NO-HOOK that WINS over yamlDefault (returns (nil, false)). Only an ABSENT (nil) hooks section falls through to yamlDefault — otherwise a per-agent opt-out of transcript egress would be silently discarded.

yamlDefault is the operator's static `runtime.hooks.run_completion` projection (nil when unset). The returned spec's AgentID is stamped from agentID (registration metadata, never an isolation key — §6). A nil registry, an empty agentID, or an agent with no active revision falls through to yamlDefault. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to yaml on a read failure.

The active revision is read ONCE per run; the returned spec is fresh, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

func ActiveSkillViews

func ActiveSkillViews(ctx context.Context, reg agentcfg.Registry, ov sessionoverlay.Store, agentID string, id identity.Quadruple, views []skills.SkillView) ([]skills.SkillView, error)

ActiveSkillViews applies an agent's active-config skills membership to the run's skill-directory views at run start. It resolves the agent's active revision once and, when the revision pins a skills section, keeps only the views whose name is in the membership set. A nil registry, an empty agentID, or an agent with no active revision / no skills section returns the views unchanged — the backward-compatible "ungated" path. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to the unfiltered view on a read failure.

The active revision is read ONCE per run; the returned slice is fresh, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). `id` carries the run's identity; only the triple is used (the registry is identity-scoped, never keyed by run).

func ApplyPromptLayers

func ApplyPromptLayers(ctx context.Context, reg agentcfg.Registry, overlayStore sessionoverlay.Store, agentID string, id identity.Quadruple, ov *planner.LLMOverrides) (*planner.LLMOverrides, error)

ApplyPromptLayers overlays the agent's active-config durable prompt layers onto the run's resolved per-run override bundle at run start. It is the ONE shared seam both run-loop drivers (the production dev driver and the harbortest devstack twin) call after resolving the LLM-parameter overrides, so the two binaries cannot drift (CLAUDE.md §17.6).

A non-empty base layer is set as ov.BasePromptLayer (overriding the run's configured base at the prompt builder); a non-empty user layer is set as ov.UserPromptLayer (composed below the base in the lower-trust position). An empty layer is treated as unset (the base inherits the configured default; no user layer is added) — so a run with no durable prompt layers is unchanged. When ov is nil but a layer is present, a fresh bundle is allocated. A registry read error is returned so the caller fails the run loudly. The returned bundle is fresh-per-run (no shared mutable state).

func EffectiveLoadingMode added in v1.10.0

func EffectiveLoadingMode(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, t tools.Tool, boot tools.LoadingMode) (tools.LoadingMode, error)

EffectiveLoadingMode resolves tool t's projected EFFECTIVE LoadingMode under agentID's ADMIN-tier active config revision — the SAME precedence ActivePlannerCatalogView applies at run start, exposed for the `tools.describe` read surface's optional `agent_id` path. boot is the boot-effective mode (already reflecting the driver default + any boot `tools.entries[].loading_mode`); a nil registry, an empty agentID, or an agent with no active revision / no tool-exposure section returns boot unchanged — the backward-compatible path byte-identical to `tools.describe` behaviour before this projection existed. A registry read error is returned so the caller fails the request loudly (CLAUDE.md §13).

func FilterSkillViewsByMembership

func FilterSkillViewsByMembership(views []skills.SkillView, names []string) []skills.SkillView

FilterSkillViewsByMembership keeps only the views whose Name is in the membership set. An empty membership keeps nothing (the rollback-to-empty case — an explicit empty skills section disables every skill for the next run). The returned slice is always freshly allocated.

func ReconcileConnections added in v1.11.0

func ReconcileConnections(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, detacher ConnectionDetacher, bootDeclared map[string]struct{}) (int, error)

ReconcileConnections is the DETACH leg of run-start reconciliation. It compares the agent's DECLARED runtime-added connections (the active config revision's connections section) against the ATTACHED set (the live MCP registry) and detaches every server that is attached but no longer declared AND not boot-declared — a connection removed via `agent_config.remove_mcp_connection`, or a server re-declared away by a rollback past an add. Detaching deregisters the server's tools from the catalog + MCP registry and closes its transport, so the NEXT run's projected catalog excludes them, the registry no longer lists it, and the subprocess drains. Boot-declared (yaml) servers are NEVER detached (they are not revisioned state); the bootDeclared set gates them out.

Honest in-flight semantics (read this before assuming isolation)

This is a projection-boundary act: it fires at run START, never in the middle of the calling run, and EXPOSURE correctness is next-turn — a removed server never appears in any catalog view projected after the removal revision. Physical TEARDOWN is a separate, process-global effect: the catalog and MCP registry are shared across sessions, so a reconcile triggered by one session's run-start can deregister a source and close its transport while ANOTHER session's run is mid-flight and about to call it. That in-flight run's prompt snapshot is unchanged, but its NEXT call to the detached server fails LOUDLY — a typed catalog not-found at dispatch, or a closed-transport error from the driver — never a hang, a panic, or a silent success. This is the same failure class as an operator stopping a boot-declared server mid-run, and it is the deliberate trade: a refcount/drain protocol was rejected as complexity the removal semantics do not need.

Serialisation and idempotency

Reconcile holds NO cross-run lock of its own: safety comes from the registry read being atomic, the detacher being idempotent (an already-detached source is a no-op), and each underlying primitive (catalog deregister, registry deregister, transport close) being internally synchronised. N concurrent reconciles converge to the same state; the concurrent tests pin this.

It is DETACH-ONLY by design: attaching a declared-but-absent server (the restart-survival reconcile) is a separate, deferred concern; the live add verb is the attach path, and re-add after remove reuses the persisted agent-bound token through that verb (no second consent). Two windows are accepted until the attach leg lands: a reconcile racing a concurrent re-add of the same name can detach the freshly re-added server (a stale declared-set read — heals at the next add or restart), and AttachedSources() is today a process-global enumeration (fine for the single-agent dev wiring; the future multi-agent attach leg must scope the attached set per agent).

A nil registry, an empty agentID, or a nil detacher returns (0, nil) — the backward-compatible "no reconcile" path. A registry read error is returned wrapped in ErrReconcileRead (fail loud, never swallowed). Detach errors are joined and returned (the caller logs them loud) but do not abort the diff: one server that refuses to close must not strand the others. Only the identity triple is used (the registry is identity-scoped, never keyed by run). Returns the number of servers detached.

func RunCompletionHookFromConfig added in v1.10.0

func RunCompletionHookFromConfig(rc config.RunCompletionHookConfig) *steering.CompletionHookSpec

RunCompletionHookFromConfig projects the static `runtime.hooks.run_completion` yaml block onto a steering.CompletionHookSpec, or nil when no static hook is configured (an empty / whitespace-only tool). It is the ONE yaml half of the hook resolution, shared by every run-loop driver (the production dev binary, the devstack twin, and the embed RunOnce path) so the yaml projection cannot drift between binaries. AgentID is left empty — the run-start resolution stamps the acting agent id; timeout defaulting happens at fire time.

Types

type ConnectionDetacher added in v1.11.0

type ConnectionDetacher interface {
	// AttachedSources returns the source ids currently live in the MCP
	// registry (the attached set the reconcile diffs against the declared
	// set). A fresh slice; the caller may retain it. NOTE: the concretes
	// today enumerate the PROCESS-GLOBAL registry — correct for the
	// single-agent dev wiring, but the future multi-agent attach leg must
	// scope the attached set to the reconciling agent or one agent's
	// reconcile would see (and detach) another agent's sources.
	AttachedSources(ctx context.Context) []string
	// Detach deregisters the named source's tools from the catalog + MCP
	// registry and closes its transport gracefully. Idempotent — a source
	// already gone is a no-op the concrete swallows.
	Detach(ctx context.Context, source string) error
}

ConnectionDetacher is the driver-agnostic seam the run-start reconciliation uses to detach a no-longer-declared runtime-added MCP server at the next-turn projection boundary. The concrete (wired at the cmd/harbor + devstack boundary) deregisters the source's tools from the planner catalog view + the MCP registry and closes its transport gracefully — the physical inverse of the add path's attach. Keeping it an injected interface preserves this package's §4.4 boundary: the projection imports no concrete MCP driver.

type LoadingResolverAdapter added in v1.10.0

type LoadingResolverAdapter struct {
	Registry agentcfg.Registry
}

LoadingResolverAdapter adapts a Registry into the `internal/tools/protocol` package's narrow LoadingResolver seam (the `tools.describe` optional `agent_id` path) via EffectiveLoadingMode, so the SAME projection precedence backs both the run-start prompt-time projection and the read surface — one shared helper, no binary-local reimplementation (CLAUDE.md §17.6). Satisfies `tools/protocol.LoadingResolver` structurally; this package does not import `tools/protocol` to avoid a needless dependency (Go interfaces are satisfied structurally).

func (LoadingResolverAdapter) EffectiveLoading added in v1.10.0

func (a LoadingResolverAdapter) EffectiveLoading(ctx context.Context, id identity.Identity, agentID string, t tools.Tool, boot tools.LoadingMode) (tools.LoadingMode, error)

EffectiveLoading implements the `tools/protocol.LoadingResolver` seam.

type NamingResolution added in v1.12.0

type NamingResolution struct {
	Policy steering.NamingPolicy
	Model  string
}

NamingResolution is the resolved session auto-naming policy for a run: the defaulted steering.NamingPolicy plus the model the naming call should request (empty = the run's effective model / the client default). Returned by ActiveNamingPolicy when a policy is active for the run.

func ActiveNamingPolicy added in v1.12.0

func ActiveNamingPolicy(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, yamlDefault config.RuntimeNamingConfig) (NamingResolution, bool, error)

ActiveNamingPolicy resolves the session auto-naming policy for a run at run start with next-run projection semantics. Resolution precedence is pinned here (and by a table test — CLAUDE.md §17.6): the agent-config `naming` section (when present) over the static yaml `runtime.naming` fleet default over off. The two run-loop drivers (production dev + devstack twin) call this ONE helper, so the precedence cannot drift between binaries.

A section is "active" only when its Auto is true — an agent-config section present with Auto false is an explicit per-agent off that WINS over the yaml default (returns active=false), and a config-free runtime resolves to off, so the opt-in invariant holds (no counters, no LLM calls, no events). The returned policy is defaulted (WithDefaults) so the trigger consumes concrete values.

A nil registry, an empty agentID, or an agent with no active revision / no naming section falls through to the yaml default. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through on a read failure. The active revision is read ONCE per run; the returned value is fresh (concurrent-reuse clean). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

Jump to

Keyboard shortcuts

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