@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
@@ -12,9 +12,10 @@ import type {
12
12
  ServerRequest,
13
13
  ToolAnnotations,
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
+ import * as z from "zod";
15
16
  import { withSpan } from "../otel";
16
17
  import type { PermissionVerb } from "../rbac/permissions";
17
- import { scrubSecrets } from "../utils/secret-scrubber";
18
+ import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
18
19
 
19
20
  type Meta = RequestHandlerExtra<ServerRequest, ServerNotification>;
20
21
 
@@ -91,6 +92,183 @@ function toolResultAttributes(result: CallToolResult) {
91
92
  };
92
93
  }
93
94
 
95
+ /**
96
+ * Canonical result every swarm MCP tool returns. The registrar — not the tool —
97
+ * composes the wire-level CallToolResult from it, so the text channel and
98
+ * structuredContent can never diverge (see runbooks/mcp-tool-results.md for the
99
+ * per-harness evidence behind this contract).
100
+ */
101
+ export type SwarmToolData = Record<string, unknown>;
102
+
103
+ export type SwarmToolResult<TData extends SwarmToolData = SwarmToolData> = {
104
+ /** Truthful outcome. Becomes `isError = !ok` and `structuredContent.success`. */
105
+ ok: boolean;
106
+ /** One-line summary. Required, non-empty — the first thing every harness shows the model. */
107
+ message: string;
108
+ /** Model-needed payload rendering (tables, lists, error detail). Appended to the text channel. */
109
+ details?: string;
110
+ /** Structured payload, spread into structuredContent alongside the envelope keys. */
111
+ data?: TData;
112
+ /** Single-sentence conditional steer, appended to BOTH channels. */
113
+ nudge?: string;
114
+ /**
115
+ * Skip the finalize pipeline's secret scrubbing for this result. ONLY for
116
+ * deliberate credential-reveal branches (oauth-access-token, script-apis
117
+ * create/rotate, get-config includeSecrets) whose entire purpose is handing
118
+ * the agent a secret — the central scrubber would otherwise redact the
119
+ * reveal. Everything else stays scrubbed.
120
+ */
121
+ allowSecretEgress?: boolean;
122
+ };
123
+
124
+ export const toolOk = <TData extends SwarmToolData = SwarmToolData>(
125
+ message: string,
126
+ extras: Omit<SwarmToolResult<TData>, "ok" | "message"> = {},
127
+ ): SwarmToolResult<TData> => ({ ok: true, message, ...extras });
128
+
129
+ export const toolErr = <TData extends SwarmToolData = SwarmToolData>(
130
+ message: string,
131
+ extras: Omit<SwarmToolResult<TData>, "ok" | "message"> = {},
132
+ ): SwarmToolResult<TData> => ({ ok: false, message, ...extras });
133
+
134
+ /**
135
+ * Envelope keys the registrar writes into structuredContent for every tool.
136
+ * Output schemas must include these and must be LOOSE (`z.looseObject`):
137
+ * plain `z.object` emits `additionalProperties: false`, which makes
138
+ * client-side validators (opencode's official-SDK client) reject the spread
139
+ * `data` keys after the write already landed.
140
+ */
141
+ export const swarmToolEnvelopeShape = {
142
+ success: z.boolean(),
143
+ message: z.string(),
144
+ details: z.string().optional(),
145
+ nudge: z.string().optional(),
146
+ };
147
+
148
+ /** Build a permissive output schema: envelope + optional tool-specific data shape. */
149
+ export const swarmToolOutputSchema = <S extends z.ZodRawShape>(dataShape?: S) =>
150
+ z.looseObject({ ...swarmToolEnvelopeShape, ...(dataShape ?? ({} as S)) });
151
+
152
+ export const SCRIPT_AUTHORING_NUDGE =
153
+ "Scripts must `export default async function (args, ctx)` — args FIRST, ctx second; run script-query-types (no name) for the full ctx/SDK type surface, and see the `swarm-scripts` skill for authoring patterns.";
154
+
155
+ // Only steer on failures plausibly caused by the script itself (typecheck or
156
+ // runtime) — on lookup/transport/authorization errors the authoring advice
157
+ // distracts from the reported problem.
158
+ const scriptAuthoringNudge = (r: SwarmToolResult): string | undefined =>
159
+ !r.ok && /Typecheck failed:|Script run .*failed/.test(r.message)
160
+ ? SCRIPT_AUTHORING_NUDGE
161
+ : undefined;
162
+
163
+ /**
164
+ * Central conditional nudges, keyed by tool name. Applied by the finalize
165
+ * pipeline when the tool did not set an explicit nudge. Keep entries to a
166
+ * single sentence; derive only from already-scrubbed result fields.
167
+ */
168
+ export const NUDGES: Record<string, (result: SwarmToolResult) => string | undefined> = {
169
+ "script-run": scriptAuthoringNudge,
170
+ "script-upsert": scriptAuthoringNudge,
171
+ "launch-script-run": scriptAuthoringNudge,
172
+ "get-script-run": scriptAuthoringNudge,
173
+ "script-search": (r) => {
174
+ if (!r.ok) return undefined;
175
+ // proxyScriptsApi wraps the parsed HTTP body as data = { status, data },
176
+ // so the results array lives one level down.
177
+ const body = (r.data as { data?: { results?: unknown[] } } | undefined)?.data;
178
+ const results = body?.results;
179
+ return Array.isArray(results) && results.length === 0
180
+ ? "No scripts matched — the catalog ships seeded example scripts; re-run script-search with an empty query to list them."
181
+ : undefined;
182
+ },
183
+ "memory-search": (r) => {
184
+ if (!r.ok) return undefined;
185
+ const results = (r.data as { results?: Array<{ rateHint?: unknown }> } | undefined)?.results;
186
+ return Array.isArray(results) && results.some((entry) => Boolean(entry?.rateHint))
187
+ ? "Rate memories that help or mislead you with memory_rate."
188
+ : undefined;
189
+ },
190
+ };
191
+
192
+ // Cap for any rendered details string — Codex's ~10KB middle-out truncation is
193
+ // the tightest harness budget. Structured data remains intact; only the
194
+ // human/model-facing rendering is bounded.
195
+ const DETAILS_CAP = 8_000;
196
+
197
+ function truncateForText(text: string): string {
198
+ if (text.length <= DETAILS_CAP) return text;
199
+ return `${text.slice(0, DETAILS_CAP)}\n… [truncated ${text.length - DETAILS_CAP} chars]`;
200
+ }
201
+
202
+ type FinalizeContext = { toolName: string };
203
+ type FinalizeMiddleware = (result: SwarmToolResult, ctx: FinalizeContext) => SwarmToolResult;
204
+
205
+ const scrubMiddleware: FinalizeMiddleware = (result) =>
206
+ result.allowSecretEgress ? result : scrubObject(result);
207
+
208
+ const nudgeMiddleware: FinalizeMiddleware = (result, ctx) => {
209
+ if (result.nudge) return result;
210
+ const nudge = NUDGES[ctx.toolName]?.(result);
211
+ return nudge ? { ...result, nudge } : result;
212
+ };
213
+
214
+ // Ordered: scrub first so nudges (and any future middleware) only ever see
215
+ // scrubbed data. Future ctx-control middleware (response pruning, auto-KV
216
+ // overflow) slots in between nudge and the final transform.
217
+ const FINALIZE_PIPELINE: FinalizeMiddleware[] = [scrubMiddleware, nudgeMiddleware];
218
+
219
+ /**
220
+ * Transform a SwarmToolResult into the wire CallToolResult. Both channels are
221
+ * composed identically and are independently self-sufficient: Codex reads only
222
+ * structuredContent, pi/opencode/claude-managed read only content.text.
223
+ * structuredContent is ALWAYS present (opencode's SDK client throws when a
224
+ * declared outputSchema has no structuredContent).
225
+ */
226
+ export function finalizeSwarmToolResult(toolName: string, result: SwarmToolResult): CallToolResult {
227
+ let r = result;
228
+ if (!r.message?.trim()) {
229
+ console.warn(`[mcp] tool ${toolName} returned an empty message — every tool must summarize`);
230
+ r = {
231
+ ...r,
232
+ message: r.ok
233
+ ? "Tool call succeeded (no message provided)."
234
+ : "Tool call failed (no message provided).",
235
+ };
236
+ }
237
+ for (const middleware of FINALIZE_PIPELINE) r = middleware(r, { toolName });
238
+
239
+ // Normalize and cap explicit details only after the scrub middleware has
240
+ // removed secrets. The same bounded value feeds both wire channels.
241
+ // Whitespace-only details count as absent so data fallback remains visible.
242
+ const explicitDetails = r.details?.trim() ? truncateForText(r.details.trim()) : undefined;
243
+
244
+ // Text-channel completeness guarantee: when a tool sets data but no details,
245
+ // render the data as JSON into the text channel. Most harnesses only ever
246
+ // show the model content.text — without this fallback, a data-only payload
247
+ // would be invisible there. Not copied into structuredContent.details (the
248
+ // structured channel already carries data verbatim).
249
+ const dataFallback =
250
+ !explicitDetails && r.data && Object.keys(r.data).length > 0
251
+ ? truncateForText(JSON.stringify(r.data, null, 2))
252
+ : undefined;
253
+
254
+ const text = [r.message, explicitDetails ?? dataFallback, r.nudge]
255
+ .filter((part): part is string => Boolean(part?.trim()))
256
+ .join("\n\n");
257
+ const structuredContent: Record<string, unknown> = {
258
+ ...(r.data ?? {}),
259
+ success: r.ok,
260
+ message: r.message,
261
+ };
262
+ if (explicitDetails) structuredContent.details = explicitDetails;
263
+ if (r.nudge) structuredContent.nudge = r.nudge;
264
+
265
+ return {
266
+ content: [{ type: "text", text }],
267
+ structuredContent,
268
+ isError: !r.ok,
269
+ };
270
+ }
271
+
94
272
  // Infer the input type from the schema
95
273
  type InferInput<Args extends undefined | ZodRawShapeCompat | AnySchema> =
96
274
  Args extends ZodRawShapeCompat
@@ -99,15 +277,16 @@ type InferInput<Args extends undefined | ZodRawShapeCompat | AnySchema> =
99
277
  ? SchemaOutput<Args>
100
278
  : undefined;
101
279
 
102
- // Callback type with requestInfo injected as second parameter
280
+ // Callback type with requestInfo injected as second parameter.
281
+ // Tools return SwarmToolResult (ours) — never a raw MCP CallToolResult.
103
282
  type ToolCallbackWithInfo<Args extends undefined | ZodRawShapeCompat | AnySchema = undefined> =
104
283
  Args extends undefined
105
- ? (requestInfo: RequestInfo, meta: Meta) => CallToolResult | Promise<CallToolResult>
284
+ ? (requestInfo: RequestInfo, meta: Meta) => SwarmToolResult | Promise<SwarmToolResult>
106
285
  : (
107
286
  args: InferInput<Args>,
108
287
  requestInfo: RequestInfo,
109
288
  meta: Meta,
110
- ) => CallToolResult | Promise<CallToolResult>;
289
+ ) => SwarmToolResult | Promise<SwarmToolResult>;
111
290
 
112
291
  type ToolConfig<
113
292
  InputArgs extends undefined | ZodRawShapeCompat | AnySchema,
@@ -155,12 +334,13 @@ export const createToolRegistrar = (server: McpServer) => {
155
334
  return withSpan(
156
335
  "mcp.tool",
157
336
  async (span) => {
158
- const result = await (
337
+ const outcome = await (
159
338
  cb as (
160
339
  requestInfo: RequestInfo,
161
340
  meta: Meta,
162
- ) => CallToolResult | Promise<CallToolResult>
341
+ ) => SwarmToolResult | Promise<SwarmToolResult>
163
342
  )(requestInfo, meta);
343
+ const result = finalizeSwarmToolResult(name, outcome);
164
344
  span.setAttributes(toolResultAttributes(result));
165
345
  return result;
166
346
  },
@@ -176,13 +356,14 @@ export const createToolRegistrar = (server: McpServer) => {
176
356
  // trace tree. Cardinality is bounded — tool names are a fixed enum.
177
357
  `mcp.tool ${name}`,
178
358
  async (span) => {
179
- const result = await (
359
+ const outcome = await (
180
360
  cb as (
181
361
  args: InferInput<InputArgs>,
182
362
  requestInfo: RequestInfo,
183
363
  meta: Meta,
184
- ) => CallToolResult | Promise<CallToolResult>
364
+ ) => SwarmToolResult | Promise<SwarmToolResult>
185
365
  )(args, requestInfo, meta);
366
+ const result = finalizeSwarmToolResult(name, outcome);
186
367
  span.setAttributes(toolResultAttributes(result));
187
368
  return result;
188
369
  },
@@ -2,16 +2,20 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { sendKapsoText } from "@/integrations/kapso/client";
4
4
  import { getKapsoConfig } from "@/integrations/kapso/config";
5
- import { createToolRegistrar } from "@/tools/utils";
5
+ import {
6
+ createToolRegistrar,
7
+ type SwarmToolResult,
8
+ swarmToolOutputSchema,
9
+ toolErr,
10
+ toolOk,
11
+ } from "@/tools/utils";
6
12
 
7
13
  /** Shared structured-error message for the 24h session-window case. */
8
14
  const SESSION_WINDOW_HINT =
9
15
  'Outside the 24h WhatsApp session window — free-form text is rejected. Use a pre-approved template message (see the `kapso-whatsapp` skill, "Send a template").';
10
16
 
11
- const outputSchema = z.object({
17
+ const outputSchema = swarmToolOutputSchema({
12
18
  yourAgentId: z.string().optional(),
13
- success: z.boolean(),
14
- message: z.string(),
15
19
  messageId: z.string().optional(),
16
20
  sessionWindowExpired: z.boolean().optional(),
17
21
  });
@@ -79,15 +83,13 @@ async function sendAndFormat(
79
83
  params: { phoneNumberId: string; to: string; body: string; previewUrl?: boolean },
80
84
  agentId: string | undefined,
81
85
  contextMessageId: string | undefined,
82
- ) {
86
+ ): Promise<SwarmToolResult> {
83
87
  try {
84
88
  const config = getKapsoConfig();
85
89
  if (!config.apiKey) {
86
- const msg = "KAPSO_API_KEY is not configured in swarm config.";
87
- return {
88
- content: [{ type: "text" as const, text: msg }],
89
- structuredContent: { yourAgentId: agentId, success: false, message: msg },
90
- };
90
+ return toolErr("KAPSO_API_KEY is not configured in swarm config.", {
91
+ data: { yourAgentId: agentId },
92
+ });
91
93
  }
92
94
 
93
95
  const result = await sendKapsoText({
@@ -102,34 +104,19 @@ async function sendAndFormat(
102
104
 
103
105
  if (result.ok) {
104
106
  const text = `Sent WhatsApp message to ${params.to} (wamid ${result.messageId ?? "unknown"})`;
105
- return {
106
- content: [{ type: "text" as const, text }],
107
- structuredContent: {
108
- yourAgentId: agentId,
109
- success: true,
110
- message: text,
111
- messageId: result.messageId,
112
- },
113
- };
107
+ return toolOk(text, {
108
+ data: { yourAgentId: agentId, messageId: result.messageId },
109
+ });
114
110
  }
115
111
 
116
112
  const text = result.sessionWindowExpired
117
113
  ? `${SESSION_WINDOW_HINT} (Kapso: ${result.errorMessage})`
118
114
  : `Kapso send failed: ${result.errorMessage}`;
119
- return {
120
- content: [{ type: "text" as const, text }],
121
- structuredContent: {
122
- yourAgentId: agentId,
123
- success: false,
124
- message: text,
125
- sessionWindowExpired: result.sessionWindowExpired,
126
- },
127
- };
115
+ return toolErr(text, {
116
+ data: { yourAgentId: agentId, sessionWindowExpired: result.sessionWindowExpired },
117
+ });
128
118
  } catch (err) {
129
119
  const errorMessage = err instanceof Error ? err.message : String(err);
130
- return {
131
- content: [{ type: "text" as const, text: `Error: ${errorMessage}` }],
132
- structuredContent: { yourAgentId: agentId, success: false, message: errorMessage },
133
- };
120
+ return toolErr(errorMessage, { data: { yourAgentId: agentId } });
134
121
  }
135
122
  }
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { createToolRegistrar } from "@/tools/utils";
3
+ import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
4
4
  import { cancelWorkflowRun } from "@/workflows";
5
5
 
6
6
  export const registerCancelWorkflowRunTool = (server: McpServer) => {
@@ -15,26 +15,14 @@ export const registerCancelWorkflowRunTool = (server: McpServer) => {
15
15
  runId: z.string().uuid().describe("Workflow run ID to cancel"),
16
16
  reason: z.string().optional().describe("Optional reason for cancellation"),
17
17
  }),
18
- outputSchema: z.object({
19
- success: z.boolean(),
20
- message: z.string(),
21
- }),
18
+ outputSchema: swarmToolOutputSchema(),
22
19
  },
23
20
  async ({ runId, reason }) => {
24
21
  try {
25
22
  cancelWorkflowRun(runId, reason);
26
- return {
27
- content: [{ type: "text" as const, text: `Cancelled workflow run ${runId}.` }],
28
- structuredContent: {
29
- success: true,
30
- message: `Cancelled workflow run ${runId}.`,
31
- },
32
- };
23
+ return toolOk(`Cancelled workflow run ${runId}.`);
33
24
  } catch (err) {
34
- return {
35
- content: [{ type: "text" as const, text: `Failed: ${err}` }],
36
- structuredContent: { success: false, message: String(err) },
37
- };
25
+ return toolErr(String(err));
38
26
  }
39
27
  },
40
28
  );
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { authorizeAssetKeyWrite } from "@/be/asset-key-auth";
4
4
  import { resolveTaskAuditUserId } from "@/be/audit-user";
5
5
  import { createWorkflow } from "@/be/db";
6
- import { createToolRegistrar } from "@/tools/utils";
6
+ import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
7
7
  import {
8
8
  AssetKeySchema,
9
9
  CooldownConfigSchema,
@@ -84,10 +84,8 @@ export const registerCreateWorkflowTool = (server: McpServer) => {
84
84
  "Other JSON-Schema keywords are silently ignored.",
85
85
  ),
86
86
  }),
87
- outputSchema: z.object({
87
+ outputSchema: swarmToolOutputSchema({
88
88
  yourAgentId: z.string().optional(),
89
- success: z.boolean(),
90
- message: z.string(),
91
89
  workflow: z.unknown().optional(),
92
90
  }),
93
91
  },
@@ -107,27 +105,13 @@ export const registerCreateWorkflowTool = (server: McpServer) => {
107
105
  requestInfo,
108
106
  ) => {
109
107
  if (!requestInfo.agentId) {
110
- return {
111
- content: [{ type: "text" as const, text: "Agent ID required." }],
112
- structuredContent: { success: false, message: "Agent ID required." },
113
- };
108
+ return toolErr("Agent ID required.");
114
109
  }
115
110
  try {
116
111
  // Validate definition structure
117
112
  const validation = validateDefinition(definition);
118
113
  if (!validation.valid) {
119
- return {
120
- content: [
121
- {
122
- type: "text" as const,
123
- text: `Invalid definition: ${validation.errors.join("; ")}`,
124
- },
125
- ],
126
- structuredContent: {
127
- success: false,
128
- message: `Invalid definition: ${validation.errors.join("; ")}`,
129
- },
130
- };
114
+ return toolErr(`Invalid definition: ${validation.errors.join("; ")}`);
131
115
  }
132
116
 
133
117
  const createdBy =
@@ -148,25 +132,12 @@ export const registerCreateWorkflowTool = (server: McpServer) => {
148
132
  createdByAgentId: requestInfo.agentId,
149
133
  createdBy,
150
134
  });
151
- return {
152
- content: [
153
- {
154
- type: "text" as const,
155
- text: `Created workflow "${workflow.name}" (${workflow.id}).`,
156
- },
157
- ],
158
- structuredContent: {
159
- yourAgentId: requestInfo.agentId,
160
- success: true,
161
- message: `Created workflow "${workflow.name}".`,
162
- workflow,
163
- },
164
- };
135
+ return toolOk(`Created workflow "${workflow.name}".`, {
136
+ details: `Created workflow "${workflow.name}" (${workflow.id}).`,
137
+ data: { yourAgentId: requestInfo.agentId, workflow },
138
+ });
165
139
  } catch (err) {
166
- return {
167
- content: [{ type: "text" as const, text: `Failed: ${err}` }],
168
- structuredContent: { success: false, message: String(err) },
169
- };
140
+ return toolErr(String(err));
170
141
  }
171
142
  },
172
143
  );
@@ -1,7 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { deleteWorkflow } from "@/be/db";
4
- import { createToolRegistrar } from "@/tools/utils";
4
+ import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
5
5
 
6
6
  export const registerDeleteWorkflowTool = (server: McpServer) => {
7
7
  createToolRegistrar(server)(
@@ -13,29 +13,17 @@ export const registerDeleteWorkflowTool = (server: McpServer) => {
13
13
  inputSchema: z.object({
14
14
  id: z.string().uuid().describe("Workflow ID to delete"),
15
15
  }),
16
- outputSchema: z.object({
17
- success: z.boolean(),
18
- message: z.string(),
19
- }),
16
+ outputSchema: swarmToolOutputSchema(),
20
17
  },
21
18
  async ({ id }) => {
22
19
  try {
23
20
  const deleted = deleteWorkflow(id);
24
21
  if (!deleted) {
25
- return {
26
- content: [{ type: "text" as const, text: `Workflow not found: ${id}` }],
27
- structuredContent: { success: false, message: `Workflow not found: ${id}` },
28
- };
22
+ return toolErr(`Workflow not found: ${id}`);
29
23
  }
30
- return {
31
- content: [{ type: "text" as const, text: `Deleted workflow ${id}.` }],
32
- structuredContent: { success: true, message: `Deleted workflow ${id}.` },
33
- };
24
+ return toolOk(`Deleted workflow ${id}.`);
34
25
  } catch (err) {
35
- return {
36
- content: [{ type: "text" as const, text: `Failed: ${err}` }],
37
- structuredContent: { success: false, message: String(err) },
38
- };
26
+ return toolErr(String(err));
39
27
  }
40
28
  },
41
29
  );
@@ -1,7 +1,39 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { getWorkflowRun, getWorkflowRunStepsByRunId } from "@/be/db";
4
- import { createToolRegistrar } from "@/tools/utils";
4
+ import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
5
+
6
+ const STEP_VALUE_CAP = 400;
7
+
8
+ function stepValuePreview(value: unknown): string | undefined {
9
+ if (value === undefined || value === null) return undefined;
10
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
11
+ if (!serialized) return undefined;
12
+ return serialized.length > STEP_VALUE_CAP
13
+ ? `${serialized.slice(0, STEP_VALUE_CAP)}…`
14
+ : serialized;
15
+ }
16
+
17
+ function renderSteps(steps: ReturnType<typeof getWorkflowRunStepsByRunId>): string | undefined {
18
+ if (steps.length === 0) return undefined;
19
+ return steps
20
+ .map((step) => {
21
+ const nodeId = (step as { nodeId?: unknown }).nodeId ?? "?";
22
+ const status = (step as { status?: unknown }).status ?? "?";
23
+ const error = (step as { error?: unknown }).error;
24
+ const errorSuffix = typeof error === "string" && error ? ` — error: ${error}` : "";
25
+ // Step results must reach the text channel — details suppresses the
26
+ // JSON fallback, and text-only harnesses never see structured data.
27
+ const output = errorSuffix
28
+ ? undefined
29
+ : stepValuePreview((step as { output?: unknown }).output);
30
+ const outputSuffix = output ? ` — output: ${output}` : "";
31
+ const diagnostics = stepValuePreview((step as { diagnostics?: unknown }).diagnostics);
32
+ const diagnosticsSuffix = diagnostics ? ` — diagnostics: ${diagnostics}` : "";
33
+ return `- ${String(nodeId)}: ${String(status)}${errorSuffix}${outputSuffix}${diagnosticsSuffix}`;
34
+ })
35
+ .join("\n");
36
+ }
5
37
 
6
38
  export const registerGetWorkflowRunTool = (server: McpServer) => {
7
39
  createToolRegistrar(server)(
@@ -13,46 +45,24 @@ export const registerGetWorkflowRunTool = (server: McpServer) => {
13
45
  inputSchema: z.object({
14
46
  id: z.string().uuid().describe("Workflow run ID"),
15
47
  }),
16
- outputSchema: z.object({
17
- success: z.boolean(),
18
- message: z.string(),
48
+ outputSchema: swarmToolOutputSchema({
19
49
  run: z.unknown().optional(),
20
- steps: z.array(z.unknown()),
50
+ steps: z.array(z.unknown()).optional(),
21
51
  }),
22
52
  },
23
53
  async ({ id }) => {
24
54
  try {
25
55
  const run = getWorkflowRun(id);
26
56
  if (!run) {
27
- return {
28
- content: [{ type: "text" as const, text: `Workflow run not found: ${id}` }],
29
- structuredContent: {
30
- success: false,
31
- message: `Workflow run not found: ${id}`,
32
- steps: [],
33
- },
34
- };
57
+ return toolErr(`Workflow run not found: ${id}`, { data: { steps: [] } });
35
58
  }
36
59
  const steps = getWorkflowRunStepsByRunId(id);
37
- return {
38
- content: [
39
- {
40
- type: "text" as const,
41
- text: `Run ${id} — status: ${run.status}, steps: ${steps.length}.`,
42
- },
43
- ],
44
- structuredContent: {
45
- success: true,
46
- message: `Run ${id} status: ${run.status}.`,
47
- run,
48
- steps,
49
- },
50
- };
60
+ return toolOk(`Run ${id} status: ${run.status}.`, {
61
+ details: renderSteps(steps),
62
+ data: { run, steps },
63
+ });
51
64
  } catch (err) {
52
- return {
53
- content: [{ type: "text" as const, text: `Failed: ${err}` }],
54
- structuredContent: { success: false, message: String(err), steps: [] },
55
- };
65
+ return toolErr(String(err), { data: { steps: [] } });
56
66
  }
57
67
  },
58
68
  );
@@ -1,7 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { getWorkflow } from "@/be/db";
4
- import { createToolRegistrar } from "@/tools/utils";
4
+ import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
5
5
  import { generateEdges } from "@/workflows/definition";
6
6
 
7
7
  export const registerGetWorkflowTool = (server: McpServer) => {
@@ -15,9 +15,7 @@ export const registerGetWorkflowTool = (server: McpServer) => {
15
15
  inputSchema: z.object({
16
16
  id: z.string().uuid().describe("Workflow ID"),
17
17
  }),
18
- outputSchema: z.object({
19
- success: z.boolean(),
20
- message: z.string(),
18
+ outputSchema: swarmToolOutputSchema({
21
19
  workflow: z.unknown().optional(),
22
20
  edges: z.array(z.unknown()).optional(),
23
21
  }),
@@ -26,27 +24,18 @@ export const registerGetWorkflowTool = (server: McpServer) => {
26
24
  try {
27
25
  const workflow = getWorkflow(id);
28
26
  if (!workflow) {
29
- return {
30
- content: [{ type: "text" as const, text: `Workflow not found: ${id}` }],
31
- structuredContent: { success: false, message: `Workflow not found: ${id}` },
32
- };
27
+ return toolErr(`Workflow not found: ${id}`);
33
28
  }
34
29
  // Auto-generate edges for UI rendering
35
30
  const edges = generateEdges(workflow.definition);
36
- return {
37
- content: [{ type: "text" as const, text: `Workflow "${workflow.name}" (${id}).` }],
38
- structuredContent: {
39
- success: true,
40
- message: `Workflow "${workflow.name}".`,
41
- workflow,
42
- edges,
43
- },
44
- };
31
+ return toolOk(`Workflow "${workflow.name}" (${id}).`, {
32
+ // The definition must reach the text channel most harnesses never
33
+ // show the model structuredContent.
34
+ details: JSON.stringify(workflow, null, 2),
35
+ data: { workflow, edges },
36
+ });
45
37
  } catch (err) {
46
- return {
47
- content: [{ type: "text" as const, text: `Failed: ${err}` }],
48
- structuredContent: { success: false, message: String(err) },
49
- };
38
+ return toolErr(String(err));
50
39
  }
51
40
  },
52
41
  );