This example demonstrates how a local vision-language model can operate as an external observation provider for a ZeroModel policy.
The local model observes a rendered arcade frame and reports a small set of visible state variables. ZeroModel validates those observations, converts them into a stable policy-state address, and selects an action from an independently compiled Visual Policy Map.
rendered arcade frame
↓
local vision-language model
↓
bounded text observation protocol
↓
deterministic parsing and validation
↓
stable ZeroModel policy row
↓
identified VPM policy
↓
VPMPolicyLookup
↓
LEFT / RIGHT / STAY / FIRE
The model performs perception.
ZeroModel performs state validation, policy addressing, action selection, and trace construction.
The model is never given the action policy and is never asked to choose LEFT, RIGHT, STAY, or FIRE.
Every case is additionally persisted as a ProviderEvaluationCaseDTO through
the video action-set RMDTO aggregate (see
docs/architecture/provider-evaluation-rmdto.md),
so exact-state correctness and policy-action correctness stay separate,
closure-validated, queryable evidence rather than only living inside
cases.jsonl.
ZeroModel can compile a bounded policy into a deterministic artifact and execute that policy without invoking a model at decision time.
That still leaves an important upstream question:
How does a runtime observation become the stable state address required by the compiled policy?
There are several possible answers:
This example tests one of those paths:
A local vision-language model observes the frame, while ZeroModel retains authority over the downstream policy decision.
The experiment deliberately avoids turning ZeroModel into a general-purpose perception framework.
The local model remains a replaceable provider outside ZeroModel Core.
The central rule of the example is that perception and policy remain separate.
The local model receives:
It reports:
ZeroModel:
The local model does not receive:
This distinction matters. The Provider.predict boundary itself is
declared as (image: bytes, render_mode: str) -> ProviderReply - there is no
truth parameter to pass by mistake. tests/test_local_model_zero_arcade_provider_isolation.py
proves this at runtime (a spy provider records every argument an end-to-end
run actually passes to predict()), not just by inspecting the declared
signature.
For example, the model may report:
TANK_COLUMN: 0
TARGET_COLUMN: 6
COOLDOWN: READY
CONFIDENCE: 95
The deterministic adapter converts that observation into:
tank=0|target=6|cooldown=0
The compiled ZeroModel policy then returns:
RIGHT
The model observed the scene.
ZeroModel selected the action.
The first local-model experiments attempted to require schema-constrained JSON responses.
That path was unreliable with the tested Ollama and local-model combination. Some requests returned empty visible content even though the model had processed the image.
The working design uses a minimal labelled-text protocol:
TANK_COLUMN: <0-6>
TARGET_COLUMN: <NONE or 0-6>
COOLDOWN: <READY or BLOCKED>
CONFIDENCE: <0-100>
This keeps the model-facing interface simple while preserving a strict deterministic boundary.
probabilistic model output
↓
small human-readable protocol
↓
deterministic parser
↓
strict typed state
The parser may tolerate harmless formatting differences, but it does not tolerate ambiguous meaning.
Responses are rejected when they contain:
0..6;0..6;The model is not trusted merely because it produced syntactically valid text.
The bounded arcade fixture contains seven horizontal columns.
Each policy state consists of:
tank column
target column or target absence
cooldown state
The complete state surface is:
7 tank columns
×
8 target states
×
2 cooldown states
=
112 policy rows
The eight target states are:
target absent
target in column 0
target in column 1
target in column 2
target in column 3
target in column 4
target in column 5
target in column 6
The two cooldown states are:
0 = ready
1 = blocked
The candidate actions are:
LEFT
RIGHT
STAY
FIRE
The policy behavior is bounded and explicit:
STAY;FIRE;LEFT;RIGHT;STAY.These rules are compiled into the VPM policy before the visual model is invoked.
Executable example:
examples/local_model_zero_arcade_test.py
Living example documentation:
docs/examples/local_model_zero_arcade_test.md
Recorded labelled smoke result:
docs/results/local-model-zero-arcade-smoke-v1/README.md
Local generated output:
local-results/<model>-zero-arcade-<timestamp>/
├── images/
├── cases.jsonl
├── summary.json
├── report.json # only with --compile-report
└── run-manifest.json
The generated local-results/ directory is environment-specific evidence and should not normally be committed.
--store {memory,sqlite} default: memory
--sqlite-path <path> required when --store sqlite
--compile-report compile the run into a VPM report via the external adapter
--store memory (the default) keeps the run in an in-process
InMemoryVideoActionSetStore for the duration of the script.
--store sqlite --sqlite-path <path> persists identity, episode plan,
observations, and the provider-evaluation run to a durable SQLite database
through the existing zeromodel-sqlalchemy runtime composition. Either way,
the script reloads the saved aggregate immediately after saving it and fails
if the reload does not reconstruct byte-for-byte identically - a closure
round-trip proof, not just a write.
--compile-report runs the aggregate through
examples/provider_evaluation_report_adapter.py and writes report.json
with the compiled report/adapted-report/VPM artifact ids; this import is
lazy, so the default path has no dependency on zeromodel-artifacts.
A curated result may instead be preserved under docs/results/ with:
The example requires:
Install the repository dependencies:
python -m pip install -r requirements-dev.txt
python -m pip install pillow
Check the local Ollama runtime:
ollama --version
ollama list
Inspect the intended model:
ollama show qwen3.5:latest
The selected model must support image input.
The example is local-only and is not intended for GitHub Actions.
Before invoking a real local model, run the fake provider:
python .\examples\local_model_zero_arcade_test.py `
--backend fake `
--mode smoke `
--render labelled
The fake provider is a wiring check.
It verifies:
Expected result:
8 attempted
8 accepted
8 exact states
8 correct actions
0 rejections
The fake provider does not perform perception and must not be cited as visual-model evidence.
Use the exact model tag reported by ollama list.
Example:
python .\examples\local_model_zero_arcade_test.py `
--backend ollama `
--model qwen3.5:latest `
--mode smoke `
--render labelled `
--confidence-threshold 0.0
The smoke suite contains eight representative states covering:
The confidence threshold is intentionally 0.0 for initial evaluation.
The model’s confidence value is self-reported and should not be treated as calibrated until a separate calibration experiment has been performed.
--render labelled
The frame includes:
0..6;This is the easiest supported perception condition.
It primarily tests whether:
A successful labelled run is an integration result, not a robust-vision result.
--render unlabelled
The frame retains the seven-lane geometry but removes the printed lane numbers and explanatory status text.
Run:
python .\examples\local_model_zero_arcade_test.py `
--backend ollama `
--model qwen3.5:latest `
--mode smoke `
--render unlabelled `
--confidence-threshold 0.0
This is a more demanding perception test.
It asks whether the model can infer spatial lane positions rather than simply reading the printed labels.
The labelled and unlabelled results must be reported separately.
After the smoke test behaves correctly, run the complete 112-state surface:
python .\examples\local_model_zero_arcade_test.py `
--backend ollama `
--model qwen3.5:latest `
--mode all `
--render labelled `
--confidence-threshold 0.0
A complete run measures the model over every declared combination of:
This is substantially stronger evidence than the eight-state smoke suite.
The complete evaluation should report:
A wrong state can still produce the correct action.
For that reason, exact-state accuracy and action accuracy must remain separate measurements.
Each execution creates a timestamped local result directory.
images/Contains the exact rendered frames supplied to the provider.
The image filenames encode the ground-truth state for local evaluation.
The model is not given the filename.
cases.jsonlContains one detailed record per observation.
Each record may include:
provider_confidence_basis_points, the canonical 0..10000
integer, alongside a derived provider_confidence float in 0.0..1.0
recomputed from it - the float never participates in case identity);This is the most important diagnostic output.
When a summary result is surprising, inspect cases.jsonl before drawing conclusions.
summary.jsonContains aggregate measurements for the run.
Typical fields include:
{
"attempted": 8,
"accepted": 8,
"rejected": 0,
"exact_state_correct": 8,
"action_correct": 8,
"action_equivalent_count": 0,
"action_changing_count": 0,
"run_id": "sha256:...",
"factor_accuracy_over_accepted": {
"tank_column": 1.0,
"target_present": 1.0,
"target_column": 1.0,
"cooldown": 1.0
}
}
action_equivalent_count and action_changing_count come straight from the
persisted ProviderEvaluationSummaryDTO (see
docs/architecture/provider-evaluation-rmdto.md):
action_correct (exact_state_correct + action_equivalent_count) can exceed
exact_state_correct whenever a wrong predicted state still lands on a row
whose policy action matches the ground truth. A nonzero action_changing_count
means at least one case crossed a policy boundary - the predicted action
itself was wrong, which is a materially different failure than an
action-equivalent miss. run_id addresses the full aggregate in the
configured Store (--store sqlite makes this durable).
run-manifest.jsonRecords the execution arguments and policy artifact identity.
A curated result record should additionally preserve:
git rev-parse HEAD;ollama --version;Exact-state accuracy requires every state factor to match:
tank column
target presence
target column
cooldown
A prediction is exact only when the generated policy row is identical to the ground-truth row.
Action accuracy measures whether the predicted state addresses a row whose winning action matches the action from the ground-truth state.
For example, several different states may all select LEFT.
Therefore:
correct action
does not necessarily imply:
correct perception
Both metrics are required.
Factor-level measurements identify where perception fails.
Examples:
This is more informative than a single aggregate score.
A rejection is not automatically a system failure.
The adapter should reject observations it cannot map safely into the bounded state vocabulary.
A useful provider may prefer:
correctly reject uncertain observations
over:
confidently address the wrong policy row
However, rejection behavior must be measured rather than assumed.
The confidence field is generated by the local model.
It is not a probability produced by a calibrated classifier.
The persisted aggregate stores confidence as a canonical scaled integer,
provider_confidence_basis_points (0..10000), not a float - this keeps the
case’s content digest free of float non-determinism. cases.jsonl and
summary.json also expose a derived provider_confidence float
(0.0..1.0, recomputed from the integer) for convenience; that float never
carries identity and must not be relied on for exact comparisons.
Do not interpret:
CONFIDENCE: 95
as a verified 95% likelihood of correctness.
Confidence becomes operationally useful only if evaluation shows that it separates correct from incorrect predictions on held-out canonical and perturbed observations.
The first recorded local-model smoke experiment used:
| Property | Recorded value |
|---|---|
| Model | qwen3.5:latest |
| Model size | 9.7B parameters |
| Quantization | Q4_K_M |
| Runtime | Ollama |
| Render mode | Labelled |
| Cases | 8 |
| Accepted | 8 |
| Exact states | 8 |
| Correct actions | 8 |
| Rejections | 0 |
The recorded run achieved:
exact-state accuracy: 100%
action accuracy: 100%
tank-column accuracy: 100%
target presence: 100%
target-column accuracy: 100%
cooldown accuracy: 100%
The evidence and exact environment identity are preserved at:
docs/results/local-model-zero-arcade-smoke-v1/README.md
The strongest supported statement from that result is:
A local Qwen 3.5 vision model correctly observed all eight labelled canonical arcade smoke-test frames, communicated the complete bounded state through a parsed text protocol, and enabled an independently compiled ZeroModel VPM policy to select the correct action in every case.
That claim is limited to the recorded fixture and environment.
Within a successful bounded run, the example can establish that:
The architectural result is:
learned perception
↓
validated bounded observation
↓
identified deterministic policy
This is the intended integration seam.
This example does not establish:
The canonical labelled frames are:
Results must remain attached to the exact tested conditions.
examples/The local model provider requires:
It therefore does not belong in ZeroModel Core.
It also should not introduce an additional production package solely to host one experimental provider.
The example demonstrates how external learned perception can connect to ZeroModel without changing the core artifact contract.
external provider
↓
bounded adapter
↓
existing ZeroModel policy consumer
This keeps the provider replaceable and the policy independently identified.
The Ollama path is intentionally excluded from routine CI because it requires:
The fake provider can verify the local harness wiring, but it is not visual evidence.
Curated external-model results belong under:
docs/results/
with complete environment and claim documentation.
Symptoms:
Action:
ollama show <model>;Symptoms:
Action:
Symptoms:
Action:
Symptoms:
action_match is true;exact_state_match is false.Interpretation:
The model supplied the wrong state, but that state happened to select the same action.
This is not exact perception success.
Interpretation:
The self-reported confidence is not calibrated.
Do not raise the confidence threshold until confidence behavior has been evaluated on both correct and incorrect cases.
The first request may include:
Report cold and warmed latency separately when latency becomes part of the claim.
The example should be advanced through increasingly adversarial stages.
8 representative canonical states
printed lane labels
explicit cooldown text
Purpose:
8 representative canonical states
no lane numbers
no explanatory cooldown text
Purpose:
all 112 declared states
Purpose:
Run each state multiple times with pinned settings.
Purpose:
Introduce one controlled variation at a time:
Purpose:
Use the local provider to observe successive game states while the compiled policy selects each action.
Purpose:
Compare:
local VLM → predicted state → ZeroModel policy
against:
local VLM → direct action
Purpose:
determine whether separating perception from compiled policy improves:
Run the same fixture through:
Purpose:
Each materially different experiment should receive a separate result record.
Examples:
docs/results/local-model-zero-arcade-unlabelled-smoke-v1/
docs/results/local-model-zero-arcade-canonical-112-v1/
docs/results/local-model-zero-arcade-perturbations-v1/
docs/results/local-model-zero-arcade-trajectory-v1/
docs/results/local-model-zero-arcade-direct-action-comparison-v1/
Do not overwrite the original labelled smoke result with stronger later experiments.
A controlled representation-only variant of Stage 5 (colour, shape, and lane
intervention recipes compared while every other identity stays fixed) is
implemented as a separate harness:
docs/research/controlled-png-representation-benchmark.md
(examples/arcade_png_representation_benchmark.py). It reuses this
example’s fixture, renderer, response parser, and providers verbatim and
adds only the PNG intervention recipes and the comparison/classification
layer.
A real local run of that harness found labelled-v1 reaching 8/8 exact
while unlabelled-v1 and every tested implicit (non-textual) intervention
stayed at or below 4/8
(docs/results/controlled-png-representation-v1/). Because labelled-v1
bundles footer geometry, lane numerals, and cooldown text together, that
result alone cannot say which component mattered. Stage 2F factors those
three apart into footer-only-v1, lane-numerals-v1, cooldown-text-v1,
and semantic-labelled-v1 - see
docs/reviews/stage-2f-semantic-annotation-ablation.md
for the design rationale and the “seven lane centres, not eight boundaries”
lesson carried over from the lane-enhanced-v1 regression, and
docs/research/controlled-png-representation-benchmark.md
for the exact command to run it.
Each result record should contain:
This creates an evidence ladder rather than a sequence of undocumented demos.
The broader architectural principle demonstrated here is:
ZeroModel does not need to replace learned perception. It can govern how a probabilistic observation becomes a validated, typed, identity-bearing policy decision.
The provider may change.
The policy artifact may change.
The observation protocol may evolve.
The boundary remains explicit:
observe
↓
validate
↓
address
↓
act
↓
record
That boundary is the purpose of the example.