intentframe.org
Implementation guide

How

You do not have to rewrite your agents. Put six components between the caller and the model. Move one task into each component. All six components are usual code.

This page uses ASD-STE100 Simplified Technical English. The Python library supplies all six components.

01 — Architecture

Six layers, one frame

Each layer reads one section of the frame only. A layer does not know about the other layers. Only the prompt builder calls a model.

LayerConsumesCan refuse?
Admission controlconstraints, capability, criteria presenceYes — before any spend
Artifact resolutioncontext.artifactsYes — unresolvable reference
Dispatchintent_typeNo
Prompt constructiongoal, inputs, reasoning, output sectionsNo
Validationexpected_output.acceptance_criteriaYes — once retries are exhausted
Orchestrationquality, failure_policy, handoffYes — on budget exhaustion
flowchart TD
    S([Request arrives]) --> A["1 · Check limits"]
    A -- breaks a rule --> X([Refused, nothing spent])
    A -- allowed --> B["2 · Look up documents"]
    B --> C["3 · Choose settings<br/>from the kind of request"]
    C --> D["4 · Build the prompt"]
    D --> E["5 · Call the model"]
    E --> F["6 · Check the answer"]
    F -- passes --> G([Result])
    F -- "fails, retries left" --> D
    F -- out of retries --> Y([Refused: standard not met])
    class A enforce
    class B context
    class C route
    class D,E model
    class F check
    class G ok
    class X,Y refusal
The six pieces in orderThe cheap and certain steps operate first. Only step five calls a model. The loop from step six to step four makes each retry better than the last one.

The sequence is important. The layers that are cheap and certain operate first. The layers that are costly and uncertain operate after them.

02 — Admission control

Refuse before you spend

This layer is the most important one. It is also the most simple. It is a function that compares values with the policy. Your policy holds the maximum values. The frame holds the values that the sender asks for. If a value is more than the maximum, refuse the frame.

Do not decrease the value without a message. The caller must learn that its terms are not acceptable.

def admit(frame, policy):
    violations = []

    if frame.constraints.llm.maximum_tokens > policy.max_tokens_ceiling:
        violations.append(Violation("TOKEN_CEILING_EXCEEDED",
                                    "constraints.llm.maximum_tokens", ...))

    escalation = set(frame.constraints.tooling.allowed) & SIDE_EFFECT_TOOLS
    if escalation and frame.capability not in policy.privileged_capabilities:
        violations.append(Violation("TOOL_ESCALATION",
                                    "constraints.tooling.allowed", ...))

    if not frame.expected_output.acceptance_criteria:
        violations.append(Violation("UNENFORCEABLE_CONTRACT",
                                    "expected_output.acceptance_criteria", ...))

    return violations

If a frame does not pass admission, send a refusal frame. The refusal frame has refused_at="admission", a list of violations, and usage.attempts == 0. The caller learns which terms you refused. You did not spend money to find this.

Conformance test

Send a frame that asks for a tool that its capability cannot use. Then make sure that your code did not call the model. If the model operated, your constraints are only a request. This one test shows if your code conforms.

03 — Dispatch

Derive the runtime from the intent

Give each intent type a profile. A profile has a model role, a temperature, a tool mode, and a validation mode. The profile controls the settings. The values in the frame are maximum values. A frame can ask for a lower temperature than the profile.

It cannot ask for a higher temperature. A frame can ask for tools. You do not have to give those tools to it.

PROFILES = {
  "request": Profile(model="drafting", temperature=0.2,
                     tool_mode="read_only", pipeline="standard"),
  "verify":  Profile(model="checker",  temperature=0.0,
                     tool_mode="none",  pipeline="strict"),
}
flowchart TD
    F["Request arrives"] --> D{"Which kind?"}
    D -- request --> A["Do the work<br/>drafting model · temperature 0.2<br/>read-only tools · normal checks"]
    D -- verify --> B["Check the work<br/>checker model · temperature 0.0<br/>no tools at all · strict checks"]
    D -- critique --> C["Improve the work<br/>checker model · temperature 0.3<br/>read-only tools"]
    D -- inform --> E["Tell someone<br/>may use write tools,<br/>but only if the job is trusted"]
    class D route
    class A model
    class B,C check
    class E orchestrate
One field controls all of thisThe prompt text is the same for all four paths. A request to check work gets a different model, no tools, and more strict validation.

Give a role name to each model. Do not use the name of the supplier. Your deployment selects the model for the checker role. Then you can change the model for one intent type. You do not have to change a frame or a prompt.

04 — Prompt construction

Read from an allowlist

Use one function to make the prompt text from the frame. Give that function a list of the permitted fields. The other fields are for the policy, for the routes, and for the log. Do not put them into the prompt.

Two rules carry most of the weight.

Send the limits as API parameters. Do not write them as sentences. Give the maximum token count to the client. Do not also put it into the prompt. That does not make the limit stronger. It only uses more tokens.

Keep some criteria away from the model. This rule is not obvious. If the source document contains email addresses, do not rely on the prompt. The words "do not include email addresses" do not stop the model. Only a scan of the result stops the model. The check in the prompt gives you nothing.

The same rule applies to a confidence limit. If you tell a model to be 80% sure, the model writes with more confidence. It does not give you more evidence.

05 — Validation and repair

Let the validator generate the retry

Apply each criterion to the result. Collect each failure as a repair note. Put those notes into the next attempt. Do not write a new prompt by hand. Stop the loop at failure_policy.retry_attempts.

report = validate(frame.expected_output, output, assessment)

if report.passed:
    return result_frame(...)

if attempt <= frame.failure_policy.retry_attempts:
    return retry(repair_notes=report.repair_notes)

return refusal_frame(refused_at="validation", violations=report.failures)

Write each repair note as an instruction. Do not write it as a report:

✗  validation failed: c2
✓  missing required section: Recommended Action. Add it.

The note is all the new data in the next attempt. If the note is not specific, the second attempt is not better than the first. Give care to these notes.

06 — Telemetry

Emit typed events

Each layer writes a record. The record has a trace_id, an intent_type, a capability, and its own fields. The intent is a field. So you can group the records to get these numbers:

  • The retry rate for each intent type. Does a check fail more than a first draft?
  • The refusal rate for each policy rule. Which limit has the wrong value?
  • The cost for each capability. Which capability uses the most money?
  • The failure rate for each acceptance criterion.

The last number is the unusual one. If a criterion fails 80% of the time, the model is not usually the cause. The requirement is too difficult. Before you had criteria in fields, you could not see this.

07 — Migration

Adopting incrementally

Instrument one boundary

Find the handoff between two agents that causes the most incidents. Use a frame at that handoff only. A frame is useful at one boundary. You do not need frames at all the boundaries.

Write the criteria first

Get one of your prompts. Write its acceptance criteria as checks. Usually you find that nobody made a decision about the result. That is the true defect. It is there if you use frames or if you do not use frames.

Add admission control in report-only mode

Send your usual traffic through the policy layer. Do not refuse frames yet. You will find frames that passed by chance. Start to refuse frames when the list has no more surprises.

Keep your output format

You do not have to make the model give JSON. The result can stay a document. But your code applies the criteria to that document before it goes to the caller.

08 — Reference

Or install it

The Python library supplies all six layers. It also has adapters for LangGraph and for CrewAI. The example operates offline with a scripted executor. So you can do the refusal path and the repair loop again and again.

pip install "agentintentframe[langgraph]"
python examples/quickstart.py     # offline, no API key

Library documentation → · Source → · Specification →