@desplega.ai/agent-swarm 1.52.0 → 1.52.1
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/README.md +131 -0
- package/package.json +3 -1
- package/src/be/db.ts +27 -0
- package/src/be/migrations/runner.ts +4 -4
- package/src/commands/runner.ts +166 -14
- package/src/http/agents.ts +29 -0
- package/src/http/approval-requests.ts +63 -0
- package/src/http/index.ts +22 -2
- package/src/http/poll.ts +15 -0
- package/src/http/tasks.ts +94 -0
- package/src/linear/outbound.ts +12 -12
- package/src/providers/claude-adapter.ts +19 -3
- package/src/scheduler/scheduler.ts +24 -1
- package/src/slack/blocks.ts +1 -1
- package/src/tests/approval-requests.test.ts +214 -1
- package/src/tests/skill-sync.test.ts +1 -1
- package/src/tests/slack-blocks.test.ts +3 -2
- package/src/tests/structured-output.test.ts +1 -0
- package/src/tests/tool-call-progress.test.ts +207 -0
- package/src/tests/tool-registrar-no-input.test.ts +114 -0
- package/src/tests/update-profile-auth.test.ts +1 -0
- package/src/tools/request-human-input.ts +14 -3
- package/src/tools/store-progress.ts +31 -0
- package/src/tools/templates.ts +28 -0
- package/src/tools/utils.ts +9 -7
- package/src/workflows/executors/human-in-the-loop.ts +115 -2
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ Agent Swarm lets you run a team of AI coding agents that coordinate autonomously
|
|
|
56
56
|
- **Linear integration** — Bidirectional ticket tracker sync via OAuth + webhooks with AgentSession lifecycle and generic tracker abstraction
|
|
57
57
|
- **Portless local dev** — Friendly URLs for local development (`api.swarm.localhost:1355`) via portless proxy
|
|
58
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
|
|
59
61
|
|
|
60
62
|
## Quick Start
|
|
61
63
|
|
|
@@ -262,6 +264,135 @@ GITHUB_APP_PRIVATE_KEY=base64-encoded-key
|
|
|
262
264
|
| PR review submitted (on bot's PR) | Creates a notification task with review feedback |
|
|
263
265
|
| CI failure (on PRs with existing tasks) | Creates a CI notification task |
|
|
264
266
|
|
|
267
|
+
<details>
|
|
268
|
+
<summary><strong>Flow Diagrams</strong> (click to expand)</summary>
|
|
269
|
+
|
|
270
|
+
#### Task Creation Flow
|
|
271
|
+
|
|
272
|
+
How GitHub events become tasks in the swarm:
|
|
273
|
+
|
|
274
|
+
```mermaid
|
|
275
|
+
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px', 'nodeSpacing': 30, 'rankSpacing': 40}}}%%
|
|
276
|
+
flowchart TB
|
|
277
|
+
subgraph ENTRY["1. GitHub Webhook Entry Points"]
|
|
278
|
+
direction LR
|
|
279
|
+
E1["Issue<br/>opened/edited"]
|
|
280
|
+
E2["PR<br/>opened/edited"]
|
|
281
|
+
E3["Comment<br/>created"]
|
|
282
|
+
E4["Bot Assigned<br/>to Issue/PR"]
|
|
283
|
+
E5["Review Requested<br/>from Bot"]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
subgraph GATE["2. Trigger Gate"]
|
|
287
|
+
M{"@agent-swarm<br/>mention?"}
|
|
288
|
+
A{"Bot is<br/>assignee?"}
|
|
289
|
+
D{"Duplicate?<br/>60s TTL"}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
subgraph CREATE["3. Task Creation"]
|
|
293
|
+
LEAD["Find Lead Agent<br/>(online > offline > none)"]
|
|
294
|
+
TPL["resolveTemplate()"]
|
|
295
|
+
TASK["createTaskExtended()"]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
subgraph OUT["4. Output"]
|
|
299
|
+
ASSIGN["Task assigned<br/>to Lead"]
|
|
300
|
+
POOL["Task in pool<br/>(no lead)"]
|
|
301
|
+
REACT["eyes reaction<br/>on GitHub"]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
E1 & E2 & E3 --> M
|
|
305
|
+
E4 & E5 --> A
|
|
306
|
+
|
|
307
|
+
M -->|Yes| D
|
|
308
|
+
A -->|Yes| D
|
|
309
|
+
M & A -->|No| DROP1(("skip"))
|
|
310
|
+
|
|
311
|
+
D -->|New| LEAD
|
|
312
|
+
D -->|Dup| DROP2(("skip"))
|
|
313
|
+
|
|
314
|
+
LEAD --> TPL --> TASK
|
|
315
|
+
|
|
316
|
+
TASK -->|lead found| ASSIGN
|
|
317
|
+
TASK -.->|no lead| POOL
|
|
318
|
+
TASK --> REACT
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
[PNG fallback](assets/github-task-creation-flow.png)
|
|
322
|
+
|
|
323
|
+
#### Follow-up Flows
|
|
324
|
+
|
|
325
|
+
Events that create secondary tasks when an active task already exists for a PR:
|
|
326
|
+
|
|
327
|
+
```mermaid
|
|
328
|
+
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px'}}}%%
|
|
329
|
+
flowchart TB
|
|
330
|
+
subgraph EVENTS["GitHub Follow-up Events (require existing active task)"]
|
|
331
|
+
direction LR
|
|
332
|
+
F1["PR Closed<br/>(merged/closed)"]
|
|
333
|
+
F2["PR Synchronize<br/>(new commits)"]
|
|
334
|
+
F3["Review Submitted<br/>(approved/changes_requested)"]
|
|
335
|
+
F4["Check Run Failed"]
|
|
336
|
+
F5["Check Suite Failed"]
|
|
337
|
+
F6["Workflow Run Failed"]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
FIND{"findTaskByVcs()<br/>Active task for<br/>repo + PR number?"}
|
|
341
|
+
|
|
342
|
+
EVENTS --> FIND
|
|
343
|
+
|
|
344
|
+
FIND -->|No task| SKIP(("skip"))
|
|
345
|
+
|
|
346
|
+
subgraph FOLLOWUP["Follow-up Task Created (assigned to Lead)"]
|
|
347
|
+
direction LR
|
|
348
|
+
T1["github-pr-status<br/>PR merged/closed"]
|
|
349
|
+
T2["github-pr-update<br/>New commits pushed"]
|
|
350
|
+
T3["github-review<br/>Review feedback"]
|
|
351
|
+
T4["github-ci<br/>CI failure alert"]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
F1 --> FIND -->|task found| T1
|
|
355
|
+
F2 --> FIND -->|task found| T2
|
|
356
|
+
F3 --> FIND -->|task found| T3
|
|
357
|
+
F4 & F5 & F6 --> FIND -->|task found| T4
|
|
358
|
+
|
|
359
|
+
NOTE["All follow-up tasks reference<br/>the original task ID for routing"]
|
|
360
|
+
|
|
361
|
+
FOLLOWUP --> NOTE
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
[PNG fallback](assets/github-followup-flows.png)
|
|
365
|
+
|
|
366
|
+
#### Cancellation Flows
|
|
367
|
+
|
|
368
|
+
How unassigning the bot cancels active tasks:
|
|
369
|
+
|
|
370
|
+
```mermaid
|
|
371
|
+
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px'}}}%%
|
|
372
|
+
flowchart TB
|
|
373
|
+
subgraph EVENTS["Cancellation Events"]
|
|
374
|
+
direction LR
|
|
375
|
+
C1["Bot Unassigned<br/>from Issue"]
|
|
376
|
+
C2["Bot Unassigned<br/>from PR"]
|
|
377
|
+
C3["Review Request<br/>Removed from Bot"]
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
BOT{"isBotAssignee()"}
|
|
381
|
+
FIND{"findTaskByVcs()<br/>Active task?"}
|
|
382
|
+
CANCEL["failTask()<br/>Cancel with reason"]
|
|
383
|
+
NOOP(("no-op"))
|
|
384
|
+
|
|
385
|
+
EVENTS --> BOT
|
|
386
|
+
BOT -->|Not bot| NOOP
|
|
387
|
+
BOT -->|Is bot| FIND
|
|
388
|
+
FIND -->|No task| NOOP
|
|
389
|
+
FIND -->|Task found| CANCEL
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
[PNG fallback](assets/github-cancellation-flows.png)
|
|
393
|
+
|
|
394
|
+
</details>
|
|
395
|
+
|
|
265
396
|
### GitLab
|
|
266
397
|
|
|
267
398
|
Set up a GitLab webhook to receive events when the bot is @mentioned or assigned to issues/MRs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.52.
|
|
3
|
+
"version": "1.52.1",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"e2e:workflows:docker": "bun scripts/e2e-workflow-test.ts --with-docker",
|
|
75
75
|
"docs:mcp": "bun scripts/generate-mcp-docs.ts",
|
|
76
76
|
"docs:openapi": "bun scripts/generate-openapi.ts",
|
|
77
|
+
"docs:business-use": "bun scripts/generate-business-use-docs.ts",
|
|
77
78
|
"pm2-start": "pm2 start ecosystem.config.cjs",
|
|
78
79
|
"pm2-stop": "pm2 stop ecosystem.config.cjs",
|
|
79
80
|
"pm2-restart": "pm2 restart ecosystem.config.cjs",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"dependencies": {
|
|
94
95
|
"@ai-sdk/openai": "^3.0.41",
|
|
95
96
|
"@asteasolutions/zod-to-openapi": "^8.0.0",
|
|
97
|
+
"@desplega.ai/business-use": "^0.4.2",
|
|
96
98
|
"@desplega.ai/localtunnel": "^2.2.0",
|
|
97
99
|
"@inkjs/ui": "^2.0.0",
|
|
98
100
|
"@linear/sdk": "^77.0.0",
|
package/src/be/db.ts
CHANGED
|
@@ -940,6 +940,23 @@ export function getTasksByAgentId(agentId: string): AgentTask[] {
|
|
|
940
940
|
return taskQueries.getByAgentId().all(agentId).map(rowToAgentTask);
|
|
941
941
|
}
|
|
942
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Get the most recently updated in-progress task for an agent.
|
|
945
|
+
* Used as a fallback when X-Source-Task-Id header is missing (e.g. lead agent HITL requests).
|
|
946
|
+
*
|
|
947
|
+
* Note: if agent has multiple in-progress tasks, returns the most recently
|
|
948
|
+
* updated one. This is a best-effort fallback — the X-Source-Task-Id header
|
|
949
|
+
* is the authoritative source when available.
|
|
950
|
+
*/
|
|
951
|
+
export function getAgentCurrentTask(agentId: string): AgentTask | null {
|
|
952
|
+
const row = getDb()
|
|
953
|
+
.prepare<AgentTaskRow, [string]>(
|
|
954
|
+
"SELECT * FROM agent_tasks WHERE agentId = ? AND status = 'in_progress' ORDER BY lastUpdatedAt DESC LIMIT 1",
|
|
955
|
+
)
|
|
956
|
+
.get(agentId);
|
|
957
|
+
return row ? rowToAgentTask(row) : null;
|
|
958
|
+
}
|
|
959
|
+
|
|
943
960
|
export function getTasksByStatus(status: AgentTaskStatus): AgentTask[] {
|
|
944
961
|
return taskQueries.getByStatus().all(status).map(rowToAgentTask);
|
|
945
962
|
}
|
|
@@ -6881,6 +6898,16 @@ export function resolveApprovalRequest(
|
|
|
6881
6898
|
return row ? rowToApprovalRequest(row) : null;
|
|
6882
6899
|
}
|
|
6883
6900
|
|
|
6901
|
+
export function updateApprovalRequestNotifications(
|
|
6902
|
+
id: string,
|
|
6903
|
+
notificationChannels: Array<{ channel: string; target: string; messageTs?: string }>,
|
|
6904
|
+
): void {
|
|
6905
|
+
const now = new Date().toISOString();
|
|
6906
|
+
getDb()
|
|
6907
|
+
.prepare("UPDATE approval_requests SET notificationChannels = ?, updatedAt = ? WHERE id = ?")
|
|
6908
|
+
.run(JSON.stringify(notificationChannels), now, id);
|
|
6909
|
+
}
|
|
6910
|
+
|
|
6884
6911
|
export function listApprovalRequests(filters?: {
|
|
6885
6912
|
status?: string;
|
|
6886
6913
|
workflowRunId?: string;
|
|
@@ -129,7 +129,7 @@ export function runMigrations(db: Database): void {
|
|
|
129
129
|
const initialMigration = migrations.find((m) => m.version === 1);
|
|
130
130
|
if (initialMigration) {
|
|
131
131
|
if (shouldBootstrapInitialMigration(db)) {
|
|
132
|
-
console.
|
|
132
|
+
console.debug("[migrations] Existing database detected — bootstrapping migration tracking");
|
|
133
133
|
db.run(
|
|
134
134
|
"INSERT INTO _migrations (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
|
|
135
135
|
[
|
|
@@ -145,7 +145,7 @@ export function runMigrations(db: Database): void {
|
|
|
145
145
|
checksum: initialMigration.checksum,
|
|
146
146
|
});
|
|
147
147
|
} else {
|
|
148
|
-
console.
|
|
148
|
+
console.warn(
|
|
149
149
|
"[migrations] Existing database appears incomplete — applying 001_initial migration",
|
|
150
150
|
);
|
|
151
151
|
}
|
|
@@ -169,7 +169,7 @@ export function runMigrations(db: Database): void {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Apply migration in a transaction
|
|
172
|
-
console.
|
|
172
|
+
console.debug(`[migrations] Applying: ${migration.name}`);
|
|
173
173
|
const start = performance.now();
|
|
174
174
|
|
|
175
175
|
db.transaction(() => {
|
|
@@ -183,6 +183,6 @@ export function runMigrations(db: Database): void {
|
|
|
183
183
|
})();
|
|
184
184
|
|
|
185
185
|
const elapsed = (performance.now() - start).toFixed(1);
|
|
186
|
-
console.
|
|
186
|
+
console.debug(`[migrations] Applied: ${migration.name} (${elapsed}ms)`);
|
|
187
187
|
}
|
|
188
188
|
}
|
package/src/commands/runner.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { ensure, initialize } from "@desplega.ai/business-use";
|
|
3
4
|
import type { TemplateResponse } from "../../templates/schema.ts";
|
|
4
5
|
import { type BasePromptArgs, getBasePrompt } from "../prompts/base-prompt.ts";
|
|
5
6
|
import {
|
|
@@ -215,10 +216,94 @@ async function fetchResolvedEnv(
|
|
|
215
216
|
return env;
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
/** Tools that produce noise — skip auto-progress for these */
|
|
220
|
+
const SKIP_PROGRESS_TOOLS = new Set(["ToolSearch", "TodoRead", "TodoWrite"]);
|
|
221
|
+
|
|
222
|
+
/** Pretty labels for agent-swarm MCP tools. null = skip (meta/noise). */
|
|
223
|
+
const SWARM_TOOL_LABELS: Record<string, string | null> = {
|
|
224
|
+
"store-progress": null,
|
|
225
|
+
"get-task-details": "📋 Reviewing task details",
|
|
226
|
+
"get-tasks": "📋 Checking task list",
|
|
227
|
+
"poll-task": "📡 Polling for tasks",
|
|
228
|
+
"send-task": "📤 Delegating task",
|
|
229
|
+
"task-action": "⚡ Performing task action",
|
|
230
|
+
"join-swarm": "🔗 Joining swarm",
|
|
231
|
+
"my-agent-info": "🪪 Checking agent info",
|
|
232
|
+
"get-swarm": "👥 Checking swarm status",
|
|
233
|
+
"post-message": "💬 Sending message",
|
|
234
|
+
"read-messages": "💬 Reading messages",
|
|
235
|
+
"request-human-input": "🙋 Requesting human input",
|
|
236
|
+
"cancel-task": "🚫 Cancelling task",
|
|
237
|
+
"db-query": "🗃️ Querying database",
|
|
238
|
+
"inject-learning": "🧠 Storing learning",
|
|
239
|
+
"memory-search": "🧠 Searching memory",
|
|
240
|
+
"memory-get": "🧠 Retrieving memory",
|
|
241
|
+
"update-profile": "🪪 Updating profile",
|
|
242
|
+
// Slack
|
|
243
|
+
"slack-post": "💬 Posting to Slack",
|
|
244
|
+
"slack-reply": "💬 Replying in Slack",
|
|
245
|
+
"slack-read": "💬 Reading Slack",
|
|
246
|
+
"slack-list-channels": "💬 Listing Slack channels",
|
|
247
|
+
"slack-download-file": "📥 Downloading from Slack",
|
|
248
|
+
"slack-upload-file": "📤 Uploading to Slack",
|
|
249
|
+
// Tracker
|
|
250
|
+
"tracker-status": "📊 Checking tracker status",
|
|
251
|
+
"tracker-sync-status": "📊 Syncing tracker status",
|
|
252
|
+
"tracker-link-task": "🔗 Linking task to tracker",
|
|
253
|
+
"tracker-link-epic": "🔗 Linking epic to tracker",
|
|
254
|
+
"tracker-unlink": "🔗 Unlinking from tracker",
|
|
255
|
+
"tracker-map-agent": "🔗 Mapping agent to tracker",
|
|
256
|
+
// Epics
|
|
257
|
+
"create-epic": "📦 Creating epic",
|
|
258
|
+
"get-epic-details": "📦 Reviewing epic",
|
|
259
|
+
"list-epics": "📦 Listing epics",
|
|
260
|
+
"update-epic": "📦 Updating epic",
|
|
261
|
+
// Workflows
|
|
262
|
+
"trigger-workflow": "⚙️ Triggering workflow",
|
|
263
|
+
"get-workflow": "⚙️ Checking workflow",
|
|
264
|
+
"list-workflows": "⚙️ Listing workflows",
|
|
265
|
+
"create-workflow": "⚙️ Creating workflow",
|
|
266
|
+
// Skills
|
|
267
|
+
"skill-search": "🔎 Searching skills",
|
|
268
|
+
"skill-install": "📦 Installing skill",
|
|
269
|
+
"skill-install-remote": "📦 Installing remote skill",
|
|
270
|
+
"skill-get": "📦 Getting skill details",
|
|
271
|
+
"skill-list": "📦 Listing skills",
|
|
272
|
+
// Config
|
|
273
|
+
"get-config": "⚙️ Reading config",
|
|
274
|
+
"set-config": "⚙️ Setting config",
|
|
275
|
+
"list-config": "⚙️ Listing config",
|
|
276
|
+
// Schedules
|
|
277
|
+
"create-schedule": "📅 Creating schedule",
|
|
278
|
+
"list-schedules": "📅 Listing schedules",
|
|
279
|
+
"run-schedule-now": "📅 Running schedule",
|
|
280
|
+
// Context
|
|
281
|
+
"context-diff": "📜 Viewing context diff",
|
|
282
|
+
"context-history": "📜 Viewing context history",
|
|
283
|
+
// Channels
|
|
284
|
+
"create-channel": "📢 Creating channel",
|
|
285
|
+
"list-channels": "📢 Listing channels",
|
|
286
|
+
"delete-channel": "📢 Deleting channel",
|
|
287
|
+
// Services
|
|
288
|
+
"register-service": "🔌 Registering service",
|
|
289
|
+
"list-services": "🔌 Listing services",
|
|
290
|
+
"unregister-service": "🔌 Unregistering service",
|
|
291
|
+
"update-service-status": "🔌 Updating service status",
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/** Convert kebab-case to sentence case: "get-task-details" → "Get task details" */
|
|
295
|
+
export function humanizeToolName(name: string): string {
|
|
296
|
+
if (!name) return name;
|
|
297
|
+
return name.charAt(0).toUpperCase() + name.slice(1).replaceAll("-", " ");
|
|
298
|
+
}
|
|
299
|
+
|
|
218
300
|
/**
|
|
219
301
|
* Convert a tool call into a human-readable progress description.
|
|
302
|
+
* Returns null for noisy/meta tools that should be skipped.
|
|
220
303
|
*/
|
|
221
|
-
function toolCallToProgress(toolName: string, args: unknown): string {
|
|
304
|
+
export function toolCallToProgress(toolName: string, args: unknown): string | null {
|
|
305
|
+
if (SKIP_PROGRESS_TOOLS.has(toolName)) return null;
|
|
306
|
+
|
|
222
307
|
const a = args as Record<string, unknown>;
|
|
223
308
|
const shortPath = (p: unknown) => {
|
|
224
309
|
if (typeof p !== "string") return "";
|
|
@@ -229,30 +314,43 @@ function toolCallToProgress(toolName: string, args: unknown): string {
|
|
|
229
314
|
|
|
230
315
|
switch (toolName) {
|
|
231
316
|
case "Read":
|
|
232
|
-
return
|
|
317
|
+
return `📖 Reading ${shortPath(a.file_path)}`;
|
|
233
318
|
case "Edit":
|
|
234
319
|
case "MultiEdit":
|
|
235
|
-
return
|
|
320
|
+
return `✏️ Editing ${shortPath(a.file_path)}`;
|
|
236
321
|
case "Write":
|
|
237
|
-
return
|
|
322
|
+
return `📝 Writing ${shortPath(a.file_path)}`;
|
|
238
323
|
case "Bash":
|
|
239
|
-
return a.description ?
|
|
324
|
+
return a.description ? `⚡ ${a.description}` : "⚡ Running shell command";
|
|
240
325
|
case "Grep":
|
|
241
|
-
return
|
|
326
|
+
return `🔍 Searching for "${a.pattern}"`;
|
|
242
327
|
case "Glob":
|
|
243
|
-
return
|
|
328
|
+
return `📁 Finding files matching ${a.pattern}`;
|
|
244
329
|
case "Agent":
|
|
245
330
|
case "Task":
|
|
246
|
-
return a.description ?
|
|
331
|
+
return a.description ? `🤖 ${a.description}` : "🤖 Delegating sub-task";
|
|
247
332
|
case "Skill":
|
|
248
|
-
return
|
|
333
|
+
return `⚙️ Running /${a.skill}`;
|
|
249
334
|
default: {
|
|
250
|
-
// MCP tools: mcp__server__tool
|
|
335
|
+
// MCP tools: mcp__server__tool
|
|
251
336
|
if (toolName.startsWith("mcp__")) {
|
|
252
337
|
const parts = toolName.split("__");
|
|
253
|
-
|
|
338
|
+
if (parts.length >= 3) {
|
|
339
|
+
const server = parts[1];
|
|
340
|
+
const tool = parts.slice(2).join("__");
|
|
341
|
+
// Agent-swarm tools get pretty labels
|
|
342
|
+
if (server === "agent-swarm") {
|
|
343
|
+
const label = SWARM_TOOL_LABELS[tool];
|
|
344
|
+
if (label === null) return null; // skip
|
|
345
|
+
if (label) return label;
|
|
346
|
+
return `🔌 ${humanizeToolName(tool)}`;
|
|
347
|
+
}
|
|
348
|
+
// Other MCP servers: "🔌 server: Humanized tool"
|
|
349
|
+
return `🔌 ${server}: ${humanizeToolName(tool)}`;
|
|
350
|
+
}
|
|
351
|
+
return `🔌 ${toolName}`;
|
|
254
352
|
}
|
|
255
|
-
return
|
|
353
|
+
return `🔧 ${toolName}`;
|
|
256
354
|
}
|
|
257
355
|
}
|
|
258
356
|
}
|
|
@@ -1369,9 +1467,13 @@ async function spawnProviderProcess(
|
|
|
1369
1467
|
// Auto-progress: report tool activity as task progress (throttled)
|
|
1370
1468
|
const now = Date.now();
|
|
1371
1469
|
if (effectiveTaskId && opts.apiUrl && now - lastProgressTime >= PROGRESS_THROTTLE_MS) {
|
|
1372
|
-
lastProgressTime = now;
|
|
1373
1470
|
const progress = toolCallToProgress(event.toolName, event.args);
|
|
1374
|
-
|
|
1471
|
+
if (progress) {
|
|
1472
|
+
lastProgressTime = now;
|
|
1473
|
+
updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, progress).catch(
|
|
1474
|
+
() => {},
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1375
1477
|
}
|
|
1376
1478
|
break;
|
|
1377
1479
|
}
|
|
@@ -1579,6 +1681,22 @@ async function checkCompletedProcesses(
|
|
|
1579
1681
|
}
|
|
1580
1682
|
await ensureTaskFinished(apiConfig, role, taskId, result.exitCode, failureReason);
|
|
1581
1683
|
|
|
1684
|
+
ensure({
|
|
1685
|
+
id: "worker_process_finished",
|
|
1686
|
+
flow: "task",
|
|
1687
|
+
runId: taskId,
|
|
1688
|
+
depIds: ["worker_process_spawned"],
|
|
1689
|
+
data: {
|
|
1690
|
+
taskId,
|
|
1691
|
+
agentId: apiConfig.agentId,
|
|
1692
|
+
role,
|
|
1693
|
+
exitCode: result.exitCode,
|
|
1694
|
+
success: result.exitCode === 0,
|
|
1695
|
+
failureReason,
|
|
1696
|
+
},
|
|
1697
|
+
validator: (data) => data.exitCode === 0,
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1582
1700
|
// Commit channel activity cursors after successful processing
|
|
1583
1701
|
// If the task failed, cursors stay uncommitted so messages are re-seen on next poll
|
|
1584
1702
|
if (cursorUpdates && cursorUpdates.length > 0 && result.exitCode === 0) {
|
|
@@ -1665,6 +1783,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1665
1783
|
const { defaultPrompt, metadataType } = config;
|
|
1666
1784
|
let role = config.role;
|
|
1667
1785
|
|
|
1786
|
+
// Initialize Business-Use SDK for worker-side instrumentation
|
|
1787
|
+
initialize();
|
|
1788
|
+
|
|
1668
1789
|
// Create provider adapter based on HARNESS_PROVIDER env var (default: claude)
|
|
1669
1790
|
const adapter = createProviderAdapter(process.env.HARNESS_PROVIDER || "claude");
|
|
1670
1791
|
|
|
@@ -2308,6 +2429,24 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2308
2429
|
if (trigger) {
|
|
2309
2430
|
console.log(`[${role}] Trigger received: ${trigger.type}`);
|
|
2310
2431
|
|
|
2432
|
+
if (
|
|
2433
|
+
trigger.taskId &&
|
|
2434
|
+
(trigger.type === "task_assigned" || trigger.type === "task_offered")
|
|
2435
|
+
) {
|
|
2436
|
+
ensure({
|
|
2437
|
+
id: "worker_received",
|
|
2438
|
+
flow: "task",
|
|
2439
|
+
runId: trigger.taskId,
|
|
2440
|
+
depIds: ["started"],
|
|
2441
|
+
data: {
|
|
2442
|
+
taskId: trigger.taskId,
|
|
2443
|
+
agentId,
|
|
2444
|
+
triggerType: trigger.type,
|
|
2445
|
+
role,
|
|
2446
|
+
},
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2311
2450
|
// Build prompt based on trigger
|
|
2312
2451
|
let triggerPrompt = await buildPromptForTrigger(
|
|
2313
2452
|
trigger,
|
|
@@ -2524,6 +2663,19 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2524
2663
|
continue;
|
|
2525
2664
|
}
|
|
2526
2665
|
|
|
2666
|
+
ensure({
|
|
2667
|
+
id: "worker_process_spawned",
|
|
2668
|
+
flow: "task",
|
|
2669
|
+
runId: runningTask.taskId,
|
|
2670
|
+
depIds: ["worker_received"],
|
|
2671
|
+
data: {
|
|
2672
|
+
taskId: runningTask.taskId,
|
|
2673
|
+
agentId,
|
|
2674
|
+
role,
|
|
2675
|
+
model: taskModel,
|
|
2676
|
+
},
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2527
2679
|
// Attach trigger metadata for logging
|
|
2528
2680
|
runningTask.triggerType = trigger.type;
|
|
2529
2681
|
|
package/src/http/agents.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { ensure } from "@desplega.ai/business-use";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import {
|
|
4
5
|
createAgent,
|
|
@@ -179,6 +180,34 @@ export async function handleAgentRegister(
|
|
|
179
180
|
return { agent, created: true };
|
|
180
181
|
})();
|
|
181
182
|
|
|
183
|
+
if (result.created) {
|
|
184
|
+
ensure({
|
|
185
|
+
id: "registered",
|
|
186
|
+
flow: "agent",
|
|
187
|
+
runId: agentId,
|
|
188
|
+
data: {
|
|
189
|
+
agentId,
|
|
190
|
+
name: parsed.body.name,
|
|
191
|
+
isLead: parsed.body.isLead ?? false,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
ensure({
|
|
196
|
+
id: "reconnected",
|
|
197
|
+
flow: "agent",
|
|
198
|
+
runId: agentId,
|
|
199
|
+
depIds: ["registered"],
|
|
200
|
+
data: {
|
|
201
|
+
agentId,
|
|
202
|
+
name: parsed.body.name,
|
|
203
|
+
},
|
|
204
|
+
validator: (_data, ctx) => {
|
|
205
|
+
// Validates that registered happened before reconnected
|
|
206
|
+
return ctx.deps.length > 0;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
182
211
|
json(res, result.agent, result.created ? 201 : 200);
|
|
183
212
|
return true;
|
|
184
213
|
}
|
|
@@ -2,10 +2,13 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
createApprovalRequest,
|
|
5
|
+
createTaskExtended,
|
|
5
6
|
getApprovalRequestById,
|
|
7
|
+
getTaskById,
|
|
6
8
|
listApprovalRequests,
|
|
7
9
|
resolveApprovalRequest,
|
|
8
10
|
} from "../be/db";
|
|
11
|
+
import { resolveTemplate } from "../prompts/resolver";
|
|
9
12
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
10
13
|
import { route } from "./route-def";
|
|
11
14
|
import { json, jsonError } from "./utils";
|
|
@@ -186,6 +189,40 @@ export async function handleApprovalRequests(
|
|
|
186
189
|
});
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
// For standalone (non-workflow) requests, create a follow-up task
|
|
193
|
+
// so the requesting agent is notified of the human's response
|
|
194
|
+
if (!updated.workflowRunId && updated.sourceTaskId) {
|
|
195
|
+
const sourceTask = getTaskById(updated.sourceTaskId);
|
|
196
|
+
if (sourceTask) {
|
|
197
|
+
// Format responses for the template
|
|
198
|
+
const formattedResponses = formatResponses(
|
|
199
|
+
updated.questions as Array<{ id: string; type: string; label: string }>,
|
|
200
|
+
updated.responses as Record<string, unknown>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const { text: taskText } = resolveTemplate("hitl.follow_up", {
|
|
204
|
+
request_id: updated.id,
|
|
205
|
+
title: updated.title,
|
|
206
|
+
status: updated.status,
|
|
207
|
+
responses: formattedResponses,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
createTaskExtended(taskText, {
|
|
211
|
+
agentId: sourceTask.agentId,
|
|
212
|
+
parentTaskId: updated.sourceTaskId,
|
|
213
|
+
source: "system",
|
|
214
|
+
taskType: "hitl-follow-up",
|
|
215
|
+
tags: ["hitl", "follow-up"],
|
|
216
|
+
// Explicit Slack metadata — parentTaskId auto-inherits too,
|
|
217
|
+
// but being explicit ensures the follow-up task always gets
|
|
218
|
+
// the right thread context even if inheritance logic changes.
|
|
219
|
+
slackChannelId: sourceTask.slackChannelId ?? undefined,
|
|
220
|
+
slackThreadTs: sourceTask.slackThreadTs ?? undefined,
|
|
221
|
+
slackUserId: sourceTask.slackUserId ?? undefined,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
189
226
|
json(res, { approvalRequest: updated });
|
|
190
227
|
return true;
|
|
191
228
|
}
|
|
@@ -245,3 +282,29 @@ export async function handleApprovalRequests(
|
|
|
245
282
|
|
|
246
283
|
return false;
|
|
247
284
|
}
|
|
285
|
+
|
|
286
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
function formatResponses(
|
|
289
|
+
questions: Array<{ id: string; type: string; label: string }>,
|
|
290
|
+
responses: Record<string, unknown>,
|
|
291
|
+
): string {
|
|
292
|
+
return questions
|
|
293
|
+
.map((q) => {
|
|
294
|
+
const answer = responses[q.id];
|
|
295
|
+
let answerText: string;
|
|
296
|
+
if (answer == null) {
|
|
297
|
+
answerText = "(no answer)";
|
|
298
|
+
} else if (q.type === "approval") {
|
|
299
|
+
const a = answer as { approved?: boolean; comment?: string };
|
|
300
|
+
answerText = a.approved ? "Approved" : "Rejected";
|
|
301
|
+
if (a.comment) answerText += ` — ${a.comment}`;
|
|
302
|
+
} else if (typeof answer === "object") {
|
|
303
|
+
answerText = JSON.stringify(answer);
|
|
304
|
+
} else {
|
|
305
|
+
answerText = String(answer);
|
|
306
|
+
}
|
|
307
|
+
return `- ${q.label}: ${answerText}`;
|
|
308
|
+
})
|
|
309
|
+
.join("\n");
|
|
310
|
+
}
|
package/src/http/index.ts
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
type Server,
|
|
5
5
|
type ServerResponse,
|
|
6
6
|
} from "node:http";
|
|
7
|
+
import { assert, initialize } from "@desplega.ai/business-use";
|
|
7
8
|
import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
-
import { hasCapability } from "@/server";
|
|
9
|
+
import { getEnabledCapabilities, hasCapability } from "@/server";
|
|
9
10
|
import { initAgentMail } from "../agentmail";
|
|
10
11
|
import { closeDb } from "../be/db";
|
|
11
12
|
import { initGitHub } from "../github";
|
|
@@ -45,6 +46,7 @@ const globalState = globalThis as typeof globalThis & {
|
|
|
45
46
|
__httpServer?: Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
46
47
|
__transports?: Record<string, StreamableHTTPServerTransport>;
|
|
47
48
|
__sigintRegistered?: boolean;
|
|
49
|
+
__runId?: string;
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
// Clean up previous server on hot reload
|
|
@@ -167,10 +169,26 @@ if (!globalState.__sigintRegistered) {
|
|
|
167
169
|
process.on("SIGTERM", shutdown);
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
if (!globalState.__runId) {
|
|
173
|
+
globalState.__runId = `run_${Date.now()}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// business-use initialization (no-op if envs not set)
|
|
177
|
+
initialize();
|
|
178
|
+
|
|
170
179
|
httpServer
|
|
171
180
|
.listen(port, async () => {
|
|
172
181
|
console.log(`MCP HTTP server running on http://localhost:${port}/mcp`);
|
|
173
182
|
|
|
183
|
+
assert({
|
|
184
|
+
id: "listen",
|
|
185
|
+
flow: "api",
|
|
186
|
+
runId: globalState.__runId!,
|
|
187
|
+
data: {
|
|
188
|
+
capabilities: getEnabledCapabilities(),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
174
192
|
// Load global swarm configs into process.env (so integrations can read them)
|
|
175
193
|
// Infrastructure-level env vars take precedence — only missing keys are filled.
|
|
176
194
|
try {
|
|
@@ -205,7 +223,9 @@ httpServer
|
|
|
205
223
|
const { startScheduler } = await import("../scheduler");
|
|
206
224
|
const { getExecutorRegistry } = await import("../workflows");
|
|
207
225
|
const intervalMs = Number(process.env.SCHEDULER_INTERVAL_MS) || 10000;
|
|
208
|
-
startScheduler(getExecutorRegistry(), intervalMs
|
|
226
|
+
startScheduler(getExecutorRegistry(), intervalMs, {
|
|
227
|
+
runId: globalState.__runId!,
|
|
228
|
+
});
|
|
209
229
|
}
|
|
210
230
|
|
|
211
231
|
// Start heartbeat triage (unless disabled)
|