@desplega.ai/agent-swarm 1.67.2 → 1.67.4
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.
- package/LICENSE +1 -1
- package/README.md +96 -387
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/be/db.ts +22 -0
- package/src/be/migrations/040_slack_thread_composite_index.sql +3 -0
- package/src/http/index.ts +9 -0
- package/src/prompts/session-templates.ts +1 -1
- package/src/slack/handlers.ts +17 -2
- package/src/slack/router.ts +20 -2
- package/src/tests/follow-up-redelivery-guard.test.ts +267 -0
- package/src/tests/prompt-template-remaining.test.ts +5 -0
- package/src/tests/slack-bot-filter.test.ts +156 -0
- package/src/tools/send-task.ts +31 -0
- package/src/tools/templates.ts +5 -0
- package/tsconfig.json +2 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
<sub>Built by <a href="https://desplega.sh">desplega.sh</a> — by builders, for builders.</sub>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<p align="center">
|
|
15
|
+
<video src="https://github.com/user-attachments/assets/e220712e-c54d-4f46-b059-bac04639d229" controls muted playsinline width="720"></video>
|
|
16
|
+
</p>
|
|
17
|
+
<p align="center">
|
|
18
|
+
<sub>▸ <a href="./assets/agent-swarm.mp4">daily evolution</a> · <a href="./assets/agent-swarm-slack-to-pr.mp4">slack → pr</a> · <a href="./assets/video-source">Making of</a></sub>
|
|
19
|
+
</p>
|
|
15
20
|
|
|
16
21
|
<p align="center">
|
|
17
22
|
<a href="https://agent-swarm.dev">
|
|
@@ -33,101 +38,90 @@ https://github.com/user-attachments/assets/bd308567-d21e-44a5-87ec-d25aeb1de3d3
|
|
|
33
38
|
|
|
34
39
|
> **What if your AI agents remembered everything, learned from every mistake, and got better with every task?**
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## Key Features
|
|
39
|
-
|
|
40
|
-
- **Lead/Worker coordination** — A lead agent delegates and tracks work across multiple workers
|
|
41
|
-
- **Docker isolation** — Each worker runs in its own container with a full dev environment
|
|
42
|
-
- **Slack, GitHub, GitLab & Email integration** — Create tasks by messaging the bot, @mentioning it in issues/PRs/MRs, or sending an email
|
|
43
|
-
- **Task lifecycle** — Priority queues, dependencies, pause/resume across deployments
|
|
44
|
-
- **Compounding memory** — Agents learn from every session and get smarter over time
|
|
45
|
-
- **Persistent identity** — Each agent has its own personality, expertise, and working style that evolves
|
|
46
|
-
- **Dashboard UI** — Real-time monitoring of agents, tasks, and inter-agent chat
|
|
47
|
-
- **Service discovery** — Workers can expose HTTP services and discover each other
|
|
48
|
-
- **Scheduled tasks** — Cron-based recurring task automation
|
|
49
|
-
- **Templates registry** — Pre-built agent templates (9 official: lead, coder, researcher, reviewer, tester, FDE, content-writer, content-reviewer, content-strategist) with a gallery UI and docker-compose builder
|
|
50
|
-
- **GitLab integration** — Full GitLab webhook support alongside GitHub via provider adapter pattern
|
|
51
|
-
- **Working directory support** — Tasks can specify a custom starting directory for agents via the `dir` parameter
|
|
52
|
-
- **Multi-provider** — Run agents with Claude Code, pi-mono, or OpenAI Codex (`HARNESS_PROVIDER=claude|pi|codex`)
|
|
53
|
-
- **Agent-fs integration** — Persistent, searchable filesystem shared across the swarm with auto-registration on first boot
|
|
54
|
-
- **Debug dashboard** — SQL query interface with Monaco editor and AG Grid results for database inspection
|
|
55
|
-
- **Workflow engine** — DAG-based workflow automation with executor registry, checkpoint durability, webhook/schedule/manual triggers, per-step retry, structured I/O schemas, fan-out/convergence, configurable failure handling, and version history
|
|
56
|
-
- **Linear integration** — Bidirectional ticket tracker sync via OAuth + webhooks with AgentSession lifecycle and generic tracker abstraction
|
|
57
|
-
- **Portless local dev** — Friendly URLs for local development (`api.swarm.localhost:1355`) via portless proxy
|
|
58
|
-
- **Onboarding wizard** — Interactive CLI wizard (`agent-swarm onboard`) to set up a new swarm from scratch with presets, credential collection, and docker-compose generation
|
|
59
|
-
- **Skill system** — Reusable procedural knowledge: create, install, publish, and sync skills from GitHub with scope resolution (agent → swarm → global)
|
|
60
|
-
- **Human-in-the-Loop** — Workflow nodes that pause for human approval or input, with a dashboard UI for reviewing and responding to requests
|
|
61
|
-
- **MCP server management** — Register, install, and manage MCP servers for agents with scope cascade (agent → swarm → global) and auto-injection into worker containers
|
|
62
|
-
- **Context usage tracking** — Monitor context window utilization and compaction events per task with visual indicators in the dashboard
|
|
41
|
+
## What it does
|
|
63
42
|
|
|
64
|
-
|
|
43
|
+
Agent Swarm runs a team of AI coding agents that coordinate autonomously. A **lead agent** receives tasks — from Slack, GitHub, GitLab, email, or the API — breaks them down, and delegates to **worker agents** running in Docker containers. Workers execute tasks, ship code, and write their learnings back to a shared memory so the whole swarm gets smarter every session.
|
|
65
44
|
|
|
66
|
-
|
|
45
|
+
Learn more in the [architecture overview](https://docs.agent-swarm.dev/docs/architecture/overview).
|
|
67
46
|
|
|
68
|
-
|
|
69
|
-
|
|
47
|
+
```mermaid
|
|
48
|
+
flowchart LR
|
|
49
|
+
subgraph IN["Tasks come in"]
|
|
50
|
+
direction TB
|
|
51
|
+
S["Slack"]
|
|
52
|
+
G["GitHub / GitLab"]
|
|
53
|
+
E["Email"]
|
|
54
|
+
A["API / CLI"]
|
|
55
|
+
end
|
|
70
56
|
|
|
71
|
-
|
|
57
|
+
LEAD(["Lead Agent<br/>plans & delegates"])
|
|
72
58
|
|
|
73
|
-
|
|
59
|
+
subgraph WORKERS["Workers in Docker"]
|
|
60
|
+
direction TB
|
|
61
|
+
W1["Worker"]
|
|
62
|
+
W2["Worker"]
|
|
63
|
+
W3["Worker"]
|
|
64
|
+
end
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
subgraph BRAIN["Persistent brain"]
|
|
67
|
+
direction TB
|
|
68
|
+
MEM["Memory<br/>(vector search)"]
|
|
69
|
+
ID["Identity<br/>(SOUL, CLAUDE.md)"]
|
|
70
|
+
end
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
subgraph OUT["Work ships"]
|
|
73
|
+
direction TB
|
|
74
|
+
PR["Pull Requests"]
|
|
75
|
+
REPLY["Slack replies"]
|
|
76
|
+
EMAIL["Email replies"]
|
|
77
|
+
end
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
IN --> LEAD --> WORKERS
|
|
80
|
+
WORKERS -->|reads context| BRAIN
|
|
81
|
+
WORKERS -->|writes learnings| BRAIN
|
|
82
|
+
WORKERS --> OUT
|
|
85
83
|
```
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
## Highlights
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
- **Lead/worker orchestration in Docker** — isolated dev environments, priority queues, pause/resume across deploys. [Architecture →](https://docs.agent-swarm.dev/docs/architecture/overview)
|
|
88
|
+
- **Compounding memory & persistent identity** — agents remember past sessions and evolve their own persona, expertise, and notes. [Memory →](https://docs.agent-swarm.dev/docs/architecture/memory) · [Agents →](https://docs.agent-swarm.dev/docs/architecture/agents)
|
|
89
|
+
- **Multi-channel inputs** — Slack, GitHub, GitLab, email, Linear, and the HTTP API all create tasks. [Integrations](#integrations)
|
|
90
|
+
- **Workflow engine with Human-in-the-Loop** — DAG-based automation with approval gates, retries, and structured I/O. [Workflows →](https://docs.agent-swarm.dev/docs/concepts/workflows)
|
|
91
|
+
- **Scheduled & recurring tasks** — cron-based automation for standing work. [Scheduling →](https://docs.agent-swarm.dev/docs/concepts/scheduling)
|
|
92
|
+
- **Multi-provider** — run with Claude Code, OpenAI Codex, or pi-mono. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration)
|
|
93
|
+
- **Skills & MCP servers** — reusable procedural knowledge and per-agent MCP servers with scope cascade. [MCP tools →](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
|
|
94
|
+
- **Real-time dashboard** — monitor agents, tasks, and inter-agent chat. [app.agent-swarm.dev →](https://app.agent-swarm.dev)
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
## Quick Start
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
**Prerequisites:** [Docker](https://docker.com) and a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) OAuth token (`claude setup-token`).
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
git clone https://github.com/desplega-ai/agent-swarm.git
|
|
97
|
-
cd agent-swarm
|
|
98
|
-
bun install
|
|
100
|
+
The fastest way is the onboarding wizard — it collects credentials, picks presets, and generates a working `docker-compose.yml`:
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Edit .env — set API_KEY
|
|
103
|
-
bun run start:http
|
|
102
|
+
```bash
|
|
103
|
+
bunx @desplega.ai/agent-swarm onboard
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Prefer manual setup? Clone and run with Docker Compose:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
bun run docker:run:worker
|
|
109
|
+
git clone https://github.com/desplega-ai/agent-swarm.git
|
|
110
|
+
cd agent-swarm
|
|
111
|
+
cp .env.docker.example .env
|
|
112
|
+
# edit .env — set API_KEY and CLAUDE_CODE_OAUTH_TOKEN
|
|
113
|
+
docker compose -f docker-compose.example.yml --env-file .env up -d
|
|
115
114
|
```
|
|
116
115
|
|
|
117
|
-
|
|
116
|
+
The API runs on port `3013`, with interactive docs at `http://localhost:3013/docs` and an OpenAPI 3.1 spec at `http://localhost:3013/openapi.json`.
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
# After starting the API server (Option B, step 1):
|
|
123
|
-
bunx @desplega.ai/agent-swarm connect
|
|
124
|
-
```
|
|
118
|
+
<details>
|
|
119
|
+
<summary><strong>Other setups</strong></summary>
|
|
125
120
|
|
|
126
|
-
|
|
121
|
+
- **Local API + Docker workers** — run the API on your host, workers in Docker. See [Getting Started](https://docs.agent-swarm.dev/docs/getting-started).
|
|
122
|
+
- **Claude Code as the lead agent** — `bunx @desplega.ai/agent-swarm connect`, then tell Claude Code to register as the lead.
|
|
127
123
|
|
|
128
|
-
|
|
129
|
-
Register yourself as the lead agent in the agent-swarm.
|
|
130
|
-
```
|
|
124
|
+
</details>
|
|
131
125
|
|
|
132
126
|
## How It Works
|
|
133
127
|
|
|
@@ -141,312 +135,37 @@ Worker Worker Worker
|
|
|
141
135
|
(Docker containers with full dev environments)
|
|
142
136
|
```
|
|
143
137
|
|
|
144
|
-
1.
|
|
145
|
-
2.
|
|
146
|
-
3.
|
|
147
|
-
4.
|
|
148
|
-
5.
|
|
149
|
-
6.
|
|
150
|
-
|
|
151
|
-
## Agents Get Smarter Over Time
|
|
152
|
-
|
|
153
|
-
Agent Swarm agents aren't stateless. They build compounding knowledge through multiple automatic mechanisms:
|
|
154
|
-
|
|
155
|
-
### Memory System
|
|
138
|
+
1. A task arrives via Slack DM, GitHub @mention, email, or the API.
|
|
139
|
+
2. The lead plans and delegates subtasks to workers.
|
|
140
|
+
3. Workers execute in isolated Docker containers (git, Node.js, Python, etc.).
|
|
141
|
+
4. Progress streams to the dashboard, Slack threads, or the API.
|
|
142
|
+
5. Results ship back out as PRs, issue replies, or Slack messages.
|
|
143
|
+
6. Session learnings are extracted and become memory for future tasks.
|
|
156
144
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- **Session summaries** — At the end of each session, a lightweight model extracts key learnings: mistakes made, patterns discovered, failed approaches, and codebase knowledge. These summaries become searchable memories.
|
|
160
|
-
- **Task completions** — Every completed (or failed) task's output is indexed. Failed tasks include notes about what went wrong, so the agent avoids repeating the same mistake.
|
|
161
|
-
- **File-based notes** — Agents write to `/workspace/personal/memory/` in their per-agent directory. Files are automatically indexed and can be promoted to swarm scope.
|
|
162
|
-
- **Lead-to-worker injection** — The lead agent can push specific learnings into any worker's memory using the `inject-learning` tool, closing the feedback loop.
|
|
163
|
-
|
|
164
|
-
Before starting each task, the runner automatically searches for relevant memories and includes them in the agent's context. Past experience directly informs future work.
|
|
165
|
-
|
|
166
|
-
### Persistent Identity
|
|
167
|
-
|
|
168
|
-
Each agent has four identity files that persist across sessions and evolve over time:
|
|
169
|
-
|
|
170
|
-
| File | Purpose | Example |
|
|
171
|
-
|------|---------|---------|
|
|
172
|
-
| **SOUL.md** | Core persona, values, behavioral directives | "You're not a chatbot. Be thorough. Own your mistakes." |
|
|
173
|
-
| **IDENTITY.md** | Expertise, working style, track record | "I'm the coding arm of the swarm. I ship fast and clean." |
|
|
174
|
-
| **TOOLS.md** | Environment knowledge — repos, services, APIs | "The API runs on port 3013. Use `wts` for worktree management." |
|
|
175
|
-
| **CLAUDE.md** | Persistent notes and instructions | Learnings, preferences, important context |
|
|
176
|
-
|
|
177
|
-
Agents can edit these files directly during a session. Changes are synced to the database in real-time (on every file edit) and at session end. When the agent restarts, its identity is restored from the database. Version history is tracked for all changes.
|
|
178
|
-
|
|
179
|
-
The default templates encourage self-improvement:
|
|
180
|
-
- Tools you wished you had? Update your startup script.
|
|
181
|
-
- Environment knowledge gained? Record it in TOOLS.md.
|
|
182
|
-
- Patterns discovered? Add them to your notes.
|
|
183
|
-
- Mistakes to avoid? Add guardrails.
|
|
184
|
-
|
|
185
|
-
### Startup Scripts
|
|
186
|
-
|
|
187
|
-
Each agent has a startup script (`/workspace/start-up.sh`) that runs at every container start. Agents can modify this script to install tools, configure their environment, or set up workflows — and the changes persist across restarts. An agent that discovers it needs `ripgrep` will install it once, and it'll be there for every future session.
|
|
188
|
-
|
|
189
|
-
## Agent Configuration
|
|
190
|
-
|
|
191
|
-
### Identity Management
|
|
192
|
-
|
|
193
|
-
Agent identity is stored in the database and synced to the filesystem at session start. There are three ways to configure it:
|
|
194
|
-
|
|
195
|
-
1. **Default generation** — On first registration, the system generates templates based on the agent's name, role, and description.
|
|
196
|
-
2. **Self-editing** — Agents modify their own identity files during sessions. A PostToolUse hook syncs changes to the database in real-time.
|
|
197
|
-
3. **API / MCP tool** — Use the `update-profile` tool to programmatically set any identity field (soulMd, identityMd, toolsMd, claudeMd, setupScript).
|
|
198
|
-
|
|
199
|
-
### System Prompt Assembly
|
|
200
|
-
|
|
201
|
-
The system prompt is built from multiple layers, assembled at task start:
|
|
202
|
-
|
|
203
|
-
1. **Base role instructions** — Lead or worker-specific behavior rules
|
|
204
|
-
2. **Agent identity** — SOUL.md + IDENTITY.md content
|
|
205
|
-
3. **Repository context** — If the task targets a specific GitHub repo, that repo's CLAUDE.md is included
|
|
206
|
-
4. **Filesystem guide** — Memory directories, personal/shared workspace, setup script instructions
|
|
207
|
-
5. **Self-awareness** — How the agent is built (runtime, hooks, memory system, task lifecycle)
|
|
208
|
-
6. **Additional prompt** — Custom text from `SYSTEM_PROMPT` env var or `--system-prompt` CLI flag
|
|
209
|
-
|
|
210
|
-
### Hook System
|
|
211
|
-
|
|
212
|
-
Six hooks fire during each Claude Code session, providing safety, context management, and persistence:
|
|
213
|
-
|
|
214
|
-
| Hook | When | What it does |
|
|
215
|
-
|------|------|-------------|
|
|
216
|
-
| **SessionStart** | Session begins | Writes CLAUDE.md from DB, loads concurrent session context for leads |
|
|
217
|
-
| **PreCompact** | Before context compaction | Injects a "goal reminder" with current task details so the agent doesn't lose track |
|
|
218
|
-
| **PreToolUse** | Before each tool call | Checks for task cancellation, detects tool loops (same tool/args repeated), blocks excessive polling |
|
|
219
|
-
| **PostToolUse** | After each tool call | Sends heartbeat, syncs identity file edits to DB, auto-indexes memory files |
|
|
220
|
-
| **UserPromptSubmit** | New iteration starts | Checks for task cancellation |
|
|
221
|
-
| **Stop** | Session ends | Saves PM2 state, syncs all identity files, runs session summarization via Haiku, marks agent offline |
|
|
145
|
+
More detail in the [task lifecycle docs](https://docs.agent-swarm.dev/docs/concepts/task-lifecycle).
|
|
222
146
|
|
|
223
147
|
## Integrations
|
|
224
148
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Message the bot directly to create tasks. Workers reply in threads with progress updates. Optionally restrict access with `SLACK_ALLOWED_EMAIL_DOMAINS` or `SLACK_ALLOWED_USER_IDS`.
|
|
236
|
-
|
|
237
|
-
### GitHub App
|
|
238
|
-
|
|
239
|
-
Set up a [GitHub App](https://github.com/settings/apps/new) to receive webhooks when the bot is @mentioned or assigned to issues/PRs.
|
|
240
|
-
|
|
241
|
-
**Webhook URL:** `https://<your-domain>/api/github/webhook`
|
|
242
|
-
|
|
243
|
-
**Required permissions:**
|
|
244
|
-
- Issues: Read & Write
|
|
245
|
-
- Pull requests: Read & Write
|
|
246
|
-
|
|
247
|
-
**Subscribe to events:** Issues, Issue comments, Pull requests, Pull request reviews, Pull request review comments, Check runs, Check suites, Workflow runs
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
# Add to your .env
|
|
251
|
-
GITHUB_WEBHOOK_SECRET=your-webhook-secret
|
|
252
|
-
GITHUB_BOT_NAME=your-bot-name # Default: agent-swarm-bot
|
|
253
|
-
|
|
254
|
-
# Optional: Enable bot reactions (emoji acknowledgments on GitHub)
|
|
255
|
-
GITHUB_APP_ID=123456
|
|
256
|
-
GITHUB_APP_PRIVATE_KEY=base64-encoded-key
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
**Supported events:**
|
|
260
|
-
|
|
261
|
-
| Event | What happens |
|
|
262
|
-
|-------|-------------|
|
|
263
|
-
| Bot assigned to PR/issue | Creates a task for the lead agent |
|
|
264
|
-
| Review requested from bot | Creates a review task |
|
|
265
|
-
| `@bot-name` in comment/issue/PR | Creates a task with the mention context |
|
|
266
|
-
| PR review submitted (on bot's PR) | Creates a notification task with review feedback |
|
|
267
|
-
| CI failure (on PRs with existing tasks) | Creates a CI notification task |
|
|
268
|
-
|
|
269
|
-
<details>
|
|
270
|
-
<summary><strong>Flow Diagrams</strong> (click to expand)</summary>
|
|
271
|
-
|
|
272
|
-
#### Task Creation Flow
|
|
273
|
-
|
|
274
|
-
How GitHub events become tasks in the swarm:
|
|
275
|
-
|
|
276
|
-
```mermaid
|
|
277
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px', 'nodeSpacing': 30, 'rankSpacing': 40}}}%%
|
|
278
|
-
flowchart TB
|
|
279
|
-
subgraph ENTRY["1. GitHub Webhook Entry Points"]
|
|
280
|
-
direction LR
|
|
281
|
-
E1["Issue<br/>opened/edited"]
|
|
282
|
-
E2["PR<br/>opened/edited"]
|
|
283
|
-
E3["Comment<br/>created"]
|
|
284
|
-
E4["Bot Assigned<br/>to Issue/PR"]
|
|
285
|
-
E5["Review Requested<br/>from Bot"]
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
subgraph GATE["2. Trigger Gate"]
|
|
289
|
-
M{"@agent-swarm<br/>mention?"}
|
|
290
|
-
A{"Bot is<br/>assignee?"}
|
|
291
|
-
D{"Duplicate?<br/>60s TTL"}
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
subgraph CREATE["3. Task Creation"]
|
|
295
|
-
LEAD["Find Lead Agent<br/>(online > offline > none)"]
|
|
296
|
-
TPL["resolveTemplate()"]
|
|
297
|
-
TASK["createTaskExtended()"]
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
subgraph OUT["4. Output"]
|
|
301
|
-
ASSIGN["Task assigned<br/>to Lead"]
|
|
302
|
-
POOL["Task in pool<br/>(no lead)"]
|
|
303
|
-
REACT["eyes reaction<br/>on GitHub"]
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
E1 & E2 & E3 --> M
|
|
307
|
-
E4 & E5 --> A
|
|
308
|
-
|
|
309
|
-
M -->|Yes| D
|
|
310
|
-
A -->|Yes| D
|
|
311
|
-
M & A -->|No| DROP1(("skip"))
|
|
312
|
-
|
|
313
|
-
D -->|New| LEAD
|
|
314
|
-
D -->|Dup| DROP2(("skip"))
|
|
315
|
-
|
|
316
|
-
LEAD --> TPL --> TASK
|
|
317
|
-
|
|
318
|
-
TASK -->|lead found| ASSIGN
|
|
319
|
-
TASK -.->|no lead| POOL
|
|
320
|
-
TASK --> REACT
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
[PNG fallback](assets/github-task-creation-flow.png)
|
|
324
|
-
|
|
325
|
-
#### Follow-up Flows
|
|
326
|
-
|
|
327
|
-
Events that create secondary tasks when an active task already exists for a PR:
|
|
328
|
-
|
|
329
|
-
```mermaid
|
|
330
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px'}}}%%
|
|
331
|
-
flowchart TB
|
|
332
|
-
subgraph EVENTS["GitHub Follow-up Events (require existing active task)"]
|
|
333
|
-
direction LR
|
|
334
|
-
F1["PR Closed<br/>(merged/closed)"]
|
|
335
|
-
F2["PR Synchronize<br/>(new commits)"]
|
|
336
|
-
F3["Review Submitted<br/>(approved/changes_requested)"]
|
|
337
|
-
F4["Check Run Failed"]
|
|
338
|
-
F5["Check Suite Failed"]
|
|
339
|
-
F6["Workflow Run Failed"]
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
FIND{"findTaskByVcs()<br/>Active task for<br/>repo + PR number?"}
|
|
343
|
-
|
|
344
|
-
EVENTS --> FIND
|
|
345
|
-
|
|
346
|
-
FIND -->|No task| SKIP(("skip"))
|
|
347
|
-
|
|
348
|
-
subgraph FOLLOWUP["Follow-up Task Created (assigned to Lead)"]
|
|
349
|
-
direction LR
|
|
350
|
-
T1["github-pr-status<br/>PR merged/closed"]
|
|
351
|
-
T2["github-pr-update<br/>New commits pushed"]
|
|
352
|
-
T3["github-review<br/>Review feedback"]
|
|
353
|
-
T4["github-ci<br/>CI failure alert"]
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
F1 --> FIND -->|task found| T1
|
|
357
|
-
F2 --> FIND -->|task found| T2
|
|
358
|
-
F3 --> FIND -->|task found| T3
|
|
359
|
-
F4 & F5 & F6 --> FIND -->|task found| T4
|
|
360
|
-
|
|
361
|
-
NOTE["All follow-up tasks reference<br/>the original task ID for routing"]
|
|
362
|
-
|
|
363
|
-
FOLLOWUP --> NOTE
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
[PNG fallback](assets/github-followup-flows.png)
|
|
367
|
-
|
|
368
|
-
#### Cancellation Flows
|
|
369
|
-
|
|
370
|
-
How unassigning the bot cancels active tasks:
|
|
371
|
-
|
|
372
|
-
```mermaid
|
|
373
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px'}}}%%
|
|
374
|
-
flowchart TB
|
|
375
|
-
subgraph EVENTS["Cancellation Events"]
|
|
376
|
-
direction LR
|
|
377
|
-
C1["Bot Unassigned<br/>from Issue"]
|
|
378
|
-
C2["Bot Unassigned<br/>from PR"]
|
|
379
|
-
C3["Review Request<br/>Removed from Bot"]
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
BOT{"isBotAssignee()"}
|
|
383
|
-
FIND{"findTaskByVcs()<br/>Active task?"}
|
|
384
|
-
CANCEL["failTask()<br/>Cancel with reason"]
|
|
385
|
-
NOOP(("no-op"))
|
|
386
|
-
|
|
387
|
-
EVENTS --> BOT
|
|
388
|
-
BOT -->|Not bot| NOOP
|
|
389
|
-
BOT -->|Is bot| FIND
|
|
390
|
-
FIND -->|No task| NOOP
|
|
391
|
-
FIND -->|Task found| CANCEL
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
[PNG fallback](assets/github-cancellation-flows.png)
|
|
395
|
-
|
|
396
|
-
</details>
|
|
397
|
-
|
|
398
|
-
### GitLab
|
|
399
|
-
|
|
400
|
-
Set up a GitLab webhook to receive events when the bot is @mentioned or assigned to issues/MRs.
|
|
401
|
-
|
|
402
|
-
**Webhook URL:** `https://<your-domain>/api/gitlab/webhook`
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
# Add to your .env
|
|
406
|
-
GITLAB_WEBHOOK_SECRET=your-webhook-secret
|
|
407
|
-
GITLAB_TOKEN=your-gitlab-token # PAT or Group Access Token
|
|
408
|
-
GITLAB_BOT_NAME=agent-swarm-bot # Bot name for @mentions
|
|
409
|
-
GITLAB_URL=https://gitlab.com # GitLab instance URL
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
**Supported events:**
|
|
413
|
-
|
|
414
|
-
| Event | What happens |
|
|
415
|
-
|-------|-------------|
|
|
416
|
-
| Bot assigned to MR/issue | Creates a task for the lead agent |
|
|
417
|
-
| `@bot-name` in comment/issue/MR | Creates a task with the mention context |
|
|
418
|
-
| Pipeline failure (on MRs with existing tasks) | Creates a CI notification task |
|
|
419
|
-
|
|
420
|
-
Workers have `glab` CLI pre-installed for GitLab operations (creating MRs, commenting on issues, etc.).
|
|
421
|
-
|
|
422
|
-
### AgentMail
|
|
423
|
-
|
|
424
|
-
Give your agents email addresses via [AgentMail](https://agentmail.to). Emails are routed to agents as tasks or inbox messages.
|
|
425
|
-
|
|
426
|
-
**Webhook URL:** `https://<your-domain>/api/agentmail/webhook`
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
# Add to your .env
|
|
430
|
-
AGENTMAIL_WEBHOOK_SECRET=your-svix-secret
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
Agents self-register which inboxes they receive mail from using the `register-agentmail-inbox` MCP tool. Emails to a worker's inbox become tasks; emails to a lead's inbox become inbox messages for triage. Follow-up emails in the same thread are automatically routed to the same agent.
|
|
434
|
-
|
|
435
|
-
### Sentry
|
|
436
|
-
|
|
437
|
-
Workers can investigate Sentry issues directly with the `/investigate-sentry-issue` command. Add `SENTRY_AUTH_TOKEN` and `SENTRY_ORG` to your worker's environment.
|
|
149
|
+
| Integration | What it does | Setup |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| **Slack** | DM or @mention the bot to create tasks; workers reply in threads | [Guide](https://docs.agent-swarm.dev/docs/guides/slack-integration) |
|
|
152
|
+
| **GitHub App** | @mention or assign the bot on issues/PRs; CI failures create follow-up tasks | [Guide](https://docs.agent-swarm.dev/docs/guides/github-integration) |
|
|
153
|
+
| **GitLab** | Same model as GitHub — webhooks on issues/MRs, `glab` preinstalled in workers | [Guide](https://docs.agent-swarm.dev/docs/guides/gitlab-integration) |
|
|
154
|
+
| **AgentMail** | Give each agent an inbox; emails become tasks or lead messages | [Guide](https://docs.agent-swarm.dev/docs/guides/agentmail-integration) |
|
|
155
|
+
| **Linear** | Bidirectional ticket sync via OAuth + webhooks | [Guide](https://docs.agent-swarm.dev/docs/guides/linear-integration) |
|
|
156
|
+
| **Sentry** | Workers can triage Sentry issues with the `/investigate-sentry-issue` command | [Guide](https://docs.agent-swarm.dev/docs/guides/sentry-integration) |
|
|
438
157
|
|
|
439
158
|
## Dashboard
|
|
440
159
|
|
|
441
|
-
|
|
160
|
+
Real-time monitoring of agents, tasks, and inter-agent chat. Use the hosted version at [app.agent-swarm.dev](https://app.agent-swarm.dev), or run locally:
|
|
442
161
|
|
|
443
162
|
```bash
|
|
444
163
|
cd new-ui && pnpm install && pnpm run dev
|
|
445
164
|
```
|
|
446
165
|
|
|
447
|
-
Opens at `http://localhost:
|
|
166
|
+
Opens at `http://localhost:5274`.
|
|
448
167
|
|
|
449
|
-
## CLI
|
|
168
|
+
## [CLI](https://docs.agent-swarm.dev/docs/reference/cli)
|
|
450
169
|
|
|
451
170
|
```bash
|
|
452
171
|
bunx @desplega.ai/agent-swarm <command>
|
|
@@ -456,33 +175,23 @@ bunx @desplega.ai/agent-swarm <command>
|
|
|
456
175
|
|---------|-------------|
|
|
457
176
|
| `onboard` | Set up a new swarm from scratch (Docker Compose wizard) |
|
|
458
177
|
| `connect` | Connect this project to an existing swarm |
|
|
459
|
-
| `api`
|
|
460
|
-
| `
|
|
461
|
-
| `
|
|
462
|
-
| `
|
|
463
|
-
| `docs` | Open documentation (`--open` to launch in browser) |
|
|
464
|
-
| `artifact` | Manage agent artifacts |
|
|
178
|
+
| `api` | Start the API + MCP HTTP server |
|
|
179
|
+
| `worker` | Run a worker agent |
|
|
180
|
+
| `lead` | Run a lead agent |
|
|
181
|
+
| `docs` | Open documentation (`--open` to launch in browser) |
|
|
465
182
|
|
|
466
183
|
## Deployment
|
|
467
184
|
|
|
468
|
-
For production deployments, see [DEPLOYMENT.md](./DEPLOYMENT.md)
|
|
469
|
-
|
|
470
|
-
- Docker Compose setup with multiple workers
|
|
471
|
-
- systemd deployment for the API server
|
|
472
|
-
- Graceful shutdown and task resume
|
|
473
|
-
- Integration configuration (Slack, GitHub, AgentMail, Sentry)
|
|
185
|
+
For production deployments (Docker Compose with multiple workers, systemd for the API, graceful shutdown, integration config), see [DEPLOYMENT.md](./DEPLOYMENT.md) and the [deployment guide](https://docs.agent-swarm.dev/docs/guides/deployment).
|
|
474
186
|
|
|
475
187
|
## Documentation
|
|
476
188
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
| [CONTRIBUTING.md](./CONTRIBUTING.md) | Development setup and project structure |
|
|
484
|
-
| [UI.md](./UI.md) | Dashboard UI overview |
|
|
485
|
-
| [MCP.md](./MCP.md) | MCP tools reference (auto-generated) |
|
|
189
|
+
Everything lives at **[docs.agent-swarm.dev](https://docs.agent-swarm.dev)**. Good starting points:
|
|
190
|
+
|
|
191
|
+
- [Getting Started](https://docs.agent-swarm.dev/docs/getting-started) — install, configure, and run your first task
|
|
192
|
+
- [Architecture overview](https://docs.agent-swarm.dev/docs/architecture/overview) — how the swarm is wired together
|
|
193
|
+
- [CLI reference](https://docs.agent-swarm.dev/docs/reference/cli) and [Environment variables](https://docs.agent-swarm.dev/docs/reference/environment-variables)
|
|
194
|
+
- [API reference](https://docs.agent-swarm.dev/docs/api-reference) — every HTTP endpoint
|
|
486
195
|
|
|
487
196
|
## Contributing
|
|
488
197
|
|
|
@@ -509,4 +218,4 @@ Join our [Discord](https://discord.gg/KZgfyyDVZa) if you have questions or want
|
|
|
509
218
|
|
|
510
219
|
## License
|
|
511
220
|
|
|
512
|
-
[MIT](./LICENSE) — 2025-2026 [desplega.
|
|
221
|
+
[MIT](./LICENSE) — 2025-2026 [desplega.sh](https://desplega.sh)
|
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.67.
|
|
5
|
+
"version": "1.67.4",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
package/src/be/db.ts
CHANGED
|
@@ -84,6 +84,7 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
84
84
|
const templateBytes = templateGlobals.__testMigrationTemplate;
|
|
85
85
|
if (templateBytes) {
|
|
86
86
|
db = Database.deserialize(templateBytes);
|
|
87
|
+
db.run("PRAGMA busy_timeout = 5000;");
|
|
87
88
|
db.run("PRAGMA foreign_keys = ON;");
|
|
88
89
|
configureDbResolver(resolvePromptTemplate);
|
|
89
90
|
// Ensure the encryption key is resolved even when restoring from the test
|
|
@@ -98,6 +99,7 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
98
99
|
|
|
99
100
|
const database = db;
|
|
100
101
|
database.run("PRAGMA journal_mode = WAL;");
|
|
102
|
+
database.run("PRAGMA busy_timeout = 5000;");
|
|
101
103
|
database.run("PRAGMA foreign_keys = ON;");
|
|
102
104
|
|
|
103
105
|
// Load sqlite-vec extension for vector search.
|
|
@@ -1488,6 +1490,26 @@ export function getMostRecentTaskInThread(channelId: string, threadTs: string):
|
|
|
1488
1490
|
return row ? rowToAgentTask(row) : null;
|
|
1489
1491
|
}
|
|
1490
1492
|
|
|
1493
|
+
export function findCompletedTaskInThread(
|
|
1494
|
+
channelId: string,
|
|
1495
|
+
threadTs: string,
|
|
1496
|
+
windowMinutes: number,
|
|
1497
|
+
): AgentTask | null {
|
|
1498
|
+
const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
|
|
1499
|
+
const row = getDb()
|
|
1500
|
+
.prepare<AgentTaskRow, [string, string, string]>(
|
|
1501
|
+
`SELECT * FROM agent_tasks
|
|
1502
|
+
WHERE slackChannelId = ?
|
|
1503
|
+
AND slackThreadTs = ?
|
|
1504
|
+
AND status = 'completed'
|
|
1505
|
+
AND lastUpdatedAt > ?
|
|
1506
|
+
ORDER BY lastUpdatedAt DESC
|
|
1507
|
+
LIMIT 1`,
|
|
1508
|
+
)
|
|
1509
|
+
.get(channelId, threadTs, since);
|
|
1510
|
+
return row ? rowToAgentTask(row) : null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1491
1513
|
export function completeTask(id: string, output?: string): AgentTask | null {
|
|
1492
1514
|
const oldTask = getTaskById(id);
|
|
1493
1515
|
const finishedAt = new Date().toISOString();
|
package/src/http/index.ts
CHANGED
|
@@ -43,6 +43,15 @@ import { getPathSegments, parseQueryParams, setCorsHeaders } from "./utils";
|
|
|
43
43
|
import { handleWebhooks } from "./webhooks";
|
|
44
44
|
import { handleWorkflows } from "./workflows";
|
|
45
45
|
|
|
46
|
+
// Last-line-of-defense: never let a single bad request (e.g. a SQLITE_BUSY
|
|
47
|
+
// thrown out of a transaction callback) kill the API process. Log and keep going.
|
|
48
|
+
process.on("uncaughtException", (err) => {
|
|
49
|
+
console.error("[fatal] uncaughtException:", err);
|
|
50
|
+
});
|
|
51
|
+
process.on("unhandledRejection", (reason) => {
|
|
52
|
+
console.error("[fatal] unhandledRejection:", reason);
|
|
53
|
+
});
|
|
54
|
+
|
|
46
55
|
const port = parseInt(process.env.PORT || process.argv[2] || "3013", 10);
|
|
47
56
|
const apiKey = process.env.API_KEY || "";
|
|
48
57
|
|
|
@@ -94,7 +94,7 @@ If you explicitly assign to a different worker, session resume gracefully falls
|
|
|
94
94
|
When a worker completes or fails a task, you receive an automatic follow-up task. Handle it by:
|
|
95
95
|
1. Review the output/failure reason
|
|
96
96
|
2. If the task has Slack metadata, use \`slack-reply\` with the task's ID to post the result back to the originating thread
|
|
97
|
-
3.
|
|
97
|
+
3. Complete this task. Do NOT re-delegate or create new worker tasks from a follow-up \u2014 the worker's result IS the answer. Only escalate to the stakeholder if the worker explicitly failed and the failure needs human attention.
|
|
98
98
|
|
|
99
99
|
#### Heartbeat Checklist
|
|
100
100
|
|
package/src/slack/handlers.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { resolveTemplate } from "../prompts/resolver";
|
|
|
13
13
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
14
14
|
import { buildTreeBlocks, type TreeNode } from "./blocks";
|
|
15
15
|
import type { SlackFile } from "./files";
|
|
16
|
-
import { extractTaskFromMessage, routeMessage } from "./router";
|
|
16
|
+
import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
|
|
17
17
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
18
18
|
import "./templates";
|
|
19
19
|
import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "./thread-buffer";
|
|
@@ -162,6 +162,12 @@ interface MessageEvent {
|
|
|
162
162
|
* - `bot_id` present (newer Slack API, may lack subtype)
|
|
163
163
|
* - `user` matches the bot's own user ID (catches edge cases where
|
|
164
164
|
* messages posted with `username` override lack `bot_id`)
|
|
165
|
+
*
|
|
166
|
+
* Note: intentionally does NOT filter on `app_id`/`bot_profile`/`username` —
|
|
167
|
+
* those signals also appear on human messages sent via Slack apps that proxy
|
|
168
|
+
* a user (e.g. Claude.ai's Slack integration sends with `app_id` + `bot_profile`
|
|
169
|
+
* set, but the poster is still a real human). Filtering those drops legitimate
|
|
170
|
+
* human @mentions of the swarm.
|
|
165
171
|
*/
|
|
166
172
|
export function isBotMessage(
|
|
167
173
|
event: { subtype?: string; bot_id?: string; user?: string },
|
|
@@ -439,8 +445,17 @@ export function registerMessageHandler(app: App): void {
|
|
|
439
445
|
}
|
|
440
446
|
}
|
|
441
447
|
|
|
442
|
-
// ADDITIVE_SLACK: Buffer non-mention thread messages
|
|
448
|
+
// ADDITIVE_SLACK: Buffer non-mention thread messages.
|
|
449
|
+
// Skip if the message @-mentions someone other than our bot (e.g. "@Devin wdyt?"):
|
|
450
|
+
// that message is directed at a different bot/user and must not be fed to
|
|
451
|
+
// the swarm as an implicit follow-up.
|
|
443
452
|
if (additiveSlack && !botMentioned && msg.thread_ts && !requireMentionForThreadFollowup) {
|
|
453
|
+
if (hasOtherUserMention(effectiveText, botUserId)) {
|
|
454
|
+
console.log(
|
|
455
|
+
`[Slack] Skipping ADDITIVE buffer in ${msg.channel}/${msg.thread_ts}: message mentions another user`,
|
|
456
|
+
);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
444
459
|
// Check if this thread has any swarm activity (existing tasks)
|
|
445
460
|
const hasSwarmActivity = getAgentWorkingOnThread(msg.channel, msg.thread_ts) !== null;
|
|
446
461
|
|
package/src/slack/router.ts
CHANGED
|
@@ -6,6 +6,15 @@ export interface ThreadContext {
|
|
|
6
6
|
threadTs: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the text contains a `<@U...>` mention of anyone other than our bot.
|
|
11
|
+
* Exported for testing.
|
|
12
|
+
*/
|
|
13
|
+
export function hasOtherUserMention(text: string, botUserId: string): boolean {
|
|
14
|
+
const mentions = text.match(/<@([A-Z0-9]+)>/g) ?? [];
|
|
15
|
+
return mentions.some((m) => m !== `<@${botUserId}>`);
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* Routes a Slack message to the appropriate agent(s) based on mentions.
|
|
11
20
|
*
|
|
@@ -16,7 +25,7 @@ export interface ThreadContext {
|
|
|
16
25
|
*/
|
|
17
26
|
export function routeMessage(
|
|
18
27
|
text: string,
|
|
19
|
-
|
|
28
|
+
botUserId: string,
|
|
20
29
|
botMentioned: boolean,
|
|
21
30
|
threadContext?: ThreadContext,
|
|
22
31
|
): AgentMatch[] {
|
|
@@ -46,8 +55,17 @@ export function routeMessage(
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
// Thread follow-up — route to agent already working in this thread
|
|
58
|
+
// Thread follow-up — route to agent already working in this thread.
|
|
59
|
+
// Skip if the message @-mentions someone other than our bot (e.g. "@Devin wdyt?")
|
|
60
|
+
// and does not mention our bot: that message is directed at a different bot/user,
|
|
61
|
+
// not a follow-up intended for the swarm.
|
|
50
62
|
if (matches.length === 0 && threadContext && (!requireMentionForThreadFollowup || botMentioned)) {
|
|
63
|
+
if (!botMentioned && hasOtherUserMention(text, botUserId)) {
|
|
64
|
+
console.log(
|
|
65
|
+
`[Slack] Skipping thread follow-up in ${threadContext.channelId}/${threadContext.threadTs}: message mentions another user`,
|
|
66
|
+
);
|
|
67
|
+
return matches;
|
|
68
|
+
}
|
|
51
69
|
const workingAgent = getAgentWorkingOnThread(threadContext.channelId, threadContext.threadTs);
|
|
52
70
|
if (workingAgent && workingAgent.status !== "offline") {
|
|
53
71
|
console.log(
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
completeTask,
|
|
6
|
+
createAgent,
|
|
7
|
+
createTaskExtended,
|
|
8
|
+
findCompletedTaskInThread,
|
|
9
|
+
getDb,
|
|
10
|
+
getTaskById,
|
|
11
|
+
initDb,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
|
|
14
|
+
const TEST_DB_PATH = "./test-follow-up-redelivery-guard.sqlite";
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
initDb(TEST_DB_PATH);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
closeDb();
|
|
22
|
+
try {
|
|
23
|
+
unlinkSync(TEST_DB_PATH);
|
|
24
|
+
unlinkSync(`${TEST_DB_PATH}-wal`);
|
|
25
|
+
unlinkSync(`${TEST_DB_PATH}-shm`);
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore if files don't exist
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("findCompletedTaskInThread", () => {
|
|
32
|
+
test("finds completed tasks in a thread within the time window", () => {
|
|
33
|
+
const agent = createAgent({
|
|
34
|
+
name: "dedup-worker-1",
|
|
35
|
+
isLead: false,
|
|
36
|
+
status: "idle",
|
|
37
|
+
capabilities: [],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const task = createTaskExtended("test task for thread", {
|
|
41
|
+
agentId: agent.id,
|
|
42
|
+
slackChannelId: "C_DEDUP_1",
|
|
43
|
+
slackThreadTs: "1000.0001",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Mark as completed
|
|
47
|
+
completeTask(task.id, "done");
|
|
48
|
+
|
|
49
|
+
// Should find the completed task within a 2880-minute (48h) window
|
|
50
|
+
const result = findCompletedTaskInThread("C_DEDUP_1", "1000.0001", 2880);
|
|
51
|
+
expect(result).not.toBeNull();
|
|
52
|
+
expect(result!.id).toBe(task.id);
|
|
53
|
+
expect(result!.status).toBe("completed");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns null when no completed tasks exist in the thread", () => {
|
|
57
|
+
const agent = createAgent({
|
|
58
|
+
name: "dedup-worker-2",
|
|
59
|
+
isLead: false,
|
|
60
|
+
status: "idle",
|
|
61
|
+
capabilities: [],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create a task but don't complete it
|
|
65
|
+
createTaskExtended("pending task in thread", {
|
|
66
|
+
agentId: agent.id,
|
|
67
|
+
slackChannelId: "C_DEDUP_2",
|
|
68
|
+
slackThreadTs: "2000.0001",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = findCompletedTaskInThread("C_DEDUP_2", "2000.0001", 2880);
|
|
72
|
+
expect(result).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns null outside the time window", () => {
|
|
76
|
+
const agent = createAgent({
|
|
77
|
+
name: "dedup-worker-3",
|
|
78
|
+
isLead: false,
|
|
79
|
+
status: "idle",
|
|
80
|
+
capabilities: [],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const task = createTaskExtended("old completed task", {
|
|
84
|
+
agentId: agent.id,
|
|
85
|
+
slackChannelId: "C_DEDUP_3",
|
|
86
|
+
slackThreadTs: "3000.0001",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
completeTask(task.id, "done long ago");
|
|
90
|
+
|
|
91
|
+
// Backdate the lastUpdatedAt to 49 hours ago (beyond the 48h window)
|
|
92
|
+
const fortyNineHoursAgo = new Date(Date.now() - 49 * 60 * 60 * 1000).toISOString();
|
|
93
|
+
getDb().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
94
|
+
fortyNineHoursAgo,
|
|
95
|
+
task.id,
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// Should not find with a 48 hour window
|
|
99
|
+
const result = findCompletedTaskInThread("C_DEDUP_3", "3000.0001", 2880);
|
|
100
|
+
expect(result).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns null for a different thread", () => {
|
|
104
|
+
const agent = createAgent({
|
|
105
|
+
name: "dedup-worker-4",
|
|
106
|
+
isLead: false,
|
|
107
|
+
status: "idle",
|
|
108
|
+
capabilities: [],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const task = createTaskExtended("task in different thread", {
|
|
112
|
+
agentId: agent.id,
|
|
113
|
+
slackChannelId: "C_DEDUP_4",
|
|
114
|
+
slackThreadTs: "4000.0001",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
completeTask(task.id, "done");
|
|
118
|
+
|
|
119
|
+
// Search in a different thread — should not find
|
|
120
|
+
const result = findCompletedTaskInThread("C_DEDUP_4", "4000.9999", 2880);
|
|
121
|
+
expect(result).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("follow-up re-delegation guard logic", () => {
|
|
126
|
+
let leadAgent: ReturnType<typeof createAgent>;
|
|
127
|
+
let workerAgent: ReturnType<typeof createAgent>;
|
|
128
|
+
|
|
129
|
+
beforeAll(() => {
|
|
130
|
+
leadAgent = createAgent({
|
|
131
|
+
name: "guard-lead",
|
|
132
|
+
isLead: true,
|
|
133
|
+
status: "idle",
|
|
134
|
+
capabilities: [],
|
|
135
|
+
});
|
|
136
|
+
workerAgent = createAgent({
|
|
137
|
+
name: "guard-worker",
|
|
138
|
+
isLead: false,
|
|
139
|
+
status: "idle",
|
|
140
|
+
capabilities: [],
|
|
141
|
+
maxTasks: 5,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("blocks re-delegation when source task is a follow-up and thread has completed work", () => {
|
|
146
|
+
// Step 1: Create and complete a worker task in a Slack thread
|
|
147
|
+
const workerTask = createTaskExtended("implement feature X", {
|
|
148
|
+
agentId: workerAgent.id,
|
|
149
|
+
slackChannelId: "C_GUARD_1",
|
|
150
|
+
slackThreadTs: "5000.0001",
|
|
151
|
+
});
|
|
152
|
+
completeTask(workerTask.id, "Feature X implemented");
|
|
153
|
+
|
|
154
|
+
// Step 2: Create a follow-up task (as store-progress would)
|
|
155
|
+
const followUpTask = createTaskExtended("Worker task completed — review needed.", {
|
|
156
|
+
agentId: leadAgent.id,
|
|
157
|
+
source: "system",
|
|
158
|
+
taskType: "follow-up",
|
|
159
|
+
parentTaskId: workerTask.id,
|
|
160
|
+
slackChannelId: "C_GUARD_1",
|
|
161
|
+
slackThreadTs: "5000.0001",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Step 3: Simulate the guard logic from send-task.ts
|
|
165
|
+
// The lead's sourceTaskId would be the follow-up task
|
|
166
|
+
const sourceTask = getTaskById(followUpTask.id);
|
|
167
|
+
expect(sourceTask).not.toBeNull();
|
|
168
|
+
expect(sourceTask!.taskType).toBe("follow-up");
|
|
169
|
+
expect(sourceTask!.slackChannelId).toBe("C_GUARD_1");
|
|
170
|
+
expect(sourceTask!.slackThreadTs).toBe("5000.0001");
|
|
171
|
+
|
|
172
|
+
// The guard should find the completed worker task
|
|
173
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
174
|
+
sourceTask!.slackChannelId!,
|
|
175
|
+
sourceTask!.slackThreadTs!,
|
|
176
|
+
2880,
|
|
177
|
+
);
|
|
178
|
+
expect(recentCompleted).not.toBeNull();
|
|
179
|
+
expect(recentCompleted!.id).toBe(workerTask.id);
|
|
180
|
+
|
|
181
|
+
// → Guard would block: re-delegation should be prevented
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("allows delegation when source task is NOT a follow-up (normal behavior)", () => {
|
|
185
|
+
// Create a normal Slack task (not a follow-up)
|
|
186
|
+
const slackTask = createTaskExtended("user asked a question", {
|
|
187
|
+
agentId: leadAgent.id,
|
|
188
|
+
source: "slack",
|
|
189
|
+
taskType: "inbox",
|
|
190
|
+
slackChannelId: "C_GUARD_2",
|
|
191
|
+
slackThreadTs: "6000.0001",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Even if there are completed tasks in the thread, guard shouldn't trigger
|
|
195
|
+
// because the source task is not a "follow-up"
|
|
196
|
+
const sourceTask = getTaskById(slackTask.id);
|
|
197
|
+
expect(sourceTask).not.toBeNull();
|
|
198
|
+
expect(sourceTask!.taskType).not.toBe("follow-up");
|
|
199
|
+
|
|
200
|
+
// Guard condition: sourceTask?.taskType === "follow-up" → false
|
|
201
|
+
// → Guard does NOT block: delegation proceeds normally
|
|
202
|
+
const shouldBlock =
|
|
203
|
+
sourceTask?.taskType === "follow-up" && sourceTask.slackThreadTs && sourceTask.slackChannelId;
|
|
204
|
+
expect(shouldBlock).toBeFalsy();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("allows delegation when source task is a follow-up but thread has NO completed work", () => {
|
|
208
|
+
// Create a follow-up task in a thread with no completed work
|
|
209
|
+
const followUpTask = createTaskExtended("Worker task failed — action needed.", {
|
|
210
|
+
agentId: leadAgent.id,
|
|
211
|
+
source: "system",
|
|
212
|
+
taskType: "follow-up",
|
|
213
|
+
slackChannelId: "C_GUARD_3",
|
|
214
|
+
slackThreadTs: "7000.0001",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const sourceTask = getTaskById(followUpTask.id);
|
|
218
|
+
expect(sourceTask).not.toBeNull();
|
|
219
|
+
expect(sourceTask!.taskType).toBe("follow-up");
|
|
220
|
+
|
|
221
|
+
// No completed tasks in this thread
|
|
222
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
223
|
+
sourceTask!.slackChannelId!,
|
|
224
|
+
sourceTask!.slackThreadTs!,
|
|
225
|
+
2880,
|
|
226
|
+
);
|
|
227
|
+
expect(recentCompleted).toBeNull();
|
|
228
|
+
|
|
229
|
+
// → Guard does NOT block: first-time delegation is fine
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("allows delegation when source task is a follow-up but completed work is outside time window", () => {
|
|
233
|
+
// Create and complete a worker task, then backdate it
|
|
234
|
+
const oldWorkerTask = createTaskExtended("old task", {
|
|
235
|
+
agentId: workerAgent.id,
|
|
236
|
+
slackChannelId: "C_GUARD_4",
|
|
237
|
+
slackThreadTs: "8000.0001",
|
|
238
|
+
});
|
|
239
|
+
completeTask(oldWorkerTask.id, "done long ago");
|
|
240
|
+
|
|
241
|
+
// Backdate to 49 hours ago (beyond the 48h window)
|
|
242
|
+
const fortyNineHoursAgo = new Date(Date.now() - 49 * 60 * 60 * 1000).toISOString();
|
|
243
|
+
getDb().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
244
|
+
fortyNineHoursAgo,
|
|
245
|
+
oldWorkerTask.id,
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
// Create a follow-up in the same thread
|
|
249
|
+
const followUpTask = createTaskExtended("Worker task completed — review needed.", {
|
|
250
|
+
agentId: leadAgent.id,
|
|
251
|
+
source: "system",
|
|
252
|
+
taskType: "follow-up",
|
|
253
|
+
slackChannelId: "C_GUARD_4",
|
|
254
|
+
slackThreadTs: "8000.0001",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const sourceTask = getTaskById(followUpTask.id);
|
|
258
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
259
|
+
sourceTask!.slackChannelId!,
|
|
260
|
+
sourceTask!.slackThreadTs!,
|
|
261
|
+
2880,
|
|
262
|
+
);
|
|
263
|
+
expect(recentCompleted).toBeNull();
|
|
264
|
+
|
|
265
|
+
// → Guard does NOT block: completed work is too old
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -339,6 +339,11 @@ Task: "Fix the login page CSS"
|
|
|
339
339
|
Output:
|
|
340
340
|
Fixed the CSS alignment issue on the login form
|
|
341
341
|
|
|
342
|
+
IMPORTANT: Do NOT re-delegate or re-answer the original request. The worker has already handled it. Your job is ONLY to:
|
|
343
|
+
1. Review the output above
|
|
344
|
+
2. If the task has Slack metadata, use \`slack-reply\` to post the result to the thread (if the worker hasn't already)
|
|
345
|
+
3. Complete this follow-up task
|
|
346
|
+
|
|
342
347
|
Use \`get-task-details\` with taskId "task-abc-123" for full details.`;
|
|
343
348
|
|
|
344
349
|
expect(result.text).toBe(expected);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { closeDb, createAgent, createTaskExtended, initDb } from "../be/db";
|
|
4
|
+
import { isBotMessage } from "../slack/handlers";
|
|
5
|
+
import { hasOtherUserMention, routeMessage } from "../slack/router";
|
|
6
|
+
import type { Agent } from "../types";
|
|
7
|
+
|
|
8
|
+
const TEST_DB_PATH = "./test-slack-bot-filter.sqlite";
|
|
9
|
+
|
|
10
|
+
let leadAgent: Agent;
|
|
11
|
+
let workerAgent: Agent;
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
initDb(TEST_DB_PATH);
|
|
15
|
+
leadAgent = createAgent({
|
|
16
|
+
name: "filter-lead",
|
|
17
|
+
isLead: true,
|
|
18
|
+
status: "idle",
|
|
19
|
+
capabilities: [],
|
|
20
|
+
});
|
|
21
|
+
workerAgent = createAgent({
|
|
22
|
+
name: "filter-worker",
|
|
23
|
+
isLead: false,
|
|
24
|
+
status: "idle",
|
|
25
|
+
capabilities: [],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
closeDb();
|
|
31
|
+
try {
|
|
32
|
+
unlinkSync(TEST_DB_PATH);
|
|
33
|
+
unlinkSync(`${TEST_DB_PATH}-wal`);
|
|
34
|
+
unlinkSync(`${TEST_DB_PATH}-shm`);
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("isBotMessage", () => {
|
|
41
|
+
const BOT_ID = "UBOT123";
|
|
42
|
+
|
|
43
|
+
test("plain human message → false", () => {
|
|
44
|
+
expect(isBotMessage({ user: "UHUMAN" }, BOT_ID)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("subtype bot_message → true", () => {
|
|
48
|
+
expect(isBotMessage({ subtype: "bot_message", user: "UHUMAN" }, BOT_ID)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("bot_id present → true", () => {
|
|
52
|
+
expect(isBotMessage({ bot_id: "B001", user: "UHUMAN" }, BOT_ID)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("own bot user ID (self-posted) → true", () => {
|
|
56
|
+
expect(isBotMessage({ user: BOT_ID }, BOT_ID)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("empty event with no bot signals → false", () => {
|
|
60
|
+
expect(isBotMessage({ user: "UHUMAN" }, BOT_ID)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("hasOtherUserMention", () => {
|
|
65
|
+
const BOT_ID = "UBOT123";
|
|
66
|
+
|
|
67
|
+
test("no mentions → false", () => {
|
|
68
|
+
expect(hasOtherUserMention("hello everyone", BOT_ID)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("only our bot mentioned → false", () => {
|
|
72
|
+
expect(hasOtherUserMention(`hey <@${BOT_ID}> pls`, BOT_ID)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("different user mentioned → true", () => {
|
|
76
|
+
expect(hasOtherUserMention("hey <@UDEVIN01> wdyt", BOT_ID)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("our bot AND another user mentioned → true", () => {
|
|
80
|
+
expect(hasOtherUserMention(`<@${BOT_ID}> and <@UDEVIN01> hi`, BOT_ID)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("routeMessage — thread follow-up skips messages aimed at other users", () => {
|
|
85
|
+
const BOT_ID = "UBOT123";
|
|
86
|
+
|
|
87
|
+
test("plain follow-up (no mentions) still routes to active worker", () => {
|
|
88
|
+
const channelId = "C_BF_100";
|
|
89
|
+
const threadTs = "1100.0001";
|
|
90
|
+
createTaskExtended("original", {
|
|
91
|
+
agentId: workerAgent.id,
|
|
92
|
+
source: "slack",
|
|
93
|
+
slackChannelId: channelId,
|
|
94
|
+
slackThreadTs: threadTs,
|
|
95
|
+
slackUserId: "U_HUMAN",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const matches = routeMessage("and also the weather", BOT_ID, false, {
|
|
99
|
+
channelId,
|
|
100
|
+
threadTs,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(matches).toHaveLength(1);
|
|
104
|
+
expect(matches[0].agent.id).toBe(workerAgent.id);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("follow-up mentioning another bot (Devin) does NOT route", () => {
|
|
108
|
+
const channelId = "C_BF_200";
|
|
109
|
+
const threadTs = "1200.0001";
|
|
110
|
+
createTaskExtended("original", {
|
|
111
|
+
agentId: workerAgent.id,
|
|
112
|
+
source: "slack",
|
|
113
|
+
slackChannelId: channelId,
|
|
114
|
+
slackThreadTs: threadTs,
|
|
115
|
+
slackUserId: "U_HUMAN",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const matches = routeMessage("<@UDEVIN01> wdyt?", BOT_ID, false, {
|
|
119
|
+
channelId,
|
|
120
|
+
threadTs,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(matches).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("follow-up mentioning BOTH our bot and another bot routes to swarm", () => {
|
|
127
|
+
const channelId = "C_BF_300";
|
|
128
|
+
const threadTs = "1300.0001";
|
|
129
|
+
createTaskExtended("original", {
|
|
130
|
+
agentId: workerAgent.id,
|
|
131
|
+
source: "slack",
|
|
132
|
+
slackChannelId: channelId,
|
|
133
|
+
slackThreadTs: threadTs,
|
|
134
|
+
slackUserId: "U_HUMAN",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const matches = routeMessage(`<@${BOT_ID}> and <@UDEVIN01> please coordinate`, BOT_ID, true, {
|
|
138
|
+
channelId,
|
|
139
|
+
threadTs,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(matches).toHaveLength(1);
|
|
143
|
+
expect(matches[0].agent.id).toBe(workerAgent.id);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("no thread activity + only another bot mentioned → no match", () => {
|
|
147
|
+
const matches = routeMessage("<@UDEVIN01> hi", BOT_ID, false, {
|
|
148
|
+
channelId: "C_BF_400",
|
|
149
|
+
threadTs: "1400.0001",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(matches).toHaveLength(0);
|
|
153
|
+
// Silence unused lead warning — lead exists but should not be routed to
|
|
154
|
+
expect(leadAgent.id).toBeDefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
package/src/tools/send-task.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import {
|
|
4
4
|
createTaskExtended,
|
|
5
|
+
findCompletedTaskInThread,
|
|
5
6
|
getActiveTaskCount,
|
|
6
7
|
getAgentById,
|
|
7
8
|
getDb,
|
|
@@ -187,6 +188,36 @@ export const registerSendTaskTool = (server: McpServer) => {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// Guard: prevent re-delegation from follow-up tasks
|
|
192
|
+
// When the source task is a "follow-up" (worker completed/failed notification),
|
|
193
|
+
// check if there are completed tasks in the same Slack thread recently.
|
|
194
|
+
// This prevents the cycle: worker completes → follow-up → Lead re-delegates → repeat.
|
|
195
|
+
if (requestInfo.sourceTaskId) {
|
|
196
|
+
const sourceTask = getTaskById(requestInfo.sourceTaskId);
|
|
197
|
+
if (
|
|
198
|
+
sourceTask?.taskType === "follow-up" &&
|
|
199
|
+
sourceTask.slackThreadTs &&
|
|
200
|
+
sourceTask.slackChannelId
|
|
201
|
+
) {
|
|
202
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
203
|
+
sourceTask.slackChannelId,
|
|
204
|
+
sourceTask.slackThreadTs,
|
|
205
|
+
2880, // 48 hours in minutes
|
|
206
|
+
);
|
|
207
|
+
if (recentCompleted) {
|
|
208
|
+
const msg = `Blocked: re-delegation from follow-up task in a thread that already has completed work (task ${recentCompleted.id.slice(0, 8)}). The original request was already handled.`;
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: msg }],
|
|
211
|
+
structuredContent: {
|
|
212
|
+
yourAgentId: requestInfo.agentId,
|
|
213
|
+
success: false,
|
|
214
|
+
message: msg,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
190
221
|
const txn = getDb().transaction(() => {
|
|
191
222
|
const finalTags = tags;
|
|
192
223
|
|
package/src/tools/templates.ts
CHANGED
|
@@ -22,6 +22,11 @@ Task: "{{task_desc}}"
|
|
|
22
22
|
Output:
|
|
23
23
|
{{output_summary}}
|
|
24
24
|
|
|
25
|
+
IMPORTANT: Do NOT re-delegate or re-answer the original request. The worker has already handled it. Your job is ONLY to:
|
|
26
|
+
1. Review the output above
|
|
27
|
+
2. If the task has Slack metadata, use \`slack-reply\` to post the result to the thread (if the worker hasn't already)
|
|
28
|
+
3. Complete this follow-up task
|
|
29
|
+
|
|
25
30
|
Use \`get-task-details\` with taskId "{{task_id}}" for full details.`,
|
|
26
31
|
variables: [
|
|
27
32
|
{ name: "agent_name", description: "Worker agent name or ID prefix" },
|