Asking within a context
Why @kanzo-tech/ai/data describes each type once, why the answer's schema names a field list once, and why the linker, not a token count, keeps a question inside a small model's context — measured on five graphs, and what would reverse each.
Asking data settled what the model is told: the types it may answer about, with their fields. This page is about how much that costs, measured, and how to say the same in a fraction of it. The measurements were taken on the agent Ask was before it was a Task, which offered every relation; they are why it stopped.
What was measured
Keasy's Discover sent its two calls on five graphs, captured at the BFF and replayed to Docker Model
Runner with max_tokens: 1. The counts are exact: the model's own tokenizer and chat template
answer usage.prompt_tokens, or refuse with request (N tokens) exceeds the available context. A
part's cost is the request's tokens less the request's without it.
| Graph | Relations (hops) | Starters, Hermes 3B | Agent, Hermes 8B | Agent, Qwen3 |
|---|---|---|---|---|
| e2e corpus | 4 (2) | 743 | 1,033 | 2,485 |
| OpenFlights | 3 (2) | 1,290 | 1,580 | 3,813 |
| Nobel | 15 (10) | 1,523 | 1,813 | 3,787 |
| CORDIS | 12 (8) | 3,569 | 3,859 | 7,397 |
| LDBC SNB | 37 (28) | 10,466 | 10,756 | 16,565 |
The dev stack serves complete and chat with an 8,192-token context, and make demo's Qwen3 with
16,384. SNB's starters and SNB's first question both overflow before the model writes a word:
the 10,466 in the logs at 09:46, 12:39 and 13:48 is SNB's starters each time, and snb-ask on Qwen3
is refused at 16,565 of 16,384 with no history yet.
Where the tokens go, SNB:
| Part | Hermes 3B / 8B | Qwen3 |
|---|---|---|
QUESTIONS (starters' instructions) | 170 | — |
| The relations, each with its fields | 10,292 | ≈ 12,900 in the agent's instructions |
The answer tool's schema | 0 — see below | 3,597 |
The output schema (response_format) | 0 — a grammar, not prompt | — |
The relations are nearly all of it, and most of them are repeats. 37 relations list 300 field
lines, of only 44 distinct fields: each type's fields are written again in every relation that
reaches it, values and all — Person.firstName: category (John, Carlos, A., Rahul, Shweta, Jun)
seven times. Category values are 72% of the characters. Each relation also carries its literal,
{"root":"Place","path":[{"edge":"Place_isPartOf_Place","direction":"out"}]}, which its heading,
Place>isPartOf>Place, already says.
The tool's schema repeats too. The field enum — every field name of every relation — is
written ten times, once for each place a field goes (a measure's field twice, each condition's,
trend, x, color, facet, columns), and the relation's description lists every relation key
once more.
Hermes in Model Runner counts the tool at 0: its template there does not put tools into the
prompt, so the 8B is never told the answer tool exists. That is the behaviour
e2e/demos/models.yml records — Hermes writes a query of its own instead of calling the tool — and
it is the template, not the model. A separate finding; the services' compose is where it is fixed.
Say each thing once
Types are described once, with their fields; a relation is one line, its key.
### Types
#### Person
- firstName: category (John, Carlos, A., Rahul, Shweta, Jun)
- gender: category (female, male)
- birthday: time (1980-02-06 – 1990-01-28)
…
### Relations
A relation is a type and the hops taken from it. Its fields are those of every type it reaches,
named Type.field; a type met twice is numbered, Person2.
- Person
- Person>knows>Person
- Person<hasCreator<Comment
…This is LangChain's SQLDatabase.get_table_info: each table's columns once, with a few sample
rows (sample_rows_in_table_info), and the joins as foreign keys rather than as a table per join.
Vanna and LlamaIndex's SQLDatabase describe a schema the same way. A type's values are read from
the relation over the type alone, which is what a reader sees of it; the values a hop narrows to
(Place2.name over countries only) are a detail the answer's rows say, not the description.
The model writes no relation. It names the type it counts, of, and the relation is the
resolver's — so the prompt lists no relation at all, which was most of what remained. An answer
still carries its relation as a key, "Person>knows>Person", through the same relationKey that
reads it back; Answer, what AnswerCard and a dashboard read, keeps the object.
The field list is one definition. The schema names it once under $defs and every place a field
goes is { "$ref": "#/$defs/field" }. JSON Schema's own reuse; OpenAI's and Anthropic's tools
resolve local $refs, and so does llama.cpp's JSON-schema grammar — checked in Docker Model Runner,
2026-10-10, with a schema whose two fields both refer to one enum of three names and a prompt asking
for names outside it: Qwen3 called the tool with two names from the enum, and Hermes 3 8B and 3B
under response_format wrote only names from it. (Hermes calls no tool there with or without $ref
— the template finding above.)
The same five graphs, sent by the starters and the agent with each type said once — each graph's relations rebuilt from its captured prompt, the request caught at the HTTP boundary and counted the same way:
| Graph | Starters, Hermes 3B | Agent, Hermes 8B | Agent, Qwen3 |
|---|---|---|---|
| e2e corpus | 743 → 448 | 1,033 → 696 | 2,485 → 1,812 |
| OpenFlights | 1,290 → 438 | 1,580 → 686 | 3,813 → 1,895 |
| Nobel | 1,523 → 544 | 1,813 → 792 | 3,787 → 2,056 |
| CORDIS | 3,569 → 822 | 3,859 → 1,070 | 7,397 → 2,535 |
| LDBC SNB | 10,466 → 1,290 | 10,756 → 1,538 | 16,565 → 3,212 |
SNB's starters fit the 3B's 8K six times over. snb-ask — the page filtered to women, Which
browsers do they use most? on Qwen3's 16,384 — takes 3,211 on its first question and, with three
turns of history before it (each a question, the answer call, its result of up to 30 rows and a
reply), 5,164: room for the answer and many more turns. Dropping the category values as well saves
little once they are said once (SNB's starters 1,290 → 905), so values stay unless the budget asks.
What would reverse it: a model that answers measurably worse from a type's fields than from a relation's spelled out — one that cannot apply the numbering rule. Then the relation lines carry their fields' names, still without the types' values.
The linker is the budget
A question is asked over the types it names and their neighbours, not over the graph. Ask's
linker keeps the types the question names — by the type's name, a field's or a value's — with the
types one hop away, the type the previous answer counted, and the types the page's chips are on; the
schema's enums and the prompt are theirs. This is LlamaIndex's ObjectIndex /
SQLTableRetriever, which retrieves the tables a question needs, without the index: a graph's
names are few enough to match outright. A question that names nothing is offered every type, and a
type the linker missed is the reachability oracle's to say, as a diagnostic the model repairs from.
Nothing counts tokens. The agent once fitted each call to the context its alias declared — an
estimate of 0.4 tokens a character, a reserve for the answer, the category values dropped first, a
failure coded ai/context when nothing fitted. Each of those was there because the prompt held the
whole graph; a prompt that holds a question's neighbourhood holds SNB's 8 types at most, about 3,500
characters with their values, inside any context a model of the size this is for is served with.
What would reverse it: a graph whose one-hop neighbourhood of a question's types is measured past a served model's context. Then the linker keeps the named types without their neighbours first, and the values after — and the token count comes back as the linker's stopping rule, not as a failure.
Held by packages/ai/src/data/ask.test.ts, "offers the types the question names, and their
neighbours"; "finds a type by a value it holds, and keeps what the page's chips are on"; "offers every
type to a question that names none"; "is one branch per type offered, each its own enum of paths
from it".
Asking data
Why an answer in @kanzo-tech/ai/data is a tile over a relation the model fills from typed names, not SQL it writes — and what would reverse it.
Tasks
Why Ask and Fossil's assistant become one pattern — a small model fills a small artefact under contract, a deterministic oracle checks it, and a bounded loop repairs it or asks the person — and what would reverse each part.