Markdown for LLMs
Prepare and publish an answer
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# Prepare and publish an answer
Answering happens in the conversation. Publishing moves an answer out of it:
first into a frozen, checkable document, then optionally to a public address
that can later be withdrawn. This page describes the mechanism, separates what
was observed from what was not run, and distinguishes three states that are
easy to confuse: holding an answer, holding a prepared document, and
publishing.
Publication is a separate permission. Never call a publication tool as a side
effect of answering; run it only when the user asked for a frozen or public
document, and enforce that check in your application code
([Safety and reliability](/agent-engineering/safety-and-reliability/)).
## The flow
```text
ask (law_ask) --> capture (law_capture_answer) --> prepare (law_prepare_answer)
|
v
frozen document (24 h, not public)
|
v
publish (law_publish_answer)
|
v
public address + separate revoke secret
|
v
revoke (law_revoke_answer)
closes page, API and download
```
Preparing re-executes rather than trusts: it repeats the earlier calls through
the engine and compares every input, the law, the resources, the tool code and
the result; any mismatch rejects the whole document. That behavior is
contract-described. No prepare succeeded in this track, so the re-execution
itself is NOT RUN. The step order and tool names are verified surfaces.
The four publication tools are a thin proxy to an external answers service.
Without its address and token (`LAW_ANSWERS_URL`, `LAW_ANSWERS_TOKEN`) they
refuse before reading any argument, and nothing leaves the host.
## Before planning: the capabilities matrix
`law_answer_capabilities` reports which producers can be captured and
prepared. Observed (schema `law.answers.capabilities/0.2`): 9 supported, 3
`declared_only` (a schema without an executable witness, so capture cannot
succeed).
| Producer id | Status |
|---|---|
| `law_ask` | supported |
| `law_case_ask` | supported |
| `law_process_run` | supported |
| `law_editions` | supported |
| `law_argue` | supported |
| `law_amend` | supported |
| `law_draft_impact` | supported |
| `law_finit` | supported |
| `law_loophole` | supported |
| `compliance` | declared_only |
| `law_query` | declared_only |
| `rule_application` | declared_only |
Producer support is about capture and preparation, not about the same-named
tool running. In this track `law_argue` is a supported producer while the
`law_argue` call itself refused on a world merge, and the structural
`law query` command runs while `law_query` is a declared-only producer. Do not
carry support in either direction.
## Failure codes
The refusal observed on capture and prepare (server locale Russian):
```text
Ошибка: Arxo Answers не настроен: нужны LAW_ANSWERS_URL и LAW_ANSWERS_TOKEN;
обычный law_ask продолжает работать без них
```
with `isError: true` and structured code `ANSWERS_UNAVAILABLE`. The shipped
English string reads: "Arxo Answers is not configured: LAW_ANSWERS_URL and
LAW_ANSWERS_TOKEN are required; an ordinary law_ask keeps working without
them".
| Code | Meaning | Status |
|---|---|---|
| `ANSWERS_UNAVAILABLE` | Service address or token not configured | Observed |
| `ANSWERS_BAD_RESPONSE` | The service answered with an invalid result | Read in the server source |
| `ANSWERS_TOO_LARGE` | The analysis exceeds the transfer limit; split it across publications | Read in the server source |
On `ANSWERS_UNAVAILABLE`, stop the publication plan and name the missing
setting; retrying does not help.
## The threaded sequence
The refusal reached step by step, each request carrying the control values
really returned by the previous step. The case is the feeding-break question
over `kz-labour-code` (Labour Code of the Republic of Kazakhstan): one child,
a 25-minute break, legalTime 2026-09-01.
**Step 0: ask.** `law_ask` returns `COMPUTED` / `TRUE_ONLY` through the
one-child rule. The response carries a replay control (schema
`law.answers.replay-control/0.1`) that later steps echo back: the request as
asked, the checks `caseHash`, `codeHash`, `programHash`, `resultHash`,
`rustCodeHash` (equal to `codeHash` here), and `responseHash`:
```text
caseHash sha256:d6ae0115bc6d23dda0734a4be87d055d2925a2624f397580c0cf94ae54389251
codeHash sha256:5a64bffcce72767c5db83b46698ef11f3bac835c9e71455f82964e3993dd5038
programHash sha256:09de6428d383eeea903d37a1a1aba64c46d7c868089d57cd37df9ee7b02b8302
resultHash sha256:cf19c47a357f480896d7715f1583ed3bee4fa4c7bec389e96e4dc009478ec5d9
responseHash sha256:75fc31d73d336c0369b6b7e6fba1e9dfac14c85c5191226e9e863d35d7091b21
```
`codeHash` moves with the engine build; `resultHash` stays.
**Step 1: capture.** The request threads the producer id and the request
echoed from step 0:
```json
{"producer": "law_ask",
"request": {"kind": "truth", "package": "kz-labour-code",
"predicate": "feeding_break_too_short",
"args": ["urn:kz:tk:employee:1", "urn:kz:tk:employer:1"],
"facts": [
{"predicate": "children_under_eighteen_months",
"args": ["urn:kz:tk:employee:1", 1]},
{"predicate": "child_feeding_break_minutes",
"args": ["urn:kz:tk:employee:1", "urn:kz:tk:employer:1", 25]}],
"legalTime": "2026-09-01"}}
```
Observed: `isError: true`, `ANSWERS_UNAVAILABLE`. No producer control was
issued.
**Step 2: prepare.** The request carries the human question and one
calculation holding the same `request` and the full `replayControl` from
step 0 (the wire call carries the request twice: beside the replay control and
inside it). Shape, with the objects abbreviated:
```json
{"question": "Is the feeding break too short?",
"calculations": [{"id": "c1", "label": "feeding break truth",
"request": {"kind": "truth", "package": "kz-labour-code", "…": "as in step 1"},
"replayControl": {"schemaVersion": "law.answers.replay-control/0.1",
"request": {"…": "as in step 1"},
"checks": {"caseHash": "…", "codeHash": "…", "programHash": "…",
"resultHash": "…", "rustCodeHash": "…"},
"responseHash": "…"}}]}
```
Status of this block: labeled shape; the sent call carried the full objects
and the hash values printed in step 0. The runnable script below takes both
objects from the step-0 response as objects, never retyped.
Observed: the identical refusal. No preparation id was issued.
**Step 3: publish. NOT RUN.** Publish takes a preparation id, and no prepare
call issued one. Blocker: the answers service is not configured.
Reproduction: set `LAW_ANSWERS_URL` and `LAW_ANSWERS_TOKEN`, rerun steps 1
and 2 to obtain a preparation id, then call `law_publish_answer` with it.
**Step 4: revoke. NOT RUN.** Revoke takes the public id and the separate
secret from a publish that never ran. Reproduction: after a successful
publish, call `law_revoke_answer` with the returned public id and revoke
secret.
## Not run: the end-to-end path
No prepare, publish or revoke call completed in this track. Capture and
prepare ran and refused with real threaded values; publish and revoke never
ran. What follows is the tool contract, not an observed run.
- Prepare takes 1 to 12 earlier `law_ask` calls, or 1 to 12 producer runs (the
two inputs exclude each other), each with its request and replay control,
plus the human question and an optional assistant summary and explanations
tied to calculation ids. It returns a preparation id for a frozen,
non-public document held for 24 hours.
- Publish takes a preparation id and returns a public address. Repeating it
with the same id returns the same address and the same revoke secret
instead of creating a second document.
- Revoke takes the public id and the separate secret (the public id alone
carries no control rights) and closes the page, the API and the download.
This track therefore makes no claim about preparation latency, link formats,
access control on the preparation link, or how a published page renders.
## Three states, three promises
| State | How it arises | Who can read it | What it promises |
|---|---|---|---|
| Held answer | A computed answer in the conversation, with proof and hashes | Conversation participants | The bytes reproduce from the recorded inputs; nothing about access or permanence |
| Prepared document | `law_prepare_answer` succeeds; a preparation id exists | Whoever holds the preparation link, for 24 hours | Frozen inputs and re-executed results; explicitly non-public |
| Published page | `law_publish_answer` succeeds; a public address exists | Anyone with the address, until revoked | A stable public address; withdrawal via the revoke secret |
Confusing these states is the main publishing failure. A hash is not access:
identical hashes say the computation reproduces, not who can read it.
Engine determinism does not mean two agents write the same summary. And a
prepared document is not published: it expires, and it was never public.
## How to verify
```json
// 1. MCP: the support matrix. Only supported producers can be captured.
{"tool": "law_answer_capabilities", "arguments": {}}
```
```json
// 2. MCP: ask first; the response carries the replay control.
{"tool": "law_ask",
"arguments": {"package": "kz-labour-code",
"predicate": "feeding_break_too_short",
"args": ["urn:kz:tk:employee:1", "urn:kz:tk:employer:1"],
"facts": [
{"predicate": "children_under_eighteen_months",
"args": ["urn:kz:tk:employee:1", 1]},
{"predicate": "child_feeding_break_minutes",
"args": ["urn:kz:tk:employee:1", "urn:kz:tk:employer:1", 25]}],
"legalTime": "2026-09-01"}}
```
3. Runnable: [`examples/publishing/thread_sequence.py`](/agent-engineering/files/publishing/thread_sequence.py)
threads the step-0 objects into capture and prepare. It needs a source
checkout with the prebuilt MCP server binary (standard library only; it finds
the checkout root from its own location). From this section's directory:
```bash
LAW_MCP_PROFILE=kz python3 examples/publishing/thread_sequence.py
```
Expected without the answers service: step 0 `COMPUTED` / `TRUE_ONLY`; steps
1 and 2 `isError: true` with `ANSWERS_UNAVAILABLE`; steps 3 and 4 NOT RUN with
the reason taken from the prepare response. On a configured service a
successful prepare prints its preparation id, and the script still stops
there.
```json
// 4. NOT RUN: publish needs a preparation id no prepare call issued.
{"tool": "law_publish_answer",
"arguments": {"preparationId": "(from a successful prepare)"}}
```
For connection setup, see [the MCP connection guide](/guide/mcp/).
## Validation record
Transport: the session's harnessed MCP tools were unusable ("MCP stdio
connection is closed"), so the evidence below came from direct stdio to the
prebuilt local MCP server with profile `kz` (server `arxo-law-kz 0.1.0`,
canon `kz.sources/2026.2.0`). Commit not recorded.
| Step | Date | Result |
|---|---|---|
| `tools/list` | 2026-10-03 | The four publication tools present |
| Step 0 ask | 2026-10-03 | `COMPUTED` / `TRUE_ONLY`, `resultHash sha256:cf19c47a…` |
| Capabilities matrix | 2026-10-03 | 9 supported, 3 `declared_only`, as tabled |
| Capture, full step-0 request | 2026-10-03 | `isError: true`, `ANSWERS_UNAVAILABLE` |
| Prepare, full request and replay control | 2026-10-03 | Identical refusal, no preparation id |
| Threaded rerun (`thread_sequence.py`) | 2026-10-04 | Step 0 same `resultHash` (newer `codeHash`); steps 1 and 2 `ANSWERS_UNAVAILABLE`; steps 3 and 4 NOT RUN |
| Publish, revoke | — | NOT RUN: answers service unconfigured |
| `ANSWERS_BAD_RESPONSE`, `ANSWERS_TOO_LARGE`, refusal strings (ru, en) | — | Read in the server source and shipped strings |
| `law_argue` refusal (producer/tool asymmetry) | 2026-10-03 | `Ошибка: мир не слить: пакет "calc-obligations" объявляет семантику 0.2.2, остальные — 0.2.4` (isError, no structured code) |
Previous: [Safety and reliability](/agent-engineering/safety-and-reliability/)
Next: [Evaluate, debug and upgrade](/agent-engineering/evaluations/)