Catalyst Code components communicate over newline-delimited JSON on stdio. Commands flow from the frontend (TUI/web/SDK) to the core on stdin; events flow from the core to the frontend on stdout.
This document describes every command and event type defined in the wire
protocol. Commands live in core/src/protocol/commands.rs, common wire types in
common.rs, event construction in events.rs, and the version/capability
catalog in version.rs.
- Transport Conventions
- Common Types
- Commands (Frontend → Core)
- Events (Core → Frontend)
- Typical Session Flow
- Error Handling
Encoding: UTF-8 JSON, exactly one JSON object per line (\n).
Direction:
Tagged union: Every message carries a "type" field that identifies the
variant. Commands use #[serde(tag = "type")]; events use an explicit
"type" field in the JSON.
Buffering: Frontends should either flush stdin after each command or set line-buffered mode. The core reads stdin line-by-line with a read buffer.
Output authority: runtime::EventSink is the only core component that
writes protocol lines to stdout. It serializes each event as one locked line,
adds lifecycle metadata, redacts recognized secret fields, and drops events
whose run/session scope is no longer active.
The current wire version is 2. Every emitted event includes
protocol_version. Events emitted inside a foreground run also include
session_id, run_id, and a monotonically increasing per-run sequence.
Session-owned background work includes session_id; child-agent events retain
their explicit child run_id fields.
Clients must tolerate unknown fields and unknown optional event types. This is what allows a v1 client to continue operating while a v2 core adds lifecycle metadata.
Returned in ready and models events.
| Field | Type | Description |
|---|---|---|
id |
string | Model identifier (e.g. "glm-5.2") |
name |
string | Human-readable name |
reasoning |
boolean | Whether the model supports reasoning/thinking |
context_window |
integer | Context window in tokens |
max_tokens |
integer | Maximum output tokens |
thinking_levels |
string[] | Reasoning effort levels (["low","medium","high"]) |
vision |
boolean | Whether the model accepts image inputs |
provider |
string | Provider name that owns this model (for multi-provider per-turn routing) |
Every event is an object with a "type" field plus additional data fields:
{"type": "event_name", "field1": "value1", "field2": "value2"}Construction in the codebase uses the builder pattern:
Event::new("event_name")
.with("field", json!(value))Source: Event (core/src/protocol/events.rs).
All commands are deserialized via #[serde(tag = "type")]. The type field
determines which variant is parsed.
Initialize a core subprocess. Sent once at startup. The core responds with a
ready event containing the initial model list and config, followed
by protocol_hello with the supported version and capabilities.
{
"type": "init",
"protocol_version": 2,
"client": {
"name": "catcode-tui",
"version": "0.2.0",
"capabilities": ["run_ids", "session_ids", "event_sequence"]
}
}All fields other than type are optional. The legacy plain
{"type":"init"} command remains supported.
Full reset: clear the in-memory conversation and the session file. Re-emits
a reset event.
{"type": "reset"}Clear only the in-memory conversation. The session file is preserved so a restart can resume.
{"type": "clear"}Drop the last turn (user prompt + assistant reply + tool calls/results). Also restores the latest auto filesystem checkpoint when one exists.
{"type": "undo"}Abort the currently running turn and drop any queued prompt. A v2 core emits
run_cancelled with the cancelled identity/reason and retains the legacy
aborted terminal signal.
{"type": "abort"}Drop a queued follow-up/steer prompt without aborting the running turn. Useful for the TUI's Esc key to cancel just the queued message.
{"type": "clear_queue"}Request a session statistics summary. Returns a stats event.
{"type": "stats"}Lifecycle diagnostics are read-only and expose coordinator-owned work:
{"type": "runtime_status"}The runtime_status response includes the active session_id/run_id, the
number of discarded stale results, the last cancellation, registered resources
(including whether cancellation has fired), and pending approval/ask/sudo
counts. Resource labels are operational identifiers and never contain tool
arguments or credentials.
On startup, session_recovered reports ignored malformed/truncated records and
run IDs that had a started record without a terminal state. Those runs are
persisted as interrupted; destructive work is never restarted automatically.
Request a token-usage breakdown of the current context. Returns a
context_breakdown event with total tokens, context
window usage, per-role buckets, and the top token consumers.
{"type": "context"}Request provider plan/rate-limit usage for the currently selected model. Each
provider implements its own stats. Returns a usage event.
{"type": "usage", "model": "glm-5.2"}| Field | Type | Required | Description |
|---|---|---|---|
model |
string | no | Override the last-used model for routing |
Send a user prompt and start an assistant turn. This is the primary way to talk to the model.
{
"type": "send",
"prompt": "Write a Rust function that reads a file",
"model": "glm-5.2",
"reasoning_effort": "high",
"images": ["data:image/png;base64,..."]
}| Field | Type | Required | Description |
|---|---|---|---|
prompt |
string | yes | The user's message |
model |
string | yes | Model ID to use for this turn |
reasoning_effort |
string | no | Reasoning/thinking effort level (e.g. "low", "high") |
images |
string[] | no | Image data URLs (data:image/...;base64,...) or absolute file paths |
Interrupt an in-flight turn and redirect it with a new prompt. If no turn is
running, behaves like send.
{
"type": "steer",
"prompt": "Actually, use async instead",
"model": "glm-5.2",
"reasoning_effort": "high"
}Same fields as send. Emits a steer event.
Apply an API key to a provider at runtime. Overrides both the config file
api_key and api_key_env for that provider.
{"type": "set_key", "api_key": "sk-...", "provider": "umans"}| Field | Type | Required | Description |
|---|---|---|---|
api_key |
string | yes | The API key |
provider |
string | no | Provider name; omitted = apply to currently active provider ("default" slot) |
Emits authed event. The provider's models are refreshed.
Set or clear a search-tool API key (Exa / Tavily) for web_search.
{"type": "set_search_key", "provider": "exa", "api_key": "sk-..."}| Field | Type | Required | Description |
|---|---|---|---|
provider |
string | yes | "exa" or "tavily" |
api_key |
string | yes | Empty string clears the stored key |
Persisted to config.json search_keys. Emits search_key_set.
Switch the active model provider at runtime.
{"type": "set_provider", "name": "opencode-go"}| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Provider name from config; unknown names are ignored |
Re-resolves base URL, key, and wire protocol. Re-discovers models. Emits
provider_changed and (when keyed) authed.
List the built-in provider presets (Umans, OpenCode Go, OpenRouter, DeepSeek) plus plugin
OAuth providers. Emits provider_presets.
{"type": "list_provider_presets"}Log in to a first-party provider preset with an API key.
{"type": "login", "preset": "umans", "api_key": "sk-..."}| Field | Type | Required | Description |
|---|---|---|---|
preset |
string | yes | Preset ID ("umans", "opencode-go", "openrouter") |
api_key |
string | no | API key. Required when the preset has no api_key_env set. |
Creates the provider config, sets its API key, persists, re-aggregates models.
Multiple providers can be logged in simultaneously. Emits provider_changed,
authed, and info events.
Log out of a provider: drop its runtime key and remove it from the configured providers.
{"type": "logout", "provider": "umans"}| Field | Type | Required | Description |
|---|---|---|---|
provider |
string | yes | Provider name to log out |
Emits info, provider_changed, and authed events. No-op with error
event when not logged in.
Start plugin-based OAuth login for a plugin-declared provider_id.
{"type": "login_oauth", "preset": "chatgpt"}| Field | Type | Required | Description |
|---|---|---|---|
preset |
string | yes | Provider ID matching a plugin's oauth.provider_id |
Emits oauth_prompt events for the user to authorize, then process the code.
Complete a pending plugin OAuth login by submitting the authorization/user code
from a prior oauth_prompt.
{"type": "oauth_code", "code": "...ABC123..."}| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | The authorization code obtained from the OAuth provider |
Change the approval mode at runtime.
{"type": "set_approval", "mode": "always"}| Field | Type | Required | Description |
|---|---|---|---|
mode |
string | yes | "never", "destructive", or "always" |
Emits approval_changed.
Change a runtime config knob.
{"type": "set_config", "key": "bash_timeout_secs", "value": 60}| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | Recognized keys: bash_timeout_secs, sandbox, auto_compact |
value |
any | yes | Coerced from the JSON type |
Emits config_changed.
Get the current vision-handoff configuration. Emits vision_config.
{"type": "get_vision_config"}Set the vision-handoff configuration and persist to .catalyst-code/vision.json.
{
"type": "set_vision_config",
"enabled": true,
"vision_model": "glm-5.2-vision",
"vision_models": ["glm-5.2-vision", "gpt-4o"]
}| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | yes | Enable vision handoff (default: true when absent) |
vision_model |
string | no | Preferred handoff target; empty = cheapest same-provider |
vision_models |
string[] | no | Curated list of vision-capable models |
Emits vision_config.
List available session files. Emits sessions.
{"type": "list_sessions"}Load a specific session file, replacing the current conversation.
{"type": "load_session", "path": "sessions/2026-07-15.jsonl"}| Field | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Path to the session file |
Set a human-readable title for a saved session.
{"type": "rename_session", "path": "sessions/2026-07-15.jsonl", "title": "Auth refactor"}| Field | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Path to the session file |
title |
string | yes | New title string |
Delete a non-active saved session and its metadata.
{"type": "delete_session", "path": "sessions/old-session.jsonl"}Pin or unpin a session in the picker.
{"type": "pin_session", "path": "sessions/important.jsonl", "pinned": true}Start a fresh session file in the same project directory. An optional path (a
filename, not a full path) overrides the auto-generated name.
{"type": "new_session", "path": "refactor-auth.jsonl"}| Field | Type | Required | Description |
|---|---|---|---|
path |
string | no | Custom session filename |
Create a hybrid filesystem checkpoint (git stash ref or file snapshot).
{"type": "create_checkpoint", "label": "before-auth-refactor", "paths": ["src/auth.rs"]}| Field | Type | Required | Description |
|---|---|---|---|
label |
string | no | Human-readable label |
paths |
string[] | no | Specific paths to snapshot (omitted = snapshot all) |
List known checkpoints for this session/workspace. Emits checkpoints.
{"type": "list_checkpoints"}Restore a checkpoint by id (filesystem only; conversation unchanged).
{"type": "restore_checkpoint", "id": "ck-abc123"}Force a context compaction now, regardless of the threshold.
{"type": "compact", "instructions": "Focus on code samples and API usage"}| Field | Type | Required | Description |
|---|---|---|---|
instructions |
string | no | Override compact_instructions for this call only |
Emits compacting, then compacted, then optionally error if still over
limit.
Save a durable memory note (persisted across sessions). Core generates a name,
saves it, and refreshes system-prompt injection. Emits memory_saved.
{"type": "save_memory", "text": "User prefers async Rust patterns", "tags": ["rust", "async"], "scope": "workspace"}| Field | Type | Required | Description |
|---|---|---|---|
text |
string | yes | Memory content |
tags |
string[] | no | Optional tags for retrieval |
scope |
string | no | "workspace" (default) or "global" |
List saved memories (both scopes). Emits memory_list.
{"type": "list_memory"}Delete a memory by its id. Emits memory_saved describing the outcome.
{"type": "forget_memory", "id": "my-memory-slug", "scope": "workspace"}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Memory id (slug or name) |
scope |
string | no | Scope to search; omitted searches both |
Ask core to re-inject memories into the system prompt (called after saving a memory externally or to force a refresh).
{"type": "refresh_memory"}Install a plugin from a local directory or a GitHub Release.
{"type": "install_plugin", "path": "owner/repo@v1.0.0", "scope": "global"}| Field | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Local directory, owner/repo[@tag], or full GitHub URL |
scope |
string | no | "global" (default — ~/.catalyst-code/plugins, every workspace) or "workspace" |
Remove a named plugin.
{"type": "remove_plugin", "name": "my-plugin"}Re-enable a disabled plugin.
{"type": "enable_plugin", "name": "my-plugin"}Disable a plugin without removing it.
{"type": "disable_plugin", "name": "my-plugin"}List all installed plugins with their enabled/disabled status. Emits
plugin_list.
{"type": "list_plugins"}Re-scan plugin directories, preserving enabled/disabled flags.
{"type": "reload_plugins"}Re-emit a plugin_trust_prompt event for this project's untrusted
project-scoped plugins (the /plugin-trust command). Includes plugins with a
recorded decision so the user can change their mind.
{"type": "plugin_trust_prompt"}Record the user's trust decisions for this project's plugins, persist them,
re-scan so newly-trusted plugins load, and emit plugin_trust_applied.
Decisions are merged per plugin name (names not mentioned keep their prior
state).
{"type": "plugin_trust_decisions", "decisions": {"my-plugin": "trust", "shady": "deny"}}| Field | Type | Required | Description |
|---|---|---|---|
decisions |
object | yes | plugin name → "trust" | "deny" |
Run a plugin-declared slash command by name.
{"type": "plugin_command", "name": "my-command", "args": "--flag value"}List slash commands declared by enabled plugins.
{"type": "list_plugin_commands"}Re-discover available subagents (builtin + user + project) and emit an agents
event.
{"type": "list_agents"}List discoverable skills (project then user scope). Emits a skills event with
each skill's name, description, and location.
{"type": "list_skills"}Invoke a skill by name: the core reads the matching SKILL.md, builds a
prompt, and runs a normal assistant turn.
{"type": "apply_skill", "name": "repository-documentation-factory", "task": "document the CLI", "model": "glm-5.2"}On-demand model-cache refresh. Forces a LIVE /models discovery for every
logged-in provider, bypassing the 8-hour disk-cache TTL
(~/.config/catalyst-code/models-cache.json) and rewriting the cache, then
re-aggregates and re-emits models + provider_presets. Runs off the command
loop; emits info up front and a terminal models_refreshed event. A dead
endpoint falls back to the stale cache / curated snapshot, so the list never
shrinks. Triggered by TUI /refresh and the web model-picker refresh button.
{"type": "refresh_models"}| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Skill name (resolved project > user scope) |
task |
string | no | Optional follow-up appended to the skill instructions |
model |
string | yes | Model to use |
reasoning_effort |
string | no | Reasoning effort level |
Start goal mode: plan then deploy subagents under the given configuration.
{
"type": "start_goal",
"goal": "Refactor the auth module",
"concurrency": 4,
"max_tasks": 12,
"allowed_models": ["glm-5.2", "claude-sonnet-4"],
"auto_deploy": true,
"planner_model": "glm-5.2",
"worker_model": "claude-sonnet-4",
"model_concurrency": {"glm-5.2": 2, "claude-sonnet-4": 4}
}Fields: goal (required), concurrency, max_tasks, allowed_models,
allowed_providers, auto_deploy, planner_model, worker_model,
reviewer_model, model_concurrency, model, reasoning_effort.
Cancel the active goal (interrupts planning/deploy runs).
{"type": "cancel_goal"}Re-emit the current goal_state (+ goal_plan if present).
{"type": "goal_status"}Approve a plan that is waiting at plan_ready (when auto_deploy was false).
{"type": "approve_goal_plan"}Re-enter planning with user feedback (from plan_ready / failed).
{"type": "revise_goal", "feedback": "Add validation checks", "model": "glm-5.2"}Respond to a pending approval request.
{"type": "approve", "request_id": "req-123", "decision": "yes", "pattern": "//src/**"}| Field | Type | Required | Description |
|---|---|---|---|
request_id |
string | yes | ID from the approval_request event |
decision |
string | yes | "yes", "no", "always", "allow_session", "allow_pattern" |
pattern |
string | no | Path/command glob for allow_pattern; defaults to the tool's path arg |
Reply to a pending ask_request (the ask tool).
{"type": "ask_reply", "request_id": "ask-456", "answers": {"q1": "Use async-std", "q2": "Yes"}}| Field | Type | Required | Description |
|---|---|---|---|
request_id |
string | yes | ID from the ask_request event |
answers |
object/null | yes | Map of question id → answer string, or null to skip questions |
Reply to a pending sudo_request (a bash command that invokes sudo).
{"type": "sudo_reply", "request_id": "sudo-789", "approved": true, "password": "hunter2"}| Field | Type | Required | Description |
|---|---|---|---|
request_id |
string | yes | ID from the sudo_request event |
approved |
boolean | no | true to run the command with sudo -S |
password |
string | no | Password fed to sudo -S stdin (used once, not stored) |
Reply to a subagent's contact_supervisor need_decision ask.
{"type": "intercom_reply", "request_id": "inter-111", "reply": "Use the existing adapter"}User-initiated bash from the composer (!cmd / !!cmd), PI-compatible. Runs
in the workspace with the same sandbox/denylist as the agent bash tool.
{"type": "user_bash", "command": "git status", "exclude_from_context": false}| Field | Type | Required | Description |
|---|---|---|---|
command |
string | yes | Shell command to run |
exclude_from_context |
boolean | no | true for !!cmd — shows output but does not add to LLM context |
Emits bash_execution events for the UI.
All events are JSON objects with a "type" string field plus additional data.
Emitted once after init completes. Carries the full initial state.
{
"type": "ready",
"models": [ /* ModelInfo[] */ ],
"authed": false,
"workspace": "/home/user/project",
"approval": "destructive",
"base_url": "https://api.code.umans.ai/v1",
"provider": "umans",
"provider_kind": "openai",
"has_key": false,
"has_vision_config": false,
"idle_timeout_secs": 120,
"bash_timeout_secs": 30,
"auto_compact": true,
"context_compact_at": 0.9,
"context_digest_at": 0.7,
"sandbox": "none",
"resumed_messages": 0,
"plugins": [],
"plugins_skipped": []
}plugins_skipped lists project-scoped plugins that are not loaded and have
no recorded trust decision yet (a deliberately denied plugin is not listed —
it would nag on every startup). When that list is non-empty the core also emits
a plugin_trust_prompt right after ready so the client can surface the trust
modal once.
Emitted after reset or clear. No extra fields.
{"type": "reset"}A text delta from the model's response stream. Multiple delta events are
emitted per turn, one per chunk.
{"type": "delta", "text": "async fn read_file"}A thinking/reasoning text delta from the model. Interleaved with delta events.
{"type": "thinking", "text": "I need to understand the file structure first..."}A tool call requested by the model.
{
"type": "tool_call",
"id": "call_abc123",
"name": "bash",
"args": "{\"command\": \"ls -la\"}"
}Protocol v2 adds status with one of success, denied, cancelled,
timed_out, failed, stale, or partially_completed. The legacy ok
boolean remains for compatibility. If an older executor supplies only
ok/output, the event sink normalizes it before emission.
The result of a tool execution.
{
"type": "tool_result",
"id": "call_abc123",
"ok": true,
"name": "bash",
"output": "total 24\ndrwxrwxr-x ..."
}Fields: id, ok (boolean), name, output (truncated to 32 KiB for bash,
bounded for other tools). On error: ok: false with error message in output.
Emitted when a turn is aborted (via abort command, user denial, or internal
error). Usually followed by done.
{"type": "aborted"}Emitted when a turn finishes (successfully, aborted, or errored).
{"type": "done"}Emitted when a steer command is received, before the turn is redirected.
{"type": "steer", "prompt": "Actually, use async instead"}Full model list update. Emitted after login, logout, set_provider, and
refresh_models.
{"type": "models", "models": [ /* ModelInfo[] */ ]}Terminal event for refresh_models (after the models re-emit). Carries the
total model count and per-provider model ids so clients can clear their
refresh spinner and report what changed.
{"type": "models_refreshed", "count": 12, "providers": {"umans": ["glm-5.2"]}}List of available provider presets (built-in + plugin OAuth).
{
"type": "provider_presets",
"presets": [
{"id": "umans", "label": "Umans (GLM-5.2)", "kind": "openai", "has_key": true, "can_oauth": false},
{"id": "opencode-go", "label": "OpenCode Go", "kind": "openai", "has_key": false, "can_oauth": false}
]
}Emitted when the active provider switches (login, logout, set_provider).
{"type": "provider_changed", "provider": "umans", "kind": "openai", "base_url": "https://api.code.umans.ai/v1", "has_key": true}Authentication status change. Emitted on login, logout, and set_key.
{"type": "authed", "ok": true, "provider": "umans"}Search key change confirmation.
{"type": "search_key_set", "provider": "exa", "has_key": true}Emitted during plugin OAuth login to ask the user to authorize and enter a code.
{
"type": "oauth_prompt",
"url": "https://provider.com/authorize?code=ABC",
"code": "ABC123",
"message": "Open this URL in your browser, then paste the code",
"needs_code": true
}needs_code is true when the user must paste a code/callback via
oauth_code (manual / headless). It is false for automatic flows (device
poll or loopback redirect) that finish after browser/device approval without
a paste. Clients should treat a missing needs_code as true for
back-compat with older cores.
A tool call is waiting for human approval.
{
"type": "approval_request",
"request_id": "req-123",
"tool": "write_file",
"args": "{\"path\": \"src/main.rs\", \"content\": \"...\"}",
"diff": "--- a/src/main.rs\n+++ b/src/main.rs\n@@ -1,3 +1,5 @@..."
}diff is present only for write_file, edit, and patch (generated before
the tool executes, so the user sees what will change).
Emitted after set_approval.
{"type": "approval_changed", "mode": "always"}Emitted after set_config.
{"type": "config_changed", "key": "bash_timeout_secs", "value": 60}The ask tool is waiting for user input.
{
"type": "ask_request",
"request_id": "ask-456",
"questions": [
{"id": "q1", "type": "text", "label": "Which library should I use?"},
{"id": "q2", "type": "select", "label": "Async runtime?", "options": ["tokio", "async-std", "smol"]}
]
}A bash command that invokes sudo is waiting for a password.
{
"type": "sudo_request",
"request_id": "sudo-789",
"command": "apt install -y postgresql"
}A subagent has sent a contact_supervisor message to the orchestrator.
{
"type": "intercom_message",
"request_id": "inter-111",
"from_agent": "worker-1",
"message": "Need decision: use adapter A or B?"
}Emitted after list_sessions. Contains available session files.
{
"type": "sessions",
"sessions": [
{"path": "sessions/2026-07-15.jsonl", "title": "Auth refactor", "pinned": true, "mtime": "..."}
]
}Emitted after undo. Contains the truncated conversation history.
{"type": "history", "messages": [ /* ... */ ], "tokens_in": 1234}Emitted after stats command.
{
"type": "stats",
"tokens_in": 15000,
"tokens_out": 3200,
"total_in": 15000,
"total_out": 3200,
"auto_compactions": 2,
"human_corrections": 0,
"subagent_calls": 5,
"tool_calls": 47,
"bash_calls": 12,
"edit_calls": 8
}Emitted after context command. Shows token usage breakdown.
{
"type": "context_breakdown",
"total": 15000,
"context_window": 128000,
"pct": 11.7,
"roles": {"system": 5000, "user": 6000, "assistant": 4000},
"top_consumers": [
{"role": "user", "content_preview": "Please review this large file...", "tokens": 3000}
],
"model_id": "glm-5.2"
}Emitted after usage command. Provider-specific usage data.
{
"type": "usage",
"provider": "umans",
"provider_kind": "openai",
"model": "glm-5.2",
"usage": { "concurrent": 1, "requests_this_hour": 25, "requests_limit": 100 }
}Confirmation of a memory save or forget.
{"type": "memory_saved", "id": "my-memory-slug", "message": "memory saved"}Emitted after list_memory.
{
"type": "memory_list",
"entries": [ /* memory objects */ ],
"count": 5
}Emitted after list_checkpoints.
{
"type": "checkpoints",
"checkpoints": [
{"id": "ck-abc", "label": "before-auth-refactor", "kind": "git-stash", "created_at": "..."}
]
}Emitted when compaction begins.
{"type": "compacting", "before_tokens": 115000, "trigger": "auto"}Emitted when compaction completes.
{
"type": "compacted",
"before_tokens": 115000,
"after_tokens": 45000,
"before_messages": 120,
"after_messages": 45
}Emitted during goal mode to report phase changes.
{
"type": "goal_state",
"goal": "Refactor the auth module",
"phase": "planning",
"progress": 0.3,
"total_tasks": 12,
"completed": 4,
"failed": 0,
"active": 3
}Emitted when a goal plan is ready.
{
"type": "goal_plan",
"prompts": [ /* step-by-step plan */ ],
"auto_deploy": false
}Emitted after list_plugins.
{
"type": "plugin_list",
"plugins": [
{"name": "catcode-chatgpt-provider", "enabled": true, "version": "1.0.0"}
]
}Emitted automatically at startup when project-scoped plugins are gated off
with no recorded trust decision (so it appears once — decisions are
persisted), and again on the plugin_trust_prompt command (/plugin-trust).
Each entry carries manifest metadata plus the recorded decision ("" =
undecided, "trust", or "deny").
{
"type": "plugin_trust_prompt",
"plugins": [
{
"name": "shady",
"version": "1.0.0",
"description": "Hook-heavy linter",
"path": "/ws/.catalyst-code/plugins/shady",
"decision": ""
}
]
}Emitted after plugin_trust_decisions is applied: the decided names, how many
plugins are now loaded, and refreshed plugins_list / plugin_commands
events follow so newly-trusted plugins are live immediately.
{
"type": "plugin_trust_applied",
"trusted": ["my-plugin"],
"denied": ["shady"],
"loaded": 3
}Emitted after list_agents.
{
"type": "agents",
"agents": [
{"name": "scout", "label": "Scout", "description": "Quickly explore an unfamiliar codebase..."},
{"name": "planner", "label": "Planner", "description": "Decompose a goal into sub-steps..."}
]
}Emitted after list_skills.
{
"type": "skills",
"skills": [
{"name": "repository-documentation-factory", "description": "Create/proofread/repair docs", "location": "user"}
]
}Emitted after get_vision_config or set_vision_config.
{
"type": "vision_config",
"enabled": true,
"vision_model": "glm-5.2-vision",
"vision_models": ["glm-5.2-vision", "gpt-4o"]
}A non-fatal error occurred.
{"type": "error", "message": "unknown provider preset 'foo'; available: umans, opencode-go, openrouter"}An informational message.
{"type": "info", "message": "logged into Umans."}Emitted after user_bash command.
{
"type": "bash_execution",
"command": "git status",
"exit_code": 0,
"stdout": "On branch master\nnothing to commit..."
}Frontend Core
| |
|-------- init ---------------->|
| |
|<------- ready ----------------| (models, config, provider state)
| |
|---- set_key / login --------->| (optional)
|<-- authed / provider_changed -|
| |
|-------- send ----------------->|
|<--- thinking (stream) --------|
|<--- delta (stream) -----------|
|<--- tool_call ----------------|
|<--- approval_request ---------|
|--- approve (yes)------------->|
|<--- tool_result --------------|
|<--- delta (stream) -----------|
|<--- done ---------------------|
| |
|-------- stats --------------->|
|<------- stats ----------------|
Errors are reported as {"type": "error", "message": "..."} events. These are
non-fatal — the core continues running after emitting an error. Common
error scenarios:
| Scenario | Error Message |
|---|---|
| Unknown login preset | "unknown provider preset '{name}'; available: ..." |
| Missing API key on login | "no API key provided for '{preset}' — paste a key via /login..." |
| Logout of non-logged-in provider | "not logged into '{provider}'" |
| Unknown set_provider name | "unknown provider '{name}'; not switching" |
| Unknown model in send | "unknown model: {model}" |
| OAuth with no plugin | "'{preset}' has no plugin OAuth login..." |
| OAuth code without pending login | "No pending OAuth login..." |
| Invalid set_search_key provider | "set_search_key: unknown provider '{provider}'..." |
Fatal errors (e.g., panic recovery) cause the core to emit an error event,
then done, and continue with the next input.
Source: Error handling throughout main.rs (/core/src/main.rs) event dispatch.