An architecture walk-through of src/kiss/agents/third_party_agents/ — backends, carriers, the module-as-tools-file contract, the in-process kiss-web daemon, and the three ways channel agents run.
The third_party_agents package connects KISS Sorcar to 23 external messaging and communication platforms: Slack, Gmail, Discord, Telegram, WhatsApp, IRC, Matrix, Signal, SMS, iMessage, BlueBubbles, Google Chat, MS Teams, Mattermost, Feishu, LINE, Zalo, Twitch, Nostr, Tlon, Nextcloud Talk, Synology Chat, and phone control (plus one outlier, govee.py, a standalone smart-light CLI).
The most important design fact — the result of a recent refactor — is that none of these classes is an executable agent. Every task is submitted to the kiss-web daemon through the public API kiss.server.sorcar.run(), and the daemon builds and runs its own chat agent. The channel classes are thin carriers of three things:
SorcarAgent inheritance removed an entire parallel execution path (and ~700 lines of redundant code) while keeping one uniform behavior: everything runs on the daemon, with live web streaming, follow-ups, stop support, and chat persistence for free.
Whatever entry point you use (interactive CLI, poll mode, cron poller, or a direct agent.run(...) call in Python), the flow converges on a single function, run_agent_via_kiss_web(), which passes the agent's OWN module file as the API's tools= path and submits the prompt to an in-process kiss-web daemon over a private Unix socket. The daemon imports that module and calls its top-level get_tools(), which builds a fresh agent from the credentials persisted under ~/.kiss.
get_tools(). Results (YAML summary, cost, tokens, steps, chat id) are written back onto the carrier.~/.kiss, so the fresh instance authenticates identically. Because nothing lives in process memory, the daemon no longer has to share the caller's process — the launcher keeps the in-process daemon only so channel agents work without an externally started kiss-web. The active workspace travels through the KISS_CHANNEL_WORKSPACE environment variable, set by the launcher for the duration of the task.
All 23 modules are stamped from the same template: an API-wrapper backend plus a thin agent subclass. Learning one platform means learning them all.
ToolMethodBackend and automatic tool discovery
get_tool_methods() reflects over the backend and returns every public method that is not in the _NON_TOOL_METHODS frozenset. Adding a new LLM tool to a platform is therefore just adding a public method — no registration code. The infrastructure methods (connect, poll_messages, send_message, …) form the protocol that framework code — ChannelRunner and channel_main() — calls; the LLM never sees them.
check_x_auth() — validates the stored credential against the live API (e.g. Slack's auth.test).authenticate_x(credential) — validates then saves the token (chmod 600).clear_x_auth() — deletes the stored credential.start_x_browser_auth() — returns instructions telling the daemon agent to open the provider's developer console with its own browser tools, create an app, add scopes, install it, and copy the token — autonomously, never asking the user to do it manually (the user is asked only for human-required login screens).
BaseChannelAgent.run(prompt_template, **kwargs) filters its kwargs through the LAUNCH_KWARG_NAMES allowlist (model_name, work_dir, max_budget, tools, use_worktree, model_config, web_tools, is_parallel, timeout, sock_path) and delegates to run_agent_via_kiss_web(), which performs the sequence below.
get_tools(); the caller's carrier only names the module and receives results. The workspace env var is restored when the task ends.
The public API sorcar.run() accepts extra tools only as a file path to a Python module whose top-level get_tools() returns the tool callables. For channel agents that file is simply the agent's own module — no bridge, registry, wrapper, or generated file exists:
x_agent.py defines a module-level get_tools() that instantiates a fresh XAgent() and returns agent._get_tools() (the 4 auth tools, plus the backend tool methods when the stored credential authenticates). Slack's variant reads the KISS_CHANNEL_WORKSPACE environment variable to pick the workspace.agent.tools_file — the path of the module defining the agent's class — as sorcar.run(tools=...). The daemon compiles and executes the module's current source and calls its get_tools(); the returned callables become agent tools as-is, with their original names, signatures, and docstrings.~/.kiss/third_party_agents/<service>/, so the fresh daemon-side agent authenticates exactly like the caller's carrier. A token saved by authenticate_x(...) during one task is picked up by the next task's fresh get_tools().# every agent module ends with (slack shown; others take no workspace)
def get_tools() -> list:
"""Return the Slack channel tools (kiss.server.sorcar.run tools-file contract)."""
workspace = os.environ.get("KISS_CHANNEL_WORKSPACE", "default") or "default"
return SlackAgent(workspace=workspace)._get_tools()
get_tools() the daemon calls, carries the workspace, and receives the results — while the daemon-built agent talks to the platform through an identically-authenticated fresh backend.
| Mode | Entry point | Who drives it | Typical use |
|---|---|---|---|
| Interactive CLI | kiss-slack -t "Send 'Hello!' to #general" → each module's main() is a one-liner: channel_main(SlackAgent, "kiss-slack", ...) |
channel_main() instantiates the agent (forwarding --workspace if the constructor accepts it), builds run kwargs via _channel_cli.py, launches, prints run stats |
One-off tasks with live webview streaming |
| One-shot poll mode | kiss-slack --channel CH [--allow-users u1,u2] |
ChannelRunner.run_once() using the backend's infrastructure methods (Fig. 4) |
Answer pending channel messages once (e.g. from cron) |
| Cron pollers (Slack only) | slack_sorcar_poller.py (DMs), slack_channel_sorcar_poller.py (default #sorcar) |
Poller scripts with their own slack_sdk client, using KissWebChatAgent for chat identity |
Slack as a persistent chat UI: each thread = a resumable Sorcar conversation |
ChannelRunner is generic — it works for all 23 platforms because it speaks only the infrastructure-method protocol. channel_main() builds it from the module's _make_backend() factory and resolves --allow-users names via find_user() first.
ChannelRunner.run_once(): connect → join → poll → filter → one daemon task per pending message → threaded reply → disconnect.
Two sibling scripts turn Slack into a persistent front-end for Sorcar. Unlike ChannelRunner they keep conversation identity: they use KissWebChatAgent, the one carrier subclass that adds a chat-id surface (chat_id, new_chat(), resume_chat_by_id()).
scripts/kiss-slack-sorcar-cron.sh fires every minute, takes an exclusive fcntl lock (overlapping ticks exit instantly), then loops with a 3 s poll for 57 s. The wrapper also resolves the newest installed extension venv and exports the login $SHELL so API keys load in cron's bare environment.:rotating_light: alert to the channel and a recovery notice when fixed.KISS_SLACK_WORKSPACE, KISS_SLACK_USER, KISS_SLACK_CHANNEL, KISS_SLACK_MODEL, KISS_SLACK_BUDGET (default $5/task).| File | Role |
|---|---|
_channel_agent_utils.py | The core: ToolMethodBackend (+ _NON_TOOL_METHODS and tool discovery), BaseChannelAgent (carrier: _get_tools(), allowlist-filtered run(), result attributes), ChannelConfig (chmod-600 JSON credentials under ~/.kiss/third_party_agents/<service>/, lazily rebased on $KISS_HOME), ChannelRunner, channel_main(), and the JSON-config helpers. |
_kiss_web_launcher.py | _ensure_api_server() (lazy in-process daemon on a private 0600 Unix socket, dedicated asyncio thread), KissWebChatAgent (chat-id carrier), run_agent_via_kiss_web() (the funnel of Fig. 3). |
_channel_cli.py | Shared argparse builder (-m model, -b budget, -w workdir, -t task / -f file, -e endpoint), budget parsing, launch work-dir resolution, run-stats printing. |
_backend_utils.py | Helpers for webhook-style backends: ThreadedHTTPServer, drain_queue_messages(), stop_http_server(), is_headless_environment(). |
<platform>_agent.py × 23 | One module per platform, each stamped from the Fig. 2 template: XChannelBackend + XAgent + _make_backend() factory + one-liner main() → channel_main(XAgent, "kiss-x"). |
slack_sorcar_poller.py, slack_channel_sorcar_poller.py | Cron pollers (Section 8). They use their own slack_sdk.WebClient helpers rather than SlackChannelBackend. |
govee.py | The outlier: standalone Govee smart-light CLI (list/on/off/brightness/color/kelvin via the Govee Developer API, keyed by $GOVEE_API_KEY) — not a channel agent. |
{"ok": false, "error": ...}, never as raised exceptions at the model.~/.kiss/third_party_agents/<service>/ with chmod 600; Slack adds a per-workspace subdirectory.ChannelRunner/channel_main() need, so a new platform is: wrap the API, override what's non-trivial, add 4 auth tools, write a one-liner main().
Each of the 23 modules is a ToolMethodBackend API wrapper whose public methods auto-become LLM tools, plus a BaseChannelAgent carrier with four auth tools and a channel_system_prompt, plus a module-level get_tools() that rebuilds those tools from persisted credentials. Nothing executes locally: run_agent_via_kiss_web() passes the agent module's own path as the tools= file of kiss.server.sorcar.run() and submits every task to an in-process kiss-web daemon. The daemon's own agent — standard tools plus the channel tools from the module's get_tools() — does the work, and results plus chat identity are written back onto the carrier. A uniform CLI (channel_main), a generic poll loop (ChannelRunner), and the Slack cron pollers layer interactive tasks, one-shot message answering, and persistent thread↔chat conversations on top of the same funnel.