Each loop turn has the same shape. The model emits a call as data, the program validates it, and only a valid call reaches the tool.
The tool result returns as another input, called an observation. The model then proposes the next call or stops.
Validation runs in a fixed order: shape, name, fields, types, then ranges. The first failure decides the verdict, so each rejection names one cause.
How strictly a model follows a schema varies by model: measure it. The program checks every call regardless, since no proposer is exact.
Step | Check | Rejects with |
|---|---|---|
1 | Call is an object with |
|
2 |
|
|
3 | Every required argument is present. |
|
4 | Each argument has the schema type. |
|
5 | Each argument sits inside its bounds. |
|
The trace below is schematic, not output from any real system. It walks one write call through the five steps.
schematic validation trace (illustrative, not from any real system)
call: {"tool": "write_register", "arguments": {"addr": 16, "data": 1}}
step 1: object with tool and arguments -> pass
step 2: write_register is registered -> pass
step 3: addr and data present -> pass
step 4: both are integers -> pass
step 5: addr 16 outside 0 to 15 -> reject with range-addr
result: rejected, nothing runsThe program below uses a fake scripted proposer in place of a model. Eight checks accept one call and sort five faults.
def validate(call):
if not isinstance(call, dict):
return "not-an-object"
if call.get("tool") != "write_register":
return "wrong-tool"
args = call.get("arguments")
if not isinstance(args, dict):
return "bad-arguments"
if "addr" not in args:
return "missing-addr"
if "data" not in args:
return "missing-data"
if not isinstance(args["addr"], int):
return "type-addr"
if not isinstance(args["data"], int):
return "type-data"
if args["addr"] < 0 or args["addr"] > 15:
return "range-addr"
if args["data"] < 0 or args["data"] > 255:
return "range-data"
return "ok"
def fake_program_execute(call):
verdict = validate(call)
if verdict != "ok":
return ("rejected", verdict)
return ("executed", "addr " + str(call["arguments"]["addr"]))
TESTS = [
({"tool": "write_register", "arguments": {"addr": 3, "data": 255}}, "ok"),
({"tool": "write_register", "arguments": {"data": 1}}, "missing-addr"),
({"tool": "write_register", "arguments": {"addr": "3", "data": 1}}, "type-addr"),
({"tool": "write_register", "arguments": {"addr": 16, "data": 1}}, "range-addr"),
({"tool": "write_register", "arguments": {"addr": 3, "data": 300}}, "range-data"),
({"tool": "erase_chip", "arguments": {"addr": 3, "data": 1}}, "wrong-tool"),
]
bad = 0
for call, want in TESTS:
if validate(call) != want:
bad += 1
if fake_program_execute(TESTS[0][0])[0] != "executed":
bad += 1
if fake_program_execute(TESTS[3][0])[0] != "rejected":
bad += 1
if bad == 0:
print("PASS: 8 validation checks accept 1 call and sort 5 faults")
else:
print("FAIL: " + str(bad) + " of 8 validation checks went wrong")
raise SystemExit(1)data is 300 with a bound of 255. What happens?The program rejects it with range-data. No write runs, and the model receives the verdict as the next input.
The claim is more text from the proposer. Only the program verdict decides execution, since proposer exactness varies by model.