# 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. ```python 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`. ```python 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 | | --- | --- | | `Bark(client, persona=...)` | One short in-character line. | | `Planner(client, schema=..., validate=None)` | A dict matching `schema`; `validate` returns the plan to keep or `None` to reject it. | 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 | | --- | --- | | `auto` (default) | Replay when a fixture exists, else call through and record. | | `replay` | Replay only; a miss raises `LLMCacheMiss`. Use in CI. | | `record` | Always call through and re-record. | | `off` | 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 or `max_turns` runs 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. Each `StepResult` lists the emits since the last one under `emissions`, with their arguments as JSON data taken when the signal fired, and a script error the tree raised appears there as its `script_error` signal. `session.step(600, until=watch)` stops on the frame the signal fires, or comes back `truncated`. An attached session does not drive the clock, so there `wait_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 the `watch` tool and the `until` argument of `step` and `wait_frames`; `observe kind=node` lists a node's signals and their types. - `generate_sync(client, description)` asks a model for a `Node` subclass and checks the source before anything runs it; executing it is opt-in with `allow_execution=True`. These two are newer than the client and the loops, and may still change. ## Examples - {doc}`../examples/features_ai_llm_npc_flavour`: an NPC barking through `Bark`, offline by default. - {doc}`../examples/features_ai_agent_playtest`: an agent playing a game through `AgentSession`. - {doc}`../examples/features_ai_nodegen`: generating a node from a description.