Concepts: Agents & Actions
An Agent (app.agent) in Invariant is a model-driven decision layer that evaluates user intent and decides what should happen next.
"The developer defines the Agent's world. The Runtime projects its current position within that world."
1. What is an Agent?
An Agent does not execute arbitrary code directly or own application state.
Instead, an Agent is registered with a set of allowed workflows and developer business context. When given a request, the Agent evaluates the current situation and proposes a constrained Runtime Action to the Invariant Runtime.
import { app } from "./runtime";
import { refundWorkflow, cancelSubscriptionWorkflow } from "./workflows";
export const supportAgent = app.agent("customer-support", {
instructions: "Help customers with order inquiries and drive refund or cancellation workflows.",
workflows: [refundWorkflow, cancelSubscriptionWorkflow],
context: ({ session }) => ({
customer: {
id: session.context.customerId,
tier: session.context.accountTier,
},
}),
});2. What Does the Model Actually Receive?
When an Agent turn executes (await supportAgent.run({ session, message })), developers often ask: What is actually sent to the LLM? Do I write the system prompt? What tools does it receive?
Invariant assembles the model-facing context from two distinct sources:
You Define Invariant Derives
────────── ─────────────────
Agent instructions Current execution position
Developer context Available workflows & schemas
Registered workflows Expected input schema
Currently valid actions
│ │
└────────────┬────────────┘
▼
Model Context Payload
│
▼
LLMWho Defines What?
| Model Input Element | Who Defines It? | Where Does It Come From? |
|---|---|---|
| Agent instructions | Developer | app.agent({ instructions: "..." }) |
| Business context | Developer | context: ({ session }) => ({ ... }) |
| Available workflows | Developer + Runtime | Workflows registered on Agent, projected by Runtime |
| Workflow descriptions | Developer | Workflow metadata (description) |
| Workflow input schemas | Developer | Zod inputSchema defined on app.workflow() |
| Active execution | Runtime | Durable state of current runId |
| Expected input | Runtime | Current .wait() boundary schema |
| Valid actions | Runtime | Derived from current execution position |
| User message | Application / User | Current turn input |
You define what the Agent is allowed to know and which Workflows it may drive. Invariant derives the execution state required to use those Workflows safely.
3. What Tools Does the Agent Receive?
Developers do not need to manually convert every node of every workflow into an unconstrained list of LLM tools:
NOT an unconstrained, bloated tool list:
refundCustomer()
cancelSubscription()
checkRefundPolicy()
loadCustomer()
issueStripeRefund()
selectRefundReason()
...Instead, the model interacts with a tight, dynamic surface of Runtime Actions:
start_workflow
submit_input
cancel_workflowThe Runtime dynamically updates which actions are valid based on the active execution position:
No Active Execution:
Available Workflows: [refund, cancel-subscription]
Valid Actions: [start_workflow]
Execution Paused at .wait("provide-order-id"):
Expected Input: { orderId: string }
Valid Actions: [submit_input, cancel_workflow]4. One Agent Turn
Here is how a turn flows from user intent to structured action:
const turn = await supportAgent.run({
session,
message: "My order arrived damaged. I want a refund.",
});"My order arrived damaged"
│
▼
┌───────────┐
│ Agent │
└─────┬─────┘
│
What can I do?
│
▼
┌──────────────────────────────┐
│ Available Workflows │
│ - refund │
│ - cancel-subscription │
└──────────────┬───────────────┘
│
LLM interprets intent
│
▼
start_workflow("refund", {
orderId: "order_123",
reason: "damaged"
})
│
▼
Runtime validates
│
▼
Refund WorkflowThe Agent did not directly issue a refund to Stripe. It proposed a structured request to the Runtime.
5. Agent Context Projection & Developer Context
The complete model-facing view assembled for a decision is called the Agent Context Projection:
Agent Context Projection
│
┌───────────────┴───────────────┐
▼ ▼
Developer Context Runtime Projection
("What business info ("Where execution is
may the model know?") & what can happen next?")
│ │
▼ ▼
customerId, tier, active workflow, expected input,
user preferences available workflows, valid actions- Developer Context answers: "What business information may the model know?"
- Runtime Projection answers: "Where is execution now, and what can happen next?"
6. The 3 Isolation Boundaries
Invariant places model reasoning between three explicit boundaries:
What can the model KNOW? ──► Knowledge Boundary (Agent Context Projection)
Where can execution GO? ──► Execution Boundary (Registered Workflow Graphs)
What can the model actually DO? ──► Authority Boundary (Runtime Action Validation)| Boundary | Controlled By | Constraint | Framework Benefit |
|---|---|---|---|
| Knowledge | Application Developer | What the model can KNOW | Bounded token costs, zero irrelevant history noise. |
| Execution | Workflow Definition | Where execution can GO | Constrained hallucination blast radius, predictable flows. |
| Authority | Runtime Kernel | What the model can DO | Safe side effects; no unvalidated execution against stale state. |
7. Fresh-State Validation & Authority
"Context Projection is a view, not an authorization key."
If the execution state changes while the LLM is generating a response (for example, if a background webhook completed a step or a timeout fired), the Runtime re-validates the incoming Runtime Action against the fresh durable state in PostgreSQL before executing it.
If the active execution position no longer accepts submit_input or if the revision is stale, the Runtime rejects the action safely without mutating state or triggering side effects.
8. Intent Routing & Workflow Metadata
When an Agent evaluates user intent, the runtime projects the descriptions and sanitized input schemas (filterSystemInputs) of all registered candidate workflows:
[AVAILABLE WORKFLOWS]
- ID: refund
Description: Evaluates eligibility and issues Stripe refunds.
Required initialData Schema: {"type":"object","properties":{"reason":{"type":"string"}},"required":["reason"]}
- ID: cancel-subscription
Description: Cancels active paid subscriptions and downgrades tier.
Required initialData Schema: {"type":"object","properties":{"subscriptionId":{"type":"string"}},"required":["subscriptionId"]}The model reads these declarations to match user intent ("My order arrived damaged") to the correct workflow (refund) and construct a valid input payload.
9. Delivery Modes
Adapters decide how the Agent Context Projection reaches the model:
"Invariant produces projections. Adapters choose the delivery mechanism."
Agent Context Projection
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
Turn-Based LLM Persistent Realtime External MCP
(Chat Turn) (Live WebSocket) (Cursor / Claude)
│ │ │
Prompt Context Tool Result Tool Result- Turn-based Chat: Invariant builds prompt context before each turn and returns the model response.
- Persistent Realtime (Live): The WebSocket model session stays open; after each Runtime Action, the updated Runtime Projection is returned as a tool result in a continuous loop.
- External MCP Adapter: Cursor or Claude Desktop acts as the external reasoning engine, calling
invariant.get_stateand issuing Runtime Actions over@invariant/mcp.
10. Exact TypeScript API & Projection Types
For reference, here are the exact TypeScript interfaces for Agent Context Projections:
interface AgentContextProjection<TContext = unknown> {
readonly context: TContext; // Developer Context (Business Info)
readonly runtime: RuntimeProjection; // Runtime Projection (Execution State)
}
interface RuntimeProjection {
readonly revision: number; // Durable state revision counter
readonly relevantWorkflows: readonly WorkflowProjection[];
readonly validActions: readonly RuntimeActionProjection[];
readonly activeExecution?: ExecutionProjection;
readonly expectedInput?: InputProjection;
}
// Example projected payload:
{
context: {
customerId: "cust_91",
accountTier: "VIP"
},
runtime: {
revision: 17,
relevantWorkflows: [
{ id: "refund", description: "Evaluates eligibility and issues refunds" }
],
validActions: [
{ name: "submit_input", description: "Submit input to active execution" },
{ name: "cancel_workflow", description: "Cancel active execution" }
],
activeExecution: {
runId: "run_refund_991",
workflowId: "refund",
currentNodeId: "wait-for-reason",
status: "waiting"
},
expectedInput: {
schema: {
type: "object",
properties: { reason: { type: "string" } },
required: ["reason"]
}
}
}
}