Contents TandemLLM Docs
v0.9.2
Star58

Part IV, The conductor

20Packages, the library and the deciders

The router’s note carries more than an effort sentence. It carries packages: texts that tell the model how the work at hand is done well. This chapter covers the packages, the library of longer skills the model can search itself, the roles that sign one contract, and the small decision models the engine can serve beside the big one.

20.1 Packages

A package is a text the router may put into a turn’s note: a task skill, a test skill, a card that describes a client’s tools, or a coding guide. The packages document lists them with their source (an inline text, or a skill file on disk), a switch, a max_tokens limit, and optional flags. The shipped texts are in packages/ of the repository and are stored as the first version of the document on a new box. The testing package sets an order and a budget: the deliverable before any test tooling, a verification tool that fails twice dropped with one sentence, servers in the background with a time limit and stopped before “done”, everything the model creates kept inside the project (never /tmp), a scratch directory a cut run left cleaned first, enough verification named (it loads, no console error, one run with real input), never a product change made only for the model’s own test, and a final report of what was built, verified and left untested. Laya’s playtest is a check with a stop rule: a spot only the bot fails is not a bug, and two runs that end at the same spot end it. tools/packages_migrate.py brings a box’s stored texts up to date.

A class lists the packages that fit it, at most 12. One marked general goes in whole on every turn of the class, before the task skill, and so does one the class names in its policy’s core, the class’s defining skill (threejs-scene for three_d, coding for code_edit and debug): left to the package question, the 3D skill had been answered none on a three_d turn that never said 3D, and the model built a flat page. The router’s package question picks one among the rest, asked of the same classifiers as the class question. A package’s text is cut at its max_tokens (3.5 characters a token) with a fixed note that says so. The packages fit the request’s tools: the router reads what the tools can do off their schemas (a shell is a tool with a command argument, a file write one with a path and a content argument), and a package names what its steps need in requires_tools (absent: a shell when its text calls TandemLLM through \{tandem_url\}). A package the tools cannot carry out stays out of the turn or gives way to its fallback (coding to coding-answer), and the note says which tools the chat has and to deliver the files in the answer. A plain Open WebUI chat asking for a landing page used to get the testing packages, call tools it did not have and take 518 s; it now answers in one request. Once in a session, a package stays live in the session’s notes and replays at its position; it is not offered again.

Two cards are kept live the same way on the coding classes with tools. The tool card lists the request’s own tools (names, required arguments starred, which take a path) with the client’s pitfalls package (tools-opencode, tools-hermes). The tandem-harness card says what the runtime around the model does and where its addresses are.

20.2 The library

The library is a set of 44 longer skills in opencode’s SKILL.md form: frontmatter with a name and a description, the text that goes in, then a ## Reference section with snippets and commands. An INDEX.md names the classes each skill fits. Each skill is a package with library: true; its injected text is the part before the reference, which the model reads only on request.

server/conductor/libsearch.py keeps a BM25 index (k1 1.2, b 0.75) over every enabled library package’s name (weighted three times), description (twice) and text. No embeddings. It is rebuilt when the packages version, a skill file or the index moves; 44 skills build in about 2 ms and a 900-character query searches in about 40 µ s on the board’s CPU (measured).

The model searches it itself, from the scripts it writes, through routes that are open like the chat API:

shell
curl -s "$TANDEM_URL/v1/library/search?q=playwright+screenshot+test&k=5"
curl -s "$TANDEM_URL/v1/library/test-web"       # body and reference

GET /v1/library lists every enabled skill. Errors are OpenAI-shaped (422, 404, 429 over 240 calls a minute). Every call is a journal row with the query as a hash and a length, never its text.

The router can retrieve too, under a profile’s library switch (off, shadow, on). On a new user message, and on a tool turn that went badly (a low-quality or stuck outcome), it searches the digest of the message, keeps the skills that fit the turn’s class, and asks the package question over the best 4. With on the pick becomes a note at the prompt’s tail; with shadow the journal says what would have gone in, and the prompt is the one off serves. The shipped coding and hermes profiles have it in shadow. The tandem-library card (191 tokens: the library exists, how to search it and read a skill) stays live on the coding classes with tools.

20.3 The Packages screen and proposals

The dashboard’s Conductor view, Packages screen, lists every package with its kind, source, switch, general flag, the tokens of the text that goes in on the engine’s own tokenizer against its limit (a library skill’s limit is 350 tokens), the classes that list it, and its use in the last 24 hours and 7 days. A search asks the library’s own index. A package’s detail shows its text rendered or raw, its frontmatter and reference, its classes and its recent rows; an inline package edits in a Markdown editor with a live token count. Every save is checked first, shows its diff, then becomes a new version with a note.

The model may propose a change to a package, from a memory block with op=propose (Chapter 23). The proposal is a file with its evidence; the Proposals tab shows its diff against the live text, and Apply stores it as a new version of the packages document. The model never applies a proposal itself.

20.4 Roles

Everything beside the big model that the conductor asks is a role in the roles document, and every role signs one contract:

  • its kind, one of six (reasoner, decider, probe, clerk, librarian, tool); a task asks for a kind, never a model;
  • its memory_claim_gib, the declared peak the residency planner checks;
  • its window, when it may use the GPU: idle between requests, in_decode beside a decoding model (a decider or a probe only), or any;
  • whether it is preemptible, its latency_budget_ms and its fallback past the budget (pass, stale, skip, the next role);
  • what it writes: only the reasoner may write tokens the client sees; the others write hidden notes, memory files or nothing;
  • its tokenizer_sha256 and the forms of sharing it may use: text always, tokens only with the engine’s own tokenizer hash, states only for a probe inside the engine.

A role without a claim or a window is refused, as is any unknown value. The built-in roles include the decider (the router’s decider stage, off in the shipped router), the tester (the test judge), the narrator and the shadow reader (Chapter 22). A role may name the engine itself (self), a model the engine serves, or, behind the proxy, another backend (backend:<id>/<model>).

20.5 Decision models

The engine can serve small decision models itself (engine/decision/): models that answer typed questions over a text in one forward pass with calibrated probabilities. A question is a choice (one of N options), a score (an expected level) or a yes-no probability. Laya’s three published checkpoints (Apache-2.0) are the models it serves: laya-english (ModernBERT-large, 512 tokens of state and question), laya-multilingual and laya-typed-decisions (1,024 tokens each). QWEN38_DECIDER_MODELS names the ones a server loads; empty, nothing of it is imported and its routes are absent.

The arithmetic is the vendor’s own, through transformers’ ModernBERT, so the answers equal the vendor’s SDK’s (tools/decider_equivalence.py). Nothing is shared with Qwen: each decision model’s tensors live in a memory pool of their own, its forwards run on a stream of their own, and a forward runs only while no request holds the engine. A completion that arrives during a decision waits at most one decision forward; a question asked while a completion runs waits for it up to its deadline and is dropped after. A model loads through the residency planner (its claim is 8.2 GiB), at the start for the deciders document’s on_start models or on first need; the Deciders screen loads and unloads by hand.

What asks a decider:

  • the router’s first classifier stage, through the decider role, asked in process, when the router document turns that stage on;
  • the memory writer’s gate: does the session hold something worth keeping;
  • POST /v1/systemone (and /v1/predict), typed decisions under the admin rule;
  • the model’s own scripts, through POST /v1/laya/ask;
  • the test judge, in shadow: on a run’s output with no exit code or test counts, Laya is asked after the request whether it shows a failure, and the answer is journaled beside the structural twin. A question may act only after tools/judge_report.py shows a confident precision of 0.95 at a coverage of 0.3.

20.6 Laya for the model’s scripts

POST /v1/laya/ask is the small, stable shape a script needs, open like the chat API beside it. A request names a question, its choices (2 to 20 labels, or labels with meanings), a scale of 2 to 10 levels, or neither for a yes-no question, and a text or several texts:

The packages that teach it name the engine only as {tandem_url}; a package whose text calls /v1/laya, /v1/library or /v1/memory at a literal address is refused at save, because a literal https:// address fails wherever the machine running the model’s scripts does not trust the server’s certificate authority. The helper they give the model exits 3 with one line when the call fails (“the call failed on this machine, your code is not the cause”), and the model goes on without the verdict. The same packages keep their scratch files in {tandem_scratch}, have the model remove its state hooks (tandem_state, /__state, --dump-state) before it reports done unless the user asked for one, and forbid downloading a runtime or an executable. tools/packages_migrate.py applies these texts to a box’s saved packages document as a new version.

json
POST /v1/laya/ask
{"question": "Does this test log show a failure?",
 "choices": {"failure": "a test failed or crashed", "clean": "every test passed"},
 "text": "<the tail of the log>"}
-> {"label": "failure", "p": 0.97, "sure": true, "probs": {...}, "model": "laya-english", "ms": 31}

Laya reads a window of a long text, its head and its tail, cut by code to what the model reads (1,120 characters for laya-english). A call runs only in the idle window: a caller that meets a running completion waits up to its deadline (15 s) and then gets a 503 busy. The route allows 240 texts a minute by default, answers CORS so a test inside a page can call it, and journals each call with the question as a hash, never the text.

A playtest kit builds on it. GET /v1/laya/kit/laya_drive.py and laya-drive.js serve a driver the model copies into its project: it reads the program’s state each step (a page’s window.tandem_state(), a headless program’s JSON line, a state command), asks Laya which action of a fixed set comes next, applies it as real input, and logs deaths, wins and stuck spots. A driver session has its own rate (10 texts a second) and one journal row at its end.

The same kit checks a page without Laya playing it: laya_drive.py page URL --step ... starts the kit’s headless Chrome, runs the steps with real input (click:, key:, type:, wait:, read:, expect:, shot:), takes screenshots at 1280 and 390 px with an overflow reading, lists console errors, uncaught exceptions and failed loads, asks Laya once for a verdict, closes Chrome on every path and prints one report, in about 5 s. The testing packages make it the browser for a page: never a driver of the model’s own, one check with every main control, fix, check again, then the final message, and no Laya playtest for a one-screen tool. The final message is for the user: what was built, where it is and how to open it, how to use it, one plain sentence on how it was checked, what is untested. Laya’s labels and probabilities, the kit, state hooks, the scratch folder, draw calls and fps stay out of it, and a readout added only for the check is removed before “done” or kept behind ?debug. An opencode run that built a one-file page found no Chrome on its PATH, only Firefox, and spent the rest of its 600 s on a Marionette driver of its own; with the page check, five runs of the same task ended with a checked page and a report in 221 to 260 s (measured). The kit starts Chrome with --password-store=basic and --use-mock-keychain: without them every start waited about 25 s for the desktop keyring.