Agent Playtest¶
an LLM agent drives a running SimVX game, headless.
đź“„ Docs onlyTags: 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:
observereads the game as text: the scene tree, one node’s properties, a search or a snapshot.send_inputpresses 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_statesets aPropertydirectly, or pauses and resumes, for setup and probing.watchrecords every emit of a node’s signal without changing the game; eachstepthen lists the emits since the last one, so the agent reads what happened instead of polling for it.stepadvances N frames and reportsterminated, with the outcome asreason, once the session’sterminal_fnnames 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)