@desplega.ai/agent-swarm 1.147.0 → 1.149.0

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 (185) hide show
  1. package/README.md +13 -1
  2. package/dist/{acp-adapter-9n319wqc.js → acp-adapter-4jncb126.js} +12 -5
  3. package/dist/{actions-2vxqvpr9.js → actions-31vp3ae7.js} +10 -9
  4. package/dist/{app-w066xfy0.js → app-1hh67x8w.js} +6 -6
  5. package/dist/{assistant-kt31dj2h.js → assistant-z66dmz6p.js} +13 -11
  6. package/dist/{boot-reembed-az5rassp.js → boot-reembed-fqzxxzjh.js} +7 -7
  7. package/dist/{boot-reembed-j3mm3rfz.js → boot-reembed-wxa8ya0s.js} +8 -8
  8. package/dist/{boot-scrub-logs-b6h54817.js → boot-scrub-logs-jnyndh79.js} +5 -5
  9. package/dist/{claude-adapter-w1z04ann.js → claude-adapter-3qpa9w0j.js} +9 -9
  10. package/dist/{claude-managed-adapter-sactwn31.js → claude-managed-adapter-qvsmh6mv.js} +2 -2
  11. package/dist/{claude-sdk-session-f0cc1kec.js → claude-sdk-session-2yxgjs5d.js} +9 -9
  12. package/dist/{cli-y6b6fb76.js → cli-08b07b4r.js} +1 -1
  13. package/dist/{cli-8fc8de14.js → cli-0z2v4nhw.js} +1 -1
  14. package/dist/{cli-de14znh8.js → cli-19f354qr.js} +2 -2
  15. package/dist/{cli-hbv0kq6w.js → cli-2qts1hys.js} +3 -3
  16. package/dist/{cli-kpv03zhj.js → cli-3apbzgyk.js} +27 -15
  17. package/dist/{cli-1th1728g.js → cli-3q5ejx9p.js} +2 -2
  18. package/dist/{cli-d8brzxjb.js → cli-4wtq5jjv.js} +2 -2
  19. package/dist/{cli-ct0et58h.js → cli-5my3bjsd.js} +1 -1
  20. package/dist/{cli-cn6mmc7f.js → cli-6664w7y4.js} +1 -1
  21. package/dist/{cli-yz3djrm0.js → cli-6pqcb4q0.js} +53 -13
  22. package/dist/{cli-719k9j2c.js → cli-7smrsr25.js} +3 -3
  23. package/dist/{cli-rw51bq3j.js → cli-9fy6nk6g.js} +1 -1
  24. package/dist/{cli-zhjmqxzh.js → cli-9hspd3dp.js} +2 -2
  25. package/dist/{cli-3tgwnf43.js → cli-aae6hz6a.js} +4 -4
  26. package/dist/{cli-89xfd7f9.js → cli-cr12paw2.js} +5 -5
  27. package/dist/{cli-b5bq9crk.js → cli-d8ftsp62.js} +8 -8
  28. package/dist/{cli-4nr988h6.js → cli-e95cx1eb.js} +3 -3
  29. package/dist/{cli-n8508nre.js → cli-gjhjfeeg.js} +1 -1
  30. package/dist/{cli-p1d5b073.js → cli-gv88e1vf.js} +1 -1
  31. package/dist/{cli-rvqz34y5.js → cli-jecc01cb.js} +1 -1
  32. package/dist/{cli-8v0g1yc8.js → cli-k0cr4kat.js} +1 -1
  33. package/dist/{cli-mmemxxdc.js → cli-kgz4np97.js} +4 -4
  34. package/dist/{cli-w9kb3bk1.js → cli-kjwt6hdf.js} +3 -1
  35. package/dist/{cli-f7dsy61y.js → cli-mnmsfd1w.js} +6 -2
  36. package/dist/{cli-54wwve05.js → cli-n3dz2942.js} +37 -26
  37. package/dist/{cli-12fz972k.js → cli-nye12xk5.js} +8 -5
  38. package/dist/{cli-tzvk9haz.js → cli-phsxnkp6.js} +20 -20
  39. package/dist/{cli-53s590z8.js → cli-q50ef8g0.js} +1 -1
  40. package/dist/{cli-f146mn5s.js → cli-qaacms9y.js} +7 -6
  41. package/dist/{cli-xqcq9y5e.js → cli-r2ap2czm.js} +1 -0
  42. package/dist/{cli-m8yrsg97.js → cli-sd5pv3b1.js} +3 -3
  43. package/dist/{cli-0b6y7kxm.js → cli-xm87hzbw.js} +1 -1
  44. package/dist/{cli-ftht3zzj.js → cli-yab68w40.js} +285 -11
  45. package/dist/{cli-atcve9y8.js → cli-yb9qhqam.js} +227 -59
  46. package/dist/{cli-smby5m17.js → cli-ygweqcn4.js} +133 -22
  47. package/dist/{cli-dttph1bw.js → cli-ykxrnd1q.js} +2 -2
  48. package/dist/{cli-m9sxbkhm.js → cli-zvz62chv.js} +3 -3
  49. package/dist/cli.js +16 -14
  50. package/dist/{codex-adapter-zw4dwsx7.js → codex-adapter-8meawydj.js} +4 -4
  51. package/dist/{codex-hook-bw8p5nh4.js → codex-hook-hfrt9shr.js} +2 -2
  52. package/dist/{codex-session-runner-k981s65v.js → codex-session-runner-gjgxyp5j.js} +4 -4
  53. package/dist/{commands-sykrxx7e.js → commands-hz3nb95f.js} +5 -5
  54. package/dist/{db-sfbsc8wt.js → db-ggz97zbm.js} +5 -5
  55. package/dist/{e2b-6hb103d8.js → e2b-p04vmtx0.js} +1 -1
  56. package/dist/{handlers-0aspcejm.js → handlers-nbzdzn9k.js} +14 -11
  57. package/dist/{hook-hxccs7m5.js → hook-bks2q1f6.js} +7 -7
  58. package/dist/{hook-4nvqj2sb.js → hook-kq9wp0ep.js} +8 -8
  59. package/dist/{http-y06z517d.js → http-hrf77v87.js} +96 -45
  60. package/dist/{index-tce1rss4.js → index-6nsbg88z.js} +443 -144
  61. package/dist/{index-x5j894zf.js → index-8hmeg8j1.js} +14 -14
  62. package/dist/{index-k8znfx0z.js → index-cqtebbhb.js} +13 -13
  63. package/dist/{index-hssww7kj.js → index-n28pw1zt.js} +15 -15
  64. package/dist/{keepalive-w733ax66.js → keepalive-dxc12gvg.js} +7 -7
  65. package/dist/{lead-75bjqa9d.js → lead-yckfn18d.js} +32 -32
  66. package/dist/{maintenance-w4f1zjck.js → maintenance-6kfb2m5e.js} +8 -8
  67. package/dist/{oauth-refresh-sweep-wdetky26.js → oauth-refresh-sweep-1x1ac722.js} +6 -6
  68. package/dist/{onboard-bb5net8s.js → onboard-dcyt1609.js} +4 -3
  69. package/dist/{opencode-adapter-fef1mrn2.js → opencode-adapter-dce4wskd.js} +2 -2
  70. package/dist/{otel-impl-0w7c14ap.js → otel-impl-tbk3e5dr.js} +2 -2
  71. package/dist/{pi-mono-adapter-2has4gnp.js → pi-mono-adapter-zvshk5cj.js} +2 -2
  72. package/dist/{pricing-refresh-9nzsrgcq.js → pricing-refresh-kaqg567e.js} +7 -7
  73. package/dist/{rbac-roles-z1jrffbt.js → rbac-roles-ammq6rd2.js} +5 -5
  74. package/dist/{rbac-roles-r3hqjp09.js → rbac-roles-cf97v1vd.js} +6 -6
  75. package/dist/{render-v2-vbr89fnb.js → render-v2-80w603vy.js} +6 -6
  76. package/dist/{seed-pricing-g62hy1pk.js → seed-pricing-zbrn83xv.js} +6 -6
  77. package/dist/{setup-gnfqnptp.js → setup-fe66kczs.js} +2 -2
  78. package/dist/{worker-kznxp4qm.js → worker-qmb2d1e2.js} +32 -32
  79. package/dist/{x-n2phr5vm.js → x-n3e0v3xa.js} +2 -2
  80. package/openapi.json +46 -4
  81. package/package.json +3 -1
  82. package/src/agentmail/handlers.ts +5 -0
  83. package/src/automation-preflight-alert.ts +41 -0
  84. package/src/be/automation-preflight.ts +3 -3
  85. package/src/be/budget-refusal-notify.ts +1 -0
  86. package/src/be/db/tasks/read.ts +5 -0
  87. package/src/be/db.ts +132 -2
  88. package/src/be/migrations/150_deferred_task_waits.sql +18 -0
  89. package/src/be/migrations/151_repair_scheduled_task_required_params.sql +39 -0
  90. package/src/be/migrations/152_routing_decisions.sql +44 -0
  91. package/src/be/scripts/typecheck.ts +33 -22
  92. package/src/be/seed-scripts/catalog/delegate.ts +5 -1
  93. package/src/be/seed-skills/bundled-files.generated.json +10 -0
  94. package/src/be/seed-skills/index.ts +3 -0
  95. package/src/be/steering.ts +1 -0
  96. package/src/be/swarm-config-guard.ts +11 -0
  97. package/src/commands/onboard/steps/post-task.tsx +1 -0
  98. package/src/github/handlers.ts +9 -0
  99. package/src/gitlab/handlers.ts +4 -0
  100. package/src/heartbeat/heartbeat.ts +8 -0
  101. package/src/http/approval-requests.ts +1 -0
  102. package/src/http/apps.ts +1 -0
  103. package/src/http/config.ts +15 -6
  104. package/src/http/mcp.ts +31 -4
  105. package/src/http/script-runs.ts +6 -0
  106. package/src/http/tasks.ts +52 -30
  107. package/src/integrations/kapso/inbound.ts +1 -0
  108. package/src/jira/sync.ts +2 -0
  109. package/src/linear/sync.ts +2 -0
  110. package/src/prompts/session-templates.ts +29 -15
  111. package/src/scheduler/deferred-task-waits.ts +123 -0
  112. package/src/scheduler/schedule-task.ts +42 -0
  113. package/src/scheduler/scheduler.ts +22 -31
  114. package/src/script-workflows/workflow-ctx.ts +2 -0
  115. package/src/scripts-runtime/types/stdlib.d.ts +38 -24
  116. package/src/scripts-runtime/types/swarm-sdk.d.ts +38 -24
  117. package/src/server.ts +10 -1
  118. package/src/slack/actions.ts +1 -0
  119. package/src/slack/assistant.ts +2 -0
  120. package/src/slack/handlers.ts +3 -0
  121. package/src/slack/thread-buffer.ts +2 -0
  122. package/src/tasks/worker-follow-up.ts +9 -0
  123. package/src/tests/acp-adapter.test.ts +35 -6
  124. package/src/tests/acp-session-token.test.ts +67 -0
  125. package/src/tests/asset-key-api.test.ts +15 -2
  126. package/src/tests/asset-key-mcp.test.ts +2 -0
  127. package/src/tests/automation-preflight-alert.test.ts +91 -0
  128. package/src/tests/claude-worker-parity.test.ts +3 -0
  129. package/src/tests/config-session-auth.test.ts +143 -0
  130. package/src/tests/defer-task.test.ts +117 -6
  131. package/src/tests/deferred-task-wake.test.ts +456 -0
  132. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  133. package/src/tests/http-api-integration.test.ts +33 -4
  134. package/src/tests/mcp-input-ergonomics.test.ts +386 -0
  135. package/src/tests/model-control.test.ts +18 -5
  136. package/src/tests/opencode-adapter.test.ts +2 -2
  137. package/src/tests/promote-draft-task-route.test.ts +4 -0
  138. package/src/tests/prompt-template-session.test.ts +13 -4
  139. package/src/tests/rbac-wire-e2e.test.ts +2 -2
  140. package/src/tests/routing-decision-persistence.test.ts +520 -0
  141. package/src/tests/routing-reason-contract.test.ts +89 -0
  142. package/src/tests/routing-reason-inventory.test.ts +55 -0
  143. package/src/tests/schedule-target-type.test.ts +48 -1
  144. package/src/tests/scheduled-task-required-params-migration.test.ts +182 -0
  145. package/src/tests/scheduled-tasks.test.ts +9 -5
  146. package/src/tests/script-connections.test.ts +4 -0
  147. package/src/tests/script-runs-http.test.ts +31 -0
  148. package/src/tests/scripts-typecheck.test.ts +11 -0
  149. package/src/tests/secret-scrubber.test.ts +12 -0
  150. package/src/tests/send-task-output-schema.test.ts +66 -1
  151. package/src/tests/send-task-requested-by.test.ts +1 -0
  152. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  153. package/src/tests/store-progress-blocked-waiting-gate.test.ts +151 -0
  154. package/src/tests/swarm-tool-result-gate.test.ts +24 -0
  155. package/src/tests/task-tool-manifest.test.ts +47 -0
  156. package/src/tests/task-tool-preload-mcp.test.ts +125 -0
  157. package/src/tests/task-tools-ctx.test.ts +2 -0
  158. package/src/tests/tool-output-agent-id.test.ts +1 -0
  159. package/src/tests/ui-followup-lead-delegation.test.ts +6 -1
  160. package/src/tests/workflow-agent-task.test.ts +24 -1
  161. package/src/tests/workflow-engine-v2.test.ts +58 -2
  162. package/src/tools/accept-steer.ts +0 -1
  163. package/src/tools/defer-task.ts +73 -21
  164. package/src/tools/memory-get.ts +10 -3
  165. package/src/tools/memory-rate.ts +19 -7
  166. package/src/tools/memory-search.ts +2 -1
  167. package/src/tools/memory-store.ts +25 -5
  168. package/src/tools/send-task.ts +40 -2
  169. package/src/tools/skills/skill-publish.ts +1 -0
  170. package/src/tools/store-progress.ts +78 -4
  171. package/src/tools/templates.ts +2 -1
  172. package/src/tools/utils.ts +33 -2
  173. package/src/types.ts +14 -1
  174. package/src/utils/acp-session-token.ts +14 -4
  175. package/src/utils/secret-scrubber.ts +2 -0
  176. package/src/utils/task-tool-manifest.ts +33 -0
  177. package/src/workflows/engine.ts +4 -1
  178. package/src/workflows/executors/agent-task.ts +7 -1
  179. package/templates/ai-toolbox.manifest.json +6 -1
  180. package/templates/skills/comms/config.json +18 -0
  181. package/templates/skills/comms/content.md +74 -0
  182. package/templates/skills/comms/files/references/ste-rules.md +73 -0
  183. package/templates/skills/comms/files/references/visual-shapes.md +112 -0
  184. package/templates/skills/swarm-scripts/SKILL.md +3 -2
  185. package/templates/skills/swarm-scripts/content.md +3 -2
@@ -154,6 +154,7 @@ export async function routeKapsoInbound(payload: KapsoWebhookPayload): Promise<K
154
154
 
155
155
  const task = await createTaskWithSiblingAwareness(await buildTaskDescription(payload), {
156
156
  agentId: mapping.agentId ?? null,
157
+ routingReason: mapping.agentId ? "human_pinned" : undefined,
157
158
  source: "system",
158
159
  taskType: "kapso-inbound",
159
160
  tags: ["kapso-whatsapp", "inbound"],
package/src/jira/sync.ts CHANGED
@@ -582,6 +582,7 @@ async function createInitialJiraTask(input: {
582
582
 
583
583
  const task = await createTaskWithSiblingAwareness(result.text, {
584
584
  agentId: lead?.id ?? "",
585
+ routingReason: lead ? "skill" : undefined,
585
586
  source: "jira",
586
587
  taskType: "jira-issue",
587
588
  requestedByUserId: input.requestedByUserId,
@@ -627,6 +628,7 @@ async function createCommentMentionTask(input: {
627
628
 
628
629
  const task = await createTaskWithSiblingAwareness(result.text, {
629
630
  agentId: lead?.id ?? "",
631
+ routingReason: lead ? "skill" : undefined,
630
632
  source: "jira",
631
633
  taskType: "jira-issue",
632
634
  requestedByUserId: input.requestedByUserId,
@@ -614,6 +614,7 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
614
614
 
615
615
  const task = await createTaskWithSiblingAwareness(templateResult.text, {
616
616
  agentId: lead?.id ?? "",
617
+ routingReason: lead ? "skill" : undefined,
617
618
  source: "linear",
618
619
  taskType: "linear-issue",
619
620
  requestedByUserId,
@@ -897,6 +898,7 @@ export async function handleAgentSessionPrompted(event: Record<string, unknown>)
897
898
 
898
899
  const task = await createTaskWithSiblingAwareness(followupResult.text, {
899
900
  agentId: lead?.id ?? "",
901
+ routingReason: lead ? "skill" : undefined,
900
902
  source: "linear",
901
903
  taskType: "linear-issue",
902
904
  requestedByUserId: promptedRequestedByUserId,
@@ -90,14 +90,15 @@ registerTemplate({
90
90
  Your output is delegation and review. Workers implement, research, analyze, and write. Data gathering, even a quick query, goes to a worker. You answer simple factual questions yourself.
91
91
 
92
92
  \`get-swarm\` is the roster. Route by capability and load.
93
- Delegate with \`send-task\`. Read a child's result with \`get-task-details\`.
93
+ \`send-task\`: include \`routingReason\` with \`agentId\`. Read results via \`get-task-details\`.
94
94
  A task states the goal, the repo URL when there is one, and the constraints. Workers know git, the skills, and \`store-progress\`.
95
95
  Delegate by the shape of the work: a workflow for multi-step or fan-out work, a schedule for recurring work, a script for bulk data, an inline \`script-run\` for a one-off bulk job you can run yourself. The \`workflow-iterate\`, \`scheduling\`, and \`swarm-scripts\` skills build them.
96
96
  Research or exploration: tell the worker to use the \`researching\` skill. A large feature: a task for the \`planning\` skill first, then a task for the \`implementing\` skill with \`parentTaskId\`. A small fix: direct implementation.
97
97
  A follow-up that continues earlier work carries \`parentTaskId\`. The worker receives the prior context.
98
- A task whose result depends on the workers' output: wait for the children with the \`wait-for-task\` script, then merge and complete the task yourself. A turn that ends with children still running leaves the task unfinished.
99
98
 
100
- A worker's completion or failure arrives as a follow-up task. Review the output and complete the follow-up. The worker's result is the answer. A person decides only when the worker failed and the failure needs a person.
99
+ Worker completion/failure triggers a follow-up by default. For longer work, \`defer-task\` with a wake-up or complete this task. Review the worker's answer and complete the follow-up. Escalate only failures needing a person.
100
+
101
+ Wait inline via \`wait-for-task\` only for an essential result expected within ~1 minute. Set \`send-task.followUpConfig.disabled=true\`; cap all waits at ~1 minute total. If still running, \`defer-task\` with a wake-up.
101
102
 
102
103
  A task from an unknown user: register them with \`manage-user\`, then continue.
103
104
  Your heartbeat runbook is the \`heartbeatMd\` profile field. Edit it with \`update-profile\`. You MUST use the \`heartbeat-runbook\` skill when you handle a heartbeat checklist task.
@@ -229,16 +230,20 @@ registerTemplate({
229
230
  defaultBody: `
230
231
  ## How you write
231
232
 
232
- These rules cover everything a person reads from you: Slack, PR and issue comments, tickets, email, pages, task output.
233
+ For human-facing replies and artifacts: lead with the result. Use plain words, active voice, one idea per sentence, and sentence case headings.
234
+ State blockers and reasons. Preserve meaningful uncertainty.
235
+ Omit em dashes, filler, sign-offs, praise, repetition, step narration, and generic offers to elaborate.
236
+
237
+ Simple replies: one to three sentences. Routine replies: under 120 words, using short paragraphs or up to three bullets.
238
+ Use more detail for requested depth or essential evidence, caveats, or instructions.
239
+ Include context only when it changes understanding or action. Link extensive supporting material when useful, but make the reply self-contained.
240
+ Do not shorten investigation, required artifacts, or schema-defined output to meet reply limits.
233
241
 
234
- Lead with the result. Context comes after.
235
- One idea per sentence. Active voice with a named actor.
236
- Sentence case headings. Plain words: use, help, many, if.
237
- Keep a hedge only when you are unsure. "May have failed" stays "may have failed".
238
- When something is broken, blocked, or a bad idea, say so and say why.
239
- Reply in the requester's language, at the depth they asked for. A one-line question gets the answer first.
240
- A Requester Profile section, when present, wins on tone, depth, and format. Correctness wins over style.
241
- Em dashes, filler, sign-offs, and praise of the question are out.
242
+ Follow the current request and Requester Profile for language, tone, depth, and format. Correctness wins over style.
243
+ Use the \`comms\` skill when available for requested simplification or clarity rewrites. Ordinary replies need no skill invocation.
244
+ Use Visual mode only for explicit visual requests with delivery tools available in the active channel.
245
+ Requests to show data are not visual requests. New facts, diagnosis, and actions still require work.
246
+ Output schemas and channel delivery rules, including Slack's engine-owned delivery, override skill defaults.
242
247
  `,
243
248
  variables: [],
244
249
  category: "system",
@@ -316,6 +321,15 @@ registerTemplate({
316
321
  // lists are rendered at the call site and interpolated into the static text)
317
322
  // ============================================================================
318
323
 
324
+ registerTemplate({
325
+ eventType: "system.agent.tool_preload",
326
+ header: "",
327
+ defaultBody:
328
+ "This session preloads selected swarm tools. If a tool definition is already available in context, call it directly. Use your harness tool search only for tools whose definitions are not yet available.",
329
+ variables: [],
330
+ category: "system",
331
+ });
332
+
319
333
  registerTemplate({
320
334
  eventType: "system.agent.tools_skills",
321
335
  header: "",
@@ -386,8 +400,8 @@ This swarm runs in **scripts-only mode**. The ONLY swarm MCP tools available are
386
400
  The script authoring contract in the \`swarm-scripts\` skill (entry signature, \`ctx\` shape, secret handling) applies here unchanged. The full SDK is \`ctx.swarm.*\`: task lifecycle (\`task_get\`, \`task_send\`, \`task_storeProgress\`, \`task_action\`, \`task_list\`), Slack (\`slack_reply\`, \`slack_post\`, \`slack_read\`), memory, kv, swarm info (\`swarm_get\`, \`agent_info\`), and more. Responses are usually wrapped; prefer \`res?.data ?? res\`.
387
401
 
388
402
  **Built-in coordination scripts, USE THESE FIRST (\`script-run\` with \`name\` + \`args\`):**
389
- - \`delegate\` {agentName, task, parentTaskId?} → subtask for an agent by name; returns {taskId}
390
- - \`wait-for-task\` {taskId} → waits up to ~25s for a terminal state; returns {done, status, output}; while done=false call it again
403
+ - \`delegate\` {agentName, task, routingReason, parentTaskId?} → subtask for an agent by name; returns {taskId}
404
+ - \`wait-for-task\` {taskId} → waits up to ~25s for a terminal state; returns {done, status, output}; only for a child expected to finish within about a minute whose result the answer requires. Bound all calls to about a minute total; if still done=false, use \`defer-task\` with a wake-up to collect the result.
391
405
  - \`get-child-outputs\` {parentTaskId} → all children with status+output
392
406
  - \`complete-task\` {taskId, output} → THE way to finish your assigned task
393
407
  - \`report-progress\` {taskId, note} → progress update
@@ -397,7 +411,7 @@ Rules of the road:
397
411
  - Prefer a built-in script over inline source; write inline TypeScript only for logic no built-in covers. Check \`script-search\` first, and \`script-query-types\` for the live \`swarm-sdk.d.ts\` before authoring anything non-trivial.
398
412
  - \`taskId\` is NOT ambient inside scripts; pass it explicitly via \`args\`.
399
413
  - Report progress and completion via \`complete-task\` / \`report-progress\` (or \`ctx.swarm.task_storeProgress\` inline). This is how you update, complete, or fail your task; there is no other way.
400
- - Scripts are killed after ~30s and stdout is capped at 1 MB. Never sleep/loop longer than ~25s inside one script; chain \`wait-for-task\` calls instead.
414
+ - Scripts are killed after ~30s and stdout is capped at 1 MB. Never sleep/loop longer than ~25s inside one script. Default to the child's automatic follow-up; chained \`wait-for-task\` calls are only for the inline exception above and must stay within about a minute total.
401
415
  - Aggregate inside the script and return only the derived result; never dump raw data.
402
416
  - Batch related SDK calls into a single script when it reduces round trips.
403
417
  `,
@@ -0,0 +1,123 @@
1
+ import { getDbClient, getScheduledTaskById, getUserById, updateScheduledTask } from "@/be/db";
2
+ import type { AgentTask } from "@/types";
3
+ import { workflowEventBus } from "@/workflows/event-bus";
4
+ import { createStandaloneScheduleTask } from "./schedule-task";
5
+
6
+ export type TaskWakeEvent = "task.completed" | "task.failed" | "settled";
7
+
8
+ type TaskWait = {
9
+ scheduleId: string;
10
+ taskId: string;
11
+ eventName: TaskWakeEvent;
12
+ status: "pending" | "fired";
13
+ };
14
+
15
+ /** Undefined means a normal schedule; an empty result means another caller won. */
16
+ export async function dispatchDeferredTaskWait(
17
+ scheduleId: string,
18
+ reason: "ceiling" | "task.completed" | "task.failed",
19
+ extraTags: string[] = [],
20
+ ): Promise<{ task?: AgentTask } | undefined> {
21
+ const client = getDbClient();
22
+ // Keep ordinary schedule dispatch off the write-lock path.
23
+ if (
24
+ !(await client.get<TaskWait>("SELECT * FROM deferred_task_waits WHERE scheduleId = ?", [
25
+ scheduleId,
26
+ ]))
27
+ )
28
+ return undefined;
29
+ return client.transaction(async () => {
30
+ const wait = await client.get<TaskWait>(
31
+ "SELECT * FROM deferred_task_waits WHERE scheduleId = ?",
32
+ [scheduleId],
33
+ );
34
+ if (!wait || wait.status !== "pending") return {};
35
+ if (reason !== "ceiling" && wait.eventName !== "settled" && wait.eventName !== reason)
36
+ return {};
37
+ let schedule = await getScheduledTaskById(scheduleId);
38
+ if (!schedule?.enabled) return {};
39
+ const now = new Date().toISOString();
40
+ const claimed = await client.run(
41
+ "UPDATE deferred_task_waits SET status = 'fired', firedBy = ?, resolvedAt = ?, updated_at = ? WHERE scheduleId = ? AND status = 'pending'",
42
+ [reason, now, now, scheduleId],
43
+ );
44
+ if (!claimed.changes) return {};
45
+ if (schedule.createdBy && !(await getUserById(schedule.createdBy))) {
46
+ schedule = { ...schedule, createdBy: undefined };
47
+ }
48
+ if (!schedule.taskTemplate) throw new Error(`Schedule "${schedule.name}" has no taskTemplate`);
49
+ const cause = reason === "ceiling" ? "ceiling expired" : `${reason} for task ${wait.taskId}`;
50
+ const task = await createStandaloneScheduleTask(
51
+ {
52
+ ...schedule,
53
+ taskTemplate: `${schedule.taskTemplate}\n\nWake-up cause: ${cause}.`,
54
+ },
55
+ extraTags,
56
+ );
57
+ await client.run("UPDATE deferred_task_waits SET childTaskId = ? WHERE scheduleId = ?", [
58
+ task.id,
59
+ scheduleId,
60
+ ]);
61
+ await updateScheduledTask(scheduleId, {
62
+ enabled: false,
63
+ nextRunAt: null,
64
+ lastRunAt: now,
65
+ lastUpdatedAt: now,
66
+ consecutiveErrors: 0,
67
+ lastErrorAt: null,
68
+ lastErrorMessage: null,
69
+ });
70
+ return { task };
71
+ });
72
+ }
73
+
74
+ /** Reconcile durable producer state, including terminal transitions missed by this process. */
75
+ export async function reconcileDeferredTaskWaits(taskId?: string): Promise<void> {
76
+ const rows = await getDbClient().query<TaskWait & { taskStatus: "completed" | "failed" }>(
77
+ `SELECT w.*, t.status AS taskStatus FROM deferred_task_waits w
78
+ JOIN agent_tasks t ON t.id = w.taskId
79
+ JOIN scheduled_tasks s ON s.id = w.scheduleId
80
+ WHERE w.status = 'pending' AND s.enabled = 1 AND t.status IN ('completed', 'failed')
81
+ AND NOT EXISTS (
82
+ SELECT 1 FROM scheduled_tasks d
83
+ WHERE d.parentTaskId = t.id AND d.taskType = 'deferred' AND d.enabled = 1
84
+ )
85
+ AND (w.eventName = 'settled' OR w.eventName = 'task.' || t.status)
86
+ ${taskId ? "AND w.taskId = ?" : ""}`,
87
+ taskId ? [taskId] : [],
88
+ );
89
+ for (const row of rows) {
90
+ try {
91
+ await dispatchDeferredTaskWait(row.scheduleId, `task.${row.taskStatus}`);
92
+ } catch (err) {
93
+ // A broken wait must not stop other event wakes or the ceiling poller.
94
+ console.error(`[Scheduler] Deferred task wake failed for ${row.scheduleId}:`, err);
95
+ }
96
+ }
97
+ }
98
+
99
+ function onTaskTerminal(payload: unknown): void {
100
+ if (
101
+ !payload ||
102
+ typeof payload !== "object" ||
103
+ !("taskId" in payload) ||
104
+ typeof payload.taskId !== "string"
105
+ )
106
+ return;
107
+ void reconcileDeferredTaskWaits(payload.taskId).catch((err) => {
108
+ console.error("[Scheduler] Deferred task event wake failed:", err);
109
+ });
110
+ }
111
+
112
+ /** Two fixed listeners read durable waits, so boot needs no per-wait in-memory registry. */
113
+ export async function initDeferredTaskWaits(): Promise<void> {
114
+ stopDeferredTaskWaits();
115
+ workflowEventBus.on("task.completed", onTaskTerminal);
116
+ workflowEventBus.on("task.failed", onTaskTerminal);
117
+ await reconcileDeferredTaskWaits();
118
+ }
119
+
120
+ export function stopDeferredTaskWaits(): void {
121
+ workflowEventBus.off("task.completed", onTaskTerminal);
122
+ workflowEventBus.off("task.failed", onTaskTerminal);
123
+ }
@@ -0,0 +1,42 @@
1
+ import { getDbClient } from "@/be/db";
2
+ import { scheduleContextKey } from "@/tasks/context-key";
3
+ import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
4
+ import type { AgentTask, ScheduledTask } from "@/types";
5
+
6
+ export async function createStandaloneScheduleTask(
7
+ schedule: ScheduledTask,
8
+ extraTags: string[] = [],
9
+ ): Promise<AgentTask> {
10
+ if (!schedule.taskTemplate) {
11
+ throw new Error(`Schedule "${schedule.name}" has no taskTemplate (targetType=agent-task)`);
12
+ }
13
+ const task = await createTaskWithSiblingAwareness(schedule.taskTemplate, {
14
+ key: schedule.key,
15
+ creatorAgentId: schedule.createdByAgentId,
16
+ taskType: schedule.taskType,
17
+ tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, ...extraTags],
18
+ priority: schedule.priority,
19
+ agentId: schedule.targetAgentId,
20
+ routingReason: schedule.targetAgentId ? "human_pinned" : undefined,
21
+ model: schedule.model,
22
+ modelTier: schedule.modelTier,
23
+ scheduleId: schedule.id,
24
+ source: "schedule",
25
+ requestedByUserId: schedule.createdBy,
26
+ contextKey: scheduleContextKey({ scheduleId: schedule.id }),
27
+ // Set only by `defer-task`. An explicit parent wins over the sibling-awareness
28
+ // auto-wiring (see withSiblingAwareness in src/tasks/sibling-awareness.ts), so
29
+ // the wake-up run continues the deferred task rather than a random sibling.
30
+ parentTaskId: schedule.parentTaskId,
31
+ });
32
+ // Both timer and event dispatch wrap this helper in a transaction. Transfer
33
+ // pending watchers before the resume task becomes visible or its schedule is
34
+ // disabled; their own schedules retain the original ceilings.
35
+ if (schedule.taskType === "deferred" && schedule.parentTaskId) {
36
+ await getDbClient().run(
37
+ "UPDATE deferred_task_waits SET taskId = ?, updated_at = ? WHERE status = 'pending' AND taskId = ?",
38
+ [task.id, new Date().toISOString(), schedule.parentTaskId],
39
+ );
40
+ }
41
+ return task;
42
+ }
@@ -1,5 +1,6 @@
1
1
  import { ensure } from "@desplega.ai/business-use";
2
2
  import { CronExpressionParser } from "cron-parser";
3
+ import { notifyAutomationPreflightFailure } from "@/automation-preflight-alert";
3
4
  import {
4
5
  getAutomationSetupStates,
5
6
  isNeedsSetupFailure,
@@ -24,8 +25,6 @@ import {
24
25
  import { buildScriptCredentialBindings } from "@/be/script-credential-broker";
25
26
  import { getScript } from "@/be/scripts/db";
26
27
  import { runScript } from "@/scripts-runtime/loader";
27
- import { scheduleContextKey } from "@/tasks/context-key";
28
- import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
29
28
  import { telemetry } from "@/telemetry";
30
29
  import type { AgentTask, ScheduledTask } from "@/types";
31
30
  import { getExecutorRegistry as getWorkflowExecutorRegistry } from "@/workflows";
@@ -33,6 +32,16 @@ import { startWorkflowExecution } from "@/workflows/engine";
33
32
  import type { ExecutorRegistry } from "@/workflows/executors/registry";
34
33
  import { handleScheduleTrigger } from "@/workflows/triggers";
35
34
 
35
+ import {
36
+ dispatchDeferredTaskWait,
37
+ initDeferredTaskWaits,
38
+ reconcileDeferredTaskWaits,
39
+ stopDeferredTaskWaits,
40
+ } from "./deferred-task-waits";
41
+ import { createStandaloneScheduleTask } from "./schedule-task";
42
+
43
+ export { createStandaloneScheduleTask } from "./schedule-task";
44
+
36
45
  let schedulerInterval: ReturnType<typeof setInterval> | null = null;
37
46
  let isProcessing = false;
38
47
  let executorRegistry: ExecutorRegistry | null = null;
@@ -53,33 +62,6 @@ function resolveExecutorRegistry(): ExecutorRegistry | null {
53
62
  }
54
63
  }
55
64
 
56
- export async function createStandaloneScheduleTask(
57
- schedule: ScheduledTask,
58
- extraTags: string[] = [],
59
- ): Promise<AgentTask> {
60
- if (!schedule.taskTemplate) {
61
- throw new Error(`Schedule "${schedule.name}" has no taskTemplate (targetType=agent-task)`);
62
- }
63
- return await createTaskWithSiblingAwareness(schedule.taskTemplate, {
64
- key: schedule.key,
65
- creatorAgentId: schedule.createdByAgentId,
66
- taskType: schedule.taskType,
67
- tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, ...extraTags],
68
- priority: schedule.priority,
69
- agentId: schedule.targetAgentId,
70
- model: schedule.model,
71
- modelTier: schedule.modelTier,
72
- scheduleId: schedule.id,
73
- source: "schedule",
74
- requestedByUserId: schedule.createdBy,
75
- contextKey: scheduleContextKey({ scheduleId: schedule.id }),
76
- // Set only by `defer-task`. An explicit parent wins over the sibling-awareness
77
- // auto-wiring (see withSiblingAwareness in src/tasks/sibling-awareness.ts), so
78
- // the wake-up run continues the deferred task rather than a random sibling.
79
- parentTaskId: schedule.parentTaskId,
80
- });
81
- }
82
-
83
65
  /**
84
66
  * Execute a schedule's `targetType='script'` target directly via the
85
67
  * scripts-runtime — no agent/LLM in the loop. Reuses the same `runScript()`
@@ -151,12 +133,16 @@ export async function dispatchScheduleTarget(
151
133
  schedule: ScheduledTask,
152
134
  extraTags: string[] = [],
153
135
  ): Promise<DispatchScheduleResult> {
136
+ const deferred = await dispatchDeferredTaskWait(schedule.id, "ceiling", extraTags);
137
+ if (deferred) return { triggeredWorkflows: false, ...deferred };
138
+
154
139
  const preflight = preflightAutomation(
155
140
  schedulePreflightInput(schedule),
156
141
  await getAutomationSetupStates(),
157
142
  );
158
143
  if (preflight.state === "needs_setup") {
159
- await recordSchedulePreflightFailure(schedule.id, preflight.failureReason!);
144
+ const recorded = await recordSchedulePreflightFailure(schedule.id, preflight.failureReason!);
145
+ if (recorded) await notifyAutomationPreflightFailure(preflight);
160
146
  throw new AutomationNeedsSetupError(preflight.failureReason!);
161
147
  }
162
148
  // Params are safe and integrations are ready. Only now render values into
@@ -489,7 +475,10 @@ export function startScheduler(
489
475
  console.log(`[Scheduler] Starting with ${intervalMs}ms polling interval`);
490
476
 
491
477
  // Recover missed schedules from downtime, then run normal processing
492
- void recoverMissedSchedules().then(() => processSchedules());
478
+ void initDeferredTaskWaits()
479
+ .then(() => recoverMissedSchedules())
480
+ .then(() => processSchedules())
481
+ .catch((err) => console.error("[Scheduler] Recovery failed:", err));
493
482
 
494
483
  schedulerInterval = setInterval(async () => {
495
484
  await processSchedules();
@@ -523,6 +512,7 @@ async function processSchedules(): Promise<void> {
523
512
  isProcessing = true;
524
513
 
525
514
  try {
515
+ await reconcileDeferredTaskWaits();
526
516
  const dueSchedules = await getDueScheduledTasks();
527
517
 
528
518
  for (const schedule of dueSchedules) {
@@ -541,6 +531,7 @@ async function processSchedules(): Promise<void> {
541
531
  * Stop the scheduler polling loop.
542
532
  */
543
533
  export function stopScheduler(): void {
534
+ stopDeferredTaskWaits();
544
535
  if (schedulerInterval) {
545
536
  clearInterval(schedulerInterval);
546
537
  schedulerInterval = null;
@@ -32,6 +32,8 @@ type AgentTaskConfig = {
32
32
  template?: string;
33
33
  task?: string;
34
34
  agentId?: string;
35
+ routingReason?: "skill" | "continuity" | "overflow" | "human_pinned" | "reroute_fault";
36
+ routingNote?: string;
35
37
  tags?: string[];
36
38
  priority?: number;
37
39
  offerMode?: boolean;
@@ -28,6 +28,41 @@ declare module "swarm-sdk" {
28
28
  export type ScriptFsMode = "none" | "workspace-rw";
29
29
  export type ScriptApiRawOptions = { raw: true };
30
30
  export type ScriptApiDefaultOptions = { raw?: false };
31
+ export type RoutingReason =
32
+ | "skill"
33
+ | "continuity"
34
+ | "overflow"
35
+ | "human_pinned"
36
+ | "reroute_fault";
37
+ export type TaskSendArgs = Record<string, unknown> & {
38
+ task: string;
39
+ routingNote?: string;
40
+ } & (
41
+ | { agentId: string; routingReason: RoutingReason }
42
+ | { agentId?: never; routingReason?: never }
43
+ );
44
+ export type AgentTaskStepConfig = {
45
+ template?: string;
46
+ task?: string;
47
+ agentId?: string;
48
+ routingReason?: RoutingReason;
49
+ routingNote?: string;
50
+ tags?: string[];
51
+ priority?: number;
52
+ offerMode?: boolean;
53
+ dir?: string;
54
+ vcsRepo?: string;
55
+ model?: string;
56
+ parentTaskId?: string;
57
+ requestedByUserId?: string;
58
+ outputSchema?: Record<string, unknown>;
59
+ /** Wait for the dispatched task to reach a terminal status before resolving. Default: true. */
60
+ waitForCompletion?: boolean;
61
+ /** Max ms to wait for a terminal status before throwing. Default: 2h. Only used when waitForCompletion is true. */
62
+ timeoutMs?: number;
63
+ /** Throw when the task ends failed/cancelled/superseded (default), or resolve with {taskId,status,error} when false. */
64
+ failOnTaskFailure?: boolean;
65
+ };
31
66
 
32
67
  export interface ScriptApiRawResult {
33
68
  ok: boolean;
@@ -147,6 +182,7 @@ declare module "swarm-sdk" {
147
182
  taskId: string;
148
183
  delayMs?: number;
149
184
  runAt?: string;
185
+ wakeOn?: { event: "task.completed" | "task.failed" | "settled"; taskId: string };
150
186
  summary: string;
151
187
  note: string;
152
188
  checks?: string[];
@@ -373,7 +409,7 @@ declare module "swarm-sdk" {
373
409
  }): Promise<unknown>;
374
410
 
375
411
  // --- write: tasks ---
376
- task_send(args: Record<string, unknown>): Promise<unknown>;
412
+ task_send(args: TaskSendArgs): Promise<unknown>;
377
413
  task_cancel(args: { taskId: string }): Promise<unknown>;
378
414
  task_steer(args: {
379
415
  taskId: string;
@@ -579,29 +615,7 @@ declare module "swarm-sdk" {
579
615
  label: string,
580
616
  config: { prompt: string; model?: string; schema?: Record<string, unknown> },
581
617
  ): Promise<unknown>;
582
- agentTask(
583
- label: string,
584
- config: {
585
- template?: string;
586
- task?: string;
587
- agentId?: string;
588
- tags?: string[];
589
- priority?: number;
590
- offerMode?: boolean;
591
- dir?: string;
592
- vcsRepo?: string;
593
- model?: string;
594
- parentTaskId?: string;
595
- requestedByUserId?: string;
596
- outputSchema?: Record<string, unknown>;
597
- /** Wait for the dispatched task to reach a terminal status before resolving. Default: true. */
598
- waitForCompletion?: boolean;
599
- /** Max ms to wait for a terminal status before throwing. Default: 2h. Only used when waitForCompletion is true. */
600
- timeoutMs?: number;
601
- /** Throw when the task ends failed/cancelled/superseded (default), or resolve with {taskId,status,error} when false. */
602
- failOnTaskFailure?: boolean;
603
- },
604
- ): Promise<unknown>;
618
+ agentTask(label: string, config: AgentTaskStepConfig): Promise<unknown>;
605
619
  swarmScript(
606
620
  label: string,
607
621
  config: {
@@ -10,6 +10,41 @@ declare module "swarm-sdk" {
10
10
  export type ScriptFsMode = "none" | "workspace-rw";
11
11
  export type ScriptApiRawOptions = { raw: true };
12
12
  export type ScriptApiDefaultOptions = { raw?: false };
13
+ export type RoutingReason =
14
+ | "skill"
15
+ | "continuity"
16
+ | "overflow"
17
+ | "human_pinned"
18
+ | "reroute_fault";
19
+ export type TaskSendArgs = Record<string, unknown> & {
20
+ task: string;
21
+ routingNote?: string;
22
+ } & (
23
+ | { agentId: string; routingReason: RoutingReason }
24
+ | { agentId?: never; routingReason?: never }
25
+ );
26
+ export type AgentTaskStepConfig = {
27
+ template?: string;
28
+ task?: string;
29
+ agentId?: string;
30
+ routingReason?: RoutingReason;
31
+ routingNote?: string;
32
+ tags?: string[];
33
+ priority?: number;
34
+ offerMode?: boolean;
35
+ dir?: string;
36
+ vcsRepo?: string;
37
+ model?: string;
38
+ parentTaskId?: string;
39
+ requestedByUserId?: string;
40
+ outputSchema?: Record<string, unknown>;
41
+ /** Wait for the dispatched task to reach a terminal status before resolving. Default: true. */
42
+ waitForCompletion?: boolean;
43
+ /** Max ms to wait for a terminal status before throwing. Default: 2h. Only used when waitForCompletion is true. */
44
+ timeoutMs?: number;
45
+ /** Throw when the task ends failed/cancelled/superseded (default), or resolve with {taskId,status,error} when false. */
46
+ failOnTaskFailure?: boolean;
47
+ };
13
48
 
14
49
  export interface ScriptApiRawResult {
15
50
  ok: boolean;
@@ -129,6 +164,7 @@ declare module "swarm-sdk" {
129
164
  taskId: string;
130
165
  delayMs?: number;
131
166
  runAt?: string;
167
+ wakeOn?: { event: "task.completed" | "task.failed" | "settled"; taskId: string };
132
168
  summary: string;
133
169
  note: string;
134
170
  checks?: string[];
@@ -355,7 +391,7 @@ declare module "swarm-sdk" {
355
391
  }): Promise<unknown>;
356
392
 
357
393
  // --- write: tasks ---
358
- task_send(args: Record<string, unknown>): Promise<unknown>;
394
+ task_send(args: TaskSendArgs): Promise<unknown>;
359
395
  task_cancel(args: { taskId: string }): Promise<unknown>;
360
396
  task_steer(args: {
361
397
  taskId: string;
@@ -561,29 +597,7 @@ declare module "swarm-sdk" {
561
597
  label: string,
562
598
  config: { prompt: string; model?: string; schema?: Record<string, unknown> },
563
599
  ): Promise<unknown>;
564
- agentTask(
565
- label: string,
566
- config: {
567
- template?: string;
568
- task?: string;
569
- agentId?: string;
570
- tags?: string[];
571
- priority?: number;
572
- offerMode?: boolean;
573
- dir?: string;
574
- vcsRepo?: string;
575
- model?: string;
576
- parentTaskId?: string;
577
- requestedByUserId?: string;
578
- outputSchema?: Record<string, unknown>;
579
- /** Wait for the dispatched task to reach a terminal status before resolving. Default: true. */
580
- waitForCompletion?: boolean;
581
- /** Max ms to wait for a terminal status before throwing. Default: 2h. Only used when waitForCompletion is true. */
582
- timeoutMs?: number;
583
- /** Throw when the task ends failed/cancelled/superseded (default), or resolve with {taskId,status,error} when false. */
584
- failOnTaskFailure?: boolean;
585
- },
586
- ): Promise<unknown>;
600
+ agentTask(label: string, config: AgentTaskStepConfig): Promise<unknown>;
587
601
  swarmScript(
588
602
  label: string,
589
603
  config: {
package/src/server.ts CHANGED
@@ -7,6 +7,7 @@ import { seedPricingFromModelsDev } from "./be/seed-pricing";
7
7
  import { isSteeringEnabled } from "./be/steering";
8
8
  import { registerGithubTaskReactions } from "./github/task-reactions";
9
9
  import { loadGlobalConfigsIntoEnv } from "./http/core";
10
+ import { resolveTemplate } from "./prompts/resolver";
10
11
  import { isRbacEnabled } from "./rbac";
11
12
  import { registerAcceptSteerTool } from "./tools/accept-steer";
12
13
  import { registerAppDiffTool } from "./tools/app-diff";
@@ -163,6 +164,7 @@ import { registerUnregisterServiceTool } from "./tools/unregister-service";
163
164
  // Profiles capability
164
165
  import { registerUpdateProfileTool } from "./tools/update-profile";
165
166
  import { registerUpdateServiceStatusTool } from "./tools/update-service-status";
167
+ import { setPreloadedTools } from "./tools/utils";
166
168
  import {
167
169
  registerReplyWhatsappMessageTool,
168
170
  registerSendWhatsappMessageTool,
@@ -274,7 +276,9 @@ export function isScriptsOnlyMcp(): boolean {
274
276
  return resolveScriptsOnlyMode({ env: process.env.SCRIPTS_ONLY_MCP });
275
277
  }
276
278
 
277
- export async function createServer(opts: { scriptsOnly?: boolean; fullSurface?: boolean } = {}) {
279
+ export async function createServer(
280
+ opts: { scriptsOnly?: boolean; fullSurface?: boolean; preloadedTools?: readonly string[] } = {},
281
+ ) {
278
282
  // Reload env
279
283
  await loadGlobalConfigsIntoEnv(true);
280
284
 
@@ -316,12 +320,17 @@ export async function createServer(opts: { scriptsOnly?: boolean; fullSurface?:
316
320
  description: pkg.description,
317
321
  },
318
322
  {
323
+ ...(opts.preloadedTools?.length
324
+ ? { instructions: resolveTemplate("system.agent.tool_preload", {}).text }
325
+ : {}),
319
326
  capabilities: {
320
327
  logging: {},
321
328
  },
322
329
  },
323
330
  );
324
331
 
332
+ if (opts.preloadedTools?.length) setPreloadedTools(server, opts.preloadedTools);
333
+
325
334
  // Scripts-only surface (experimental code-mode): register just the script
326
335
  // catalog tools and stop. script-connections / script-apis stay out — they
327
336
  // are lead-only security admin and excluded from the scripts SDK too.
@@ -84,6 +84,7 @@ export function registerActionHandlers(app: App): void {
84
84
  });
85
85
  const followUpTask = await createTaskWithSiblingAwareness(followUpText, {
86
86
  agentId: lead?.id,
87
+ routingReason: lead ? "skill" : undefined,
87
88
  source: "slack",
88
89
  parentTaskId: taskId,
89
90
  slackChannelId: originalTask.slackChannelId,
@@ -166,6 +166,7 @@ export function createAssistant(): Assistant {
166
166
  renderedMessageText,
167
167
  {
168
168
  agentId: workingAgent.id,
169
+ routingReason: "continuity",
169
170
  source: "slack",
170
171
  slackChannelId: channelId,
171
172
  slackThreadTs: threadTs,
@@ -247,6 +248,7 @@ export function createAssistant(): Assistant {
247
248
  renderedMessageText + channelContext,
248
249
  {
249
250
  agentId: lead.id,
251
+ routingReason: "skill",
250
252
  source: "slack",
251
253
  slackChannelId: channelId,
252
254
  slackThreadTs: threadTs,