A CPU-first, text-only agent specialised for tool usage and function calling in conversation.
imagine is a fork of Qwen3.5-0.8B (Apache-2.0),
reduced and specialised for one job: being a genuinely capable small agent that dispatches tools
well, on a CPU, with no GPU anywhere in the runtime path.
It is not a router. A router punts every question to a tool and waits. imagine is meant to
already have an opinion, call a tool to sharpen or confirm it, and keep being useful when the tool
returns nothing, errors, or needs synthesising. Conversational capability is an asset we protect,
not overhead we strip.
v0.1.0 — reskin. Identity and provenance metadata only. Weights are byte-for-byte upstream. Distillation has not started. The GGUF says so itself:
imagine.stage reskin
imagine.weights_modified false
imagine.base_gguf_sha256 bd258782e35f7f458f8aced1adc053e6e92e89bc735ba3be89d38a06121dc517
Baseline on the stock upstream weights, Q4_K_M, 2 vCPU / no GPU, llama.cpp b1-6ea215d:
| metric | value |
|---|---|
| max RSS @ 4096 ctx | 873 MB |
| prompt throughput | 82.7 tok/s |
| generation throughput | 25.3 tok/s |
| cold load → first answer | 3.6 s |
| GGUF size (Q4_K_M) | 532 MB |
| tensors | 320 (text-only; no vision, no MTP) |
These are 2-core numbers. The reference hardware contract is 4 cores / 1 GB / no GPU. Contract verification happens on target hardware, not here.
Graded by the hardened harness, which asserts arguments and not merely tool names. The v0.1.0 release notes reported "tool selection 3/5" — that number came from a tool-name-only assertion and was too kind. Corrected figures:
| axis | result |
|---|---|
| Conversational restraint (should NOT call) | 4/4 |
| Recovery after tools (empty / error / synthesise) | 3/3 |
| Tool invention (drift probes) | 0 invented |
| Dispatch a usable call (right tool AND valid arguments) | 2/7 |
| Repair an argument after a parse error | 0/1 |
Everything the base model was already good at — conversation, restraint, recovery — held at 9/9. Everything to do with constructing a correct call is where it falls down.
The two failures worth knowing about:
-
SQL where NQL is required. Asked to query NEDB, it picks the right tool and emits
SELECT COUNT(*) FROM orders WHERE status = 'paid'. Reproducible across cases. The grammar cannot stop this: llama.cpp constrains a"type": "string"argument to any text, and JSON Schema has no way to say "this string must be NQL." -
It does not repair, it escapes. Handed an NQL parse error with the correct grammar inline, it did not retry the query tool. It called
shell.runwithnql -e "SELECT COUNT(*) FROM orders WHERE status = 'paid'"— same SQL, wrapped in a shell command, invoking a CLI that does not exist. Ignored the correction, escalated to a more powerful tool, and invented an interface.
Failure 2 is the more serious one, and it sets the roadmap: the base model's conversation and restraint need preserving, while argument construction and error repair need teaching.
call-06 ("in the agent database, show me the 5 most important memories") is graded WRONG_TOOL for
choosing recall_context over nedb.query. That is arguably a defect in the case, not the model —
"memories" genuinely overlaps both tools. It is left in and flagged rather than quietly deleted,
because deleting inconvenient cases is how a harness starts lying again.
Qwen3.5's native tool-call format is sentinel-delimited, not JSON:
<tool_call>
<function=recall_context>
<parameter=query>
a multi-line value needs no escaping
</parameter>
</function>
</tool_call>
Payloads are extracted by delimiter and never re-parsed as code, so quotes, braces and newlines cannot corrupt them. This makes multi-line arguments — a diff, a query, a prompt — work natively, and makes grammar-constrained decoding easier than it would be over JSON.
Upstream ships 488 tensors: 320 text LM, 153 vision, 15 multi-token-prediction. In GGUF form the
vision tower is already a separate mmproj-*.gguf, so a text-only runtime simply does not load it.
Stripping vision costs nothing and requires no weight surgery.
The 15 MTP tensors are a built-in speculative-decoding draft head. They are not reflexively discarded — on CPU that is potentially free throughput, and it is measured before it is cut.
bench/ baseline harness — tools, cases, runner
tools/ reskin.py — GGUF identity + provenance rewriter
# 1. build llama.cpp (CPU, no web UI)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=ON -DLLAMA_BUILD_UI=OFF
cmake --build build -j --target llama-cli llama-server llama-bench llama-quantize
# 2. reskin
python3 tools/reskin.py Qwen3.5-0.8B-Q4_K_M.gguf imagine-0.8b-Q4_K_M.gguf --version v0.1.0
# 3. serve
llama-server -m imagine-0.8b-Q4_K_M.gguf --jinja -rea off -c 8192 -t 4
# 4. baseline
python3 bench/run_baseline.py --url http://127.0.0.1:8080Weights ship as GitHub release assets, never in git history. Tooling ships as packages. One aligned version across everything. Never retag — bump.
Two licences, one bright line between them.
| what | licence |
|---|---|
| Code — reskin tooling, harness, grammar/validation, runtime | BUSL-1.1, Change Date 2030-08-04, Change License Apache-2.0 |
| Weights — GGUF release assets | Apache-2.0, matching upstream |
The weights derive from Qwen3.5-0.8B (Apache-2.0), so they ship Apache-2.0 with upstream
attribution preserved both in NOTICE and embedded in every artifact's GGUF metadata
(general.license, general.base_model.0.*). BUSL does not reach them, and the LICENSE says so
explicitly — nothing here restricts a right you already hold in the weights under Apache-2.0.
BUSL-1.1 converts to Apache-2.0 on the Change Date. Open source on a timer, not open source withheld.