Skip to main content
KISS Sorcar

Version License Python Website arXiv

"Everything should be made as simple as possible, but not simpler." — Albert Einstein

KISS Sorcar

Open-source general-purpose AI agent for long-horizon tasks and AI discovery

KISS Sorcar is a free, simple, local-first, bring-your-own-key AI agent framework. It runs as a browser/mobile web app served by a local daemon, and offers a Python client API for scripting tasks. Your prompts and code go directly to the model provider or local endpoint you configure, never through our servers. Multi-model workflows take a paragraph of prompt, not a pipeline: complex AI systems and techniques can be replaced with a paragraph of prompt in KISS Sorcar.

curl -fsSL https://raw.githubusercontent.com/ksenxx/kiss_ai/main/scripts/install.sh | bash

This README is the quick start. The detailed references are:


Table of Contents
KISS Sorcar UI

Terminal-Bench 2.0: KISS Sorcar vs Pi, Codex CLI, and Claude Code

The HarnessTax study (Pan, Yang, Arabzadeh, Chiang, Stoica, Zaharia; UC Berkeley and Arena Intelligence) holds the model fixed, swaps the harness between Claude Code, Codex CLI, and Pi, and finds that the harness moves cost far more than it moves what gets solved. We added KISS Sorcar to their Terminal-Bench 2.0 table: the same 30 sampled tasks, the same seven models, three attempts per task, graded by the official Terminal-Bench 2.0 verifier, with the system prompt cut to 727 words of coding rules (papers/kisssorcar/evidence/tb2_prompt.txt). Averaged over the seven models, KISS Sorcar solved 75.6% of attempts; Pi 70.0%, Codex CLI 65.7%, Claude Code 65.1%, with the best point estimate on every model.

Model KISS Sorcar solved $/att. Pi solved $/att. Codex CLI solved $/att. Claude Code solved $/att.
Claude Fable 5 80.0 1.50 71.1 1.08 72.2 0.98 75.6 1.55
Claude Opus 4.8 74.4 1.21 72.2 0.76 72.2 0.85 68.9 0.90
Claude Sonnet 4.6 76.7 2.23 65.6 0.61 63.3 0.55 62.2 0.67
Claude Haiku 4.5 51.1 0.39 47.8 0.25 31.1 0.21 41.1 0.26
GPT-5.6 Sol 85.6 0.69 83.3 0.42 78.9 0.76 71.1 1.35
GPT-5.6 Luna 78.9 0.05 76.7 0.04 72.2 0.06 70.0 0.10
Kimi K3 82.2 1.14 73.3 0.38 70.0 0.45 66.7 0.52
Mean of 7 models 75.6 1.03 70.0 0.51 65.7 0.55 65.1 0.77

Terminal-Bench 2.0, the study's 30-task sample, three attempts per task: percentage of attempts solved and cost per attempt in USD. KISS Sorcar: 630 attempts run on 25 September 2026, no turn cap, $50 budget per attempt, providers' default request parameters. The other three columns are the study's published numbers (100-turn cap, high reasoning effort, priced on a 1 September list). Bold marks the best point estimate per row.

Percentage of Terminal-Bench 2.0 attempts solved per model under KISS Sorcar, Pi, Codex CLI, and Claude Code

Thirty tasks is a small sample (the pooled 95% interval, 63.8 to 85.9, contains all three published means), so we reran Pi ourselves, paired, on Claude Fable 5 with no turn cap and the same price table. On the study's 30 tasks KISS Sorcar solved 80.0% of attempts to Pi's 68.9%, a gap of +11.1 points (95% interval +3.3 to +20.0) for five cents more per attempt; on the 57 tasks the study did not sample, 82.5% to 71.9%, a gap of +10.5 points (+2.9 to +18.7) for 63 cents more. Only Pi was rerun, on one model, and the benchmark exercises the loop, six tools, and the coding rules with the discovery procedures, memory, reviewer, and IDE features switched off. Full write-up: The Harness Tax, Audited; method and intervals: the paper, Section 5; per-attempt records: papers/kisssorcar/evidence/tb2_trials.json; the runners live in the separate benchmarkings package (a sibling checkout, or KISS_BENCHMARKINGS_DIR), which the paper scripts under papers/kisssorcar/ locate.

What is in the Name

KISS Agent Framework is a deliberately small agent runtime organized around the KISS principle ("Keep it Simple, Stupid"). The name "Sorcar" pays homage to P. C. Sorcar, the legendary Bengali magician, evoking the idea of an agent that performs feats that appear magical yet are grounded in disciplined engineering. Note: Sorcar also means government in Bengali.

Installation

Install

curl -fsSL https://raw.githubusercontent.com/ksenxx/kiss_ai/main/scripts/install.sh | bash

The one-liner clones or updates ~/.kiss/kiss_ai and runs its install.sh from the directory you ran the command in, so the daemon serves that directory (unless that directory is inside ~/.kiss or the brand's home, as when the daemon updates itself from its own clone: then it serves your home directory). From a checkout, the same thing is:

git clone https://github.com/ksenxx/kiss_ai.git && cd kiss_ai && ./install.sh

The installer targets macOS and Linux on x86_64, aarch64, and arm64. install.sh copies the runtime into ~/.kiss/kiss: the Python environment (uv), the library byte-compiled to .pyc files, the files a runtime reads as Python scripts (the bundled SEAs under src/kiss/agents/seas/, the channel and cron SEAs, _version.py, and the modules the database and memory syncs ship to a remote machine as source: scripts/sync_db.py with chat_summary.py and history_columns.py, and scripts/merge_memory_pages.py) kept as source, and the web app frontend (src/kiss/webapp/media/). It then installs the sorcar CLI (with the rsorcar and sorcar-docker launchers; rsorcar user@host deploys this checkout to a remote Linux machine and merges the two machines' task databases both ways, which need not be on the same release: a history column one side lacks is added with the other's declaration, a column only the target has keeps its default for incoming rows, and the is_cron flag is worked out for rows from before it existed. A deploy changes only the remote machine, and a task running on either side does not stop it: the ordinary delta syncs run beside both live web apps and hold back a task still in flight until it has finished, so a later sync brings it over complete; a deleted task is recorded in deleted_tasks, and a deleted chat records its tasks there and its own id in deleted_chats, so the sync removes them on the other machine instead of bringing them back (a task still running there is left alone). A task counts as running while its newest event is within SORCAR_LIVE_TASK_WINDOW seconds (default 300; 0 turns the check off), and while one runs on the remote the final kiss-web restart is skipped, so the remote keeps serving the previous release until the next deploy or systemctl --user restart kiss-web; the full-copy fallback (a first deploy, or a merge that fails) stops the remote web app for the swap, so it refuses while a task runs there and leaves both databases as they were. SORCAR_FORCE_RESTART=1 restarts and swaps anyway, killing the task) in ~/.local/bin, Playwright's Chromium and cloudflared, refreshes ~/.kiss/MODEL_INFO.json, registers the kiss-web daemon as your user service (launchd on macOS, a systemd user unit on Linux) serving the directory it was run from, sets a random remote-access password on the first install (printed in a box of * at the end of every run, after the app has opened, and stored in ~/.kiss/config.json; when the run's terminal has gone away by then, as it does when install.sh runs from the web app's own Terminal tab and restarts the daemon hosting it, the install carries on with its output in the log, which says the password is in Settings rather than printing it), and opens the web app at http://127.0.0.1:8787 as a standalone app: its own window with no address bar or tabs (Chromium's app mode, run by kiss-app from the app's venv in Google Chrome, Chromium, Edge or Brave if installed, else in Playwright's Chromium, under its own profile in ~/.kiss/webapp-profile), plus a launcher in your applications menu (~/.local/share/applications/kiss-sorcar.desktop on Linux, ~/Applications/KISS Sorcar.app on macOS) named and iconed after the brand; the running window carries that icon in the dock or taskbar as well (the Linux entry names the window's X11 class and Wayland app id; the macOS bundle is a clone of the browser's, its binary and frameworks copied rather than symlinked, since Chromium's sandboxed helper processes read only their own bundle's real path, re-signed under the brand's identity and refreshed whenever the browser updates; clicking that icon while the app is open brings its window to the front rather than opening a browser window; kiss-app waits up to --wait seconds (default 120) for the daemon's URL file (~/.kiss/remote-url.json, written as the daemon starts) but not for the daemon itself: when the daemon is not answering yet, as after a reboot on macOS, where the app is relaunched at login about a minute before the daemon's LaunchAgent, the window opens at once on a packaged waiting page (src/kiss/webapp/media/appStart.html, a file:// page showing the app's name) that asks the daemon every half second and replaces itself with the app at the first answer, rather than staying closed or opening onto Chromium's error page (on macOS the waiting page first moves into the app's named popup window, which then restores its saved bounds and navigates to the app in place), and on macOS the daemon's watchdog quits a browser it finds running under the app's profile with no window for several sweeps in a row, which is what the app leaves behind when its window was closed while the daemon was down, so the next click starts the app afresh rather than opening an empty browser window; on Linux the bundled Chromium runs without its sandbox, since it has no setuid helper for hosts that restrict unprivileged user namespaces; a web link in the chat opens in your default browser rather than inside the app window, which has no address bar to get back from; kiss-app asks the daemon directly, ignoring any http_proxy in your environment, which could not reach this machine's loopback anyway; and kiss-app --print-command prints the browser command it would run, quoted for your platform's shell, without waiting for the daemon). Once the window is up the installer opens the folder holding that launcher in the file manager so you can see where the app is and pin it. Without such a browser it falls back to the default browser; on a remote machine it prints the cloudflared URL to open on your own device instead. A browser on the machine itself needs no password; from any other device type that password into the web app (the daemon checks it on every command such a browser sends). The Settings panel shows it in a plain text box you can edit: the new password is in force as soon as it is saved, with no restart (other devices then sign in again with it), and an empty box turns remote access off. Re-running install.sh updates the app in place, keeps the password (so the one your devices know stays valid), and restarts the daemon only when no task is running (otherwise the daemon restarts itself once its tasks are done, see below); from a shell, ~/.kiss/kiss/.venv/bin/python -m kiss.core.remote_password --rotate draws a new random one. On a machine that also has a white-label brand home (a ~/.<brand> directory with post-install hooks, or the one the running service was started from), the installer repeats itself into that home as well, each home keeping its own app copy (named after the home: ~/.s10s/s10s), model catalog and password, and starts the brand's daemon rather than the stock one. Whatever fails during a run (a hook, a brand home's install, a daemon that does not come up) is recorded in .install-problems under the home of the daemon the run starts (a nested brand home's failures are folded in, prefixed with that home) and shown as a notification in the web app for up to a week, or until a run succeeds. ./install.sh --non-interactive takes the default answer to every question, ./install.sh --restart-only installs nothing and only restarts the daemon from the app in KISS_HOME, KISS_SKIP_LAUNCH=1 skips starting the daemon and opening the app, KISS_HOME overrides the state directory, and the log is in ~/.kiss/install.log.

When a new release is available, the update toast in the web app offers Update and Update when idle; the daemon installs an idle update as soon as no task is running. install.sh never restarts a daemon that reports tasks in flight: it defers the restart, records it in .kiss-web.restart-pending in the daemon's home (the path the busy daemon names as restartPendingFile in its active-tasks answer and scripts/check-kiss-web-active-tasks.py prints; a JSON record of the installer, the state directory and the work dir to serve), and the daemon finishes it by itself: it looks for the record every 5 s and, once no task is in flight and no installer it launched is still running, raises the run-admission barrier so no new task starts meanwhile and runs the recorded install.sh --restart-only (which waits for an install that still holds the update lock, then restarts the daemon onto the app already installed and, apart from retiring a legacy app directory the new one has replaced, does nothing else, keeping the deferred run's .install-problems for the restarted daemon to report). The record is taken away as it is claimed, so a broken one is not retried, every restart by install.sh that brings the daemon up removes it, and a --restart-only run that still finds tasks in flight writes no new one, so the daemon cannot retry in a loop. So a Sorcar task can run ./install.sh, and the daemon it runs under moves to the new build once the task is done. A daemon from before this behavior cannot finish the restart, and the installer then says to re-run it once the tasks finish. From a checkout, scripts/reinstall-kiss-web-when-idle.sh re-runs the whole ./install.sh once the daemon has no task in flight: it starts a detached one-shot job (a launchd plist with KeepAlive off on macOS; on Linux a transient systemd-run --collect unit, or a setsid session where systemd-run is missing) and returns at once. If the update fails, run the installation command again. It will not delete your history. A web app page that was open through the install or update reloads itself once the restarted daemon is up: on every reconnect the page asks GET /api/daemon for the daemon's per-process token and reloads when the token differs from the one it saw at its first sign-in (after the commands it queued during the outage have been confirmed, so none is lost; a probe that cannot reach the daemon or gets a server error is retried with a growing delay, 5 s up to 60 s, while the connection lasts), while a network blip, which reconnects to the same daemon process, keeps the page as it is. The refresh button at the right end of the Chats row reloads the page by hand, which is the browser's reload made reachable from the app window and a phone's home-screen app, neither of which has an address bar. After an install or update the chat panel shows the tips from src/kiss/TIPS.md once per version. An update on a machine that still has the retired VS Code extension tries to uninstall it (through the code, code-insiders, cursor and code-server CLIs, or the one KISS_CODE_CLI names; if a copy remains, the terminal tells you to remove it from the Extensions view) and, for a week, the web app shows a "VS Code extension removed" notice pointing to the app; its Do not show this again checkbox silences it for good. All state (history database, settings, memories, TLS material) lives in ~/.kiss; KISS_HOME overrides the directory, and a white-label brand names its own through home_dir in brand.json. Executable scripts in ~/.kiss/post-install.d/ run in name order at the end of every install; a hook that is an exec <script> stub naming a script inside a git checkout with an upstream (as a brand's installer writes it) has that checkout fetched and rebased onto the upstream first (local edits stashed around the rebase; if the rebase cannot complete, the checkout is reset to the upstream, with the local commits kept on a brand-update-backup-<time> branch and the edits in a stash entry, both named in the warning), and a failing hook is reported and the install still completes. A brand that wants you signed in to a service of its own ships an executable signin hook in its home: while the hook reports you signed out, the web app shows a sign-in window with one button per provider the hook lists (Google and Apple, say), and a pasted redirect URL or code completes a sign-in started from another device; a missing or broken hook shows nothing, so a brand's sign-in can never lock you out of your own daemon. Branding (src/kiss/webapp/media/brand.json and a git-ignored .brand/ overlay for white-label builds) and the installer's steps are described in FEATURES.md.

Docker

./sorcar-docker [PORT] [--rebuild]

This builds a plain Debian image (Dockerfile) and starts a container whose entrypoint (scripts/docker-startup.sh) clones the repository (the private kiss.git when GH_TOKEN is set, else the public kiss_ai.git), runs install.sh, sets the web app password (KISS_WEB_PASSWORD, or a generated one printed in docker logs) and serves the web app on port 8787 (host port PORT, 8787 by default); the API keys in your environment are forwarded. State persists in the kiss-sorcar-web-home volume. Open https://localhost:PORT; the certificate is self-signed, so accept it once or install /ca.crt. Inside a container the Settings panel hides the LAN and tunnel URL bars, since the container's daemon is reached only through the published port.

Python package install

If you only want the Python package (the kiss-web daemon, the Python client API, and the messaging-agent entry points):

pipx install kiss-agent-framework
# or
uv tool install kiss-agent-framework

KISS Sorcar requires Python 3.13+.

Configure model access

Provide at least one model backend. You can use environment variables such as:

export ANTHROPIC_API_KEY=...
export OPENAI_API_KEY=...
export ZAI_API_KEY=...
export MOONSHOT_API_KEY=...
export TOGETHER_API_KEY=...
export OPENROUTER_API_KEY=...
export GEMINI_API_KEY=...

Claude models can also run through Amazon Bedrock: the catalog lists the Claude ids Bedrock serves as bedrock/<Bedrock model or inference-profile id> (for example bedrock/global.anthropic.claude-opus-5-5 or bedrock/us.anthropic.claude-opus-4-6-v1, with the usual -high/-max effort suffixes), and any other id the region serves can be used by name. Set either a Bedrock API key or an AWS access key pair, plus the region:

export AWS_BEARER_TOKEN_BEDROCK=...   # Bedrock API key (bearer token), or:
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...          # temporary credentials only
export AWS_REGION=us-east-1           # default when unset

Without either, the request is signed with whatever boto3's default credential chain finds on the machine, and the picker lists the Bedrock models as soon as such a source is configured: a web-identity token file (EKS with IRSA), ECS container credentials, or a profile in the shared ~/.aws files (AWS_PROFILE or default; static keys, or an assume-role, SSO or credential_process profile judged by its configuration alone), all read from the files and environment without a network call; only EC2 instance-metadata credentials go undetected, since probing them is a network round-trip.

Bedrock models are priced as their claude-* catalog entry, which holds the global. profile rate; an id of a Claude 4.5-or-later model under any other cross-region prefix (us., eu., apac., jp., au., ca., us-gov., ...) or the in-region anthropic. form is billed 10% above it, as on AWS's price list. An inference-profile ARN (bedrock/arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.anthropic.claude-opus-4-6-v1) is sent to Bedrock as it is and priced as the profile its last path segment names; a Claude model the direct catalog no longer lists is priced by its OpenRouter twin (Opus 4.1), and one the catalog holds only as a bedrock/ entry (Sonnet 4, Claude 3 Haiku) by that entry's base rate with the premium of the profile actually used applied once. The bundled bedrock/* entries are the ids the model updater (uv run python -m kiss.scripts.update_models, or the Update Models button in Settings) found in its own AWS region: Bedrock lists the cross-region inference profiles and on-demand model ids per region, so running the updater on a machine in another region relists what that region serves, and each entry copies its claude-* twin's context length, capabilities and effort suffixes with the prices scaled as above. With Bedrock as the only configured provider, the default and fast models are the global. profiles of the Anthropic default and fast models. An id the catalog does not list can still be typed as a model name or added under Settings → Custom Models (name only, no endpoint), which puts it in the model picker from the next daemon start.

You can also set API keys, a custom model endpoint, and custom HTTP headers in the Settings panel of the web app. The Custom Models section of the Settings panel registers your own models (a local vLLM/Ollama endpoint, or a provider model not in the bundled catalog); entries are stored in ~/.kiss/MY_MODELS.json and appear in the model picker alongside the bundled catalog; an entry named after a catalog model (exactly, under a provider prefix such as openai/<model>, or as a bedrock/<id> profile) starts from that model's context length and prices, its prompt_tiers included, so it is billed at the catalog rate. A hand-edited entry whose context length or price is missing, or is not a finite, non-negative number (a negative price, NaN, 1e309, a quoted number), is skipped with a warning when the catalog loads rather than billing a negative or NaN cost. The Bedrock section after it holds the Bedrock API key, AWS access key pair, session token and region. The Claude Code section after that is for the cc/* models, which run the claude CLI: it shows whether that CLI is logged in and how it will bill (claude auth status), holds the switch Bill my Claude subscription for cc/ models (on by default: the CLI is run without ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN, so a claude login on a Pro or Max plan pays for the turn instead of API tokens; off, the CLI inherits the daemon's environment and an API key bills per token) and an optional subscription token from claude setup-token (CLAUDE_CODE_OAUTH_TOKEN) for a machine where nobody ran claude login. The Codex section after that does the same for the codex/* models, which run the codex CLI: it shows whether that CLI is logged in and how it will bill (codex login status), holds the switch Bill my ChatGPT subscription for codex/ models (on by default: the CLI is run without OPENAI_API_KEY / CODEX_API_KEY, so it bills the ChatGPT plan it is logged in to, and a turn is refused when the CLI's saved login is an API key from codex login --with-api-key, which would bill per token anyway; off, OPENAI_API_KEY bills per token), a Sign in with ChatGPT button that starts codex login --device-auth on the daemon's machine and shows the link and one-time code to enter in any browser (the way to log a headless machine in to a Plus/Pro plan), and a box for a Codex access token (CODEX_ACCESS_TOKEN, ChatGPT Business/Enterprise) for a machine where nobody ran codex login. Both CLIs are driven as plain models: their own tools, MCP servers, hooks and project instructions are switched off and they work with Sorcar's system prompt and tools, like every other model (Codex is handed Sorcar's tools, every one but finish and set_model, as a kiss MCP server, so one codex/* step can make several tool calls, each executed by Sorcar; the bridge listens on a loopback port any local user could reach, so every request must carry a per-launch secret that Codex forwards to the bridge's proxy through its environment (KISS_MCP_BRIDGE_TOKEN, never on a command line), an unauthenticated request is refused, and a tool name the bridge does not list is answered with an error; finish and set_model are called through the text-based tool protocol, the first because it ends the step, the second because it must not replace the model under a running turn). The API Keys, Custom Models, Bedrock, Claude Code and Codex sections start collapsed every time Settings opens. Settings opens for the machine the chat header's machine drop-down names: for a registered machine the form shows and saves that machine's keys, settings and custom models (the daemon forwards the commands over the registered link), and hides what only concerns this daemon (its URLs and the Update, Reset Server and Update Models buttons); the form's text fields and custom-model list are blanked while it switches machines, so one machine's password and keys never show under another's name. A save that cannot write config.json (a full disk, the file locked by another process) answers "Could not save the settings: ..." and sends the stored settings back instead of applying the change; an API key is stored only under a name that is a valid environment-variable name and with a value free of line breaks and NULs, any other entry is dropped with a warning in the log.

The picker also lists four bundled router agents under the Router group: autorouter runs its routing session on openrouter/moonshotai/kimi-k3 (pre-run classification off, web tools and memory on), splits a task into units that have a mechanical acceptance check and dispatches those to the cheapest model tier (small, medium, frontier) that passes the check, escalating on failure, keeps guarded and uncheckable work and the final acceptance of the whole on the frontier tier, and switches its own model only at phase boundaries; bestrouter runs every task on claude-fable-5-1-max and has gpt-6-astra review the result read-only; orautorouter and orbestrouter are the same two routers restricted to the OpenRouter vendor, so a single OpenRouter key runs them: orautorouter's tiers hold only openrouter/... models (the OpenRouter twins of the autorouter's candidates, in the same order) and orbestrouter works on openrouter/anthropic/claude-fable-5.1-max with openrouter/openai/gpt-6-astra reviewing. They are Sorcar Extension Agents (see below), not models.

Using KISS Sorcar

KISS Sorcar has two client interfaces, both served by one local daemon: the web/mobile app and the Python client API. A third interface, the sorcar terminal command, runs a SorcarAgent directly in the current directory without the daemon: sorcar -t "Summarize README.md" runs an inline task, sorcar -f task.txt runs the file's content as the task (exactly one of -t/-f is required; see sorcar --help for the model, budget, and work-dir flags).

Web and mobile app

Open the web app in a browser and type or speak your task. The chat interface provides:

  • @ file/folder mentions with ranked completion from a persistent index of your working directory and home directory. In a chat bound to a registered machine the picker lists that machine's files: the daemon sends the request to the machine with the chat's working directory there, keeps asking for up to a minute while the machine is still indexing the folder, and answers in the background, in order per tab and machine, so a slow scan holds up neither a message you send meanwhile nor another tab's list; a mention picked there is counted on that machine, so its ranking learns it.
  • Git worktree isolation that the agent manages itself, with no setting to toggle: inside a git checkout, the agent calls its create_worktree tool before its first change to a tracked file, which moves the task onto a private kiss/wt-* branch checked out under <repo>/.kiss-worktrees/ (an edit to a tracked file of the checkout outside a worktree is refused), and commit_and_merge once the work is verified, which commits the branch with a generated message and squash-merges it into the branch the task started from (the bundled /merge agent resolves conflicts). A worktree a task leaves unmerged is offered to you with a Merge / Discard bar. In a git checkout a top-level task also has checkpoint(name) and rollback(name): the first snapshots every file git does not ignore (tracked or not) as a git tree object written through a private index, so HEAD, the branch, the real index and the stash are untouched and nothing is committed; the second rewrites the files changed or deleted since and removes the ones created since, leaves ignored files (build output, tmp/) alone, and saves the state it replaces as before-rollback so it can be undone. Snapshots live for the task; a sub-agent gets neither tool, since it edits its caller's tree in place and a rollback would undo the caller's and its siblings' work too, and a run inside a Docker image has neither (the coding SEA's container-side checkpoint/rollback, which archive the container's work dir, are the pair's origin).
  • An evidence gate on finish: in a run that has the edit tools, a finish(success=True) whose summary says the result was guessed, untested or unverified is refused once with the phrases it found, so the agent spends one more round on a check instead of a claim; a second finish always passes.
  • A pre-run task classifier that marks a task involving neither software development nor Internet search as simple, so it runs with a lite system prompt. A run that cannot edit files anyway (a review or shell tool profile, or a script-only SEA such as a channel agent) gets the lite prompt whatever the verdict; a library run() with such a profile skips the classifier's model call, while the daemon's task runner, which also runs run_agent sub-tasks, still classifies a submitted task before the run. The classifier and the agent's decide tool use the ~typesafe/jev-latest decisions model through OpenRouter when the classify_with_decisions key in ~/.kiss/config.json is on (the default; it has no Settings toggle) and an OpenRouter key is configured.
  • A model picker (the picker of the machine the chat header's drop-down shows: it lists that machine's models and default model, a pick becomes the tab's model and that machine's default and leaves this daemon's alone, the page keeps each machine's last list so a switch back repaints at once while the list is asked afresh, and a model switch made by a running agent shows only while the chat is on the machine its task runs on), per-task budget caps, chat history with tags (at most three per task, always including secret for a trajectory that carries a credential) and per-chat summaries (double-click a chat's title, or click its pencil button, to retype it; your text replaces the generated summary on every device until you clear it, and a collapsed chat shows when its latest task finished), ordered with running tasks first and then by when each task finished, newest first (a task with no completion time yet, because it is still running or predates that column, counts from its launch instead; a chat ranks by the newest such time among its tasks, and the same time drives a row's "3 hours ago" label and the date filter), filterable by state, date, SEA, machine, working directory and tag, with a Cron chip, off by default, that brings in the tasks the scheduler launched (they are hidden otherwise) (each task row's details show, where known, its working directory, model and parallel mode, then the machine it ran on with its budget cap and the agent it ran as, then its chat, task and parent ids), a Task Info view (tokens, cost, steps, time, working directory, budget, model, and the ids of the chat, the task and its parent), a machine: directory header above the chat with two drop-downs (both stay on screen at phone widths, the directory wrapping onto a second line when the machine name needs the room): the machine the chat's next task runs on (this one or any registered machine that answered the daemon's last ping, each shown by the name you gave it or, when you registered it by its address, by its own host name; a remote pick is green, red once the machine is reported down; a sub-agent tab of a task running on a registered machine shows that machine, as its parent's tab does; the pick is sent with each task and kept across reloads, and changing it while a task runs leaves that task where it is and routes the next one) and the working directory (one daemon-wide directory, persisted in ~/.kiss/config.json, that every task from every surface runs in unless the run() call or the task's SEA names another, as the channel agents' ~/.kiss/channel_work does; the drop-down lists the current directory, then every directory picked before that still exists, newest first, and a Choose ... entry that opens a folder picker; a pick there or the Explorer's "Set as Working Directory" check mark changes it for all connected clients at once; while the machine drop-down names a registered machine, the directory drop-down lists that machine's own working directory and the directories picked there before, a pick there changes that machine's working directory and leaves this one's alone, Choose ... browses that machine's folders, and the Explorer and Source Control views show that machine's working directory and repository, with files opened from them read and saved there), and inline rendering of tool-generated images, including pictures the agent names by file path (<img src="tmp/plot.png"> in a result, answer or Markdown panel, resolved against the task's working directory; in a previewed .html or .md file, against that file's own directory): the page's origin serves no files, so each one is fetched over the authenticated connection, from the machine the chat is bound to (a relative path in a tab bound to a registered machine is resolved against that machine's working directory, not this daemon's, and so is a file link clicked there), and shown as a data: URI; in a result, answer or Markdown panel, one the daemon cannot serve shows its alt text (the path, when the tag gives none) in a dashed box whose tooltip says so, while in a file preview it keeps its original src.
  • Image and PDF attachments via the picker, paste, or drag-and-drop; files attached to a message sent while a task is running go with it (to the live agent for a steering message, with each queued task for a <task> follow-up).
  • A finished task, or a chat, can be deleted from the Chats panel: task deletion removes the task and its sub-tasks, chat deletion removes the chat's records from this daemon's history.db. The daemon refuses while any record it would remove is still running. Tasks stored on registered machines are untouched, so a remote task row, or a chat whose tasks all ran remotely, offers no delete button here.
  • Persistent agent memory (on by default): Markdown pages under ~/.kiss/memories with a vector index, plus a per-repository memory for tasks run inside a git checkout. A task with memory on starts with a search keyed by its own text (a task over 4,000 characters searches by its first and last 2,000), and up to five matching pages (name, date, title, summary) are appended to its first message; a search that fails leaves the message as it was. Toggle it in Settings or with KISS_USE_MEMORY=0.
  • Wake-word voice chat ("Abracadabra, …") via the mic button, including steering a running agent by voice. The speech engine and model load in the page: a load that has not settled in 30 s is reported as failed and the next click of the mic retries it (a model download still under way is reused rather than started again), and each transcription goes to the conversation that was on screen when you spoke, dropped rather than typed into another chat if you switched tabs meanwhile.
  • Live steering: inject a message into a running agent or switch its model mid-run; wrap the message in <task>…</task> to queue it as the next task instead.
  • When an agent asks you a question, every surface brings its tab forward and the composer becomes the answer box; a tab that is not in front shows a ? badge ("Waiting for your answer") until the question is answered on any device, the task ends, or the tab closes. Each question carries an id of its own and an answer names it, so an answer sent from a second device after the first one's was taken is dropped rather than taken as the answer to the agent's next question, even one with the same text.
  • Tab mirroring: every connected browser shows the same chat tabs (the daemon owns the one canonical tab list and binds each chat to at most one tab), and a sub-agent opens a nested tab of its own on every client that closes when it finishes (a reload, or a surface connecting later, is told such a finished tab as closed history, so it is not reopened; a finished sub-agent's tab you reopen yourself from its parent's fan-out panel is announced as reopened to every surface and stays open there until a client closes it). A chat or task you open (a history row, a sub-agent tab, a switch back to a parked chat) lands at the end of its transcript while its task is running and, once the task has finished, with that task's panel at the top of the view so the result fills the rest; a replay you did not ask for, such as a reconnect's, keeps your reading position. A replay into a chat tab first tells the tab whether the daemon still runs its task, so a tab that missed its task's end while the page was offline drops its spinner and Stop button, and one whose task started during the outage picks them up. A chat opened from a registered machine says "Loading the chat from …" under the task panel until the machine's chat arrives. There is no row of chat tabs: the chat on screen is the one you pick in the Chats panel or open with +, and a group strip under it lists that chat's sub-agent tabs; on the mobile web app's file screen the strip also lists the file, browser and terminal tabs the task opened (its chat screen lists just the chat and its sub-agents); the strip is hidden for a single conversation. On a phone, swiping left or right across the middle of the screen moves between the chat history, the chat, the open files and the task info; a sticky hint says so until its Do not show this again box is ticked. The desktop web app splits the window instead: the chat on the left keeps the strip, and the content tabs get a row of their own on the right.
  • A Browser tab that streams a real browser running next to the daemon to every surface. The stream is paced per viewer: a frame (JPEG, up to 15 a second while the page repaints) goes out only while at most two of that viewer's frames await its acknowledgement, a frame produced meanwhile is held back and the newest one goes out when the acknowledgement arrives, so a slow link shows the latest picture at its own pace instead of a backlog that would delay the connection's keepalive and drop the Browser tab from every surface (a viewer that never acknowledges, such as an older surface, has a frame counted as delivered after 5 s); a page of a registered machine's browser is relayed the same way, the daemon acknowledging each frame to the machine and holding the newest back for a surface that is behind. An agent's show_browser() moves the page it is browsing into that tab when a login, CAPTCHA, or live demo needs you (out of a Chrome it attached to, only the page's URL comes along, never that profile's cookies or localStorage). When the chat on screen is bound to a registered machine, the Browser button opens a page of that machine's browser instead, and the tab is labelled with the machine. Besides navigating, clicking and typing, the agent has DevTools-grade tools on that browser: evaluate_script runs a JavaScript function or expression in the page and returns its result as JSON, list_console_messages and list_network_requests return the console output (uncaught errors included) and the requests (with status, filterable by URL substring) recorded since the page loaded or since their last clear, and wait_for blocks until a text appears on, or disappears from, the page. It can also drive your own Chrome rather than the daemon's: attach_to_chrome connects over the DevTools protocol to a Chrome or Chromium you started with --remote-debugging-port (default http://localhost:9222, a separate profile is needed since Chrome ignores that flag on the default one), and use_my_chrome works through the KISS Sorcar Bridge extension in your everyday Chrome (list, open, navigate and snapshot tabs, click or type into numbered elements, run a script), which talks to a loopback WebSocket the bridge serves (a mutual challenge-response handshake against a token the bridge writes, with its port, into bridge-config.json in the extension folder; the token never crosses the wire). The bridge copies the extension's files into ~/.kiss/chrome_extension when it starts, and you load that folder once from chrome://extensions (Developer mode, Load unpacked). The first use_my_chrome call starts the bridge and may wait a few seconds for the extension to find it; tick Drive my Chrome in Settings (the chrome_bridge key of ~/.kiss/config.json, off by default) and the daemon serves the socket from start-up instead.
  • Scheduled automations: ask in plain language ("every weekday at 9am, summarize my unread Slack messages") and the built-in cron agent (kiss-cron from the shell) creates, lists, pauses, resumes, or removes the schedule; the Schedule panel does the same with buttons. Schedules are kept in Pacific time, and a job can deliver its result to a messaging channel. A job's prompt may be a slash command (/recommendations now), which runs that SEA directly, and an in_chat job submits its prompt as a message typed into a new chat. Every installation is seeded once with two such jobs, a 6-hourly /recommendations now and an 8:00 in_chat /good_morning now; edit or remove them as you like, they are not re-created. Like cron, the scheduler does not catch up on occurrences missed while the daemon was down: a repeating job found more than ten minutes overdue is rescheduled from now without running (a one-shot job still runs, late). A job that fails the same way twice in a row (same first non-blank line of the result, whitespace-trimmed: an expired sign-in, a dead host) is not delivered again, and its next run is pushed to at least an hour from now, that minimum doubling with every further repeat up to a day (a run already scheduled later than that stays put). A prompt job that is not in_chat gets a run context block ahead of its prompt: the time now (in Pacific time and UTC), the schedule, the start of the window this run covers (the last run that ended ok or silent, or the job's creation while none has; a failed attempt covers nothing, so the window keeps growing until one succeeds), the previous attempt with its status and the tail of its result, so the run neither asks the clock nor reads its own log. Before such a job is dispatched, each Composio-backed Google connector its prompt names (Gmail, Google Calendar, Docs, Drive, Sheets, Chat) has its connection status checked, and a connector that is not active fails the run with a fixed message instead of spending the run's steps on it (an in_chat job, a prompt that asks to authenticate or reconnect, and a status the check could not determine are let through). Each home keeps its own store ($KISS_HOME/cron/jobs.json) and only the running daemon's store ticks, so a daemon that starts from a different home than the last one did (a brand's ~/.<brand> after the stock ~/.kiss, or the other way round; the last daemon's home is recorded in ~/.kiss-web-home) adopts the previous home's jobs once at start-up, before seeding the defaults: every job whose name this store lacks (paused ones included; a job already here under the same name, or a default's former name, stands as it is) is copied in with a clean failure streak, a repeating job gets a fresh next run, a one-shot keeps its deadline and a finished job stays finished; a store that cannot be read or written is left alone and the marker still moves on.

The web app is also served over a cloudflared tunnel: copy the URL and password from the Settings panel and open it on any device; the browser tab is titled KISS Sorcar: <machine name> so several servers are easy to tell apart. The daemon also posts each new tunnel URL, followed by the machine name in parentheses, to a private per-machine ntfy.sh topic linked from the same panel, so a phone subscribed to the topics of several machines can tell their URLs apart after a restart. Its desktop mode adds a Task Info sidebar with live token, cost, and step metrics and a Task update for the running task (the /ask agent's short answer to what the task has done so far, first requested a minute after the task starts, refreshed every ten minutes or on demand with the refresh button, and charged to the task; for a task running on a registered machine, that machine's daemon answers), plus Schedule (every cron job, with a + in the header that creates one in place and, per job, edit-in-place, run-now, enable/disable, copy and delete buttons), Machines (this machine and every registered one, labelled as in the chat header's drop-down, with its address and reachability; the tooltip shows the resolved IP and the daemon port; the report is cached for a minute, or for ten seconds while any machine is unreachable, and the next request after that re-pings the machines (the page asks for the panels every thirty seconds while it is visible, and the panel's refresh button asks at once), so a row turns green soon after a laptop wakes, and a machine that failed to answer is not dialled again for five seconds, doubling with every failure in a row up to thirty (a rejected password waits the full thirty from the first failure, so the remote daemon's lockout after a few wrong passwords is not tripped); when the daemon finds the host has slept for thirty seconds or more, which it tells from the wall clock running ahead of the monotonic clock, it abandons its cached sockets and ssh forwards to every registered machine and clears the back-offs, so the first request after a wake dials afresh rather than hanging on a dead socket; the header's + registers a new one, and a machine registered any way, there, by /machine add or by the CLI, brings the subpanel on screen), Apps (channel sign-in state; click one to connect it), Spend (a per-day heatmap of what your tasks cost and a cost-by-model bar chart; a task's total, sub-agents included, is split among the models its own calls and its sub-agents actually ran on, so a set_model switch or a sub-agent on another model is filed under that model, and anthropic/claude-... and claude-... count as one model), and Recommendations (ten suggested next tasks drawn by the /recommendations SEA, which a default cron job runs every six hours, from your tasks of the last 24 hours plus enough older ones to review at least 20 completed tasks, with at least three of the ten combining the third-party apps you are signed in to, or ten starter tasks for a new project when there are no recent tasks; clicking one opens a new chat with the task in the composer, unsent) panels. The Schedule, Machines, Apps, Spend and Recommendations panels, like the model picker and the Settings sheet, show the machine the chat header's drop-down has picked: for a registered machine the daemon forwards their requests over the link, so they list (and Schedule and Machines edit) that machine's jobs, registered machines, sign-ins, spend and suggestions; the drop-down itself keeps listing this daemon's registry. A Settings sheet left open while the drop-down's machine changes saves the edits made so far to the machine they were made for, then loads the new machine's settings, and a task sent to a registered machine carries no Use web tools override from this page, so the machine applies its own setting. The activity bar holds an Explorer (workspace file tree with Ctrl/Cmd-click, Shift-click, and Ctrl/Cmd+A multi-select; Add Folder to Explorer... opens a folder picker whose "Recently opened" list offers the working directories picked so far that the Explorer does not already show; a rename that changes only the case of a name, a.txt to A.txt, works on a case-insensitive file system too, through a temporary name in the same folder, with the old name put back if the second step fails) and a Source Control view (the header's badge counts the uncommitted files of the worktree the chat's task is working in, or of the checkout itself when there is none, never those of other tasks' worktrees; changes grouped per worktree, with foldable sections, and a commit graph whose file rows open a side-by-side diff). When the chat on screen is bound to a registered remote machine, both views browse that machine's working directory and repository instead of the local daemon's folders (the Explorer then shows only that one directory and hides Add Folder to Explorer...), and a file opened from it saves and reloads back to that machine. On the first load after an install or update only Recommendations is expanded (the browser remembers the version, so later loads keep your own folds); when a task starts, the sidebar shows Task Info and Task update expanded and every other section collapsed; afterwards the sidebar is an accordion: clicking a collapsed section's header opens it and folds the others. File links open in content tabs with a Monaco editor, Markdown/HTML preview, and a PDF viewer (a file link in the transcript of a chat bound to a remote machine resolves and opens on that machine; a prompt that is nothing but the path of an existing file, sent from a chat with no task running, opens that file in such a tab instead of starting a task, on the machine the chat is bound to, where the tab then saves and reloads it); a tab showing a file or folder on disk has a Reload from disk button at its top right (at the right end of the Save bar when the tab has one, floating over the view otherwise) that re-reads it, replacing any unsaved edits, while a virtual tab such as a commit's patch or a search result has none. The "..." menu holds the appearance switches (Dark mode / Light mode, an Auto mode that is dark from 7 pm to 7 am by the device's clock, and High contrast / Low contrast, low being the default; each choice is remembered by the browser); its Git Commit item stages every change in the repository that holds the chat's working directory (git add -A), has the model write a commit message from the staged diff and commits (a clean tree is "Nothing to commit."; when the chat header names a registered machine, the commit runs in that machine's folder and its outcome comes back as the toast here; the item reads "Committing…" meanwhile and re-arms on its own after two minutes if no outcome has arrived, while a remote commit may take longer), and its Terminal item opens a shell on the daemon's machine in a tab, or on the machine the chat on screen is bound to, labelled with that machine (xterm.js; bash and zsh start with your own dotfiles plus a hook that writes each command to the history file as it is entered, so Up/Down in a new terminal tab recall the commands of every earlier one). Sections 15 to 19 of FEATURES.md describe each of these in full.

The kiss-web daemon

The kiss-web daemon hosts the agents, chat sessions, and the web app, and services every client command over a WebSocket on one port (8787 by default; plain on the machine's own loopback, TLS for everyone else). Browsers sign in with the password from Settings; same-machine clients (the Python client, cron jobs, and channel agents) present a per-start token the daemon writes to ~/.kiss/sorcar-local.json (KISS_SORCAR_LOCAL overrides the path). install.sh registers it as your user service; you can also manage it yourself:

# Start the daemon (serves the web app).
kiss-web

# Pin the daemon's working directory.
kiss-web --workdir "$HOME/projects/my-repo"

# Print the active remote (cloudflared) URL and exit.
kiss-web --url

# Trust the daemon's TLS certificate in this user's browsers and exit.
kiss-web --trust-ca

The Local URL (http://127.0.0.1:PORT) is plain HTTP: loopback traffic never leaves the machine. The same port speaks TLS to everyone else: the LAN URLs use a certificate issued by a machine-local certificate authority kept in ~/.kiss/tls/; kiss-web --trust-ca installs it in the browsers on the daemon's machine, and a phone or tablet on the same network installs it from https://<lan-ip>:PORT/ca.crt (compare the SHA-256 fingerprint the command prints). Plain HTTP from any other machine is refused. The CA key never leaves ~/.kiss/tls/, and the server certificate is re-issued automatically when it expires or the LAN address changes.

The WebSocket is built for slow links. A frame above 64 KiB (a replayed transcript) goes to the web app, to another daemon's machine link and to the Python client as separate part frames that the receiver joins, each one a sign of life for the page's stale-socket check, which drops a socket silent for 45 s and would otherwise take one big frame's minutes of transfer for a dead connection; a link between two daemons negotiates permessage-deflate on top. A replayed transcript leaves its pictures out: every screenshot or plot in a tool result travels as a handle, and the page fetches the picture (from the machine the chat is bound to, for a remote chat) once it scrolls to within 400 px of the view (at once in a browser without IntersectionObserver). A Chats row carries its task text once, cut to 2,000 characters, and the composer's ArrowUp history is refreshed with the texts used since the list the page holds rather than the whole list at every task start or finish (the whole list again after a task or chat deletion). A peer whose socket takes nothing of a frame for 30 s (the bytes it has not taken yet, looked at once a second: the daemon's own write buffer plus, on Linux and macOS, the kernel's send queue; the write buffer alone on Windows) is dropped, however slow its link; a peer that keeps reading, however slowly, is kept.

Every HTTP answer the daemon's request handler builds (a HEAD health check is answered before that handler and skips this) passes through speed_up_response (src/kiss/server/http_speed.py, the process_response hook of its listeners): a 200 text body (HTML, JavaScript, CSS, JSON, SVG, WebAssembly) of 1 KiB or more goes out gzip-compressed to a client whose Accept-Encoding admits gzip (with Vary: Accept-Encoding either way), compressed once per distinct body since the results are kept in memory keyed by the sha256 of the bytes (64 MiB at most), which cuts the web app's assets to about 40% of their size on the way through a tunnel or a brand's hub relay; and a /media/<file>?v=<hash> asset, whose URL changes whenever its bytes do, is marked Cache-Control: public, max-age=31536000, immutable so the browser keeps it for a year, while the page and every other such answer stay no-store. A brand that needs a helper running for exactly as long as the daemon (Seamless Loop's hub relay, which lets a browser reach this daemon through the brand's hub from any device without a tunnel) drops an executable into ~/.kiss/services.d/: the daemon starts every executable file there (dot files skipped) once it has written its local endpoint file, each in its own process group with KISS_HOME set and the home as its working directory, appends its output to ~/.kiss/logs/services/<name>.log, restarts one that exits after 1 s, doubling up to 60 s while it keeps failing (a run of a minute or more resets the delay), and at shutdown sends each process group SIGTERM, then SIGKILL after 5 s when ignored (src/kiss/server/services.py).

Python client API

Any Python process can run a task on the daemon with kiss.server.sorcar.run and block until it finishes (up to timeout, one hour by default):

from kiss.server import sorcar

result = sorcar.run("Summarize README.md", work_dir="/path/to/repo")
print(result.text, result.success, result.cost, result.tokens, result.steps)

# Continue the same chat (the agent sees the prior task as context):
follow_up = sorcar.run("Now fix the typos you found", chat_id=result.chat_id)

run() accepts keyword options mirroring the chat interface (model, work_dir, chat_id, max_budget, model_config, use_web_tools, use_memory, auto_classify, tool_profile, docker_image, timeout, and more) plus options that customize the agent itself: system_prompt, add_to_system_prompt, add_to_prompt, and sea_path (run a Sorcar Extension Agent — a Python file whose settings() configures the run and whose tools() supplies extra tool functions, imported and run in the daemon process). Every option is documented in src/kiss/server/README.md.

Sorcar Extension Agents (SEAs)

A Sorcar Extension Agent (SEA) is a Python file, <name>/<name>_sea.py, that defines one class deriving from one of the three base classes in kiss.agents.seas.base.base_sea; you pass the file's path as sea_path to sorcar.run() (or as the agent of the run_agent tool). The daemon executes it on every run, instantiates the class and calls the methods it defines; every method on BaseSea passes its input through (its one rule of its own is the summary cadence: a run that has the summary tool must call it at every tenth step, and every other tool call but finish is refused until it does). What a SEA is is its base class: BaseSea is an ordinary Sorcar session with the caller's or the user's settings; WorkerSea is a focused tool-bound run on the caller's tree (it lays WORKER_DEFAULTS under the subclass's settings: no classifier, no browser, no memory; and its tools() drops create_worktree and commit_and_merge, so a worker edits the caller's tree in place); ChannelSea is a worker that serves an external service from $KISS_HOME/channel_work (unless its own work_dir says otherwise, as /cron's cron/work does), holds its channel workspace, gets the channel preamble, never inherits from a calling task and has work_dir and the worker keys locked (every behaviour is listed in sea_settings.CHANNEL_BEHAVIOURS). settings(self, settings) receives the dict the base classes built and returns the dict to use (settings | {...}): any of run()'s per-run parameters (model, max_budget, tool_profile, work_dir, chat_id, use_web_tools, use_memory, auto_classify, model_config, docker_image), plus timeout (seconds a run_agent call waits for the script before handing back a job id; 3600 by default), locked (keys an explicit run_agent argument may not change) and hidden (True keeps the SEA out of the command list, so it is no /command and no run_agent agent name; it is still loadable by path and as a base class, and the hidden sorcar SEA is what an empty agent means). The former kind, preset, channel, allow_fan_out, is_parallel, inherit, extends, append_basic_tools, use_worktree, auto_commit and append_to_*/add_to_* prompt keys are refused with a message explaining what took their place (sea_settings.REMOVED_SETTINGS). A relative work_dir, in a SEA or a call's option, is a path under the calling task's directory. A subclass's keys override its base's defaults; an explicit run_agent argument or option overrides the SEA's value unless the SEA lists the key in locked, and parameters the SEA leaves out keep the caller's values (the daemon's defaults for a channel, which inherits nothing from the calling task, or when the call passes inherit: false). settings() is data; text and code come from the other methods: prompt(self, task) receives the task text and returns the prompt body ({task_id} in it becomes the calling task's id), system_prompt(self, system_prompt) receives the assembled system prompt and returns the one to use (return your own text to replace it, system_prompt + "\n\n" + more to append), tools(self, tools) receives the run's toolset and returns the run's (tools + [mine] adds tool callables to the built-in toolset; "tool_profile": "none" drops the built-in toolset so the returned callables plus finish are the agent's entire tool set). A SEA builds on another by Python inheritance (class MySea(ShSea), or sea_class("sh") from kiss.agents.sorcar.sea_commands); the launcher applies the base class's methods first, so methods never call super(). Every command SEA also defines description(), one sentence that /<name> help prints; /<name> check loads the class and prints its inheritance chain, effective settings, model, tools, defined methods and the sample prompt a run would use, or the first error. From the shell, sea lint [paths] [--fix] [--registered] checks SEA files against the same contract (every bundled SEA by default; --registered adds the ones your SEAS.md lists; --fix rewrites renamed and removed keys, old base classes and verdicts), and sea docs [--check] [pages...] regenerates the settings, base-class, channel, precedence, option and command tables between the <!-- sea-docs: … --> markers of the docs (by default the bundled pages that exist; --check only reports stale blocks, and a page named on the command line that does not exist is reported as missing: <path>). Two hook methods, llm_call_hook(self, new_messages) and tool_call_hook(self, name, args), run before each model call and tool call of the task's executor sessions (internal helper sessions and run_parallel sub-agents are not hooked; a tool hook returns a Verdict: refuse(text) suppresses the call and hands text to the model, ALLOW lets it run; both come from base_sea). One file is a complete custom agent:

# weather/weather_sea.py — a minimal SEA
from typing import Any

import requests

from kiss.agents.seas.base.base_sea import WorkerSea


def get_weather(city: str) -> str:
    """Return current weather for a city from wttr.in.

    Args:
        city: City name to look up.
    """
    resp = requests.get(f"https://wttr.in/{city}?format=3", timeout=10)
    resp.raise_for_status()
    return resp.text.strip()


class WeatherSea(WorkerSea):
    def description(self) -> str:
        return "Reports the current weather in San Francisco from wttr.in."

    def settings(self, settings: dict[str, Any]) -> dict[str, Any]:
        """A worker run whose only tools are get_weather and finish."""
        return settings | {
            "max_budget": 0.50,
            "tool_profile": "none",
        }

    def prompt(self, task: str) -> str:
        return f"Look up the current weather in {task or 'San Francisco'} and report it."

    def system_prompt(self, system_prompt: str) -> str:
        return ("You are a weather assistant. Use the get_weather tool "
                "to look up weather, then call finish with the result.")

    def tools(self, tools: list[Any]) -> list[Any]:
        """With tool_profile "none" above, get_weather and finish are the whole tool set."""
        return tools + [get_weather]
from kiss.server import sorcar

result = sorcar.run("placeholder", sea_path="weather/weather_sea.py")

Slash commands. Every SEA folder in a scanned folder whose settings do not mark it hidden is also a chat command: /xxx some text runs xxx/xxx_sea.py directly in the tab with "some text" as the task, the SEA's settings, system prompt, and tools applied to that very run (no relay turn by the chat agent and no nested sub-agent tab); /xxx help prints the SEA's description() without a model call. A task you type that names a command elsewhere in its text, or whose earlier tasks in the chat do (... You must use /discover ..., then continue), gets each named command's description() after the task under # Slash commands the user named, so the agent knows what the command does before it calls it with run_agent. Typing / at the start of any word in the composer opens a completion popup (a / inside a path such as src/ or @docs/ does not) listing the three commands you ran most recently, then the ones run at least twice, most-run first, then the rest, as every surface sees them, since the ranking comes from the task history; the accepted command replaces the text from that / to the cursor and keeps what is before and after it. The same SEA runs as a sub-task of any task through run_agent(agent="xxx", task=...). The channel agents are registered this way (/slack, /gmail, ...), and so are 26 of the 28 bundled SEAs in src/kiss/agents/seas/ (the base folder holds the base classes, not a command): /ask (answer a question about the current task from its persisted events), /sh (run a shell command), /sync (commit the checkout's uncommitted changes on the current branch, fetch origin/main, merge it into that branch and push; with machines registered, first do the same on every machine one at a time, as a run_agent(agent="sync", machine=<name>) sub-task on that machine's checkout of the same repository, found by its remote URL, wherever the machine keeps it, then here, then on every machine once more, so each checkout ends up with what the others pushed; /sync alone is enough, since the SEA supplies its own task text, /sync <remote>/<branch> merges another remote branch, and /sync this machine only skips the machines, which is what the sub-task sent to a machine says so it does not fan out again), /sync_history_and_memory (bring history.db and memories/ of this machine and of every registered ssh machine together in one run, through one sync_history_and_memory tool call with no other tools and a $1 budget: every machine is reached over one multiplexed ssh connection and worked on concurrently, first pulling what each machine has into this one and then pushing the union back to each, so one run converges every machine that completes all three waves (a machine that fails in one wave is left out of the later ones and reported as skipped or half-synced); history rows travel through scripts/sync_db.py as a union of tasks by id and events by (task_id, seq) with deletions carried over, memory pages travel only when their content hash differs, the newer updated stamp winning and two writes no further apart than the measured spread of the machines' clocks reported as a conflict and kept on both sides; a task whose row is about to travel while it is still running, the later of its start and newest event younger than 300 s on its own machine (24 h for a row still carrying the web app's start placeholder), is held back and travels once it has finished, so running the sync while tasks run loses nothing; a machine registered by URL rather than ssh is reported as skipped; /sync_history_and_memory alone syncs every machine, /sync_history_and_memory <machine> one of them, /sync_history_and_memory dry run reports what would travel), /good_morning and /recommendations (the morning greeting and the stored next-task suggestions the web app shows), /machine (register, forget, or list remote Sorcar machines, /machine add user@host, an SSH login used once to enroll this daemon's certificate on that machine and from then on reached through a persistent ssh -N -L port-forward to its daemon port with that certificate (the daemon counts the forward as up only once ssh itself reports the local port bound, and moves to a fresh local port when ssh could not bind the one it picked), so only the ssh port needs to be open and revoking the machine means removing its ssh access, or /machine add <name> <https://host:port> <password>; the Chats panel then lists that machine's tasks next to the local ones (every registered machine's chats are merged into one list, running tasks first and then newest first, and a machine that does not answer keeps showing the default, unfiltered page it last returned instead of dropping out; with a search or tag filter on, or when the machine has never answered, or when its registration changed since, it contributes nothing until it answers again), the chat header's machine drop-down can send a chat's tasks to it, and run_agent(machine=<name>, ...) runs a sub-task there with that machine's tools, files and models, its tab nested under the caller's on every surface), /rsi7d (seven-day recursive self-improvement of the bundled SEAs from their logged runs), /merge (resolve a conflicted git merge), /task_update (progress report on a task), /autorouter, /bestrouter, /orautorouter and /orbestrouter (the model routers above), /skillopt (optimize a skill or prompt constant against an evaluation set), /write_paper, /review_paper, and /revise_and_review_paper (write, review, and iterate on a research paper), /write_patent and /review_patent (draft a utility patent application in the 37 CFR 1.77 layout with proofreader gates and a DOCX/PDF build, and review a draft or a published patent like an examiner: prior-art search, eligibility, novelty, obviousness and definiteness claim by claim), /git_extract_knowledge (build and maintain a repository's durable memory), /write (prose for a general audience), /devtools (drive a web app like a developer in DevTools: every click verified with evaluate_script, list_console_messages, list_network_requests and wait_for rather than by re-reading the accessibility tree, the browser tools switched on and your own Chrome reachable through attach_to_chrome or use_my_chrome; /devtools <page or app, what to do or what is broken>), /discover (the AI discovery loop, run in a git repository so that every evaluated idea is a commit held by a ref refs/discover/<task id>/<id> that outlives resets and squash merges: fix a dev / held-out split with its evaluation command and noise floor (the held-out part is off limits from then on, read by nothing but the held-out check), profile the baseline, search the state of the art, write and pairwise-judge ideas, implement the winner on its parent point's commit, evaluate and commit it, and keep a Pareto frontier of the winners against the user's metrics, each point a commit with a parent; an evaluated idea is logged KEPT, TIE or FAILED (TIE and FAILED are never retried, evicted points DOMINATED), and one random number drawn from a Bash call picks the next operator, a variant of a frontier point reasoned from its observations, a composition of two frontier points of different aspects, or fresh ideas from a web search, gpt-6-astra or gemini-4-argon; a frontier point that meets the goal on the dev metrics is checked once on the held-out split, which ends the loop when the held-out metrics meet the goal too and evicts the point as OVERFIT when they do not (the baseline goes back on the frontier if that empties it); the loop also stops at the budget or the round limit, 100 rounds unless the task says otherwise, then resets the working tree to the best frontier point's commit and reports it with its dev and held-out metrics; a task that asks only for research, ideas or a ranking still gets the loop, ranked by measured dev metrics, and one that forbids changing its repository gets it in a lab, a copy of the artifact's current files in <scratch dir>/lab with its own git init and create_worktree, the best point handed back as <scratch dir>/best.patch from git diff --binary for the user to apply; the loop's instructions live in the SEA, appended to its system prompt, not in SYSTEM.md), and /remember and /forget (add or remove a standing instruction in ~/.kiss/AGENTS.md). /write, /review_paper, /revise_and_review_paper, /review_patent and /discover pin the bestrouter model in their settings, so they run on the router whatever the tab's model picker says. The other two are hidden ("hidden": True in settings(), so no slash command): sorcar, the plain sub-agent run_agent runs when agent is empty, and coding, the unattended benchmark harness whose generated per-trial SEAs are loaded by path. List your own SEA folders, one per line, in ~/.kiss/SEAS.md; they are picked up within two seconds. The settings() contract, type checking, hooks, and dispatch flow are in src/kiss/server/README.md and docs/sea-commands.md.

Skills, MCP servers, and customization

  • Agent Skills loaded from ~/.kiss/skills, <project>/.kiss/skills, Claude skill directories, .agents/skills, and the git-ignored src/kiss/agents/claude_skills/ folder of the checkout, where Claude Code plugins can be dropped (<plugin>/skills/<name>/SKILL.md, loaded as <plugin>:<name>); a SKILL.md whose YAML frontmatter does not parse still loads, with no metadata and its first paragraph as the description; no skill ships with the repository itself.
  • MCP server discovery from ~/.kiss/mcp.json, <project>/.kiss/mcp.json, and <project>/.mcp.json. Remote servers that follow the MCP authorization spec (Notion, Linear, Asana, or any URL) are signed into from the chat with the connect_mcp_server tool (zoom is known too but needs a pre-registered app named by KISS_MCP_ZOOM_CLIENT_ID); tokens are stored under ~/.kiss/mcp_auth/. Every edit of those config files (a sign-in saving its server, a removal) holds one lock, ~/.kiss/mcp.lock, instead of leaving a .lock beside a checked-in .mcp.json, and refuses to write over a file that is not valid {"mcpServers": {...}} JSON, so a hand-edited file with a syntax error keeps its other servers; a server's tool list is read page by page (tools/list cursors), and a tool call the user stops is cancelled at once on the session's loop instead of being waited out. A curated catalog of privacy-first MCP connectors ships in connectors/.
  • "Tricks" (inject-instruction snippets) come from your ~/.kiss/MY_INJECTION.md and the bundled src/kiss/INJECTIONS.md; the Inject panel in the chat lists, inserts, edits, and deletes them; a promptlet you add or edit there must not be empty and may not contain a line that is a bare ## or starts with ## (the delimiter the file's sections are split at; ### headings are fine).

Messaging & Third-Party Agents

KISS Sorcar includes 44 third-party agents that act on messaging services, mailboxes, devices, and web services on your behalf. 32 are messaging-channel agents:

BlueBubbles · DingTalk · Discord · Email (IMAP/SMTP) · Feishu · Gmail · Google Chat · Home Assistant · iMessage · IRC · LINE · Matrix · Mattermost · Microsoft Teams · Nextcloud Talk · Nostr · ntfy · Phone Control · QQ · Signal · SimpleX · Slack · SMS · Synology Chat · Telegram · Tlon · Twitch · Webhook · WeCom · WeiXin · WhatsApp · Zalo

Ten more are service agents that give Sorcar authenticated API tools for productivity and data services:

Brave Search (kiss-brave) · Firecrawl (kiss-firecrawl) · GitHub (kiss-github) · Google Calendar (kiss-gcal) · Google Docs (kiss-gdocs) · Google Drive (kiss-gdrive) · Google Sheets (kiss-gsheets) · Notion (kiss-notion) · Overleaf (kiss-overleaf) · PostgreSQL (kiss-postgres)

In a chat task, just say what you want ("send 'running late' to Alice on WhatsApp", "list my open GitHub PRs") and Sorcar dispatches the matching agent through its run_agent tool (run_agent(agent="whatsapp", task=...)): the channel SEA (a class deriving from ChannelSea) runs in $KISS_HOME/channel_work, inheriting nothing from the calling task, with the channel's authenticated tools and its own system-prompt preamble. Each agent also has its own CLI entry point (kiss-slack, kiss-gmail, kiss-whatsapp, ...). Gateway-capable channels also work inbound: a recurring poll tick (ask for "an always-on Telegram gateway" in chat) runs each new message as a Sorcar task, with thread continuity, sender allow-lists, and an optional pairing handshake. Two infrastructure agents round out the set: an A2A agent (kiss-a2a) exposing Sorcar over the agent-to-agent protocol and an OpenAI-compatible server (kiss-oai).

Sign-in. Every service agent carries check_<service>_auth / authenticate_<service> tools, so a task can connect a service on the spot, and the Apps panel of the web app starts such a task when you click a service that is not connected. The six Google Workspace agents go through Composio (Google only lets verified OAuth apps request Workspace scopes, so no Google token is stored locally); GitHub, Microsoft Teams, Slack, and Discord use public OAuth apps with device-code or PKCE flows and no client secret. Every sign-in ends on a page only you may complete, shown in the Browser tab under the daemon; the agent never asks for your password or a 2FA code. On Linux, credentials for the 18 Muse-covered connectors are isolated by default behind a local auth daemon that swaps opaque surrogate tokens for the real ones at the network edge and applies an allow/deny/ask policy with an audit log (python -m kiss.agents.third_party_agents.muse_auth; opt out with KISS_MUSE_AUTH=0).

The complete catalog, credentials, and 26 worked examples are in src/kiss/agents/third_party_agents/README.md.

Models Supported

KISS Sorcar ships a catalog of 996 models across 10 provider categories, with built-in prices (for an image-generation model also the rate of its generated image tokens, image_output_price_per_1M, which the Gemini image models bill well above their text output; a model without one bills image tokens at its text output rate), context lengths, and capability flags (fc function calling, gen generation, emb embedding, dec typed decisions via OpenRouter's /api/alpha/decisions). The source of truth is src/kiss/core/models/MODEL_INFO.json; the per-provider counts and the full model list are in MODELS.md. Models are grouped by the provider that routes them, so the cc/* and codex/* namespaces (Claude Code CLI and Codex CLI) are categories of their own, and the open-weight openai/gpt-oss-* and google/gemma-* models count under Together AI, which serves them.

Cost and budget tracking use the catalog prices, except for openrouter/* models, where the cost OpenRouter reports for each response is billed instead, since the same model id is priced differently per upstream route. The catalog holds each provider's standard rates and, for long prompts, two kinds of tier. An openrouter/* entry can carry prompt_tiers, OpenRouter's own long-prompt prices (107 entries: the Claude Sonnet 4.5 and Haiku 5.5 twins, Gemini 2.5 and 3.1 Pro, GPT-5.4 through GPT-6.1 with their ~openai/*-latest aliases, Grok 4.x and grok-build, Qwen Plus, Flash and Coder Flash, Seed 1.6 and 2.0, Sakana Fugu Ultra): each tier is a min_prompt_tokens threshold with the per-million prices that replace the entry's own once a single request's prompt (input, cache-read, cache-write and audio-input tokens together) is strictly longer than it, a later tier winning per price when several apply (Qwen 3.7 Flash steps at 32K and again at 256K) and an omitted price keeping the base value; the model updater (uv run python -m kiss.scripts.update_models) imports them from each OpenRouter listing's pricing.overrides, skips an override with any condition other than min_prompt_tokens (a time-of-day window, say) so the base price applies to it, and drops a tier list OpenRouter no longer publishes. An entry without tiers falls back to the name-based rule in src/kiss/core/models/model_info.py (_long_context_uplift), which bills the whole request at the family's multiples of the short-context rates: Claude Sonnet 4.5 under its direct and Bedrock names bills a prompt over 200K tokens at twice the input and cache rates and 1.5 times the output rate (the OpenRouter twin's tier holds the same figures, $6/$22.50 and $0.60 cache read), Haiku 5.5 over 100K at five times every rate, GPT-5.4 through GPT-6.1 (not the 5.4 mini and nano) over 272K and Gemini 2.5 and 3.1 Pro over 200K at 2x input and cache and 1.5x output, while Sonnet 4.6 and the other 1M-context Claude models stay at their standard rates. Neither rule is applied to a summed multi-request count, since a sum cannot tell one long request from many short ones; when the response says a differently priced tier served the call, the estimate is scaled by that tier's factor: OpenAI's service_tier (Flex at half the standard rate, Fast at the model's published ratio, Ultrafast at six times; a Fast request downgraded to default bills standard) and Anthropic's usage.speed (fast mode at twice the rate) and usage.inference_geo (US-only inference at 1.1x on Claude 4.6 and later, which a workspace default can select without the request asking for it; the two stack, and Bedrock, whose regional premium the catalog prices already carry, bills at the catalog rate). A cc/* or codex/* turn costs $0 while the subscription switch bills the CLI's plan; with the switch off and an API key paying, the turn is priced at the catalog rate of the model the CLI resolved (cc/opus as claude-opus-5-5, a dated snapshot as its undated entry; a Claude Code turn of several requests bills its summed usage at the short-context rates and adds the long-context premium of each request that crossed the threshold on its own, since the tier is a per-request rule and the CLI reports every request's usage, while the fast-mode and US-inference factors the CLI's stream reports scale the whole turn; an unknown Claude model, or a turn that used more than one model, falls back to the CLI's own numeric total_cost_usd, and an unpriced Codex name such as codex/default stays at $0 with one warning per model instance). Billing is decided once per CLI launch, so flipping the switch mid-turn does not reprice that turn. A Codex turn whose CLI exits non-zero after reporting the turn's usage, or that Stop interrupts after the turn has completed, still bills that turn, once. OpenAI's prompt-cache writes, free before the GPT-5.6 family and unpriced on the -pro variants, bill at 1.25x the input rate for the default 30-minute cache and at 2.5x for the 12-hour one a prompt_cache_options.ttl of 12h in model_config asks for (Sorcar does not ask for it by itself). Anthropic's cache writes bill at 1.25x the input rate for the five-minute cache Sorcar requests and at 2x for the one-hour one; a streamed response whose aggregate cache-write count outgrows the per-lifetime split of its message_start has the excess billed at the five-minute rate when the split shows only five-minute writes, and at the one-hour rate otherwise, so an unknown lifetime is never under-billed. Prompt caching is on for Claude through OpenRouter as well (openrouter/anthropic/... and the openrouter/~anthropic/... aliases get a top-level ephemeral cache_control, unless model_config sets enable_cache to false), so a long conversation re-reads its prefix at the cache rate; on Bedrock, whose API rejects the top-level marker, the adapter instead puts an explicit cache_control on the last non-thinking block of each request's final message (unless caching is off, that message already carries a marker, or the request is at the API's limit of four), moving it forward turn by turn, so the conversation is cached there too, not only the tools and system prompt. A response the adapters reject after the provider has billed it, the usage the provider has already reported for a streamed response that Stop interrupts, or a non-streamed OpenAI-compatible reply that arrives after Stop (the request runs on a worker thread while the task's stop event is watched; Stop shuts the pending socket, one still connecting, in its TLS handshake or going through a proxy mount included, and drops the client so the SDK does not re-send it), still count towards the task's cost and budget, and the task total shown in the UI includes the task classifier's spend, every earlier session of a task continued after a crash, the whole spend of the sub-tasks the task dispatches with run_agent and run_parallel, and the spend of the /ask answers and Task update runs on its tab, including answers given while the task was still setting up. The Budget: figure in the usage line the model sees after every tool result counts the same way, earlier sessions and sub-agents included, so the agent's own view of its spend matches the budget check that stops it.

Contributing

Contributions in the form of issues are welcome. KISS Sorcar should be able to help implement and review them. If you want to send a pull request (PR), please make sure that uv run check --full passes (ruff, mypy, pyright, and the web app's lint when npm is installed and src/kiss/webapp/node_modules exists) and that all Python (uv run pytest) and JavaScript (npm test in src/kiss/webapp) tests pass on macOS and Linux. While you work, uv run impacted-tests lists the test files within two import hops (--depth; 0 for no bound) of the Python files you changed under src/ (uncommitted and untracked files, or the paths you pass; other files are ignored), and --splits N prints instead a JSON list of at most N uv run pytest commands balanced by file size for run_commands_parallel, so you need not run the whole suite after every edit.

License

Apache-2.0. See LICENSE.

Citation

If you use KISS Sorcar in your research, please cite:

@misc{sen2026kisssorcar,
  title         = {KISS Sorcar: A Stupidly-Simple General-Purpose and Software Engineering AI Assistant},
  author        = {Sen, Koushik},
  year          = {2026},
  eprint        = {2604.23822},
  archivePrefix = {arXiv},
  primaryClass  = {cs.SE},
  url           = {https://arxiv.org/abs/2604.23822}
}

Metadata

Release files for kiss-agent-framework 2026.10.26

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kiss-agent-framework 2026.10.26
File Size Uploaded Provenance Actions
kiss_agent_framework-2026.10.26.tar.gz 12.5 MB Without provenance Details for kiss_agent_framework-2026.10.26.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for kiss-agent-framework 2026.10.26
File Interpreter ABI Platform Actions
kiss_agent_framework-2026.10.26-py3-none-any.whl Python 3 none any Details for kiss_agent_framework-2026.10.26-py3-none-any.whl

Total release size: 27.1 MB

Release files / kiss_agent_framework-2026.10.26.tar.gz

Download URL kiss_agent_framework-2026.10.26.tar.gz
Size 12.5 MB
Tags Source
SHA-256 checksum
How to use checksums
4681ed958fd2f12647b9f1e2ca185aebd690dc90c2d476a3ef345366ca42a430
BLAKE2b-256 checksum
How to use checksums
85082ba0b372e51d50cc482303f2d5fe98d1aa90be798b37541a098e020df56c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / kiss_agent_framework-2026.10.26-py3-none-any.whl

Download URL kiss_agent_framework-2026.10.26-py3-none-any.whl
Size 14.6 MB
Tags Python 3
SHA-256 checksum
How to use checksums
23668d1831e576c1f38e576377a86db14506e845ab267e575d51466b13cc3fa7
BLAKE2b-256 checksum
How to use checksums
bf67e1f4b50fa7d17c1fc479d506048e2372db0ec7836bccf0c8e50f152bf227
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for kiss-agent-framework 2026.10.26

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2026.10.26 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page