@desplega.ai/agent-swarm 2.7.4 → 2.7.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/README.md +1 -1
- package/dist/acp-adapter-g3ads55s.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
- package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.js} +7 -7
- package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
- package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-0wyynpn1.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
- package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
- package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
- package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
- package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
- package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
- package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
- package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
- package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
- package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
- package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
- package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
- package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
- package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
- package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
- package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
- package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
- package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
- package/dist/cli-fw2w0kva.js +47 -0
- package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
- package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
- package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
- package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
- package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
- package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
- package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
- package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
- package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
- package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
- package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
- package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
- package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
- package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
- package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
- package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
- package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
- package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
- package/dist/cli-zh3fp8v2.js +151 -0
- package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
- package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
- package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
- package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
- package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
- package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
- package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
- package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
- package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
- package/dist/fx-adapter-gt6pncj2.js +18 -0
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
- package/dist/handlers-vwmba1m2.js +31 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
- package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
- package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
- package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
- package/dist/index-5r71rf98.js +32 -0
- package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
- package/dist/index-anmwxs87.js +29 -0
- package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
- package/dist/index-spp0ra5c.js +36 -0
- package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
- package/dist/lead-fjy418cw.js +69 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
- package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
- package/dist/worker-0c5tyzqe.js +69 -0
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +97 -32
- package/package.json +1 -1
- package/src/be/db/session-summaries.ts +8 -2
- package/src/be/db/session-tree.ts +174 -61
- package/src/be/db/session-visibility.ts +5 -4
- package/src/be/db.ts +8 -4
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed/schedules-seeder.ts +135 -9
- package/src/be/seed/workflows-seeder.ts +65 -1
- package/src/be/seed-pricing.ts +23 -0
- package/src/be/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/provider-credentials.ts +29 -2
- package/src/commands/runner.ts +86 -5
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +27 -1
- package/src/extensions/dispatcher.ts +4 -0
- package/src/http/agents.ts +1 -0
- package/src/http/mcp-bridge.ts +4 -0
- package/src/integrations/chat-sdk/inbound.ts +15 -5
- package/src/integrations/chat-sdk/steering.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/prompts/base-prompt.ts +4 -1
- package/src/prompts/session-templates.ts +26 -0
- package/src/providers/acp-adapter.ts +190 -26
- package/src/providers/acp-targets.ts +306 -6
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +9 -44
- package/src/providers/codex-oauth/flow.ts +24 -3
- package/src/providers/codex-oauth/storage.ts +56 -17
- package/src/providers/codex-oauth/subscription.ts +67 -0
- package/src/providers/fx-adapter.ts +211 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/mcp-loopback-proxy.ts +94 -0
- package/src/providers/pi-mono-adapter.ts +98 -13
- package/src/providers/pricing-sources.md +8 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/scheduler/scheduler.ts +36 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +5 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/handlers.ts +18 -5
- package/src/slack/left-threads.ts +65 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- package/src/tests/acp-adapter.test.ts +79 -0
- package/src/tests/base-prompt.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +30 -0
- package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
- package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
- package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
- package/src/tests/codex-oauth-credential-info.test.ts +40 -0
- package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/explicit-model-validation.test.ts +99 -0
- package/src/tests/extensions-run-context.test.ts +11 -0
- package/src/tests/extensions-steering.test.ts +353 -0
- package/src/tests/extensions-validate.test.ts +5 -0
- package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
- package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
- package/src/tests/fixtures/fake-fx.ts +167 -0
- package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
- package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
- package/src/tests/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/mcp-loopback-proxy.test.ts +193 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -5
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-registration.test.ts +2 -1
- package/src/tests/query-audit-plans.test.ts +3 -2
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -0
- package/src/tests/session-cost-recompute-unit.test.ts +39 -0
- package/src/tests/session-costs-golden.test.ts +26 -0
- package/src/tests/session-summaries-plans.test.ts +120 -0
- package/src/tests/session-summaries.test.ts +72 -12
- package/src/tests/sessions-tree.test.ts +11 -3
- package/src/tests/slack-leave-thread.test.ts +431 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-engine-v2.test.ts +42 -0
- package/src/tests/workflow-script-principal.test.ts +207 -7
- package/src/tools/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +19 -0
- package/src/utils/codex-auth-failure.ts +30 -0
- package/src/utils/context-window.ts +4 -1
- package/src/utils/credentials.ts +7 -0
- package/src/utils/fx-models.ts +43 -0
- package/src/workflows/cooldown.ts +3 -2
- package/src/workflows/engine.ts +24 -9
- package/templates/schedules/dreaming-daily/config.json +1 -1
- package/templates/schedules/dreaming-daily/content.md +6 -14
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- package/dist/handlers-jg7v74ty.js +0 -31
- package/dist/index-8c1zbz59.js +0 -29
- package/dist/index-8k8tmfxd.js +0 -32
- package/dist/index-hq617xdj.js +0 -36
- package/dist/lead-567kw7x3.js +0 -68
- package/dist/worker-fjdqra6q.js +0 -68
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
+
CHATGPT_SUBSCRIPTION_PROVIDER,
|
|
17
18
|
claudeCatalogModelId,
|
|
18
19
|
cursorCatalogRef,
|
|
19
20
|
dshCatalogRef,
|
|
21
|
+
fxCatalogRef,
|
|
20
22
|
grokCatalogRef,
|
|
21
23
|
REASONING_EFFORT_LEVELS,
|
|
22
24
|
type ReasoningEffortLevel,
|
|
@@ -37,7 +39,8 @@ export type ReasoningHarness =
|
|
|
37
39
|
| "dsh"
|
|
38
40
|
| "cursor"
|
|
39
41
|
| "amp"
|
|
40
|
-
| "grok"
|
|
42
|
+
| "grok"
|
|
43
|
+
| "fx";
|
|
41
44
|
|
|
42
45
|
export interface ReasoningCapability {
|
|
43
46
|
supported: boolean;
|
|
@@ -65,6 +68,7 @@ export type ReasoningEffortApplication =
|
|
|
65
68
|
| { kind: "amp-effort"; reasoningEffort: ReasoningEffort }
|
|
66
69
|
| { kind: "cursor-effort"; reasoningEffort: ReasoningEffort }
|
|
67
70
|
| { kind: "grok-effort"; reasoningEffort: ReasoningEffort }
|
|
71
|
+
| { kind: "fx-effort"; reasoningEffort: ReasoningEffort }
|
|
68
72
|
| { kind: "noop" };
|
|
69
73
|
|
|
70
74
|
// --- Capability lookup --------------------------------------------------------
|
|
@@ -102,9 +106,16 @@ function lookupModel(
|
|
|
102
106
|
({ providerId, modelId } = cursorCatalogRef(model));
|
|
103
107
|
} else if (harness === "grok") {
|
|
104
108
|
({ providerId, modelId } = grokCatalogRef(model));
|
|
109
|
+
} else if (harness === "fx") {
|
|
110
|
+
// Only the AI Gateway route (`vercel/…`) has an effort control.
|
|
111
|
+
const ref = fxCatalogRef(model);
|
|
112
|
+
if (!ref) return undefined;
|
|
113
|
+
({ providerId, modelId } = ref);
|
|
105
114
|
} else {
|
|
106
115
|
({ providerId, modelId } = splitProviderModel(model));
|
|
107
116
|
if (!providerId) return undefined;
|
|
117
|
+
// A ChatGPT subscription id names an OpenAI catalog model.
|
|
118
|
+
if (providerId.toLowerCase() === CHATGPT_SUBSCRIPTION_PROVIDER) providerId = "openai";
|
|
108
119
|
}
|
|
109
120
|
const facts = runtimeCatalogModel(providerId, modelId);
|
|
110
121
|
return facts ? { id: modelId, facts } : undefined;
|
|
@@ -240,6 +251,10 @@ export function applyReasoningEffort(
|
|
|
240
251
|
// Grok's `--reasoning-effort` takes the normalized names as-is; see
|
|
241
252
|
// `src/providers/grok-adapter.ts`.
|
|
242
253
|
return { kind: "grok-effort", reasoningEffort: level };
|
|
254
|
+
case "fx":
|
|
255
|
+
// fx's `effort` config option takes the normalized names; the adapter
|
|
256
|
+
// sets it on the AI Gateway route only (see `fxTargetProfile`).
|
|
257
|
+
return { kind: "fx-effort", reasoningEffort: level };
|
|
243
258
|
default: {
|
|
244
259
|
const _exhaustive: never = harness;
|
|
245
260
|
return _exhaustive;
|
|
@@ -256,6 +271,7 @@ const REASONING_HARNESSES: ReadonlySet<string> = new Set<ReasoningHarness>([
|
|
|
256
271
|
"cursor",
|
|
257
272
|
"amp",
|
|
258
273
|
"grok",
|
|
274
|
+
"fx",
|
|
259
275
|
]);
|
|
260
276
|
|
|
261
277
|
/**
|
package/src/providers/types.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
getDbClient,
|
|
13
13
|
getDueScheduledTasks,
|
|
14
|
+
getLeadAgent,
|
|
14
15
|
getScheduledTaskById,
|
|
15
16
|
getUserById,
|
|
16
17
|
getWorkflow,
|
|
@@ -26,8 +27,9 @@ import {
|
|
|
26
27
|
import { buildScriptCredentialBindingsWithFailures } from "@/be/script-credential-broker";
|
|
27
28
|
import { getScript } from "@/be/scripts/db";
|
|
28
29
|
import { runScriptWithRunCredential } from "@/be/scripts/run-credential";
|
|
30
|
+
import { seededWorkflowState } from "@/be/seed/workflows-seeder";
|
|
29
31
|
import { telemetry } from "@/telemetry";
|
|
30
|
-
import type { AgentTask, ScheduledTask } from "@/types";
|
|
32
|
+
import type { AgentTask, ScheduledTask, Workflow } from "@/types";
|
|
31
33
|
import { getExecutorRegistry as getWorkflowExecutorRegistry } from "@/workflows";
|
|
32
34
|
import { startWorkflowExecution } from "@/workflows/engine";
|
|
33
35
|
import type { ExecutorRegistry } from "@/workflows/executors/registry";
|
|
@@ -252,8 +254,7 @@ export async function dispatchScheduleTarget(
|
|
|
252
254
|
};
|
|
253
255
|
const runId = await startWorkflowExecution(workflow, triggerData, registry, {
|
|
254
256
|
triggerType: "schedule",
|
|
255
|
-
|
|
256
|
-
triggeredByAgentId: schedule.createdByAgentId,
|
|
257
|
+
triggeredByAgentId: await scheduledWorkflowPrincipal(schedule, workflow),
|
|
257
258
|
requestedByUserId: schedule.createdBy,
|
|
258
259
|
});
|
|
259
260
|
await throwIfWorkflowNeedsSetup(schedule.id, [runId]);
|
|
@@ -295,6 +296,38 @@ export async function dispatchScheduleTarget(
|
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
/**
|
|
300
|
+
* The agent a scheduled workflow run acts as: the schedule's creator, as for
|
|
301
|
+
* script schedules (`targetAgentId` and trigger data are caller-chosen, so
|
|
302
|
+
* neither is ever the principal). A creator-less (seeded) schedule that fires
|
|
303
|
+
* a seeded workflow runs as the lead, resolved at fire time; with no lead the
|
|
304
|
+
* fire fails instead of running agentless. Operator settings such as inputs
|
|
305
|
+
* or on/off keep that. Any agent can edit a workflow, so one whose nodes or
|
|
306
|
+
* params changed never inherits the lead and its fire fails. Workflows the
|
|
307
|
+
* seeder never wrote keep their previous principal.
|
|
308
|
+
*/
|
|
309
|
+
async function scheduledWorkflowPrincipal(
|
|
310
|
+
schedule: ScheduledTask,
|
|
311
|
+
workflow: Workflow,
|
|
312
|
+
): Promise<string | undefined> {
|
|
313
|
+
if (schedule.createdByAgentId) return schedule.createdByAgentId;
|
|
314
|
+
if (workflow.createdByAgentId) return undefined;
|
|
315
|
+
const seeded = await seededWorkflowState(workflow);
|
|
316
|
+
if (seeded === "unseeded") return undefined;
|
|
317
|
+
if (seeded === "edited") {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Schedule "${schedule.name}" has no creator and the nodes or params of seeded workflow "${workflow.name}" were edited, so it will not run as the lead. Re-create the schedule as the agent it should run as.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const lead = await getLeadAgent();
|
|
323
|
+
if (!lead) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Schedule "${schedule.name}" has no creator and the swarm has no lead to run its workflow as`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return lead.id;
|
|
329
|
+
}
|
|
330
|
+
|
|
298
331
|
async function throwIfWorkflowNeedsSetup(scheduleId: string, runIds: string[]): Promise<void> {
|
|
299
332
|
for (const runId of runIds) {
|
|
300
333
|
const run = await getWorkflowRun(runId);
|
|
@@ -104,6 +104,7 @@ export const SDK_TOOL_NAME_MAP = {
|
|
|
104
104
|
slack_downloadFile: "slack-download-file",
|
|
105
105
|
slack_delete: "slack-delete", // external: mutates Slack, destructive
|
|
106
106
|
slack_update: "slack-update", // external: mutates Slack
|
|
107
|
+
slack_leaveThread: "slack-leave-thread",
|
|
107
108
|
|
|
108
109
|
// ── chat sdk ──
|
|
109
110
|
chat_reply: "chat-reply", // external: sends to the task's chat thread
|
|
@@ -493,6 +493,11 @@ declare module "swarm-sdk" {
|
|
|
493
493
|
slack_downloadFile(args: { url: string }): Promise<unknown>;
|
|
494
494
|
slack_delete(args: { channelId: string; messageTs: string }): Promise<unknown>;
|
|
495
495
|
slack_update(args: { channelId: string; messageTs: string; message: string }): Promise<unknown>;
|
|
496
|
+
slack_leaveThread(args: {
|
|
497
|
+
taskId?: string;
|
|
498
|
+
channelId?: string;
|
|
499
|
+
threadTs?: string;
|
|
500
|
+
}): Promise<unknown>;
|
|
496
501
|
|
|
497
502
|
// --- write: chat sdk ---
|
|
498
503
|
chat_reply(args: { text: string }): Promise<unknown>;
|
|
@@ -98,6 +98,7 @@ declare module "swarm-extension" {
|
|
|
98
98
|
| "amp"
|
|
99
99
|
| "cursor"
|
|
100
100
|
| "grok"
|
|
101
|
+
| "fx"
|
|
101
102
|
| undefined;
|
|
102
103
|
leadOnly?: boolean | undefined;
|
|
103
104
|
}
|
|
@@ -233,6 +234,7 @@ declare module "swarm-extension" {
|
|
|
233
234
|
| "amp"
|
|
234
235
|
| "cursor"
|
|
235
236
|
| "grok"
|
|
237
|
+
| "fx"
|
|
236
238
|
| undefined;
|
|
237
239
|
providerMeta?: Record<string, unknown> | undefined;
|
|
238
240
|
harnessVariant?: string | undefined;
|
|
@@ -255,6 +257,7 @@ declare module "swarm-extension" {
|
|
|
255
257
|
| "amp"
|
|
256
258
|
| "cursor"
|
|
257
259
|
| "grok"
|
|
260
|
+
| "fx"
|
|
258
261
|
| undefined;
|
|
259
262
|
leadOnly?: boolean | undefined;
|
|
260
263
|
}
|
|
@@ -276,6 +279,10 @@ declare module "swarm-extension" {
|
|
|
276
279
|
runtimeInstanceId?: string | null | undefined;
|
|
277
280
|
};
|
|
278
281
|
|
|
282
|
+
export type SteerMode = "steer" | "queue";
|
|
283
|
+
|
|
284
|
+
export type SteeringSource = "script" | "slack" | "mcp" | "api" | "ui" | "chat";
|
|
285
|
+
|
|
279
286
|
export type RequestInfo = {
|
|
280
287
|
sessionId: string | undefined;
|
|
281
288
|
agentId: string | undefined;
|
|
@@ -486,6 +493,27 @@ declare module "swarm-extension" {
|
|
|
486
493
|
leadAgentId: string;
|
|
487
494
|
summary: string;
|
|
488
495
|
}
|
|
496
|
+
export interface TaskSteerEvent {
|
|
497
|
+
task: AgentTask;
|
|
498
|
+
message: string;
|
|
499
|
+
mode: SteerMode;
|
|
500
|
+
source: SteeringSource;
|
|
501
|
+
sender: {
|
|
502
|
+
kind: "user" | "agent" | "system";
|
|
503
|
+
userId?: string;
|
|
504
|
+
agentId?: string;
|
|
505
|
+
};
|
|
506
|
+
messageTimestamps?: string[];
|
|
507
|
+
/** Slack ingress metadata; not persisted or writable through modify. */
|
|
508
|
+
slack?: {
|
|
509
|
+
channelId: string;
|
|
510
|
+
threadTs: string;
|
|
511
|
+
userId?: string;
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
export interface TaskSteerModify {
|
|
515
|
+
message: string;
|
|
516
|
+
}
|
|
489
517
|
export type TaskFollowUpModify = Partial<
|
|
490
518
|
Pick<CreateTaskOptions, "agentId" | "priority" | "followUpConfig">
|
|
491
519
|
> & {
|
|
@@ -718,6 +746,11 @@ declare module "swarm-extension" {
|
|
|
718
746
|
modify: TaskFollowUpModify;
|
|
719
747
|
result: PreResult<TaskFollowUpModify>;
|
|
720
748
|
};
|
|
749
|
+
"pre.task.steer": {
|
|
750
|
+
event: TaskSteerEvent;
|
|
751
|
+
modify: TaskSteerModify;
|
|
752
|
+
result: PreResult<TaskSteerModify>;
|
|
753
|
+
};
|
|
721
754
|
"pre.slack.route": {
|
|
722
755
|
event: SlackRouteEvent;
|
|
723
756
|
modify: SlackRouteModify;
|
|
@@ -475,6 +475,11 @@ declare module "swarm-sdk" {
|
|
|
475
475
|
slack_downloadFile(args: { url: string }): Promise<unknown>;
|
|
476
476
|
slack_delete(args: { channelId: string; messageTs: string }): Promise<unknown>;
|
|
477
477
|
slack_update(args: { channelId: string; messageTs: string; message: string }): Promise<unknown>;
|
|
478
|
+
slack_leaveThread(args: {
|
|
479
|
+
taskId?: string;
|
|
480
|
+
channelId?: string;
|
|
481
|
+
threadTs?: string;
|
|
482
|
+
}): Promise<unknown>;
|
|
478
483
|
|
|
479
484
|
// --- write: chat sdk ---
|
|
480
485
|
chat_reply(args: { text: string }): Promise<unknown>;
|
package/src/server.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
|
|
|
11
11
|
import { resolveTemplate } from "./prompts/resolver";
|
|
12
12
|
import { isRbacEnabled } from "./rbac";
|
|
13
13
|
import { getSlackConfiguration } from "./slack/config";
|
|
14
|
+
import { isSlackLeaveThreadDisabled } from "./slack/left-threads";
|
|
14
15
|
import { registerAcceptSteerTool } from "./tools/accept-steer";
|
|
15
16
|
import { registerAppActionRunTool } from "./tools/app-action-run";
|
|
16
17
|
import { registerAppDiffTool } from "./tools/app-diff";
|
|
@@ -162,6 +163,7 @@ import { registerSlackCreateChannelTool } from "./tools/slack-create-channel";
|
|
|
162
163
|
import { registerSlackDeleteTool } from "./tools/slack-delete";
|
|
163
164
|
import { registerSlackDownloadFileTool } from "./tools/slack-download-file";
|
|
164
165
|
import { registerSlackInviteToChannelTool } from "./tools/slack-invite-to-channel";
|
|
166
|
+
import { registerSlackLeaveThreadTool } from "./tools/slack-leave-thread";
|
|
165
167
|
import { registerSlackListChannelsTool } from "./tools/slack-list-channels";
|
|
166
168
|
import { registerSlackPostTool } from "./tools/slack-post";
|
|
167
169
|
import { registerSlackReadTool } from "./tools/slack-read";
|
|
@@ -693,6 +695,11 @@ export async function createServer(
|
|
|
693
695
|
registerSlackDownloadFileTool(server);
|
|
694
696
|
registerSlackDeleteTool(server);
|
|
695
697
|
registerSlackUpdateTool(server);
|
|
698
|
+
// The scripts bridge builds its full-surface server once and caches it, so
|
|
699
|
+
// it always registers the tool; the handler refuses while the flag is set.
|
|
700
|
+
if (opts.fullSurface === true || !isSlackLeaveThreadDisabled()) {
|
|
701
|
+
registerSlackLeaveThreadTool(server);
|
|
702
|
+
}
|
|
696
703
|
}
|
|
697
704
|
|
|
698
705
|
// End of default-enabled capabilities
|
package/src/slack/assistant.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
fetchSlackFiles,
|
|
21
21
|
notifySlackFileFailures,
|
|
22
22
|
} from "./inbound-files";
|
|
23
|
+
import { shouldSkipLeftSlackThread } from "./left-threads";
|
|
23
24
|
import {
|
|
24
25
|
collectSlackMessageFiles,
|
|
25
26
|
type SlackMessageLike,
|
|
@@ -194,11 +195,26 @@ export function createAssistant(
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
const botMentioned = !!cachedBotUserId && rawText.includes(`<@${cachedBotUserId}>`);
|
|
199
|
+
// A thread the swarm left (slack-leave-thread) stays silent until someone
|
|
200
|
+
// @mentions the bot.
|
|
201
|
+
if (
|
|
202
|
+
await shouldSkipLeftSlackThread(
|
|
203
|
+
channelId,
|
|
204
|
+
msg.thread_ts as string | undefined,
|
|
205
|
+
botMentioned,
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
console.log(
|
|
209
|
+
`[Slack] assistant: skipping message in left thread ${channelId}/${threadTs}`,
|
|
210
|
+
);
|
|
211
|
+
return ignoreSlackInbound("thread_left");
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
// If the message @-mentions someone OTHER than our bot and does NOT mention
|
|
198
215
|
// our bot, it is addressed to a different agent/user — do not spawn a task
|
|
199
216
|
// (unless SLACK_THREAD_WAKE_ON_ANY_MESSAGE=true).
|
|
200
217
|
if (cachedBotUserId) {
|
|
201
|
-
const botMentioned = rawText.includes(`<@${cachedBotUserId}>`);
|
|
202
218
|
if (!botMentioned && isAddressedToOtherUser(rawText, cachedBotUserId)) {
|
|
203
219
|
console.log(
|
|
204
220
|
`[Slack] assistant: skipping message in ${channelId}/${threadTs} — mentions another user, not us`,
|
package/src/slack/handlers.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getTasksByAgentId,
|
|
11
11
|
isExtensionAgent,
|
|
12
12
|
} from "../be/db";
|
|
13
|
+
import { SteeringBlockedError } from "../be/steering";
|
|
13
14
|
import { dispatchPre } from "../extensions/dispatcher";
|
|
14
15
|
import { resolveTemplate } from "../prompts/resolver";
|
|
15
16
|
import { slackContextKey } from "../tasks/context-key";
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
leaveExternalChannel,
|
|
52
53
|
shouldBlockChannel,
|
|
53
54
|
} from "./external-channel";
|
|
55
|
+
import { shouldSkipLeftSlackThread } from "./left-threads";
|
|
54
56
|
import {
|
|
55
57
|
collectSlackMessageFiles,
|
|
56
58
|
extractSlackMessageText,
|
|
@@ -486,6 +488,17 @@ export function registerMessageHandler(app: App): void {
|
|
|
486
488
|
return ignoreSlackInbound("unauthorized_user");
|
|
487
489
|
}
|
|
488
490
|
|
|
491
|
+
// Check if bot was mentioned (in original text only)
|
|
492
|
+
const botMentioned = !!cachedBotUserId && !!msg.text?.includes(`<@${cachedBotUserId}>`);
|
|
493
|
+
|
|
494
|
+
// A thread the swarm left (slack-leave-thread) stays silent until someone
|
|
495
|
+
// @mentions the bot. Checked before the workflow event, buffering, routing,
|
|
496
|
+
// or any AI call.
|
|
497
|
+
if (await shouldSkipLeftSlackThread(msg.channel, msg.thread_ts, botMentioned)) {
|
|
498
|
+
console.log(`[Slack] Skipping message in left thread ${msg.channel}/${msg.thread_ts}`);
|
|
499
|
+
return ignoreSlackInbound("thread_left");
|
|
500
|
+
}
|
|
501
|
+
|
|
489
502
|
// Resolve canonical user identity via the three-step cascade:
|
|
490
503
|
// fast-path alias lookup → enrich+auto-link by email → unmapped tracker.
|
|
491
504
|
// Returns undefined when no email is recoverable; task creation proceeds
|
|
@@ -519,9 +532,6 @@ export function registerMessageHandler(app: App): void {
|
|
|
519
532
|
}
|
|
520
533
|
const botUserId = cachedBotUserId;
|
|
521
534
|
|
|
522
|
-
// Check if bot was mentioned (in original text only)
|
|
523
|
-
const botMentioned = !!msg.text?.includes(`<@${botUserId}>`);
|
|
524
|
-
|
|
525
535
|
// Detect assistant thread context. Bolt's Assistant middleware claims DM thread
|
|
526
536
|
// messages with no subtype or `file_share` (`isAssistantMessage` in @slack/bolt), so
|
|
527
537
|
// those — file uploads included — are handled in assistant.ts. An assistant-thread
|
|
@@ -862,6 +872,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
862
872
|
message: taskDescription,
|
|
863
873
|
messageTimestamps: [msg.ts],
|
|
864
874
|
requestedByUserId,
|
|
875
|
+
slackUserId: msg.user,
|
|
865
876
|
})
|
|
866
877
|
: null;
|
|
867
878
|
if (steering) {
|
|
@@ -937,12 +948,14 @@ export function registerMessageHandler(app: App): void {
|
|
|
937
948
|
} catch (error) {
|
|
938
949
|
// A policy block is an answered outcome; anything else is a failure the
|
|
939
950
|
// user sees as "could not assign" and the dispatcher must not call success.
|
|
940
|
-
|
|
951
|
+
const blocked =
|
|
952
|
+
error instanceof TaskCreationBlockedError || error instanceof SteeringBlockedError;
|
|
953
|
+
if (!blocked) {
|
|
941
954
|
reportSlackInboundFailure("task_creation_error");
|
|
942
955
|
}
|
|
943
956
|
results.failed.push({
|
|
944
957
|
agentName: agent.name,
|
|
945
|
-
reason:
|
|
958
|
+
reason: blocked ? error.reason : "error",
|
|
946
959
|
});
|
|
947
960
|
}
|
|
948
961
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { deleteKv, getKv, upsertKv } from "../be/db";
|
|
2
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Slack threads the swarm left via `slack-leave-thread`.
|
|
6
|
+
*
|
|
7
|
+
* A left thread drops every inbound message that does not @mention the bot,
|
|
8
|
+
* in code, before buffering, routing, or any AI call. A mention clears the
|
|
9
|
+
* state and the message is handled as usual. Stored in kv_entries so a
|
|
10
|
+
* restart keeps it.
|
|
11
|
+
*/
|
|
12
|
+
export const SLACK_LEFT_THREAD_KV_NAMESPACE = "slack-left-thread";
|
|
13
|
+
|
|
14
|
+
/** Operator flag that turns the `slack-leave-thread` tool off. */
|
|
15
|
+
export const SLACK_LEAVE_THREAD_DISABLE = "SLACK_LEAVE_THREAD_DISABLE";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read at call time, not cached: a global swarm_config row is hydrated into
|
|
19
|
+
* process.env on every config reload, so a flip applies without a restart.
|
|
20
|
+
*/
|
|
21
|
+
export function isSlackLeaveThreadDisabled(): boolean {
|
|
22
|
+
return isEnvFlagEnabled(SLACK_LEAVE_THREAD_DISABLE, false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function threadKey(channelId: string, threadTs: string): string {
|
|
26
|
+
return `${channelId}:${threadTs}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function leaveSlackThread(
|
|
30
|
+
channelId: string,
|
|
31
|
+
threadTs: string,
|
|
32
|
+
agentId: string,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
await upsertKv({
|
|
35
|
+
namespace: SLACK_LEFT_THREAD_KV_NAMESPACE,
|
|
36
|
+
key: threadKey(channelId, threadTs),
|
|
37
|
+
value: { agentId, leftAt: new Date().toISOString() },
|
|
38
|
+
valueType: "json",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function isSlackThreadLeft(channelId: string, threadTs: string): Promise<boolean> {
|
|
43
|
+
return (await getKv(SLACK_LEFT_THREAD_KV_NAMESPACE, threadKey(channelId, threadTs))) !== null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Inbound gate for left threads. Returns true when the message must be dropped:
|
|
48
|
+
* the thread is left and the message does not @mention the bot. A mention
|
|
49
|
+
* clears the left state and returns false.
|
|
50
|
+
*/
|
|
51
|
+
export async function shouldSkipLeftSlackThread(
|
|
52
|
+
channelId: string,
|
|
53
|
+
threadTs: string | undefined,
|
|
54
|
+
botMentioned: boolean,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
if (!threadTs) return false;
|
|
57
|
+
const key = threadKey(channelId, threadTs);
|
|
58
|
+
if (botMentioned) {
|
|
59
|
+
if (await deleteKv(SLACK_LEFT_THREAD_KV_NAMESPACE, key)) {
|
|
60
|
+
console.log(`[Slack] Rejoined left thread ${key}: bot mentioned`);
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return isSlackThreadLeft(channelId, threadTs);
|
|
65
|
+
}
|
package/src/slack/steering.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface SlackThreadSteeringRequest {
|
|
|
9
9
|
message: string;
|
|
10
10
|
messageTimestamps?: string[];
|
|
11
11
|
requestedByUserId?: string;
|
|
12
|
+
slackUserId?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface SlackThreadSteeringResult {
|
|
@@ -52,6 +53,8 @@ export async function requestSlackThreadSteering(
|
|
|
52
53
|
source: "slack",
|
|
53
54
|
createdByKind: "user",
|
|
54
55
|
createdByUserId: args.requestedByUserId,
|
|
56
|
+
messageTimestamps: args.messageTimestamps,
|
|
57
|
+
slack: { channelId: args.channelId, threadTs: args.threadTs, userId: args.slackUserId },
|
|
55
58
|
});
|
|
56
59
|
for (const messageTs of args.messageTimestamps ?? []) {
|
|
57
60
|
await createLogEntry({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getLeadAgent,
|
|
5
5
|
getMostRecentTaskInThread,
|
|
6
6
|
} from "../be/db";
|
|
7
|
+
import { SteeringBlockedError } from "../be/steering";
|
|
7
8
|
import { findUserByExternalId } from "../be/users";
|
|
8
9
|
import { createAdditiveBuffer } from "../tasks/additive-buffer";
|
|
9
10
|
import { slackContextKey } from "../tasks/context-key";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
fetchSlackFiles,
|
|
22
23
|
notifySlackFileFailures,
|
|
23
24
|
} from "./inbound-files";
|
|
25
|
+
import { isSlackThreadLeft } from "./left-threads";
|
|
24
26
|
import { extractSlackMessageText } from "./message-text";
|
|
25
27
|
import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
|
|
26
28
|
import { getAgentDisplayName, getAgentSlackIcon } from "./responses";
|
|
@@ -164,6 +166,12 @@ async function slackFlush(
|
|
|
164
166
|
return;
|
|
165
167
|
}
|
|
166
168
|
const { channelId, threadTs } = split;
|
|
169
|
+
// The thread was left (slack-leave-thread) while these replies waited. Only
|
|
170
|
+
// non-mentions are buffered, so none of them rejoins it.
|
|
171
|
+
if (await isSlackThreadLeft(channelId, threadTs)) {
|
|
172
|
+
console.log(`[Slack] Buffer flush: dropping ${items.length} message(s) in left thread ${key}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
167
175
|
// Buffer is guaranteed to have at least one item — the first carries the
|
|
168
176
|
// original requester's userId (same semantics as the pre-refactor version).
|
|
169
177
|
const originalRequesterId = items[0]!.userId;
|
|
@@ -226,22 +234,38 @@ async function slackFlush(
|
|
|
226
234
|
|
|
227
235
|
// Steering carries text only. As on the mention path, files need a follow-up
|
|
228
236
|
// task so its attachments exist before a worker can claim it.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
let steering: Awaited<ReturnType<typeof requestSlackThreadSteering>>;
|
|
238
|
+
try {
|
|
239
|
+
steering =
|
|
240
|
+
files.length === 0
|
|
241
|
+
? await requestSlackThreadSteering({
|
|
242
|
+
channelId,
|
|
243
|
+
threadTs,
|
|
244
|
+
message:
|
|
245
|
+
nextSenderIndex === -1
|
|
246
|
+
? combinedText
|
|
247
|
+
: await rewriteSlackMentions(
|
|
248
|
+
steeringItems.map((item) => item.text).join("\n---\n"),
|
|
249
|
+
botUserId,
|
|
250
|
+
),
|
|
251
|
+
messageTimestamps: steeringItems.map((item) => item.ts),
|
|
252
|
+
requestedByUserId,
|
|
253
|
+
slackUserId: originalRequesterId,
|
|
254
|
+
})
|
|
255
|
+
: null;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (!(error instanceof SteeringBlockedError)) throw error;
|
|
258
|
+
console.info("[Slack] pre.task.steer blocked:", error.extension.name, error.reason);
|
|
259
|
+
if (app) {
|
|
260
|
+
for (const item of steeringItems) {
|
|
261
|
+
await ackSlackMessage(app.client, channelId, item.ts, "no_entry");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (nextSenderIndex !== -1) {
|
|
265
|
+
await slackFlush(items.slice(nextSenderIndex), key, immediate);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
245
269
|
if (steering) {
|
|
246
270
|
console.log(
|
|
247
271
|
`[Slack] Buffer flushed → steering ${steering.result.outcome} for task ${steering.task.id}`,
|
|
@@ -1179,6 +1179,85 @@ ${serveFakeAcpAgent("FakeCopilot")}
|
|
|
1179
1179
|
expect(toAcpMcpServers(null)).toEqual([]);
|
|
1180
1180
|
});
|
|
1181
1181
|
|
|
1182
|
+
test("a target without the opt-in hooks keeps its session/new payload, MCP URL and no steering", async () => {
|
|
1183
|
+
const cwd = makeTempDir();
|
|
1184
|
+
const agentPath = join(cwd, "fake-acp-hooks-off.ts");
|
|
1185
|
+
const captureFile = join(cwd, "session-new.json");
|
|
1186
|
+
await Bun.write(
|
|
1187
|
+
agentPath,
|
|
1188
|
+
`
|
|
1189
|
+
import { Readable, Writable } from "node:stream";
|
|
1190
|
+
import { PROTOCOL_VERSION, agent, methods, ndJsonStream } from "${ACP_SDK_PATH}";
|
|
1191
|
+
class FakeAgent {
|
|
1192
|
+
constructor(connection) { this.connection = connection; }
|
|
1193
|
+
async initialize() {
|
|
1194
|
+
// Advertises every capability the opt-in hooks key off; a target without
|
|
1195
|
+
// the hooks must ignore them.
|
|
1196
|
+
return {
|
|
1197
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1198
|
+
agentCapabilities: {
|
|
1199
|
+
loadSession: true,
|
|
1200
|
+
mcpCapabilities: { http: true, sse: false },
|
|
1201
|
+
sessionCapabilities: { resume: {}, systemPrompt: {} },
|
|
1202
|
+
_meta: { fx: { steering: true } },
|
|
1203
|
+
},
|
|
1204
|
+
agentInfo: { name: "fake", version: "9.9.9" },
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async newSession(params) {
|
|
1208
|
+
await Bun.write(${JSON.stringify(captureFile)}, JSON.stringify(params));
|
|
1209
|
+
return { sessionId: "hooks-off-1" };
|
|
1210
|
+
}
|
|
1211
|
+
async prompt() { return { stopReason: "end_turn" }; }
|
|
1212
|
+
async cancel() {}
|
|
1213
|
+
}
|
|
1214
|
+
${serveFakeAcpAgent("FakeAgent")}
|
|
1215
|
+
`,
|
|
1216
|
+
);
|
|
1217
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
1218
|
+
"hooks-off-token",
|
|
1219
|
+
"aseph_hooksofftoken1234567890",
|
|
1220
|
+
);
|
|
1221
|
+
// A reachable, plain-HTTP, non-loopback URL: only requiresSecureMcpUrl rewrites it.
|
|
1222
|
+
const publicStyleUrl = apiUrl.replace("127.0.0.1", "0.0.0.0");
|
|
1223
|
+
try {
|
|
1224
|
+
const adapter = new ACPAdapter();
|
|
1225
|
+
const session = await adapter.createSession(
|
|
1226
|
+
baseConfig({
|
|
1227
|
+
cwd,
|
|
1228
|
+
apiUrl: publicStyleUrl,
|
|
1229
|
+
resumeSessionId: "ignored-without-a-resume-hook",
|
|
1230
|
+
env: {
|
|
1231
|
+
PATH: process.env.PATH ?? "",
|
|
1232
|
+
HOME: process.env.HOME ?? "",
|
|
1233
|
+
ACP_TARGET_COMMAND: "bun",
|
|
1234
|
+
ACP_TARGET_ARGS: JSON.stringify([agentPath]),
|
|
1235
|
+
},
|
|
1236
|
+
}),
|
|
1237
|
+
);
|
|
1238
|
+
const events: ProviderEvent[] = [];
|
|
1239
|
+
session.onEvent((event) => events.push(event));
|
|
1240
|
+
await session.waitForCompletion();
|
|
1241
|
+
|
|
1242
|
+
expect(session.sessionId).toBe("hooks-off-1");
|
|
1243
|
+
expect(session.deliverSteering).toBeUndefined();
|
|
1244
|
+
expect(await adapter.canResume("hooks-off-1")).toBe(false);
|
|
1245
|
+
const params = JSON.parse(await Bun.file(captureFile).text());
|
|
1246
|
+
expect(Object.keys(params).sort()).toEqual(["cwd", "mcpServers"]);
|
|
1247
|
+
expect(params.mcpServers.map((server: { url: string }) => server.url)).toEqual([
|
|
1248
|
+
`${publicStyleUrl}/mcp`,
|
|
1249
|
+
]);
|
|
1250
|
+
// Every ACP target records the agent's own version on session_init.
|
|
1251
|
+
expect(events.find((event) => event.type === "session_init")).toMatchObject({
|
|
1252
|
+
sessionId: "hooks-off-1",
|
|
1253
|
+
provider: "acp",
|
|
1254
|
+
harnessVariantMeta: { version: "9.9.9" },
|
|
1255
|
+
});
|
|
1256
|
+
} finally {
|
|
1257
|
+
await closeTokenStub();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1182
1261
|
test("completes before the revoke response and logs a failed revocation", async () => {
|
|
1183
1262
|
const cwd = makeTempDir();
|
|
1184
1263
|
const agentPath = join(cwd, "fake-acp-ephem-agent.ts");
|
|
@@ -1290,6 +1290,18 @@ describe("getBasePrompt: harness tool discovery", () => {
|
|
|
1290
1290
|
expect(result).toContain('import { store_progress } from "agent-swarm"');
|
|
1291
1291
|
});
|
|
1292
1292
|
|
|
1293
|
+
test("fx gets capability_search guidance with mcp_swarm_ tool names", async () => {
|
|
1294
|
+
const result = await getBasePrompt({
|
|
1295
|
+
...minimalArgs,
|
|
1296
|
+
provider: "fx",
|
|
1297
|
+
traits: { ...localTraits, hasToolSearch: false },
|
|
1298
|
+
});
|
|
1299
|
+
expect(result).not.toContain(deferred);
|
|
1300
|
+
expect(result).not.toContain("already in your tool list");
|
|
1301
|
+
expect(result).toContain("`capability_search`");
|
|
1302
|
+
expect(result).toContain("`store-progress` is `mcp_swarm_store-progress`");
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1293
1305
|
test("codex gets exec guidance with the normalized tool name", async () => {
|
|
1294
1306
|
const result = await getBasePrompt({
|
|
1295
1307
|
...minimalArgs,
|