kiss/agents/vscode talks to kiss/server, kiss/agents/sorcar, and kiss/core| Layer | Directory | What 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. |
Printer they were handed. The server layer also imports kiss/core directly (config, model catalog, stop signals).The dependency direction is enforced by real tests
(src/kiss/tests/core/test_layering_invariants.py walks the full AST of every core file
and src/kiss/tests/agents/sorcar/test_layering_invariants.py does the same for the sorcar layer, while
src/kiss/tests/core/test_core_sorcar_layer_separation.py re-imports core in a fresh
interpreter with kiss.agents deleted):
kiss/core/ — it is a standalone foundation.kiss/server.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.
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:
run queued during an outage must not be replayed into a different daemon that answers later — that would start an agent nobody asked for. Dropped commands are announced (commandDropped) so the UI can undo its optimistic “running” state.StringDecoder and a 32 MB line buffer keep multi-byte UTF-8 and huge events intact.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.
| Direction | Type name | Representative 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.
extension.ts activates SorcarSidebarView (the webview provider), registers palette commands (runSelection, stopTask, focus toggles), and hijacks
git.generateCommitMessage so the SCM sparkle button asks the daemon for a commit message
(answered as a commitMessage event and written into the SCM input box).SorcarSidebarView.ts owns exactly one AgentClient + SorcarApi pair. Webview postMessage traffic (FromWebviewMessage) is validated and forwarded as
AgentCommands; daemon events flow back with webview.postMessage().media/main.js, media/api.js…) are served by the daemon to remote browsers over HTTPS/WSS, so the wire protocol is identical for both front ends — remote clients just add an auth handshake (password from ~/.kiss/config.json, optional Cloudflare tunnel).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:
uv if missing and builds the venv inside the bundled kiss_project/.sorcar.sock for active tasks so an update never kills a running agent..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.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.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 module | Used 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.
| Core module | Who uses it | Role 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. |
media/main.js posts
{type:'submit', prompt, model, tabId, useWorktree, …} to the extension host.SorcarSidebarView.ts forwards it through SorcarApi.run();
AgentClient.ts writes one JSON line to ~/.kiss/sorcar.sock.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.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).KISSAgent loop in kiss/core, which streams model output.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.askUser
event; your typed reply comes back as a userAnswer command and is handed to the waiting
agent thread.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.worktreeAction; merge_flow.py drives
GitWorktreeOps from the sorcar layer and streams progress events until your branch has
the changes.~/.kiss/sorcar.sock, with
kiss/server/sorcar.py as the authoritative command catalog and
types.ts as its TypeScript mirror.kiss-web installed,
healthy, and up to date.Printer abstraction is the hinge. Agents depend only on core;
the server injects a core-typed JsonPrinter to pull agent output up into the UI world —
a dependency inversion enforced by AST-walking layering tests.