Intent IR
Package matrix/mcl/ir defines the Go types for the Intent IR — the central typed artifact that Matrix produces and operates on. Everything the MCL compiler produces ends up here...
Package matrix/mcl/ir defines the Go types for the Intent IR — the central typed artifact that Matrix produces and operates on. Everything the MCL compiler produces ends up here. Everything the executor consumes comes from here.
The IR is canonical JSON with sorted keys for deterministic hashing (D11). CBOR encoding is layered on top for the wire protocol via the envelope package. When you see IntentCompiledBody.IntentJSON, it's the canonical JSON bytes of one of these types.
Source files: MCL/ir/intent.go, MCL/ir/plan.go, MCL/ir/plan_validate.go, MCL/ir/encode.go.
Intent
The top-level type. Every interaction produces one.
type Intent struct {
ID string // ULID
Version string // "mcl/0.1"
Parent string // parent IntentRef for sub-intents (omitempty)
Actor string // who wants this (UserRef or AgentRef DID)
Agent string // who will execute (AgentRef DID)
Prose string // original NL goal — display only, never authoritative
Frame Frame // typed source of truth — what gets signed and executed
Unknowns []Unknown // registered gaps
References []Reference // grounding URIs
State string // IntentState constant
Confidence float64 // 0..1
Budget *Budget
Deadline string // ISO8601
GoalID string // links to a parent standing Goal (omitempty)
SignedBy string // actor's public key (hex-encoded ed25519)
Hash string // sha256 self-hash for content addressing
CompileMetadata *CompileMetadata // D11 compilation trace
}
The Prose field is purely for display. It goes in the journal and shows up in UIs. The Frame is the source of truth — it's what the executor uses, what the user signs, and what gets content-addressed.
GoalID links a one-shot chat intent to a standing Goal memory in cortex, so per-goal cost telemetry works. Empty for standalone intents.
Frame
The typed source-of-truth surface. Five named fields.
type Frame struct {
Verb string // D7 closed vocab
Objects []SlotEntry // typed named referents
Constraints []Constraint // what must hold
SuccessCriteria []Predicate // what defines completion
Preferences []Preference // soft tie-breakers
}
SlotEntry
type SlotEntry struct {
Name string // slot name from §INPUTS
Value string // resolved value or NL text (pre-D13)
URI string // resolved matrix:// URI (post D13) — omitempty
Type string // type annotation — omitempty
}
After stage 5 (entity resolution), URI is populated for all referents that were successfully resolved. Slots that couldn't be resolved become Unknown entries instead.
Constraint
Typed predicates that must hold throughout execution:
type Constraint struct {
Type string // "budget", "deadline", "jurisdiction", "quality", "rule", "policy", "x:*"
Hard bool // hard=true → fail the intent if violated
// Type-specific fields (only the relevant ones populated)
Max *AssetAmount // budget: max spend
By string // deadline: ISO8601 deadline
Allow []string // jurisdiction: allowed regions
Deny []string // jurisdiction: denied regions
Metric string // quality: metric name
Min float64 // quality: minimum value
Rule string // rule: RuleRef URI
Policy string // policy: Argus policy ref
Schema string // x:custom: schema URI
Data string // x:custom: opaque JSON
}
The closed type set: budget, deadline, jurisdiction, quality, rule, policy. x: prefix for custom types. The grammar constraint on the LLM forces output into this schema.
Predicate
Checkable completion criteria:
type Predicate struct {
Type string // "delivered", "signed_off", "external", "attestation", "x:*"
Artifact string // delivered: artifact URI
By string // signed_off: UserRef
URL string // external: check URL
Check string // external: check string
Source string // attestation: AgentRef
Topic string // attestation: topic
Schema string // x:custom schema
Data string // x:custom opaque JSON
}
All predicates must evaluate to true before the executor emits intent.attest.
Unknown
A typed gap blocking or delaying execution.
type Unknown struct {
ID string // local id "u1", "u2" etc.
Field string // SlotPath into the Frame: "frame.constraints[0].max"
Type string // expected type
Severity string // "blocking", "preferred", "optional"
Rationale string // human-readable reason
Default string // suggested fill
Options []string // enum-like choices
SourceHint string // cortex location that might fill this
}
blocking severity stops execution — the intent cannot proceed until this gap is filled. preferred and optional are advisory.
The ID field correlates unknowns to clarify questions in the intent.clarify body (ClarifyQuestion.UnknownID).
IntentState
The lifecycle state machine constants:
const (
StateDraft = "draft" // initial state at creation
StateProposed = "proposed" // intent.compiled emitted, awaiting accept
StateClarifying = "clarifying" // intent.clarify sent, awaiting intent.answer
StateAccepted = "accepted" // user signed via intent.accept
StateExecuting = "executing" // executor is running
StateCompleted = "completed" // intent.attest sent
StateFailed = "failed" // intent.fail sent
StateCancelled = "cancelled" // intent.cancel received
)
Transitions: draft → proposed → (clarifying →)* accepted → executing → completed|failed|cancelled.
D7 verb constants
const (
VerbFind = "find"
VerbAcquire = "acquire"
VerbBuild = "build"
VerbModify = "modify"
VerbDeliver = "deliver"
VerbAnalyze = "analyze"
VerbNegotiate = "negotiate"
VerbSchedule = "schedule"
VerbMonitor = "monitor"
VerbDelegate = "delegate"
)
ir.ValidVerb(v) returns true for any of these or any x:-prefixed extension verb. Extension verbs are valid but not first-class — they get no routing table entry in verb.mtx and no specialist executor model.
CompileMetadata
Records the full compilation trace for D11 replay-verification:
type CompileMetadata struct {
Seed string // sha256(intent.id || actor || snapshot_hash || mtx_digest || model_digest)
MtxDigest string // sha256 of canonical SKILL.mtx + core/*.mtx ASTs
ModelDigest string // digest of the compiler model weights
ModelVersion string // model identifier
Temperature float64
Grammar string // grammar constraint ID used, e.g. "intent_frame@1"
SkillID string // which skill was selected
SkillVersion string
CortexSnapshotHash string // Merkle root of cortex at compile time
}
Anyone with access to the same SKILL.mtx version, the same model weights, and the same cortex snapshot can rerun the compilation and verify they get the same Hash on the resulting Intent. This is the D11 replay invariant.
PlanTree
The executor produces a PlanTree when a skill's §PROCEDURE on-block contains planning steps. The plan tree is the decomposition of the Intent into an executable graph of steps.
type PlanTree struct {
ID string // ULID
Version string // "mcl/0.1"
IntentID string // back-reference to the parent Intent
CreatedAt string // ISO-8601
CreatedBy string // matrix://agent/<did>
SkillRef string // version-pinned skill URI
ModelDigest string // executor model digest at plan-emission time
Root PlanNode // entry point
Budget *Budget
Hash string // sha256 self-hash
}
A PlanTree is a DAG, in practice a tree with shared leaves for citations. Walks are depth-first by default. The Root is the single entry point.
PlanNode
A single node in the plan. Kind discriminates the payload.
type PlanNode struct {
ID string // stable within the tree, e.g. "n1", "n2"
Kind string // node kind constant
Description string // short human-readable label
Children []PlanNode // child nodes (empty for terminal kinds)
// Payload fields — exactly one is populated based on Kind
Step *StepPayload
ToolCall *ToolCallPayload
SubDispatch *SubDispatchPayload
Gate *GatePayload
ResultText string // runtime-only, excluded from hash (json:"-")
}
ResultText is populated at runtime by the walker with the actual tool output or LLM step text. It's excluded from canonical encoding so it never enters D11 or the signed plan — it's pure walk state that lets later steps reference upstream outputs via ${nodeID.output}.
Node kinds (closed)
| Kind | Payload | Semantics |
|---|---|---|
sequential | — | Run Children in order; first failure halts |
parallel | — | Run Children concurrently; first failure cancels siblings |
step | Step | In-skill LLM prompt step |
tool_call | ToolCall | Single tool invocation |
sub_dispatch | SubDispatch | Sub-skill or sub-agent dispatch |
gate | Gate | Human-in-loop checkpoint |
StepPayload
type StepPayload struct {
PromptName string // skill prompt block to invoke (empty = default)
Inputs map[string]string // slot bindings for interpolation
ExpectedOutputs []string // slot names this step should produce
Kind string // step kind hint for model routing (omitempty)
}
Kind drives executor-tier model routing. The closed set is defined in ir.StepKindNames. Empty defaults to "reason" at routing time — this is safe because the default model handles generic reasoning.
The omitempty on Kind preserves byte-identity for plans authored before the model router (Session 31b) was added. Any plan that doesn't set Kind is byte-identical to what it would have been before. This is a deliberate backward-compatibility decision.
ToolCallPayload
type ToolCallPayload struct {
ToolRef string // version-pinned matrix://tool/... URI
Args map[string]string // typed arguments
TimeoutMs int // 0 = tool's manifest default
SideEffectClass string // "read", "write", "network", "shell", "chain"
}
SideEffectClass is cross-checked against the agent's allowed side-effect set at the executor's capability gate before dispatch. The closed set: read, write, network, shell, chain.
Sensitive values (API keys, tokens) must not appear inline in Args. Use environment variable references instead so they don't leak into the plan's content address.
SubDispatchPayload
type SubDispatchPayload struct {
SkillRef string // version-pinned sub-skill URI
AgentRef string // target agent (empty = in-process)
SubIntent *Frame // the Frame for the sub-intent
ScopeURI string // CortexScope grant for cross-agent reads
}
In-process dispatch (same agent, AgentRef empty) is the v1 default. Cross-agent dispatch requires a ScopeURI granting the child agent read access to the relevant cortex memories.
The parent skill must declare the sub-skill URI in its §SUB_SKILLS section (enforced at plan validation).
GatePayload
type GatePayload struct {
RuleRef string // matrix://rule/<id> that triggered
Question string // human-readable prompt
Options []string // allowed answers (empty = free text)
TimeoutMs int // 0 = no timeout (block forever)
}
When the executor hits a NodeGate, it emits policy.gate to the user and halts the walk until policy.gate.resolve comes back. If TimeoutMs is set and expires with no response, the gate is treated as denied.
Plan validation
ir.plan_validate.go enforces the hard rules on a PlanTree before the executor runs it:
- S3:
Rootmust be non-nil - S4: All
ToolRefandSkillRefURIs must be version-pinned (no bare refs) - S5: All node
Kindvalues must be inValidNodeKinds - S6:
SubDispatchPayload.SkillRefmust appear in the skill manifest's declared§SUB_SKILLS - S7:
SideEffectClassvalues must be inValidSideEffectClasses
Content addressing
Both Intent and PlanTree carry a Hash field. It's the sha256 of the canonical JSON encoding of the struct with Hash cleared. This makes them self-describing content addresses.
Usage:
intent.acceptbody carriesIntentHash— the receiver verifies this matches the local IR before treating the acceptance as valid- Journal storage uses the hash as the key
intent.correctpatches reference the current hash to prevent stale updates
The canonical encoding uses sorted JSON keys (not the default Go encoder order). ir.encode.go implements this.
