Detailed architecture reference for Ho Launcher. For a concise overview see Architecture.md.
Tauri desktop app with React frontend and Rust backend.
| Layer | Technology |
|---|---|
| Shell | Tauri |
| Frontend | React, TypeScript, Vite, Tailwind CSS, DaisyUI |
| Backend | Rust, Tokio |
| MCP SDK | rmcp |
| LLM | rig-core, graph-flow, toon-format |
| Script runtimes | RustPython, Boa |
Identifier: cool.thinkers.ho.
Ho-ho/
├── backend/ # Rust/Tauri backend
│ ├── src/ # Application modules
│ ├── prompts/ # LLM prompt templates
│ └── capabilities/ # Tauri permission scopes
├── frontend/ # React/Vite frontend (app/, features/, shared/)
├── test/ # CLI E2E scripts and fixtures
├── build/ # Platform build scripts
├── scripts/ # Version and verify helpers
├── EULA/ # End-user license
└── docs/ # Documentation
| Module | Path | Responsibility |
|---|---|---|
agent |
agent/ |
Graph-based agent run, rig tool loop, handoff, conversation store, pending_run, commands |
domain |
domain/ |
Shared config types and validation (agent, mcp, model, skill, guardrail, budget, attachment, config_path) |
runtime |
runtime/ |
Durable Run + RunEvent store, graph session persistence, Tauri commands |
approvals |
approvals/ |
Persisted approval requests and resolution service |
registry |
registry/ |
Split config files (models.json, mcp-servers.json, agent.json, skills.json, etc.) |
guardrails |
guardrails/ |
Guardrail profile service |
budget |
budget/ |
Token budget check and ledger |
skills |
skills/ |
SKILL.md loader, registry service, MCP context injection |
agent_os |
agent_os/ |
Platform capabilities for builtin MCP |
app_platform |
app_platform/ |
Installed app discovery and launch |
app_run |
app_run/ |
App discovery, execution, built-in App Launcher MCP |
launcher |
launcher/ |
Tool suggestions, direct tool execution |
llm |
llm/ |
rig transport, completion, health checks, TOON, prompts |
mcp |
mcp/ |
MCP client service (stdio + HTTP + builtin), OAuth, client handler, task polling |
cli |
cli/ |
Headless operator commands |
script_run |
script_run/ |
Script generation + RustPython/Boa execution |
settings |
settings/ |
Core settings.json, split registry merge (service.rs), field updaters, prompt service, registry access, EULA |
system |
system/ |
Tray, shortcuts, OS-specific behavior |
window |
window/ |
Window show/hide, positioning, click-through, DevTools overlay |
init |
init/ |
App state init, async startup sequence |
utils |
utils/ |
Logging, analytics, file context, tool scoping, json_store, path utils |
dev_tools |
dev_tools/ |
DevTools toggle |
backend/src/init/state.rs:
MCPAppState,LLMAppState,ScriptRunState,AppRunState,SettingsState,AgentManager,ApprovalStore,GraphSessionStore,RunManager
backend/src/init/services.rs → setup_async_initialization:
- Load settings (merge split registry files) → emit settings events to frontend
- Setup windows, tray, global shortcut
- Start built-in App Launcher MCP HTTP server (port 1228)
- Initialize configured MCP servers; auto-connect if enabled
- Initialize LLM from settings models; health-check via tiered probe
- Trigger app discovery; register settings event listeners
Frontend (React)
│ invoke()
▼
Tauri Commands (94 registered in `commands.rs`)
│
├── launcher/ → MCP tool suggestions & execution
├── agent/ → graph-flow → rig agent → MCP tools
├── runtime/ → run_get, run_list, run_list_events
├── approvals/ → list_approvals
├── script_run/ → LLM script gen → embedded runtime
├── mcp/ → Server connect, OAuth, tool/resource/prompt calls, elicitation
├── llm/ → rig completions & health checks
├── app_run/ → OS app discovery & launch
└── settings/ → JSON persistence + registry split files
│
│ emit events (run-event, launcher-progress, …)
▼
Frontend (runTimelineStore + event listeners)
Core settings.json: general, appearance, advanced, prompts, script_run, rules.
Split registry (atomic write via write_json_atomic):
| File | Contents |
|---|---|
models.json |
LLM models and LLM settings |
mcp-servers.json |
MCP servers and MCP settings |
agent.json |
Agent settings (skillIds, guardrail limits) |
skills.json |
Skill registry |
skills-policy.json |
Skill loading mode |
guardrails.json |
Guardrail profiles |
token-budgets.json |
Token budget limits |
approvals.json |
Approval request history |
LLM models are flat objects:
{
"id": "...",
"name": "...",
"provider": "openai",
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-4o",
"apiKey": "env:OPENAI_API_KEY",
"enabled": true
}- Debug path:
{project_root}/app-data/ - Release path: OS config dir →
thinkers/ho/ - Run files:
{app_data_dir}/runs/{runId}.json,{app_data_dir}/events/{runId}.jsonl - Conversations:
{app_data_dir}/conversations/{convId}.json - Graph sessions:
{app_data_dir}/sessions/{runId}.graph.json - Pending agent runs:
{app_data_dir}/pending-runs/{runId}.json - Cancel / resume IPC:
{app_data_dir}/cancel-requests/{runId},{app_data_dir}/resume-requests/{runId} - Token ledger:
{app_data_dir}/token-ledger.jsonl - App launcher cache:
{app_data_dir}/app_cache.json - Application logs:
{app_data_dir}/logs/ho.log.*(daily rotation)
Core UI settings live in settings.json. Registry data (models.json, mcp-servers.json, agent.json, etc.) is merged on load.
Graph-flow task pipeline (backend/src/agent/graph.rs):
| Task | Module | Role |
|---|---|---|
prepare_run |
tasks/prepare.rs |
Build preamble; load skills + MCP skill context |
execute_agent |
tasks/execute.rs |
rig Agent + all MCP RigMcpTool + AgentApprovalHook + optional handoff |
complete_run |
tasks/complete.rs |
Terminal run status and events |
All scoped MCP tools are bound to the rig tool server. Rig tool names use {serverId}_{toolName} slugging (mcp/naming.rs).
Graph execution state persists across approval waits via GraphSessionStore. Runs enter RunStatus::Blocked while awaiting approval.
Observable state follows a single durable trace separate from orchestrator internals.
| Layer | Ho module | Role | SSOT? |
|---|---|---|---|
| Run record | runtime/store.rs → runs/{runId}.json |
Status, input, timestamps | Yes — durable run metadata |
| Run events | events/{runId}.jsonl |
Append-only trace: tool.*, message.completed, run.* |
Yes — UI, audit, recovery |
| Graph checkpoint | GraphSessionStore → sessions/{runId}.graph.json |
graph-flow task pointer + context for resume after approval | Internal only |
| Pending run | agent/pending_run.rs → pending-runs/{runId}.json |
Detached CLI worker payload | Internal only |
| Run coordination | AgentManager |
Cancel tokens + resume notifiers keyed by run_id |
Ephemeral only |
| UI projection | runTimelineStore |
Subscribes to run-event; rebuilds from run_list_events |
Derived from events |
Rules:
Run+RunEventJSONL are the only durable observable state.GraphSessionStoreis a private orchestrator checkpoint keyed byrun_id.- All user-visible progress is emitted as run events; frontend listens on
run-event. agent_get_sessionremoved — userun_get+run_list_events.AgentManagerholds coordination only (cancel, resume), not duplicated domain state.
All provider HTTP goes through rig-core OpenAI-compatible client:
llm/openai_compatible.rs— resolve API key (includingenv:refs), build clientllm/completion.rs—complete, health helpersllm/types/error.rs— typedLLMErrorCode, secret redaction
Launcher suggestions use local keyword search only. Agent mode uses native tool calling.
Shipped templates in backend/prompts/ (3-layer: preamble, *-system, *-user per workflow).
| Category | Runtime use | User-customizable preamble |
|---|---|---|
result_analysis |
Outcome formatting | Yes |
script_run |
Vibe script generation | Yes |
agent_planning |
Agent preamble only | Yes |
app_analysis |
App metadata batches | No |
system |
Global prefix for workflow system prompts | No |
Launcher suggestions use local keyword search only — no LLM prompt category.
Custom preamble overrides live in settings.json → prompts.customTemplates. One enabled override per customizable category.
Reference implementation: backend/src/approvals/. New feature modules follow this shape.
feature/
├── mod.rs # pub re-exports only
├── commands.rs # #[tauri::command] → service (thin handlers)
├── service.rs # orchestration
├── store.rs # persistence (when module owns files)
└── types.rs # module-local types; import domain/* for shared config
Optional: tasks/, tools/ subdirs for complex features (agent).
| Layer | Type | Used in | Maps to IPC |
|---|---|---|---|
domain/error.rs |
AppError, validation |
domain, registry, services | via BackendError::from |
errors.rs |
BackendError, BackendResult |
commands, all IPC handlers | JSON { message } to frontend |
llm/types/error.rs |
LLMError, LLMErrorCode |
llm service internals | BackendError::msg in llm commands |
mcp/types |
MCPError |
mcp client/service | BackendError or MCPResponse::error |
Rules:
- Feature code returns
BackendResult<T>at the command boundary only - Services and stores use
AppErroror module-specific errors internally - Never expose internal error types across IPC; redact secrets in LLM errors
commands → service → store → domain → utils
domain/never imports feature modules- One persistence writer per JSON file (
RegistryStorefor split config) - Internal errors (
AppError,MCPError,LLMError) map toBackendErrorat IPC boundary only