Files
Brummel 8391209618 experiment(cma): tool-calling re-run at max_tokens=8000 — higher budget does not help, it degenerates (refs #68)
User noted the context window is ~128k and the bad-json failures looked like
truncation, so the max_tokens=2500 cap may have skewed the tool-calling test.
Fair point: the L4/L7 bad-json cases WERE truncation. Verified max_tokens=8000
is accepted, re-ran.

Result: 1/8, slightly worse, not better. The bad-json cases became check
failures (full AST now produced, but semantically wrong), confirming
truncation was a real but secondary factor. And the extra room exposed the
deeper problem: at L2 ("print 7", trivial) Qwen emitted 8000 tokens / ~20KB of
AST JSON and ran off the end — a repetition loop on a one-line program; at L7
it nested JSON deep enough to hit the parser's recursion limit. The verbose
AST-JSON surface is not budget-limited in a recoverable way — more room just
lets it loop or over-nest. Tool-calling stays at the bottom; the higher budget
makes it degenerate, even on trivial tasks. Updated format-findings.md.
2026-06-02 19:26:25 +02:00

133 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Format probe — tool-calling / structured AST. Instead of writing surface
syntax (where bracket-tracking is the wall), Qwen submits the program as the
canonical AILang AST JSON via a `submit_program` tool. Hypothesis: the model
produces structured JSON through the tool channel (which it is trained hard on,
and which the API validates) more reliably than free-text syntax. No converter
risk: the tool argument IS the .ail.json, written and `ail check`ed directly.
Same ablation ladder. Reads $IONOS_API_TOKEN (never printed). Live IONOS.
"""
from __future__ import annotations
import json, os, re, subprocess, urllib.request
from pathlib import Path
ENDPOINT="https://openai.inference.de-txl.ionos.com/v1/chat/completions"
MODEL="Qwen/Qwen3-Coder-Next"
REPO=Path(__file__).resolve().parents[2]
AIL=os.environ.get("AIL_BIN", str(REPO/"target/debug/ail"))
DOC=REPO/"experiments/2026-05-12-cross-model-authoring/qwen-tool-8k.md"
def ail_ast(src):
p=Path("/tmp/abl/_fs.ail"); p.write_text(src+"\n")
return subprocess.run([AIL,"parse",str(p)],capture_output=True,text=True).stdout.strip()
A="""(module demo_a
(fn double
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params x) (body (app + x x)))
(fn main
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params) (body (seq (app print (app double 21)) (do io/print_str "\\n")))))"""
B="""(module demo_b
(data IntList (ctor Nil) (ctor Cons (con Int) (con IntList)))
(fn sum
(type (fn-type (params (own (con IntList))) (ret (own (con Int)))))
(params xs) (body (match xs (case (pat-ctor Nil) 0) (case (pat-ctor Cons h t) (app + h (app sum t))))))
(fn main
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params) (body (let xs (term-ctor IntList Cons 1 (term-ctor IntList Cons 2 (term-ctor IntList Nil)))
(seq (app print (app sum xs)) (do io/print_str "\\n"))))))"""
S="""(module demo_s
(fn main
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params) (body (let s (new Series (con Float) 3)
(let s2 (app Series.push s 1.0)
(let s3 (app Series.push s2 2.0)
(seq (app print (app Series.len s3)) (do io/print_str "\\n"))))))))"""
TOOL={"type":"function","function":{
"name":"submit_program",
"description":"Submit a complete AILang program as its canonical AST JSON.",
"parameters":{"type":"object","properties":{
"module":{"type":"object","description":"The full module AST: an object with keys schema, name, imports, defs."}},
"required":["module"]}}}
def sys_base(extra=""):
return ("You author AILang programs as a canonical Abstract Syntax Tree in JSON, "
"submitted with the submit_program tool (the `module` argument is the full AST: "
"an object with keys schema, name, imports, defs). The AST node forms are shown "
"by these complete, valid example programs.\n\n"
"Example 1 AST:\n"+ail_ast(A)+"\n\nExample 2 AST:\n"+ail_ast(B)+"\n"+extra+
"\nAlways call submit_program with the full module AST.")
SYS=sys_base()
SYS_SERIES=sys_base("\nExample 3 AST (uses the Series library — `new` term with type-arg "
"Float and capacity; Series.push/Series.at/Series.len/Series.total_count called via "
"app on a `var` named e.g. \"Series.push\"):\n"+ail_ast(S)+"\n")
SMA_OUT="3.0\n5.33333\n5.66667\n5.33333\n"
SMA_TASK=("Write a program named m computing a simple moving average, window 3, over the "
"float stream 1.0, 5.0, 3.0, 8.0, 6.0, 2.0 using the Series library: create a Float "
"Series lookback 3, push each value in turn, and after each push once at least 3 "
"values have been pushed, print the average of the most recent 3 (their sum divided "
"by 3.0) on its own line. Expected stdout: "+repr(SMA_OUT))
STAGES=[
("L0_const",SYS,"Write a program named m with a function answer taking no parameters that returns the Int 42.",None),
("L1_arith",SYS,"Write a program named m with a function triple taking one Int parameter, returning it multiplied by 3.",None),
("L2_print",SYS,"Write a program named m whose main prints the Int 7 on its own line.","7\n"),
("L3_adt",SYS,"Write a program named m with an ADT Light (ctors Red, Green) and a function code returning Int 0 for Red and 1 for Green via match.",None),
("L4_rec",SYS,"Write a program named m with an IntList ADT (Nil/Cons), a function length counting elements by recursion, and a main that prints the length of the list [1,2,3] on its own line.","3\n"),
("L5_series_len",SYS_SERIES,"Write a program named m using the Series library: create a Float Series lookback 3, push 1.0 then 2.0 then 3.0, then print its Series.len on its own line.","3\n"),
("L6_series_at",SYS_SERIES,"Write a program named m using the Series library: create a Float Series lookback 3, push 1.0 then 2.0 then 3.0, then print (Series.at on index 0, the newest) on its own line.","3.0\n"),
("L7_sma",SYS_SERIES,SMA_TASK,SMA_OUT),
]
def call(system,task):
body=json.dumps({"model":MODEL,"temperature":0.2,"max_tokens":8000,
"messages":[{"role":"system","content":system},{"role":"user","content":task}],
"tools":[TOOL],"tool_choice":{"type":"function","function":{"name":"submit_program"}}}).encode()
req=urllib.request.Request(ENDPOINT,data=body,headers={
"Authorization":f"Bearer {os.environ['IONOS_API_TOKEN']}","Content-Type":"application/json"})
with urllib.request.urlopen(req,timeout=120) as r: d=json.loads(r.read())
return d["choices"][0]["message"], d.get("usage",{})
def evaluate(msg,expected):
tc=msg.get("tool_calls")
if not tc: return "no-tool-call",f"model returned content instead: {str(msg.get('content'))[:200]}",""
try: args=json.loads(tc[0]["function"]["arguments"])
except Exception as e: return "bad-json",f"tool arguments not valid JSON: {e}",tc[0]["function"]["arguments"][:400]
module=args.get("module",args)
if not isinstance(module,dict): return "bad-shape",f"module not an object: {type(module).__name__}",json.dumps(args)[:400]
module.setdefault("schema","ailang/v0"); module.setdefault("imports",[])
name=module.get("name","m")
f=Path(f"/tmp/abl/{name}.ail.json"); f.parent.mkdir(exist_ok=True)
f.write_text(json.dumps(module,indent=2)+"\n")
pretty=json.dumps(module,indent=1)
chk=subprocess.run([AIL,"check",str(f)],capture_output=True,text=True)
if chk.returncode!=0: return "check",(chk.stderr or chk.stdout).strip(),pretty
if expected is None: return "green","(check-only)",pretty
run=subprocess.run([AIL,"run",str(f)],capture_output=True,text=True)
if run.returncode!=0: return "run",(run.stderr or run.stdout).strip(),pretty
if run.stdout!=expected: return "output",f"got {run.stdout!r}, expected {expected!r}",pretty
return "green",run.stdout,pretty
def main():
log=["# Qwen format probe — tool-calling / structured AST\n",
f"**Model:** {MODEL}; submit_program tool, forced; AST written directly to .ail.json.\n","---\n"]
summary=[]
for sid,sysmsg,task,exp in STAGES:
msg,usage=call(sysmsg,task)
stage,fb,pretty=evaluate(msg,exp)
ok=stage=="green"; mark="✅ green" if ok else f"❌ {stage}"
summary.append((sid,mark)); print(f"{sid}: {mark}")
log+=[f"## {sid}{mark}",
f"\ntokens: prompt={usage.get('prompt_tokens','?')} completion={usage.get('completion_tokens','?')}\n",
"submitted AST:\n```json\n"+pretty[:1400]+"\n```\n",
("" if ok else f"**{stage}:**\n```\n{fb[:600]}\n```\n")]
log+=["\n---\n## Ladder\n"]+[f"- {s}: {m}" for s,m in summary]
DOC.write_text("\n".join(log)); print("doc →",DOC)
if __name__=="__main__":
main()