deskd
An orchestration engine for multi-agent desks. deskd owns the part that is hard and domain-agnostic: knowing which agents are alive, what they're doing, what's queued for them, and — the difficult bit — reliably waking the right agent at the right time and proving the message actually landed.
Your agents do the domain work. deskd does everything else.
Built for headless Claude Code agents (
claude -p+ aPostToolUsehook), but the engine is a plain Python package over SQLite with no hard dependency on any particular agent runtime.
Why
Multi-agent systems usually rot in the same three places:
- Agents poll. Every agent runs its own
sleep/wake loop, burning tokens to discover there's nothing to do — and still missing the thing that mattered. - Messages vanish. "I sent it" ≠ "they read it." Nothing distinguishes notified from read, so a stuck message is invisible until someone notices hours later.
- Nobody knows what's running. Two sessions of the same role stomp each other; a crashed agent looks identical to an idle one.
deskd's position: agents must never manage their own waking. They end their turn and the orchestrator wakes them — on a timer, on a calendar, on a custom watcher, on a message, or on an escalation ladder when a wake doesn't land.
What you get
| Presence | One live session per role, enforced by a role-scoped flock. Heartbeats from the in-session hook; crash-safe (the kernel releases the lock). |
| Unified inbox | Every notification — alerts, signals, system events, meeting messages — lands in one queue per role, with per-key dedup so a re-firing alert never piles up. |
| Wake orchestration | Collect demand → route by presence → record the attempt → verify the loop closed → escalate. A wake that doesn't land climbs: in-session hook → resume → spawn → human (Discord/email) → a red badge on the supervisor console that never times out. |
| Self-service wake hooks | An agent registers its own wakes: --at (one-shot), --every (interval), --cron (calendar, DST-correct), or --probe (your own watcher function — return a dict and it wakes you). |
| Delivery ledger | Per message × recipient: queued → notified → read. Past SLA and unread with nobody reacting = overdue — surfaced red. Rows are a projection of durable messages, so a delivery can't be silently lost. |
| Bounded meetings | Multi-agent meetings with check-in/quorum, mandatory 1:1 replies with an SLA, message budgets, and a mutual termination handshake. Bounded by construction — no infinite agent chatter. |
| Cross-session tasks | Work items that outlive a session. Soft deadlines (due_at) sort to the top but never wake anyone; only priority=urgent does. |
| Session lifecycle | Intraday continuity, cross-day rollover: wind the old session down with a handoff, start fresh the next day. |
| Supervisor console | A web board (live status + queue + hooks + wake activity), a per-agent detail page with full execution history, and a meetings console — behind an access-code or Ed25519 trusted-device gate. |
Install
pip install "deskd[web]" # from PyPI: engine + web console
pip install -e ".[web]" # or from a checkout, for development
Quickstart
Describe your desk in a module that defines configure_deskd():
# myapp/desk.py
from deskd.config import RoleSpec, PromptBuilder, configure
class MyPrompts(PromptBuilder):
def bootstrap(self, role: str) -> str:
return f"Load the myapp skill, declare role={role}, follow its playbook."
def configure_deskd(): # deskd calls this at startup
configure(
roles=(
RoleSpec("researcher", "Researcher", ("research", "review")),
RoleSpec("operator", "Operator", ("execution",), {"can_execute": True}),
),
timezone="America/New_York",
inbox_sources=("alert", "signal", "system", "meeting", "supervisor"),
probe_allowlist=("myapp.watchers",), # empty = no probes may run
prompt_builder=MyPrompts(),
)
Point deskd at it with DESKD_CONFIG_MODULE. Every deskd process — the CLI,
deskd serve, the cron driver — imports that module and calls configure_deskd()
before it touches the engine, so your roles are registered everywhere. Without it
a deskd process starts empty (no roles) and every role-scoped command is rejected.
export DESKD_CONFIG_MODULE=myapp.desk # (myapp must be importable — on PYTHONPATH)
deskd serve # supervisor console on 127.0.0.1:8000
deskd status set --role operator --activity "watching the queue"
deskd inbox enqueue --for operator --source alert --title "threshold crossed" --priority urgent
deskd wake sources --role operator # what can wake me, and how to change it
Wake the desk from cron (the driver is the only thing that spawns sessions):
# cron has its own environment — set both vars on the line (or in the crontab header)
* * * * * DESKD_CONFIG_MODULE=myapp.desk DESKD_WAKE_EXECUTE=1 /path/to/deskd/scripts/cron/wake_orchestrator.sh
It is dry-run by default — schedule it, watch the log, then set
DESKD_WAKE_EXECUTE=1 when the decisions look right.
Agents schedule themselves — declaratively
# a calendar wake (weekday 06:15, in your configured tz)
deskd hook add --for operator --title "daily digest" --cron "15 6 * * *"
# your own watcher algorithm: return a dict -> it wakes you
deskd hook add --for operator --title "queue depth watch" \
--probe myapp.watchers:queue_depth --every 600
# myapp/watchers.py — a probe may observe and notify. Nothing else.
def queue_depth():
n = measure()
if n > 100:
return {"title": f"queue at {n}", "priority": "urgent"}
return None # None = don't wake anyone
Three consecutive probe errors auto-disable the hook and notify its owner — a broken watcher can't rot silently or stall the tick.
Design notes
Headless sessions can't be interrupted mid-turn. So "deliver to the agent"
means two things: while it's running, its PostToolUse hook surfaces the queue
into context; while it's idle, the orchestrator resumes its session with the
queued items as the prompt. "Current session" = a resumable session id, not a
live process.
Storage is SQLite (WAL) and it is the only source of truth. No broker, no daemon holding state. Every tick rebuilds its decisions from the DB, so a crashed orchestrator self-heals on the next tick. SQLite can't wake a dormant process — the engine doesn't pretend otherwise; it makes every wake attempt an auditable row with a closed loop and an escalation path.
Nothing here executes your domain. The engine wakes agents and delivers notifications. It never acts as an agent, and it has no path to your side-effecting systems.
Security
- The supervisor is not an agent role: agent APIs reject it, and supervisor actions only enter through the authenticated web adapter.
simplemode = an access code (convenience, trusted host).signedmode = short-lived Ed25519 assertions from a trusted device; the public key path is fixed at/etc/deskd/supervisor_ed25519.pub, must be root-owned, and is deliberately not environment-overridable — an agent must not be able to point verification at a key it wrote. Keep the private key off the host.- Never hardcode the access code into a client/static file. A pre-filled credential in page source is the credential. (Ask us how we know.)
- Probes only import from your explicit
probe_allowlist. Empty = deny all.
See docs/security.md.
Docs
docs/design.md— architecture and the decisions behind itdocs/roadmap.md— where this is going, in dependency orderskills/agent-orchestration/— a skill teaching an agent to operate and evolve a deskd desk
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deskd-0.1.1.tar.gz.
File metadata
- Download URL: deskd-0.1.1.tar.gz
- Upload date:
- Size: 155.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd46212d2eb8fe84549010e67d3bdbf0dfc1d3dc52e1e991fdf470d9f919c1f7
|
|
| MD5 |
8fbf3ac9c6d3704eb0c9f7c18a2843a7
|
|
| BLAKE2b-256 |
8d96b4498236094ea2b84e30adebafe4fc1a31fa8aad6f457d12c6e019c6c0bd
|
Provenance
The following attestation bundles were made for deskd-0.1.1.tar.gz:
Publisher:
release.yml on hongdp/deskd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deskd-0.1.1.tar.gz -
Subject digest:
bd46212d2eb8fe84549010e67d3bdbf0dfc1d3dc52e1e991fdf470d9f919c1f7 - Sigstore transparency entry: 2202982784
- Sigstore integration time:
-
Permalink:
hongdp/deskd@050f9b13b074fa09fd55c2fd798f4999ffc3ab80 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/hongdp
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@050f9b13b074fa09fd55c2fd798f4999ffc3ab80 -
Trigger Event:
release
-
Statement type:
File details
Details for the file deskd-0.1.1-py3-none-any.whl.
File metadata
- Download URL: deskd-0.1.1-py3-none-any.whl
- Upload date:
- Size: 129.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04d253134ad8d2b32a49c7182e94b42f2b7f73ac7c1ec3712677658530b83289
|
|
| MD5 |
efd282f4b169ebe60f06d74531c3b9a2
|
|
| BLAKE2b-256 |
e157ddcbced24e9b7a5226c32a4c21ee913886dccf63335303c8fefba6a958a9
|
Provenance
The following attestation bundles were made for deskd-0.1.1-py3-none-any.whl:
Publisher:
release.yml on hongdp/deskd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deskd-0.1.1-py3-none-any.whl -
Subject digest:
04d253134ad8d2b32a49c7182e94b42f2b7f73ac7c1ec3712677658530b83289 - Sigstore transparency entry: 2202982834
- Sigstore integration time:
-
Permalink:
hongdp/deskd@050f9b13b074fa09fd55c2fd798f4999ffc3ab80 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/hongdp
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@050f9b13b074fa09fd55c2fd798f4999ffc3ab80 -
Trigger Event:
release
-
Statement type: