Skip to content

Agents governed by the case

The static agent settles one question when its loop starts: which tools may this agent ever call. In a CMMN case the answer changes while the case runs. A discretionary item is not planned yet, an applicability rule does not hold, a role has not been granted the item. Case mode makes the case’s own state the agent’s tool list, re-read on every turn, and expresses every rule about it in the case model.

What changes when a spec sets manifest: case

Section titled “What changes when a spec sets manifest: case”

A spec with manifest unset or set to static is the static agent. A spec with manifest: case keeps the same loop and changes one input to it: at the top of every turn the loop reads the tools the case grants now, through a ManifestSource the host supplies.

The grants come from the one construct each standard has for work that is authorised and not yet scheduled:

  • CMMN: the discretionary items the agent’s role may plan on its planning table right now. cmmn.ToolGrants projects them. An item counts when its planner is in a state that permits planning, the planning table authorises the agent’s role, its applicability rules hold, and it is not already planned. An item with no repetition rule can be planned once.
  • BPMN: the parked entry points of an open ad-hoc sub-process marked <kirto:planning mode="external"/>. bpmn.ToolGrants projects them. BPMN declares no planner roles, so there is no role narrowing.

Only items that carry a <kirto:taskDefinition type="ns:op"> become tools. Human tasks and bare tasks stay plannable by people and are never offered to the model.

A case grant only narrows the static allowlist. allowedToolNamespaces stays the outer boundary, and the case decides what is live inside it. A call to a tool the allowlist permits and the case does not grant this turn is refused with TOOL_NOT_AVAILABLE, which goes back to the model as a tool error. The loop carries on.

An input marked <kirto:input pinned="true"> on the item is set by the model author. The tool’s schema leaves it out, and a value the model supplies for it is dropped.

Attaching an agent to a case or to one task

Section titled “Attaching an agent to a case or to one task”

An agent attaches to the whole case with <kirto:orchestrator> on the case plan model:

<casePlanModel id="root">
<extensionElements>
<kirto:orchestrator id="orch" type="agent:orchestrate" role="assistant">
<kirto:input name="agentRef" value="researcher"/>
<kirto:input name="prompt" value="orchestrate the case"/>
<kirto:output name="answer" source="text"/>
</kirto:orchestrator>
</extensionElements>
<!-- plan items, tasks, planning tables -->
</casePlanModel>

The parser rewrites it into an ordinary root-level plan item that starts with the case, and no engine or host code knows the item was synthesised. Its role, assistant, is the role it plans under: any item the root planning table authorises for that role is a tool it can call. It completes when its loop returns, and terminating the case tears it down like any other task.

An agent attaches to one task with plannerRole on its task definition:

<task id="t-triage" name="Triage" isBlocking="true">
<extensionElements>
<kirto:taskDefinition type="agent:run" plannerRole="triage-agent">
<kirto:input name="agentRef" value="triage-bot"/>
</kirto:taskDefinition>
</extensionElements>
</task>

Its planner scope is the stage that encloses the task. An agent inside a stage is granted what that stage’s planning table authorises for its role, and nothing from the rest of the case.

With planToolCalls: true, a granted call is planned into the case as its discretionary item instead of being dispatched directly. The item runs the whole plan-item lifecycle: its sentries are evaluated, sentries watching it fire on its completion, and the case’s completion rules see it. The model’s arguments are laid over the item’s inputs, except the pinned ones, and the loop waits for the item’s result.

A tool call the model made is then a line in the case’s own history, recorded the same way as an item a person planned.

The suggestion mechanism is CMMN’s own manualActivationRule on the discretionary item. With planToolCalls on and the rule set, a planned call lands in Enabled and waits:

  1. The model calls a granted tool, and the item is planned.
  2. The rule holds, so the item enters Enabled and is shown to a person as a suggestion.
  3. A ManualStart accepts it. The item becomes Active and the tool runs with the model’s arguments.
  4. A ManualDisable rejects it. The item moves to Disabled, the tool never runs, and the agent’s waiting call is released with a discarded result.

The approval is a property of the case model, visible to whoever reads it, and it composes with what CMMN already does with plan items: sentries, exit criteria, autoComplete.

With manifestPark: true, a turn with nothing granted does not call the model with an empty tool list. The loop parks until the case’s grants change, at no token cost. The park is still bounded by deadlineSeconds.

A case-mode spec on a host with no case-aware manifest source fails with AGENT_MANIFEST_UNAVAILABLE. It never falls back to the static allowlist. manifestPark or planToolCalls against a source that cannot honour them fails the same way, and either flag on a static spec is an AGENT_SPEC_ERROR. A misconfigured deployment does not run the agent; the model never receives a wider tool list than the model author intended.

Case mode runs on the DBOS host, for CMMN cases and for BPMN processes, and on the in-process CMMN runner used in tests and development. The broker’s agent engine does not support it yet: it has no per-turn grant event, and it refuses a case-mode spec with AGENT_SPEC_ERROR when the loop starts.

The grants are read once per turn. A grant revoked mid-turn takes effect at the next turn, and a model call already in flight finishes.

Sentries as tool gates sets out the case for this design for architects and risk owners.