Skip to content

§5 Rendering

FunctionSignatureReturns
render(agent, inputs) → stringRendered string
render_async(agent, inputs) → stringRendered string

Both functions MUST be traced: emit a render span.

The following property kinds contain structured data that cannot be directly interpolated into a template string:

RICH_KINDS = {thread, image, file, audio}
KindContainsRendering BehaviorResolution Point
threadMessage[] (history)Nonce → expanded in parser§6 Parsing
imageImage data/URLNonce → resolved in wire§7 Wire Format
fileFile data/URLNonce → resolved in wire§7 Wire Format
audioAudio data/URLNonce → resolved in wire§7 Wire Format

All rich kinds are replaced with nonce strings during rendering. Only thread is expanded back into Message[] during parsing (§6 Parsing). The others are resolved during wire format conversion (§7 Wire Format).

__PROMPTY_THREAD_<hex8>_<propertyName>__
ComponentDescription
__PROMPTY_THREAD_Fixed prefix (identifies Prompty nonces)
<hex8>8 bytes of cryptographic random data, hex-encoded (16 chars)
<propertyName>The input property name (for debuggability)
__Fixed suffix

Example: __PROMPTY_THREAD_a1b2c3d4e5f6a7b8_conversation__

Requirements:

  • MUST use cryptographically secure random bytes (not pseudo-random sequences).
  • MUST be unique per render invocation (a new nonce for each call to render).
  • The nonce-to-value mapping MUST be stored in thread-safe per-request storage.
  • Nonce generation and storage MUST be thread-safe.
function render(agent, inputs):
1. validated_inputs ← validate_inputs(agent, inputs) // §12
2. prepared_inputs ← {}
nonce_map ← {} // thread-safe map
FOR EACH property in agent.inputs:
name ← property.name
value ← validated_inputs[name]
IF property.kind IN RICH_KINDS:
nonce ← "__PROMPTY_THREAD_" + crypto_random_hex(8) + "_" + name + "__"
prepared_inputs[name] ← nonce
nonce_map[nonce] ← value
ELSE:
prepared_inputs[name] ← value
3. renderer ← get_renderer(agent.template.format.kind)
// Look up via invoker discovery (§11)
// Default: "jinja2" → Jinja2-compatible engine
// "mustache" → Mustache engine
// Unknown key → RAISE InvokerError
4. rendered ← renderer.render(agent.instructions, prepared_inputs)
5. Store nonce_map in per-request thread-safe storage
// The parser (§6) needs this to expand thread nonces
6. RETURN rendered

§5.5 Jinja2 Common Subset (Conformance Floor)

Section titled “§5.5 Jinja2 Common Subset (Conformance Floor)”

The default renderer (format kind "jinja2") MUST support the following Jinja2 template features as a conformance floor:

MUST support:

FeatureSyntax
Variable output{{variable}}, {{object.property}}
Conditionals{% if cond %}...{% elif %}...{% else %}...{% endif %}
Loops{% for item in list %}...{% endfor %}
Comments{# comment text #}
Filter: default{{var|default("fallback")}}
Filter: upper{{var|upper}}
Filter: lower{{var|lower}}
Filter: join{{list|join(", ")}}
Filter: length{{list|length}}
Filter: trim{{var|trim}}

MAY support:

  • Template inheritance ({% extends %}, {% block %})
  • Macros ({% macro name(args) %}...{% endmacro %})
  • Custom filters (registered by the implementation)
  • Advanced expressions and tests (is defined, is none, etc.)

Sandboxing: The template engine SHOULD run in sandboxed mode where available, to prevent template injection attacks. Implementations MUST NOT allow templates to execute arbitrary code, access the filesystem, or make network calls.

Implementations MAY support additional template engines beyond Jinja2 and Mustache via the invoker discovery mechanism (§11 Registries & Plugin Discovery). Custom renderers MUST implement the same interface:

interface Renderer:
render(template: string, inputs: dict) → string
render_async(template: string, inputs: dict) → string
ConditionError Type
Unknown template format kind (no renderer)InvokerError
Template syntax error (invalid Jinja2, etc.)ValueError
Missing required input (not in inputs dict)ValueError