LiteTUI
Agent-local CLI control
A registered seat from this build can advertise an authenticated loopback
JSONL endpoint using the existing GUI management dispatcher. Interactive and
--rpc seats share these operations; existing stdio clients are unchanged.
This code half adds the server and capability reader only. The corresponding
LiteHarness CLI adapter and live attachment proof are separate work; do not
assume the installed CLI exposes these verbs. No spawn, resume, replacement,
or reparenting operation is added to this endpoint.
The per-seat token is kept under the seat's data root in an owner-only file (0600 on POSIX, protected user-only DACL on Windows). Presence advertises only its path and runtime identity, never the token. If protection or ownership cannot be established, attachment is unavailable rather than unauthenticated. This is OS-user-local control, not protection from another process running as the same user. No remote listener, automatic replacement or authority reparenting is provided. A timeout after dispatch has an ambiguous outcome: inspect state and do not automatically retry a mutating command.
litetui --capabilities --backend B --model M reads only LiteTUI's existing
pinned Claude catalogue or Codex model cache (no app, network, refresh or model
startup). Unknown offline capability fails loudly; Haiku does not support
high. Connected capability requests use the backend's catalogue and refuse
unknown models rather than guessing. There is no second CLI-owned model list.
Model load/context and engine start use their existing backend owners and VRAM
guards; local attachment does not bypass them. Unsupported remote load/context
or non-NInfer engine operations are errors, not successful no-ops. Positive
model/engine operations require separate runtime authorization and validation.
A terminal chat client and agent harness for local LLMs. Textual TUI, streaming, tool use, vision, per-agent memory, and compaction.
Two engines, one seam (src/litetui/llm_backend.py): LM Studio's
desktop server, or LiteTUI's own llama-server from llama.cpp. Switch between
them mid-conversation with /backend — the history survives.
Originally built against qwen3.8-27b on an RTX 5090. LM Studio and llama.cpp run locally; the optional Codex OAuth backend sends inference requests to the hosted Codex service using subscription credentials.
uv sync
run.bat # or: uv run --locked litetui
Either engine will do. When both are detected, a one-time picker runs at first
boot; /backend changes the answer later.
| engine | where | notes |
|---|---|---|
lmstudio |
http://localhost:1234 |
Needs LM Studio's server up. Control (load / unload / context length) goes through the official lmstudio SDK, not a lms shell-out. |
llamacpp |
http://localhost:7470 |
LiteTUI's own llama-server in router mode: one process, every chat GGUF on the box behind it. Attaches to LiteSuite's server on :8088 when that is healthy, otherwise spawns its own — detached, console-safe, with a log file, and killed with the app. |
Neither engine loads anything at boot, and the router runs --models-max 2 so a
model switch cannot quietly fill 32 GB of VRAM. The router's model list is
generated: LiteSuite's install dir, LM Studio's dirs, the HuggingFace cache and
any custom roots, deduplicated, with voice and embedding GGUFs filtered out by
the file's own general.architecture header rather than by guessing from names.
Seat-scoped MCP servers
Use litetui --mcp-servers none for a scout/doc seat with no project MCP
servers, or --mcp-servers SOTS_MCP_CORE,VibeUE for an exact named set.
The default is all. This is invocation-only: it never rewrites .mcp.json
or shared settings. Unknown selected names produce a startup warning rather
than silently looking like an empty tool set. Native Codex receives process-local config overrides for
project registrations; unrelated global Codex servers are outside this scope.
Host-managed stdio servers start lazily by default. /mcp shows not started
(lazy) until tool_search loads one of their tools or a tool is called.
After the first real connection, names and schemas are cached in
.mcp-tool-cache/ under the data root, keyed by launch config and executable/
argument-file timestamps. A cold or invalidated cache has no tool names yet:
an unmatched keyword tool_search discovers enabled cold servers on demand,
then answers from their live tools. This can start multiple cold servers once,
since their tool keywords are unknowable until discovery. An exact server-name
query or select:mcp__server__tool limits discovery to that server. Empty tool
lists remain cold. Parallel first calls wait behind startup/maintenance rather
than failing busy. HTTP servers retain their existing startup.
--mcp-start eager restores eager host startup.
Native Codex owns its direct MCP processes and fixed thread inventory; host lazy-start/cache does not defer those native registrations. Seat selection works there, but an included native server may still start at thread open. There is no automatic idle stop. Disconnect/reconnect are explicit lifecycle operations; no live project-server or RAM benchmark is implied by this feature.
llama.cpp speculative decoding (opt-in)
In /modelcfg → Load → Speculative decoding, choose:
- Unset: preserve existing draft-model settings and server defaults.
- Off (
spec_type: "none"): suppress all draft inputs, including a previously saved external draft path. Clearing the selector is not the same as Off. - External draft (
draft-simple): use an existing draft-model file. - Embedded MTP (
draft-mtp): use the target GGUF's prediction heads, ignoring any saved external draft path. Offered only when the installed binary supports the mode/flags and discovery finds a positive integer<architecture>.nextn_predict_layersin the model metadata—not its filename.
MTP uses max/min/probability defaults 3 / 0 / 0 when tuning is blank; explicit
values are preserved. Stored settings live in llama_load_settings[model].
Eligibility is rechecked at apply/load time using file-identity-cached metadata.
INI regeneration validates the complete proposed preset before evicting resident
models; an already-served target load validates only that target's profile.
Applying load settings still
restarts an owned router and reloads the selected model; it does not restore
other residents. An adopted router's preset belongs to its owner and cannot be
rewritten here. If another model's stale profile prevents the restart, the
resident stays serving; the UI may already have saved the valid target profile.
Speculation is never enabled automatically. Performance depends on the model, quantization and workload; enabling MTP is not a guaranteed speedup. No binary upgrade or model benchmark is performed by this setting.
Claude Agent (experimental native backend)
Install the optional, pinned runtime while LiteTUI is closed:
uv sync --locked --extra claude
claude auth login
uv run --locked --extra claude litetui --backend claude
Or select /backend claude in an installed session. Authentication is delegated
entirely to the official SDK/CLI, matching LiteSuite's ClaudeAdapter: LiteTUI does
not read/copy login tokens or implement OAuth. The tested pair is Python Agent
SDK 0.2.159 and bundled CLI 2.1.281. An explicit claude_executable setting
must report that tested CLI version; reconnect after changing it. Other backends
need no Claude extra. Anthropic's permission requirements for offering subscription
login in third-party products remain a separate distribution gate; a local smoke
test is not distribution approval.
Claude owns its agent loop, tools and native context; LiteTUI owns compaction.
LiteTUI renders text/thinking and native activity, persists session references and
input delivery, and supplies guarded host services through in-process MCP.
Native calls pass mandatory PreToolUse policy checks; host calls pass the existing
LiteTUI dispatcher. Questions and approvals work in TUI and JSONL RPC.
/claude status: native identity and pending delivery states (not prompt content)./claude new: fresh independent segment; old native history and held inputs stay saved./claude continue: explicitly continue unsent prepared inputs after restart./claude resolve: explicitly resolve uncertain deliveries without replaying them. Inspect their effects/history first; this is not rollback.- Returning to Claude resumes the selected Claude segment. Other-provider history is not imported. Busy prompts are durably queued for their original segment.
LiteTUI reads Claude's effective session window before sending, rather than treating
all model IDs as 1M or using another model's cumulative usage. It pauses native tool
turns at the configured auto-compaction threshold, drains the interrupted turn,
summarises, and admits one continuation in the fresh session. Existing local-backend
tool-loop pauses also resume once. User stop, newer input, and changed owners win;
failed or uncertain turns are never automatically replayed. This interrupted-turn
recovery does not enable the optional wake_after_compact preference: ordinary
manual compaction still follows that preference.
A failed window measurement holds the input unsent and keeps the session alive;
/claude continue retries once the metadata service recovers. A preflight-held,
non-RPC input remains prepared if summarising fails. On success, its original
content, profile and source become a fresh prepared input, recoverable through
/claude continue even if the automatic continuation is cancelled. Already delivered
work receives a resume instruction, not a replay of the original prompt or tools.
An RPC input with an operation_id is not transferred between sessions: when already
above threshold, inspect /claude status, run /claude new, then resend as a new
RPC operation ID. The old held input remains saved without replay; /claude continue
cannot shrink that full session. A session already beyond its hard limit may also
refuse the summary request; no success or automatic recovery is claimed in that case.
Native agents/background launches, image attachments, host goal-loop followups, legacy subagent/summary calls, raw native slash passthrough and local loading/context/sampling controls are not enabled. Unsupported calls are refused rather than sent through an OpenAI fallback. Unknown context/usage remains unknown; cumulative cost is not described as per-turn cost.
Initial configuration is controlled: user/project/local settings, external MCP,
skills and Chrome integration are not automatically loaded. Native prompt preset
plus a small LiteTUI append replaces the local-backend system prompt. Changes to
tool inventory or session-creation prompt/configuration require a new session.
The SDK retains its own session files; LiteTUI's companion claude_ledger.json
tracks selected segments and prepared/submitted/terminal/uncertain deliveries.
String-query delivery does not currently supply a stable native user-message UUID;
LiteTUI does not fabricate provider acknowledgment. In-flight deliveries become
uncertain after restart and are never automatically replayed. Native activity
cards are saved at turn settlement; a mid-turn crash can lose those display cards,
not the SDK's authoritative history. There is no old-segment picker or rollback.
Live probes are opt-in, not default tests: e2e/claude_contract_probe.py
(isolated offline controls; --with-query uses no credentials and an unreachable
endpoint), e2e/claude_live_probe.py, and LITETUI_CLAUDE_LIVE=1 for
e2e/claude_tui_smoke.py, e2e/claude_rpc_smoke.py,
e2e/claude_negative_probe.py, e2e/claude_timeout_probe.py, and
e2e/claude_restart_probe.py. The restart probe uses two real OS processes and
checks native identity, context continuity, foreign-history isolation, and owned
child-process cleanup. It does not run inference on a second provider.
Evidence and remaining release gates are in
artifacts/claude-implementation-progress.md.
What it does
Chat + agent loop. Streams replies, renders the thinking trace in a collapsible block, and runs a tool loop until the model gives a plain answer.
Four tools, pi-style, capped at 2000 lines / 50KB per result:
bash · read · write · web_fetch. Toggle with Ctrl+T.
Vision. Paste an image with Ctrl+O, or give it a path — quoted or bare,
with or without a question after it:
C:\shots\screen.png what is the error in this dialog?
"C:\My Folder\screen.png"
A model can only see images if LM Studio reports it as
type: vlm. A GGUF shipped without anmmprojprojector loads astype: llmand returns HTTP 400 on image input — the fix is to place a matchingmmproj-*.ggufbeside the weights, not to change anything here.
Conversations
Every launch opens an agent home, and every conversation lives inside one:
.agents/<Name>/
settings.json the agent's backend, model and thinking level
memory.md an INDEX the agent maintains
soul.md who it is here: preferences, standing corrections
handoff.md in flight / owed / absent-by-decision / caveats
memories/ the memories themselves, one file per idea
conversations/<uuid>/
convo.jsonl append-only transcript
settings.json this conversation's contextual settings
A plain litetui launch opens the agent named LiteTUI by default (your seat
name, when you have set one); if that home is busy, the launch gets its own,
LiteTUI-<8 hex>. /resume picks an agent first. Memory, soul and handoff
belong to the agent and are shared by all of its conversations.
Upgrading from 0.25.0: conversations saved under .convos/<uuid>/ stay on disk
as a read-only archive. They are not migrated and cannot be continued;
litetui --convo <old id> explains how to export one (--export-conversation).
The agent is told its own uuid and absolute path in the system prompt, and
memory.md, soul.md and handoff.md are injected once, into the system
message at the start of the conversation — a snapshot, not a live view. They
are not re-sent each turn: three files on every request is affordable at 1M
context and is not on a local 27B, where it crowds out the conversation. To
see current contents the agent reads them with the read tool, and the prompt
says so. Index lines are capped at ~50 tokens: pointers, never the memory itself.
The transcript is append-only, always
convo.jsonl is never rewritten and never backed up and replaced. A torn
append costs one line, which the reader tolerates; a failed rewrite costs the
conversation. Record types applied in file order:
| type | effect |
|---|---|
meta |
id / created / model |
msg |
one message, verbatim |
edit |
replace ONE message in place |
truncate |
drop a head range, splice new messages in front |
snapshot |
whole list (read for older files; no longer written) |
/compact records a truncate rather than a fresh copy of everything.
Measured on a 30-message conversation: 303 bytes instead of 5,445 — and the
raw history stays on disk behind the marker.
Commands
/new /clear |
start a new conversation (new folder on disk) |
/system <text> |
set the system prompt |
/model [n|name] |
show or switch the agent's model; startup defaults stay unchanged |
/backend [--default] [name] |
switch the agent's backend; only --default also changes the startup default |
/load /unload |
put a model into memory, or free it |
/modelcfg |
per-model Info / Load / Inference screen (see below) |
/think [level] |
off · minimal · low · medium · high · xhigh · unset |
/compact [hint] |
summarise older messages, keep the last 4 |
/convos |
list saved conversations with sizes |
/resume <n|id> |
load one (id = uuid prefix) |
/reconnect /quit |
Esc stops the current turn (asks first; Esc again forces). Ctrl+O paste
image · Ctrl+X clear image · Ctrl+L new conversation · Ctrl+T tools.
/modelcfg — a control an engine cannot drive is greyed, never hidden
Three tabs per model: Info, Load, Inference. Load covers context
length, GPU offload, threads, batch sizes, parallel slots, flash attention, KV
cache quantization, mlock/mmap, RoPE, seed, a draft model for speculative
decoding, a vision mmproj, and the chat template. Inference layers per-model
sampling over your global /settings, plus structured output (a JSON schema)
and named presets.
The two engines do not expose the same knobs. A control the active engine cannot drive renders greyed with the reason attached — it is never silently absent and never a switch that does nothing. That distinction is the point: a missing control is a question, and a fake one is a bug you find much later.
/think — unset is not off
unset sends no reasoning_effort at all, and LM Studio's own default for an
absent value is xhigh. So "unset" means maximum thinking, not none.
/think off is a different thing and sends reasoning_effort: "none".
The six accepted values come from the server's own 400 body, not its docs, which omit the parameter entirely:
Invalid 'reasoning_effort' value: 'x'.
Supported values: none, minimal, low, medium, high, xhigh.
/compact writes to memory before it summarises
Compaction is the moment context is about to be destroyed, so the compaction
prompt runs in two steps: persist first (durable lesson → a new file in
memories/ plus one pointer line; a correction → soul.md; anything in flight
→ handoff.md), then produce the summary. Tools are passed to that call so
the instruction can actually reach disk — without them it would be theatre.
It reports which of the three happened, including
persisted: nothing — TOOLS ARE OFF, so it could not write.
The summarisation call forces reasoning_effort: "none" regardless of
/think: at the server default a 12k budget was spent entirely on the thinking
trace and returned an empty answer, and a compaction that returns nothing is
worse than not compacting.
Skills
skills/<name>/SKILL.md, same shape Claude Code uses — YAML frontmatter with
name and description, then the body.
Only the index (name + one line each) goes into the system prompt. The body
loads on demand through the skill tool. Inlining every body would spend the
context window on instructions the model doesn't need this turn; an index with
no way to open it would be worse.
A directory with no SKILL.md is skipped. One that can't be read becomes a
skill whose description says so — a skill that vanishes on a decode error looks
exactly like one that was never written.
MCP
A standard mcp.json in the repo root; .mcp.json (the Claude Code project
convention) is read too when present, with mcp.json winning a name collision:
{ "mcpServers": {
"files": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] },
"litesuite-tools": { "type": "http", "url": "http://localhost:7423/mcp" }
} }
An entry with a url and no command is reached over plain-JSON HTTP POST —
one request per call, the response body IS the JSON-RPC reply (the stateless
variant of MCP streamable-HTTP; what LiteSuite's /mcp endpoint implements).
Everything else is spawned as a stdio child.
Each server's tools are registered as mcp__<server>__<tool> and dispatched
like any other tool, so the agent loop has no MCP-specific branch. disabled:
true skips one.
A server that fails to start is recorded, not swallowed — the others still load. A tool that silently never appears looks like one the model chose not to use.
Server stderr goes to
mcp.log, never to the terminal. An MCP server is a long-running child process, and a child that inherits this console paints straight over a TUI that owns every cell.
Two instances at once
Running two LiteTUI windows against one model server is supported, and they run in parallel. llama.cpp and LM Studio both serve concurrent requests; two instances on the same loaded model cost one set of weights and do not wait on each other.
The one thing you will be asked about: a different model
Loading a different model in a second instance puts a second set of weights in VRAM, and on a card that was already full that is an OOM. So whenever another LiteTUI is running, a load that would add a model you do not already have resident stops and asks first:
Another LiteTUI instance is running (another session). Loading a different model puts a second model in VRAM and can OOM depending on your setup. Load qwen/qwen3-8b anyway? [Load] [Cancel]
- It asks every time, for as long as the instance is alive. There is no "don't ask again" — the cost is real every time, not just the first.
- It asks on every route into a load:
/model, the picker, a/modelcfgcontext change (a reload is a load), the pre-turn load, and the rpcset_modela host drives. The check lives inside the backend, so a route added later is covered without anyone remembering to add it. - The same model raises nothing. That is the parallel case, and it is the point.
- A headless child (LiteSuite's rpc LiteTUI) cannot show a modal, so it refuses instead of deciding for you, and the host renders the refusal.
"Another instance" means a LiteHarness registry row with cli = litetui and a
live session — which includes LiteSuite's headless children, because they load
models too.
The agent remembers its own setup
An agent's backend, model and thinking level are saved in its home,
.agents/<Name>/settings.json. Switching model or engine publishes there first,
so the agent keeps its choice across launches; another agent you have open does
not move. A conversation's own settings.json keeps the contextual knobs (the
codex reasoning effort, which is a separate vocabulary, and the llama.cpp or LM
Studio load settings for the model in use), and the global settings.json keeps
being the defaults a new agent is born from, plus the app-wide knobs (theme,
seat name, dialog style).
Resuming a conversation does not restore the backend, model or thinking
level it was saved with: the agent's home decides, and a saved load setting for a
different model is reported as not applied. A new agent home with no default
model asks you to choose one with /model before the first send. A launch flag
(--backend, --model, a thinking level) that differs from the agent's saved
setting is refused with an explanation, and LITETUI_MODEL, LITETUI_THINKING
and LITETUI_BACKEND only seed a new agent home; change a saved agent with
/model, /backend and /think. Reconnecting does not request
a local model load. If the saved model/provider is unavailable or connection
fails, sending is blocked with an explanation; choose an available model/provider
or retry /reconnect rather than silently falling back to another model.
The shared settings.json
Two processes from one checkout share one data root and therefore one
settings.json — 70 keys, one file. A save reads it, writes back only the keys
this instance changed, and replaces it atomically, so the think level you set
in one window survives a theme change in the other. Env-sourced fields
(LITETUI_MODEL, LITETUI_THINKING, LITETUI_BACKEND, …) are written on every
save: the file records what you chose, so unsetting a variable must not
silently revert the knob.
⚠️ background-tasks.json no longer lives in the data root. Each conversation
keeps its own background-tasks.json in its conversation folder, so two windows
only meet in a store when they share a conversation (which the session lease
forbids). The old shared file is neither read nor written, and its rows are not copied
into a conversation; task output logs still live in output/tasks/.
⚠️ The model ceiling is shared too. The llama.cpp router holds at most
--models-max models and that limit belongs to whichever instance started it, so
a load can push out a model the other window is mid-turn on. The instance doing
the loading names what is resident before it displaces anything.
Or keep them completely separate
Give one its own root before it starts:
LITETUI_DATA_ROOT=~/.litetui-b litetui
That instance gets its own settings.json, .agents/, tasks and jobs — and,
being a separate data root, no shared anything. LITETUI_SEAT_NAME names the
default agent; LITETUI_MODEL, LITETUI_THINKING and LITETUI_BACKEND seed a
new agent home and do not change a saved one.
Launching backends from a terminal or another agent
Run litetui --help (or python -m litetui.cli --help) for the complete argument
contract. Unknown flags are errors. Options apply to this invocation and do not
overwrite saved defaults; on an existing agent, a backend, model or thinking
level that differs from the agent's saved one is refused.
Normal launches open the TUI in the current terminal. Add --rpc for JSONL over
stdio. Agents must wait for the ready event with launch_status: "ready" and
check its backend, model, base_url, thinking_level and context_length
before sending a {"type":"prompt","message":"..."} command. Failed startup or
an unavailable explicit model blocks the launch prompt. --prompt can supply an
initial prompt for a headed launch; avoid putting sensitive text on process argv.
| Backend | Connect/start | Model and context |
|---|---|---|
custom |
--base-url URL; optional --start-server --server-command JSON |
Server-native arguments load the model. --model selects an advertised ID; --context-length declares the client budget, not a server resize. |
llamacpp |
--server-mode connect or --start-server; optional --server-executable PATH and --base-url URL |
--load-model --model ID --context-length N; optional --model-path GGUF adds its directory to discovery. |
lmstudio |
--server-mode connect or --start-server using installed lms server start |
--load-model --model ID --context-length N uses the LM Studio SDK. |
ninfer |
Connect to discovery or --base-url URL; --start-server uses an allocated port |
Startup accepts --model-path ARTIFACT --context-length N --server-executable PATH. --model must match the served ID. Requires RTX 5090. |
strata |
Connect to the install's port (8080) or --base-url URL; --start-server runs the install's own server |
Strata runs Qwen3.8-Flash-Next on one GPU plus system RAM. --model-path strata-MODEL.json picks a prepared model; --context-length N with --start-server starts it at that size. Install it and download models from LiteSuite's Model Hub, or with Strata's own START-HERE.bat. |
codex |
Subscription login; connects using the configured Codex adapter (native app-server is managed by that adapter when enabled) | --model ID --reasoning-effort LEVEL. Endpoint, context and output limits belong to Codex; local-server overrides are rejected. |
For Codex, --codex-engine native selects the official app-server agent loop;
--codex-engine http selects LiteTUI's own loop over the subscription HTTP API.
Precedence is CLI flag > LITETUI_CODEX_ENGINE > saved conversation/global
codex_native_engine setting > http default. The environment variable is
resolved at CLI startup and accepts the same lowercase native/http values;
an unset or empty variable leaves the saved setting alone. An invalid selected
value is a startup error (an explicit valid flag supersedes the environment).
Both inputs are invocation-only, including on resume: unrelated settings saves
and new conversations do not remember the override. The existing Settings →
Engine toggle remains the way to save a preference. Neither input selects the
backend: use --backend codex or an already Codex-configured agent.
litetui --backend codex --codex-engine native
litetui --backend codex --codex-engine http
$env:LITETUI_CODEX_ENGINE = 'native'
litetui --backend codex
Remove-Item Env:LITETUI_CODEX_ENGINE
--server-mode auto preserves each backend's existing behavior. connect never
starts an inference server. --start-server explicitly authorizes starting one;
it does not restart or evict a server already running. An explicit llama.cpp URL
never falls back to another router. --load-model explicitly authorizes loading
or reloading on llama.cpp/LM Studio; context changes require this flag.
--thinking-level and --reasoning-effort are mutually exclusive aliases.
none is accepted as an alias for UI off. Available levels depend on the
backend/model: NInfer uses off/low/medium/xhigh; Codex uses its model metadata;
custom servers receive the selected value and their template defines support.
--max-tokens sets the output limit for local/custom servers. Use
--server-timeout SECONDS for slow model loads (default 600).
Codex HTTP failure handling
The OAuth/Responses transport retries HTTP 500/502/503/504 and selected connection failures up to two times, with 0.5s then 1s backoff, only before that completion request emits any text, reasoning, tool delta or usage chunk. The same retry budget covers connection loss while waiting for the first chunk. A retry resends that request, not the agent turn; previously completed host tools are not rerun. The server may still have processed a failed request before the connection broke, so this is not an exactly-once generation guarantee.
After any output, failure is surfaced without automatic replay. SSE
error/response.failed/response.incomplete events report their supplied
code/message/reason (including max_output_tokens or content_filter when
present), rather than guessing that context is the cause. These events are not
automatically retried. Bounded, secret-redacted diagnostics also go to
.logs/runtime-errors.log; raw response bodies and authorization headers do not.
HTTP server failures name the status and retry count rather than blaming login.
Existing 401 login refresh, 403 access refusal and 429 usage-limit behavior is
unchanged. Native Codex app-server retry behavior is separate and unchanged.
Codex tool-argument diagnostics
The OAuth/Responses transport preserves terminal-only function arguments from
argument-done, item-done and completed events, including expanded parallel
batches. It appends only the missing suffix, not duplicate JSON. Conflicting
snapshots fail before pending calls are released. This is a demonstrated protocol
fix, not a conclusive attribution of historical missing-argument incidents:
saved conversations retain assembled calls, not the original stream. There is
no local multi_tool_use.parallel unpacker.
LITETUI_CAPTURE_TOOL_EVENTS=1 enables metadata-only diagnostics for a future
explicitly authorized session (OFF by default; no restart/probe is implied).
Only a currently leased agent can append to its own conversation's
tool-call-stream.jsonl. Ownership and non-linked paths are checked on every
append; linked/reparse-point paths, hard-linked files and unowned fallbacks are
refused. No arbitrary output path or raw-argument mode exists.
Records contain event type, per-request sequence, bounded call ID/tool name, and argument/delta character count only. No argument content, headers, auth, reasoning, output text or tool results are saved: arguments can contain sensitive credentials, paths and private text. This instrument shows event order and where nonempty fields arrived, but cannot reconstruct a wrapper or validate its JSON.
Retention is one fixed file per conversation, capped at 1 MiB. At the cap, recording stops without rotation, deletion or growth. Missing folders are not created. Records append across requests (sequence numbers restart); disabling the flag does not remove evidence. Explicit owner cleanup is separate. This uses the store's normal lease/path protections, not a sandbox against hostile filesystem races. Storage refusal only stops diagnostics, not the model reply.
parallel_tool_calls remains True. Setting it to False would ask the provider
for at most one function per response, serializing independent tools into extra
model round trips (latency/token cost). It would neither repair received batches
nor guarantee provider compliance; no new parallel-disable switch was added.
Examples (PowerShell):
# Existing Prism or another OpenAI-compatible server, no environment override needed
litetui --backend custom --base-url http://127.0.0.1:1235/v1 --model ternary-bonsai-2-27b --thinking-level off --context-length 32768
# Start our llama.cpp router, then load a model before accepting the first prompt
litetui --backend llamacpp --start-server --load-model --model qwen3.8-27b-q4_k_m --context-length 32768 --thinking-level medium
# Start LM Studio's API server (LM Studio/lms must already be installed)
litetui --backend lmstudio --start-server --load-model --model qwen/qwen3.8-27b --context-length 32768 --thinking-level medium
# Start NInfer from an explicitly chosen artifact; verify its advertised model ID
litetui --backend ninfer --start-server --model-path D:/models/model.ninfer --model YOUR_SERVED_MODEL_ID --context-length 32768 --thinking-level xhigh
litetui --backend codex --model gpt-5.6-sol --reasoning-effort high
# Custom executable arguments are a JSON array, never a shell command.
# A PowerShell script file avoids nested Start-Process -Command quote loss.
$serverArgs = @('D:/Prism/llama-server.exe', '-m', '{model_path}', '--alias', '{model}', '-c', '{context_length}', '--host', '{host}', '--port', '{port}', '--jinja') | ConvertTo-Json -Compress
litetui --backend custom --base-url http://127.0.0.1:1235/v1 --start-server --server-command $serverArgs --model-path D:/models/bonsai.gguf --model bonsai --context-length 32768 --thinking-level off
Custom command placeholders are {model}, {model_path}, {context_length},
{host} and {port}. Add native flags (GPU layers, projector, cache settings)
as separate array elements. Only loopback endpoints can be started locally.
Output goes to custom-server.log in the data root. LiteTUI cleans up custom
processes it starts; attaching to an existing server never claims ownership.
For authentication use --api-key-env VARIABLE_NAME; the secret stays in the
environment. Custom URL, key-variable name and declared context budget are also
available under /settings, and /backend includes Custom.
The internal LaunchSpec accepts the same controls in its optional launch
object (base_url, server_mode, load_model, context_length, max_tokens,
server_executable, model_path, server_command, api_key_env, timeout).
Its supervised headless path supports all five backends and verifies readiness
before forwarding the prompt. For headed launches use the public CLI in a visible
terminal; the internal headed transport remains unavailable.
Harness seat
LiteTUI registers as a LiteHarness agent and monitors its own inbox, so other agents can reach it and mail wakes it — the message is delivered as a turn.
It does not run liteharness.hooks watch or check_inbox. Both are
consumers that move files out of inbox/new/ for whichever agent id they
resolve, and a second consumer on a shared mailbox is the defect the
ls-liteharness fix retracted — mail vanished for three hours. watch_inbox
also writes to stdout, which a Textual app cannot survive.
Instead it reads the maildir directly under one rule:
Only ever touch a file whose
tois this agent.
Anything addressed elsewhere is left in new/ exactly as found — unread,
unmoved, unclaimed. Expired messages (past ttl_minutes) are cleared without
delivery; the agent's own echo is skipped.
Approval replies and explicit authority changes
A pending RPC approval widget also accepts an addressed inbox line
APPROVE appr-<id> or DENY appr-<id> from the spawner recorded when that request
started. It resolves once, never remembers permission; a wrong sender, addressee,
unknown/expired id or duplicate cannot grant approval. The RPC host's own answer
path is unchanged. Ordinary mail is still delivered as a turn.
Spawner authority is captured from the launch identity envelope at process startup,
not read back from registry metadata. Editing spawned_by in a presence row or
re-registering a running seat does not grant another leader approval authority.
After a leader restart, changing that authority is an explicit security action:
use the supported liteharness spawn --resume <id> --kill-old --spawned-by <newleader>
flow to start the resumed seat with the new launcher identity. There is no automatic
parent-lineage redirect, registry takeover or fallback to a different approver.
Tests
Focused local merge check
Before integrating a candidate, the reviewer names its affected production domain, existing neighboring tests, and intentional omissions in a JSON evidence file:
{
"base": "<full target commit ID before integration>",
"candidate": "<full reviewed worker commit ID>",
"domain": "sidecar jobs and owned app startup",
"review": "<reviewer and durable review reference>",
"neighbors": ["tests/test_sidecar_jobs_t1082.py", "tests/test_sidecar_reader.py"],
"exceptions": []
}
On a clean, committed integration-preview tree containing both commits, run:
python tools/merge_gate.py --base <target-before> --candidate <worker-tip> --review-evidence <review.json> tests/test_new_regression.py
Use the project's existing development interpreter; the final positional test
files are optional additions. Keep evidence outside the checkout or in an ignored
scratch directory. The command requires a nonempty domain, review reference and
neighbor selection; neighbors must already exist at the base commit. Evidence
must name the exact resolved base/candidate IDs. exceptions must be present,
with reasons for intentional omissions (or []), and cannot bypass required tests.
Every run includes test_autoscroll.py and test_sidecar_jobs_t1082.py: these two
files were found red on main by neighboring checks on 2026-10-07. This is a small
incident-derived floor, not automatic dependency discovery. Select additional
existing neighbors by tracing the changed behavior, not just the new test file.
The command reuses run_all's inventory and per-file collection accounting, runs
only the selected files with their proper runner, checks git diff --check, and
fails on errors, zero-item selected files, timeouts, or a changed/dirty tree.
Inherited PYTEST_ADDOPTS and configured pytest addopts are deliberately cleared
so they cannot silently expand the selection. Other pytest configuration remains.
Its PASS names the exact HEAD tested; rerun if the integrated tree changes.
Enforced: evidence shape and commit identity, existing-neighbor selection, commit ancestry, clean stable tree, and selected-check exit status. Reviewed by people: whether the domain and neighbors cover the actual change, exceptions are justified, and the review reference is genuine. This command does not authenticate the reviewer, install hooks, merge, or enforce GitHub branch protection. The merge owner must require it on the actual integration tree before accepting that tree; a worker-only green is not integration evidence. Required review, current-intent and human-look gates remain separate.
Full validation (when explicitly requested)
uv run --locked python tests/run_all.py
Believe the exit code rather than a pass count. The runner maintains a reviewed
inventory of pytest files and legacy standalone scripts; bare pytest is not a
substitute for that inventory. CI's full validation is unchanged. Neither engine
nor network is required by the selected unit tests; no live-service checks are
included in the focused merge command.
Bundled
chrome-bridge/ — drive Chrome from Python (navigate, read page, click,
screenshot) via an MV3 extension. pccontrol/ — Win32 mouse/keyboard/window
automation and OCR helpers. Both are separate tools that happen to live here;
chrome-bridge also has a canonical copy elsewhere, so treat this one as a
snapshot rather than the source of truth.
Metadata
Release files for litetui 0.25.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| litetui-0.25.1.tar.gz | 2.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| litetui-0.25.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 4.1 MB
Release files / litetui-0.25.1.tar.gz
| Download URL | litetui-0.25.1.tar.gz |
|---|---|
| Size | 2.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
92be8147c980108c1ee5e3677198b38f623b75dfd439057a03e38e00a2e6f16d
|
|
BLAKE2b-256 checksum How to use checksums |
a4c827e3f12b2f9c37f7d4d1bb62e20bd4d99c9ee2046f26907a1d1a8d943da4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / litetui-0.25.1-py3-none-any.whl
| Download URL | litetui-0.25.1-py3-none-any.whl |
|---|---|
| Size | 1.4 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
863d3c5dc276ab7f2fae6aa138222e55eb5f23c116f2e69a3c7748efb105f5b2
|
|
BLAKE2b-256 checksum How to use checksums |
711774dddaa6129482c4f052c6a99384bddbae97902b80a3cb55418344165dde
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|