@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.
- package/dist/{acp-adapter-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
- package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
- package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-m7q0a085.js} +2 -2
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
- package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
- package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
- package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
- package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
- package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
- package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
- package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
- package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
- package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
- package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
- package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
- package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
- package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
- package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
- package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
- package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
- package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
- package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
- package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
- package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
- package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
- package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
- package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
- package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
- package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
- package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
- package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
- package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
- package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
- package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
- package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
- package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
- package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
- package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
- package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
- package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
- package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
- package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
- package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
- package/dist/handlers-7gw64dxw.js +39 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
- package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
- package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
- package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
- package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
- package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
- package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
- package/dist/index-tfb26zhy.js +41 -0
- package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
- package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
- package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
- package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
- package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
- package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
- package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
- package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
- package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
- package/openapi.json +9 -2
- package/package.json +7 -7
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/be/chunking.ts +42 -9
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +14 -1
- package/src/be/db.ts +5 -1
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/swarm-config-guard.ts +24 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +7 -0
- package/src/fs/provider.ts +13 -1
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/fs.ts +54 -34
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/users.ts +12 -1
- package/src/integrations/chat-sdk/adapters.ts +7 -3
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +120 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +234 -4
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.ts +4 -4
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +558 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/swarm-config-export-to-env.test.ts +22 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/types.ts +6 -0
- package/src/utils/config-env-export.ts +3 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/transcription.ts +246 -0
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/dist/handlers-0pvee9nw.js +0 -39
- 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
|
-
/**
|
|
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
|
|
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
|
-
| {
|
|
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
|
|
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
|
|
19
|
-
| `enforce` | Asks
|
|
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
|
|
30
|
-
2. **@mention pass-through.** A new message that @mentions Lead passes without a
|
|
31
|
-
3. Otherwise one
|
|
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
|
-
##
|
|
36
|
+
## Provider
|
|
37
37
|
|
|
38
|
-
|
|
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` |
|
|
41
|
-
|
|
42
|
-
| `typesafe` |
|
|
43
|
-
| `openrouter` |
|
|
44
|
-
| `
|
|
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
|
-
|
|
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
|
|
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` |
|
|
67
|
-
| `provider` | `auto` | `auto`, `typesafe`, or `
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
10
|
-
* Lead should answer, and drops the wake-up when the
|
|
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
|
|
19
|
-
* - Otherwise one
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
52
|
-
|
|
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
|
|
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
|
|
183
|
-
*
|
|
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
|
|
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 =
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
*
|
|
208
|
-
*
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
};
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
335
|
-
*
|
|
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
|
|
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
|
-
|
|
357
|
+
target: DecisionTarget,
|
|
345
358
|
cfg: Cfg,
|
|
346
359
|
outer?: AbortSignal,
|
|
347
|
-
): Promise<
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
388
|
+
},
|
|
389
|
+
timeoutMs: Math.max(budgetMs, MIN_DECIDE_TIMEOUT_MS),
|
|
390
|
+
maxRetries: 0,
|
|
379
391
|
});
|
|
380
|
-
|
|
381
|
-
|
|
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:
|
|
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
|
|
461
|
+
/** Map a verdict to the typed decision. An unsure skip verdict replies. */
|
|
412
462
|
export const decide = (
|
|
413
|
-
verdict:
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
5
|
-
"version": "1.
|
|
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
|
}
|