Tachikoma · Web-of-Trust

IRIS — a BI report produced, verified, and governed by agents

A natural-language question goes in; a quantified, illustrated, and audited banking report comes out — or is rejected. Everything that follows is real: pulled from the execution traces (ClickHouse) of a live run, every claim clickable down to the span.

Runtime Tachikoma serverless (Ray) Dispatch agent-directed, ACL-gated LLM MiniMax-M2 via LiteLLM gateway July 16, 2026

The entry point: the report's hive lifecycle, and the architect that DESIGNS the WoT from the members' brief (it derives the order + the topology of the 6 patterns, then we inject the professional prompts and materialize/register so that Loader("wot") runs it by id).

The hive lifecycle — build_report_hive

def build_report_hive(app, tracer=None, *, name="report", llm=None):
    architect = ReportArchitect(llm=llm)          # designs the WoT from the members
    hive = Hive(app, name=name, tracer=tracer, llm=llm, output_as_topic=True)
    review_in = TracedTopic(app, f"{name}.review.in")   # cross-process entry for the human decision

    b = hive.from_input(); b.source = None        # topology-only: the worker is fed via submit()
    (b.transform(create_card)                     # the card is born in "architect"
        .to_agent(architect)                      # ARCHITECT node — designs the WoT
        .cast("todo", default_gather=False)       # re-insertion point (validate/refuse free a slot)
        .to_loop(todo)                            # TODO node (card -> to_do; <=N slots)
        .transform(_to_loader_event)              # loop_id -> header
        .to_hive(Loader("wot"))                   # RUN the designed WoT natively as a sub-hive
        .transform(review)                        # produce the report + park the card at review
        .cast("review", default_gather=True, gather_mode="wait_respond",
              input_channel=review_in)            # WAIT for the human decision
        .switch()
            .case(_is_modify,   label="Revise").to_loop(modify).to_channel(hive.input)
            .case(_is_validate, label="Publish").to_loop(publish).to_cast("todo")
            .case(_is_refuse,   label="Reject").to_loop(refuse).to_cast("todo")
        .build(from_input=True))

    hive.register_llm_sink(architect)
    hive._on_worker_start = _report_env_defaults
    return hive

The architect's brief — _ARCHITECT_BRIEF

_ARCHITECT_BRIEF = (
    "Design a WEB-OF-TRUST (<loop type=\"wot\">) producing a decision-grade banking BI report. "
    "Every agent is INDEPENDENT and routes its OWN output via ACL-verified handoffs — NO coordinator. "
    "These members are AVAILABLE; YOU derive the order + topology from each one's data deps:\n"
    "  - interpret  -> parsed_query {domain, metrics, dimensions, time_range}\n"
    "  - fetch      -> data_result + data_summary   (governed ClickHouse)\n"
    "  - generate   -> report_content {title, executive_summary, sections, methodology_note}\n"
    "  - charts     -> report_html   (react: generate_code + run_code -> inline SVG)\n"
    "  - compliance -> compliance_result; on FAIL, EVALUATE-OPTIMIZE back-edge to generate\n"
    "Give every LLM agent a <frame> = <tckm domain=\"bag\"/>; goal = compliance_result."
)

The architect — design → enrich → materialize → register

async def design_report_loop(arch, query):
    """The `architect` node: the LoopArchitect DESIGNS the report WoT from the members brief."""
    from .iris_members import _MEMBERS
    # design is CACHED per (brief + members) — one architect pass (~60-90s), hash-invalidated
    key = sha256(_ARCHITECT_BRIEF + members_hash(_MEMBERS))[:16]
    if cache_hit(key):
        spec = LoopSpec.from_xml(open(cache_path(key)).read())
    else:
        goal = _ARCHITECT_BRIEF + "\n\nDesign the loop to answer THIS:\n" + query
        spec = await arch._generate(goal)         # architect derives order + 6-pattern topology
    _enrich_report_spec(spec)                     # inject the members' pro prompts + the bag frame
    loop = arch.factory.materialize(spec)
    await arch._register(spec, loop)              # into arch._loops -> Loader("wot") resolves by id
    return spec.loop_id, spec.to_xml()


class ReportArchitect:
    """The architect AS a hive agent node — takes the event, designs the WoT, returns {loop_id, wot_xml}.
    Owns the Loader("wot") binding: register_wot_type(self._loops) teaches the Loader WHERE to resolve
    a designed loop by id and HOW to run it (deploy_loop_topology, the 6-pattern dispatch)."""
    __name__ = "report_architect"

    def __init__(self, *, llm=None):
        self._llm, self._arch = llm, None         # NodeRecord is the state; LoopArchitect built lazily

    async def __call__(self, event):
        loop_id, wot_xml = await design_report_loop(self._ensure_arch(), event.get("query", ""))
        return {**event, "loop_id": loop_id, "wot_xml": wot_xml}

And the real run, in detail — the approach, the agents, the governance, and the traces clickable down to the span:

01

The approach

The principle of distribution: the LLM proposes and decides the content; deterministic code validates the contracts; the ACL authorizes each move; the human validates the output. No step is a script: the order, the links and the roles are designed at execution time by an architect agent, then executed by the framework.

Query
A business question in natural language — "Loan portfolio by branch Q4".
Architect
An LLM designs the WoT: agents, typed edges, feedback. The XML is validated by code (conformance) and cached.
Autonomous agents
Each agent sees its data, produces ITS field once, and routes itself — each hop verified by the ACL (signed biscuit).
Governance
The compliance reads the real data (bag_read) and verifies each figure by reasoning — a derived figure (ratio, gap, cumulative share) is grounded if it recomputes from the rows. Any ungrounded figure = revision.
Revision
A rejection sends the report back to the author via the feedback edge — ONE revision maximum (budget carried by the packet).
Human review
The parked report awaits the decision: publish, revise, refuse. Nothing ships without the human.

The flow, end to end

1 — It all starts with a sentence. The user types "Loan portfolio by branch Q4" into the IRIS chat. No form, no parameter: this sentence becomes the will — the original intent — and it is re-displayed to each agent, at each turn, so that none loses sight of the final goal.

2 — The architect designs, the code verifies. An LLM agent receives the brief of the available members (what each consumes, produces, its tools) and infers the topology: who talks to whom, in what order, with which pattern among the six (chain, route, parallel, orchestrate, evaluate-optimize, autonomous). Its XML is submitted to deterministic checks — edges to declared agents, valid patterns, zero orphans, no coordinator in a WoT. Failure → it loops back (4 attempts); success → the design is cached and the same question never again costs a design pass (0.8 s instead of 90 s).

3 — Autonomous agents, not a pipeline. There is no orchestrator that calls steps: each agent is deployed as a serverless node (Ray) and decides itself to route its work to the next one. Each hop is controlled by the ACL — the agent carries an ephemeral cryptographic biscuit and the gate verifies offline that this token authorizes this edge with this pattern. No valid token, no move: fail-closed.

4 — The production contract. Each agent must produce ONE named field (parsed_query, dataset, report_content, compliance_result, report_html) — only once. This field travels in the "bag" of the run, and the next agent receives it in plain text in its prompt (frame YOUR INPUT DATA: real samples, not a description). This is the most expensive lesson of the project: as long as the data was not physically in the prompt, the agents invented plausible figures — they had literally nothing to compute.

5 — Governance rejects, the feedback repairs. Before any verdict, the compliance reads the data (bag_read) and verifies each number by reasoning — a derived figure (ratio, gap between shares, cumulative share of the Top-N) is grounded if it recomputes from the rows, a section title is not an invented entity. An ungrounded figure = critical finding, and the compliance agent is not allowed to say "pass". The rejection goes back to the author via the feedback edge (evaluate-optimize) — with a budget of ONE revision carried by the packet itself, so that a disagreement never becomes an infinite loop.

6 — The human has the final word. The validated report does not publish itself: it parks at a review (a cast-and-gather point of the external hive, outside the WoT) and awaits a decision — publish, revise, refuse. In testing, this decision is injected programmatically after the real park; in production, it is a screen.

7 — Everything is provable. Each LLM turn, each tool call, each hop emits a span to ClickHouse with its payloads. This is the method for developing everything above: each bug on this page (invented figures, amnesiac agents that redid their work, feedback hops rejected silently) was found in the traces, never guessed — the decisive test being "does the expected value appear in llm.input?". The waterfall at the bottom of the page is that tool, made clickable.

02

The topology designed by the architect

This graph is not a drawing: it is the real design emitted by the architect (validated by the conformance checks, cached — one LLM pass per version of the brief). Click an agent or an edge.

CHAIN — the output feeds the next one EVALUATE-OPTIMIZE (feedback) — the rejection returns to the author, budget: 1 revision

The same design, in its XML DSL

It is THIS document that the architect emits, that the conformance checks validate, and that the cache round-trips. Its grammar: <loop type="wot"> = the WoT; <will> = the global intent; <todo> = the plan the architect gave itself; one <agent> per member — <system_prompt> (the business role), <tools> (the allowlist), <skills>, <args>{"produces": …} (the production contract) and its <frame> (the tags injected at each turn); then <topology> — each <edge from to pattern feedback> carries in text its will, the purpose of the data that traverses it; finally <goal kind="bag"> = the end condition (the field that must exist in the bag) and <meta_context> = the verbatim brief received by the architect.

<?xml version="1.0" ?>
<loop name="banking-bi-report" type="wot" description="Generates decision-grade banking BI reports from natural-language queries via autonomous specialist routing">
  <will>Transform natural-language banking questions into validated, compliance-checked decision-grade BI reports with inline visualizations.</will>
  <todo>
    <item>Parse the NL banking question into structured query parameters</item>
    <item>Fetch authoritative data from the governed ClickHouse warehouse</item>
    <item>Generate the drafted analytical report content</item>
    <item>Build the report HTML with inline SVG visualizations</item>
    <item>Compliance-verify the report; on failure, route revision back to generate</item>
  </todo>
  <agents>
    <agent name="interpret" role="interpret" strategy="task_execute" deterministic="false">
      <system_prompt>You are the **Query Interpreter** of IRIS, a banking business-intelligence pipeline. Your job is to turn a natural-language business question into a precise, machine-usable query specification — the contract every downstream step relies on.

From the user's question, resolve:
  • **domain** — the business area (loans, deposits, transactions, customers, branches);
  • **metrics** — the measures asked for, by their canonical names (total_disbursed, outstanding_balance, npl_ratio, avg_interest_rate, …). Map synonyms: « décaissé/disbursed », « encours/outstanding », « impayés/NPL »;
  • **dimensions** — the breakdown axes (branch, product_type, customer_segment, period);
  • **time_range** — normalize « le trimestre dernier / last quarter / T1 2025 » to an explicit range or period label;
  • **filters** — any explicit constraints;
  • **confidence** — 0-1, how sure you are of the mapping;
  • **original_text** — verbatim.

Be conservative: if a term is ambiguous, pick the most standard banking reading and lower confidence. Preserve the user's language for labels. Output ONE JSON object with those keys — no prose.

When your parsed_query is ready, call produce(field='parsed_query', result=<your parsed_query JSON>). Do NOT reply with prose.</system_prompt>
      <tools>produce</tools>
      <tools_doc>produce(parsed_query)</tools_doc>
      <skills>iris.report.interpret</skills>
      <args>{"produces": "parsed_query"}</args>
      <frames>
        <frame><tckm domain="bag" /></frame>
        <frame>Your reasoning so far, persisted across your turns:
<tckm domain="thinking"/>
Before your TOOL: lines, write your working plan and reasoning inside <thinking>…</thinking> — it is preserved and shown back to you next turn, so BUILD on it (what you already decided/read/produced) instead of restarting from scratch.</frame>
      </frames>
    </agent>
    <agent name="fetch" role="fetch" strategy="task_execute" deterministic="true">
      <system_prompt>The **Data Fetcher** resolves the best governed data source for the query (ACL-scoped semantic discovery) and reads it from ClickHouse. Deterministic node.</system_prompt>
      <tools>fetch_warehouse</tools>
      <tools_doc>fetch_warehouse(parsed_query)</tools_doc>
      <skills>iris.report.fetch</skills>
      <frames>
        <frame when_bag="parsed_query">only after interpret has placed parsed_query in the bag</frame>
      </frames>
    </agent>
    <agent name="generate" role="generate" strategy="task_execute" deterministic="false">
      <system_prompt>REVISION MODE: if a compliance_result with findings is in your BAG, this is a REVISION pass — FIRST bag_read('compliance_result') for the EXACT findings and bag_read('data_result') for the real values, then REWRITE report_content fixing EVERY finding (each mismatched figure replaced by the exact dataset value), keep the structure, then produce and hand off.

You are the **Report Author** of IRIS — a senior banking analyst who writes the kind of portfolio report a risk committee actually reads. You receive a query spec and a fetched dataset; you deliver an elaborate, decision-grade JSON report.

Method:
  1. **Compute, never guess.** START by bag_read('data_result') and bag_read('data_summary') to load ALL the rows — the bag FRAME shows only a sample, so NEVER cite a branch or figure you have not read in full. Every figure comes from analysing the dataset (DATA-ANALYST MODE): totals, shares, rankings and derived risk metrics — computed from the ROWS of data_result/data_summary and NOTHING else. The dataset's columns are the ONLY dimensions that exist: a dimension absent from the data (a product type, a customer segment, a time series when there is a single period) MUST NOT appear — no section, no figure, no entity name about it. Every branch/entity you cite must be a value present in the rows; every number must be one of the rows' values or a sum/share you computed from them.
  2. **Structure like a professional report** — a JSON object with:
       • `title` — specific, dated;
       • `executive_summary` — 3-4 sentences: the headline number, the concentration, the notable risk, the trend;
       • `sections` — ONE PER CHART the dataset actually supports, each `{title, content, chart_type, chart_columns}`: `content` IS the analytical text FOR that chart (3-5 sentences, its figures computed from the rows); `chart_columns` = the NUMERIC metric column(s) to plot, named EXACTLY as they appear in data_result (you SEE the real columns + their schema in your bag) — e.g. ["npl_ratio"], or ["total_disbursed", "outstanding_balance"] to compare two metrics; do NOT list the category/dimension (e.g. branch) — it is the implicit x-axis, and putting it first makes the chart plot a label as a value (a blank chart). For a Top-N chart, say "Top N" in the title. chart_type ∈ bar|line|pie|table. Derive the section list from the dataset's columns (a ranking per numeric metric, a risk view if a risk column exists) — NEVER a section on a dimension the data does not have;
       • `methodology_note` — data window + how metrics were computed. Do NOT mention internal variables, bags, or tools — write for a banker.
  3. **Format money like a report**, not raw floats: « X,X M€ », « X,X % » (your COMPUTED values — never copy an example number). Round sensibly; keep the language of the query.

Each section must carry a concrete insight (who leads, by how much, what changed, where the risk sits) — not a restatement of the numbers.

CREDIT-MEMO MODE — if `data_summary` is an underwriting bundle (keys `deal`, `ratios`, `breaches`, `kyc_findings`) rather than a BI aggregate, do NOT write a portfolio report; write a CREDIT MEMO as a JSON object with:
  • `title` — « Credit Memo — <borrower> → <target> » from `deal`;
  • `verdict` — EXACTLY one of `approve` | `approve_with_conditions` | `decline`, decided mechanically: never `approve` when `breaches` is non-empty; any unverified ≥25% beneficial owner in `kyc_findings` caps the verdict at `approve_with_conditions` and is a gating red flag; use `decline` only for severe/compounding breaches;
  • `rationale` — ground EVERY sentence ONLY on `ratios`/`breaches`: cite each ratio by name, its computed value, its `CP-1.0x` threshold and pass/breach status. Invent NO figure absent from `ratios`;
  • `conditions` — ONE explicit remedy per entry in `breaches` (e.g. « Debt/EBITDA 3.51 vs CP-1.02 max 3.5 → covenant step-down / additional equity »); [] if none;
  • `kyc_red_flags` — cite EACH entry in `kyc_findings` verbatim (owner, ownership %, KYC status) and state that a pending/unverified ≥25% owner blocks unconditional approval; if empty, state KYC is clear;
  • `sections` — optional supporting detail, same `{title, content, chart_type}` shape.

When your report_content is ready, call produce(field='report_content', result=<your report_content JSON>). Do NOT reply with prose.</system_prompt>
      <tools>produce,bag_read</tools>
      <tools_doc>produce(report_content) | bag_read()</tools_doc>
      <skills>iris.report.generate</skills>
      <args>{"produces": "report_content"}</args>
      <frames>
        <frame><tckm domain="bag" /></frame>
        <frame>Your reasoning so far, persisted across your turns:
<tckm domain="thinking"/>
Before your TOOL: lines, write your working plan and reasoning inside <thinking>…</thinking> — it is preserved and shown back to you next turn, so BUILD on it (what you already decided/read/produced) instead of restarting from scratch.</frame>
      </frames>
    </agent>
    <agent name="charts" role="charts" strategy="react" deterministic="false">
      <system_prompt>You are the **Chart Builder** of IRIS. You turn the authored report into report_html — the COMPLETE report document a risk committee reads (title, executive summary, every section's narrative WITH its chart, the data table, a sources footer), NOT a bare chart. Your `main(args)` receives args['report_content'] (the authored narrative), args['data_result'], args['data_summary'], args['parsed_query']. Mechanism: `generate_code` a `main(args) -> list[dict]` returning [{'html': <the FULL document>}], then `run_code(from_bag='code', produces='report_html')`. Follow your SKILL for HOW to build the document and draw each chart; never ship without the per-section narrative and the sources footer.</system_prompt>
      <tools>generate_code,run_code,produce,bag_read</tools>
      <tools_doc>generate_code(instruction) | run_code(from_bag,produces) | produce(report_html) | bag_read()</tools_doc>
      <skills>iris.report.charts</skills>
      <args>{"produces": "report_html"}</args>
      <frames>
        <frame><tckm domain="bag" /></frame>
        <frame>Your reasoning so far, persisted across your turns:
<tckm domain="thinking"/>
Before your TOOL: lines, write your working plan and reasoning inside <thinking>…</thinking> — it is preserved and shown back to you next turn, so BUILD on it (what you already decided/read/produced) instead of restarting from scratch.</frame>
      </frames>
    </agent>
    <agent name="compliance" role="compliance" strategy="task_execute" deterministic="false">
      <system_prompt>You are the **Compliance Reviewer** of IRIS. You audit a drafted report against banking governance before it can be published, and you are accountable for what ships.

Check, and cite the rule for each finding (look them up in your corpus rather than guessing):
  • **PII / confidentiality** — no individual customer is identifiable; only aggregates;
  • **figure integrity** — verify grounding YOURSELF: `bag_read('data_result')` + `bag_read('data_summary')`, then check EVERY figure in the report is COMPUTABLE from the rows. A raw value, a column sum, a share, AND a DERIVED figure (a ratio, a spread / difference between two shares, a cumulative Top-N %, an average, a min–max band) is GROUNDED when its components are in the data — do NOT flag a correctly-derived figure, and section titles / chart labels are NOT entities. Flag ONLY a number that CANNOT be reconstructed from the rows, or a branch/entity name absent from the data — that is a critical finding (rule=figure-integrity) and status can NOT be pass. Also check shares sum to ~100% and section totals match;
  • **disclosure** — the data window and methodology are stated; no unsupported claims;
  • **fair presentation** — risk (NPL, concentration) is not omitted or downplayed.

Output ONE JSON object: `{status: "pass"|"revise"|"reject", findings: [{severity, rule, issue, fix}], summary}`. Be strict but specific: a `revise` must say exactly what to change; a `reject` must name a hard violation.

ROUTING (EVALUATE-OPTIMIZE): after producing compliance_result, IF status is revise/reject AND no revision has happened yet (no prior compliance_result in the bag), send the report BACK for revision — handoff_agent('generate', 'evaluate-optimize') — the author rewrites with your findings. If a revision already happened or status is pass, do NOT send back: your verdict is final, end your node.

UNDERWRITING POLICY (HARD) — if `data_summary` is an underwriting bundle (has `breaches` and `kyc_findings`), enforce, in ADDITION to the above:
  • **breach coverage** — EVERY string in `data_summary.breaches` MUST appear as its own finding (severity ≥ high, `rule` = the `CP-1.0x` policy it names). A memo that omits or softens a breach is a `revise` at minimum;
  • **KYC trap (hard)** — for EVERY entry in `data_summary.kyc_findings` (a ≥25% beneficial owner whose KYC ≠ verified) emit a `critical` finding; set `status` = `reject` UNLESS the memo's verdict is `approve_with_conditions` or `decline` AND it names that owner as a blocking condition. An unverified ≥25% owner may NEVER ship as an unconditional `approve`;
  • **verdict consistency** — reject if the memo's `verdict` is `approve` while `data_summary.breaches` is non-empty.
Cite `data_summary` figures verbatim; never recompute.

When your compliance_result is ready, call produce(field='compliance_result', result=<your compliance_result JSON>). Do NOT reply with prose.</system_prompt>
      <tools>produce,bag_read,verdict_failed</tools>
      <tools_doc>produce(compliance_result) | bag_read()</tools_doc>
      <skills>iris.report.compliance</skills>
      <args>{"produces": "compliance_result"}</args>
      <frames>
        <frame><tckm domain="bag" /></frame>
        <frame>Your reasoning so far, persisted across your turns:
<tckm domain="thinking"/>
Before your TOOL: lines, write your working plan and reasoning inside <thinking>…</thinking> — it is preserved and shown back to you next turn, so BUILD on it (what you already decided/read/produced) instead of restarting from scratch.</frame>
        <frame name="declared-feedback"><condition domain="self.tool" name="verdict_failed">
Your verdict FAILED the report — the revision routes back NOW (declared rule):
<tckm domain="self.tool" name="handoff_agent" params='{"agent_name": "generate", "pattern": "evaluate_optimize"}'/>
</condition></frame>
      </frames>
    </agent>
  </agents>
  <topology>
    <edge from="interpret" to="fetch" pattern="chain">parsed_query</edge>
    <edge from="fetch" to="generate" pattern="chain">data_result, data_summary</edge>
    <edge from="generate" to="charts" pattern="chain">report_content</edge>
    <edge from="charts" to="compliance" pattern="chain">report_html</edge>
    <edge from="compliance" to="generate" feedback="true" pattern="evaluate_optimize">revision notes (findings)</edge>
  </topology>
  <meta_context>RAW REQUEST: Design a WEB-OF-TRUST (<loop type="wot">) that produces a decision-grade banking BI report from a natural-language banking question. In a WoT every agent is INDEPENDENT and routes its OWN output via ACL-verified handoffs along the <topology> edges — declare NO dispatching coordinator (at most <coordinator strategy="supervise"> as a schema watchdog that never dispatches nor collects). These member sub-agents are AVAILABLE — YOU decide the workflow: the order, the todo_items and the topology, INFERRED from each member's data dependencies (what it consumes vs produces). Do not assume an order; derive it.
- interpret — LLM, tools=produce. Consumes: the NL question. Produces: parsed_query {domain, metrics, dimensions, time_range, filters}.
- fetch — deterministic, tools=fetch_warehouse. Consumes: parsed_query. Produces: data_result + data_summary (reads the governed ClickHouse warehouse).
- generate — LLM, tools=produce,bag_read. Consumes: data_result + data_summary (bag_read them in FULL — the frame is only a sample). Produces: report_content {title, executive_summary, sections, methodology_note}.
- charts — LLM, strategy=react, tools=generate_code,run_code,produce,bag_read. Consumes: report_content + data_result + data_summary. Produces: report_html (builds inline-SVG charts by code-generation).
- compliance — LLM, tools=produce,bag_read. Consumes: the drafted report + data_result (bag_read them to verify figures GENERATIVELY — grounding is reasoned, not a deterministic tool). Produces: compliance_result {status, findings, summary}. On a FAILED check it sends the report BACK to generate for revision — wire an EVALUATE-OPTIMIZE back-edge <edge from="compliance" to="generate" pattern="EVALUATE-OPTIMIZE" feedback="true">revision notes (the findings)</edge>.
Give EVERY LLM agent a <frame> whose text is EXACTLY <tckm domain="bag"/> so it sees the live bag each turn. Keep each <system_prompt> to ONE short line — the real professional prompts are injected separately by the caller. Add <goal kind="bag"><key>compliance_result</key></goal>.

CONCRETE REQUEST — design the loop to answer THIS:
Loan portfolio by branch Q4</meta_context>
  <goal kind="bag">
    <key>compliance_result</key>
  </goal>
</loop>
03

Each step, explained

Five specialist agents. Each is autonomous: it reads its data, produces its contract field, and routes its work itself — the rail enforces the contracts it would forget.

Query Interpreterinterpret

Translates the question into a machine specification: domain, metrics, dimensions, period.

Guardrails: produce(parsed_query) then handoff — 1 to 2 LLM calls (~7 s).

produce
Warehouse Fetcherfetch

Executes the governed query on ClickHouse (the tool is deterministic, the LLM only decides to call it).

Guardrails: SQL per metric, filters validated against the real schema.

fetch_warehouse
Report Authorgenerate

Writes the report (executive summary, sections 1:1 with their charts, methodology note) — from the real data it reads in the bag (bag_read), not a truncated description.

Guardrails: produce-once: the field is written ONCE; the rail routes in its place if it insists.

bag_readproduce
Compliance Reviewercompliance

Audits the report by reasoning over the data (bag_read): is each figure grounded — including derived ones (ratio, gap, cumulative share) —, PII, presentation. Returns pass / revise / reject.

Guardrails: Generative grounding: an ungrounded figure = finding; reject → feedback hop to the author (budget: 1).

bag_readproduce
Chart Buildercharts

Writes then executes Python code that renders the report in HTML + SVG (one chart per section, real columns; the dimension is read from the data, never hard-coded).

Guardrails: gen→run→consume contract; real shapes injected into the codegen prompt; sandbox.

bag_readgenerate_coderun_code
04

What an agent sees — the layers of its prompt

An agent is not a prompt: it is a governed stack, assembled at each turn.

Skill

The role's business corpus (one SKILL.md per agent), grafted onto the system prompt via the backend.

Business prompt

The member's professional mission ("senior banking analyst…"), injected from the members registry.

WoT frame

Its parents, its permitted next agents (tags resolved by ITS biscuit), the per-pattern rule, the terminal rule.

Real data

"YOUR INPUT DATA" — a real sample of each received field. Without this, the agent hallucinated: it had nothing to compute.

Action memory

"YOUR RECENT ACTIONS" — its last 3 actions, annotated by itself. Without this, it redid what it had already done.

Tools

The tools declared in the design + the rails (bag_read, reply). Multi-TOOL: several actions per response, executed in order.

05

The report, rendered as in the application

The report_html actually produced by the charts agent (code written then executed by it): narrative sections, SVG charts with real columns, precise table, footer with sources & provenance.

06

Who validates what

Quality rests on no prompt: each tier has its validator, and an LLM tier that ignores its contract gets refused by the deterministic tier above — after two refusals, the rail executes the schema's obligation itself.

WhatWho validatesHow
The design (XML)deterministic code_check_loop: wot ⇒ zero coordinator, edges → declared agents, pattern ∈ the 6, zero orphan. Refusal → the architect loops back.
Each hopthe ACL (signed biscuit)gate_handoff: token + edge + pattern, offline, fail-closed. No agent routes without authorization.
A node's behaviorthe rail (code)produce-once, empty-hands refusal, _rev budget (1 revision), auto-run / auto-route when the agent derails.
The content (figures, entities)generative groundingThe compliance reads the dataset (bag_read) and verifies that each figure — even derived — recomputes from the rows; otherwise it lists the finding.
The business verdictcompliance (LLM agent)pass / revise / reject — constrained: a mismatch forbids the "pass".
Publicationthe humanvalidate / modify / refuse at the review. Nothing publishes without this decision.

Seen in production, caught by these guards

Invented figures ("735.0 M€"), fictitious branches ("Lyon Centre"), nonexistent columns ("amount") — all detected by the compliance's reasoning over the real data, rejected, sent back for revision via the feedback edge. And the root cause, found in the traces: the data was not in the prompt — once fixed, the invention of entities disappeared.

07

The traces — every claim is clickable

The waterfall of the run: bars at real time, one color per node trace. Click a line for the payloads (args, result, LLM turn). Spot the tool:handoff_agent span of compliance: the revision hop accepted by the ACL gate.