Skip to content

tutorial

Chapter 4 of 6

Chapter 4 — Derived Rendering

by Rod Rivera Published

The renderer takes state and nothing else. There is no parameter through which a model could pass content, which is what makes the guarantee structural.

Here is the entire claim of this tutorial, expressed as a function signature:

def render_markdown(state: DocumentState) -> str:

No content. No overrides. No extra_sections, no template hook, no post-processing callback. The renderer takes state and nothing else, so a “document” assembled anywhere else has nowhere to land.

It is asserted, because a signature is exactly the kind of thing a helpful refactor widens:

def test_render_markdown_takes_only_state(self):
    params = list(inspect.signature(render_markdown).parameters)
    self.assertEqual(params, ["state"],
        "render_markdown grew a parameter; content can now bypass the field set")

The other half: the edit functions

A renderer that only reads state is worth nothing if anything can put arbitrary text into state. So docpkg/edits.py is the only door, and it is governed by three rules.

Rule 1 — a sourced field takes a citation, not a value.

def set_sourced_field(
    state: DocumentState,
    field_key: str,
    *,
    source_id: str,
    record_id: str,
    record_field: str,
    reason: str,
) -> EditResult:

Read the parameter list again and notice what is not in it. There is no value. The function takes a record id and a field name, looks the value up itself, and stores what it found.

A model that wants the total portfolio value to read £900,000 has no argument through which to say so. It can name a record, and the record says what it says. That is the intended difficulty — it is the difference between reporting and inventing.

Rule 2 — a negotiated field takes a value from a closed set.

set_negotiated_field does accept a value, which makes it the obvious back door. It is closed by checking the value against a registry before it reaches state:

ALLOWED_NEGOTIATED: dict[str, tuple[str, ...]] = {
    "addressed_to": ("client", "adviser", "client_and_adviser"),
    "include_property_breakdown": ("yes", "no"),
}

A field with no entry here cannot be set at all.

Rule 3 — there is no third function.

No set_text, no append_paragraph, no set_section_body. The absence is the mechanism. A model cannot call a tool that does not exist, and a future author who needs one has to add it to this file, where the refusal rules are the first thing they read.

The tools the model actually calls

This is where the previous chapter’s lesson could quietly be lost. The engine publishes every non-context tool parameter to the model as a JSON schema, so a tool’s signature is its permission list — and asserting on the internal function while the tool underneath it differs is exactly the trap patterns/voice-handoff-context documented after falling into it.

So the tests check the tools:

def test_the_agent_tool_for_setting_a_field_exposes_no_value_argument(self):
    from tools.document import point_field_at_record
    params = set(inspect.signature(point_field_at_record).parameters)
    self.assertNotIn("value", params)

def test_the_render_tool_accepts_nothing_that_could_become_content(self):
    from tools.document import render_document
    params = [p for p in inspect.signature(render_document).parameters if p != "context"]
    self.assertEqual(params, [])

render_document() takes no parameters at all. There is nothing to inject.

Determinism is a requirement, not a nicety

The same state renders the same bytes, every time. No timestamp of rendering, no random ids, no dictionary iteration order — fields come from a declared tuple and the provenance table is sorted.

def test_rendering_is_stable_across_many_runs(self):
    renders = {render_markdown(build_fixture_state()).encode() for _ in range(5)}
    self.assertEqual(len(renders), 1)

This is what makes the next chapter mean anything. If rendering were nondeterministic, every diff would be noise with a real change buried in it, and nobody would read the diffs — which is the same as not having them.

The document does carry dates: the valuation date, the disclosure approval dates. Those are sourced field values, not render-time clock reads. A document that changes when you render it twice is not a record of anything.

Deletion needs no mechanism

def clear_field(state: DocumentState, field_key: str, *, reason: str) -> EditResult:
    values = dict(state.values)
    values.pop(field_key, None)

No tombstone, no deleted flag, no special case in the renderer. The renderer walks the declared field list and asks state for each key; a key that is not there has no value, and a field with no value renders blank.

Delete a field and it disappears from the document. That is not a feature somebody added — it is the only behaviour available, which is a much stronger thing for it to be.