Sign in to edit tickets from this page.

← all tickets · home

Clean cutover to explicit cognition workflows with ambient context sources, model-elected tools, source invocations, and WorldPatch commits

resolved d8d95ef4-a2c2-4102-92e3-e6ea7d733634

created_at
2026-04-28
updated_at
2026-04-30
priority
P1
ticket_type
feature
labels
cognition, workflow, llm, tools, ambient_sources, source_neutral, world_patch, observability, http_json, clean_cutover
resolved_at
2026-04-30
resolution
accepted

Body


Ticket: Clean cutover to explicit cognition workflows with ambient context sources, model-elected tools, source invocations, and WorldPatch commits

Priority: P1 Type: feature / architecture Labels: cognition, workflow, llm, tools, ambient-sources, source-neutral, world-patch, observability, http-json, mcp-ready, clean-cutover

Summary

Replace Chukwa’s current hardcoded cognition path with an explicitly authored workflow system.

The new system must support:

  1. Ambient context sources Sources invoked automatically because the workflow says to include them.

    Examples:

    • toy weather API for a park;
    • public announcement source for a PA speaker;
    • Bob’s phone inbox;
    • entity-specific status feed;
    • future real weather API;
    • future MCP-backed sensor/tool source.
  2. Model-elected tools Sources exposed to an LLM node and invoked only when the model emits a permitted tool call.

    Examples:

    • vending machine;
    • drinking fountain;
    • dummy send-text tool;
    • toy light switch;
    • future MCP tool.
  3. Source invocation tracing Every LLM generation, ambient source call, and model-elected tool call gets a durable source_invocations record. LLM source invocations link to the existing rich llm_calls trace machinery.

  4. WorldPatch commits External source outputs are JSON context only. They never directly mutate the world.

    The final LLM output emits a kernel-owned WorldPatch.

    Only WorldPatch mutates Chukwa world state.

  5. Multiple WorldPatches per turn A committed turn may contain multiple accepted WorldPatch values. A Bob/vending-machine patch and an ant/crumb patch can both land in the same T1 snapshot.

This is a clean cutover.

There is no hidden default workflow.

There is no compatibility runtime path.

There is no old hardcoded perceive → intend → adjudicate → apply_adjudication path kept alive.

There is no preservation requirement for pre-cutover worlds, cognition profiles, schemas, or scenario component shapes.

A simple LLM-only scenario still works, but only because it is authored as ordinary new workflow data.

Current repo facts anchoring this ticket

The current bug class is concrete.

src/scenarios.rs currently stores cognition profiles with:

pub struct CognitionProfile {
    pub perceive_system: String,
    pub intend_system: String,
    pub adjudicate_system: String,
    pub adjudication_schema: Value,
    pub adjudication_retry_budget: u32,
}

validate_adjudication_schema only checks that the schema is a JSON object.

src/minds.rs still defines a fixed runtime response shape:

pub struct Adjudication {
    pub narration: String,
    pub agent_state_after: String,
    pub agent_memory_append: String,
    pub entity_mutations: Vec<EntityStateMutation>,
    pub environment_mutations: Vec<EnvironmentMutation>,
}

minds::adjudicate currently calls:

JsonCompletion<Adjudication>

So the substrate can accept a schema that the kernel cannot deserialize or apply. The schema memo describes the exact failure: user schema accepted, LLM output matches that schema, kernel still rejects because Rust expected agent_state_after .

The repo already has adjudication retry behavior in minds::adjudicate. It loops over adjudication_retry_budget + 1, records rejected attempts, appends corrective context, and retries the same adjudication generation. This ticket must preserve that behavior in the new WorldPatch-producing LLM node. Do not add a second retry lane.

The repo already has rich LLM trace machinery from the previous observability work: llm_calls, request messages, chunks, artifacts, token observations, parse errors, validation errors, and audit links. Preserve that richness. Do not flatten it into a lowest-common-denominator source trace.

The repo currently has execute_with_fallback in src/llm.rs, which can silently retry with response_format removed and the schema appended to prompt text if upstream rejects structured response format. This ticket removes that implicit degradation from the normal path.

The repo also has graph-browser/resource-catalog/read-model/MCP surfaces for the old component model: perceive systems, intend systems, adjudicate systems, and adjudication schemas. Those surfaces must be removed or replaced, not left as crust.

Required outcome

After this ticket:

  1. Every executable scenario references an explicit cognition workflow.
  2. A scenario without an explicit workflow cannot assemble or run.
  3. The old hardcoded cognition path is gone from production runtime.
  4. The old Adjudication struct is not used as a production parse target.
  5. The old apply_adjudication path is gone from production runtime.
  6. The terminal applyable output is the new WorldPatch schema.
  7. A workflow can declare ambient context sources.
  8. Ambient context sources can apply to the whole world, an environment, a specific entity, or the acting workflow subject.
  9. A workflow can expose model-elected tools to an LLM node.
  10. Available tools do not run merely because they exist.
  11. If the model emits no tool call, no model-elected tool invocation is created.
  12. If the model emits a permitted tool call, the runner executes that source and returns the result to the model as context.
  13. HTTP JSON ambient sources work.
  14. HTTP JSON model-elected tools work.
  15. MCP is not implemented yet, but the source/tool abstraction leaves a clean McpToolSource implementation slot.
  16. External source outputs do not directly mutate world state.
  17. Existing LLM retry semantics are preserved in the new WorldPatch-producing LLM node.
  18. Replay is out of scope and is not an acceptance requirement.
  19. A committed turn may contain multiple accepted WorldPatch values.
  20. Old component, route, read-model, MCP, and graph-browser surfaces for perceive/intend/adjudicate/adjudication-schema are removed or explicitly rejected.

Required deletion / replacement

This ticket must not merely add the new workflow/source/WorldPatch system. It must remove or replace obsolete cognition surfaces.

The following production-live surfaces must be deleted, replaced, or made unreachable outside tests/historical migrations:

src/minds.rs::Adjudication
src/minds.rs::EntityStateMutation, unless replaced by WorldEffect
src/minds.rs::EnvironmentMutation, unless replaced by WorldEffect
src/minds.rs::AdjudicationOutcome
src/minds.rs::AdjudicationError, unless reworked into new workflow/tool-loop retry error type
src/minds.rs::validate_adjudication
src/minds.rs::perceive
src/minds.rs::intend
src/minds.rs::adjudicate
src/kernel.rs::run_cognition_loop as hardcoded perceive/all → intend/all → adjudicate/apply path
src/kernel.rs::apply_adjudication
live uses of JsonCompletion<Adjudication>
live uses of LlmPhase::Perceive / LlmPhase::Intend / LlmPhase::Adjudicate
live use of llm_phase as primary LLM trace identity
default use of execute_with_fallback
old MCP tools for put_perceive_system / put_intend_system / put_adjudicate_system / put_adjudication_schema
old MCP tools for get_perceive_system / get_intend_system / get_adjudicate_system / get_adjudication_schema
graph-browser routes for /perceive-systems
graph-browser routes for /intend-systems
graph-browser routes for /adjudicate-systems
graph-browser routes for /adjudication-schemas
old ResourceKind variants PerceiveSystem / IntendSystem / AdjudicateSystem / AdjudicationSchema
old ComponentKind variants PerceiveSystem / IntendSystem / AdjudicateSystem / AdjudicationSchema
old cognition-profile hash input composed from perceive/intend/adjudicate/schema hashes

If any of these names remain, the implementer must explain why they are not reachable from production runtime, MCP, graph-browser, read-model, scenario assembly, or source invocation paths.

Load-bearing decisions

D1 — Clean cutover only

There is no compatibility execution path.

There is no hidden runtime workflow.

There is no default workflow invented by the kernel.

There is no old hardcoded path kept alive.

There is no data-preservation requirement for pre-cutover worlds, profiles, or schemas.

A simple LLM-only scenario must be authored in the new workflow format.

D2 — Sources have two binding modes

A response source may be bound as:

ambient_context
model_elected_tool

Both binding modes use the same source abstraction and source invocation trace layer.

Neither binding mode directly mutates world state.

Only final WorldPatch mutates world state.

D3 — Ambient context sources are automatic

Ambient context sources are invoked because the workflow explicitly says to include them.

Examples:

weather report for a park
public announcement feed for a PA speaker
inbound phone/SMS inbox for Bob’s phone
diabetes/symptom/status feed for a specific entity
background sensor attached to a room, fountain, vending machine, or instrumented object

The LLM does not choose whether these ambient sources run.

The workflow chooses.

The source result becomes JSON context.

An empty result is still a successful source invocation if it matches the declared schema.

Examples:

{ "announcements": [] }

or:

{ "messages": [] }

Those are successes, not failures.

A failed HTTP call, non-JSON body, schema-invalid body, or missing required source result fails the attempt before world commit.

There is no optional ambient-source degradation mode in this ticket.

D4 — Model-elected tools are chosen by the LLM

Do not add deterministic workflow when conditions to decide whether model-elected tools are used.

The workflow exposes tools/sources to an LLM node.

The LLM may call one.

The LLM may also call none.

The runner executes only tool calls actually emitted by the model.

This is required so that the mere presence of a vending machine, drinking fountain, phone, MCP server, weather API, or toy light switch does not force source usage.

Example: an agent with no money and pockets full of candy should usually not call buy_candy. The kernel does not enforce that behavioral policy. The model’s cognition does.

D5 — Tool calls are JSON control objects, not native provider tool calls in this ticket

For this ticket, model-elected tools are represented by the model emitting a structured JSON ToolLoopOutput.

Tool call:

{
  "kind": "tool_call",
  "tool_call": {
    "name": "buy_candy",
    "arguments": {
      "actor_id": "bob",
      "machine_id": "vending_machine",
      "button": "C"
    }
  }
}

Final patch:

{
  "kind": "final_patch",
  "patch": {
    "narration": "Bob ignores the vending machine.",
    "effects": []
  }
}

Do not implement OpenAI-native tools, tool_choice, tool role messages, or streamed provider-native tool_calls parsing in this ticket.

Tool results are appended back into the LLM context as ordinary JSON-bearing messages.

Native provider tool-call support is a future LLM-source adapter enhancement.

D6 — Tool calls are validated, not invented by the kernel

If the model emits a tool call, the runner must verify:

tool name is listed in the node’s available tools
arguments match the declared argument schema
referenced source resolves
source result parses as JSON
source result matches declared result schema, if present

If the model emits no tool call and instead emits final patch JSON, no model-elected tool source is invoked.

D7 — External source outputs are context only

External source outputs are never applied directly to world state in this ticket.

HTTP JSON responses, future MCP tool results, toy source results, weather results, PA announcements, phone inbox results, and dummy send-text results are captured as source/tool/ambient context.

The final LLM output emits the only applyable WorldPatch.

Do not implement:

deterministic source-to-world mutation
source-result-to-patch mapping
direct tool-result commit
adapter conditionals

D8 — The terminal world mutation contract is WorldPatch

The kernel owns one small apply contract.

Suggested Rust shape:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorldPatch {
    pub narration: String,
    pub effects: Vec<WorldEffect>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum WorldEffect {
    SetEntityState {
        entity_id: String,
        state: String,
    },
    AppendEntityMemory {
        entity_id: String,
        content: String,
    },
    SetEnvironmentContent {
        environment_label: String,
        content: String,
    },
}

Allowed effects in this ticket:

set_entity_state
append_entity_memory
set_environment_content

Do not add:

create/delete entity operations
inventory transfer operations
physics operations
resource allocation operations
conflict-resolution policy operations

The kernel validates every effect against the live working world before applying it.

D9 — WorldPatch is not the source result

A toy weather API may return:

{
  "temperature_f": 62,
  "condition": "windy",
  "message": "A cold front is moving through."
}

That does not mutate the Chukwa world.

It becomes context for the LLM.

The final LLM may emit:

{
  "narration": "Bob shivers as the cold front moves through the park.",
  "effects": [
    {
      "op": "set_entity_state",
      "entity_id": "bob",
      "state": "cold and distracted in the park"
    }
  ]
}

or:

{
  "narration": "The park air is cooler now.",
  "effects": []
}

Both are valid.

The weather source did not mutate the world. The final WorldPatch either did or did not.

D10 — Multiple WorldPatches per turn

A WorldPatch is the applyable output of one workflow subject. It is not the entire turn-level diff.

For one attempted turn, the kernel may accept and apply multiple WorldPatch values to the in-memory working world before committing. Each accepted WorldPatch stages its own audit event and touched-entity records. The committed turn snapshot contains the accumulated result of all accepted patches.

Do not commit one database/world turn per WorldPatch.

Do not require one LLM call to emit a global patch touching every changed entity in the world.

Example: in the same attempted turn, Bob may emit a patch that gives Bob a candy bar and empties the vending machine, while an ant emits a separate patch that consumes a crumb. The committed T1 snapshot contains both changes.

Internal execution order is not simulation-time turn taking.

D11 — WorldPatch application is provisional until turn commit

WorldPatch application during an attempt is provisional until the final turn commit.

If any subject workflow fails after earlier patches have been applied to the in-memory working world, the attempt fails and no turn snapshot is committed.

No accepted patch from a failed attempt becomes canonical world history.

Diagnostic traces, source invocations, and LLM calls may remain durable for the failed attempt, but canonical world state does not advance.

D12 — Patch transition semantics are per-patch

Each accepted WorldPatch receives a patch_seq within the attempted turn.

For transition records inside a patch, state_before means the state immediately before that patch was applied to the working world.

It does not necessarily mean global turn-start state.

Do not preserve comments or tests claiming every transition’s state_before is the turn-start state.

Future read models may expose both:

per-patch before/after
whole-turn T(N-1) → T(N) diff

but this ticket only requires honest per-patch semantics.

D13 — Preserve existing LLM retry semantics

Do not add a second retry lane.

The repo already has adjudication retry semantics through adjudication_retry_budget.

Move that behavior into the new WorldPatch-producing LLM node.

If the workflow representation needs a node-level field such as max_generation_attempts, that field replaces the old profile-level budget as data. It must not introduce a second retry implementation.

Retry remains for model-content failures, including:

non-JSON output
JSON that fails declared schema validation
unknown tool name emitted by the model
malformed tool arguments emitted by the model
final WorldPatch with invalid shape
final WorldPatch referencing unknown entities or environments
final WorldPatch attempting a disallowed operation

Retry means:

same workflow node
same LLM source
same declared contract
corrective context appended
new LLM call recorded

Retry does not mean:

switch source
switch model
drop response_format
change schema delivery mode
change output contract
silently degrade

Each retry must create its own durable LLM trace row and source invocation row.

D14 — Remove implicit structured-output fallback

The current execute_with_fallback behavior must not remain in the normal path.

If the configured LLM source requires structured JSON output and upstream rejects response_format, the source invocation fails loud.

Prompt-only schema delivery may exist only if explicitly authored as a source configuration. It must not be silently selected by the client.

D15 — LLM trace remains rich

Do not flatten LLM traces into a generic lowest-common-denominator trace.

Keep the rich LLM trace machinery:

llm_calls
request messages
streamed chunks
artifacts
token observations
raw assistant text
normalized assistant text
parse errors
validation errors
audit links

Add source invocation tracing beside it.

For LLM invocations, the generic source invocation links to the rich llm_calls row.

For HTTP JSON source invocations, the source invocation records request JSON, response JSON/body, status, headers, duration, validation status, and failure class.

D16 — Replace LlmPhase with workflow-node identity

Do not add new enum variants such as:

LlmPhase::Act
LlmPhase::ToolLoop

That would preserve the old phase model under a new name.

The new LLM/source trace identity is workflow-node provenance:

attempt_id
world_slug
attempted_turn
source_invocation_id
workflow_hash
workflow_node_id
workflow_subject_entity_id
logical_generation_attempt
tool_loop_round
model_output_kind

LlmPhase::Perceive, LlmPhase::Intend, and LlmPhase::Adjudicate must not remain as production-live primary trace identity.

D17 — Source invocation lifecycle wraps LLM calls

For LLM generations:

  1. Create source_invocations row before sending bytes to the LLM provider.
  2. Start the llm_calls row and link it to source_invocation_id.
  3. Both rows must be durable while the provider call is in flight.
  4. On success, finish both rows.
  5. On failure, fail both rows.
  6. A process crash mid-call should leave running/interrupted trace records, not an invisible call.

The previous observability ticket required durable LLM traces while calls are running; preserve that property here .

D18 — JSON Schema validation happens inside Chukwa

Add a JSON Schema validation crate to Cargo.toml.

Use it for:

ToolLoopOutput validation
WorldPatch validation
model-elected tool argument validation
HTTP JSON source result validation
ambient source result validation
LLM final response validation

Do not hand-roll JSON Schema validation.

Do not repeat the current “schema is object-shaped” bug.

D19 — No native scheduler or cron

Do not implement a scheduler or cron system.

Ambient source cadence in this ticket is only:

once_per_turn
before_subject_workflow

If weather gets colder over turns, the toy weather source computes that from request turn / simulation_time.

If the PA speaks occasionally, the toy PA source returns either:

{ "announcements": [] }

or:

{ "announcements": ["..."] }

based on request turn / simulation_time.

D20 — MCP is the next source implementation, not a kernel change

This ticket implements HTTP JSON source execution.

This ticket does not implement MCP consumer support.

After this ticket, adding MCP should require:

McpToolSource implementation
MCP server/source configuration
tool discovery or explicitly authored tool definitions
MCP tool-call execution
MCP tool-result normalization into JSON
source invocation tracing
schema validation

It must not require changes to:

kernel world apply
WorldPatch validation
turn commit semantics
audit commit semantics
LLM retry semantics

D21 — Replay is out of scope and not required

Replay is not an acceptance criterion.

The source invocation trace is a forensic record, not a replay handle.

The system must preserve:

what source was called
why it was called
with what JSON
at what time
what came back
how it validated
what LLM did next
what WorldPatch was applied

The system does not need to guarantee that the same turn can be replayed and produce the same LLM/tool/API result.

D22 — Real versus toy is opaque to Chukwa

Chukwa must not care whether a source is connected to:

fake weather server
real weather API
toy vending machine
actual instrumented vending machine
toy drinking fountain
real drinking-fountain telemetry
dummy SMS service
real SMS provider
MCP server
LLM pretending to be a PA system

To Chukwa, these are source definitions.

The source definition and source implementation handle transport.

The workflow decides how the result enters context.

The kernel only applies final WorldPatch.

WorldPatch examples

Example 1 — Toy vending machine dispenses candy

Initial world state, conceptually:

{
  "entities": {
    "bob": {
      "state": "standing near the vending machine",
      "memory": ""
    },
    "vending_machine": {
      "state": "contains one candy bar"
    }
  }
}

HTTP tool result:

{
  "status": "dispensed",
  "remaining": 0,
  "message": "A candy bar was dispensed."
}

Final LLM WorldPatch:

{
  "narration": "Bob presses the vending-machine button. A candy bar drops into the tray.",
  "effects": [
    {
      "op": "set_entity_state",
      "entity_id": "bob",
      "state": "holding a candy bar"
    },
    {
      "op": "append_entity_memory",
      "entity_id": "bob",
      "content": "I bought a candy bar from the vending machine."
    },
    {
      "op": "set_entity_state",
      "entity_id": "vending_machine",
      "state": "empty"
    }
  ]
}

Kernel action:

validate bob exists
validate vending_machine exists
validate operations are allowed
apply state/memory changes to working world
stage audit event

Example 2 — Toy vending machine is empty

HTTP tool result:

{
  "status": "empty",
  "remaining": 0,
  "message": "No candy bars remain."
}

Final LLM WorldPatch:

{
  "narration": "Bob presses the vending-machine button, but no candy comes out.",
  "effects": [
    {
      "op": "append_entity_memory",
      "entity_id": "bob",
      "content": "I tried the vending machine, but it was empty."
    }
  ]
}

The vending-machine HTTP response did not mutate the world. The LLM’s final patch mutated Bob’s memory.

Example 3 — Agent ignores vending machine

Available tool:

{
  "name": "buy_candy",
  "description": "Buy candy from the vending machine using the acting agent's money."
}

World context:

Bob has no money.
Bob has three candy bars in his pockets.
A vending machine is nearby.

Model emits no tool call.

Final LLM WorldPatch:

{
  "narration": "Bob ignores the vending machine and eats one of the candy bars from his pocket.",
  "effects": [
    {
      "op": "set_entity_state",
      "entity_id": "bob",
      "state": "eating a candy bar from his pocket"
    }
  ]
}

Expected trace:

no source invocation for buy_candy
one final LLM generation source invocation
one linked LLM call
one applied WorldPatch

Example 4 — Toy weather API gets colder over turns

Ambient weather source result on turn 1:

{
  "temperature_f": 72,
  "condition": "sunny",
  "message": "Warm and sunny."
}

Ambient weather source result on turn 2:

{
  "temperature_f": 64,
  "condition": "windy",
  "message": "A cold front is arriving."
}

Ambient weather source result on turn 3:

{
  "temperature_f": 55,
  "condition": "cold",
  "message": "The cold front has settled over the park."
}

These results are visible in source invocation traces and injected context.

They do not directly mutate world state.

A final LLM patch may react to the cold:

{
  "narration": "Bob pulls his jacket tighter as the temperature drops.",
  "effects": [
    {
      "op": "set_entity_state",
      "entity_id": "bob",
      "state": "cold and walking through the park"
    }
  ]
}

Example 5 — Public announcement system occasionally speaks

Ambient PA source result on turn 1:

{
  "announcements": []
}

Ambient PA source result on turn 2:

{
  "announcements": [
    "Attention park visitors: the east vending area is closed for maintenance."
  ]
}

Ambient PA source result on turn 3:

{
  "announcements": []
}

The PA source is invoked automatically because the workflow declared it.

The LLM sees the non-empty announcement on turn 2 and may decide Bob no longer tries the vending machine.

No direct PA-to-world mutation exists.

Example 6 — Bob’s phone receives inbound spam

Ambient phone inbox source result:

{
  "messages": [
    {
      "from": "Unknown",
      "body": "Limited time offer: free candy coupons!",
      "kind": "spam"
    }
  ]
}

This is injected into Bob’s context.

Bob may ignore it, get distracted by it, or use the model-elected send_text tool.

The inbound message itself does not mutate world state unless the final LLM patch writes memory/state.

Example 7 — Bob and ant both act in the same committed turn

At T0:

Bob is hungry beside a vending machine.
The vending machine contains one candy bar.
An ant is hungry on a plate.
A crumb is on the plate.

Bob’s workflow emits one WorldPatch:

{
  "narration": "Bob buys a candy bar from the vending machine.",
  "effects": [
    {
      "op": "set_entity_state",
      "entity_id": "bob",
      "state": "holding a candy bar"
    },
    {
      "op": "set_entity_state",
      "entity_id": "vending_machine",
      "state": "empty"
    }
  ]
}

The ant’s workflow emits a separate WorldPatch:

{
  "narration": "The ant reaches the crumb and eats it.",
  "effects": [
    {
      "op": "set_entity_state",
      "entity_id": "ant",
      "state": "fed, standing where the crumb was"
    },
    {
      "op": "set_entity_state",
      "entity_id": "crumb",
      "state": "gone"
    }
  ]
}

At T1:

Bob has the candy bar.
The vending machine is empty.
The ant is fed.
The crumb is gone.

One chronon.

Two patches.

One committed turn.

New conceptual objects

Cognition workflow

A content-addressed JSON document.

A workflow is explicit scenario data. The kernel does not invent one.

Suggested shape:

{
  "version": 1,
  "execution": "per_subject_ordered",
  "ambient_sources": [
    {
      "id": "park_weather",
      "source_ref": {
        "hash": "<toy-weather-source-hash>"
      },
      "run": "once_per_turn",
      "scope": {
        "environment_label": "park"
      },
      "visible_to": {
        "environment_label": "park"
      },
      "request_template": {
        "environment_label": "park",
        "turn": {
          "$from": "/world/attempted_turn"
        },
        "simulation_time": {
          "$from": "/world/simulation_time"
        }
      },
      "result_schema_ref": {
        "hash": "<weather-result-schema-hash>"
      },
      "inject_as": "/ambient/environments/park/weather"
    },
    {
      "id": "park_pa",
      "source_ref": {
        "hash": "<toy-pa-source-hash>"
      },
      "run": "once_per_turn",
      "scope": {
        "entity_id": "park_pa_speaker"
      },
      "visible_to": {
        "environment_label": "park"
      },
      "request_template": {
        "speaker_id": "park_pa_speaker",
        "turn": {
          "$from": "/world/attempted_turn"
        }
      },
      "result_schema_ref": {
        "hash": "<pa-result-schema-hash>"
      },
      "inject_as": "/ambient/environments/park/pa"
    },
    {
      "id": "bob_phone_inbox",
      "source_ref": {
        "hash": "<toy-phone-inbox-source-hash>"
      },
      "run": "before_subject_workflow",
      "scope": {
        "entity_id": "bob_phone"
      },
      "visible_to": {
        "entity_id": "bob"
      },
      "request_template": {
        "owner_entity_id": "bob",
        "phone_entity_id": "bob_phone",
        "turn": {
          "$from": "/world/attempted_turn"
        }
      },
      "result_schema_ref": {
        "hash": "<phone-inbox-result-schema-hash>"
      },
      "inject_as": "/ambient/entities/bob/phone/inbox"
    }
  ],
  "nodes": [
    {
      "id": "act",
      "type": "llm_tool_loop",
      "llm_source_ref": {
        "hash": "<llm-source-hash>"
      },
      "prompt_template": {
        "messages": [
          {
            "role": "system",
            "content": "You are controlling the acting subject. You may call available tools only if the subject actually chooses to use them. Ambient context is information the subject may observe; tools are optional actions. Return either a tool call or a final WorldPatch."
          },
          {
            "role": "user",
            "content": "World:\n{{world.projection}}\n\nActing subject:\n{{subject.rendered}}\n\nAmbient context:\n{{ambient.visible}}\n\nAvailable tools:\n{{tools.available}}\n\nReturn either a tool call or a final WorldPatch."
          }
        ]
      },
      "available_tools": [
        {
          "name": "buy_candy",
          "description": "Use only if the acting subject chooses to buy candy from the vending machine.",
          "source_ref": {
            "hash": "<http-vending-source-hash>"
          },
          "arguments_schema_ref": {
            "hash": "<buy-candy-arguments-schema-hash>"
          },
          "result_schema_ref": {
            "hash": "<vending-result-schema-hash>"
          }
        },
        {
          "name": "use_drinking_fountain",
          "description": "Use only if the acting subject chooses to press the drinking fountain button.",
          "source_ref": {
            "hash": "<http-drinking-fountain-source-hash>"
          },
          "arguments_schema_ref": {
            "hash": "<drinking-fountain-arguments-schema-hash>"
          },
          "result_schema_ref": {
            "hash": "<drinking-fountain-result-schema-hash>"
          }
        },
        {
          "name": "send_text",
          "description": "Use only if the acting subject chooses to send a text message from an available phone.",
          "source_ref": {
            "hash": "<dummy-send-text-source-hash>"
          },
          "arguments_schema_ref": {
            "hash": "<send-text-arguments-schema-hash>"
          },
          "result_schema_ref": {
            "hash": "<send-text-result-schema-hash>"
          }
        }
      ],
      "max_generation_attempts": 3,
      "max_tool_calls": 3,
      "final_schema_ref": {
        "hash": "<world-patch-schema-hash>"
      }
    }
  ],
  "apply": {
    "from": "act.final"
  }
}

Rules:

max_generation_attempts is required
max_tool_calls is required for llm_tool_loop
no implicit workflow values
no hidden runtime workflow
no direct source-to-world commit
ambient sources run only according to declared run mode
model-elected tools run only when the model emits valid permitted tool calls

Ambient source binding

Suggested Rust shape:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmbientSourceBinding {
    pub id: HumanId,
    pub source_ref: ContentHashRef,
    pub run: AmbientRunMode,
    pub scope: AmbientScope,
    pub visible_to: AmbientVisibility,
    pub request_template: serde_json::Value,
    pub result_schema_ref: Option<ContentHashRef>,
    pub inject_as: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmbientRunMode {
    OncePerTurn,
    BeforeSubjectWorkflow,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmbientScope {
    World,
    Environment {
        environment_label: String,
    },
    Entity {
        entity_id: String,
    },
    ActingSubject,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmbientVisibility {
    AllSubjects,
    Environment {
        environment_label: String,
    },
    Entity {
        entity_id: String,
    },
    ActingSubject,
}

Do not add cron.

Do not add a scheduler engine.

ToolLoopOutput

The LLM node emits one of two shapes.

Tool call:

{
  "kind": "tool_call",
  "tool_call": {
    "name": "buy_candy",
    "arguments": {
      "actor_id": "bob",
      "machine_id": "vending_machine",
      "button": "C"
    }
  }
}

Final patch:

{
  "kind": "final_patch",
  "patch": {
    "narration": "Bob ignores the vending machine.",
    "effects": []
  }
}

For this ticket, support one model-elected tool call per LLM generation.

After a tool result is returned to the LLM context, the model may emit another tool call or emit final patch, up to max_tool_calls.

Do not implement parallel tool calls in this ticket.

Response source

A content-addressed source definition.

LLM source:

{
  "version": 1,
  "label": "chat_router",
  "interface": {
    "name": "llm_chat_completions",
    "model": "@chat",
    "schema_delivery": "response_format"
  }
}

HTTP JSON source:

{
  "version": 1,
  "label": "toy_vending_machine",
  "interface": {
    "name": "http_json",
    "method": "POST",
    "url_env": "CHUKWA_TOY_VENDING_URL",
    "path": "/buy_candy",
    "timeout_ms": 5000
  }
}

Toy weather source:

{
  "version": 1,
  "label": "toy_park_weather",
  "interface": {
    "name": "http_json",
    "method": "POST",
    "url_env": "CHUKWA_TOY_WEATHER_URL",
    "path": "/weather",
    "timeout_ms": 5000
  }
}

Toy PA source:

{
  "version": 1,
  "label": "toy_park_pa",
  "interface": {
    "name": "http_json",
    "method": "POST",
    "url_env": "CHUKWA_TOY_PA_URL",
    "path": "/announcement",
    "timeout_ms": 5000
  }
}

Toy phone inbox source:

{
  "version": 1,
  "label": "toy_phone_inbox",
  "interface": {
    "name": "http_json",
    "method": "POST",
    "url_env": "CHUKWA_TOY_PHONE_URL",
    "path": "/inbox",
    "timeout_ms": 5000
  }
}

Dummy send-text source:

{
  "version": 1,
  "label": "dummy_send_text",
  "interface": {
    "name": "http_json",
    "method": "POST",
    "url_env": "CHUKWA_TOY_PHONE_URL",
    "path": "/send",
    "timeout_ms": 5000
  }
}

Future MCP source shape, not implemented here:

{
  "version": 1,
  "label": "weather_mcp",
  "interface": {
    "name": "mcp_tool",
    "server_ref": {
      "hash": "<mcp-server-config-hash>"
    },
    "tool_name": "get_forecast",
    "timeout_ms": 5000
  }
}

The future MCP shape is included to keep the abstraction honest. Do not implement MCP execution in this ticket.

Source invocation

A durable runtime record for one source call.

This includes:

each LLM generation
each ambient context source call
each HTTP JSON model-elected tool call
future MCP tool calls

For LLM invocations, link to llm_call_id.

For HTTP JSON invocations, record request/response directly.

Suggested fields:

source_invocation_id
attempt_id
world_slug
attempted_turn
invocation_seq
workflow_hash
workflow_node_id
workflow_subject_entity_id
source_hash
invocation_kind              -- llm_generation | ambient_context | model_elected_tool
ambient_source_id
tool_name
parent_source_invocation_id  -- for tool calls emitted by an LLM generation
status
failure_class
failure_message
started_at
ended_at
duration_ms
request_json
response_json
response_text
response_headers
http_status
llm_call_id
input_schema_hash
output_schema_hash
validation_status
logical_generation_attempt
tool_loop_round
model_output_kind            -- tool_call | final_patch | invalid
metadata

Migration / storage work

Add:

migrations/0006_cognition_workflows.sql

This migration may be destructive for cognition-profile/scenario-runtime shape. That is acceptable.

Domain requirement

0006 must not introduce label_text.

migrations/0005_human_id_grammar.sql replaced old label grammar. Use:

human_id_text for workflow node ids, ambient source ids, human-facing labels, world slugs where current schema expects human id grammar
entity_id_text for entity ids
TEXT for opaque external ids, URLs, provider names, tool names, error strings

Review gate:

rg "label_text" migrations/0006_cognition_workflows.sql

Expected: no matches.

New content-addressed tables

Exact names flexible, but preferred:

CREATE TABLE json_schemas (
    hash       sha256_hex PRIMARY KEY,
    content    JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE response_sources (
    hash       sha256_hex PRIMARY KEY,
    content    JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE cognition_workflows (
    hash       sha256_hex PRIMARY KEY,
    content    JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Cognition profiles

Update cognition_profiles so executable profiles reference workflows.

Preferred new shape:

cognition_profiles.workflow_hash NOT NULL REFERENCES cognition_workflows(hash)

Drop or make unreachable old columns:

perceive_system_hash
intend_system_hash
adjudicate_system_hash
adjudication_schema_hash
adjudication_retry_budget

If it is cleaner to drop/recreate cognition_profiles during the destructive migration, do it.

Source invocations

Add:

CREATE TYPE source_invocation_status AS ENUM (
    'running',
    'succeeded',
    'failed',
    'interrupted'
);

CREATE TYPE source_invocation_kind AS ENUM (
    'llm_generation',
    'ambient_context',
    'model_elected_tool'
);

Suggested table:

CREATE TABLE source_invocations (
    source_invocation_id UUID PRIMARY KEY,

    attempt_id UUID NOT NULL,
    world_slug human_id_text NOT NULL,
    attempted_turn BIGINT NOT NULL CHECK (attempted_turn >= 1),
    invocation_seq INT NOT NULL CHECK (invocation_seq >= 1),

    workflow_hash sha256_hex REFERENCES cognition_workflows(hash),
    workflow_node_id human_id_text,
    workflow_subject_entity_id entity_id_text,
    source_hash sha256_hex NOT NULL REFERENCES response_sources(hash),

    invocation_kind source_invocation_kind NOT NULL,
    ambient_source_id human_id_text,
    tool_name TEXT,
    parent_source_invocation_id UUID REFERENCES source_invocations(source_invocation_id),

    input_schema_hash sha256_hex REFERENCES json_schemas(hash),
    output_schema_hash sha256_hex REFERENCES json_schemas(hash),

    llm_call_id UUID REFERENCES llm_calls(llm_call_id),

    logical_generation_attempt INT CHECK (
        logical_generation_attempt IS NULL OR logical_generation_attempt >= 1
    ),
    tool_loop_round INT CHECK (
        tool_loop_round IS NULL OR tool_loop_round >= 0
    ),
    model_output_kind TEXT,

    status source_invocation_status NOT NULL DEFAULT 'running',
    failure_class TEXT,
    failure_message TEXT,

    started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    ended_at TIMESTAMPTZ,
    duration_ms BIGINT CHECK (duration_ms IS NULL OR duration_ms >= 0),

    request_json JSONB NOT NULL,
    response_json JSONB,
    response_text TEXT,
    response_headers JSONB NOT NULL DEFAULT '{}'::jsonb,
    http_status INT,

    validation_status TEXT,
    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,

    UNIQUE (attempt_id, invocation_seq),

    CONSTRAINT source_invocations_attempt_fk
        FOREIGN KEY (world_slug, attempt_id)
        REFERENCES attempts(world_slug, attempt_id)
        ON DELETE CASCADE
);

Add indexes:

CREATE INDEX source_invocations_attempt_seq_idx
    ON source_invocations(attempt_id, invocation_seq);

CREATE INDEX source_invocations_world_time_idx
    ON source_invocations(world_slug, started_at DESC);

CREATE INDEX source_invocations_workflow_idx
    ON source_invocations(workflow_hash);

CREATE INDEX source_invocations_source_idx
    ON source_invocations(source_hash);

CREATE INDEX source_invocations_kind_idx
    ON source_invocations(invocation_kind);

CREATE INDEX source_invocations_parent_idx
    ON source_invocations(parent_source_invocation_id);

CREATE INDEX source_invocations_llm_call_idx
    ON source_invocations(llm_call_id);

CREATE INDEX source_invocations_subject_idx
    ON source_invocations(world_slug, workflow_subject_entity_id);

Attempt summary fields

Add:

ALTER TABLE attempts
    ADD COLUMN last_source_invocation_id UUID,
    ADD COLUMN source_invocation_count INT NOT NULL DEFAULT 0 CHECK (source_invocation_count >= 0),
    ADD COLUMN source_trace_summary JSONB NOT NULL DEFAULT '{}'::jsonb;

Add deferred FK if practical:

ALTER TABLE attempts
    ADD CONSTRAINT attempts_last_source_invocation_fk
    FOREIGN KEY (last_source_invocation_id)
    REFERENCES source_invocations(source_invocation_id)
    DEFERRABLE INITIALLY DEFERRED;

LLM trace provenance reshaping

llm_calls currently has phase-shaped provenance. Rewrite it to workflow/source provenance.

Add or replace with:

source_invocation_id UUID REFERENCES source_invocations(source_invocation_id)
workflow_hash sha256_hex REFERENCES cognition_workflows(hash)
workflow_node_id human_id_text
workflow_subject_entity_id entity_id_text
logical_generation_attempt INT
tool_loop_round INT
model_output_kind TEXT

Drop or make historical-only/non-live:

phase
profile_label
perceive_system_hash
intend_system_hash
adjudicate_system_hash
adjudication_schema_hash
fallback_of_call_id as default behavior

No new runtime write may populate old component hash fields.

Audit/timeline linkage

Update attempt_timeline_events:

ALTER TABLE attempt_timeline_events
    ADD COLUMN source_invocation_id UUID REFERENCES source_invocations(source_invocation_id),
    ADD COLUMN cognition_workflow_hash sha256_hex REFERENCES cognition_workflows(hash),
    ADD COLUMN workflow_node_id human_id_text,
    ADD COLUMN workflow_subject_entity_id entity_id_text,
    ADD COLUMN patch_seq INT CHECK (patch_seq IS NULL OR patch_seq >= 1);

Update world_audit_events:

ALTER TABLE world_audit_events
    ADD COLUMN source_invocation_id UUID REFERENCES source_invocations(source_invocation_id),
    ADD COLUMN cognition_workflow_hash sha256_hex REFERENCES cognition_workflows(hash),
    ADD COLUMN response_source_hash sha256_hex REFERENCES response_sources(hash),
    ADD COLUMN workflow_node_id human_id_text,
    ADD COLUMN workflow_subject_entity_id entity_id_text,
    ADD COLUMN patch_seq INT CHECK (patch_seq IS NULL OR patch_seq >= 1);

Drop or stop live-writing old audit provenance columns:

profile_label
perceive_system_hash
intend_system_hash
adjudicate_system_hash
adjudication_schema_hash

Drop indexes that only support old component-hash event lookup if the columns are dropped.

Do not remove or flatten existing LLM trace tables. Reshape their provenance.

Cargo dependency work

Add a JSON Schema validation dependency to Cargo.toml.

The implementer may choose the crate/version consistent with the repo’s Rust and dependency constraints.

Requirements:

no hand-rolled JSON Schema validator
schema validation available for generic serde_json::Value
validation errors surfaced in source invocation / LLM artifacts

File-level implementation matrix

src/scenarios.rs

Replace CognitionProfile old fields:

perceive_system
intend_system
adjudicate_system
adjudication_schema
adjudication_retry_budget

with a workflow-bearing profile shape.

A scenario remains a content-addressed assembly of environments, entities, and cognition profiles, but a cognition profile now resolves to an explicit workflow.

Remove old validation helpers for perceive/intend/adjudicate/adjudication_schema or make them unreachable from production.

Add workflow validation entry points.

src/scenario_store/mod.rs

Replace old component refs:

PerceiveSystemRef
IntendSystemRef
AdjudicateSystemRef
AdjudicationSchemaRef

with:

JsonSchemaRef
ResponseSourceRef
CognitionWorkflowRef

Reshape CognitionProfileInput so it references a workflow.

Remove old trait methods:

put_perceive_system
put_intend_system
put_adjudicate_system
put_adjudication_schema
get_perceive_system
get_intend_system
get_adjudicate_system
get_adjudication_schema

Add:

put_json_schema
put_response_source
put_cognition_workflow
get_json_schema
get_response_source
get_cognition_workflow

Update ComponentKind.

src/scenario_store/postgres.rs

Implement new component model.

Delete old resolver paths that assemble cognition profiles from four old subcomponents.

Update assemble_scenario and fork/derivation paths to validate workflow executability.

Update component listing/count/details/uses for the new component kinds.

src/scenario_store/memory.rs

Mirror the Postgres store behavior for tests.

Do not preserve old component maps except in tests that assert old inputs are rejected.

src/canonical_json.rs

Remove live use of old hashers:

canonical_perceive_system_hash
canonical_intend_system_hash
canonical_adjudicate_system_hash
canonical_adjudication_schema_hash

Add:

canonical_json_schema_hash
canonical_response_source_hash
canonical_cognition_workflow_hash

Reshape canonical_cognition_profile_hash to hash the workflow reference, not four old components.

src/minds.rs

Do not keep this as the hardcoded cognition pipeline module.

Either delete it or reduce it to reusable prompt/render helpers used by the workflow runner.

Remove production-live use of:

Adjudication
AdjudicationOutcome
validate_adjudication
perceive
intend
adjudicate
JsonCompletion<Adjudication>

The new workflow/tool-loop runner may live in src/workflow.rs, src/cognition.rs, or another module, but production must not call the old functions.

src/kernel.rs

Replace hardcoded cognition execution with workflow execution.

Replace apply_adjudication with apply_world_patch.

Replace AdjudicationApplied with WorldPatchApplied.

Stage one audit event per accepted WorldPatch.

Allow multiple WorldPatch values per attempted turn.

Commit exactly one turn snapshot per attempted turn.

Remove old component-hash audit provenance logic.

src/world_patch.rs

New module.

Define:

WorldPatch
WorldEffect
WorldPatchApplied
WorldPatchError

Implement:

pub fn validate_world_patch(world: &World, patch: &WorldPatch) -> Result<(), WorldPatchError>;

pub fn apply_world_patch(
    world: &mut World,
    patch: &WorldPatch,
) -> io::Result<WorldPatchApplied>;

Allowed ops only:

set_entity_state
append_entity_memory
set_environment_content

src/workflow.rs

New module.

Define:

CognitionWorkflow
AmbientSourceBinding
AmbientRunMode
AmbientScope
AmbientVisibility
WorkflowNode
LlmToolLoopNode
AvailableTool
ToolLoopOutput
WorkflowApply

Implement structural validation.

src/sources.rs

New module.

Define:

#[async_trait]
pub trait ResponseSource: Send + Sync {
    async fn invoke_json(
        &self,
        ctx: &SourceInvocationContext,
        request: SourceRequest,
    ) -> Result<SourceOutput, SourceError>;
}

Implement:

LlmChatSource
HttpJsonSource

Do not implement MCP execution in this ticket.

Design the registry so McpToolSource is additive later.

src/llm.rs

Keep rich streaming trace.

Remove default execute_with_fallback path.

Allow explicit prompt-only schema source only if explicitly configured.

Replace LlmCognitionContext/LlmPhase provenance with workflow/source invocation provenance.

Persist parsed ToolLoopOutput and accepted WorldPatch artifacts, not just parse errors.

src/llm_trace.rs

Replace LlmCognitionContext with workflow/source invocation context.

Remove phase-based primary identity.

Track:

source_invocation_id
workflow_hash
workflow_node_id
workflow_subject_entity_id
logical_generation_attempt
tool_loop_round
model_output_kind

src/world_store/mod.rs

Add SourceInvocation DTOs and store trait methods.

Update attempt status/read models with source invocation summary fields.

Update audit event input/output structs with:

source_invocation_id
cognition_workflow_hash
workflow_node_id
workflow_subject_entity_id
patch_seq
response_source_hash

Remove old component hash fields from new live write paths.

src/world_store/postgres.rs

Implement source invocation persistence.

Update LLM call persistence to include source invocation/workflow node provenance.

Update audit event persistence.

Update attempt summaries.

Update list/detail read paths.

src/world_store/memory.rs

Mirror Postgres behavior.

Tests should exercise both memory and Postgres paths where existing suite conventions allow.

src/resource_catalog.rs

Remove old browseable resource kinds:

PerceiveSystem
IntendSystem
AdjudicateSystem
AdjudicationSchema

Add:

JsonSchema
ResponseSource
CognitionWorkflow
SourceInvocation

Update reference rules for:

workflow_hash
cognition_workflow_hash
source_hash
response_source_hash
json_schema_hash
input_schema_hash
output_schema_hash
final_schema_hash
arguments_schema_hash
result_schema_hash
source_invocation_id
parent_source_invocation_id
llm_call_id

src/read_models.rs

Add list/detail payloads for new resources.

Update attempt detail to include source invocations.

Update LLM call detail to link source invocation.

Update event detail to link source invocation, workflow, source, and LLM call.

Remove old component read models.

src/render.rs

Update structural key mapping.

Remove mappings for:

perceive_system
intend_system
adjudicate_system
adjudication_schema

Add mappings for new workflow/source/schema/source-invocation keys.

src/server.rs

Remove old graph-browser routes:

/perceive-systems
/perceive-systems/hash/:hash
/perceive-systems/hash/:hash/uses

/intend-systems
/intend-systems/hash/:hash
/intend-systems/hash/:hash/uses

/adjudicate-systems
/adjudicate-systems/hash/:hash
/adjudicate-systems/hash/:hash/uses

/adjudication-schemas
/adjudication-schemas/hash/:hash
/adjudication-schemas/hash/:hash/uses

Add:

/json-schemas
/json-schemas/hash/:hash
/json-schemas/hash/:hash/uses

/response-sources
/response-sources/hash/:hash
/response-sources/hash/:hash/uses

/cognition-workflows
/cognition-workflows/hash/:hash
/cognition-workflows/hash/:hash/uses

/source-invocations
/source-invocations/:source_invocation_id
/attempts/:attempt_id/source-invocations

All routes remain behind graph UI auth and support ?format=json.

Remove old unknown-component route/error mappings such as:

UNKNOWN_PERCEIVE_SYSTEM
UNKNOWN_INTEND_SYSTEM
UNKNOWN_ADJUDICATE_SYSTEM
UNKNOWN_ADJUDICATION_SCHEMA

Add appropriate new errors:

UNKNOWN_JSON_SCHEMA
UNKNOWN_RESPONSE_SOURCE
UNKNOWN_COGNITION_WORKFLOW
UNKNOWN_SOURCE_INVOCATION

src/mcp.rs

Remove old MCP tools:

put_perceive_system
put_intend_system
put_adjudicate_system
put_adjudication_schema
get_perceive_system
get_intend_system
get_adjudicate_system
get_adjudication_schema

Add:

put_json_schema
put_response_source
put_cognition_workflow
get_json_schema
get_response_source
get_cognition_workflow
list_source_invocations
get_source_invocation

Update put_cognition_profile and assemble_scenario input parsing to the new workflow-bearing shape.

Update create-world/helper paths that currently build inline old CognitionProfileInput.

src/mcp/tests.rs

Remove old MCP tests for perceive/intend/adjudicate/adjudication-schema tools.

Add tests proving old tools are absent/rejected and new tools work.

Update tool partition/full-surface tests.

src/test_fixtures.rs

Rewrite early.

Do not keep old PERCEIVE_SYSTEM, INTEND_SYSTEM, ADJUDICATE_SYSTEM, or old five-field adjudication schema as canonical cognition truth.

Add fixture helpers for:

simple LLM-only workflow
ambient weather workflow
ambient PA workflow
Bob phone inbox workflow
vending/drinking/send-text tool workflow
Bob + ant same-turn multiple patch workflow

docs/terms.md

Update despite usual docs deferral because this file currently encodes obsolete runtime semantics.

Replace old terms around:

adjudication
agent_state_after
entity_mutations
minds::adjudicate retry

with:

ToolLoopOutput
WorldPatch
model-content retry
source invocation
ambient context source
model-elected tool
multiple WorldPatches per turn

tests/*

Update existing tests that assert old routes/tools/component kinds.

Add tests for new workflow, source invocation, ambient source, tool-loop, WorldPatch, and cleanup behavior.

migrations/*

0006 may destructively reshape old cognition tables.

Do not edit historical migrations unless existing project convention requires it.

Do ensure 0006 removes or neutralizes old live surfaces.

Kernel execution model

New turn execution:

start attempted turn N
load T(N-1) into working world

run once_per_turn ambient sources
inject ambient results into turn context

for each workflow subject in deterministic order:
    run before_subject_workflow ambient sources visible to this subject
    inject ambient results into subject context

    execute subject workflow
    obtain final WorldPatch

    validate WorldPatch against current working world
    apply WorldPatch to in-memory working world
    assign patch_seq
    stage audit event and touched-entity records

after all workflow subjects:
    commit exactly one turn snapshot T(N)
    commit all staged audit events
    clear attempt lease

Do not commit one database/world turn per subject.

Do not commit one database/world turn per WorldPatch.

Do not require one global patch for all entities.

No global all-agent adjudication in this ticket.

No source result directly writes to world state.

No external API directly writes to world state.

No old Adjudication application path remains live.

Ambient context runner

Implement declared ambient source execution.

Semantics:

for each once_per_turn ambient source:
    render request_template
    invoke source
    validate result schema
    store source invocation
    inject result into turn ambient context

for each subject:
    for each before_subject_workflow ambient source visible to this subject:
        render request_template
        invoke source
        validate result schema
        store source invocation
        inject result into subject ambient context

Rules:

missing template path fails loudly
source failure fails before world commit
ambient results do not directly mutate world
empty source results are traced
prompt rendering may omit empty ambient prose, but raw context remains durable

LLM tool-loop runner

Implement LlmToolLoopNode.

Semantics:

build initial LLM messages from world, subject, ambient context, and available tools

loop:
    call LLM source
    parse as ToolLoopOutput

    if invalid model output:
        use existing retry semantics

    if output is final_patch:
        validate WorldPatch
        if invalid:
            use existing retry semantics
        else:
            return WorldPatch

    if output is tool_call:
        validate tool name and arguments
        if invalid:
            use existing retry semantics

        execute corresponding source
        capture source result
        append tool result to LLM context
        continue until max_tool_calls

If max_tool_calls is exhausted before final patch, fail the attempt.

Do not call any available tool unless the model emits a valid permitted tool call.

Scenario store / assembly validation

assemble_scenario must reject non-executable workflow contracts.

Required validation:

every cognition profile has a workflow hash
workflow hash resolves
workflow JSON parses
workflow version = 1
workflow execution is supported
node IDs are unique
ambient source IDs are unique
source refs resolve
schema refs resolve
every ambient source has id, source_ref, run, scope, visible_to, request_template, inject_as
every ambient run mode is supported
every ambient scope is structurally valid
every ambient visibility is structurally valid
every llm_tool_loop has max_generation_attempts >= 1
every llm_tool_loop has max_tool_calls >= 0
every available tool has name, description, source ref, and argument schema
tool names are unique within a node
final schema resolves and is the WorldPatch schema or structurally equivalent to it
apply.from references a final output produced by a node

Do not call external sources during assembly.

Do not call LLMs during assembly.

Graph browser / resource catalog

The graph browser is a registry-governed substrate browser. Preserve that pattern from the prior graph-browser work .

Add resource kinds:

JsonSchema
ResponseSource
CognitionWorkflow
SourceInvocation

Remove resource kinds:

PerceiveSystem
IntendSystem
AdjudicateSystem
AdjudicationSchema

New routes:

/json-schemas
/json-schemas/hash/:hash
/json-schemas/hash/:hash/uses

/response-sources
/response-sources/hash/:hash
/response-sources/hash/:hash/uses

/cognition-workflows
/cognition-workflows/hash/:hash
/cognition-workflows/hash/:hash/uses

/source-invocations
/source-invocations/:source_invocation_id
/attempts/:attempt_id/source-invocations

All routes must:

require graph UI auth
support ?format=json
use registry-governed list/detail rendering
link to related resources structurally

Source invocation detail should link to:

attempt
world
workflow
workflow subject
response source
parent source invocation, if present
LLM call, if llm_call_id exists

Operator MCP surface

Add/update MCP tools:

put_json_schema
put_response_source
put_cognition_workflow
get_json_schema
get_response_source
get_cognition_workflow
get_component
list_components
list_uses_of
list_source_invocations
get_source_invocation

Remove old cognition component tools.

Do not implement MCP consumer/source execution in this ticket.

MCP consumer support is the next likely ticket. This ticket must make that next ticket additive.

Out of scope

Do not implement:

MCP consumer execution
replay
replay guarantees
direct source-result world mutation
source-result-to-patch mapping
deterministic tool-result patching
cron/scheduler engine
create/delete entity effects
inventory systems
physics
collision resolution
resource allocation
fairness policy
global all-intents adjudication
parallel tool calls
concurrent agent execution
source switching after LLM failure
silent schema-delivery degradation
external transaction reconciliation
idempotency
real weather API integration
real SMS integration
stock/news API implementation
UI editing
native OpenAI tool-calling protocol

Phase reporting protocol

At the end of every phase, post a ticket comment containing:

phase letter and commit SHA
branch state and recent log
files created/modified/deleted
tests run and counts
deviations from this ticket with rationale
surfaced-but-not-blocking observations
deployability statement for that phase
next phase being started

Do not pause between phases unless blocked.

Status comments are visibility, not gates.

Phase plan

Phase A — Inventory and removal plan

Before adding new workflow/source code, post a short implementation note listing every old cognition surface found and planned disposition:

delete
replace
rename
test-only
historical migration only

The inventory must include:

src/scenarios.rs
src/scenario_store/mod.rs
src/scenario_store/postgres.rs
src/scenario_store/memory.rs
src/canonical_json.rs
src/minds.rs
src/kernel.rs
src/llm.rs
src/llm_trace.rs
src/world_store/*
src/resource_catalog.rs
src/read_models.rs
src/render.rs
src/server.rs
src/mcp.rs
src/mcp/tests.rs
src/test_fixtures.rs
tests/*
docs/terms.md
migrations/*

Acceptance:

inventory posted
each old cognition surface has planned disposition
no implementation pause required after posting

Phase B — Migration and component skeleton

Add migrations/0006_cognition_workflows.sql.

Add new store/component DTOs for:

JSON schemas
response sources
cognition workflows
source invocations

Add Rust enum/resource scaffolding.

Add Postgres and memory-store skeleton methods.

Add JSON Schema validation dependency to Cargo.toml.

Acceptance:

migration applies cleanly
new tables/columns/FKs/indexes exist
source_invocation_kind exists
no label_text introduced in 0006
production code can no longer assemble executable profiles without workflow hashes
build passes
migration tests pass

Phase C — Store implementation and assembly validation

Implement store methods:

put_json_schema
put_response_source
put_cognition_workflow
get/list/count/details/uses for new component kinds
start/finish/fail/list/get source_invocation

Implement canonical hashers:

canonical_json_schema_hash
canonical_response_source_hash
canonical_cognition_workflow_hash

Implement workflow structural validation.

Acceptance:

component puts are idempotent
workflow validation rejects missing source refs
workflow validation rejects missing schema refs
workflow validation rejects missing workflow profiles
workflow validation rejects duplicate ambient source IDs
workflow validation rejects duplicate tool names in one node
workflow validation rejects missing ambient source inject_as
workflow validation rejects missing max_generation_attempts
workflow validation rejects missing max_tool_calls
Postgres and memory tests pass

Phase D — WorldPatch contract, apply path, and multiple patches per turn

Add WorldPatch and WorldEffect.

Implement validation and application.

Replace live apply_adjudication usage with apply_world_patch.

Remove live JsonCompletion<Adjudication> usage.

Implement turn accumulation semantics:

many WorldPatches may be applied to one working world
one committed turn snapshot is written at the end

Acceptance:

final LLM output parses as WorldPatch
WorldPatch applies entity state changes
WorldPatch appends agent memory
WorldPatch sets environment content
invalid entity ID fails
invalid environment label fails
append memory to prop fails
empty effects are allowed
no silent ID normalization exists
no live path depends on Adjudication
two workflow subjects can each produce a WorldPatch in one attempted turn
the committed turn number advances once, not once per patch
patch_seq is assigned per accepted patch
state_before semantics are per-patch, not falsely turn-start

Phase E — Source invocation layer and LLM source wrapper

Implement ResponseSource trait and source registry.

Implement source invocation persistence and summary updates.

Wrap existing LLM client as LlmChatSource.

Preserve rich LLM tracing.

Replace LlmPhase/LlmCognitionContext with workflow/source provenance.

Remove implicit structured-output degradation from the normal path.

Acceptance:

every LLM generation has a source invocation row
source invocation row is created before provider bytes are sent
every LLM source invocation links to an llm_calls row
existing LLM chunks/tokens/artifacts still persist
structured-output rejection fails loud by default
attempt source summary updates
LLM observability tests still pass or are updated to workflow node names without losing trace detail
no production-live LlmPhase primary identity remains

Phase F — LLM tool-loop node and retry refactor

Implement LlmToolLoopNode execution.

Preserve current adjudication retry behavior in the new WorldPatch-producing LLM node.

Do not introduce a second retry lane.

Acceptance:

invalid LLM JSON retries through same node/source/contract
invalid tool name retries through same node/source/contract
malformed tool arguments retry through same node/source/contract
invalid WorldPatch retries through same node/source/contract
each retry creates durable source invocation and LLM trace
no second retry lane is introduced
no model-elected tool is called unless model emits valid permitted tool call
native provider tool-calling protocol is not implemented

Phase G — HTTP JSON source and model-elected toy tools

Implement HttpJsonSource.

Requirements:

POST JSON body
require JSON response
validate result schema if provided
persist request/response/status/headers/duration
no idempotency
no replay
no external state reconciliation

Add toy HTTP handlers/fixtures for:

vending machine
drinking fountain
dummy send-text

Acceptance:

HTTP 200 JSON response succeeds
HTTP 500 fails and persists error body
non-JSON response fails
schema-invalid JSON response fails
successful source result is appended to LLM context
source result does not directly mutate world
available vending tool not called when fake LLM emits final patch
available drinking-fountain tool not called when fake LLM emits final patch
dummy send-text source is called only when fake LLM emits send_text

Phase H — Ambient context sources and toy weather/PA/phone fixtures

Implement ambient source execution.

Add toy HTTP handlers/fixtures for:

weather source
public announcement source
phone inbox source

Weather fixture behavior:

turn 1 -> warmer temperature
turn 2 -> lower temperature
turn 3 -> still lower temperature

PA fixture behavior:

some turns -> announcements: []
some turns -> announcements: ["..."]

Phone inbox fixture behavior:

some turns -> messages: []
some turns -> inbound/spam messages

Acceptance:

once-per-turn weather source runs automatically
weather source invocation appears for each turn
weather response JSON shows temperature decreasing across multiple turns
weather result is injected into relevant environment/subject context
weather source result does not directly mutate world
PA source runs automatically according to workflow binding
PA source sometimes returns empty announcements
PA source sometimes returns non-empty utterance
PA result is injected into relevant environment/subject context
phone inbox source runs automatically for Bob when bound to Bob/Bob’s phone
phone inbox can inject inbound/spam messages
phone inbox result does not directly mutate world
Bob may be influenced by phone inbox context through final WorldPatch
no model-elected tool source invocation is created merely because ambient context exists

Phase I — Combined park scenario proof

Create a production-like fixture scenario with:

park environment
Bob hungry near vending machine
Bob has phone in pocket
PA speaker mounted in park
toy weather ambient source
toy PA ambient source
toy phone inbox ambient source
vending machine tool
drinking fountain tool
send-text tool
ant on plate with crumb

Required tests:

Test 1 — Ambient weather over multiple turns

Run at least three turns.

Expected:

one weather ambient source invocation per turn
response temperatures decrease across turns
attempt/source detail shows decreasing temperature
no direct weather-to-world mutation exists

Test 2 — PA occasional utterance

Run enough turns to observe both empty and non-empty PA outputs.

Expected:

PA source invoked automatically
empty PA results are traced
non-empty PA utterance is injected into LLM context
PA output does not directly mutate world

Test 3 — Phone inbox ambient context

Configure Bob’s phone inbox source to return a spam/inbound message on a known turn.

Expected:

phone inbox source invocation exists
message JSON is injected into Bob’s context
final WorldPatch may react to message
phone inbox source does not directly mutate world

Test 4 — Available tools not called

Use fake LLM behavior where Bob receives vending/drinking/send-text tools but emits final WorldPatch without a tool call.

Expected:

no vending source invocation
no drinking-fountain source invocation
no send-text source invocation
final WorldPatch applies normally

Test 5 — Tool called once

Use fake LLM behavior where Bob emits one buy_candy tool call, receives the result, then emits final WorldPatch.

Expected:

one HTTP source invocation
source response captured
final LLM patch sees the result
world mutation comes only from final WorldPatch

Test 6 — Two agents / one candy bar

Use deterministic fake LLM behavior where two subjects both emit buy_candy.

Expected:

first tool call receives dispensed
second tool call receives empty
both source invocations are durable
final WorldPatches apply through the kernel
no direct vending-machine-to-world mutation path exists

Test 7 — Bob and ant both change same committed turn

Use fake LLM behavior where Bob gets candy and ant gets crumb in the same attempted turn.

Expected:

Bob’s workflow emits one accepted WorldPatch
ant’s workflow emits a separate accepted WorldPatch
one committed turn snapshot contains both changes
turn number increments once
audit has separate staged events/touched-entity records for each patch

Phase J — Graph browser and operator MCP tools

Expose new resources through graph browser and operator MCP tools.

Acceptance:

/cognition-workflows/hash/:hash works
/response-sources/hash/:hash works
/json-schemas/hash/:hash works
/source-invocations/:id works
attempt detail links source invocations
source invocation detail links parent source invocation if present
source invocation detail links LLM call if present
all graph routes require auth
all support ?format=json
old cognition component routes are gone or reject
operator MCP tools can list/get new components and source invocations
old MCP cognition component tools are gone or reject

Phase K — Read models, resource catalog, render cleanup

Complete graph-browser/read-model cleanup.

Acceptance:

resource catalog no longer exposes PerceiveSystem/IntendSystem/AdjudicateSystem/AdjudicationSchema
resource catalog exposes JsonSchema/ResponseSource/CognitionWorkflow/SourceInvocation
structural linker maps new workflow/source/schema/source-invocation keys
structural linker no longer maps old cognition keys
read models no longer produce old component relation sections
attempt detail includes source invocation summary
LLM call detail links source invocation
event detail links workflow/source/source invocation/LLM call

Phase L — Docs and fixture cleanup

Rewrite src/test_fixtures.rs.

Update docs/terms.md.

Acceptance:

test fixtures no longer export old canonical perceive/intend/adjudicate/adjudication-schema truth
fixtures include simple LLM workflow, ambient weather, PA, phone, tool workflow, Bob+ant same-turn workflow
docs/terms.md describes ToolLoopOutput, WorldPatch, source invocation, ambient source, model-elected tool, multiple patches per turn
docs/terms.md no longer teaches old Adjudication shape as runtime truth

Phase M — Acceptance sweep

Run full test suite and add regression tests.

Required test coverage:

schema accepted by substrate cannot bypass workflow executability validation
no scenario can run without explicit workflow
simple LLM-only workflow emits WorldPatch and commits
LLM available tool may go unused
LLM emitted tool call executes
HTTP tool result is context only
ambient HTTP source result is context only
final LLM patch is the only world mutation source
invalid final patch retries using existing retry semantics
response_format rejection fails loud by default
source invocations persist for LLM, ambient HTTP, and model-elected HTTP calls
LLM traces preserve request/messages/chunks/artifacts/tokens/errors
multiple WorldPatches can accumulate into one committed turn
old MCP tools/routes/component kinds are absent or rejected
replay is not required by any test

Phase N — Deploy and live smoke

Deploy.

Run live or production-like smoke.

Smoke 1 — Simple LLM-only workflow:

one agent
no available tools
LLM emits WorldPatch
turn commits

Smoke 2 — Available tool not used:

agent has no money and candy in pockets
vending tool available
model emits final patch without tool call
no vending source invocation exists
turn commits

Smoke 3 — Vending tool used:

agent wants candy and has money
model emits buy_candy
HTTP source returns dispensed
model emits final WorldPatch
turn commits

Smoke 4 — Ambient weather:

park weather source runs across multiple turns
temperature decreases across source responses
source invocations show the cold front
world mutation occurs only through final patches

Smoke 5 — Ambient PA:

PA source runs across multiple turns
some turns return no utterance
some turns return announcement
source invocations show both

Smoke 6 — Ambient phone inbox:

Bob phone inbox source runs
known turn returns inbound/spam message
Bob context includes message
world mutation only from final patch

Smoke 7 — Bob and ant same turn:

Bob gets candy
ant gets crumb
one committed turn contains both changes

Smoke 8 — Failure:

HTTP source returns non-JSON
source invocation fails
attempt fails before world commit
trace contains failure details

Acceptance criteria

A1 — No hidden runtime workflow

A scenario without an explicit workflow cannot assemble or run.

A2 — Hardcoded adjudication path removed

The live path no longer parses LLM output as:

JsonCompletion<Adjudication>

The live path no longer applies:

apply_adjudication

A3 — Old cognition components removed

Production-live scenario assembly, graph browser, MCP, read models, and resource catalog no longer expose:

PerceiveSystem
IntendSystem
AdjudicateSystem
AdjudicationSchema

A4 — WorldPatch is the only applyable final output

The final world mutation path accepts WorldPatch.

External source responses are never directly applied.

A5 — Multiple WorldPatches per turn

A single attempted turn may accept multiple WorldPatches.

The kernel applies each accepted patch to the working world and commits one accumulated turn snapshot.

A6 — Ambient context sources work

Ambient sources run automatically according to explicit workflow bindings.

Ambient results are injected into context.

Ambient results do not directly mutate world state.

A7 — Model-elected tool use works

Available tools do not run merely because they exist.

Tools run only after the model emits a valid permitted tool call.

A8 — HTTP JSON source works for both binding modes

The same HTTP JSON source implementation supports:

ambient context calls
model-elected tool calls

A9 — Weather proof works

Toy weather source shows decreasing temperature across multiple turns.

The decrease is visible in source invocation traces and context.

No direct weather-to-world mutation exists.

A10 — PA proof works

Toy PA source sometimes returns empty announcements and sometimes returns non-empty utterances.

Both are traced.

Non-empty utterances enter LLM context.

No direct PA-to-world mutation exists.

A11 — Phone proof works

Toy phone inbox source can inject inbound/spam messages for Bob.

Dummy send-text tool can be called only if the model emits send_text.

Inbound and outbound phone sources are traced.

No direct phone-source-to-world mutation exists.

A12 — Tool non-use works

A fake or real LLM can receive buy_candy, use_drinking_fountain, and send_text as available tools and still emit a final patch without calling any of them.

No source invocation is created for uncalled tools.

A13 — Retry behavior is preserved, not duplicated

Invalid LLM generations retry under the same node/source/contract.

No second retry lane exists.

Retries are visible as separate source invocations and LLM traces.

A14 — No implicit degradation

Structured-output rejection fails loud unless an explicitly authored source configuration chooses a different schema-delivery mode.

A15 — LLM observability survives

LLM traces remain rich and browseable.

Source invocation rows link to LLM call rows.

A16 — Source invocation lifecycle is durable

For every source call, the source invocation is durable while the call is in flight.

This applies to:

LLM generations
ambient HTTP calls
model-elected HTTP tool calls

A17 — MCP is additive next

The ticket does not implement MCP execution.

The code shape must allow a future McpToolSource without changing WorldPatch application or kernel commit semantics.

A18 — Replay is not required

No acceptance test requires replay.

No source invocation table or API claims deterministic replay.

A19 — Clean cutover

No production runtime path preserves previous cognition execution behavior outside the new workflow system.

Final source-search cleanup gate

Before proposing resolution, run:

rg "JsonCompletion<Adjudication>|apply_adjudication\\(|struct Adjudication|validate_adjudication|AdjudicationOutcome" src tests

rg "perceive_system|intend_system|adjudicate_system|adjudication_schema" src tests docs migrations/0006_cognition_workflows.sql

rg "LlmPhase|llm_phase|LlmCognitionContext" src tests migrations/0006_cognition_workflows.sql

rg "execute_with_fallback|fallback_of_call_id" src tests migrations/0006_cognition_workflows.sql

rg "put_perceive_system|put_intend_system|put_adjudicate_system|put_adjudication_schema|get_perceive_system|get_intend_system|get_adjudicate_system|get_adjudication_schema" src tests

rg "/perceive-systems|/intend-systems|/adjudicate-systems|/adjudication-schemas" src tests

rg "label_text" migrations/0006_cognition_workflows.sql

Expected:

No production live-path matches.

Allowed matches only:

historical migrations prior to 0006
tests asserting old inputs/routes/tools are rejected
docs sections explicitly marked obsolete/removed, if any

Given this ticket’s clean-cutover posture, prefer zero old-shape matches in src/ except deliberate rejection code.

Proposed resolution format

Resolution should include:

one-sentence outcome
phase summary table
commit SHAs
migration status
test counts
source-search cleanup output
smoke attempt IDs
source invocation IDs
linked LLM call IDs
vending-machine evidence
drinking-fountain evidence
send-text evidence
weather cold-front evidence
PA utterance evidence
phone inbox evidence
tool non-use evidence
multiple-WorldPatch same-turn evidence
acceptance criteria walkthrough
any surfaced follow-ups

Closing principle

Chukwa executes explicitly authored cognition workflows.

Some sources are ambient context sources and run automatically because the workflow says to include them.

Some sources are model-elected tools and run only when the LLM emits a permitted tool call.

All source outputs are JSON context.

The final LLM emits WorldPatch.

A turn may accumulate multiple WorldPatches.

Only WorldPatch mutates the world.

There are no hidden workflows, no direct source commits, no replay requirement, no legacy cognition path, and no silent degradation.

Proposed resolution

Follow-up is now committed, pushed, deployed, and externally verified.

Commit: 1d3fca1 fix(mcp): move workflow tools to consumer surface Push: gitlab/main now points at 1d3fca1. Deploy: k8s/deploy.sh completed successfully; deployment/chukwa rolled out and pod chukwa-7f46b9c9d5-j8kdq is Ready 1/1.

Live verification from https://chukwa.benac.dev/chukwa-repo.zip:

Live landing page also reports /mcp as 32 tools and /operator-mcp as 27 tools.

Verification before commit/deploy:

No workflow runtime, validation, scenario assembly, source invocation, ticketing, code navigation, git, or LLM trace behavior was changed.

History (26 events)

Sign in as a human to drive this ticket from the page, or use the MCP tools.