@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.
Files changed (55) hide show
  1. package/README.md +131 -0
  2. package/openapi.json +767 -4
  3. package/package.json +3 -1
  4. package/src/be/db.ts +669 -0
  5. package/src/be/migrations/019_skills.sql +65 -0
  6. package/src/be/migrations/020_approval_requests.sql +41 -0
  7. package/src/be/migrations/runner.ts +4 -4
  8. package/src/be/skill-parser.ts +70 -0
  9. package/src/be/skill-sync.ts +106 -0
  10. package/src/commands/runner.ts +299 -52
  11. package/src/http/agents.ts +29 -0
  12. package/src/http/approval-requests.ts +310 -0
  13. package/src/http/config.ts +3 -3
  14. package/src/http/index.ts +26 -2
  15. package/src/http/poll.ts +15 -0
  16. package/src/http/skills.ts +479 -0
  17. package/src/http/tasks.ts +94 -0
  18. package/src/linear/outbound.ts +12 -12
  19. package/src/prompts/base-prompt.ts +8 -0
  20. package/src/providers/claude-adapter.ts +19 -3
  21. package/src/scheduler/scheduler.ts +24 -1
  22. package/src/server.ts +29 -0
  23. package/src/slack/blocks.ts +1 -1
  24. package/src/tests/approval-requests.test.ts +948 -0
  25. package/src/tests/skill-parser.test.ts +178 -0
  26. package/src/tests/skill-sync.test.ts +171 -0
  27. package/src/tests/slack-blocks.test.ts +3 -2
  28. package/src/tests/structured-output.test.ts +1 -0
  29. package/src/tests/tool-annotations.test.ts +2 -1
  30. package/src/tests/tool-call-progress.test.ts +207 -0
  31. package/src/tests/tool-registrar-no-input.test.ts +114 -0
  32. package/src/tests/update-profile-auth.test.ts +1 -0
  33. package/src/tests/workflow-executors.test.ts +4 -2
  34. package/src/tools/request-human-input.ts +117 -0
  35. package/src/tools/skills/index.ts +11 -0
  36. package/src/tools/skills/skill-create.ts +105 -0
  37. package/src/tools/skills/skill-delete.ts +67 -0
  38. package/src/tools/skills/skill-get.ts +75 -0
  39. package/src/tools/skills/skill-install-remote.ts +152 -0
  40. package/src/tools/skills/skill-install.ts +101 -0
  41. package/src/tools/skills/skill-list.ts +77 -0
  42. package/src/tools/skills/skill-publish.ts +123 -0
  43. package/src/tools/skills/skill-search.ts +43 -0
  44. package/src/tools/skills/skill-sync-remote.ts +128 -0
  45. package/src/tools/skills/skill-uninstall.ts +60 -0
  46. package/src/tools/skills/skill-update.ts +128 -0
  47. package/src/tools/store-progress.ts +31 -0
  48. package/src/tools/templates.ts +28 -0
  49. package/src/tools/tool-config.ts +16 -0
  50. package/src/tools/utils.ts +9 -7
  51. package/src/types.ts +54 -0
  52. package/src/workflows/executors/human-in-the-loop.ts +273 -0
  53. package/src/workflows/executors/registry.ts +2 -0
  54. package/src/workflows/recovery.ts +72 -0
  55. package/src/workflows/resume.ts +65 -1
@@ -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 { clonePath, claudeMd: null, warning };
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 `Reading ${shortPath(a.file_path)}`;
317
+ return `📖 Reading ${shortPath(a.file_path)}`;
227
318
  case "Edit":
228
319
  case "MultiEdit":
229
- return `Editing ${shortPath(a.file_path)}`;
320
+ return `✏️ Editing ${shortPath(a.file_path)}`;
230
321
  case "Write":
231
- return `Writing ${shortPath(a.file_path)}`;
322
+ return `📝 Writing ${shortPath(a.file_path)}`;
232
323
  case "Bash":
233
- return a.description ? `${a.description}` : "Running shell command";
324
+ return a.description ? `⚡ ${a.description}` : "Running shell command";
234
325
  case "Grep":
235
- return `Searching for "${a.pattern}"`;
326
+ return `🔍 Searching for "${a.pattern}"`;
236
327
  case "Glob":
237
- return `Finding files matching ${a.pattern}`;
328
+ return `📁 Finding files matching ${a.pattern}`;
238
329
  case "Agent":
239
330
  case "Task":
240
- return a.description ? `${a.description}` : "Delegating sub-task";
331
+ return a.description ? `🤖 ${a.description}` : "🤖 Delegating sub-task";
241
332
  case "Skill":
242
- return `Running /${a.skill}`;
333
+ return `⚙️ Running /${a.skill}`;
243
334
  default: {
244
- // MCP tools: mcp__server__tool → "server:tool"
335
+ // MCP tools: mcp__server__tool
245
336
  if (toolName.startsWith("mcp__")) {
246
337
  const parts = toolName.split("__");
247
- return parts.length >= 3 ? `Using ${parts[1]}:${parts[2]}` : `Using ${toolName}`;
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 `Using ${toolName}`;
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
- updateProgressViaAPI(opts.apiUrl, opts.apiKey, effectiveTaskId, progress).catch(() => {});
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
- const runningTask = await spawnProviderProcess(
2132
- adapter,
2133
- {
2134
- prompt: resumePrompt,
2135
- logFile,
2136
- systemPrompt: resolvedSystemPrompt,
2137
- additionalArgs: resumeAdditionalArgs,
2138
- role,
2139
- apiUrl,
2140
- apiKey,
2141
- agentId,
2142
- runnerSessionId: resumeRunnerSessionId,
2143
- iteration,
2144
- taskId: task.id,
2145
- model: (task as { model?: string }).model,
2146
- cwd: resumeCwd,
2147
- },
2148
- logDir,
2149
- isYolo,
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
- const runningTask = await spawnProviderProcess(
2412
- adapter,
2413
- {
2414
- prompt: triggerPrompt,
2415
- logFile,
2416
- systemPrompt: taskSystemPrompt,
2417
- additionalArgs: effectiveAdditionalArgs,
2418
- role,
2419
- apiUrl,
2420
- apiKey,
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
- runnerSessionId: taskRunnerSessionId,
2423
- iteration,
2424
- taskId: trigger.taskId,
2674
+ role,
2425
2675
  model: taskModel,
2426
- cwd: effectiveCwd,
2427
2676
  },
2428
- logDir,
2429
- isYolo,
2430
- );
2677
+ });
2431
2678
 
2432
2679
  // Attach trigger metadata for logging
2433
2680
  runningTask.triggerType = trigger.type;
@@ -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
  }