intent

package
v0.18.1 Latest Latest
Warning

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

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

Documentation

Overview

Package intent implements governance R3 — the intent-alignment policy check.

The MUST from the governance framework: every action is evaluated against a policy that considers both the action itself AND its alignment with the stated agent intent. Forge's pre-R3 policy evaluation (guardrails, egress, admission) covered the "action itself" half — this package fills the second half with a per-tool- call cosine-similarity check between the user's stated intent and the tool call the LLM is about to make.

Wire shape:

  1. On tasks/send entry, the A2A handler calls Engine.RegisterIntent with the task ID + the first user message text. The engine computes and caches the intent embedding once per task.

  2. On every BeforeToolExec hook, the runner calls Engine.Score with the task ID + tool description + tool-input JSON. The engine embeds the concatenated action text (cached per hash), computes cosine similarity against the intent embedding, compares against the configured thresholds, and returns a Result.

  3. The hook consumer emits the Result as an `intent_alignment` audit event and, on DecisionDeny, aborts the tool call.

Fail-closed posture: when Config.Enabled is true, an unavailable embedder (config error, transient network) causes Score to return DecisionDeny with reason="embedder unavailable". Governance- critical: silent bypass is not a valid failure mode.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Enabled turns the check on. Default false. When false, the hook
	// short-circuits — no embedder calls, no audit emit.
	Enabled bool

	// Threshold is the SOFT similarity floor. Scores strictly below
	// threshold but at-or-above HardThreshold produce DecisionWarn.
	// Sensible default 0.5 — operators SHOULD start warn-only for a
	// sprint to gather the score distribution before turning deny on.
	Threshold float64

	// HardThreshold is the HARD floor. Scores strictly below produce
	// DecisionDeny. Set equal to Threshold to disable the warn tier;
	// set to a negative value (e.g. -1) to run warn-only during the
	// initial rollout. Sensible default 0.3.
	HardThreshold float64

	// CacheSize is the max number of action-side embeddings to
	// remember in the LRU. Zero disables the action cache (still
	// caches the per-task intent). 1024 is a reasonable default.
	CacheSize int

	// IntentTTL controls how long a task's intent embedding is
	// kept in memory before it's evicted. Zero → 1 hour default.
	IntentTTL time.Duration
}

Config carries the operator-tunable knobs for the alignment check. Populated from forge.yaml security.intent_alignment. See docs/security/intent-alignment.md.

func (Config) Validate

func (c Config) Validate() error

Validate returns an error when Config's values would produce nonsensical decisions. Called at Engine construction so the runner fails startup rather than at first Score.

type Decision

type Decision int

Decision is the alignment engine's contribution to the policy evaluation. Maps 1:1 to runtime.PolicyDecision but the intent package doesn't import runtime (avoid a cycle: intent → runtime via memory → intent later); the runner adapts.

const (
	// DecisionAllow — score at or above the soft threshold.
	DecisionAllow Decision = iota

	// DecisionWarn — score below soft but at-or-above hard threshold.
	// The runner emits the audit event and lets the tool call proceed;
	// operators tail the audit stream to tune.
	DecisionWarn

	// DecisionDeny — score below hard threshold, or the engine
	// couldn't produce a score (fail-closed on embedder error).
	// The runner aborts the tool call.
	DecisionDeny
)

func (Decision) String

func (d Decision) String() string

String returns the audit-safe token.

type DriftConfig

type DriftConfig struct {
	// Enabled turns the analyzer on. When false, RecordAndCheck is
	// a no-op and no drift signals fire.
	Enabled bool

	// Window is the number of most-recent scores considered by the
	// rolling-mean test. Must be ≥ 2 — a window of 1 can't
	// distinguish "trending down" from "just low."
	Window int

	// DriftThreshold is the mean-score floor. When the rolling
	// window mean falls strictly below this value, drift enters
	// the "mean_below_threshold" state.
	DriftThreshold float64

	// MonotoneN, when non-zero, additionally trips drift when the
	// last N scores are strictly decreasing (even if the mean is
	// still above DriftThreshold). Catches slow-boil patterns
	// where each individual step is small but cumulative drift is
	// large. Zero disables the monotone check.
	MonotoneN int
}

DriftConfig configures the R7 (#214) rolling-window drift signal. Where the R3 alignment check is a per-call policy gate, drift is longitudinal telemetry — it watches the sequence of alignment scores accumulating for a task and flags trends that suggest the agent is progressively wandering from the stated intent.

func (DriftConfig) Validate

func (c DriftConfig) Validate() error

Validate returns an error when DriftConfig would produce nonsensical decisions. Called at Engine construction so runners fail startup rather than at first RecordAndCheck.

type DriftSignal

type DriftSignal struct {
	// Severity is the human-readable classification. One of
	// "mean_below_threshold", "monotone_decrease", "both", or
	// "recovered" (transition OUT of drift).
	Severity string

	// Mean is the rolling-window mean of the last Window scores at
	// the moment the signal fired. Included on both entry and
	// recovery transitions for the audit event.
	Mean float64

	// Window is the number of scores in the mean.
	Window int

	// Transition is "entered" for on-entry signals or "recovered"
	// for the exit signal. Makes SIEM queries "when did I go into
	// drift?" trivial.
	Transition string
}

DriftSignal describes a drift-state transition. Populated on engine.Score's return value when a state change happens on that call; nil otherwise. The runner emits an `intent_drift` audit event iff Signal != nil.

State-transition emission (rather than "emit every call while in drift") keeps the audit stream from flooding — one event when the task first crosses into drift, one when it recovers.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the per-runtime intent-alignment coordinator. Safe for concurrent use — RegisterIntent + Score fire from independent goroutines (one per A2A request handler + one per hook fire).

func New

func New(cfg Config, embedder llm.Embedder) (*Engine, error)

New constructs an Engine with just the R3 alignment check. The R7 drift analyzer stays off. embedder may be nil when cfg.Enabled is false; enabling without an embedder returns an error.

func NewWithDrift

func NewWithDrift(cfg Config, drift DriftConfig, embedder llm.Embedder) (*Engine, error)

NewWithDrift constructs an Engine with the R3 alignment check AND the R7 rolling-window drift analyzer. drift.Enabled requires cfg.Enabled to be true (drift is derived from alignment scores).

func (*Engine) Enabled

func (e *Engine) Enabled() bool

Enabled reports whether the engine is armed. Runners check this to short-circuit hook wiring on unconfigured deployments.

func (*Engine) Forget

func (e *Engine) Forget(taskID string)

Forget removes the intent embedding AND any drift-analyzer state for the given task. Called from the A2A handler at session_end so long-running processes don't retain per-task state indefinitely.

func (*Engine) RegisterIntent

func (e *Engine) RegisterIntent(ctx context.Context, taskID, statedIntent string) error

RegisterIntent records the stated intent for a task. Called from the A2A handler on tasks/send entry. Idempotent per task ID — the FIRST call wins; subsequent calls for the same task are no-ops so mid-conversation user messages don't overwrite the original intent.

func (*Engine) Score

func (e *Engine) Score(ctx context.Context, taskID, actionText string) Result

Score computes the alignment between the previously-registered stated intent for taskID and the current action (tool call).

actionText is the concatenation of the tool's description and the LLM-supplied args JSON — chosen over tool name because tool names are arbitrary handles (`fn_42`) that don't reflect semantics, while descriptions carry what the tool DOES and args carry the specific values.

Returns DecisionDeny when:

  • The engine is enabled but no intent was registered for taskID (guards against a hook firing before the A2A handler set up the task).
  • The embedder call fails or returns zero vectors.
  • The score is strictly below cfg.HardThreshold.

When cfg.Enabled == false, returns DecisionAllow with Score=NaN so the caller can distinguish "not checked" from "checked and passed at 1.0."

type Result

type Result struct {
	// Score is the cosine similarity ∈ [-1, 1]. NaN when the engine
	// couldn't produce a value (embedder error, missing intent).
	Score float64

	// Decision is the alignment engine's verdict.
	Decision Decision

	// Reason is a short human-readable classification — surfaced on
	// the audit event and, on Deny, returned as the tool-exec error.
	Reason string

	// Drift is non-nil ONLY on the tool call that transitions the
	// task into or out of drift (R7 / #214). The hook consumer
	// emits an `intent_drift` audit event when set. State-transition
	// semantics keep the audit stream from flooding across long
	// stretches of drift.
	Drift *DriftSignal
}

Result carries the outcome of one Score call. Surfaced as fields on the `intent_alignment` audit event.

Jump to

Keyboard shortcuts

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