@desplega.ai/agent-swarm 1.123.0 → 1.124.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 (234) hide show
  1. package/README.md +2 -1
  2. package/dist/{actions-e4c9f0xy.js → actions-z3d0fk7z.js} +6 -6
  3. package/dist/{app-vmvmj2v3.js → app-q2rc9tdr.js} +4 -4
  4. package/dist/{assistant-yn7a792a.js → assistant-3stjq7fz.js} +9 -9
  5. package/dist/{boot-reembed-xg1r86h2.js → boot-reembed-5867whnb.js} +3 -3
  6. package/dist/{boot-reembed-chxty50a.js → boot-reembed-v6r1df32.js} +4 -4
  7. package/dist/{boot-scrub-logs-exwddvp5.js → boot-scrub-logs-8b8jegk3.js} +2 -2
  8. package/dist/{cli-1592nfd6.js → cli-0925phzv.js} +1 -1
  9. package/dist/{cli-hbbxmhec.js → cli-2yr6edr7.js} +4 -4
  10. package/dist/{cli-8cq4z4x7.js → cli-3z4q09tn.js} +182 -19
  11. package/dist/{cli-vz9drmce.js → cli-5jn6wnd9.js} +17 -8
  12. package/dist/{cli-5arfywtm.js → cli-5ncdb7ff.js} +19 -9
  13. package/dist/{cli-3tv9z4wx.js → cli-92bqgbgy.js} +1 -1
  14. package/dist/{cli-fjf04hpa.js → cli-dq2ssyf0.js} +48 -56
  15. package/dist/{cli-mngsj9fx.js → cli-gkvf8d83.js} +1 -1
  16. package/dist/{cli-mepk1j0t.js → cli-gzjsamwc.js} +2 -2
  17. package/dist/{cli-c9m59f83.js → cli-h89g0qrd.js} +1 -1
  18. package/dist/{cli-n8mf7x4b.js → cli-jp4mdj9d.js} +2 -2
  19. package/dist/{cli-822d4bjk.js → cli-kk58tg33.js} +7 -7
  20. package/dist/{cli-9hj1adgp.js → cli-mcez7z4p.js} +1 -1
  21. package/dist/{cli-byv3720g.js → cli-mmk5vagg.js} +2 -2
  22. package/dist/{cli-zdp11t8m.js → cli-p0ec4vv6.js} +1 -1
  23. package/dist/{cli-h2say6xk.js → cli-p0zgv8pj.js} +3 -1
  24. package/dist/{cli-8ef5ky75.js → cli-perb20ma.js} +1 -1
  25. package/dist/{cli-zkj6f2sk.js → cli-qf7acvyr.js} +90 -4
  26. package/dist/{cli-kmm1rwmp.js → cli-qwsagsmg.js} +1 -1
  27. package/dist/{cli-we91ywvy.js → cli-sqv6c9b8.js} +6 -6
  28. package/dist/{cli-np0yhmmk.js → cli-vm20sh8d.js} +3 -3
  29. package/dist/{cli-p11rf59d.js → cli-wn3m6q54.js} +3 -3
  30. package/dist/{cli-mkpf4ndf.js → cli-x268say8.js} +2976 -5621
  31. package/dist/{cli-ngbsk4yx.js → cli-y43n987v.js} +1 -1
  32. package/dist/{cli-4509mwm5.js → cli-ykthjw29.js} +8 -8
  33. package/dist/{cli-9eka6jwv.js → cli-zxb72ctk.js} +2 -2
  34. package/dist/cli.js +12 -10
  35. package/dist/{commands-sjk3zfyr.js → commands-sda76yn8.js} +2 -2
  36. package/dist/{db-2je5agjb.js → db-vn9e4rqj.js} +6 -2
  37. package/dist/{e2b-2s4psnbf.js → e2b-q345y8s4.js} +0 -4
  38. package/dist/{handlers-yzv369hf.js → handlers-xfja18ac.js} +9 -9
  39. package/dist/{hook-rfsj2r8x.js → hook-2medrp47.js} +1 -40
  40. package/dist/{http-v9rr144m.js → http-937pyjfc.js} +99 -44
  41. package/dist/{index-nr2199m6.js → index-2bqd2q9v.js} +11 -11
  42. package/dist/{index-3xtk777g.js → index-gw4en7jg.js} +9 -9
  43. package/dist/{index-c3tan44f.js → index-knqczmnm.js} +10 -10
  44. package/dist/{index-smjdc3h9.js → index-vah60vxp.js} +8 -8
  45. package/dist/{keepalive-5ds53bjd.js → keepalive-20rd6hxh.js} +5 -5
  46. package/dist/{lead-5xhez25b.js → lead-7w2k3a49.js} +21 -21
  47. package/dist/{maintenance-b43d24q6.js → maintenance-p3x918yw.js} +4 -4
  48. package/dist/{oauth-refresh-sweep-qe286vn2.js → oauth-refresh-sweep-shm0nzm4.js} +4 -4
  49. package/dist/{onboard-c4r3ds3z.js → onboard-py9dnf54.js} +6 -2
  50. package/dist/{otel-impl-nv3rjw61.js → otel-impl-7yjkfg68.js} +1 -1
  51. package/dist/{pi-mono-adapter-2syx1x4d.js → pi-mono-adapter-z6h98fva.js} +4 -38
  52. package/dist/{pricing-refresh-5ct2wp4k.js → pricing-refresh-3snb6370.js} +4 -4
  53. package/dist/{rbac-roles-m502w0qv.js → rbac-roles-5e72bbpk.js} +3 -3
  54. package/dist/{rbac-roles-b82cxpxn.js → rbac-roles-gqxdp8st.js} +4 -4
  55. package/dist/{seed-pricing-jhjphbf0.js → seed-pricing-bg42d7h2.js} +3 -3
  56. package/dist/{setup-zbaspydq.js → setup-nzk3kbbq.js} +2 -2
  57. package/dist/{worker-snvhpax5.js → worker-0p1jdzkm.js} +21 -21
  58. package/openapi.json +146 -1
  59. package/package.json +3 -1
  60. package/src/be/db.ts +81 -4
  61. package/src/be/models-catalog.ts +156 -0
  62. package/src/be/modelsdev-cache.ts +14 -1
  63. package/src/be/pricing-refresh.ts +2 -0
  64. package/src/be/scripts/typecheck.ts +17 -4
  65. package/src/be/seed-scripts/catalog/task-context-gathering.ts +1 -1
  66. package/src/commands/onboard/env-generator.ts +12 -0
  67. package/src/e2b/env.ts +0 -4
  68. package/src/hooks/hook.ts +0 -67
  69. package/src/http/all-routes.ts +1 -0
  70. package/src/http/index.ts +2 -0
  71. package/src/http/models-catalog.ts +65 -0
  72. package/src/http/workflows.ts +16 -4
  73. package/src/prompts/base-prompt.ts +8 -7
  74. package/src/prompts/session-templates.ts +106 -12
  75. package/src/providers/pi-mono-adapter.ts +12 -2
  76. package/src/providers/pi-mono-extension.ts +0 -64
  77. package/src/providers/pricing-sources.md +16 -1
  78. package/src/scripts-runtime/eval-harness.ts +18 -1
  79. package/src/scripts-runtime/swarm-sdk.ts +10 -0
  80. package/src/scripts-runtime/types/stdlib.d.ts +13 -0
  81. package/src/scripts-runtime/types/swarm-sdk.d.ts +13 -0
  82. package/src/server-user.ts +4 -7
  83. package/src/slack/blocks.ts +21 -8
  84. package/src/slack/responses.ts +27 -51
  85. package/src/slack/watcher.ts +22 -5
  86. package/src/telemetry.ts +169 -3
  87. package/src/tests/base-prompt.test.ts +79 -0
  88. package/src/tests/mcp-tools-user.test.ts +62 -16
  89. package/src/tests/mcp-tools.test.ts +6 -3
  90. package/src/tests/memory-edit.test.ts +21 -4
  91. package/src/tests/model-groups-live-catalog.test.ts +65 -0
  92. package/src/tests/models-catalog.test.ts +119 -0
  93. package/src/tests/oauth-access-token-tool.test.ts +71 -1
  94. package/src/tests/pricing-refresh.test.ts +25 -0
  95. package/src/tests/prompt-template-session.test.ts +142 -5
  96. package/src/tests/rbac-charact-skills.test.ts +81 -14
  97. package/src/tests/script-apis-mcp.test.ts +56 -8
  98. package/src/tests/scripts-mcp-e2e.test.ts +157 -8
  99. package/src/tests/scripts-typecheck.test.ts +17 -0
  100. package/src/tests/sdk-allowlist.test.ts +34 -1
  101. package/src/tests/seed-scripts.test.ts +43 -0
  102. package/src/tests/slack-blocks.test.ts +2 -1
  103. package/src/tests/slack-inline-output.test.ts +24 -30
  104. package/src/tests/slack-watcher.test.ts +42 -0
  105. package/src/tests/steer-task-tool.test.ts +37 -8
  106. package/src/tests/steering-transport.test.ts +18 -3
  107. package/src/tests/swarm-tool-result-gate.test.ts +399 -0
  108. package/src/tests/task-tools-ctx.test.ts +49 -17
  109. package/src/tests/task-tools-ownership.test.ts +41 -13
  110. package/src/tests/telemetry-init.test.ts +376 -2
  111. package/src/tests/tool-output-agent-id.test.ts +14 -2
  112. package/src/tests/workflow-http-v2.test.ts +116 -0
  113. package/src/tools/accept-steer.ts +45 -53
  114. package/src/tools/cancel-task.ts +31 -41
  115. package/src/tools/context-diff.ts +19 -63
  116. package/src/tools/context-history.ts +22 -51
  117. package/src/tools/create-channel.ts +24 -39
  118. package/src/tools/create-metric.ts +19 -82
  119. package/src/tools/create-page.ts +20 -81
  120. package/src/tools/credential-bindings/tool.ts +125 -246
  121. package/src/tools/db-query.ts +13 -24
  122. package/src/tools/delete-channel.ts +25 -71
  123. package/src/tools/delete-page.ts +27 -75
  124. package/src/tools/get-metrics.ts +29 -22
  125. package/src/tools/get-swarm.ts +44 -15
  126. package/src/tools/get-task-details.ts +150 -32
  127. package/src/tools/get-tasks.ts +63 -29
  128. package/src/tools/inject-learning.ts +8 -38
  129. package/src/tools/join-swarm.ts +45 -47
  130. package/src/tools/kv/kv-delete.ts +13 -37
  131. package/src/tools/kv/kv-get.ts +38 -29
  132. package/src/tools/kv/kv-incr.ts +28 -56
  133. package/src/tools/kv/kv-list.ts +40 -31
  134. package/src/tools/kv/kv-set.ts +34 -76
  135. package/src/tools/list-channels.ts +24 -17
  136. package/src/tools/list-services.ts +37 -45
  137. package/src/tools/manage-user.ts +53 -66
  138. package/src/tools/mcp-servers/mcp-server-create.ts +19 -55
  139. package/src/tools/mcp-servers/mcp-server-delete.ts +9 -39
  140. package/src/tools/mcp-servers/mcp-server-get.ts +9 -35
  141. package/src/tools/mcp-servers/mcp-server-install.ts +13 -60
  142. package/src/tools/mcp-servers/mcp-server-list.ts +28 -26
  143. package/src/tools/mcp-servers/mcp-server-uninstall.ts +10 -36
  144. package/src/tools/mcp-servers/mcp-server-update.ts +13 -57
  145. package/src/tools/memory-delete.ts +16 -46
  146. package/src/tools/memory-edit.ts +48 -49
  147. package/src/tools/memory-get.ts +40 -36
  148. package/src/tools/memory-rate.ts +14 -44
  149. package/src/tools/memory-search.ts +46 -52
  150. package/src/tools/my-agent-info.ts +39 -33
  151. package/src/tools/oauth-access-token.ts +9 -24
  152. package/src/tools/poll-task.ts +50 -77
  153. package/src/tools/post-message.ts +24 -38
  154. package/src/tools/prompt-templates/delete.ts +16 -48
  155. package/src/tools/prompt-templates/get.ts +45 -42
  156. package/src/tools/prompt-templates/list.ts +29 -44
  157. package/src/tools/prompt-templates/preview.ts +14 -40
  158. package/src/tools/prompt-templates/set.ts +30 -52
  159. package/src/tools/read-messages.ts +45 -54
  160. package/src/tools/register-agentmail-inbox.ts +28 -84
  161. package/src/tools/register-kapso-number.ts +20 -50
  162. package/src/tools/register-service.ts +36 -44
  163. package/src/tools/repos/get-repos.ts +29 -21
  164. package/src/tools/repos/update-repo.ts +30 -27
  165. package/src/tools/request-human-input.ts +10 -33
  166. package/src/tools/resolve-user.ts +59 -16
  167. package/src/tools/schedules/create-schedule.ts +58 -173
  168. package/src/tools/schedules/delete-schedule.ts +16 -56
  169. package/src/tools/schedules/list-schedules.ts +41 -67
  170. package/src/tools/schedules/patch-schedule.ts +57 -153
  171. package/src/tools/schedules/run-schedule-now.ts +18 -59
  172. package/src/tools/schedules/update-schedule.ts +57 -153
  173. package/src/tools/script-apis.ts +46 -28
  174. package/src/tools/script-common.ts +129 -34
  175. package/src/tools/script-connections/tool.ts +59 -154
  176. package/src/tools/script-query-types.ts +43 -3
  177. package/src/tools/script-run.ts +41 -2
  178. package/src/tools/script-runs.ts +69 -1
  179. package/src/tools/script-search.ts +24 -1
  180. package/src/tools/script-upsert.ts +18 -3
  181. package/src/tools/send-task.ts +32 -81
  182. package/src/tools/skills/skill-create.ts +14 -39
  183. package/src/tools/skills/skill-delete.ts +14 -42
  184. package/src/tools/skills/skill-get-file.ts +10 -43
  185. package/src/tools/skills/skill-get.ts +9 -32
  186. package/src/tools/skills/skill-install-remote.ts +16 -52
  187. package/src/tools/skills/skill-install.ts +14 -52
  188. package/src/tools/skills/skill-list.ts +24 -26
  189. package/src/tools/skills/skill-publish.ts +18 -54
  190. package/src/tools/skills/skill-search.ts +21 -18
  191. package/src/tools/skills/skill-sync-remote.ts +12 -24
  192. package/src/tools/skills/skill-uninstall.ts +10 -26
  193. package/src/tools/skills/skill-update.ts +24 -81
  194. package/src/tools/slack-delete.ts +8 -32
  195. package/src/tools/slack-download-file.ts +17 -57
  196. package/src/tools/slack-list-channels.ts +16 -43
  197. package/src/tools/slack-post.ts +11 -40
  198. package/src/tools/slack-read.ts +33 -103
  199. package/src/tools/slack-reply.ts +16 -53
  200. package/src/tools/slack-start-thread.ts +14 -55
  201. package/src/tools/slack-update.ts +8 -35
  202. package/src/tools/slack-upload-file.ts +22 -122
  203. package/src/tools/steer-task.ts +22 -35
  204. package/src/tools/store-progress.ts +11 -26
  205. package/src/tools/swarm-config/delete-config.ts +19 -57
  206. package/src/tools/swarm-config/get-config.ts +36 -44
  207. package/src/tools/swarm-config/list-config.ts +36 -44
  208. package/src/tools/swarm-config/set-config.ts +35 -73
  209. package/src/tools/swarm-x.ts +20 -26
  210. package/src/tools/task-action.ts +26 -46
  211. package/src/tools/task-tool-ctx.ts +5 -13
  212. package/src/tools/tracker/tracker-link-task.ts +8 -22
  213. package/src/tools/tracker/tracker-map-agent.ts +8 -22
  214. package/src/tools/tracker/tracker-status.ts +27 -19
  215. package/src/tools/tracker/tracker-sync-status.ts +8 -18
  216. package/src/tools/tracker/tracker-unlink.ts +4 -16
  217. package/src/tools/unregister-service.ts +19 -59
  218. package/src/tools/update-profile.ts +60 -100
  219. package/src/tools/update-service-status.ts +42 -67
  220. package/src/tools/utils.ts +189 -8
  221. package/src/tools/whatsapp-message.ts +19 -32
  222. package/src/tools/workflows/cancel-workflow-run.ts +4 -16
  223. package/src/tools/workflows/create-workflow.ts +9 -38
  224. package/src/tools/workflows/delete-workflow.ts +5 -17
  225. package/src/tools/workflows/get-workflow-run.ts +41 -31
  226. package/src/tools/workflows/get-workflow.ts +10 -21
  227. package/src/tools/workflows/list-workflow-runs.ts +107 -34
  228. package/src/tools/workflows/list-workflows.ts +5 -17
  229. package/src/tools/workflows/patch-workflow-node.ts +11 -39
  230. package/src/tools/workflows/patch-workflow.ts +11 -36
  231. package/src/tools/workflows/retry-workflow-run.ts +4 -16
  232. package/src/tools/workflows/trigger-workflow.ts +22 -58
  233. package/src/tools/workflows/update-workflow.ts +10 -42
  234. package/templates/skills/swarm-scripts/SKILL.md +1 -1
@@ -336,9 +336,8 @@ registerTemplate({
336
336
  You have access to agent-fs — a persistent, searchable filesystem shared across the swarm.
337
337
  Use the \`agent-fs\` CLI for all thoughts, research, plans, and shared documents.
338
338
 
339
- The \`agent-fs\` skill (from the agent-fs Claude Code plugin) provides a full CLI reference —
340
- it auto-injects on relevant Bash tool calls. You can also run \`agent-fs docs\` for
341
- interactive CLI documentation.
339
+ The \`agent-fs\` skill (installed in your skills directory) provides a full CLI
340
+ reference. You can also run \`agent-fs docs\` for interactive CLI documentation.
342
341
 
343
342
  ### Writing to your personal drive (default)
344
343
  \`\`\`bash
@@ -434,10 +433,68 @@ Use this to debug issues and propose improvements to your own infrastructure.
434
433
  category: "system",
435
434
  });
436
435
 
436
+ /**
437
+ * Script authoring contract — the call convention every script must follow.
438
+ *
439
+ * Included at the top of `system.agent.script_rubric` so it reaches every
440
+ * composite that carries the rubric (lead, worker, worker.pi, lead.pi) without
441
+ * a per-composite edit. The scripts-only composite path renders after one of
442
+ * those composites, so it must NOT restate the signature (duplicate guidance).
443
+ *
444
+ * Facts here are derived from `src/scripts-runtime/ctx.ts` (+ the generated
445
+ * `types/swarm-sdk.d.ts`); do not add ctx members that don't exist.
446
+ */
447
+ registerTemplate({
448
+ eventType: "system.agent.script_authoring_contract",
449
+ header: "",
450
+ defaultBody: `
451
+ ### Script Authoring Contract (read BEFORE writing any script)
452
+
453
+ **Entry point — \`args\` FIRST, \`ctx\` SECOND.** A one-parameter \`function (ctx)\` can execute through \`script-run\`, but at runtime that parameter receives \`args\`, so every \`ctx.*\` access throws. \`script-upsert\` also rejects an untyped parameter under strict typechecking. This is the single most common cause of failed script runs.
454
+
455
+ \`\`\`ts
456
+ import type { ScriptContext } from "swarm-sdk";
457
+ import * as z from "zod";
458
+
459
+ export const argsSchema = z.object({ taskId: z.string(), limit: z.number().optional() });
460
+
461
+ export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptContext) {
462
+ const res = await ctx.swarm.task_get({ taskId: args.taskId });
463
+ const task = ((res as { data?: unknown }).data ?? res) as { title?: string };
464
+ return { title: task?.title };
465
+ }
466
+ \`\`\`
467
+
468
+ **Typechecking:** inline source passed to \`script-run\` executes without a compile-time typecheck. \`script-upsert\` typechecks before saving, so import \`ScriptContext\` from \`"swarm-sdk"\` as shown above to make inline code promotion-safe. Use \`script-query-types\` for the authoritative SDK and stdlib declarations.
469
+
470
+ **What \`ctx\` actually holds for inline/named scripts (\`script-run\` / \`script-upsert\`) — nothing else:**
471
+ - \`ctx.swarm.*\` — the swarm SDK: \`task_get\`, \`task_send\`, \`task_storeProgress\`, \`task_action\`, \`task_list\`, \`message_post\`, \`message_read\`, \`slack_reply\`, \`memory_search\`, \`kv_get\`/\`kv_set\`/\`kv_del\`/\`kv_incr\`/\`kv_list\`, \`swarm_get\`, \`agent_info\`, and more. Responses are usually wrapped — prefer \`res?.data ?? res\`.
472
+ - \`ctx.swarm.config\` — \`apiKey\`, \`agentId\`, \`mcpBaseUrl\`, plus \`ctx.swarm.config.get("KEY")\` for user values. All are \`Redacted\` wrappers: they stringify to \`<redacted>\`, and you must never unwrap them into a return value, log line, or request body you build by hand.
473
+ - \`ctx.api.<slug>\` / \`ctx.mcp.<slug>\` — typed clients for connections the lead registered (\`ctx.api.<slug>.<operationId>(...)\`, \`ctx.api.<slug>.graphql(query, vars)\`, \`ctx.mcp.<slug>.<toolName>(args)\`). They exist ONLY for registered connections — introspect with \`Object.keys(ctx.api ?? {})\` / \`Object.keys(ctx.mcp ?? {})\` before assuming one is there.
474
+ - \`ctx.stdlib\` — \`fetch\`, \`fetchJson\` (retries + 30s timeout), \`grep\`, \`glob\`, \`table\`, \`Redacted\`.
475
+ - \`ctx.logger\` — \`log\` / \`warn\` / \`error\`.
476
+
477
+ There is NO ambient task context: \`taskId\` must arrive through \`args\`. \`agentId\` IS propagated automatically (\`X-Agent-ID\` header).
478
+
479
+ **Durable workflow scripts (\`launch-script-run\`) get a DIFFERENT \`ctx\`:** \`ctx.run\` (\`id\`/\`agentId\`/\`args\`), \`ctx.step.rawLlm(label, config)\` / \`ctx.step.agentTask(label, config)\` / \`ctx.step.swarmScript(label, config)\` (durable, journaled steps), plus \`ctx.swarm.*\`, \`ctx.stdlib\`, \`ctx.logger\` as above. Durable runs have NO \`ctx.api\`/\`ctx.mcp\` connection clients and no \`ctx.swarm.config\` — call connections from an inner script via \`ctx.step.swarmScript\` instead.
480
+
481
+ **\`argsSchema\` convention:** export a Zod schema named \`argsSchema\` from every named script. \`script-upsert\` converts it to JSON Schema, so callers, schedules, and workflows can see the script's input contract. Without it the script's args are undocumented.
482
+
483
+ **Secrets — never paste a raw value:**
484
+ - Prefer a registered connection (\`ctx.api.<slug>\` / \`ctx.mcp.<slug>\`): its credentials are attached server-side and never enter the script.
485
+ - Otherwise put the placeholder \`[REDACTED:<CONFIG_KEY>]\` in the header or query value (e.g. \`Authorization: Bearer [REDACTED:GITHUB_TOKEN]\`). The runtime substitutes the real value at egress, and only for that credential binding's allowed hosts.
486
+ - NEVER bake a raw secret — or the output of \`get-config\` with \`includeSecrets: true\` — into script source, script args, a schedule's \`taskTemplate\`, or a task description. Those are stored as plaintext and replayed on every run. Use a connection or a credential binding instead.
487
+ `,
488
+ variables: [],
489
+ category: "system",
490
+ });
491
+
437
492
  registerTemplate({
438
493
  eventType: "system.agent.script_rubric",
439
494
  header: "",
440
495
  defaultBody: `
496
+ {{@template[system.agent.script_authoring_contract]}}
497
+
441
498
  ### Agent Scripts — for bulk, repetitive, or data-heavy work
442
499
 
443
500
  Use **scripts** (\`script-upsert\` + \`script-run\`) when a task involves repetitive SDK calls, large data processing, or deterministic multi-step pipelines. Scripts run out-of-process and return only their final result.
@@ -479,13 +536,7 @@ registerTemplate({
479
536
 
480
537
  This swarm runs in **scripts-only mode**. The ONLY swarm MCP tools available are the script tools: \`script-search\`, \`script-run\`, \`script-upsert\`, \`script-delete\`, \`script-query-types\`, \`launch-script-run\`, \`get-script-run\`, \`list-script-runs\` (your harness may expose them under a prefix, e.g. \`mcp__agent-swarm__script-run\` — use the exact registered tool id). They are already loaded. Named tools like \`store-progress\`, \`send-task\`, \`post-message\`, \`memory-search\` do NOT exist here — do not search for them.
481
538
 
482
- **Script entry signature (memorizeargs FIRST, ctx SECOND):**
483
-
484
- \`\`\`ts
485
- export default async function (args: any, ctx: any) { /* ... */ }
486
- \`\`\`
487
-
488
- The full SDK is \`ctx.swarm.*\` — task lifecycle (\`task_get\`, \`task_send\`, \`task_storeProgress\`, \`task_action\`, \`task_list\`), messaging (\`message_post\`, \`message_read\`), 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\`.
539
+ The script authoring contract above (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\`), messaging (\`message_post\`, \`message_read\`), 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\`.
489
540
 
490
541
  **Built-in coordination scripts — USE THESE FIRST (\`script-run\` with \`name\` + \`args\`):**
491
542
  - \`delegate\` {agentName, task, parentTaskId?} → subtask for an agent by name; returns {taskId}
@@ -535,6 +586,18 @@ You have access to the \`context-mode\` MCP tools (\`batch_execute\`, \`execute\
535
586
 
536
587
  {{@template[system.agent.script_rubric]}}
537
588
 
589
+ {{@template[system.agent.scheduling]}}
590
+ `,
591
+ variables: [],
592
+ category: "system",
593
+ });
594
+
595
+ // Standalone so the pi composites (which drop `system.agent.context_mode` and
596
+ // its ctx_* tool advertisement) can still include the scheduling rules.
597
+ registerTemplate({
598
+ eventType: "system.agent.scheduling",
599
+ header: "",
600
+ defaultBody: `
538
601
  ### Scheduling — Pick the Right targetType
539
602
 
540
603
  When creating a schedule, match \`targetType\` to the work being fired:
@@ -807,8 +870,9 @@ registerTemplate({
807
870
  // Pi-specific worker composite. Identical to `system.session.worker` except it
808
871
  // omits only the `system.agent.context_mode` MCP-tool block — pi has no
809
872
  // context-mode MCP wiring yet, so advertising the `ctx_*` tools would point at
810
- // phantom tools. It still includes the shared script rubric and seed-script
811
- // guidance so pi sessions get the same bulk-work decision policy (DES-514).
873
+ // phantom tools. It still includes the shared script rubric, scheduling rules,
874
+ // and seed-script guidance so pi sessions get the same bulk-work decision
875
+ // policy (DES-514).
812
876
  registerTemplate({
813
877
  eventType: "system.session.worker.pi",
814
878
  header: "",
@@ -819,6 +883,36 @@ registerTemplate({
819
883
  {{@template[system.agent.filesystem]}}
820
884
  {{@template[system.agent.self_awareness]}}
821
885
  {{@template[system.agent.script_rubric]}}
886
+ {{@template[system.agent.scheduling]}}
887
+ {{@template[system.agent.seed_scripts]}}
888
+
889
+ {{@template[system.agent.system]}}
890
+ {{@template[system.agent.share_urls]}}
891
+ {{@template[system.agent.code_quality]}}`,
892
+ variables: [
893
+ { name: "role", description: "The agent's role" },
894
+ { name: "agentId", description: "The agent's unique identifier" },
895
+ ],
896
+ category: "session",
897
+ });
898
+
899
+ // Pi-specific lead composite. Identical to `system.session.lead` except it
900
+ // omits only the `system.agent.context_mode` MCP-tool block — pi has no
901
+ // context-mode MCP wiring, so a pi lead would otherwise be told about `ctx_*`
902
+ // tools that don't exist. It still includes the shared script rubric (which
903
+ // carries the script authoring contract), scheduling rules, and seed-script
904
+ // guidance.
905
+ registerTemplate({
906
+ eventType: "system.session.lead.pi",
907
+ header: "",
908
+ defaultBody: `{{@template[system.agent.role]}}
909
+
910
+ {{@template[system.agent.register]}}
911
+ {{@template[system.agent.lead]}}
912
+ {{@template[system.agent.filesystem]}}
913
+ {{@template[system.agent.self_awareness]}}
914
+ {{@template[system.agent.script_rubric]}}
915
+ {{@template[system.agent.scheduling]}}
822
916
  {{@template[system.agent.seed_scripts]}}
823
917
 
824
918
  {{@template[system.agent.system]}}
@@ -287,8 +287,12 @@ function jsonSchemaToTypeBox(schema: Record<string, unknown>): TSchema {
287
287
  return Type.Unsafe(schema);
288
288
  }
289
289
 
290
- /** Convert MCP tools to pi-mono ToolDefinition objects */
291
- function mcpToolsToDefinitions(
290
+ /**
291
+ * Convert MCP tools to pi-mono ToolDefinition objects.
292
+ * Exported for the isError-propagation conformance test — pi-agent-core
293
+ * derives a tool result's error flag solely from execute() throwing.
294
+ */
295
+ export function mcpToolsToDefinitions(
292
296
  mcpClient: McpHttpClient,
293
297
  tools: Array<{ name: string; description?: string; inputSchema: Record<string, unknown> }>,
294
298
  ): ToolDefinition[] {
@@ -303,6 +307,12 @@ function mcpToolsToDefinitions(
303
307
  .map((c) => c.text ?? "")
304
308
  .filter(Boolean)
305
309
  .join("\n");
310
+ // Propagate MCP isError: pi-agent-core derives a tool result's error flag
311
+ // from whether execute() throws, so a resolved return would silently
312
+ // report failed tool calls as successes to the model.
313
+ if (result.isError) {
314
+ throw new Error(text || `Tool ${tool.name} failed with no error message`);
315
+ }
306
316
  return {
307
317
  content: [{ type: "text" as const, text: text || "(no output)" }],
308
318
  details: undefined,
@@ -165,31 +165,6 @@ async function syncSetupScriptToServer(
165
165
  }
166
166
  }
167
167
 
168
- /**
169
- * Check if a path is under the agent's own subdirectory on the shared disk.
170
- * Shared disk categories: thoughts, memory, downloads, misc.
171
- * Each agent can only write to /workspace/shared/{category}/{agentId}/
172
- */
173
- function isOwnedSharedPath(path: string, agentId: string): boolean {
174
- const sharedCategories = ["thoughts", "memory", "downloads", "misc"];
175
- return sharedCategories.some((cat) => path.startsWith(`/workspace/shared/${cat}/${agentId}/`));
176
- }
177
-
178
- /**
179
- * Build the shared disk write warning message for a given agent ID.
180
- */
181
- function sharedDiskWriteWarning(agentId: string): string {
182
- return (
183
- `⚠️ This write will fail: You don't have write access to this directory.\n\n` +
184
- `On shared workspaces, each agent can only write to their own directories:\n` +
185
- `- /workspace/shared/thoughts/${agentId}/\n` +
186
- `- /workspace/shared/memory/${agentId}/\n` +
187
- `- /workspace/shared/downloads/${agentId}/\n` +
188
- `- /workspace/shared/misc/${agentId}/\n\n` +
189
- `You CAN read any file on the shared disk. For writes, use your own subdirectory.`
190
- );
191
- }
192
-
193
168
  /** Auto-index a file written to memory directory */
194
169
  async function autoIndexMemoryFile(config: SwarmHooksConfig, editedPath: string): Promise<void> {
195
170
  try {
@@ -505,24 +480,6 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
505
480
  }
506
481
  }
507
482
 
508
- // Shared disk write prevention (Archil only — skip in local dev)
509
- if (process.env.ARCHIL_MOUNT_TOKEN) {
510
- // Pi-mono uses lowercase tool names: "write", "edit"
511
- if (event.toolName === "write" || event.toolName === "edit") {
512
- const toolInput =
513
- "input" in event
514
- ? (event.input as { file_path?: string; path?: string } | undefined)
515
- : undefined;
516
- const targetPath = toolInput?.file_path || toolInput?.path || "";
517
- if (
518
- targetPath.startsWith("/workspace/shared/") &&
519
- !isOwnedSharedPath(targetPath, config.agentId)
520
- ) {
521
- console.log(sharedDiskWriteWarning(config.agentId));
522
- }
523
- }
524
- }
525
-
526
483
  return undefined;
527
484
  });
528
485
 
@@ -562,27 +519,6 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
562
519
  }
563
520
  }
564
521
 
565
- // Shared disk write failure detection (Archil only — safety net)
566
- if (process.env.ARCHIL_MOUNT_TOKEN) {
567
- // Pi-mono uses lowercase tool names
568
- if (event.toolName === "write" || event.toolName === "edit" || event.toolName === "bash") {
569
- const resultStr = JSON.stringify(event.content ?? []);
570
- if (resultStr.includes("Read-only file system")) {
571
- const resultInput = event.input as { file_path?: string; path?: string } | undefined;
572
- const resultPath = resultInput?.file_path || resultInput?.path || "";
573
- if (
574
- resultPath.startsWith("/workspace/shared/") &&
575
- !isOwnedSharedPath(resultPath, config.agentId)
576
- ) {
577
- console.log(sharedDiskWriteWarning(config.agentId));
578
- } else if (!resultPath) {
579
- // Bash tool — no file_path, just warn generically
580
- console.log(sharedDiskWriteWarning(config.agentId));
581
- }
582
- }
583
- }
584
- }
585
-
586
522
  // File sync: check if tool wrote to identity files or memory dirs
587
523
  // Pi-mono uses tool names: "write", "edit" (lowercase, unlike Claude's "Write", "Edit")
588
524
  const toolName = event.toolName;
@@ -17,6 +17,20 @@ rate by hand should also update this file.
17
17
  - **Pinned local entries**: safe by construction. The runtime refresh only adds
18
18
  pricing rows; it does not rewrite or delete the committed snapshot.
19
19
 
20
+ ## Live UI catalog: GET /api/models-catalog
21
+
22
+ - **Runtime module**: `src/be/models-catalog.ts`; route in
23
+ `src/http/models-catalog.ts`.
24
+ - Every successful models.dev fetch in the runtime refresh above also updates
25
+ an in-memory slim catalog (openrouter / anthropic / openai / amazon-bedrock
26
+ only, picker-relevant fields only). The UI model picker
27
+ (`apps/ui/src/lib/agent-runtime-models.ts` via `useModelsCatalog()`) prefers
28
+ this over its build-time snapshot, so new models appear without a deploy.
29
+ - Pinned limited-availability entries (`PINNED_MODELSDEV_ENTRIES`) are
30
+ re-merged from the vendored snapshot when models.dev doesn't list them yet.
31
+ - Until the first successful fetch (or when models.dev is unreachable) the
32
+ endpoint serves the vendored snapshot with `source: "snapshot"`.
33
+
20
34
  ## Fallback/UI catalog: vendored models.dev snapshot
21
35
 
22
36
  - **Fallback path**: `src/be/modelsdev-cache.json`
@@ -26,7 +40,8 @@ rate by hand should also update this file.
26
40
  `seedPricingFromModelsDev()`,
27
41
  called from `src/server.ts` after `initDb`.
28
42
  - **Role**: cold-start fallback seed for pricing when models.dev is unavailable,
29
- plus the UI model-picker source for names, labels, and context windows.
43
+ plus the fallback for the UI model picker while `GET /api/models-catalog`
44
+ hasn't resolved (names, labels, and context windows).
30
45
  - **Projection rules** (see the same module for code-level detail):
31
46
  - Anthropic models → rows under `provider='claude'` AND `provider='claude-managed'`.
32
47
  Shortnames (`opus`, `sonnet`, `haiku`) ALSO get rows keyed by the current
@@ -17,8 +17,17 @@ type StructuredError = {
17
17
  userFrames: StackFrame[];
18
18
  userScriptLine?: number;
19
19
  userScriptColumn?: number;
20
+ ctxSignatureHint?: string;
20
21
  };
21
22
 
23
+ // Matches runtime errors that look like a swapped/misused (args, ctx) signature,
24
+ // e.g. destructuring `ctx.api`/`ctx.kv`/`ctx.fetchJson`/`ctx.log` off `undefined`/`null`
25
+ // because the script read them off `args` instead.
26
+ const CTX_UNDEFINED_ACCESS_RE = /cannot read propert(?:y|ies)(?: .*)? of (?:undefined|null)/i;
27
+ const CTX_MEMBER_RE = /\b(api|kv|fetchJson|log)\b/i;
28
+ const CTX_SIGNATURE_HINT =
29
+ "Hint: swarm scripts export default async function (args, ctx) — args comes first, ctx second.";
30
+
22
31
  function buildStructuredError(err: unknown, userModulePath: string): StructuredError {
23
32
  const errObj = err instanceof Error ? err : new Error(String(err));
24
33
  const name = errObj.name || "Error";
@@ -48,6 +57,11 @@ function buildStructuredError(err: unknown, userModulePath: string): StructuredE
48
57
  }
49
58
  }
50
59
 
60
+ const ctxSignatureHint =
61
+ CTX_UNDEFINED_ACCESS_RE.test(message) && CTX_MEMBER_RE.test(message)
62
+ ? CTX_SIGNATURE_HINT
63
+ : undefined;
64
+
51
65
  return {
52
66
  name,
53
67
  message,
@@ -55,6 +69,7 @@ function buildStructuredError(err: unknown, userModulePath: string): StructuredE
55
69
  userFrames,
56
70
  userScriptLine: userFrames[0]?.line,
57
71
  userScriptColumn: userFrames[0]?.column,
72
+ ...(ctxSignatureHint ? { ctxSignatureHint } : {}),
58
73
  };
59
74
  }
60
75
 
@@ -103,7 +118,9 @@ try {
103
118
 
104
119
  const mod = await import(userModulePath);
105
120
  if (typeof mod.default !== "function") {
106
- throw new Error("Swarm script must export a default function");
121
+ throw new Error(
122
+ "Swarm script must export a default function. Script must export default async function (args, ctx) — args FIRST, ctx second.",
123
+ );
107
124
  }
108
125
 
109
126
  let validatedArgs = parsedArgs;
@@ -270,10 +270,20 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest | null {
270
270
  case "workflow_listRuns": {
271
271
  const wfId = typeof body.workflowId === "string" ? body.workflowId : undefined;
272
272
  if (!wfId) throw new Error("workflow_listRuns requires string `workflowId`");
273
+ const limit = body.limit === undefined ? 20 : body.limit;
274
+ const offset = body.offset === undefined ? 0 : body.offset;
275
+ if (!Number.isInteger(limit) || (limit as number) < 1 || (limit as number) > 100) {
276
+ throw new Error("workflow_listRuns `limit` must be an integer between 1 and 100");
277
+ }
278
+ if (!Number.isInteger(offset) || (offset as number) < 0) {
279
+ throw new Error("workflow_listRuns `offset` must be a non-negative integer");
280
+ }
273
281
  return {
274
282
  method: "GET",
275
283
  path: appendQuery(`/api/workflows/${encodeURIComponent(wfId)}/runs`, {
276
284
  status: body.status,
285
+ limit,
286
+ offset,
277
287
  }),
278
288
  };
279
289
  }
@@ -182,6 +182,8 @@ declare module "swarm-sdk" {
182
182
  workflow_listRuns(args: {
183
183
  workflowId: string;
184
184
  status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled";
185
+ limit?: number;
186
+ offset?: number;
185
187
  }): Promise<unknown>;
186
188
  workflow_getRun(args: { id: string }): Promise<unknown>;
187
189
  // --- prompt templates ---
@@ -438,6 +440,17 @@ declare module "swarm-sdk" {
438
440
  logger: ScriptLogger;
439
441
  }
440
442
 
443
+ /**
444
+ * A swarm script's default export. `args` comes FIRST, `ctx` second — never swap them.
445
+ *
446
+ * @example
447
+ * import type { ScriptContext } from "swarm-sdk";
448
+ *
449
+ * export default async function (args: { name: string }, ctx: ScriptContext) {
450
+ * await ctx.logger.log(`hello ${args.name}`);
451
+ * return { ok: true };
452
+ * }
453
+ */
441
454
  // biome-ignore lint/suspicious/noExplicitAny: scripts may narrow their args type at the entrypoint.
442
455
  export type ScriptMain = (args: any, ctx: ScriptContext) => unknown | Promise<unknown>;
443
456
  }
@@ -164,6 +164,8 @@ declare module "swarm-sdk" {
164
164
  workflow_listRuns(args: {
165
165
  workflowId: string;
166
166
  status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled";
167
+ limit?: number;
168
+ offset?: number;
167
169
  }): Promise<unknown>;
168
170
  workflow_getRun(args: { id: string }): Promise<unknown>;
169
171
  // --- prompt templates ---
@@ -420,6 +422,17 @@ declare module "swarm-sdk" {
420
422
  logger: ScriptLogger;
421
423
  }
422
424
 
425
+ /**
426
+ * A swarm script's default export. `args` comes FIRST, `ctx` second — never swap them.
427
+ *
428
+ * @example
429
+ * import type { ScriptContext } from "swarm-sdk";
430
+ *
431
+ * export default async function (args: { name: string }, ctx: ScriptContext) {
432
+ * await ctx.logger.log(`hello ${args.name}`);
433
+ * return { ok: true };
434
+ * }
435
+ */
423
436
  // biome-ignore lint/suspicious/noExplicitAny: scripts may narrow their args type at the entrypoint.
424
437
  export type ScriptMain = (args: any, ctx: ScriptContext) => unknown | Promise<unknown>;
425
438
 
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
3
3
  import * as z from "zod";
4
4
  import pkg from "../package.json";
5
5
  import { enqueueAdmissionRow } from "./be/rbac-audit";
@@ -33,7 +33,7 @@ import {
33
33
  taskActionOutputSchema,
34
34
  } from "./tools/task-action";
35
35
  import { userCtx } from "./tools/task-tool-ctx";
36
- import { createToolRegistrar } from "./tools/utils";
36
+ import { createToolRegistrar, type SwarmToolResult, toolErr } from "./tools/utils";
37
37
  import { ModelTierSchema, type User } from "./types";
38
38
  import { isSteeringEnabled } from "./utils/steering-enabled";
39
39
 
@@ -66,7 +66,7 @@ async function maybeDenyUserToolAdmission(
66
66
  user: User,
67
67
  toolName: string,
68
68
  config: UserToolAdmissionConfig,
69
- ): Promise<CallToolResult | undefined> {
69
+ ): Promise<SwarmToolResult | undefined> {
70
70
  if (!isRbacEnabled()) return undefined;
71
71
 
72
72
  const grant = getUserGrant(user.id);
@@ -86,10 +86,7 @@ async function maybeDenyUserToolAdmission(
86
86
 
87
87
  if (decision.allow) return undefined;
88
88
 
89
- return {
90
- isError: true,
91
- content: [{ type: "text", text: `Forbidden: ${decision.reason}` }],
92
- };
89
+ return toolErr(`Forbidden: ${decision.reason}`);
93
90
  }
94
91
 
95
92
  export function createUserServer(user: User): McpServer {
@@ -394,15 +394,28 @@ const MAX_OUTPUT_LENGTH = 120;
394
394
  /**
395
395
  * Truncate output to the first sentence or MAX_OUTPUT_LENGTH, whichever is shorter.
396
396
  */
397
- function truncateOutput(text: string): string {
397
+ function truncateOutput(text: string): { text: string; truncated: boolean } {
398
398
  // Find first sentence boundary (. followed by space or end)
399
399
  const sentenceEnd = text.search(/\.\s/);
400
- const firstSentence = sentenceEnd !== -1 ? text.slice(0, sentenceEnd + 1) : text;
401
- if (firstSentence.length <= MAX_OUTPUT_LENGTH) return firstSentence;
402
- const boundary = text.lastIndexOf(" ", MAX_OUTPUT_LENGTH);
403
- const cut = boundary >= MAX_OUTPUT_LENGTH / 2 ? boundary : MAX_OUTPUT_LENGTH;
400
+ const sentenceCut = sentenceEnd !== -1 ? sentenceEnd + 1 : text.length;
401
+ if (sentenceCut === text.length && text.length <= MAX_OUTPUT_LENGTH) {
402
+ return { text, truncated: false };
403
+ }
404
+
405
+ let cut = Math.min(sentenceCut, MAX_OUTPUT_LENGTH);
406
+ if (cut === MAX_OUTPUT_LENGTH) {
407
+ const boundary = text.lastIndexOf(" ", MAX_OUTPUT_LENGTH);
408
+ cut = boundary >= MAX_OUTPUT_LENGTH / 2 ? boundary : MAX_OUTPUT_LENGTH;
409
+ }
404
410
  const omitted = text.length - cut;
405
- return `${text.slice(0, cut).trimEnd()}… (${omitted} more chars; full output in thread)`;
411
+ return {
412
+ text: `${text.slice(0, cut).trimEnd()}… (${omitted} more chars; open task for full output)`,
413
+ truncated: true,
414
+ };
415
+ }
416
+
417
+ export function isTreeOutputTruncated(text: string): boolean {
418
+ return truncateOutput(text).truncated;
406
419
  }
407
420
 
408
421
  /**
@@ -433,7 +446,7 @@ function renderChildDetail(node: TreeNode, indent: string): string[] {
433
446
  }
434
447
 
435
448
  if (node.status === "completed" && !node.slackReplySent && node.output) {
436
- lines.push(`${indent}${truncateOutput(markdownToSlack(node.output))}`);
449
+ lines.push(`${indent}${truncateOutput(markdownToSlack(node.output)).text}`);
437
450
  }
438
451
 
439
452
  return lines;
@@ -457,7 +470,7 @@ function renderTree(root: TreeNode): string {
457
470
  lines.push(` Error: ${root.failureReason}`);
458
471
  }
459
472
  if (root.status === "completed" && !root.slackReplySent && root.output) {
460
- lines.push(` ${truncateOutput(markdownToSlack(root.output))}`);
473
+ lines.push(` ${truncateOutput(markdownToSlack(root.output)).text}`);
461
474
  }
462
475
  return lines.join("\n");
463
476
  }
@@ -1,6 +1,6 @@
1
1
  import type { WebClient } from "@slack/web-api";
2
- import { getAgentById, getTaskAttachments } from "../be/db";
3
- import type { Agent, AgentTask, TaskAttachment } from "../types";
2
+ import { getAgentById, getTaskAttachments, markTaskSlackReplySent } from "../be/db";
3
+ import type { Agent, AgentTask } from "../types";
4
4
  import { getSlackApp } from "./app";
5
5
  import {
6
6
  buildCancelledBlocks,
@@ -11,6 +11,7 @@ import {
11
11
  formatAttachmentsBlockForSlack,
12
12
  formatDuration,
13
13
  getTaskLink,
14
+ isTreeOutputTruncated,
14
15
  markdownToSlack,
15
16
  splitSlackSectionText,
16
17
  } from "./blocks";
@@ -33,16 +34,9 @@ function classifySlackUpdateError(error: unknown): SlackUpdateResult {
33
34
  }
34
35
 
35
36
  const isDev = process.env.ENV === "development";
36
- const MIN_INLINE_OUTPUT_LENGTH = 20;
37
- export const MAX_INLINE_OUTPUT_MESSAGE_LENGTH = 4000;
38
- const INLINE_OUTPUT_FALLBACKS = new Set([
39
- "done",
40
- "completed",
41
- "complete",
42
- "task completed",
43
- "process completed successfully",
44
- "process completed successfully (no output captured)",
45
- ]);
37
+ // Ten 2,900-char sections keep each message's fallback text below Slack's
38
+ // 40,000-char message cap while allowing arbitrarily long output in batches.
39
+ const MAX_INLINE_OUTPUT_BLOCKS_PER_MESSAGE = 10;
46
40
 
47
41
  /**
48
42
  * Get the display name for an agent, with (dev) prefix if in development mode.
@@ -51,45 +45,23 @@ function getAgentDisplayName(agent: Agent): string {
51
45
  return isDev ? `(dev) ${agent.name}` : agent.name;
52
46
  }
53
47
 
54
- export function shouldPostInlineCompletionOutput(
55
- task: AgentTask,
56
- attachments: readonly TaskAttachment[],
57
- ): boolean {
48
+ export function shouldPostInlineCompletionOutput(task: AgentTask): boolean {
58
49
  if (task.status !== "completed") return false;
59
50
  if (!task.slackChannelId || !task.slackThreadTs) return false;
60
51
  if (task.slackReplySent) return false;
61
52
 
62
53
  const output = task.output?.trim();
63
- if (!output || output.length < MIN_INLINE_OUTPUT_LENGTH) return false;
64
- if (INLINE_OUTPUT_FALLBACKS.has(output.toLowerCase())) return false;
65
-
66
- return !attachments.some((attachment) => attachment.isPrimary);
54
+ return !!output && isTreeOutputTruncated(markdownToSlack(output));
67
55
  }
68
56
 
69
- export function formatInlineCompletionOutputText(opts: {
57
+ export function formatInlineCompletionOutputChunks(opts: {
70
58
  agentName: string;
71
59
  taskId: string;
72
60
  output: string;
73
- }): string {
61
+ }): string[] {
74
62
  const header = `✅ *${opts.agentName}* completed with output (${getTaskLink(opts.taskId)}):\n\n`;
75
- const tail = `\n\n…(full output in task ${getTaskLink(opts.taskId)})`;
76
63
  const slackOutput = markdownToSlack(opts.output.trim());
77
- const maxBodyLength = MAX_INLINE_OUTPUT_MESSAGE_LENGTH - header.length;
78
-
79
- if (slackOutput.length <= maxBodyLength) {
80
- return header + slackOutput;
81
- }
82
-
83
- const maxTruncatedBodyLength = Math.max(0, maxBodyLength - tail.length);
84
- let cut = slackOutput.lastIndexOf("\n", maxTruncatedBodyLength);
85
- if (cut < maxTruncatedBodyLength / 2) {
86
- cut = slackOutput.lastIndexOf(" ", maxTruncatedBodyLength);
87
- }
88
- if (cut < maxTruncatedBodyLength / 2) {
89
- cut = maxTruncatedBodyLength;
90
- }
91
-
92
- return header + slackOutput.slice(0, cut).trimEnd() + tail;
64
+ return splitSlackSectionText(header + slackOutput);
93
65
  }
94
66
 
95
67
  export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
@@ -104,24 +76,28 @@ export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
104
76
  return false;
105
77
  }
106
78
 
107
- const text = formatInlineCompletionOutputText({
79
+ const chunks = formatInlineCompletionOutputChunks({
108
80
  agentName: agent.name,
109
81
  taskId: task.id,
110
82
  output: task.output,
111
83
  });
112
84
 
113
85
  try {
114
- await sendWithPersona(app.client, {
115
- channel: task.slackChannelId,
116
- thread_ts: task.slackThreadTs,
117
- text,
118
- username: getAgentDisplayName(agent),
119
- icon_emoji: getAgentEmoji(agent),
120
- blocks: splitSlackSectionText(text).map((chunk) => ({
121
- type: "section",
122
- text: { type: "mrkdwn", text: chunk },
123
- })),
124
- });
86
+ for (let i = 0; i < chunks.length; i += MAX_INLINE_OUTPUT_BLOCKS_PER_MESSAGE) {
87
+ const batch = chunks.slice(i, i + MAX_INLINE_OUTPUT_BLOCKS_PER_MESSAGE);
88
+ await sendWithPersona(app.client, {
89
+ channel: task.slackChannelId,
90
+ thread_ts: task.slackThreadTs,
91
+ text: batch.join("\n\n"),
92
+ username: getAgentDisplayName(agent),
93
+ icon_emoji: getAgentEmoji(agent),
94
+ blocks: batch.map((chunk) => ({
95
+ type: "section",
96
+ text: { type: "mrkdwn", text: chunk },
97
+ })),
98
+ });
99
+ }
100
+ markTaskSlackReplySent(task.id);
125
101
  return true;
126
102
  } catch (error) {
127
103
  console.error(`[Slack] Failed to send inline output for task ${task.id}:`, error);