Showcase / Example
One app, shown to three different models
Every model family expects its prompt in a different format. If you write an agent for one model and then switch to another, you usually rewrite how the prompt and the tools are put together. In Affordance the app is written once and the format comes from a second, small file.
The app
This app gives a model a small task: find the largest number in a list. The model cannot see the whole list at once. It sees 48 tokens of it, and it has a tool to scroll. It also has a scratchpad to write on, an answer field, and a submit button.
Each thing the model can do is written as <aff> or as a box with an on... handler, the way a button on a web page has onclick. The file does not mention any model.
find_big_number.tsx, the whole file (30 lines)
// Pure app: no model format, tool table, or history policy.
export function FindBigNumber({ numbers, budget = 256 }) {
const [offset, setOffset] = useState(0);
const [scratch, setScratch] = useState("");
const [answer, setAnswer] = useState("");
const count = String(numbers.length);
const shown = numbers.length >= 1000 ? count.slice(0, count.length - 3) + "," + count.slice(count.length - 3) : count;
return <>
<box label="goal">{"Goal: Find the biggest of the " + shown + " notebook numbers. Write it in Answer, then press Submit.\n"}</box>
<aff label="scratchpad" description="Replace your scratchpad."
args_schema={{type:"object", properties:{text:{type:"string"}}, required:["text"]}}
onChange={({ text }) => setScratch(text)}>{p`Scratchpad: ${scratch}\n`}</aff>
<box label="notebook" max_tokens={budget} more="..."
description="Scroll the notebook by a number of rows."
args_schema={{type:"object", properties:{by:{type:"integer"}}, required:["by"]}}
onScroll={({ by }) => setOffset(o => Math.min(Math.max(0, o + by), Math.max(0, numbers.length - 1)))}>
{p`Notebook (${numbers.length} numbers), starts at row ${offset}:\n`}
{numbers.slice(offset).map((n, i) => p`${i ? "\n" : ""}${offset + i}: ${n}`)}
</box>
{"\n"}
<box label="answer-box">
<aff label="answer" description="Replace the answer field."
args_schema={{type:"object", properties:{text:{type:"string"}}, required:["text"]}}
onChange={({ text }) => setAnswer(text)}>{p`Answer: ${answer}\n`}</aff>
<aff label="submit" description="Submit the value currently in the answer field."
args_schema={{type:"object", properties:{}, required:[]}}
onClick={() => ({ submitted: answer })}>{"Submit"}</aff>
</box>
</>;
}A second file picks the model. This one picks Qwen3.6. The files for GLM-5.3 and Llama 3.1 are nearly the same. They import a different format.
import { FindBigNumber } from "./find_big_number.tsx";
import { TurnStrategy } from "../strategies/turn_strategy.tsx";
import { Qwen36App } from "../template_apps/qwen36.tsx";
export default function Main({ numbers, budget = 256, strategy = "last-turn", log = [], enable_thinking = false }) {
const app = <FindBigNumber label="find-big-number" numbers={numbers} budget={budget}/>;
return <Qwen36App enable_thinking={enable_thinking}><TurnStrategy strategy={strategy} log={log}>{app}</TurnStrategy></Qwen36App>;
}What each model reads
Click a model's name. The top of each prompt is that model's own way of listing tools. The bottom is the app's text, which is the same in all three.
<|im_start|>system
# Tools
You have access to the following functions:
<tools>
{"type": "function", "function": {"name": "change__scratchpad", "description": "Replace your scratchpad.", "parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}}
{"type": "function", "function": {"name": "scroll__notebook", "description": "Scroll the notebook by a number of rows.", "parameters": {"type": "object", "properties": {"by": {"type": "integer"}}, "required": ["by"]}}}
{"type": "function", "function": {"name": "change__answer", "description": "Replace the answer field.", "parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}}
{"type": "function", "function": {"name": "click__submit", "description": "Submit the value currently in the answer field.", "parameters": {"type": "object", "properties": {}, "required": []}}}
</tools>
If you choose to call a function ONLY reply in the following format with NO suffix:
<tool_call>
<function=example_function_name>
<parameter=example_parameter_1>
value_1
</parameter>
<parameter=example_parameter_2>
This is the value for the second parameter
that can span
multiple lines
</parameter>
</function>
</tool_call>
<IMPORTANT>
Reminder:
- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags
- Required parameters MUST be specified
- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after
- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls
</IMPORTANT>
Use the available tools to interact with the app.<|im_end|>
<|im_start|>user
Goal: Find the biggest of the 30 notebook numbers. Write it in Answer, then press Submit.
Scratchpad:
Notebook (30 numbers), starts at row 0:
0: 980993
1: 318220
2: 7764
3: 24...
Answer:
Submit<|im_end|>
<|im_start|>assistant
<think>
</think>
[gMASK]<sop><|system|>Reasoning Effort: Max<|system|>
# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
{"name": "change__scratchpad", "description": "Replace your scratchpad.", "parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}
{"name": "scroll__notebook", "description": "Scroll the notebook by a number of rows.", "parameters": {"type": "object", "properties": {"by": {"type": "integer"}}, "required": ["by"]}}
{"name": "change__answer", "description": "Replace the answer field.", "parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}
{"name": "click__submit", "description": "Submit the value currently in the answer field.", "parameters": {"type": "object", "properties": {}, "required": []}}
</tools>
For each function call, output the function name and arguments within the following XML format:
<tool_call>{function-name}<arg_key>{arg-key-1}</arg_key><arg_value>{arg-value-1}</arg_value><arg_key>{arg-key-2}</arg_key><arg_value>{arg-value-2}</arg_value>...</tool_call><|system|>Use the available tools to interact with the app.<|user|>Goal: Find the biggest of the 30 notebook numbers. Write it in Answer, then press Submit.
Scratchpad:
Notebook (30 numbers), starts at row 0:
0: 980993
1: 318220
2: 7764
3: 247006
4: 274...
Answer:
Submit<|assistant|><think><|begin_of_text|><|start_header_id|>system<|end_header_id|>
Environment: ipython
Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024
You have access to the following functions. To call a function, please respond with JSON for a function call.Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables.
{
"type": "function",
"function": {
"name": "change__scratchpad",
"description": "Replace your scratchpad.",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": [
"text"
]
}
}
}
{
"type": "function",
"function": {
"name": "scroll__notebook",
"description": "Scroll the notebook by a number of rows.",
"parameters": {
"type": "object",
"properties": {
"by": {
"type": "integer"
}
},
"required": [
"by"
]
}
}
}
{
"type": "function",
"function": {
"name": "change__answer",
"description": "Replace the answer field.",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": [
"text"
]
}
}
}
{
"type": "function",
"function": {
"name": "click__submit",
"description": "Submit the value currently in the answer field.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
Use the available tools to interact with the app.<|eot_id|><|start_header_id|>user<|end_header_id|>
Goal: Find the biggest of the 30 notebook numbers. Write it in Answer, then press Submit.
Scratchpad:
Notebook (30 numbers), starts at row 0:
0: 980993
1: 318220
2: 7764
3: 247006
4: 274717
5: 40518...
Answer:
Submit<|eot_id|><|start_header_id|>assistant<|end_header_id|>
The grey markers, such as <|im_start|>, are special tokens that each model uses to mark where a message begins and ends. The ... in the notebook is where the 48-token window stops.
The formats come from Affordance apps we wrote for 57 models. We checked each one against the model's official format, token for token.