LLM layer¶
simvx.ai reaches a language model without stalling a frame. Game behaviour stays ordinary
Python: there is no AI contract to implement. The package adds a client, a test cache, in-game
loops, and tools that let an agent play or extend a game. It is the only SimVX package that uses
asyncio, and nothing on the frame path awaits.
Client¶
OpenAICompatibleClient talks to any OpenAI-compatible /chat/completions endpoint: a local
vLLM or llama.cpp server, OpenWebUI, or a hosted API.
from simvx.ai import OpenAICompatibleClient
client = OpenAICompatibleClient.from_env() # SIMVX_LLM_BASE_URL, SIMVX_LLM_API_KEY, SIMVX_LLM_MODEL
resp = await client.complete([{"role": "user", "content": "Hello"}])
print(resp.text)
base_url runs up to, not including, /chat/completions, such as http://localhost:8000/v1.
Any subclass of LLMClient works wherever a client is taken.
In-game loops¶
A model call takes far longer than a frame, so Bark and Planner run it on a worker thread.
poll(dt, **facts) returns the last good answer at once, starts a new call when period seconds
have passed and none is in flight, and keeps the previous answer when a call fails. The facts the
model sees are exactly the keyword arguments to poll.
from simvx.ai import Bark
bark = Bark(client, persona="a weary night watchman", period=6.0)
def on_update(self, dt):
self.line = bark.poll(dt, hp=self.hp)
Loop |
Answer |
|---|---|
|
One short in-character line. |
|
A dict matching |
Call close() when the owner leaves the tree. Both are built on simvx.core.BackgroundSlot, which
runs any slow callable off the frame thread and is polled the same way.
Reproducible tests¶
CachingClient(client, cache_dir, mode=...) records each response as JSON and replays it, so a
test needs no network and no model.
Mode |
Behaviour |
|---|---|
|
Replay when a fixture exists, else call through and record. |
|
Replay only; a miss raises |
|
Always call through and re-record. |
|
Pass straight through. |
Agents and generated nodes¶
AgentSession(root)wraps a scene in a headless, steppable session an agent observes and drives through the real input path.run_agent(session, client, goal=...)loops observe, decide, act and step until the model stops ormax_turnsruns out.session.watch(path, signal)records every emit of one node signal without changing the game, and ends when it is closed or its node is destroyed. Like the act verbs, a request it cannot serve returns{"ok": False, "message": ...}; asking again for a watched signal returns the open watch. EachStepResultlists the emits since the last one underemissions, with their arguments as JSON data taken when the signal fired, and a script error the tree raised appears there as itsscript_errorsignal.session.step(600, until=watch)stops on the frame the signal fires, or comes backtruncated. An attached session does not drive the clock, so therewait_frames(600, until=watch)waits. The watch is a context manager, and a recording that watched signals crystallises into a test that asserts the same emits on the same frames, and the same arguments where they were plain values. The model sees the same thing through thewatchtool and theuntilargument ofstepandwait_frames;observe kind=nodelists a node’s signals and their types.generate_sync(client, description)asks a model for aNodesubclass and checks the source before anything runs it; executing it is opt-in withallow_execution=True.
These two are newer than the client and the loops, and may still change.
Examples¶
LLM NPC Flavour: an NPC barking through
Bark, offline by default.Agent Playtest: an agent playing a game through
AgentSession.Node-Gen: generating a node from a description.