@desplega.ai/agent-swarm 1.51.2 → 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/openapi.json +767 -4
- package/package.json +3 -1
- package/src/be/db.ts +669 -0
- package/src/be/migrations/019_skills.sql +65 -0
- package/src/be/migrations/020_approval_requests.sql +41 -0
- package/src/be/migrations/runner.ts +4 -4
- package/src/be/skill-parser.ts +70 -0
- package/src/be/skill-sync.ts +106 -0
- package/src/commands/runner.ts +299 -52
- package/src/http/agents.ts +29 -0
- package/src/http/approval-requests.ts +310 -0
- package/src/http/config.ts +3 -3
- package/src/http/index.ts +26 -2
- package/src/http/poll.ts +15 -0
- package/src/http/skills.ts +479 -0
- package/src/http/tasks.ts +94 -0
- package/src/linear/outbound.ts +12 -12
- package/src/prompts/base-prompt.ts +8 -0
- package/src/providers/claude-adapter.ts +19 -3
- package/src/scheduler/scheduler.ts +24 -1
- package/src/server.ts +29 -0
- package/src/slack/blocks.ts +1 -1
- package/src/tests/approval-requests.test.ts +948 -0
- package/src/tests/skill-parser.test.ts +178 -0
- package/src/tests/skill-sync.test.ts +171 -0
- package/src/tests/slack-blocks.test.ts +3 -2
- package/src/tests/structured-output.test.ts +1 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- 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/tests/workflow-executors.test.ts +4 -2
- package/src/tools/request-human-input.ts +117 -0
- package/src/tools/skills/index.ts +11 -0
- package/src/tools/skills/skill-create.ts +105 -0
- package/src/tools/skills/skill-delete.ts +67 -0
- package/src/tools/skills/skill-get.ts +75 -0
- package/src/tools/skills/skill-install-remote.ts +152 -0
- package/src/tools/skills/skill-install.ts +101 -0
- package/src/tools/skills/skill-list.ts +77 -0
- package/src/tools/skills/skill-publish.ts +123 -0
- package/src/tools/skills/skill-search.ts +43 -0
- package/src/tools/skills/skill-sync-remote.ts +128 -0
- package/src/tools/skills/skill-uninstall.ts +60 -0
- package/src/tools/skills/skill-update.ts +128 -0
- package/src/tools/store-progress.ts +31 -0
- package/src/tools/templates.ts +28 -0
- package/src/tools/tool-config.ts +16 -0
- package/src/tools/utils.ts +9 -7
- package/src/types.ts +54 -0
- package/src/workflows/executors/human-in-the-loop.ts +273 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/recovery.ts +72 -0
- package/src/workflows/resume.ts +65 -1
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 {
|
|
@@ -92,6 +93,10 @@ async function ensureRepoForTask(
|
|
|
92
93
|
} else {
|
|
93
94
|
await Bun.$`git clone --branch ${defaultBranch} --single-branch ${url} ${clonePath}`.quiet();
|
|
94
95
|
}
|
|
96
|
+
// Validate the clone actually created the directory
|
|
97
|
+
if (!existsSync(clonePath)) {
|
|
98
|
+
throw new Error(`Clone command succeeded but directory ${clonePath} does not exist`);
|
|
99
|
+
}
|
|
95
100
|
console.log(`[${role}] Cloned ${name}`);
|
|
96
101
|
} else {
|
|
97
102
|
console.log(`[${role}] Repo ${name} already cloned at ${clonePath}`);
|
|
@@ -114,7 +119,9 @@ async function ensureRepoForTask(
|
|
|
114
119
|
const errorMsg = (err as Error).message;
|
|
115
120
|
console.warn(`[${role}] Error setting up repo ${name}: ${errorMsg}`);
|
|
116
121
|
const warning = `Failed to clone/setup repo "${name}" at ${clonePath}: ${errorMsg}. The repo may not be available. You may need to clone it manually.`;
|
|
117
|
-
return
|
|
122
|
+
// Only return clonePath if the directory actually exists (clone may have failed)
|
|
123
|
+
const cloneExists = existsSync(clonePath);
|
|
124
|
+
return { clonePath: cloneExists ? clonePath : "", claudeMd: null, warning };
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -209,10 +216,94 @@ async function fetchResolvedEnv(
|
|
|
209
216
|
return env;
|
|
210
217
|
}
|
|
211
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
|
+
|
|
212
300
|
/**
|
|
213
301
|
* Convert a tool call into a human-readable progress description.
|
|
302
|
+
* Returns null for noisy/meta tools that should be skipped.
|
|
214
303
|
*/
|
|
215
|
-
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
|
+
|
|
216
307
|
const a = args as Record<string, unknown>;
|
|
217
308
|
const shortPath = (p: unknown) => {
|
|
218
309
|
if (typeof p !== "string") return "";
|
|
@@ -223,30 +314,43 @@ function toolCallToProgress(toolName: string, args: unknown): string {
|
|
|
223
314
|
|
|
224
315
|
switch (toolName) {
|
|
225
316
|
case "Read":
|
|
226
|
-
return
|
|
317
|
+
return `📖 Reading ${shortPath(a.file_path)}`;
|
|
227
318
|
case "Edit":
|
|
228
319
|
case "MultiEdit":
|
|
229
|
-
return
|
|
320
|
+
return `✏️ Editing ${shortPath(a.file_path)}`;
|
|
230
321
|
case "Write":
|
|
231
|
-
return
|
|
322
|
+
return `📝 Writing ${shortPath(a.file_path)}`;
|
|
232
323
|
case "Bash":
|
|
233
|
-
return a.description ?
|
|
324
|
+
return a.description ? `⚡ ${a.description}` : "⚡ Running shell command";
|
|
234
325
|
case "Grep":
|
|
235
|
-
return
|
|
326
|
+
return `🔍 Searching for "${a.pattern}"`;
|
|
236
327
|
case "Glob":
|
|
237
|
-
return
|
|
328
|
+
return `📁 Finding files matching ${a.pattern}`;
|
|
238
329
|
case "Agent":
|
|
239
330
|
case "Task":
|
|
240
|
-
return a.description ?
|
|
331
|
+
return a.description ? `🤖 ${a.description}` : "🤖 Delegating sub-task";
|
|
241
332
|
case "Skill":
|
|
242
|
-
return
|
|
333
|
+
return `⚙️ Running /${a.skill}`;
|
|
243
334
|
default: {
|
|
244
|
-
// MCP tools: mcp__server__tool
|
|
335
|
+
// MCP tools: mcp__server__tool
|
|
245
336
|
if (toolName.startsWith("mcp__")) {
|
|
246
337
|
const parts = toolName.split("__");
|
|
247
|
-
|
|
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}`;
|
|
248
352
|
}
|
|
249
|
-
return
|
|
353
|
+
return `🔧 ${toolName}`;
|
|
250
354
|
}
|
|
251
355
|
}
|
|
252
356
|
}
|
|
@@ -1363,9 +1467,13 @@ async function spawnProviderProcess(
|
|
|
1363
1467
|
// Auto-progress: report tool activity as task progress (throttled)
|
|
1364
1468
|
const now = Date.now();
|
|
1365
1469
|
if (effectiveTaskId && opts.apiUrl && now - lastProgressTime >= PROGRESS_THROTTLE_MS) {
|
|
1366
|
-
lastProgressTime = now;
|
|
1367
1470
|
const progress = toolCallToProgress(event.toolName, event.args);
|
|
1368
|
-
|
|
1471
|
+
if (progress) {
|
|
1472
|
+
lastProgressTime = now;
|
|
1473
|
+
updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, progress).catch(
|
|
1474
|
+
() => {},
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1369
1477
|
}
|
|
1370
1478
|
break;
|
|
1371
1479
|
}
|
|
@@ -1573,6 +1681,22 @@ async function checkCompletedProcesses(
|
|
|
1573
1681
|
}
|
|
1574
1682
|
await ensureTaskFinished(apiConfig, role, taskId, result.exitCode, failureReason);
|
|
1575
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
|
+
|
|
1576
1700
|
// Commit channel activity cursors after successful processing
|
|
1577
1701
|
// If the task failed, cursors stay uncommitted so messages are re-seen on next poll
|
|
1578
1702
|
if (cursorUpdates && cursorUpdates.length > 0 && result.exitCode === 0) {
|
|
@@ -1659,6 +1783,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1659
1783
|
const { defaultPrompt, metadataType } = config;
|
|
1660
1784
|
let role = config.role;
|
|
1661
1785
|
|
|
1786
|
+
// Initialize Business-Use SDK for worker-side instrumentation
|
|
1787
|
+
initialize();
|
|
1788
|
+
|
|
1662
1789
|
// Create provider adapter based on HARNESS_PROVIDER env var (default: claude)
|
|
1663
1790
|
const adapter = createProviderAdapter(process.env.HARNESS_PROVIDER || "claude");
|
|
1664
1791
|
|
|
@@ -1692,6 +1819,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1692
1819
|
let agentClaudeMd: string | undefined;
|
|
1693
1820
|
let agentProfileName: string | undefined;
|
|
1694
1821
|
let agentDescription: string | undefined;
|
|
1822
|
+
let agentSkillsSummary: { name: string; description: string }[] | undefined;
|
|
1695
1823
|
|
|
1696
1824
|
// Per-task repo context — set when processing a task with githubRepo
|
|
1697
1825
|
let currentRepoContext: BasePromptArgs["repoContext"] | undefined;
|
|
@@ -1710,6 +1838,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1710
1838
|
toolsMd: agentToolsMd,
|
|
1711
1839
|
claudeMd: agentClaudeMd,
|
|
1712
1840
|
repoContext: currentRepoContext,
|
|
1841
|
+
skillsSummary: agentSkillsSummary,
|
|
1713
1842
|
});
|
|
1714
1843
|
};
|
|
1715
1844
|
|
|
@@ -1937,6 +2066,34 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
1937
2066
|
}
|
|
1938
2067
|
}
|
|
1939
2068
|
|
|
2069
|
+
// Fetch installed skills for system prompt
|
|
2070
|
+
try {
|
|
2071
|
+
const skillsResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills`, {
|
|
2072
|
+
headers: {
|
|
2073
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2074
|
+
"X-Agent-ID": agentId,
|
|
2075
|
+
},
|
|
2076
|
+
});
|
|
2077
|
+
if (skillsResp.ok) {
|
|
2078
|
+
const skillsData = (await skillsResp.json()) as {
|
|
2079
|
+
skills: {
|
|
2080
|
+
name: string;
|
|
2081
|
+
description: string;
|
|
2082
|
+
isActive: boolean;
|
|
2083
|
+
isEnabled: boolean;
|
|
2084
|
+
}[];
|
|
2085
|
+
};
|
|
2086
|
+
agentSkillsSummary = skillsData.skills
|
|
2087
|
+
.filter((s) => s.isActive && s.isEnabled)
|
|
2088
|
+
.map((s) => ({ name: s.name, description: s.description }));
|
|
2089
|
+
if (agentSkillsSummary.length > 0) {
|
|
2090
|
+
console.log(`[${role}] Loaded ${agentSkillsSummary.length} skills for system prompt`);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
} catch {
|
|
2094
|
+
// Non-fatal — skills are optional
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1940
2097
|
// Rebuild system prompt with identity
|
|
1941
2098
|
basePrompt = await buildSystemPrompt();
|
|
1942
2099
|
resolvedSystemPrompt = additionalSystemPrompt
|
|
@@ -2005,6 +2162,37 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2005
2162
|
}
|
|
2006
2163
|
}
|
|
2007
2164
|
|
|
2165
|
+
// ========== Sync skills to filesystem ==========
|
|
2166
|
+
try {
|
|
2167
|
+
console.log(`[${role}] Syncing skills to filesystem...`);
|
|
2168
|
+
const syncHeaders: Record<string, string> = {
|
|
2169
|
+
"Content-Type": "application/json",
|
|
2170
|
+
"X-Agent-ID": agentId,
|
|
2171
|
+
};
|
|
2172
|
+
if (apiKey) syncHeaders.Authorization = `Bearer ${apiKey}`;
|
|
2173
|
+
const syncRes = await fetch(`${swarmUrl}/api/skills/sync-filesystem`, {
|
|
2174
|
+
method: "POST",
|
|
2175
|
+
headers: syncHeaders,
|
|
2176
|
+
});
|
|
2177
|
+
if (syncRes.ok) {
|
|
2178
|
+
const syncResult = (await syncRes.json()) as {
|
|
2179
|
+
synced: number;
|
|
2180
|
+
removed: number;
|
|
2181
|
+
errors: string[];
|
|
2182
|
+
};
|
|
2183
|
+
console.log(
|
|
2184
|
+
`[${role}] Skills synced: ${syncResult.synced} written, ${syncResult.removed} removed`,
|
|
2185
|
+
);
|
|
2186
|
+
if (syncResult.errors.length > 0) {
|
|
2187
|
+
console.warn(`[${role}] Skill sync errors: ${syncResult.errors.join(", ")}`);
|
|
2188
|
+
}
|
|
2189
|
+
} else {
|
|
2190
|
+
console.warn(`[${role}] Skill sync failed: HTTP ${syncRes.status}`);
|
|
2191
|
+
}
|
|
2192
|
+
} catch (err) {
|
|
2193
|
+
console.warn(`[${role}] Skill sync failed: ${(err as Error).message}`);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2008
2196
|
// ========== Resume paused tasks with PRIORITY ==========
|
|
2009
2197
|
// Check for paused tasks from previous shutdown and resume them before normal polling
|
|
2010
2198
|
try {
|
|
@@ -2128,26 +2316,36 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2128
2316
|
// Per-task runner session ID so session logs are scoped to this task
|
|
2129
2317
|
const resumeRunnerSessionId = crypto.randomUUID();
|
|
2130
2318
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2319
|
+
let runningTask: RunningTask;
|
|
2320
|
+
try {
|
|
2321
|
+
runningTask = await spawnProviderProcess(
|
|
2322
|
+
adapter,
|
|
2323
|
+
{
|
|
2324
|
+
prompt: resumePrompt,
|
|
2325
|
+
logFile,
|
|
2326
|
+
systemPrompt: resolvedSystemPrompt,
|
|
2327
|
+
additionalArgs: resumeAdditionalArgs,
|
|
2328
|
+
role,
|
|
2329
|
+
apiUrl,
|
|
2330
|
+
apiKey,
|
|
2331
|
+
agentId,
|
|
2332
|
+
runnerSessionId: resumeRunnerSessionId,
|
|
2333
|
+
iteration,
|
|
2334
|
+
taskId: task.id,
|
|
2335
|
+
model: (task as { model?: string }).model,
|
|
2336
|
+
cwd: resumeCwd,
|
|
2337
|
+
},
|
|
2338
|
+
logDir,
|
|
2339
|
+
isYolo,
|
|
2340
|
+
);
|
|
2341
|
+
} catch (spawnErr) {
|
|
2342
|
+
const errMsg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
2343
|
+
console.error(
|
|
2344
|
+
`[${role}] Failed to spawn process for resumed task ${task.id.slice(0, 8)}: ${errMsg}`,
|
|
2345
|
+
);
|
|
2346
|
+
await ensureTaskFinished(apiConfig, role, task.id, 1, `Spawn failed: ${errMsg}`);
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2151
2349
|
|
|
2152
2350
|
state.activeTasks.set(task.id, runningTask);
|
|
2153
2351
|
registerActiveSession(apiConfig, {
|
|
@@ -2231,6 +2429,24 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2231
2429
|
if (trigger) {
|
|
2232
2430
|
console.log(`[${role}] Trigger received: ${trigger.type}`);
|
|
2233
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
|
+
|
|
2234
2450
|
// Build prompt based on trigger
|
|
2235
2451
|
let triggerPrompt = await buildPromptForTrigger(
|
|
2236
2452
|
trigger,
|
|
@@ -2408,26 +2624,57 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2408
2624
|
const taskRunnerSessionId = crypto.randomUUID();
|
|
2409
2625
|
|
|
2410
2626
|
// Spawn without blocking (await to set up session, but process runs async)
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2627
|
+
let runningTask: RunningTask;
|
|
2628
|
+
try {
|
|
2629
|
+
runningTask = await spawnProviderProcess(
|
|
2630
|
+
adapter,
|
|
2631
|
+
{
|
|
2632
|
+
prompt: triggerPrompt,
|
|
2633
|
+
logFile,
|
|
2634
|
+
systemPrompt: taskSystemPrompt,
|
|
2635
|
+
additionalArgs: effectiveAdditionalArgs,
|
|
2636
|
+
role,
|
|
2637
|
+
apiUrl,
|
|
2638
|
+
apiKey,
|
|
2639
|
+
agentId,
|
|
2640
|
+
runnerSessionId: taskRunnerSessionId,
|
|
2641
|
+
iteration,
|
|
2642
|
+
taskId: trigger.taskId,
|
|
2643
|
+
model: taskModel,
|
|
2644
|
+
cwd: effectiveCwd,
|
|
2645
|
+
},
|
|
2646
|
+
logDir,
|
|
2647
|
+
isYolo,
|
|
2648
|
+
);
|
|
2649
|
+
} catch (spawnErr) {
|
|
2650
|
+
const errMsg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
2651
|
+
console.error(
|
|
2652
|
+
`[${role}] Failed to spawn process for task ${trigger.taskId?.slice(0, 8) || "unknown"}: ${errMsg}`,
|
|
2653
|
+
);
|
|
2654
|
+
if (trigger.taskId) {
|
|
2655
|
+
await ensureTaskFinished(
|
|
2656
|
+
apiConfig,
|
|
2657
|
+
role,
|
|
2658
|
+
trigger.taskId,
|
|
2659
|
+
1,
|
|
2660
|
+
`Spawn failed: ${errMsg}`,
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
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,
|
|
2421
2673
|
agentId,
|
|
2422
|
-
|
|
2423
|
-
iteration,
|
|
2424
|
-
taskId: trigger.taskId,
|
|
2674
|
+
role,
|
|
2425
2675
|
model: taskModel,
|
|
2426
|
-
cwd: effectiveCwd,
|
|
2427
2676
|
},
|
|
2428
|
-
|
|
2429
|
-
isYolo,
|
|
2430
|
-
);
|
|
2677
|
+
});
|
|
2431
2678
|
|
|
2432
2679
|
// Attach trigger metadata for logging
|
|
2433
2680
|
runningTask.triggerType = trigger.type;
|
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
|
}
|