Skip to content

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.

ts
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:

text
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


                          LLM

Who Defines What?

Model Input ElementWho Defines It?Where Does It Come From?
Agent instructionsDeveloperapp.agent({ instructions: "..." })
Business contextDevelopercontext: ({ session }) => ({ ... })
Available workflowsDeveloper + RuntimeWorkflows registered on Agent, projected by Runtime
Workflow descriptionsDeveloperWorkflow metadata (description)
Workflow input schemasDeveloperZod inputSchema defined on app.workflow()
Active executionRuntimeDurable state of current runId
Expected inputRuntimeCurrent .wait() boundary schema
Valid actionsRuntimeDerived from current execution position
User messageApplication / UserCurrent 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:

text
NOT an unconstrained, bloated tool list:
refundCustomer()
cancelSubscription()
checkRefundPolicy()
loadCustomer()
issueStripeRefund()
selectRefundReason()
...

Instead, the model interacts with a tight, dynamic surface of Runtime Actions:

text
start_workflow
submit_input
cancel_workflow

The Runtime dynamically updates which actions are valid based on the active execution position:

text
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:

ts
const turn = await supportAgent.run({
  session,
  message: "My order arrived damaged. I want a refund.",
});
text
"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 Workflow

The 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:

text
               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:

text
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)
BoundaryControlled ByConstraintFramework Benefit
KnowledgeApplication DeveloperWhat the model can KNOWBounded token costs, zero irrelevant history noise.
ExecutionWorkflow DefinitionWhere execution can GOConstrained hallucination blast radius, predictable flows.
AuthorityRuntime KernelWhat the model can DOSafe 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:

text
[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."

text
                     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_state and issuing Runtime Actions over @invariant/mcp.

10. Exact TypeScript API & Projection Types

For reference, here are the exact TypeScript interfaces for Agent Context Projections:

ts
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"]
      }
    }
  }
}

Invariant Durable Execution Engine.