@desplega.ai/agent-swarm 2.4.4 → 2.4.5

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 (221) hide show
  1. package/dist/{acp-adapter-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
  2. package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-k5ksyres.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
  15. package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
  16. package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
  25. package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
  26. package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
  29. package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
  30. package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
  31. package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
  32. package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
  33. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  34. package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
  35. package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
  36. package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
  37. package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
  38. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  39. package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
  40. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  41. package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
  42. package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
  43. package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
  44. package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
  45. package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
  46. package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
  47. package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
  48. package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
  49. package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
  50. package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
  51. package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
  52. package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
  53. package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
  54. package/dist/cli.js +19 -19
  55. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
  56. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
  57. package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
  58. package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
  59. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
  60. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
  61. package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
  62. package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
  63. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
  64. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
  65. package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
  66. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
  67. package/dist/handlers-7gw64dxw.js +39 -0
  68. package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
  69. package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
  70. package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
  71. package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
  72. package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
  73. package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
  74. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  75. package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
  76. package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
  77. package/dist/index-tfb26zhy.js +41 -0
  78. package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
  79. package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
  80. package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
  81. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  82. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  83. package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
  84. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
  85. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
  86. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
  87. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
  88. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  89. package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
  90. package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
  91. package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
  92. package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
  93. package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
  94. package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
  95. package/openapi.json +9 -2
  96. package/package.json +7 -7
  97. package/src/apps/catalog.generated.json +206 -4
  98. package/src/apps/page-validator.ts +13 -1
  99. package/src/be/chunking.ts +42 -9
  100. package/src/be/db/tasks/draft-text.ts +15 -0
  101. package/src/be/db/tasks/read.ts +14 -1
  102. package/src/be/db.ts +5 -1
  103. package/src/be/memory/index-content.ts +36 -17
  104. package/src/be/memory/providers/openai-embedding.ts +115 -61
  105. package/src/be/memory/providers/sqlite-store.ts +69 -23
  106. package/src/be/memory/reranker.ts +31 -1
  107. package/src/be/memory/search-candidates.ts +47 -0
  108. package/src/be/memory/types.ts +14 -0
  109. package/src/be/scripts/typecheck.ts +2 -0
  110. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  111. package/src/be/seed-scripts/index.ts +2 -2
  112. package/src/be/swarm-config-guard.ts +24 -1
  113. package/src/be/task-attachment-store.ts +30 -0
  114. package/src/extensions/catalog.generated.json +4 -4
  115. package/src/extensions/contract-types.generated.ts +1 -1
  116. package/src/extensions/contract.ts +7 -0
  117. package/src/fs/provider.ts +13 -1
  118. package/src/heartbeat/heartbeat.ts +31 -11
  119. package/src/http/fs.ts +54 -34
  120. package/src/http/mcp-body.ts +37 -0
  121. package/src/http/mcp-user.ts +3 -5
  122. package/src/http/mcp.ts +6 -7
  123. package/src/http/memory.ts +26 -15
  124. package/src/http/page-proxy.ts +61 -5
  125. package/src/http/pages-public.ts +27 -44
  126. package/src/http/pages.ts +4 -4
  127. package/src/http/script-runs.ts +2 -0
  128. package/src/http/users.ts +12 -1
  129. package/src/integrations/chat-sdk/adapters.ts +7 -3
  130. package/src/integrations/chat-sdk/attachments.ts +90 -12
  131. package/src/integrations/chat-sdk/facts.ts +120 -0
  132. package/src/integrations/chat-sdk/inbound.ts +156 -8
  133. package/src/integrations/chat-sdk/limits.ts +24 -0
  134. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  135. package/src/integrations/chat-sdk/outbound.ts +234 -4
  136. package/src/integrations/chat-sdk/relay.ts +4 -1
  137. package/src/integrations/chat-sdk/router.ts +28 -2
  138. package/src/integrations/chat-sdk/steering.ts +9 -0
  139. package/src/integrations/chat-sdk/templates.ts +14 -0
  140. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  141. package/src/mcp-limits.ts +20 -0
  142. package/src/pages/browser-sdk.ts +8 -3
  143. package/src/pages/feedback-overlay.ts +1 -1
  144. package/src/providers/amp-adapter.ts +1 -1
  145. package/src/providers/codex-adapter.ts +106 -119
  146. package/src/providers/codex-agents-md.ts +35 -229
  147. package/src/providers/cursor-adapter.ts +1 -1
  148. package/src/realtime/transport.ts +1 -2
  149. package/src/script-workflows/workflow-ctx.ts +1 -0
  150. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  151. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  152. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  153. package/src/server-user.ts +2 -0
  154. package/src/server.ts +2 -0
  155. package/src/slack/assistant.ts +2 -2
  156. package/src/slack/files.ts +2 -0
  157. package/src/slack/handlers.ts +4 -4
  158. package/src/slack/inbound-files.ts +39 -6
  159. package/src/slack/thread-buffer.ts +2 -1
  160. package/src/tasks/task-terminal-effects.ts +21 -17
  161. package/src/tests/apps-card-grid.test.ts +197 -0
  162. package/src/tests/apps-catalog-additions.test.ts +120 -0
  163. package/src/tests/apps-tree.test.ts +129 -0
  164. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  165. package/src/tests/chat-sdk-config.test.ts +11 -0
  166. package/src/tests/chat-sdk-facts.test.ts +23 -0
  167. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  168. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  169. package/src/tests/codex-adapter.test.ts +167 -242
  170. package/src/tests/config-api-only-keys.test.ts +9 -0
  171. package/src/tests/cors-allowlist.test.ts +1 -1
  172. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  173. package/src/tests/heartbeat-checklist.test.ts +33 -0
  174. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  175. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  176. package/src/tests/kv-page-proxy.test.ts +91 -5
  177. package/src/tests/launch-password-rejection.test.ts +2 -2
  178. package/src/tests/mcp-input-caps.test.ts +294 -0
  179. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  180. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  181. package/src/tests/memory.test.ts +14 -0
  182. package/src/tests/page-guest-session.test.ts +19 -8
  183. package/src/tests/page-proxy-authed.test.ts +1 -1
  184. package/src/tests/page-proxy.test.ts +17 -10
  185. package/src/tests/page-session.test.ts +102 -1
  186. package/src/tests/pages-authed-mode.test.ts +82 -12
  187. package/src/tests/pages-password-mode.test.ts +5 -5
  188. package/src/tests/realtime-transport.test.ts +13 -7
  189. package/src/tests/script-runs-http.test.ts +84 -0
  190. package/src/tests/scripts-http.test.ts +10 -1
  191. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  192. package/src/tests/task-audio-transcription.test.ts +299 -0
  193. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  194. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  195. package/src/tests/transcription.test.ts +299 -0
  196. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  197. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  198. package/src/tools/chat-access.ts +10 -0
  199. package/src/tools/chat-post.ts +31 -11
  200. package/src/tools/chat-reply.ts +32 -10
  201. package/src/tools/memory-search.ts +3 -1
  202. package/src/tools/system-one-decide.ts +17 -4
  203. package/src/types.ts +6 -0
  204. package/src/utils/config-env-export.ts +3 -1
  205. package/src/utils/page-session.ts +133 -20
  206. package/src/utils/transcription.ts +246 -0
  207. package/src/workflows/executors/system-one-decision.ts +28 -7
  208. package/src/workflows/executors/system-one-providers.ts +39 -4
  209. package/templates/extensions/slack-reply-gate/README.md +24 -22
  210. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  211. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  212. package/templates/skills/apps/SKILL.md +28 -6
  213. package/templates/skills/apps/content.md +28 -6
  214. package/templates/skills/chat-sdk/content.md +6 -2
  215. package/templates/skills/code-quality/content.md +2 -1
  216. package/templates/skills/heartbeat-runbook/content.md +5 -5
  217. package/templates/skills/pages/content.md +6 -4
  218. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  219. package/templates/skills/swarm-scripts/content.md +2 -2
  220. package/dist/handlers-0pvee9nw.js +0 -39
  221. package/dist/index-thm0y6a8.js +0 -41
@@ -288,15 +288,41 @@ export const SYSTEM_ONE_PROVIDERS = Object.fromEntries(
288
288
  export const SYSTEM_ONE_PROVIDER_IDS = hostsServing("decide");
289
289
  export const SYSTEM_ONE_EXTRACT_PROVIDER_IDS = hostsServing("extract");
290
290
 
291
+ /** The decision provider when neither the call nor `SYSTEM_ONE_DEFAULT_PROVIDER` names one. */
291
292
  export const SYSTEM_ONE_DEFAULT_PROVIDER: SystemOneProviderId = "typesafe";
292
293
  export const SYSTEM_ONE_EXTRACT_DEFAULT_PROVIDER: SystemOneExtractProviderId = "cloudflare";
293
294
 
295
+ /** Global config key naming the decision provider a node or call without `provider` uses. */
296
+ export const SYSTEM_ONE_DEFAULT_PROVIDER_CONFIG_KEY = "SYSTEM_ONE_DEFAULT_PROVIDER";
297
+
294
298
  export function isSystemOneProviderId(value: unknown): value is SystemOneProviderId {
295
299
  return (
296
300
  typeof value === "string" && (SYSTEM_ONE_PROVIDER_IDS as readonly string[]).includes(value)
297
301
  );
298
302
  }
299
303
 
304
+ /**
305
+ * The decision provider for a node or call that names none: the global
306
+ * `SYSTEM_ONE_DEFAULT_PROVIDER` row, then the same key in the environment, then
307
+ * `typesafe`. Read at run time, so changing the setting needs no workflow edit.
308
+ * A value that is not a decide host is skipped; the config guard refuses it on save.
309
+ */
310
+ export async function resolveSystemOneDefaultProvider(
311
+ ctx: SystemOneProviderContext,
312
+ ): Promise<SystemOneProviderId> {
313
+ let stored: string | undefined;
314
+ try {
315
+ stored = await readGlobalConfig(ctx.db, SYSTEM_ONE_DEFAULT_PROVIDER_CONFIG_KEY);
316
+ } catch {
317
+ // An unreadable row falls back like an unset one: the default stays usable.
318
+ }
319
+ for (const raw of [stored, ctx.env[SYSTEM_ONE_DEFAULT_PROVIDER_CONFIG_KEY]]) {
320
+ const id = raw?.trim().toLowerCase();
321
+ if (isSystemOneProviderId(id)) return id;
322
+ }
323
+ return SYSTEM_ONE_DEFAULT_PROVIDER;
324
+ }
325
+
300
326
  export function isSystemOneExtractProviderId(value: unknown): value is SystemOneExtractProviderId {
301
327
  return (
302
328
  typeof value === "string" &&
@@ -304,11 +330,15 @@ export function isSystemOneExtractProviderId(value: unknown): value is SystemOne
304
330
  );
305
331
  }
306
332
 
307
- /** Provider a raw node config selects. Undefined when the field is present but not a known id. */
333
+ /**
334
+ * Provider a raw node config selects, `defaultId` when it names none. Undefined
335
+ * when the field is present but not a known id.
336
+ */
308
337
  export function systemOneProviderOf(
309
338
  config: Record<string, unknown>,
339
+ defaultId: SystemOneProviderId = SYSTEM_ONE_DEFAULT_PROVIDER,
310
340
  ): SystemOneProviderId | undefined {
311
- if (config.provider === undefined) return SYSTEM_ONE_DEFAULT_PROVIDER;
341
+ if (config.provider === undefined) return defaultId;
312
342
  return isSystemOneProviderId(config.provider) ? config.provider : undefined;
313
343
  }
314
344
 
@@ -489,7 +519,12 @@ export type SystemOneTarget =
489
519
  /** The secret name the key was read under, for a rejection message. */
490
520
  keyName: string;
491
521
  }
492
- | { ok: false; error: string };
522
+ | {
523
+ ok: false;
524
+ error: string;
525
+ /** This deployment refuses the host whatever its credential (`deploymentProblem`). */
526
+ deploymentRefused?: true;
527
+ };
493
528
 
494
529
  export interface SystemOneTargetOverrides {
495
530
  /** Replaces the credential lookup. */
@@ -517,7 +552,7 @@ export async function resolveSystemOneTarget(
517
552
  const capability = overrides.capability ?? "decide";
518
553
  const service = serviceOf(id, capability);
519
554
  const deploymentProblem = host.deploymentProblem?.(ctx.env);
520
- if (deploymentProblem) return { ok: false, error: deploymentProblem };
555
+ if (deploymentProblem) return { ok: false, error: deploymentProblem, deploymentRefused: true };
521
556
 
522
557
  const errors: string[] = [];
523
558
 
@@ -1,6 +1,6 @@
1
1
  # slack-reply-gate
2
2
 
3
- Lead is woken by every human message in a Slack thread the swarm takes part in, including "lol", an emoji, or two people chatting with each other. This extension asks Jev, [TypeSafe](https://typesafe.ai)'s decision model, whether Lead should answer a new thread message, and drops the wake-up when it should not.
3
+ Lead is woken by every human message in a Slack thread the swarm takes part in, including "lol", an emoji, or two people chatting with each other. This extension asks a SystemOne decision model, Jev ([TypeSafe](https://typesafe.ai)) or Clef (Cloudflare Workers AI), whether Lead should answer a new thread message, and drops the wake-up when it should not.
4
4
 
5
5
  Install config:
6
6
 
@@ -15,8 +15,8 @@ Start in `shadow`, read the decisions in KV for a few days, then switch to `enfo
15
15
  | `mode` | Effect |
16
16
  |---|---|
17
17
  | `off` | Never evaluates. |
18
- | `shadow` (default) | Asks Jev and records the verdict. Never blocks. |
19
- | `enforce` | Asks Jev, records the verdict, and blocks the Lead task when the answer is no. The message gets :mute: instead: no Lead task, no reply, no outcome card. |
18
+ | `shadow` (default) | Asks the model and records the verdict. Never blocks. |
19
+ | `enforce` | Asks the model, records the verdict, and blocks the Lead task when the answer is no. The message gets :mute: instead: no Lead task, no reply, no outcome card. |
20
20
 
21
21
  ## What it gates
22
22
 
@@ -26,28 +26,30 @@ The header counts only at the start of the description or right after the closin
26
26
 
27
27
  ## Decision
28
28
 
29
- 1. A follow-up with downloaded files (status `draft`) passes without a Jev call.
30
- 2. **@mention pass-through.** A new message that @mentions Lead passes without a Jev call. A mention is the bot mention the engine rewrites to `<@U…> (that's you)`, a resolved `<@U…|lead>`, or a literal `@lead` (any name in `leadAliases`). Only the new messages are scanned, never the thread context, so an old mention earlier in the thread does not pass later banter.
31
- 3. Otherwise one Jev call (one `choice` question, see [Jev transport](#jev-transport)) with the new message(s), the sender kind, and the last `contextMessages` thread messages. Labels: `direct_ask`, `followup_to_lead`, `instruction_to_lead`, `reaction_only`, `banter_between_others`, `not_addressed_to_lead`, `other`.
29
+ 1. A follow-up with downloaded files (status `draft`) passes without a decision call.
30
+ 2. **@mention pass-through.** A new message that @mentions Lead passes without a decision call. A mention is the bot mention the engine rewrites to `<@U…> (that's you)`, a resolved `<@U…|lead>`, or a literal `@lead` (any name in `leadAliases`). Only the new messages are scanned, never the thread context, so an old mention earlier in the thread does not pass later banter.
31
+ 3. Otherwise one decision call (one `choice` question, see [Provider](#provider)) with the new message(s), the sender kind, and the last `contextMessages` thread messages. Labels: `direct_ask`, `followup_to_lead`, `instruction_to_lead`, `reaction_only`, `banter_between_others`, `not_addressed_to_lead`, `other`.
32
32
  4. The answer is no only when the top label is `reaction_only`, `banter_between_others`, or `not_addressed_to_lead`, and the summed probability of those three labels is at least `minConfidence`. A split between two skip labels is still a skip. An unsure verdict replies.
33
33
 
34
34
  Several buffered messages are judged together. One mention in any of them passes the batch.
35
35
 
36
- ## Jev transport
36
+ ## Provider
37
37
 
38
- Jev is reachable two ways. Both take the same request and return the same answer, so the decision is identical whichever one runs.
38
+ The decision is made on the API server by the `system-one-decide` tool (`ctx.swarm.systemOne_decide`), the same path a `system-one-decision` workflow node takes. The server reads the provider's credential, sends the request, and validates the answer, so every SystemOne host works and no key enters the hook.
39
39
 
40
- | `provider` | Endpoint | Key | Model |
41
- |---|---|---|---|
42
- | `typesafe` | `POST https://api.typesafe.ai/v1/systemone` | `TYPESAFE_API_KEY` (or `apiKey`) | `model`, default `jev-latest` |
43
- | `openrouter` | `POST https://openrouter.ai/api/alpha/decisions` | `OPENROUTER_API_KEY` | `openrouterModel`, default `typesafe/jev-1.13` |
44
- | `auto` (default) | TypeSafe when its key is set, else OpenRouter | | |
40
+ | `provider` | Model | Credential |
41
+ |---|---|---|
42
+ | `typesafe` | Jev, `model` (default `jev-latest`) | `TYPESAFE_API_KEY` |
43
+ | `openrouter` | Jev, `openrouterModel` (default `typesafe/jev-1.13`) | `OPENROUTER_API_KEY` |
44
+ | `cloudflare` | Clef, `cloudflareModel` (default `clef`, or `clef-flash`) | `CLOUDFLARE_API_TOKEN` (or `CLOUDFLARE_API_KEY`) and `CLOUDFLARE_ACCOUNT_ID` |
45
+ | `laya`, `openai` | The host's default model | See the workflows guide |
46
+ | `auto` (default) | The first of typesafe, openrouter, cloudflare whose credential is set. A provider the server refuses for a deployment setting (openrouter when `OPENROUTER_BASE_URL` points at a gateway) is skipped for the next one | |
45
47
 
46
- Under `auto`, `TYPESAFE_API_KEY` wins when both keys are set. With neither key, every message fails open and the KV row names both secrets. Each decision row records the `provider` that answered.
48
+ With no credential for `auto`, every message fails open and the KV row names the secrets. Each decision row records the `provider` that answered. `minConfidence` was tuned on Jev; check Clef's verdicts in `shadow` before you enforce on it.
47
49
 
48
50
  ## Fail open
49
51
 
50
- Every error lets the task through: a missing key, a TypeSafe HTTP error, a network error, a timeout, a malformed answer, or an unknown label. The handler never throws, so an outage cannot auto-disable the extension. Each failure is recorded as `fail-open` in KV and logged as a warning.
52
+ Every error lets the task through: a missing key, a provider error relayed by the server, a network error, a timeout, a malformed answer, or an unknown label. The handler never throws, so an outage cannot auto-disable the extension. Each failure is recorded as `fail-open` in KV and logged as a warning.
51
53
 
52
54
  ## :mute: on drop (enforce)
53
55
 
@@ -63,23 +65,23 @@ The block returns first and the Slack calls run after it. A slow or failing Slac
63
65
  | Key | Default | Notes |
64
66
  |---|---|---|
65
67
  | `mode` | `shadow` | `off`, `shadow`, or `enforce` |
66
- | `timeoutMs` | `3000` | Jev call timeout, capped at 4000 (the dispatcher cancels a handler at 5 s) |
67
- | `provider` | `auto` | `auto`, `typesafe`, or `openrouter`. See [Jev transport](#jev-transport) |
68
+ | `timeoutMs` | `3000` | Decision call timeout, capped at 4000 (the dispatcher cancels a handler at 5 s) |
69
+ | `provider` | `auto` | `auto`, `typesafe`, `openrouter`, `cloudflare`, `laya`, or `openai`. See [Provider](#provider) |
68
70
  | `model` | `jev-latest` | Jev model on TypeSafe |
69
71
  | `openrouterModel` | `typesafe/jev-1.13` | Jev model on OpenRouter |
72
+ | `cloudflareModel` | `clef` | Clef model on Cloudflare Workers AI |
70
73
  | `minConfidence` | `0.55` | Minimum summed skip probability to skip |
71
- | `contextMessages` | `6` | Thread messages sent to Jev, 0 to 20 |
74
+ | `contextMessages` | `6` | Thread messages sent to the model, 0 to 20 |
72
75
  | `leadAliases` | `["lead"]` | A literal `@alias` in a new message counts as a mention |
73
- | `botSlackUserIds` | `[]` | Slack user ids labelled `bot` as the sender for Jev |
74
- | `apiKey` | unset | Explicit TypeSafe key. Prefer the `TYPESAFE_API_KEY` secret below |
76
+ | `botSlackUserIds` | `[]` | Slack user ids labelled `bot` as the sender for the model |
75
77
  | `muteReaction` | `mute` | Reaction for a dropped message; `""` only removes the acceptance reaction(s) |
76
78
  | `slackTimeoutMs` | `5000` | Timeout per Slack call |
77
79
 
78
80
  ## Required secrets
79
81
 
80
- All are read from the API process environment at call time. Set them as global `swarm_config` secrets: the API process loads global rows into `process.env` at boot and on config reload.
82
+ Set them as global `swarm_config` values. The server reads the provider credential from the global row when it makes the decision. `auto` and the Slack token read the API process environment, which loads global rows at boot and on config reload.
81
83
 
82
- - `TYPESAFE_API_KEY` or `OPENROUTER_API_KEY`: one is required for any evaluation. Under `auto`, TypeSafe wins when both are set. Without either, every message fails open.
84
+ - One provider credential from the [Provider](#provider) table is required for any evaluation. Without one, every message fails open. The `apiKey` config field of earlier versions is gone: the hook never holds a key, so store it as the global `TYPESAFE_API_KEY` secret.
83
85
  - `SLACK_BOT_TOKEN`: the Slack app's bot token. A swarm with Slack connected already has it. Without it, `enforce` still drops the message but cannot touch its reactions, and logs a `mute-error`.
84
86
 
85
87
  No key is written to config, KV, logs, or a description.
@@ -6,8 +6,9 @@ import { z } from "zod";
6
6
  *
7
7
  * Every human message in a Slack thread the swarm takes part in is buffered and
8
8
  * flushed into a Lead task ("[Thread follow-up — N message(s) buffered]"), even
9
- * "lol", ":joy:" or two people bantering. This hook asks TypeSafe Jev whether
10
- * Lead should answer, and drops the wake-up when the answer is no.
9
+ * "lol", ":joy:" or two people bantering. This hook asks a SystemOne decision
10
+ * model (Jev or Clef) whether Lead should answer, and drops the wake-up when the
11
+ * answer is no.
11
12
  *
12
13
  * Hook: `pre.task.create`, origin "slack", description carrying the
13
14
  * thread-buffer header. `pre.slack.route` cannot be used: the buffered thread
@@ -15,17 +16,20 @@ import { z } from "zod";
15
16
  * re-target a message, never drop it.
16
17
  *
17
18
  * Rules:
18
- * - A message that @mentions Lead passes without a Jev call.
19
- * - Otherwise one Jev `choice` question classifies the message into a reason;
19
+ * - A message that @mentions Lead passes without a decision call.
20
+ * - Otherwise one `choice` question classifies the message into a reason;
20
21
  * reply=false only for reaction_only | banter_between_others |
21
22
  * not_addressed_to_lead, and only when the summed probability of those
22
23
  * three labels is >= minConfidence (a banter/reaction split is still a skip).
23
24
  * - Fails OPEN on every error, timeout or missing key.
24
25
  *
25
- * Transport (config.provider): Jev is served by TypeSafe directly and by
26
- * OpenRouter's Decisions endpoint. Both take the same {model, state, questions}
27
- * body and return the same `answers` shape. `auto` uses TYPESAFE_API_KEY when
28
- * set, else OPENROUTER_API_KEY.
26
+ * Provider (config.provider): the decision is made server-side by the
27
+ * `system-one-decide` tool (`ctx.swarm.systemOne_decide`), the same path as a
28
+ * `system-one-decision` workflow node, so every SystemOne host works and no key
29
+ * enters this hook. `auto` picks the first host with a configured credential:
30
+ * typesafe (Jev, TYPESAFE_API_KEY), openrouter (Jev, OPENROUTER_API_KEY), then
31
+ * cloudflare (Clef, CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY, plus
32
+ * CLOUDFLARE_ACCOUNT_ID). laya and openai are explicit choices only.
29
33
  *
30
34
  * modes (config.mode):
31
35
  * off -> never evaluate
@@ -48,17 +52,19 @@ import { z } from "zod";
48
52
  export const config = z.object({
49
53
  mode: z.enum(["off", "shadow", "enforce"]).default("shadow"),
50
54
  timeoutMs: z.number().int().positive().default(3000),
51
- provider: z.enum(["auto", "typesafe", "openrouter"]).default("auto"),
52
- /** Jev model on the TypeSafe transport. */
55
+ provider: z
56
+ .enum(["auto", "typesafe", "openrouter", "cloudflare", "laya", "openai"])
57
+ .default("auto"),
58
+ /** Jev model on TypeSafe. */
53
59
  model: z.string().default("jev-latest"),
54
- /** Jev model on the OpenRouter transport. */
60
+ /** Jev model on OpenRouter. */
55
61
  openrouterModel: z.string().default("typesafe/jev-1.13"),
62
+ /** Clef model on Cloudflare Workers AI: clef or clef-flash. */
63
+ cloudflareModel: z.string().default("clef"),
56
64
  minConfidence: z.number().min(0).max(1).default(0.55),
57
65
  contextMessages: z.number().int().min(0).max(20).default(6),
58
66
  leadAliases: z.array(z.string()).default(["lead"]),
59
67
  botSlackUserIds: z.array(z.string()).default([]),
60
- /** Explicit TypeSafe key; otherwise TYPESAFE_API_KEY from the API process env. */
61
- apiKey: z.string().optional(),
62
68
  /**
63
69
  * Reaction for a dropped message in enforce mode. "" only removes the bot's
64
70
  * acceptance reaction(s) and adds nothing.
@@ -71,13 +77,13 @@ type Cfg = z.infer<typeof config>;
71
77
  type Swarm = ApiCtx["swarm"];
72
78
 
73
79
  const NS = "ext:slack-reply-gate";
74
- const TYPESAFE_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
75
- const OPENROUTER_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
76
80
  const HEADER_LINE_RE = /(?:^|\n)\[Thread follow-up — \d+ message\(s\) buffered\]\n/g;
77
81
  const CTX_OPEN = "<thread_context>";
78
82
  const CTX_CLOSE = "</thread_context>";
79
83
  // The dispatcher cancels a handler after 5 s; leave room for the KV writes.
80
84
  const MAX_TIMEOUT_MS = 4000;
85
+ // The smallest time budget system-one-decide accepts.
86
+ const MIN_DECIDE_TIMEOUT_MS = 1000;
81
87
 
82
88
  export const REASONS = {
83
89
  direct_ask:
@@ -179,14 +185,14 @@ const usable = (v: string): boolean => Boolean(v) && !v.startsWith("[REDACTED");
179
185
  const envOf = (name: string): string => String(process.env[name] ?? "");
180
186
 
181
187
  /**
182
- * Resolve a secret: explicit value, then the API process env. The API process loads
183
- * global swarm_config rows into process.env at boot and on config reload.
188
+ * Resolve a secret from the API process env. The API process loads global
189
+ * swarm_config rows into process.env at boot and on config reload.
184
190
  * ctx.swarm.config_get cannot serve it: the SDK scrubs secrets to "[REDACTED:<name>]".
185
191
  */
186
- const secret = (name: string, explicit?: string): string => {
192
+ const secret = (name: string): string => {
187
193
  const cached = secretCache.get(name);
188
194
  if (cached && Date.now() - cached.at < KEY_TTL_MS) return cached.value;
189
- const value = [String(explicit ?? ""), envOf(name)].find(usable) ?? "";
195
+ const value = usable(envOf(name)) ? envOf(name) : "";
190
196
  if (usable(value)) secretCache.set(name, { value, at: Date.now() });
191
197
  return value;
192
198
  };
@@ -196,46 +202,51 @@ export const resetKeyCache = (): void => {
196
202
  secretCache.clear();
197
203
  };
198
204
 
199
- export type JevTransport = {
200
- provider: "typesafe" | "openrouter";
201
- endpoint: string;
202
- key: string;
203
- model: string;
204
- };
205
+ export type ProviderId = Exclude<Cfg["provider"], "auto">;
206
+
207
+ /** Where the decision is made. No model means the provider's own default. */
208
+ export type DecisionTarget = { provider: ProviderId; model?: string };
209
+
210
+ /** What `auto` needs per provider: every group must have one usable value. */
211
+ const AUTO_CREDENTIALS: ReadonlyArray<[ProviderId, string[][]]> = [
212
+ ["typesafe", [["TYPESAFE_API_KEY"]]],
213
+ ["openrouter", [["OPENROUTER_API_KEY"]]],
214
+ ["cloudflare", [["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_KEY"], ["CLOUDFLARE_ACCOUNT_ID"]]],
215
+ ];
216
+
217
+ const modelFor = (provider: ProviderId, cfg: Cfg): string | undefined =>
218
+ provider === "typesafe"
219
+ ? cfg.model
220
+ : provider === "openrouter"
221
+ ? cfg.openrouterModel
222
+ : provider === "cloudflare"
223
+ ? cfg.cloudflareModel
224
+ : undefined;
205
225
 
206
226
  /**
207
- * Pick the Jev transport. `auto` prefers TypeSafe and falls back to OpenRouter.
208
- * Throws when the chosen provider has no usable key; the handler fails open.
227
+ * The decision providers to try, in order. An explicit one is used as is: the
228
+ * server checks its credential. `auto` lists every provider in `AUTO_CREDENTIALS`
229
+ * whose credential is set and throws when none is; the handler then fails open.
209
230
  */
210
- export const resolveTransport = (cfg: Cfg): JevTransport => {
211
- const typesafe = (): JevTransport | null => {
212
- const key = secret("TYPESAFE_API_KEY", cfg.apiKey);
213
- return usable(key)
214
- ? { provider: "typesafe", endpoint: TYPESAFE_ENDPOINT, key, model: cfg.model }
215
- : null;
216
- };
217
- const openrouter = (): JevTransport | null => {
218
- const key = secret("OPENROUTER_API_KEY");
219
- return usable(key)
220
- ? { provider: "openrouter", endpoint: OPENROUTER_ENDPOINT, key, model: cfg.openrouterModel }
221
- : null;
222
- };
223
- const transport =
224
- cfg.provider === "typesafe"
225
- ? typesafe()
226
- : cfg.provider === "openrouter"
227
- ? openrouter()
228
- : (typesafe() ?? openrouter());
229
- if (transport) return transport;
231
+ export const resolveProviders = (cfg: Cfg): DecisionTarget[] => {
232
+ if (cfg.provider !== "auto") {
233
+ return [{ provider: cfg.provider, model: modelFor(cfg.provider, cfg) }];
234
+ }
235
+ const targets = AUTO_CREDENTIALS.filter(([, groups]) =>
236
+ groups.every((keys) => keys.some((key) => usable(secret(key)))),
237
+ ).map(([provider]) => ({ provider, model: modelFor(provider, cfg) }));
238
+ if (targets.length > 0) return targets;
230
239
  throw new Error(
231
- cfg.provider === "typesafe"
232
- ? "no TYPESAFE_API_KEY"
233
- : cfg.provider === "openrouter"
234
- ? "no OPENROUTER_API_KEY"
235
- : "no TYPESAFE_API_KEY or OPENROUTER_API_KEY",
240
+ "no TYPESAFE_API_KEY, OPENROUTER_API_KEY, or CLOUDFLARE_API_TOKEN with CLOUDFLARE_ACCOUNT_ID",
236
241
  );
237
242
  };
238
243
 
244
+ /** The first provider `resolveProviders` would try. */
245
+ export const resolveProvider = (cfg: Cfg): DecisionTarget => resolveProviders(cfg)[0]!;
246
+
247
+ /** The server refused the provider for a deployment setting and sent nothing. */
248
+ export class ProviderUnavailableError extends Error {}
249
+
239
250
  const SLACK_API = "https://slack.com/api";
240
251
  // The engine's acceptance reactions (src/slack/reaction-shortcode.ts), by config key.
241
252
  const ACCEPTANCE_REACTIONS: ReadonlyArray<[string, string]> = [
@@ -323,7 +334,7 @@ export const settleMutes = async (): Promise<void> => {
323
334
  await Promise.allSettled([...pendingMutes]);
324
335
  };
325
336
 
326
- export type JevVerdict = {
337
+ export type Verdict = {
327
338
  reason: string;
328
339
  confidence: number | null;
329
340
  probabilities: Record<string, number> | null;
@@ -331,56 +342,73 @@ export type JevVerdict = {
331
342
  };
332
343
 
333
344
  /**
334
- * One Jev call: classify the newest message into a reason. Throws on any failure.
335
- * TypeSafe and OpenRouter share the request body and the `answers` response shape.
345
+ * One decision: classify the newest message into a reason. Throws on any failure.
346
+ * The server makes the call (`system-one-decide`), so every provider returns the
347
+ * same `answers` shape. The time budget is enforced here and on the server.
336
348
  */
337
- export const askJev = async (
349
+ export const askModel = async (
350
+ swarm: Swarm,
338
351
  input: {
339
352
  messages: string[];
340
353
  context: string[];
341
354
  sender: string;
342
355
  leadNames: readonly string[];
343
356
  },
344
- transport: JevTransport,
357
+ target: DecisionTarget,
345
358
  cfg: Cfg,
346
359
  outer?: AbortSignal,
347
- ): Promise<JevVerdict> => {
348
- const controller = new AbortController();
349
- const timer = setTimeout(() => controller.abort(), Math.min(cfg.timeoutMs, MAX_TIMEOUT_MS));
350
- const onOuterAbort = () => controller.abort();
351
- outer?.addEventListener("abort", onOuterAbort);
360
+ ): Promise<Verdict> => {
361
+ const budgetMs = Math.min(cfg.timeoutMs, MAX_TIMEOUT_MS);
362
+ let timer: ReturnType<typeof setTimeout> | undefined;
363
+ let onOuterAbort: (() => void) | undefined;
364
+ const stop = new Promise<never>((_resolve, reject) => {
365
+ timer = setTimeout(() => reject(new Error(`timed out after ${budgetMs}ms`)), budgetMs);
366
+ onOuterAbort = () => reject(new Error("aborted"));
367
+ outer?.addEventListener("abort", onOuterAbort);
368
+ });
352
369
  const t0 = Date.now();
353
370
  try {
354
- const res = await fetch(transport.endpoint, {
355
- method: "POST",
356
- headers: {
357
- "Content-Type": "application/json",
358
- Authorization: `Bearer ${transport.key}`,
371
+ const call = swarm.systemOne_decide({
372
+ provider: target.provider,
373
+ ...(target.model ? { model: target.model } : {}),
374
+ state: {
375
+ role: "Decide whether Lead, the coordinating AI agent of a team's agent swarm, should answer a new Slack message in a thread it takes part in. Lead is woken by every message in the thread, including ones not meant for it. Answering noise wastes everyone's time; missing a real ask is worse.",
376
+ lead_names: [...input.leadNames],
377
+ recent_thread_messages: input.context,
378
+ new_message: input.messages.join("\n---\n"),
379
+ new_message_sender: input.sender,
359
380
  },
360
- body: JSON.stringify({
361
- state: {
362
- role: "Decide whether Lead, the coordinating AI agent of a team's agent swarm, should answer a new Slack message in a thread it takes part in. Lead is woken by every message in the thread, including ones not meant for it. Answering noise wastes everyone's time; missing a real ask is worse.",
363
- lead_names: input.leadNames,
364
- recent_thread_messages: input.context,
365
- new_message: input.messages.join("\n---\n"),
366
- new_message_sender: input.sender,
367
- },
368
- model: transport.model,
369
- questions: {
370
- reason: {
371
- type: "choice",
372
- instructions:
373
- "Which label best describes new_message from Lead's point of view? Use recent_thread_messages to tell who it is talking to. Lines starting with [Agent] are swarm bots.",
374
- criteria: REASONS,
375
- },
381
+ questions: {
382
+ reason: {
383
+ type: "choice",
384
+ instructions:
385
+ "Which label best describes new_message from Lead's point of view? Use recent_thread_messages to tell who it is talking to. Lines starting with [Agent] are swarm bots.",
386
+ criteria: REASONS,
376
387
  },
377
- }),
378
- signal: controller.signal,
388
+ },
389
+ timeoutMs: Math.max(budgetMs, MIN_DECIDE_TIMEOUT_MS),
390
+ maxRetries: 0,
379
391
  });
380
- if (!res.ok) throw new Error(`${transport.provider} ${res.status}`);
381
- const body = (await res.json()) as {
392
+ // After a timeout nobody awaits the call; its late failure must not go unhandled.
393
+ call.catch(() => {});
394
+ const res = (await Promise.race([call, stop])) as {
395
+ success?: boolean;
396
+ status?: number;
397
+ data?: unknown;
398
+ } | null;
399
+ const body = (res?.data ?? res) as {
400
+ success?: boolean;
401
+ message?: unknown;
402
+ error?: unknown;
403
+ providerUnavailable?: unknown;
382
404
  answers?: { reason?: { choice?: unknown; confidence?: unknown; probabilities?: unknown } };
383
405
  } | null;
406
+ if (res?.success === false || body?.success === false) {
407
+ const why = `${target.provider}: ${String(body?.message ?? body?.error ?? `HTTP ${res?.status}`).slice(0, 200)}`;
408
+ throw body?.providerUnavailable === true
409
+ ? new ProviderUnavailableError(why)
410
+ : new Error(why);
411
+ }
384
412
  const answer = body?.answers?.reason;
385
413
  const reason = typeof answer?.choice === "string" ? answer.choice : "";
386
414
  if (!(reason in REASONS)) throw new Error(`unexpected choice: ${String(reason).slice(0, 60)}`);
@@ -392,12 +420,34 @@ export const askJev = async (
392
420
  return { reason, confidence, probabilities, latencyMs: Date.now() - t0 };
393
421
  } finally {
394
422
  clearTimeout(timer);
395
- outer?.removeEventListener("abort", onOuterAbort);
423
+ if (onOuterAbort) outer?.removeEventListener("abort", onOuterAbort);
396
424
  }
397
425
  };
398
426
 
427
+ /**
428
+ * `askModel` on each target in turn, moving on only when the server refused the
429
+ * provider for a deployment setting (no request was sent). Any other failure, or
430
+ * a refusal of the last target, throws.
431
+ */
432
+ export const askFirstAvailable = async (
433
+ swarm: Swarm,
434
+ input: Parameters<typeof askModel>[1],
435
+ targets: readonly DecisionTarget[],
436
+ cfg: Cfg,
437
+ outer?: AbortSignal,
438
+ ): Promise<{ verdict: Verdict; target: DecisionTarget }> => {
439
+ for (const [i, target] of targets.entries()) {
440
+ try {
441
+ return { verdict: await askModel(swarm, input, target, cfg, outer), target };
442
+ } catch (err) {
443
+ if (!(err instanceof ProviderUnavailableError) || i === targets.length - 1) throw err;
444
+ }
445
+ }
446
+ throw new Error("no decision provider");
447
+ };
448
+
399
449
  /** Probability that the message needs no reply: the summed mass of the skip labels. */
400
- export const skipProbability = (verdict: JevVerdict): number | null => {
450
+ export const skipProbability = (verdict: Verdict): number | null => {
401
451
  if (verdict.probabilities) {
402
452
  let sum = 0;
403
453
  for (const [label, p] of Object.entries(verdict.probabilities)) {
@@ -408,9 +458,9 @@ export const skipProbability = (verdict: JevVerdict): number | null => {
408
458
  return verdict.confidence;
409
459
  };
410
460
 
411
- /** Map a Jev verdict to the typed decision. An unsure skip verdict replies. */
461
+ /** Map a verdict to the typed decision. An unsure skip verdict replies. */
412
462
  export const decide = (
413
- verdict: JevVerdict,
463
+ verdict: Verdict,
414
464
  minConfidence: number,
415
465
  ): { reply: boolean; reason: Reason; skipP: number | null; lowConfidence: boolean } => {
416
466
  const reason = verdict.reason as Reason;
@@ -507,7 +557,7 @@ const extension: SwarmExtension = (api) => {
507
557
  return;
508
558
  }
509
559
 
510
- const transport = resolveTransport(cfg);
560
+ const targets = resolveProviders(cfg);
511
561
 
512
562
  const sender =
513
563
  opts.slackUserId && cfg.botSlackUserIds.includes(opts.slackUserId)
@@ -516,14 +566,15 @@ const extension: SwarmExtension = (api) => {
516
566
  ? "human (known teammate)"
517
567
  : "unknown (not a known teammate; may be a bot or an external person)";
518
568
 
519
- const verdict = await askJev(
569
+ const { verdict, target } = await askFirstAvailable(
570
+ ctx.swarm,
520
571
  {
521
572
  messages,
522
573
  context: context.slice(-cfg.contextMessages).map((m) => m.slice(0, 500)),
523
574
  sender,
524
575
  leadNames: cfg.leadAliases,
525
576
  },
526
- transport,
577
+ targets,
527
578
  cfg,
528
579
  ctx.signal,
529
580
  );
@@ -542,7 +593,7 @@ const extension: SwarmExtension = (api) => {
542
593
  skipP: decision.skipP,
543
594
  lowConfidence: decision.lowConfidence,
544
595
  latencyMs: verdict.latencyMs,
545
- provider: transport.provider,
596
+ provider: target.provider,
546
597
  sender,
547
598
  preview,
548
599
  },
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "../manifest.schema.json",
3
3
  "name": "slack-reply-gate",
4
- "description": "Asks Jev (TypeSafe or OpenRouter) whether Lead should answer a buffered Slack thread follow-up, and drops the wake-up when it should not",
5
- "version": "1.2.0",
4
+ "description": "Asks a SystemOne decision model (Jev or Clef) whether Lead should answer a buffered Slack thread follow-up, and drops the wake-up when it should not",
5
+ "version": "1.3.0",
6
6
  "runtime": "api",
7
7
  "assets": { "hooks": "hooks.ts" }
8
8
  }