Architecture at a Glance¶
Tryll is a C++ TCP server that runs small language models on the user's own machine and exposes them to games and apps through a binary protocol. This page is the engineer's view: what runs where, what owns what, and what the hot path looks like when a user hits "send".
New to Tryll? Read Tryll in Ten Minutes first — it explains the ideas this page assumes. The other concept pages each zoom into one layer of the picture below.
The five layers¶
flowchart TB
subgraph Client["Client process (your game / app / script)"]
CL["tryll-client<br>(Unity / Unreal / C++ / Python)"]
end
subgraph Server["tryll-server process"]
S[Session<br>one per TCP connection]
A[Agent<br>one per CreateAgent]
G[Graph<br>nodes + exit fields]
M[Model pool<br>shared models]
end
subgraph Hardware["Local hardware"]
HW[CPU / GPU<br>llama.cpp · ONNX Runtime]
end
CL -- "TCP (localhost)<br>FlatBuffers framing" --> S
S --> A
A --> G
G --> M
M --> HW
- The server is a single C++ process. One instance, many clients. It runs small language models on-device through llama.cpp, runs speech models through Sherpa-ONNX and ONNX Runtime, and uses USearch for vector search; everything else — the workflow engine, the wire protocol, logging — is built inside the server.
- A session lives on one TCP
connection: at most one per connection, created only by
CreateSession, and ended when the socket closes. It owns the client's agents, string storages, and embedded string storages. When the socket closes, everything in the session goes with it. - An agent is one conversation. It carries its own workflow graph, its own dialog history, and its own default model. A single session can own several agents in parallel.
- The graph is a set of nodes, each owning its own exit fields that wire it to the next node. The agent walks it once per user turn — see Workflows, Graphs, and Nodes.
- Models are loaded once per process by the server and shared across every agent that needs them. The KV cache, by contrast, is per node, per agent, because each node sees a different slice of the dialog.
One turn end to end¶
The hot path when a user sends a message:
sequenceDiagram
participant C as Client
participant S as Server
participant A as Agent
participant N as Node (e.g. Generate)
participant M as Model
C->>S: SendMessageRequest
S->>A: dispatch
A->>N: execute node
N->>M: generate tokens
M-->>N: tokens
N-->>S: AnswerText delta
S-->>C: AnswerText frame
Note over N,M: repeats until stop
N-->>S: empty AnswerText terminator
S-->>C: AnswerText terminator
N-->>A: exit route
A->>A: next node or END
A-->>S: TurnComplete
S-->>C: TurnComplete frame
This is the streamed case (send=Streamed, the Generate default): every
chunk is a delta, and the node's last chunk is an empty terminator. With
send=Whole the node sends one AnswerText with is_delta=false,
is_final=true, and the full text.
A turn is always atomic at the agent level. A second SendMessage for
the same agent while a turn is live is rejected with
3004 AgentBusy; in-flight work
keeps running.
Concurrency, in one line¶
Each agent runs one turn at a time — a second SendMessage to a busy
agent gets AgentBusy — while turns on different agents, on the same
session or not, may overlap. Inference
is serialised per model: every agent that shares a loaded model queues
on that model's scheduler, which hands out work in bounded chunks,
while two different models can run at the same time.
The wire protocol¶
Clients speak to the server over local TCP using length-prefixed FlatBuffers messages. A server auto-launched by the Unity or Unreal plugin in a player build listens on a free port the OS assigns; the editor and a standalone server use port 9100 by default. The shape every message takes — and the end-to-end session flow — is documented in Wire Protocol.
The session lifecycle, in one line:
All four client libraries (Unity, Unreal, C++, Python) are thin wrappers over this protocol. Nothing else special happens on the client side — the intelligence lives in the server.
Why it is shaped this way¶
Three product constraints drove the split:
- On-device. A game or tool that wants a local model cannot afford a Python runtime or a separate inference microservice per platform. A single C++ server binary plus tiny clients keeps every integration small.
- Workflow over prompt. Turning a raw model into useful behaviour (RAG, guardrails, tool calls, canned refusals, …) needs composition. Tryll makes the composition first-class by giving you a graph instead of a prompt template.
- Shared, not duplicated, compute. Models are big, KV caches are bigger. The server loads each model once and lets every agent share it; evict rules are explicit (model management).
Edges and pitfalls¶
- A session is owned by its socket. If the TCP connection drops, the server destroys every agent and storage created through it. Client libraries surface this as a connection-changed event — reconnect and re-create what you need.
- A KV cache is per node, per agent — never shared by every node on a model. Two nodes in the same graph that use the same model still have separate caches; that is intentional, because they see different prompt prefixes. See Projection and Token Budgets.
- The server never runs user-supplied code. Tool calls are detection-only — see Tool Calling.