Agent Playtest¶

an LLM agent drives a running SimVX game, headless.

đź“„ Docs only

Tags: ai

AgentSession wraps a scene in a steppable, headless session that an agent observes and acts on. run_agent(session, client, goal=...) hands the model the session’s tools and loops observe, decide, act and step until the model stops talking or max_turns runs out:

  • observe reads the game as text: the scene tree, one node’s properties, a search or a snapshot.

  • send_input presses keys, clicks, moves the mouse or drives a controller, through the same input path a player’s hardware uses, so the game’s own actions answer.

  • set_state sets a Property directly, or pauses and resumes, for setup and probing.

  • watch records every emit of a node’s signal without changing the game; each step then lists the emits since the last one, so the agent reads what happened instead of polling for it.

  • step advances N frames and reports terminated, with the outcome as reason, once the session’s terminal_fn names one for the root.

dispatch(session, tool, arguments) is the call the loop makes for each tool the model picks, and anything else may make it too: without an endpoint this example drives the same tools from a fixed script. With SIMVX_LLM_BASE_URL, SIMVX_LLM_MODEL and SIMVX_LLM_API_KEY set, OpenAICompatibleClient.from_env() connects to any OpenAI-compatible endpoint and the model plays.

The game is a toy so the loop is easy to follow: holding the score action (Space) adds a point every frame and emits scored, and reaching 5 points wins. The session and its tools are the same for any game.

Source¶

 1# /// simvx
 2# web = { disabled = true, reason = "requires a local or remote LLM endpoint; not available in the browser runtime" }
 3# ///
 4"""Agent Playtest: an LLM agent drives a running SimVX game, headless.
 5
 6`AgentSession` wraps a scene in a steppable, headless session that an agent observes and acts on.
 7`run_agent(session, client, goal=...)` hands the model the session's tools and loops observe,
 8decide, act and step until the model stops talking or `max_turns` runs out:
 9
10- `observe` reads the game as text: the scene tree, one node's properties, a search or a snapshot.
11- `send_input` presses keys, clicks, moves the mouse or drives a controller, through the same
12  input path a player's hardware uses, so the game's own actions answer.
13- `set_state` sets a `Property` directly, or pauses and resumes, for setup and probing.
14- `watch` records every emit of a node's signal without changing the game; each `step` then
15  lists the emits since the last one, so the agent reads what happened instead of polling for it.
16- `step` advances N frames and reports `terminated`, with the outcome as `reason`, once the
17  session's `terminal_fn` names one for the root.
18
19`dispatch(session, tool, arguments)` is the call the loop makes for each tool the model picks,
20and anything else may make it too: without an endpoint this example drives the same tools from a
21fixed script. With `SIMVX_LLM_BASE_URL`, `SIMVX_LLM_MODEL` and `SIMVX_LLM_API_KEY` set,
22`OpenAICompatibleClient.from_env()` connects to any OpenAI-compatible endpoint and the model plays.
23
24The game is a toy so the loop is easy to follow: holding the `score` action (Space) adds a point
25every frame and emits `scored`, and reaching 5 points wins. The session and its tools are the same for any game.
26"""
27
28import asyncio
29import os
30
31from simvx.ai import AgentSession, OpenAICompatibleClient, dispatch, run_agent
32from simvx.core import Input, Key, Node2D, Property, Signal
33
34
35class Scorer(Node2D):
36    input_context = {"score": [Key.SPACE]}
37    score = Property(0)
38    scored = Signal(int)
39
40    def on_update(self, dt):
41        if Input.pressed("score"):
42            self.score += 1
43            self.scored.emit(self.score)
44
45
46def won(root: Scorer) -> str | None:
47    return "win" if root.score >= 5 else None
48
49
50async def agent_run(session: AgentSession) -> None:
51    client = OpenAICompatibleClient.from_env()
52    print(f"Driving model {client.model!r} at {client.base_url}\n")
53    result = await run_agent(session, client, goal="Make the Scorer reach 5 points, then report how.", max_turns=15)
54    print(result.final_text)
55    print(f"\nturns={result.turns} tool_calls={result.tool_calls} terminated={result.terminated} {result.reason!r}")
56
57
58def scripted_run(session: AgentSession) -> None:
59    print("No SIMVX_LLM_BASE_URL set: driving the same tools from a script.\n")
60    print(dispatch(session, "observe", {"kind": "describe"})["tree"], "\n")
61    print(dispatch(session, "watch", {"op": "add", "path": "Scorer", "signal": "scored"}))
62    print(dispatch(session, "send_input", {"kind": "key", "key": "space", "mode": "down"}))
63    for _ in range(10):
64        out = dispatch(session, "step", {"frames": 1})
65        scored = [emit["args"][0] for emit in out["result"]["emissions"]]
66        print(f"  frame={out['result']['frame']} scored={scored} terminated={out['terminated']}")
67        if out["terminated"]:
68            print(f"\nReached terminal state: {out['reason']!r}")
69            break
70
71
72if __name__ == "__main__":
73    session = AgentSession(Scorer(name="Scorer"), terminal_fn=won)
74    if os.environ.get("SIMVX_LLM_BASE_URL"):
75        asyncio.run(agent_run(session))
76    else:
77        scripted_run(session)