TULKAS is a macOS desktop console for working with language models. It is organized like a DAW: each track is a conversation bound to a provider; the mixer shows one channel strip per provider with a budget fader that actually enforces; the arrangement is a timeline of finished turns. Two permission systems gate what models may do: tool-exec grants (may a model's proposed command run?) and autonomy grants (may the console dispatch turns with nobody present?). Both are time-boxed and charge-limited; both are visible as lit controls, not settings.
Everything in the app travels one JSON-RPC dispatcher. The same daemon
serves the GUI, a Unix socket for the tulkas CLI, and a LAN
WebSocket for paired devices — anything you can click, you can script
(§08–§09). All state is local: SQLite databases and a key vault under
your own Application Support directory (§10). No telemetry, no
phone-home; provider keys are yours (BYOK).
TULKAS ships as a Developer-ID-signed, Apple-notarized disk image.
Download, open, drag TULKAS.app to Applications, launch.
No account, no telemetry, no phone-home — provider keys are yours (BYOK).
Release manifest (JSON) — version, build, size, and SHA-256 of the exact artifact this button serves.
- Launch
TULKAS.app. The app boots its daemon in-process and connects; the status chip at the top-right of the command bar reads BRIDGE UP when ready (usually under a second). The bottom status bar shows the durable event sequence number — if it advances, the store is live. - You start with one track, MAIN. Type into the Console prompt
box and press TRANSMIT ▸. With no key configured, pick the
fakeprovider in the Console's provider strip first — it is a built-in, zero-cost echo vendor that exercises the whole pipeline, so you can verify the instrument works before spending a token. - To use a real provider, open the Mixer (06 MIXER pill) and
read the health LEDs (§07). Add a key with the CLI —
tulkas rpc tulkas.vault.set '{"vendor_id":"deepseek","token":"…"}'— then press REFRESH in the Mixer header; the strip should flip to KEYED. - Watch a turn arrive three ways at once: streaming text in the Console, a growing clip in the Arrangement, and rows in the wire log. If those three disagree, that is a bug — report it.
Headless
alternative: tulkas serve --port 8787 runs the same daemon
without the GUI; every operation in this manual then works over
tulkas rpc (§08).
daw = console + mixer + arrange + terminal.? button in every pane. Off = no question marks anywhere.The bottom status bar shows: last durable seq, an
AUTOSAVED flash (driven by real store-write events, not a timer),
and a one-line summary of the last wire exchange (method, status,
latency, tokens).
Busy rule: one streaming turn per track. A send into a busy track
is rejected with busy — nothing queues silently. Budget
rejections behave the same way (§04) and flash the mixer's rejection LED.
One strip per provider. Top to bottom: name chip · transport LED + live
VU (streaming bytes) · the budget fader · MUTE · today's readout
(tokens · est $; a free vendor shows — for
dollars, never a fake $0.0000) · rejection LED with count · the health
LED (§07).
The fader is admission control. Detents top-to-bottom:
∞ · 1M · 500k · 200k · 100k · 50k · 20k · 10k · 5k · 1k · 0
tokens/day. Drag commits on release; the thumb renders the
daemon-confirmed value, so a failed set snaps back visibly. A send that
would exceed the cap is rejected at admission — an already-streaming turn
is never cut mid-flight; ■ STOP is the in-flight control. Caps
reset at 00:00 UTC. MUTE blocks all sends on that provider.
The TRACKS column (right side) carries per-track arms:
3:58) and a charge readout (2/10). At 0:00 or exhaustion it renders disarmed on its own — the countdown is a projection of the same expiry the enforcement check reads.used/max + a m:ss TTL countdown (default 1 h). Click while armed = disarm.The two grants are independent: an AUTO-armed track can dispatch by itself but still cannot execute a tool result unless it is ALSO record-armed.
origin) and survives relaunch.Session grid (bottom rail): prompt presets. Click a pad = launch that prompt on the focused track (arm/budget gates apply; a rejection flashes the pad red for 3 s). Long-press = overwrite the pad with the current Console prompt text. + SAVE mints a new pad from it.
To schedule an unattended run:
- Arm autonomy on the target track: Mixer → its AUTO pill → set the run count (e.g. 2) → ARM. This is the budget of unattended dispatches you are granting; each fire burns one.
- Arrangement header → + TRIGGER. Kind
SCHEDULE, pick the target track, enter the prompt, optionally a vendor (blank = the track's bound vendor), set date/time. CREATE. - A tick mark with a clock glyph appears on the track's automation lane at the fire time. The scheduler sweeps every 30 s, so firing precision is ±30 s — "around this time unattended," not cron.
- On fire: one autonomy charge burns, the turn runs, and the clip
renders hatched. The pill's readout drops (e.g.
0/2 → 1/2). On the last charge the pill goes dark by itself.
To chain tracks (follow / side-chain): same form, kind
FOLLOW, choose the SOURCE track. When the source finishes a
human-initiated turn, the trigger fires on the target.
Turns started by triggers never fire further follows — chains are capped
at one hop by construction, so A→B→A loops cannot exist. A trigger
targeting its own source is refused at creation.
not_armed_autonomyNo live autonomy grant covered the fire. NOTE: an unarmed schedule trigger stays active and re-skips every 30 s sweep until you arm the track or delete the trigger.busyThe target track was mid-turn. The charge is still spent — you armed N unattended attempts, and a collision is one of them.vendor_missingThe trigger's vendor no longer exists.To stop everything: FREEZE in the command bar (§02) — one confirmed click revokes all autonomy everywhere. In-flight turns are stopped with ■ STOP as usual.
Keys live in the vault (macOS Keychain–backed, service
TULKAS-VendorVault). Set one per provider:
fakenoneBuilt-in echo sandbox. Free. Use it to test anything.claude-cliits own loginDrives your installed claude CLI. Reports real token usage. Respects your model choice.deepseekvaultDirect API.antigravityvaultGoogle managed agents.philabnone (local)Local Phi-class small models; needs a PHILAB install.thorondorvault (bearer, optional)The built-in router: one endpoint fronting ollama / openai / anthropic / deepseek. Upstream keys live in the ROUTER's environment, not in TULKAS.The router path. Run thorondor serve (default
127.0.0.1:8787) with the upstream keys you want in its env.
TULKAS asks it what it fronts (GET /v1/providers) and, when
the router covers a provider, dispatch for that vendor_id
rides the router automatically — including trigger-fired turns. If the
router sets THORONDOR_API_TOKEN, vault the same value under
thorondor.
The CLI talks to the same dispatcher over the same socket as the GUI — a turn sent from the terminal appears in the app's Console, Arrangement, and wire log identically. This parity is load-bearing: the product's own simulation batteries drive it this way.
tulkas.llm.send / llm.abortDispatch a prompt (ack {turn_id}, events follow) / cancel a turn.tulkas.tool.resultFeed a staged command's output back. executed:true burns a tool-exec charge or is refused.tulkas.track.arm / disarmTool-exec grant on/off (TTL default 4 h, optional max_runs).tulkas.autonomy.arm / disarm / freezeAutonomy grant (max_runs REQUIRED, TTL default 1 h) / revoke one / revoke ALL.tulkas.trigger.create / list / deleteSchedule (when_spec=RFC3339) or follow (when_spec=source track) triggers.tulkas.track.list / create · conversation.getTracks with arm state · full turn history (each turn carries origin).tulkas.mixer.get / setStrips + budgets / partial budget update (max_tokens_day: null clears).tulkas.vendor.healthOn-demand probe; per-vendor {reachable, authenticated, source: routed|vault|builtin|none, error}. Never polled.tulkas.vault.setStore a provider key (write-only). Honest ack: a failed store is an error, never accepted:true.tulkas.preset.list / save / deleteSession-grid pads.tulkas.schedule.*The on-board day-planner engine (getDay, createTask, generateDayPlan, acceptProposal).tulkas.atlas.* · connectors.* · spec.get · term.logAtlas board · connector rows + probes · the embedded PROTOCOL text · terminal audit rows.tulkas.bus.subscribeTransport-level: stream every event on this connection (what --follow uses).Errors are JSON-RPC standard: -32001 busy ·
-32002 budget-rejected · -32003 exec-unarmed ·
-32602 bad params / unknown vendor · -32603
internal.
~/Library/Application Support/TULKAS/audit.dbThe durable store: turns, grants, triggers, budgets, wire log, events. SQLite; back this up and you have your history.…/TULKAS/schedule.dbThe on-board planner engine's store.…/TULKAS/tulkas.sockThe CLI socket (created at daemon boot, mode 0600).TULKAS-VendorVaultProvider keys (one item per vendor_id).TULKAS_DATA_DIR (env)Relocates the socket + databases for the headless daemon — scratch/test instances. Keep it SHORT (see §11).TULKAS_VAULT_DIR (env)File-based vault override (<vendor>.token files, 0600) instead of Keychain — test isolation.PathTooLongmacOS limits socket paths to 103 bytes; your TULKAS_DATA_DIR is too deep.Use a short dir (e.g. /tmp/tulkas-x). The error names the limit — this is deliberate; the daemon refuses to boot socketless.DaemonAlreadyRunningAnother instance holds the socket (the app's in-process daemon counts).Quit the other instance, or give the new one its own TULKAS_DATA_DIR + --port.thorondor base_url), then REFRESH.vendor_id; a typo'd id stores under the wrong name. The vault.set ack is honest — check it wasn't an error.Re-run tulkas.vault.set with the exact id from §07, then REFRESH.rpc hit another socket (env leaked a scratch TULKAS_DATA_DIR).Pass --socket explicitly at the default path, or clear the env var.TULKAS_VAULT_DIR files instead.