How kiss/agents/vscode talks to kiss/server, kiss/agents/sorcar, and kiss/core

A tour of the four layers of the KISS Sorcar system, the wire that connects them, and the rules that keep them apart.

1. The four layers in one sentence each

LayerDirectoryWhat it is
vscode src/kiss/agents/vscode/ The face: a TypeScript VS Code extension (host code in src/*.ts, chat UI in media/*.js) plus a bundled snapshot of the whole Python project (kiss_project/) that ships inside the .vsix. It contains no agent logic of its own.
server src/kiss/server/ The switchboard: a single always-on daemon, kiss-web, that owns the transports (Unix socket + WebSocket), dispatches every UI command, runs tasks on background threads, and streams events back to every connected client.
sorcar src/kiss/agents/sorcar/ The workers: the agent classes that actually do the coding work — tools, git worktrees, Docker, MCP, skills — and the SQLite persistence layer (~/.kiss/sorcar.db).
core src/kiss/core/ The foundation: the generic LLM agent loop (KISSAgent), model back-ends and catalog, the abstract Printer event interface, configuration (~/.kiss/config.json, kiss_home()), stop signals, and speech synthesis.
VS Code process — kiss/agents/vscode (TypeScript) chat webview media/main.js + api.js (also served to browsers) extension host SorcarSidebarView.ts SorcarApi.ts → AgentClient.ts DependencyInstaller.ts installs uv + venv, launches kiss-web daemon newline-delimited JSON over Unix socket ~/.kiss/sorcar.sock (AgentCommand ↓ · ToWebviewMessage ↑) spawns / restarts kiss-web daemon — kiss/server (Python, entry point web_server:main) web_server.py RemoteAccessServer: UDS + HTTPS/WSS transports, auth, cloudflared tunnel sorcar.py: API catalog, ServerApi.dispatch() server.py VSCodeServer + commands.py _cmd_run… + task_runner.py threads + merge_flow / diff_merge + autocomplete / voice_wake agent_state.py registry json_printer.py JsonPrinter(Printer) broadcast() → fan out to every tab + persist events bridge: agents ↔ server WorktreeSorcarAgent("Sorcar VS Code").run(prompt, model, printer=JsonPrinter, …) events only, via Printer agents — kiss/agents/sorcar (Python) WorktreeSorcarAgent → ChatSorcarAgent → SorcarAgent → RelentlessAgent useful_tools · web_use_tool · skills · mcp_servers · docker_manager git_worktree.py (GitWorktreeOps) · commit_message.py persistence.py — SQLite ~/.kiss/sorcar.db (tasks, chats, events) foundation — kiss/core KISSAgent (LLM loop) · Base models/ (catalog + back-ends) printer.Printer · stop_signal config · vscode_config speech_synthesis · _version
Fig. 1 — Who talks to whom. Solid arrows are direct calls or wire messages; the dashed red arrow is the only way agents reach “up” into the server: by emitting events through the Printer they were handed. The server layer also imports kiss/core directly (config, model catalog, stop signals).

2. The one rule everything obeys: strict downward layering

The dependency direction is enforced by real tests (src/kiss/tests/test_layering_invariants.py walks the full AST of every file, and test_core_sorcar_layer_separation.py re-imports core in a fresh interpreter with kiss.agents deleted):

The escape hatch: a running agent still needs to tell the UI what it is doing, ask the user questions, and register sub-agents. It does all of this through the Printer object the server hands it. kiss/server/json_printer.py’s JsonPrinter subclasses the abstract kiss.core.printer.Printer and adds duck-typed bridge methods (agent_task_allocated, agent_task_finished, drain_pending_user_messages, live_worktree_branches). The agent calls methods on “some Printer”; it never knows a server exists. This is documented explicitly in kiss/server/agent_state.py.

3. vscodeserver: a socket and a shared command catalog

3.1 The transport

src/AgentClient.ts opens a Unix-domain socket to ~/.kiss/sorcar.sock (overridable via $KISS_SORCAR_SOCK) and speaks newline-delimited JSON: one command object per line down, one event object per line up. It is deliberately defensive:

3.2 The protocol

Both ends agree on a single catalog. On the Python side, kiss/server/sorcar.py is “the single source of truth for the wire API”: it defines the API catalog, validate_command(), and ServerApi.dispatch(), which routes each JSON object on its "type" field to a _cmd_* handler. On the TypeScript side, src/types.ts mirrors it as two discriminated unions, and src/SorcarApi.ts is a thin facade whose methods map 1:1 onto catalog names.

DirectionType nameRepresentative messages
extension → daemon AgentCommand run, stop, userAnswer, appendUserMessage, selectModel/getModels, getHistory, resumeSession, complete (autocomplete), getFiles, worktreeAction (merge/discard), generateCommitMessage, getConfig/saveConfig, openTab/closeTab/ready, setWorkDir, serverReset
daemon → extension ToWebviewMessage streaming: thinking_delta, text_delta, tool_call, tool_result; lifecycle: status, result, task_done/task_error/task_stopped, stop_ack; interaction: askUser, talk (voice), followup_suggestion; worktree: worktree_created/worktree_done/autocommit_done; state sync: models, history, tabs_state, configData, commitMessage, remote_url

Every event carries an optional tabId. The daemon’s JsonPrinter is task-centric: events are recorded and persisted under the task’s database id, then fanned out to every subscribed tab (each copy stamped with that tab’s id) — which is how several VS Code windows, the sidebar, and a phone browser can all watch the same task live.

3.3 Who dials whom inside the extension

3.4 The extension is also the daemon’s babysitter

The Python backend ships inside the extension: copy-kiss.sh snapshots the whole repository into vscode/kiss_project/ before packaging the .vsix (and syncs the extension version from kiss/core/_version.py). On activation, DependencyInstaller.ts:

  1. Installs uv if missing and builds the venv inside the bundled kiss_project/.
  2. Fingerprints the installed code; when the fingerprint changes (extension update), it restarts the daemon — but only after probing sorcar.sock for active tasks so an update never kills a running agent.
  3. Launches .venv/bin/kiss-web (the pyproject.toml entry point kiss.server.web_server:main) as a macOS LaunchAgent, a systemd user service, or a detached direct spawn — guarded by a cross-process restart lock so multiple VS Code windows do not race.
  4. daemonHealth.js probes the socket; extension.ts watches ~/.kiss/.extension-updated plus the socket, and reloads the window only when the new bundle is stable and the daemon is back up.

4. serversorcar: the daemon hires the agent

The extension never touches an agent directly — the server does it on its behalf. When a run command arrives, commands.py:_cmd_run hands it to the task-runner mixin, which starts a background thread. The heart of the interaction is one call in kiss/server/task_runner.py:

state.agent = WorktreeSorcarAgent("Sorcar VS Code")   # one agent per tab, reused across runs
...
agent_returned = agent.run(
    prompt_template=task_prompt,          # one <task> segment of the submitted prompt
    model_name=model,                     # from the tab's model picker
    work_dir=work_dir,
    printer=self.printer,                 # the JsonPrinter — the agent's only voice
    ask_user_question_callback=self._ask_user_question,   # becomes the askUser round-trip
    is_parallel=state.use_parallel,
    use_worktree=use_worktree,
    auto_commit=state.auto_commit_mode,
    max_budget=..., web_tools=..., model_config=...,      # from ~/.kiss/config.json
    tools=client_tools,                   # optional user tools file, imported by the daemon
)

The agent the server instantiates sits atop a four-deep inheritance chain that crosses into core:

kiss.core.base.Base
  └─ kiss.agents.sorcar.relentless_agent.RelentlessAgent   (drives kiss.core.kiss_agent.KISSAgent in rounds)
       └─ kiss.agents.sorcar.sorcar_agent.SorcarAgent      (tools, skills, web browsing, sub-agents, MCP)
            └─ chat_sorcar_agent.ChatSorcarAgent           (chat sessions, persistence, resume)
                 └─ worktree_sorcar_agent.WorktreeSorcarAgent   (git-worktree isolation)  ← the server uses this one

Beyond launching agents, the server leans on the sorcar package for four more services:

Sorcar moduleUsed by (in kiss/server)For
persistence.py server.py, commands.py, task_runner.py, json_printer.py, autocomplete.py, merge_flow.py Everything durable: task history and search (getHistory), chat-event replay (resumeSession, reconnecting tabs), frequent tasks, model-usage ranking, favorites, task results — all in SQLite at ~/.kiss/sorcar.db. JsonPrinter persists every display event via _queue_chat_event as it broadcasts it.
git_worktree.py task_runner.py, merge_flow.py, diff_merge.py The worktreeAction flow: each task can run in an isolated .kiss-worktrees/kiss_wt-* checkout; when the user clicks Merge/Discard in the webview, the server drives GitWorktreeOps and reports worktree_progress/worktree_done events back.
commit_message.py helpers.py Generating the SCM commit message for the extension’s sparkle button from the staged diff.
mcp_servers.py, useful_tools.py, sorcar_agent.py internals web_server.py, merge_flow.py MCP manager status, stale-worktree fallback handling, commit-subject formatting.

Live run bookkeeping lives in agent_state.py: a registry agent_states: dict[task_id → AgentState] guarded by one STATE_LOCK. Each AgentState holds the agent instance, its thread, a cooperative stop_event, and worktree/merge flags. A stop command sets the event; if the agent does not yield, the runner force-stops the thread by injecting KeyboardInterrupt with PyThreadState_SetAsyncExc — but only while an ownership guard proves the thread still belongs to that task.

5. Everyone ↔ core: the shared foundation

Core moduleWho uses itRole in the vscode story
printer.Printer server (JsonPrinter), sorcar (every agent) The pivotal interface. The core agent loop emits tokens, thinking, tool calls, and results into an abstract Printer; JsonPrinter turns those calls into the JSON events the webview renders. This one abstraction is why the UI never imports agent code and the agents never import UI code.
kiss_agent.KISSAgent sorcar (RelentlessAgent), server (voice_wake) The actual LLM ⇄ tool loop: streaming, retries, context-window management, budget accounting. Every token the user watches in the sidebar originates here.
models/model_info.py server, sorcar The model catalog (MODEL_INFO.json): get_available_models() (filtered by which API keys are set) feeds the webview’s model picker; per-model cost powers the live usage_info cost display.
vscode_config.py server (getConfig/saveConfig, task launch) Despite living in core, this is the extension’s settings back-end: it persists ~/.kiss/config.json, injects API keys into shell RC files, and builds the model_config/budget passed into each agent.run(). The webview settings panel round-trips through it.
config.py (kiss_home()) all Python layers Resolves ~/.kiss/ — the rendezvous directory both sides of the socket agree on: sorcar.sock, sorcar.db, config.json, TLS certs, logs, and the .extension-updated reload marker watched by extension.ts.
stop_signal.py server, sorcar, core loop Thread-local cooperative cancellation: the Stop button in the webview becomes a set event the agent loop checks between steps.
speech_synthesis.py server (voice_wake.py, talk_player.py) Backs the talk events (voice replies played in the webview) and the wake-word voice pipeline.
_version.py server, packaging Single version source: stamped into persisted task rows and copied into the extension’s package.json by copy-kiss.sh, which is how UpdateChecker.js can compare installed vs. released versions.

6. Life of a task, end to end

  1. You type a prompt in the sidebar. media/main.js posts {type:'submit', prompt, model, tabId, useWorktree, …} to the extension host.
  2. SorcarSidebarView.ts forwards it through SorcarApi.run(); AgentClient.ts writes one JSON line to ~/.kiss/sorcar.sock.
  3. The kiss-web daemon (web_server.py) reads the line; ServerApi.dispatch() in sorcar.py validates it against the API catalog and calls VSCodeServer._cmd_run.
  4. task_runner.py spins up a background thread, registers an AgentState, splits the prompt on <task> tags, loads budget/model settings from core.vscode_config, and calls WorktreeSorcarAgent.run(…, printer=JsonPrinter).
  5. The agent (sorcar layer) creates a git worktree, assembles its tools, and drives the KISSAgent loop in kiss/core, which streams model output.
  6. Every token and tool call flows into JsonPrinter, which stamps a timestamp, persists the event to sorcar.db (via persistence._queue_chat_event), and fans it out — down the Unix socket to every VS Code window and over WSS to any remote browser — each copy tagged with the receiving tab’s tabId.
  7. If the agent calls its ask-user tool, the callback wired by the server emits an askUser event; your typed reply comes back as a userAnswer command and is handed to the waiting agent thread.
  8. On completion the runner parses the result YAML (core.printer.parse_result_yaml), emits result + task_done, runs auto-commit / offers Merge / Discard (emitting worktree_done), and persists the task row with tokens, cost, and steps.
  9. Clicking Merge sends worktreeAction; merge_flow.py drives GitWorktreeOps from the sorcar layer and streams progress events until your branch has the changes.

7. Takeaways