import os os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") import spaces # noqa: E402 (must come before torch) import base64 # noqa: E402 import difflib # noqa: E402 import hashlib # noqa: E402 import io # noqa: E402 import json # noqa: E402 import re # noqa: E402 import shutil # noqa: E402 import signal # noqa: E402 import subprocess # noqa: E402 import sys # noqa: E402 import tempfile # noqa: E402 import threading # noqa: E402 import time # noqa: E402 import uuid # noqa: E402 from pathlib import Path # noqa: E402 import gradio as gr # noqa: E402 import torch # noqa: E402 from jinja2 import StrictUndefined, Template # noqa: E402 from PIL import Image # noqa: E402 from transformers import AutoModelForImageTextToText, AutoProcessor, TextIteratorStreamer # noqa: E402 ROOT = Path(__file__).resolve().parent HARNESS_ROOT = ROOT / "harness" sys.path.insert(0, str(HARNESS_ROOT)) for _tool in (HARNESS_ROOT / "bin").iterdir(): # the Hub does not keep the executable bit _tool.chmod(0o755) from video_coder_harness.blender_exec import execute as blender_execute # noqa: E402 from video_coder_harness.guard import ( # noqa: E402 BLOCKED, REPEAT_WARNING, _normalize_output, harness_violation, ) from video_coder_harness.static_check import check_file # noqa: E402 MODEL_ID = "Video-Coder/Video-Coder-9B" # -------------------------------------------------------------------------------------------------------------------- # Blender 4.5 (bpy 4.5.14 on Python 3.11, same as the authors' bpy-agent.sif sandbox), rendered on CPU with Mesa # llvmpipe. Installed into its own venv at startup because the Space itself runs Python 3.12. # -------------------------------------------------------------------------------------------------------------------- BPY_ENV = Path(os.environ.get("BPY_ENV", str(Path.home() / "bpyenv"))) BPY_PY = BPY_ENV / "bin" / "python" def setup_bpy() -> None: if BPY_PY.exists(): r = subprocess.run([str(BPY_PY), "-c", "import bpy; print(bpy.app.version_string)"], capture_output=True, text=True) if r.returncode == 0: print("bpy ready:", r.stdout.strip()) return t0 = time.time() subprocess.run([sys.executable, "-m", "uv", "venv", "-q", "-p", "3.11", "--clear", str(BPY_ENV)], check=True) subprocess.run([sys.executable, "-m", "uv", "pip", "install", "-q", "-p", str(BPY_PY), "bpy==4.5.14"], check=True) r = subprocess.run([str(BPY_PY), "-c", "import bpy; print(bpy.app.version_string)"], capture_output=True, text=True) print(f"bpy install took {time.time() - t0:.0f}s:", r.stdout.strip(), r.stderr[-2000:]) setup_bpy() # Harness settings: configs/teacher.yaml + configs/uni_container_visual.yaml (the RL / inference setting). HARNESS = { "blender_version": "4.5", "blender_available": True, "bpy_python": str(BPY_PY), "bpy_libs": "/usr", "exec_timeout": 600, "summary_samples": 6, "content_samples": 8, "visual_feedback": True, "preview_frames": 8, "preview_width": 640, "judge": {"enabled": False}, "output_video": "output.mp4", "fps": 24, "frame_count": 120, "max_frame_count": 240, "resolution": [1280, 720], "script_name": "main.py", } SCRIPT = HARNESS["script_name"] CMD_TIMEOUT = 900 REPEAT_WARN, REPEAT_STOP, REPEAT_SIM = 3, 6, 0.9 MAX_FORMAT_ERRORS = 3 RUNS_DIR = Path(tempfile.gettempdir()) / "video_coder_runs" RUNS_DIR.mkdir(parents=True, exist_ok=True) _SECRET_ENV = re.compile(r"(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)", re.I) HARNESS_MARKERS = [str(HARNESS_ROOT), str(BPY_ENV)] SYSTEM_PROMPT = ( "You are an expert Blender technical artist and Python programmer. You write Blender Python (bpy) scripts that " "build and animate 3D scenes and render them as untextured white-model (clay) videos. You work in a Linux shell " "through a bash tool." ) # The exact user prompt of the SFT trajectories (Video-Coder/Train-Data sft_data.json), with the scene as __TASK__. INSTANCE_TEMPLATE = (HARNESS_ROOT / "instance_template.txt").read_text() TOOLS = [{ "type": "function", "function": { "name": "bash", "description": "Execute a bash command", "parameters": { "type": "object", "properties": {"command": {"type": "string", "description": "The bash command to execute"}}, "required": ["command"], }, }, }] OBSERVATION_TEMPLATE = Template("""{%- if output.output | length < 10000 -%} { "returncode": {{ output.returncode }}, "output": {{ output.output | tojson }} {%- if output.exception_info %}, "exception_info": {{ output.exception_info | tojson }}{% endif %} } {%- else -%} { "returncode": {{ output.returncode }}, "output_head": {{ output.output[:5000] | tojson }}, "output_tail": {{ output.output[-5000:] | tojson }}, "elided_chars": {{ output.output | length - 10000 }}, "warning": "Output too long." {%- if output.exception_info %}, "exception_info": {{ output.exception_info | tojson }}{% endif %} } {%- endif -%} """, undefined=StrictUndefined) FORMAT_ERROR_TEMPLATE = Template("""{% if finish_reason is defined and (finish_reason == "length" or (finish_reason == "tool_calls" and not has_tool_calls)) -%} Your previous response reached the output token limit (finish_reason={{ finish_reason }}) before you produced a tool call, so it was cut off. Respond more concisely and finish with exactly one bash tool call. {%- else -%} Tool call error: {{error}} Every response must call the 'bash' tool exactly once with {"command": "..."}. To finish, run `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` on its own. {%- endif %} """) MM_TAG = re.compile(r"(?s)(.+?)(.+?)") TOOL_CALL_RE = re.compile(r"\s*\s]+)>(.*?)\s*", re.S) PARAM_RE = re.compile(r"\s]+)>\n?(.*?)\n?", re.S) END_TOKENS = ("<|im_end|>", "<|endoftext|>") # -------------------------------------------------------------------------------------------------------------------- # Model: Video-Coder-9B (Qwen3.5-9B, vision-language) in bf16, fully on the GPU. # -------------------------------------------------------------------------------------------------------------------- processor = AutoProcessor.from_pretrained(MODEL_ID) tokenizer = processor.tokenizer model = AutoModelForImageTextToText.from_pretrained(MODEL_ID, dtype=torch.bfloat16).to("cuda").eval() GPU_CHUNK_SECONDS = 90 # one GPU call generates for at most this long; long turns continue in the next call @spaces.GPU(duration=120) def llm_chunk(messages: list, images: list, prefix: str = "", max_new_tokens: int = 16384, temperature: float = 1.0, top_p: float = 0.95): """Continue the agent's current turn for up to GPU_CHUNK_SECONDS; yields (text so far, finished, n_tokens).""" prompt = tokenizer.apply_chat_template(messages, tools=TOOLS, add_generation_prompt=True, tokenize=False) prompt += prefix inputs = processor(text=[prompt], images=images or None, return_tensors="pt").to("cuda") n_in = inputs["input_ids"].shape[1] streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=False) result = {} def _run(): try: out = model.generate(**inputs, streamer=streamer, max_new_tokens=int(max_new_tokens), do_sample=True, temperature=float(temperature), top_p=float(top_p), top_k=0, use_cache=True, max_time=GPU_CHUNK_SECONDS) result["n"] = int(out.shape[1] - n_in) except Exception as e: # surfaced below result["error"] = f"{type(e).__name__}: {e}" streamer.end() th = threading.Thread(target=_run, daemon=True) th.start() text = "" last = 0.0 for piece in streamer: text += piece if time.time() - last > 0.4: last = time.time() yield text, False, None th.join() if "error" in result: raise gr.Error(result["error"]) finished = any(t in text for t in END_TOKENS) yield text, finished, result.get("n", 0) # -------------------------------------------------------------------------------------------------------------------- # Agent loop: mini-swe-agent DefaultAgent + video_coder_harness GuardedAgent semantics, re-implemented without the # litellm/vLLM gateway (the model is called in-process). # -------------------------------------------------------------------------------------------------------------------- def agent_env() -> dict: env = dict(os.environ) env.update({k: "" for k in os.environ if _SECRET_ENV.search(k)}) env.update({"PAGER": "cat", "MANPAGER": "cat", "PIP_PROGRESS_BAR": "off", "TQDM_DISABLE": "1"}) env["PATH"] = f"{HARNESS_ROOT / 'bin'}:{os.environ.get('PATH', '')}" env["VCSFT_PYTHON"] = sys.executable env["VCSFT_HARNESS"] = json.dumps(HARNESS) return env def run_command(command: str, cwd: str) -> dict: """mini-swe-agent LocalEnvironment.execute.""" proc = subprocess.Popen(command, shell=True, text=True, cwd=cwd, env=agent_env(), encoding="utf-8", errors="replace", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True) try: stdout, _ = proc.communicate(timeout=CMD_TIMEOUT) return {"output": stdout, "returncode": proc.returncode, "exception_info": ""} except subprocess.TimeoutExpired as e: os.killpg(proc.pid, signal.SIGKILL) stdout, _ = proc.communicate() return {"output": stdout or "", "returncode": -1, "exception_info": f"An error occurred while executing the command: {e}"} def is_submit(output: dict) -> bool: lines = output.get("output", "").lstrip().splitlines() return bool(lines) and lines[0].strip() == "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT" and output["returncode"] == 0 def split_images(text: str): """video_coder_harness.models.split_images: cut frame images out of the tool output.""" images = [] def repl(m): if m.group(1).strip() != "image_url": return m.group(0) before = text[: m.start()].rstrip().splitlines() caption = before[-1].strip() if before else "" images.append((caption, m.group(2).strip())) return f"[image {len(images)} attached below]" return MM_TAG.sub(repl, text), images def data_url_to_pil(url: str) -> Image.Image: return Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1]))).convert("RGB") def parse_response(raw: str): """Split a Qwen3.5 response into (reasoning, content, tool calls) like vLLM's qwen3 reasoning + qwen3_coder parsers.""" text = raw for t in END_TOKENS: text = text.split(t)[0] reasoning = "" if "" in text: reasoning, text = text.split("", 1) reasoning = reasoning.replace("", "").strip() calls = [] for m in TOOL_CALL_RE.finditer(text): args = {k: v for k, v in PARAM_RE.findall(m.group(2))} calls.append({"name": m.group(1).strip(), "arguments": args}) content = TOOL_CALL_RE.sub("", text) content = content.split("")[0].strip() return reasoning, content, calls def sha(path: Path) -> str | None: return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None def short(text: str, n: int = 60) -> str: lines = text.splitlines() return text if len(lines) <= n else "\n".join(lines[:n]) + f"\n... ({len(lines) - n} more lines)" def read_script(workdir: Path) -> str: p = workdir / SCRIPT return p.read_text(errors="replace") if p.is_file() else "" def new_state(prompt: str, max_steps: int, temperature: float, top_p: float, max_new_tokens: int) -> dict: workdir = (RUNS_DIR / uuid.uuid4().hex).resolve() workdir.mkdir(parents=True) return { "workdir": str(workdir), "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": INSTANCE_TEMPLATE.replace("__TASK__", prompt.strip())}, ], "image_urls": [], # frames shown to the model, in order (JPEG data URLs) "chat": [], "video": None, "preview": None, "last_ok": None, # (script hash, video) of the last run_blender that reported RUN OK "last_key": None, "repeats": 0, "n_format_errors": 0, "step": 1, "raw": "", # current (possibly unfinished) model turn "used": 0, "max_steps": int(max_steps), "temperature": float(temperature), "top_p": float(top_p), "max_new_tokens": int(max_new_tokens), "done": False, "status": "Starting…", "t_start": time.time(), } def ui(state: dict, tick=None): workdir = Path(state["workdir"]) msg = f"**{state['status']}** · {time.time() - state['t_start']:.0f}s elapsed" return (state, state["chat"], state["video"], read_script(workdir), state["preview"], msg, gr.update() if tick is None else tick) def start(prompt: str, max_steps: int = 16, temperature: float = 1.0, top_p: float = 0.95, max_new_tokens: int = 16384): """Start a Video-Coder agent run for a scene description. The run then proceeds turn by turn: call `advance` with the returned state (same session) until its status says it finished. Args: prompt: description of the scene / video to build. max_steps: maximum number of agent turns (one bash command each). temperature: sampling temperature (the authors sample with 1.0). top_p: nucleus sampling (the authors use 0.95). max_new_tokens: token budget of a single agent turn. """ if not prompt or not prompt.strip(): raise gr.Error("Please enter a scene description.") state = new_state(prompt, max_steps, temperature, top_p, max_new_tokens) return ui(state, tick=time.time()) def _finish_turn(state: dict): """Parse the finished model turn, run its command(s) in the harness; a generator of UI updates.""" workdir = Path(state["workdir"]) step, raw = state["step"], state["raw"] chat, messages = state["chat"], state["messages"] if chat and chat[-1].get("metadata", {}).get("status") == "pending": chat.pop() reasoning, content, calls = parse_response(raw) state["raw"], state["used"] = "", 0 if reasoning: chat.append({"role": "assistant", "content": reasoning, "metadata": {"title": f"💭 Step {step}: reasoning", "status": "done"}}) msg = {"role": "assistant", "content": content} if reasoning: msg["reasoning_content"] = reasoning if calls: msg["tool_calls"] = [{"type": "function", "function": {"name": c["name"], "arguments": c["arguments"]}} for c in calls] messages.append(msg) error = None if not calls: error = "No tool calls found in the response. Every response MUST include at least one tool call." elif any(c["name"] != "bash" or "command" not in c["arguments"] for c in calls): error = "Unknown tool or missing 'command' argument." if error: state["n_format_errors"] += 1 finish_reason = "length" if not any(t in raw for t in END_TOKENS) else "stop" err = FORMAT_ERROR_TEMPLATE.render(error=error, finish_reason=finish_reason, has_tool_calls=bool(calls)) if content: chat.append({"role": "assistant", "content": content}) chat.append({"role": "user", "content": f"⚠️ {err}"}) messages.append({"role": "user", "content": err}) if state["n_format_errors"] >= MAX_FORMAT_ERRORS: return "FormatError" return None state["n_format_errors"] = 0 for call in calls: command = call["arguments"]["command"] shown = f"{content}\n\n" if content else "" chat.append({"role": "assistant", "content": f"{shown}```bash\n{short(command)}\n```"}) first = command.strip().splitlines()[0][:60] if command.strip() else "" state["status"] = f"Step {step}: running `{first}`" yield reason = harness_violation(command, HARNESS_MARKERS, str(workdir)) if reason: out = {"output": BLOCKED.format(reason=reason), "returncode": 1, "exception_info": ""} else: out = run_command(command, str(workdir)) if is_submit(out): return "Submitted" key = (command, _normalize_output(out.get("output", ""))) last = state["last_key"] same = (last is not None and key[1] == last[1] and difflib.SequenceMatcher(None, key[0], last[0]).ratio() >= REPEAT_SIM) state["repeats"] = state["repeats"] + 1 if same else 1 state["last_key"] = key if state["repeats"] >= REPEAT_WARN: out["output"] = (out.get("output") or "") + REPEAT_WARNING.format(n=state["repeats"], stop=REPEAT_STOP) text, imgs = split_images(out.get("output") or "") obs = OBSERVATION_TEMPLATE.render(output={**out, "output": text}) if imgs: parts = [{"type": "text", "text": obs}] for k, (caption, url) in enumerate(imgs, 1): parts += [{"type": "text", "text": f"image {k}: {caption}" if caption else f"image {k}:"}, {"type": "image"}] state["image_urls"].append(url) messages.append({"role": "tool", "content": parts}) else: messages.append({"role": "tool", "content": obs}) if command.strip().startswith("run_blender"): vid = workdir / HARNESS["output_video"] if vid.is_file(): keep = workdir / f"render_step{step}.mp4" shutil.copy(vid, keep) state["video"] = str(keep) if text.startswith("RUN OK"): state["last_ok"] = (sha(workdir / SCRIPT), str(keep)) if (workdir / "preview.png").is_file(): keep_png = workdir / f"preview_step{step}.png" shutil.copy(workdir / "preview.png", keep_png) state["preview"] = str(keep_png) chat.append({"role": "assistant", "content": f"```\n{short(text.strip(), 40)}\n```", "metadata": {"title": f"🛠️ Output (exit code {out['returncode']})", "status": "done"}}) if state["repeats"] >= REPEAT_STOP: return "RepeatedActions" return None def _accept(state: dict): """Harness acceptance: static check + the submitted script re-rendered from scratch (final_execution).""" workdir = Path(state["workdir"]) script = workdir / SCRIPT state["chat"].append({"role": "assistant", "content": "✅ Submitted. Validating the final script…"}) if not script.is_file(): state["status"] = f"Submitted, but no {SCRIPT} was written" return check = check_file(script) if not check["ok"]: state["status"] = f"Submitted, but the static check failed: {check['errors']}" return last_ok = state["last_ok"] if last_ok and last_ok[0] == sha(script): # Workbench rendering is deterministic: reuse the identical render state["video"] = last_ok[1] else: state["status"] = "Re-rendering the submitted script" yield final_dir = workdir / "final_run" final_dir.mkdir(exist_ok=True) shutil.copy(script, final_dir / SCRIPT) report = blender_execute(final_dir / SCRIPT, final_dir, HARNESS) if (final_dir / HARNESS["output_video"]).is_file(): state["video"] = str(final_dir / HARNESS["output_video"]) if (final_dir / "preview.png").is_file(): state["preview"] = str(final_dir / "preview.png") if not report["ok"]: state["status"] = f"Submitted, but the final render has problems: {report['problems']}" return state["status"] = "Accepted ✅ — draft video rendered from the agent's Blender script" def advance(state: dict): """Run the next piece of the agent: one GPU generation chunk of the current turn and, once the turn is complete, its bash command in the Video-Coder harness. Returns a new tick while the run is still going. Args: state: the run state returned by `start` / the previous `advance`. """ if not state or state.get("done"): yield ui(state) if state else (state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) return step = state["step"] chat = state["chat"] if not (chat and chat[-1].get("metadata", {}).get("status") == "pending"): chat.append({"role": "assistant", "content": "", "metadata": {"title": f"💭 Step {step}: thinking…", "status": "pending"}}) live = chat[-1] state["status"] = f"Step {step}/{state['max_steps']}: Video-Coder is thinking" yield ui(state) # ---- one GPU call (the ZeroGPU token of a request is short-lived, so each request makes exactly one) ---- images = [data_url_to_pil(u) for u in state["image_urls"]] base = state["raw"] finished, n = False, None t0 = time.time() for text, finished, n in llm_chunk(state["messages"], images, base, state["max_new_tokens"] - state["used"], state["temperature"], state["top_p"]): state["raw"] = base + text live["content"] = state["raw"][-3000:].replace("", "") state["status"] = f"Step {step}/{state['max_steps']}: Video-Coder is writing ({len(state['raw'])} chars)" yield ui(state) state["used"] += n or 0 print(f"step {step}: chunk {time.time() - t0:.1f}s, {n} tokens, finished={finished}") if not finished and state["used"] < state["max_new_tokens"]: yield ui(state, tick=time.time()) # continue this turn in the next request return # ---- the turn is complete: execute it ---- exit_status = None gen = _finish_turn(state) while True: try: next(gen) yield ui(state) except StopIteration as stop: exit_status = stop.value break state["step"] += 1 if exit_status is None and state["step"] > state["max_steps"]: exit_status = "StepLimit" if exit_status is None: state["status"] = f"Step {step}/{state['max_steps']} done" yield ui(state, tick=time.time()) return state["done"] = True if exit_status == "Submitted": for _ in _accept(state): yield ui(state) else: state["status"] = { "StepLimit": "Step limit reached before the agent submitted (showing the latest render)", "RepeatedActions": "Stopped: the agent repeated the same command", "FormatError": "Stopped after repeated format errors", }[exit_status] yield ui(state) def stop_run(state: dict): """Stop the current run.""" if state: state["done"] = True state["status"] = "Stopped" return ui(state) return (state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) # -------------------------------------------------------------------------------------------------------------------- # UI # -------------------------------------------------------------------------------------------------------------------- CSS = """ #col-container { max-width: 1280px; margin: 0 auto; } .dark .gradio-container { color: var(--body-text-color); } """ EXAMPLES = [ "The video captures a small bird perched on a tree branch. The bird, with its brown and white feathers, yellow " "beak, and black and white head, is the main focus of the video. The bird is seen in three different positions on " "the branch, each time facing a different direction. The background of the video is filled with green foliage, " "providing a natural and serene setting for the bird. The style of the video is a close-up shot.", "A single polished walnut-wood chess pawn stands on a small square pedestal of white Carrara marble. No other " "objects are in the scene. The camera makes one complete 360-degree orbit around the pawn while slowly rising.", "The video shows a sequence of three identical white cars driving on a road. The first car is in the foreground, " "the second car is slightly behind and to the right, and the third car is in the background. The cars are moving " "from left to right across the frame. The road appears to be a city street with a dark grey surface. The " "background features a city skyline with buildings of various heights, and the sky is a gradient of orange and " "yellow, suggesting either sunrise or sunset. The style of the video is realistic with a focus on the cars and " "their movement.", "a man standing in front of a machine.", "The video captures a lively scene on the ocean, with three boats in motion. The first boat, a large white yacht, " "is seen in the distance, sailing towards the horizon. The second and third boats are closer to the viewer, both " "moving at high speeds, leaving trails of white water behind them. The ocean is a deep blue, and the sky above is " "clear, suggesting a sunny day. The boats are the main focus of the video, with their movement and the water's " "reaction to their speed creating a dynamic and exciting scene. The overall style of the video is a dynamic " "aerial shot, capturing the movement and beauty of the ocean and the boats.", ] with gr.Blocks(title="Video-Coder") as demo: with gr.Column(elem_id="col-container"): gr.Markdown( "# 🎬 Video-Coder: agentic coding for video generation\n" "**Video-Coder-9B** writes and runs a Blender Python program for your scene description, inspects the " "rendered frames, fixes what is wrong and submits a **draft video** (white-model 3D render with the " "layout, motion and camera of the scene) — the structure/motion signal the paper feeds to a downstream " "video generator.\n\n" "[📖 Paper](https://huggingface.co/papers/2611.09894) · [🌐 Project page](https://cywang735.github.io/" "Video-Coder/) · [💻 Code](https://github.com/CYWang735/Video-Coder) · [🤗 Model](https://huggingface.co/" "Video-Coder/Video-Coder-9B)\n\n" "⏳ A full agent run takes several minutes: the first turn writes a long script, and every `run_blender` " "renders the animation with Blender on CPU. The trajectory streams live; keep this tab open." ) with gr.Row(): prompt = gr.Textbox(label="Scene description", lines=3, scale=4, placeholder="Describe the scene and its motion, e.g. 'a red ball rolls down a ramp " "and knocks over three dominoes'") with gr.Column(scale=1, min_width=180): run_btn = gr.Button("Generate draft video", variant="primary") stop_btn = gr.Button("Stop", variant="stop") status = gr.Markdown() with gr.Row(): with gr.Column(scale=1): video_out = gr.Video(label="Draft video (Blender render)", autoplay=True) preview_out = gr.Image(label="Latest render: frames 1, ⅓, ⅔, last", type="filepath") with gr.Column(scale=1): with gr.Tabs(): with gr.Tab("Agent trajectory"): chat_out = gr.Chatbot(label="Video-Coder harness", height=640, show_label=False) with gr.Tab("main.py"): code_out = gr.Code(label="Blender script written by the agent", language="python", lines=30) with gr.Accordion("Advanced settings", open=False): max_steps = gr.Slider(2, 40, value=16, step=1, label="Max agent steps (one bash command each)") temperature = gr.Slider(0.1, 1.5, value=1.0, step=0.05, label="Temperature") top_p = gr.Slider(0.5, 1.0, value=0.95, step=0.01, label="Top-p") max_new_tokens = gr.Slider(2048, 32768, value=16384, step=1024, label="Max tokens per agent turn") state = gr.State(None) tick = gr.Number(value=0, visible=False) outputs = [state, chat_out, video_out, code_out, preview_out, status, tick] inputs = [prompt, max_steps, temperature, top_p, max_new_tokens] run_btn.click(start, inputs=inputs, outputs=outputs, api_name="start") prompt.submit(start, inputs=inputs, outputs=outputs, api_visibility="private") # Each agent step is its own request (fresh ZeroGPU token); the tick change chains the next one. adv = tick.change(advance, inputs=[state], outputs=outputs, api_name="advance", concurrency_limit=None) stop_btn.click(stop_run, inputs=[state], outputs=outputs, cancels=[adv], api_name="stop") gr.Examples(examples=[[e] for e in EXAMPLES], inputs=[prompt], outputs=outputs, fn=start, cache_examples=False, run_on_click=True) if __name__ == "__main__": demo.queue().launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)