deskd 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deskd-0.1.0/.env.example +63 -0
- deskd-0.1.0/.gitignore +28 -0
- deskd-0.1.0/LICENSE +21 -0
- deskd-0.1.0/PKG-INFO +193 -0
- deskd-0.1.0/README.md +165 -0
- deskd-0.1.0/docs/design.md +173 -0
- deskd-0.1.0/docs/roadmap.md +283 -0
- deskd-0.1.0/docs/security.md +75 -0
- deskd-0.1.0/pyproject.toml +71 -0
- deskd-0.1.0/scripts/cron/wake_orchestrator.sh +219 -0
- deskd-0.1.0/scripts/session_hook.py +385 -0
- deskd-0.1.0/skills/agent-orchestration/KNOWLEDGE.md +44 -0
- deskd-0.1.0/skills/agent-orchestration/SKILL.md +136 -0
- deskd-0.1.0/skills/agent-orchestration/references/EVOLUTION_PROTOCOL.md +47 -0
- deskd-0.1.0/skills/agent-orchestration/references/MEETING_PROTOCOL.md +252 -0
- deskd-0.1.0/skills/agent-orchestration/references/PLAYBOOK_PATTERN.md +52 -0
- deskd-0.1.0/skills/agent-orchestration/references/SESSION_LIFECYCLE.md +68 -0
- deskd-0.1.0/skills/agent-orchestration/references/SUPERVISOR_IDENTITY.md +114 -0
- deskd-0.1.0/skills/agent-orchestration/references/WAKE_ORCHESTRATION.md +120 -0
- deskd-0.1.0/skills/agent-orchestration/scripts/knowledge_txn.py +172 -0
- deskd-0.1.0/src/deskd/__init__.py +98 -0
- deskd-0.1.0/src/deskd/__main__.py +12 -0
- deskd-0.1.0/src/deskd/auth.py +668 -0
- deskd-0.1.0/src/deskd/cli.py +616 -0
- deskd-0.1.0/src/deskd/config.py +242 -0
- deskd-0.1.0/src/deskd/mailbox.py +971 -0
- deskd-0.1.0/src/deskd/meetings.py +2163 -0
- deskd-0.1.0/src/deskd/orchestration.py +2439 -0
- deskd-0.1.0/src/deskd/web/__init__.py +16 -0
- deskd-0.1.0/src/deskd/web/app.py +224 -0
- deskd-0.1.0/src/deskd/web/static/agent.html +147 -0
- deskd-0.1.0/src/deskd/web/static/board.html +242 -0
- deskd-0.1.0/src/deskd/web/static/meetings.html +175 -0
deskd-0.1.0/.env.example
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# deskd configuration — copy to .env and fill in. NEVER commit .env.
|
|
2
|
+
|
|
3
|
+
# Coordination DB (SQLite, WAL). Default: ./data/deskd.db
|
|
4
|
+
DESKD_DB=
|
|
5
|
+
# Base dir used to derive the default DB path. Default: the process cwd.
|
|
6
|
+
DESKD_HOME=
|
|
7
|
+
|
|
8
|
+
# --- supervisor console auth -------------------------------------------------
|
|
9
|
+
# simple | signed | hybrid.
|
|
10
|
+
# simple — one shared access code over a trusted local network. Convenient,
|
|
11
|
+
# and only as strong as the code: it proves someone knows a secret,
|
|
12
|
+
# not who acted.
|
|
13
|
+
# signed — short-lived Ed25519 assertions signed on a trusted device. The
|
|
14
|
+
# public key is read from /etc/deskd/supervisor_ed25519.pub, which
|
|
15
|
+
# must be root-owned; that path is fixed and deliberately NOT
|
|
16
|
+
# settable here, because an agent that could repoint it could mint
|
|
17
|
+
# its own identity. Keep the private key OFF this host.
|
|
18
|
+
# hybrid — both accepted.
|
|
19
|
+
# NOTE: the default (simple) leaves the signed path disabled.
|
|
20
|
+
DESKD_SUPERVISOR_AUTH_MODE=simple
|
|
21
|
+
|
|
22
|
+
# simple mode: the access code. Generate a strong one, e.g.
|
|
23
|
+
# python3 -c "import secrets;print(secrets.token_urlsafe(18))"
|
|
24
|
+
# If unset, the server generates one at startup and prints it to its terminal.
|
|
25
|
+
# NEVER hardcode this into any client/static file — that publishes the credential.
|
|
26
|
+
DESKD_SUPERVISOR_ACCESS_CODE=
|
|
27
|
+
|
|
28
|
+
# --- web console -------------------------------------------------------------
|
|
29
|
+
# `deskd serve` binds loopback by default: the console carries the supervisor
|
|
30
|
+
# adapter, so exposing it on a network must be a deliberate act.
|
|
31
|
+
DESKD_HOST=127.0.0.1
|
|
32
|
+
DESKD_PORT=8000
|
|
33
|
+
|
|
34
|
+
# --- escalation channels -----------------------------------------------------
|
|
35
|
+
# deskd ships NO network code and no channels of its own: an escalation always
|
|
36
|
+
# lands in the `outbox` (a ledger row the console renders), and a host registers
|
|
37
|
+
# anything richer at startup:
|
|
38
|
+
#
|
|
39
|
+
# from deskd.meetings import CallableChannel, register_channel
|
|
40
|
+
# register_channel(CallableChannel(
|
|
41
|
+
# "discord", send=lambda subject, text: post_webhook(text),
|
|
42
|
+
# available=lambda: bool(os.environ.get("MYAPP_DISCORD_WEBHOOK")),
|
|
43
|
+
# ))
|
|
44
|
+
#
|
|
45
|
+
# Credentials for those channels are the host's business and belong in the
|
|
46
|
+
# host's own environment, not here.
|
|
47
|
+
|
|
48
|
+
# --- wake driver (scripts/cron/wake_orchestrator.sh) -------------------------
|
|
49
|
+
# The driver is DRY-RUN unless this is exactly 1. Watch logs/wake.log first.
|
|
50
|
+
DESKD_WAKE_EXECUTE=0
|
|
51
|
+
# DESKD_AGENT_CMD=claude
|
|
52
|
+
# DESKD_WAKE_MODEL=claude-opus-4-8
|
|
53
|
+
# DESKD_WAKE_TIMEOUT=1800
|
|
54
|
+
# Tools a woken session may use. Append your host's own tools here — the engine
|
|
55
|
+
# grants nothing by itself.
|
|
56
|
+
# DESKD_WAKE_ALLOWED_TOOLS=Bash,Read,Write,Edit,Glob,Grep,Skill,ToolSearch,TodoWrite,WebSearch,WebFetch
|
|
57
|
+
# DESKD_PYTHON= # interpreter; defaults to ./.venv/bin/python, else python3
|
|
58
|
+
# DESKD_BIN= # deskd CLI; defaults to ./.venv/bin/deskd, else `deskd` on PATH
|
|
59
|
+
# DESKD_LOG_DIR=
|
|
60
|
+
|
|
61
|
+
# Exported by the driver into a spawned/resumed session, and read by the
|
|
62
|
+
# PostToolUse hook (scripts/session_hook.py) to know which role it is serving.
|
|
63
|
+
# DESKD_ROLE=
|
deskd-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# --- data / secrets: NEVER commit ---
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
*.db
|
|
6
|
+
*.db-wal
|
|
7
|
+
*.db-shm
|
|
8
|
+
data/
|
|
9
|
+
logs/
|
|
10
|
+
*.log
|
|
11
|
+
|
|
12
|
+
# --- python ---
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
build/
|
|
18
|
+
dist/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
|
|
24
|
+
# --- os / editor ---
|
|
25
|
+
.DS_Store
|
|
26
|
+
*.swp
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
deskd-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hongdp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
deskd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deskd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A domain-agnostic orchestration engine for multi-agent desks: presence, a unified inbox, bounded meetings, and a wake ladder that proves delivery.
|
|
5
|
+
Project-URL: Homepage, https://github.com/hongdp/deskd
|
|
6
|
+
Project-URL: Documentation, https://github.com/hongdp/deskd/blob/main/docs/design.md
|
|
7
|
+
Project-URL: Source, https://github.com/hongdp/deskd
|
|
8
|
+
Author: deskd contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,multi-agent,orchestration,scheduling,sqlite
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
19
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: cryptography>=41
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
24
|
+
Provides-Extra: web
|
|
25
|
+
Requires-Dist: fastapi>=0.110; extra == 'web'
|
|
26
|
+
Requires-Dist: uvicorn[standard]>=0.27; extra == 'web'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# deskd
|
|
30
|
+
|
|
31
|
+
**An orchestration engine for multi-agent desks.** deskd owns the part that is
|
|
32
|
+
hard and domain-agnostic: knowing which agents are alive, what they're doing,
|
|
33
|
+
what's queued for them, and — the difficult bit — **reliably waking the right
|
|
34
|
+
agent at the right time and proving the message actually landed.**
|
|
35
|
+
|
|
36
|
+
Your agents do the domain work. deskd does everything else.
|
|
37
|
+
|
|
38
|
+
> Built for headless [Claude Code](https://claude.com/claude-code) agents
|
|
39
|
+
> (`claude -p` + a `PostToolUse` hook), but the engine is a plain Python package
|
|
40
|
+
> over SQLite with no hard dependency on any particular agent runtime.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Why
|
|
45
|
+
|
|
46
|
+
Multi-agent systems usually rot in the same three places:
|
|
47
|
+
|
|
48
|
+
1. **Agents poll.** Every agent runs its own `sleep`/wake loop, burning tokens
|
|
49
|
+
to discover there's nothing to do — and still missing the thing that mattered.
|
|
50
|
+
2. **Messages vanish.** "I sent it" ≠ "they read it." Nothing distinguishes
|
|
51
|
+
*notified* from *read*, so a stuck message is invisible until someone notices
|
|
52
|
+
hours later.
|
|
53
|
+
3. **Nobody knows what's running.** Two sessions of the same role stomp each
|
|
54
|
+
other; a crashed agent looks identical to an idle one.
|
|
55
|
+
|
|
56
|
+
deskd's position: **agents must never manage their own waking.** They end their
|
|
57
|
+
turn and the orchestrator wakes them — on a timer, on a calendar, on a custom
|
|
58
|
+
watcher, on a message, or on an escalation ladder when a wake doesn't land.
|
|
59
|
+
|
|
60
|
+
## What you get
|
|
61
|
+
|
|
62
|
+
| | |
|
|
63
|
+
|---|---|
|
|
64
|
+
| **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). |
|
|
65
|
+
| **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. |
|
|
66
|
+
| **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. |
|
|
67
|
+
| **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). |
|
|
68
|
+
| **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. |
|
|
69
|
+
| **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. |
|
|
70
|
+
| **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. |
|
|
71
|
+
| **Session lifecycle** | Intraday continuity, cross-day rollover: wind the old session down with a handoff, start fresh the next day. |
|
|
72
|
+
| **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. |
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install -e ".[web]" # engine + web console
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
Describe your desk in a module that defines `configure_deskd()`:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# myapp/desk.py
|
|
86
|
+
from deskd.config import RoleSpec, PromptBuilder, configure
|
|
87
|
+
|
|
88
|
+
class MyPrompts(PromptBuilder):
|
|
89
|
+
def bootstrap(self, role: str) -> str:
|
|
90
|
+
return f"Load the myapp skill, declare role={role}, follow its playbook."
|
|
91
|
+
|
|
92
|
+
def configure_deskd(): # deskd calls this at startup
|
|
93
|
+
configure(
|
|
94
|
+
roles=(
|
|
95
|
+
RoleSpec("researcher", "Researcher", ("research", "review")),
|
|
96
|
+
RoleSpec("operator", "Operator", ("execution",), {"can_execute": True}),
|
|
97
|
+
),
|
|
98
|
+
timezone="America/New_York",
|
|
99
|
+
inbox_sources=("alert", "signal", "system", "meeting", "supervisor"),
|
|
100
|
+
probe_allowlist=("myapp.watchers",), # empty = no probes may run
|
|
101
|
+
prompt_builder=MyPrompts(),
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Point deskd at it with **`DESKD_CONFIG_MODULE`**. Every deskd process — the CLI,
|
|
106
|
+
`deskd serve`, the cron driver — imports that module and calls `configure_deskd()`
|
|
107
|
+
before it touches the engine, so your roles are registered everywhere. Without it
|
|
108
|
+
a deskd process starts empty (no roles) and every role-scoped command is rejected.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export DESKD_CONFIG_MODULE=myapp.desk # (myapp must be importable — on PYTHONPATH)
|
|
112
|
+
|
|
113
|
+
deskd serve # supervisor console on 127.0.0.1:8000
|
|
114
|
+
deskd status set --role operator --activity "watching the queue"
|
|
115
|
+
deskd inbox enqueue --for operator --source alert --title "threshold crossed" --priority urgent
|
|
116
|
+
deskd wake sources --role operator # what can wake me, and how to change it
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Wake the desk from cron (the driver is the **only** thing that spawns sessions):
|
|
120
|
+
|
|
121
|
+
```cron
|
|
122
|
+
# cron has its own environment — set both vars on the line (or in the crontab header)
|
|
123
|
+
* * * * * DESKD_CONFIG_MODULE=myapp.desk DESKD_WAKE_EXECUTE=1 /path/to/deskd/scripts/cron/wake_orchestrator.sh
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
It is **dry-run by default** — schedule it, watch the log, then set
|
|
127
|
+
`DESKD_WAKE_EXECUTE=1` when the decisions look right.
|
|
128
|
+
|
|
129
|
+
### Agents schedule themselves — declaratively
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# a calendar wake (weekday 06:15, in your configured tz)
|
|
133
|
+
deskd hook add --for operator --title "daily digest" --cron "15 6 * * *"
|
|
134
|
+
|
|
135
|
+
# your own watcher algorithm: return a dict -> it wakes you
|
|
136
|
+
deskd hook add --for operator --title "queue depth watch" \
|
|
137
|
+
--probe myapp.watchers:queue_depth --every 600
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# myapp/watchers.py — a probe may observe and notify. Nothing else.
|
|
142
|
+
def queue_depth():
|
|
143
|
+
n = measure()
|
|
144
|
+
if n > 100:
|
|
145
|
+
return {"title": f"queue at {n}", "priority": "urgent"}
|
|
146
|
+
return None # None = don't wake anyone
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Three consecutive probe errors auto-disable the hook and notify its owner — a
|
|
150
|
+
broken watcher can't rot silently or stall the tick.
|
|
151
|
+
|
|
152
|
+
## Design notes
|
|
153
|
+
|
|
154
|
+
**Headless sessions can't be interrupted mid-turn.** So "deliver to the agent"
|
|
155
|
+
means two things: while it's running, its `PostToolUse` hook surfaces the queue
|
|
156
|
+
into context; while it's idle, the orchestrator resumes its session with the
|
|
157
|
+
queued items as the prompt. "Current session" = a *resumable session id*, not a
|
|
158
|
+
live process.
|
|
159
|
+
|
|
160
|
+
**Storage is SQLite (WAL) and it is the only source of truth.** No broker, no
|
|
161
|
+
daemon holding state. Every tick rebuilds its decisions from the DB, so a
|
|
162
|
+
crashed orchestrator self-heals on the next tick. SQLite can't wake a dormant
|
|
163
|
+
process — the engine doesn't pretend otherwise; it makes every wake attempt an
|
|
164
|
+
auditable row with a closed loop and an escalation path.
|
|
165
|
+
|
|
166
|
+
**Nothing here executes your domain.** The engine wakes agents and delivers
|
|
167
|
+
notifications. It never acts *as* an agent, and it has no path to your
|
|
168
|
+
side-effecting systems.
|
|
169
|
+
|
|
170
|
+
## Security
|
|
171
|
+
|
|
172
|
+
- The supervisor is **not** an agent role: agent APIs reject it, and supervisor
|
|
173
|
+
actions only enter through the authenticated web adapter.
|
|
174
|
+
- `simple` mode = an access code (convenience, trusted host). `signed` mode =
|
|
175
|
+
short-lived Ed25519 assertions from a trusted device; the public key path is
|
|
176
|
+
fixed at `/etc/deskd/supervisor_ed25519.pub`, must be root-owned, and is
|
|
177
|
+
deliberately **not** environment-overridable — an agent must not be able to
|
|
178
|
+
point verification at a key it wrote. Keep the private key off the host.
|
|
179
|
+
- **Never hardcode the access code into a client/static file.** A pre-filled
|
|
180
|
+
credential in page source *is* the credential. (Ask us how we know.)
|
|
181
|
+
- Probes only import from your explicit `probe_allowlist`. Empty = deny all.
|
|
182
|
+
|
|
183
|
+
See [`docs/security.md`](docs/security.md).
|
|
184
|
+
|
|
185
|
+
## Docs
|
|
186
|
+
|
|
187
|
+
- [`docs/design.md`](docs/design.md) — architecture and the decisions behind it
|
|
188
|
+
- [`docs/roadmap.md`](docs/roadmap.md) — where this is going, in dependency order
|
|
189
|
+
- [`skills/agent-orchestration/`](skills/agent-orchestration/) — a skill teaching an agent to operate and evolve a deskd desk
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
deskd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# deskd
|
|
2
|
+
|
|
3
|
+
**An orchestration engine for multi-agent desks.** deskd owns the part that is
|
|
4
|
+
hard and domain-agnostic: knowing which agents are alive, what they're doing,
|
|
5
|
+
what's queued for them, and — the difficult bit — **reliably waking the right
|
|
6
|
+
agent at the right time and proving the message actually landed.**
|
|
7
|
+
|
|
8
|
+
Your agents do the domain work. deskd does everything else.
|
|
9
|
+
|
|
10
|
+
> Built for headless [Claude Code](https://claude.com/claude-code) agents
|
|
11
|
+
> (`claude -p` + a `PostToolUse` hook), but the engine is a plain Python package
|
|
12
|
+
> over SQLite with no hard dependency on any particular agent runtime.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
Multi-agent systems usually rot in the same three places:
|
|
19
|
+
|
|
20
|
+
1. **Agents poll.** Every agent runs its own `sleep`/wake loop, burning tokens
|
|
21
|
+
to discover there's nothing to do — and still missing the thing that mattered.
|
|
22
|
+
2. **Messages vanish.** "I sent it" ≠ "they read it." Nothing distinguishes
|
|
23
|
+
*notified* from *read*, so a stuck message is invisible until someone notices
|
|
24
|
+
hours later.
|
|
25
|
+
3. **Nobody knows what's running.** Two sessions of the same role stomp each
|
|
26
|
+
other; a crashed agent looks identical to an idle one.
|
|
27
|
+
|
|
28
|
+
deskd's position: **agents must never manage their own waking.** They end their
|
|
29
|
+
turn and the orchestrator wakes them — on a timer, on a calendar, on a custom
|
|
30
|
+
watcher, on a message, or on an escalation ladder when a wake doesn't land.
|
|
31
|
+
|
|
32
|
+
## What you get
|
|
33
|
+
|
|
34
|
+
| | |
|
|
35
|
+
|---|---|
|
|
36
|
+
| **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). |
|
|
37
|
+
| **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. |
|
|
38
|
+
| **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. |
|
|
39
|
+
| **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). |
|
|
40
|
+
| **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. |
|
|
41
|
+
| **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. |
|
|
42
|
+
| **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. |
|
|
43
|
+
| **Session lifecycle** | Intraday continuity, cross-day rollover: wind the old session down with a handoff, start fresh the next day. |
|
|
44
|
+
| **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. |
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e ".[web]" # engine + web console
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
Describe your desk in a module that defines `configure_deskd()`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# myapp/desk.py
|
|
58
|
+
from deskd.config import RoleSpec, PromptBuilder, configure
|
|
59
|
+
|
|
60
|
+
class MyPrompts(PromptBuilder):
|
|
61
|
+
def bootstrap(self, role: str) -> str:
|
|
62
|
+
return f"Load the myapp skill, declare role={role}, follow its playbook."
|
|
63
|
+
|
|
64
|
+
def configure_deskd(): # deskd calls this at startup
|
|
65
|
+
configure(
|
|
66
|
+
roles=(
|
|
67
|
+
RoleSpec("researcher", "Researcher", ("research", "review")),
|
|
68
|
+
RoleSpec("operator", "Operator", ("execution",), {"can_execute": True}),
|
|
69
|
+
),
|
|
70
|
+
timezone="America/New_York",
|
|
71
|
+
inbox_sources=("alert", "signal", "system", "meeting", "supervisor"),
|
|
72
|
+
probe_allowlist=("myapp.watchers",), # empty = no probes may run
|
|
73
|
+
prompt_builder=MyPrompts(),
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Point deskd at it with **`DESKD_CONFIG_MODULE`**. Every deskd process — the CLI,
|
|
78
|
+
`deskd serve`, the cron driver — imports that module and calls `configure_deskd()`
|
|
79
|
+
before it touches the engine, so your roles are registered everywhere. Without it
|
|
80
|
+
a deskd process starts empty (no roles) and every role-scoped command is rejected.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export DESKD_CONFIG_MODULE=myapp.desk # (myapp must be importable — on PYTHONPATH)
|
|
84
|
+
|
|
85
|
+
deskd serve # supervisor console on 127.0.0.1:8000
|
|
86
|
+
deskd status set --role operator --activity "watching the queue"
|
|
87
|
+
deskd inbox enqueue --for operator --source alert --title "threshold crossed" --priority urgent
|
|
88
|
+
deskd wake sources --role operator # what can wake me, and how to change it
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Wake the desk from cron (the driver is the **only** thing that spawns sessions):
|
|
92
|
+
|
|
93
|
+
```cron
|
|
94
|
+
# cron has its own environment — set both vars on the line (or in the crontab header)
|
|
95
|
+
* * * * * DESKD_CONFIG_MODULE=myapp.desk DESKD_WAKE_EXECUTE=1 /path/to/deskd/scripts/cron/wake_orchestrator.sh
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
It is **dry-run by default** — schedule it, watch the log, then set
|
|
99
|
+
`DESKD_WAKE_EXECUTE=1` when the decisions look right.
|
|
100
|
+
|
|
101
|
+
### Agents schedule themselves — declaratively
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# a calendar wake (weekday 06:15, in your configured tz)
|
|
105
|
+
deskd hook add --for operator --title "daily digest" --cron "15 6 * * *"
|
|
106
|
+
|
|
107
|
+
# your own watcher algorithm: return a dict -> it wakes you
|
|
108
|
+
deskd hook add --for operator --title "queue depth watch" \
|
|
109
|
+
--probe myapp.watchers:queue_depth --every 600
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# myapp/watchers.py — a probe may observe and notify. Nothing else.
|
|
114
|
+
def queue_depth():
|
|
115
|
+
n = measure()
|
|
116
|
+
if n > 100:
|
|
117
|
+
return {"title": f"queue at {n}", "priority": "urgent"}
|
|
118
|
+
return None # None = don't wake anyone
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Three consecutive probe errors auto-disable the hook and notify its owner — a
|
|
122
|
+
broken watcher can't rot silently or stall the tick.
|
|
123
|
+
|
|
124
|
+
## Design notes
|
|
125
|
+
|
|
126
|
+
**Headless sessions can't be interrupted mid-turn.** So "deliver to the agent"
|
|
127
|
+
means two things: while it's running, its `PostToolUse` hook surfaces the queue
|
|
128
|
+
into context; while it's idle, the orchestrator resumes its session with the
|
|
129
|
+
queued items as the prompt. "Current session" = a *resumable session id*, not a
|
|
130
|
+
live process.
|
|
131
|
+
|
|
132
|
+
**Storage is SQLite (WAL) and it is the only source of truth.** No broker, no
|
|
133
|
+
daemon holding state. Every tick rebuilds its decisions from the DB, so a
|
|
134
|
+
crashed orchestrator self-heals on the next tick. SQLite can't wake a dormant
|
|
135
|
+
process — the engine doesn't pretend otherwise; it makes every wake attempt an
|
|
136
|
+
auditable row with a closed loop and an escalation path.
|
|
137
|
+
|
|
138
|
+
**Nothing here executes your domain.** The engine wakes agents and delivers
|
|
139
|
+
notifications. It never acts *as* an agent, and it has no path to your
|
|
140
|
+
side-effecting systems.
|
|
141
|
+
|
|
142
|
+
## Security
|
|
143
|
+
|
|
144
|
+
- The supervisor is **not** an agent role: agent APIs reject it, and supervisor
|
|
145
|
+
actions only enter through the authenticated web adapter.
|
|
146
|
+
- `simple` mode = an access code (convenience, trusted host). `signed` mode =
|
|
147
|
+
short-lived Ed25519 assertions from a trusted device; the public key path is
|
|
148
|
+
fixed at `/etc/deskd/supervisor_ed25519.pub`, must be root-owned, and is
|
|
149
|
+
deliberately **not** environment-overridable — an agent must not be able to
|
|
150
|
+
point verification at a key it wrote. Keep the private key off the host.
|
|
151
|
+
- **Never hardcode the access code into a client/static file.** A pre-filled
|
|
152
|
+
credential in page source *is* the credential. (Ask us how we know.)
|
|
153
|
+
- Probes only import from your explicit `probe_allowlist`. Empty = deny all.
|
|
154
|
+
|
|
155
|
+
See [`docs/security.md`](docs/security.md).
|
|
156
|
+
|
|
157
|
+
## Docs
|
|
158
|
+
|
|
159
|
+
- [`docs/design.md`](docs/design.md) — architecture and the decisions behind it
|
|
160
|
+
- [`docs/roadmap.md`](docs/roadmap.md) — where this is going, in dependency order
|
|
161
|
+
- [`skills/agent-orchestration/`](skills/agent-orchestration/) — a skill teaching an agent to operate and evolve a deskd desk
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
The decisions behind deskd, and why they're the way they are.
|
|
4
|
+
|
|
5
|
+
## The constraint everything follows from
|
|
6
|
+
|
|
7
|
+
A headless agent session **runs one turn and exits**. You cannot inject a prompt
|
|
8
|
+
into a turn that is already running. Everything below is a consequence.
|
|
9
|
+
|
|
10
|
+
- "Deliver to the agent" = two mechanisms: the in-session hook surfaces items
|
|
11
|
+
into a *running* turn's context; the orchestrator **resumes the session id**
|
|
12
|
+
when it's idle.
|
|
13
|
+
- "The agent's current session" = a **resumable session id**, not a live process.
|
|
14
|
+
Between wakes there is no process. That is normal.
|
|
15
|
+
- Minimum latency to a busy agent = its current sub-task, because the hook can
|
|
16
|
+
only surface; the agent acts at its next natural boundary. The alternative —
|
|
17
|
+
interrupting mid-work — is worse.
|
|
18
|
+
|
|
19
|
+
## Storage: SQLite (WAL), and it is the only truth
|
|
20
|
+
|
|
21
|
+
No broker, no daemon holding state in memory. Every tick rebuilds all decisions
|
|
22
|
+
from the database, so a crashed orchestrator self-heals on the next tick.
|
|
23
|
+
|
|
24
|
+
SQLite cannot wake a dormant process. deskd doesn't pretend otherwise: instead of
|
|
25
|
+
faking push, it makes every wake attempt an **auditable row** with a closed loop
|
|
26
|
+
and an escalation path. Honest pull with verification beats fake push.
|
|
27
|
+
|
|
28
|
+
## Waking
|
|
29
|
+
|
|
30
|
+
**Six demand sources** feed one queue: meeting wakes, stuck deliveries, urgent
|
|
31
|
+
tasks, owed replies, inbox notifications, and an idle agent's own open queue
|
|
32
|
+
(plus the hooks an agent registers itself).
|
|
33
|
+
|
|
34
|
+
**One rule with teeth:** a task's `due_at` is a *soft* deadline. It sorts to the
|
|
35
|
+
top and is surfaced everywhere, but it **never wakes anyone**. Deadlines shape
|
|
36
|
+
attention; they don't manufacture interrupts.
|
|
37
|
+
|
|
38
|
+
That rule was written against **interruption**, and it was over-broad: it also
|
|
39
|
+
stopped anything waking an *idle* agent, which interrupts nothing. An idle agent
|
|
40
|
+
with a to-do list slept forever and the list was write-only — so "agents never
|
|
41
|
+
manage their own waking" was false. What waking a task earns is now:
|
|
42
|
+
|
|
43
|
+
| task state | agent state | |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `priority=urgent` | any | **wake** — interrupting is the point |
|
|
46
|
+
| open | idle | **wake** — nothing is being interrupted; this is scheduling |
|
|
47
|
+
| open | busy | **never** — that is the interrupt the rule forbids |
|
|
48
|
+
|
|
49
|
+
A **task wake can never reach a human rung.** The ladder climbs to a person
|
|
50
|
+
because a message *must land*; a to-do list has no such property, so `idle_task`
|
|
51
|
+
is fenced to rungs that declare `leaves_machine=False`. Homework does not page
|
|
52
|
+
anyone at 3am.
|
|
53
|
+
|
|
54
|
+
**A task is in a live state or it is not open.** Doing it → `in_progress`.
|
|
55
|
+
Waiting → `blocked`, which **must name its dependency** (`blocked_on`); nothing
|
|
56
|
+
else counts as blocked. Someone else's → transfer it. Genuinely stuck → escalate,
|
|
57
|
+
and the supervisor decides whether it still matters. `pending` forever is not a
|
|
58
|
+
resting state: the list is a work queue, not a graveyard.
|
|
59
|
+
|
|
60
|
+
**Stall breaks the loop with a rule, not a cooldown.** A task woken for
|
|
61
|
+
`idle_task_stall_wakes` idle wakes without moving is *stalled* — derived at read
|
|
62
|
+
time from the wake ledger, never stored — and **leaves the actionable set**. It
|
|
63
|
+
stops causing wakes and becomes a reported fact for someone to decide about.
|
|
64
|
+
|
|
65
|
+
**The ladder** (in-session hook → resume → spawn → human → supervisor badge) is
|
|
66
|
+
append-only: escalating supersedes the old attempt and inserts a new one, so a
|
|
67
|
+
wake's full history is auditable. Each rung has an SLA; resolution closes the
|
|
68
|
+
attempt and records latency. The terminal rung never times out — it just stays red
|
|
69
|
+
on the console. Semantics are **at-least-once wake + idempotent ack**, not
|
|
70
|
+
exactly-once.
|
|
71
|
+
|
|
72
|
+
**Decision and execution are separate.** `plan_wakes()` is pure: it collects,
|
|
73
|
+
records, and returns a plan — it never spawns anything. The driver executes. This
|
|
74
|
+
is why the decision layer is testable and why a dry run can be genuinely
|
|
75
|
+
side-effect-free.
|
|
76
|
+
|
|
77
|
+
## Delivery
|
|
78
|
+
|
|
79
|
+
A ledger row per (message × recipient): `queued → notified → read`. Past SLA and
|
|
80
|
+
unread splits two ways:
|
|
81
|
+
|
|
82
|
+
- **`escalated`** — something is reacting. The system is working.
|
|
83
|
+
- **`overdue`** — nothing is reacting. **This is the guarantee breaking**, and it
|
|
84
|
+
is surfaced red rather than hidden.
|
|
85
|
+
|
|
86
|
+
The ledger is a **projection** of durable messages, so it is self-healing: a
|
|
87
|
+
missing row is re-derived because the source message still exists. Rows are never
|
|
88
|
+
deleted. Time-dependent state is computed at read time, never stored, so it can't
|
|
89
|
+
go stale.
|
|
90
|
+
|
|
91
|
+
Two rules learned the hard way:
|
|
92
|
+
|
|
93
|
+
1. **Never mark delivered speculatively.** Delivery is proven by the in-session
|
|
94
|
+
hook or an explicit ack — never by "we planned to send it". A plan whose
|
|
95
|
+
execution is skipped (role lock held) or fails would otherwise lose the item
|
|
96
|
+
*and* suppress the escalation that should have caught it.
|
|
97
|
+
2. **Scope "is this handled?" to (recipient, item)**, never to the container. A
|
|
98
|
+
thread-level flag from one role once masked every other role's stuck message on
|
|
99
|
+
that thread, permanently.
|
|
100
|
+
|
|
101
|
+
## Presence and one session per role
|
|
102
|
+
|
|
103
|
+
`agent_sessions` is keyed by **role** — reflecting the invariant that at most one
|
|
104
|
+
session per role exists, enforced by a role-scoped `flock` that every starter
|
|
105
|
+
takes. The kernel releases it on crash, so stale locks can't exist.
|
|
106
|
+
|
|
107
|
+
Liveness is derived from heartbeat age (`online`/`suspect`/`dead`/`offline`/
|
|
108
|
+
`never`). Heartbeats ride the in-session hook — no extra process. Registration is
|
|
109
|
+
explicit, so unrelated sessions never pollute the board.
|
|
110
|
+
|
|
111
|
+
The board shows "now executing" **only for a live session**. An ended session's
|
|
112
|
+
last activity is shown as history, never as current work.
|
|
113
|
+
|
|
114
|
+
## Sessions: intraday continuity, cross-day restart
|
|
115
|
+
|
|
116
|
+
Within a day, resume (context is preserved, cheap). Across days, restart: wind the
|
|
117
|
+
old session down with a **handoff note**, then open fresh.
|
|
118
|
+
|
|
119
|
+
Long-lived sessions rot: context grows monotonically, compaction drops detail
|
|
120
|
+
unpredictably, and instruction changes never land. A daily restart forces durable
|
|
121
|
+
state — anything worth keeping must be in a task, the journal, or the knowledge
|
|
122
|
+
base, not in a session's memory. Continuity is **structured state, not
|
|
123
|
+
conversation**.
|
|
124
|
+
|
|
125
|
+
## Hooks: agents schedule themselves
|
|
126
|
+
|
|
127
|
+
Agents must never poll. So they must be able to say "wake me later / when X":
|
|
128
|
+
`at`, `interval`, `cron` (calendar, DST-correct via `zoneinfo`, computed by a
|
|
129
|
+
minute-scan — trivial cost, no clever math to get wrong), and `probe` (a host
|
|
130
|
+
function; truthy return = notification = wake).
|
|
131
|
+
|
|
132
|
+
Probes are restricted to a configured allowlist, may only observe and notify, and
|
|
133
|
+
auto-disable after consecutive errors (with an inbox notice to the owner) so a
|
|
134
|
+
broken watcher can neither rot silently nor stall the tick.
|
|
135
|
+
|
|
136
|
+
**Anything that can block does not belong in a probe.** Detection that hits the
|
|
137
|
+
network runs in its own process and enqueues to the inbox; the orchestrator
|
|
138
|
+
delivers. Detection and delivery stay decoupled — otherwise one slow fetch stalls
|
|
139
|
+
all waking.
|
|
140
|
+
|
|
141
|
+
## Meetings
|
|
142
|
+
|
|
143
|
+
Bounded by construction: idle deadline, message budget, automatic consensus at a
|
|
144
|
+
threshold, one position each, and a mutual propose/confirm termination handshake.
|
|
145
|
+
The transport rejects duplicates, and rejects stacked unresolved questions for
|
|
146
|
+
callers that ask for a reply — meetings deliberately do not, tracking each as
|
|
147
|
+
its own obligation instead, so one answer may settle several. Turn-taking is
|
|
148
|
+
NOT a bound: refusing to let a present party speak because an absent one owes
|
|
149
|
+
a reply drops messages rather than preventing them, since a rejected insert is
|
|
150
|
+
the only way a message is lost once delivery and the wake ladder are carrying
|
|
151
|
+
it. An owed reply is nudged, never enforced at the door. Termination
|
|
152
|
+
votes don't consume the budget, so a meeting can always stop. The tally counts
|
|
153
|
+
only *active* attendees, so someone leaving can never deadlock closure.
|
|
154
|
+
|
|
155
|
+
The protocol's core integrity rule: **never create both sides**. An agent cannot
|
|
156
|
+
fabricate the counterpart's attendance, reports, or votes — if the other side
|
|
157
|
+
never arrives, the artifact stands alone and the meeting pauses or escalates.
|
|
158
|
+
|
|
159
|
+
## The supervisor is not an agent
|
|
160
|
+
|
|
161
|
+
A human oversight identity, outside the agent role space, reachable only through
|
|
162
|
+
an authenticated web adapter. Agent APIs reject it; a message claiming to be from
|
|
163
|
+
them carries no authority. See [security.md](security.md).
|
|
164
|
+
|
|
165
|
+
## What deskd deliberately does not do
|
|
166
|
+
|
|
167
|
+
- **Execute your domain.** It wakes agents and delivers notifications. It never
|
|
168
|
+
acts *as* an agent, and has no path to your side-effecting systems.
|
|
169
|
+
- **Own your roles.** The registry is configuration. The engine has no idea what
|
|
170
|
+
your agents are for.
|
|
171
|
+
- **Guarantee exactly-once anything.** At-least-once + idempotent acks, with every
|
|
172
|
+
attempt auditable.
|
|
173
|
+
- **Pretend it can push.** See above.
|