"""
clankerDiffusion — tool-using agent (ReAct loop) with mid-response RAG.
The model emits args blocks. We parse + execute
them locally (tools.py) and feed the back, looping until the
model stops calling tools. Each turn alternates AR <-> diffusion, so the
agent is literally "sometimes normal, sometimes diffusion".
Knowledge injection happens in TWO ways, both mid-response:
* model-driven : the model calls q; we run the
retriever and feed back, then keep generating.
* controller-driven : the RAG controller watches the partial response, builds
a query from the user question + what was just said, and if it finds
relevant passages it injects ... straight into the
stream and the model continues -- no need for the model to ask.
"""
import argparse
import infer
from tools import execute_tool, parse_tool_calls
from rag import default_kb
SYSTEM = ("You are clanker, a helpful assistant that can THINK, USE TOOLS, and "
"READ CONTEXT. When you need to compute, inspect files, or look up "
"knowledge, emit a tool call like "
"17 * 23, "
"print(2**10), "
"notes.txt, "
"., or "
"the capital of France. "
"Available tools: calc(expr), python(code), read_file(path), "
"list_dir(path), retrieve(query). If ... is "
"provided, use it to answer. After a ... appears, "
"continue and give the final answer. You may reason in ....")
def run_agent(prompt, max_turns=8, mode_cycle=("diff", "ar"),
max_new=200, gen_len=160, steps=24,
rag=True, rag_k=3, rag_budget=2):
model, tok = infer._MODEL, infer._TOK
kb = default_kb()
injected = set()
ctx = [tok.bos_id] + tok.encode(
f"{SYSTEM}{prompt}")
print(f"\n=== USER ===\n{prompt}\n")
for turn in range(max_turns):
mode = mode_cycle[turn % len(mode_cycle)]
prev_len = len(ctx)
print(f"--- turn {turn} [{mode}] ---", flush=True)
if mode == "ar":
ids = infer.generate_ar(model, tok, ctx, max_new=max_new)
else:
ids = infer.generate_diff(model, tok, ctx, gen_len=gen_len, steps=steps)
new_ids = ids[prev_len:] # ONLY the newly generated tokens
ctx = ids # full updated context (no dup)
text_new = tok.decode(new_ids)
print(text_new, flush=True)
# 1) model-driven tool calls (incl. retrieve) -> mid-response injection
calls = parse_tool_calls(text_new)
if calls:
for name, arg in calls:
print(f" [tool] {name}({arg[:120]})", flush=True)
res = execute_tool(name, arg)
print(f" [result] {res[:300]}", flush=True)
ctx = ctx + tok.encode(f"{res}")
continue # keep going within the same answer
# 2) controller-driven RAG: inject context mid-response, then continue
if rag and len(injected) < rag_budget * rag_k:
q = prompt + " " + text_new[-300:]
fresh = [p for p in kb.retrieve(q, k=rag_k) if p not in injected]
if fresh:
injected.update(fresh)
block = "\n" + "\n---\n".join(fresh) + "\n"
ctx = ctx + tok.encode(block)
print(f" [rag] injected {len(fresh)} passage(s) mid-response",
flush=True)
continue
break # nothing to do -> answer is final
final = tok.decode(ctx)
i = final.rfind("")
print("\n=== clanker (final) ===\n" + (final[i + 10:] if i >= 0 else final))
return final
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("prompt", nargs="?", default="What is 17 * 23, and what files are in the current folder?")
ap.add_argument("--ckpt", default=None)
ap.add_argument("--turns", type=int, default=8)
ap.add_argument("--no-rag", action="store_true")
a = ap.parse_args()
infer.init(ckpt_path=a.ckpt)
run_agent(a.prompt, max_turns=a.turns, rag=not a.no_rag)