Documentation
HTMLP is a prompt file format and a matching Rust API. Both build the same thing: elements with author-chosen names, optional ids, and token budgets that must be justified and signed.
- Prompt files: elements, token budgets, reasons, signatures, and variables.
- Rust interface: the
Elementbuilder, interpolation, checked rendering, and provenance. - API reference: generated directly from the current Rust source.
Install
Version 0.3.0-alpha.1 is distributed through GitHub, not crates.io. For the command-line checker:
cargo install --git https://github.com/alexmckenley/htmlp --tag v0.3.0-alpha.1 --features cli
For the library, add this Cargo dependency; add tokens to use the built-in tokenizer:
[dependencies]
htmlp = { git = "https://github.com/alexmckenley/htmlp", tag = "v0.3.0-alpha.1" }
Prompt files
A file has one htmlp root. Every other element name is yours: lowercase ASCII letters, digits, and hyphens, starting with a letter. Text is literal Markdown, {{name}} inserts a string, and all declared limits require a nonblank reason.
Files and elements
<htmlp max-tokens="2k" reason="Shared context.">
<system-prompt id="base" max-tokens="500" reason="Loaded on every request.">
Make one change. Run its tests.
</system-prompt>
</htmlp>
The root limit covers the entire rendered text. An element limit covers all its descendants. Tags, attributes, and comments do not count. Whitespace inside the root does count; indentation is preserved.
Names are open; the attribute set is closed. system-prompt and section are validated identically, and an unknown attribute is an error on either. The name is the element's kind and the optional id is its identity, so several elements can share a name and be selected together. Neither assigns a provider role or trust level.
1k is 1,000 tokens; 1.5k is 1,500. Counts use cl100k_base by default. Token counts depend on the tokenizer; this is not a universal model limit. The Rust API accepts a custom TokenCounter.
Limit each item
<checks max-tokens="10k" per-item="1k" reason="Keep checks concise.">
<check id="test">Run the affected tests.</check>
<check id="commit">Explain the change.</check>
</checks>
Place this element inside an htmlp root. The total is capped at 10,000 tokens; each item at 1,000. per-item applies to each direct child element. The smaller of that cap and the child's own cap wins. One reason explains the limits declared on that element.
Sign budgets
Run htmlp sign ./prompts after authoring. Each declared budget gets a short sig covering max-tokens, per-item, and reason. Checking rejects missing or stale signatures and asks you to rerun htmlp sign for intentional changes. Content edits need no re-signing.
Review changes before signing; run only check in CI. The checksum is not approval: anyone can re-sign with the same reason or remove a constraint.
Variables
<htmlp max-tokens="1k" reason="Request context.">
Answer: {{question}}
</htmlp>
Variables have no limits or attributes. Static checks defer counts for elements containing variables. Rendering requires every referenced string binding and enforces file and element limits on the final text. Repeated names reuse the same binding; values are inserted literally.
htmlp sign request.htmlp
htmlp render request.htmlp --vars values.json
Write {{name}} for a literal {{name}}. JSON measurements use tokens: null and deferred: true until rendering.
values.json: {"question":"What changed?"}
Commands
htmlp sign ./prompts # accept current budgets explicitly
htmlp check ./prompts # each .htmlp file independently
htmlp check rules.htmlp --json
htmlp watch ./prompts # editor diagnostics on changes
htmlp parse rules.htmlp # syntax-checked JSON AST
htmlp compile rules.htmlp # static budgets checked; variables deferred
htmlp render rules.htmlp # checked prompt text
No ancestor rules or external configuration are loaded. Scans skip symlinks and .git, node_modules, target, dist, and vendor. A path with no matching files fails.
Rust and other languages
Use parse or parse_file for typed Document, Element, and Variable values. Build the same tree in memory with Document::new and the Element builder: new, id, text, variable, template, element, max_tokens, and per_item. Call Document::sign() to accept in-memory budgets, then lint before use. sign_source preserves file formatting.
template and parse_template parse {{name}} through the same code path as markup text, so an in-memory document produces the same nodes as its parsed equivalent and renders identically, differing only in source positions; text appends literal content instead.
get_element_by_id returns a typed reference. elements() returns direct child elements and elements_by_name(name) searches descendants. to_string() extracts text with visible {{name}} placeholders; use Document::render for checked substitutions. A name such as system-prompt is a label, not an SDK role.
Other languages can invoke the CLI and consume JSON. Native bindings and build-tool plugins are not included in this alpha.
Checked rendering
Document::render(&bindings, &counter) returns an immutable RenderedDocument only after the whole document validates: element names, unique ids, budget signatures, every binding, and every root, element, and per-item budget. Nothing can fabricate checked output, and failure returns diagnostics instead of partial text.
It exposes text(), a tokenizer measurement(), element(id) and elements_by_name(name), per-element ranges, and non-overlapping spans() with element ancestry. Each ElementOrigin carries a name, a path of child indices from the root, an optional id, and a source position, so anonymous elements stay identifiable. Budget measurements include descendants; do not sum a parent and its children.
Structure here, meaning in your types
HTMLP assigns no roles, message kinds, or trust levels, and no markup attribute selects one. Applications that need those distinctions define them in their own type system and pair them with checked output: HTMLP proves the text is within budget, and your types prove it is attributed.
0.2 shipped an optional runtime feature with typed model requests, roles, and content-source categories. That encoded one harness's vocabulary as a public contract, and 0.3 removes it. Move those types into your own crate and keep using Document::render as the bridge.
Rust interface guide and examples · Plain Markdown reference
Library features
| Feature | Enables |
|---|---|
| Default | Typed nodes, parsing, signing, linting, and rendering with a supplied TokenCounter. |
tokens | Embedded Cl100k tokenizer. |
json | JSON serialization for document types, measurements, and rendered provenance. |
schema | Document JSON Schema generation; includes json. |
cli | Command-line binary; includes json and tokens. |
Strict markup
Use lowercase tags, quoted attributes, and unique IDs. Empty elements can self-close: <notes id="scratch" />. Otherwise use matching closing tags. Escape literal < as < and & as &, including inside Markdown code fences. Invalid element names and unknown attributes fail. No browser error recovery is performed. Self-closing non-void elements follow XML rules, so ordinary HTML parsing is not equivalent.
Limits and reasons are editable source. Require review or CI policy to protect changes to budgets. A reason explains a constraint; it cannot prevent an agent from changing it.