@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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
isTranscriptionConfigured,
|
|
4
|
+
NOT_TRANSCRIBED_NOTE,
|
|
5
|
+
resolveTranscriptionConfig,
|
|
6
|
+
TRANSCRIPTION_ENV_KEYS,
|
|
7
|
+
TranscriptionError,
|
|
8
|
+
transcribeAudio,
|
|
9
|
+
transcribeAudioNotes,
|
|
10
|
+
} from "../utils/transcription";
|
|
11
|
+
|
|
12
|
+
// Includes the provider keys the fallback reads: a worker shell often has
|
|
13
|
+
// OPENAI_API_KEY or ELEVENLABS_API_KEY set, which would leak into every test.
|
|
14
|
+
const ENV_KEYS = TRANSCRIPTION_ENV_KEYS;
|
|
15
|
+
const savedEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
16
|
+
|
|
17
|
+
type Captured = {
|
|
18
|
+
path: string;
|
|
19
|
+
authorization: string | null;
|
|
20
|
+
xiApiKey: string | null;
|
|
21
|
+
fields: string[];
|
|
22
|
+
model: FormDataEntryValue | null;
|
|
23
|
+
modelId: FormDataEntryValue | null;
|
|
24
|
+
filename: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const captured: Captured[] = [];
|
|
28
|
+
// The file part's Content-Type as sent. Bun's `formData()` re-derives a File's
|
|
29
|
+
// type from its extension (`.webm` reads back as video/webm), so read it raw.
|
|
30
|
+
const sentFileTypes: string[] = [];
|
|
31
|
+
let reply: () => Response = () => Response.json({ text: " hello " });
|
|
32
|
+
let stt: ReturnType<typeof Bun.serve>;
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
36
|
+
stt = Bun.serve({
|
|
37
|
+
port: 0,
|
|
38
|
+
hostname: "127.0.0.1",
|
|
39
|
+
async fetch(req) {
|
|
40
|
+
const raw = await req.clone().text();
|
|
41
|
+
sentFileTypes.push(
|
|
42
|
+
/name="file"; filename="[^"]*"\r\nContent-Type: ([^\r\n]+)/.exec(raw)?.[1] ?? "",
|
|
43
|
+
);
|
|
44
|
+
const form = await req.formData();
|
|
45
|
+
captured.push({
|
|
46
|
+
path: new URL(req.url).pathname,
|
|
47
|
+
authorization: req.headers.get("authorization"),
|
|
48
|
+
xiApiKey: req.headers.get("xi-api-key"),
|
|
49
|
+
fields: [...form.keys()].sort(),
|
|
50
|
+
model: form.get("model"),
|
|
51
|
+
modelId: form.get("model_id"),
|
|
52
|
+
filename: (form.get("file") as File).name,
|
|
53
|
+
});
|
|
54
|
+
return reply();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterAll(() => {
|
|
60
|
+
stt.stop(true);
|
|
61
|
+
for (const [key, value] of savedEnv) {
|
|
62
|
+
if (value === undefined) delete process.env[key];
|
|
63
|
+
else process.env[key] = value;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
captured.length = 0;
|
|
69
|
+
sentFileTypes.length = 0;
|
|
70
|
+
reply = () => Response.json({ text: " hello " });
|
|
71
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function configure(provider?: string) {
|
|
75
|
+
process.env.TRANSCRIPTION_API_KEY = "stt-test-key-123456";
|
|
76
|
+
process.env.TRANSCRIPTION_BASE_URL = `http://127.0.0.1:${stt.port}/v1`;
|
|
77
|
+
if (provider) process.env.TRANSCRIPTION_PROVIDER = provider;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const audio = {
|
|
81
|
+
data: new Blob([new Uint8Array([1, 2, 3])]),
|
|
82
|
+
filename: "note.ogg",
|
|
83
|
+
mimeType: "audio/ogg",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe("transcribeAudio request shape", () => {
|
|
87
|
+
test("elevenlabs posts to /speech-to-text with xi-api-key and model_id", async () => {
|
|
88
|
+
configure("elevenlabs");
|
|
89
|
+
expect(await transcribeAudio(audio)).toBe("hello");
|
|
90
|
+
expect(captured).toEqual([
|
|
91
|
+
{
|
|
92
|
+
path: "/v1/speech-to-text",
|
|
93
|
+
authorization: null,
|
|
94
|
+
xiApiKey: "stt-test-key-123456",
|
|
95
|
+
fields: ["file", "model_id"],
|
|
96
|
+
model: null,
|
|
97
|
+
modelId: "scribe_v2",
|
|
98
|
+
filename: "note.ogg",
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("elevenlabs honours TRANSCRIPTION_MODEL in model_id", async () => {
|
|
104
|
+
configure("ElevenLabs");
|
|
105
|
+
process.env.TRANSCRIPTION_MODEL = "scribe_v1";
|
|
106
|
+
await transcribeAudio(audio);
|
|
107
|
+
expect(captured[0]).toMatchObject({ fields: ["file", "model_id"], modelId: "scribe_v1" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("negative control: the default provider keeps the OpenAI shape", async () => {
|
|
111
|
+
configure();
|
|
112
|
+
expect(await transcribeAudio(audio)).toBe("hello");
|
|
113
|
+
expect(captured).toEqual([
|
|
114
|
+
{
|
|
115
|
+
path: "/v1/audio/transcriptions",
|
|
116
|
+
authorization: "Bearer stt-test-key-123456",
|
|
117
|
+
xiApiKey: null,
|
|
118
|
+
fields: ["file", "model"],
|
|
119
|
+
model: "whisper-1",
|
|
120
|
+
modelId: null,
|
|
121
|
+
filename: "note.ogg",
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("an unknown provider fails before any request", async () => {
|
|
127
|
+
configure("deepgram");
|
|
128
|
+
await expect(transcribeAudio(audio)).rejects.toThrow(
|
|
129
|
+
new TranscriptionError(
|
|
130
|
+
'TRANSCRIPTION_PROVIDER "deepgram" is not supported (use openai, groq, elevenlabs)',
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
expect(captured).toHaveLength(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("an elevenlabs error detail is surfaced with the key scrubbed", async () => {
|
|
137
|
+
configure("elevenlabs");
|
|
138
|
+
reply = () =>
|
|
139
|
+
Response.json(
|
|
140
|
+
{ detail: { status: "invalid_api_key", message: "Invalid key stt-test-key-123456" } },
|
|
141
|
+
{ status: 401 },
|
|
142
|
+
);
|
|
143
|
+
await expect(transcribeAudio(audio)).rejects.toThrow(
|
|
144
|
+
"Invalid key [REDACTED:TRANSCRIPTION_API_KEY]",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("provider key fallback", () => {
|
|
150
|
+
// Built at runtime so the secret scanner does not read them as keys.
|
|
151
|
+
const fake = (provider: string) => [provider, "fixture", "value"].join("-");
|
|
152
|
+
const ELEVEN_FAKE = fake("eleven");
|
|
153
|
+
const OPENAI_FAKE = fake("openai");
|
|
154
|
+
const GROQ_FAKE = fake("groq");
|
|
155
|
+
|
|
156
|
+
// Auto-detected keys always go to the provider's public endpoint, so record
|
|
157
|
+
// that URL and forward the request to the local stub on the same path.
|
|
158
|
+
const realFetch = globalThis.fetch;
|
|
159
|
+
const calledUrls: string[] = [];
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
calledUrls.length = 0;
|
|
162
|
+
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
163
|
+
const url = new URL(String(input));
|
|
164
|
+
calledUrls.push(url.href);
|
|
165
|
+
return realFetch(`http://127.0.0.1:${stt.port}${url.pathname}`, init);
|
|
166
|
+
}) as typeof fetch;
|
|
167
|
+
});
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
globalThis.fetch = realFetch;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("no key at all means not configured", async () => {
|
|
173
|
+
expect(resolveTranscriptionConfig()).toBeNull();
|
|
174
|
+
expect(isTranscriptionConfigured()).toBe(false);
|
|
175
|
+
const notes = await transcribeAudioNotes([{ key: "a", name: "a.ogg", data: audio.data }], "t");
|
|
176
|
+
expect(notes.get("a")).toBe(NOT_TRANSCRIBED_NOTE);
|
|
177
|
+
expect(NOT_TRANSCRIBED_NOTE).toBe(
|
|
178
|
+
"(not transcribed: no transcription provider configured, set TRANSCRIPTION_API_KEY or one of ELEVENLABS_API_KEY, OPENAI_API_KEY, GROQ_API_KEY)",
|
|
179
|
+
);
|
|
180
|
+
expect(calledUrls).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("ELEVENLABS_API_KEY alone uses the elevenlabs shape", async () => {
|
|
184
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
185
|
+
expect(resolveTranscriptionConfig()).toMatchObject({
|
|
186
|
+
provider: "elevenlabs",
|
|
187
|
+
keyEnv: "ELEVENLABS_API_KEY",
|
|
188
|
+
source: "auto",
|
|
189
|
+
});
|
|
190
|
+
expect(await transcribeAudio(audio)).toBe("hello");
|
|
191
|
+
expect(calledUrls).toEqual(["https://api.elevenlabs.io/v1/speech-to-text"]);
|
|
192
|
+
expect(captured[0]).toMatchObject({
|
|
193
|
+
xiApiKey: ELEVEN_FAKE,
|
|
194
|
+
authorization: null,
|
|
195
|
+
modelId: "scribe_v2",
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("OPENAI_API_KEY alone uses the openai shape", async () => {
|
|
200
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
201
|
+
expect(resolveTranscriptionConfig()).toMatchObject({ provider: "openai", source: "auto" });
|
|
202
|
+
await transcribeAudio(audio);
|
|
203
|
+
expect(calledUrls).toEqual(["https://api.openai.com/v1/audio/transcriptions"]);
|
|
204
|
+
expect(captured[0]).toMatchObject({
|
|
205
|
+
authorization: `Bearer ${OPENAI_FAKE}`,
|
|
206
|
+
model: "whisper-1",
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("GROQ_API_KEY alone uses Groq's OpenAI-compatible endpoint and whisper model", async () => {
|
|
211
|
+
process.env.GROQ_API_KEY = GROQ_FAKE;
|
|
212
|
+
expect(resolveTranscriptionConfig()).toMatchObject({
|
|
213
|
+
provider: "groq",
|
|
214
|
+
keyEnv: "GROQ_API_KEY",
|
|
215
|
+
});
|
|
216
|
+
await transcribeAudio(audio);
|
|
217
|
+
expect(calledUrls).toEqual(["https://api.groq.com/openai/v1/audio/transcriptions"]);
|
|
218
|
+
expect(captured[0]).toMatchObject({
|
|
219
|
+
authorization: `Bearer ${GROQ_FAKE}`,
|
|
220
|
+
model: "whisper-large-v3-turbo",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("ElevenLabs wins over OpenAI and Groq when all are set", async () => {
|
|
225
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
226
|
+
process.env.GROQ_API_KEY = GROQ_FAKE;
|
|
227
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
228
|
+
await transcribeAudio(audio);
|
|
229
|
+
expect(captured[0]).toMatchObject({ xiApiKey: ELEVEN_FAKE, authorization: null });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("OpenAI wins over Groq", () => {
|
|
233
|
+
process.env.GROQ_API_KEY = GROQ_FAKE;
|
|
234
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
235
|
+
expect(resolveTranscriptionConfig()?.provider).toBe("openai");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("an explicit TRANSCRIPTION_API_KEY overrides both detected keys", async () => {
|
|
239
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
240
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
241
|
+
configure();
|
|
242
|
+
expect(resolveTranscriptionConfig()).toMatchObject({
|
|
243
|
+
provider: "openai",
|
|
244
|
+
keyEnv: "TRANSCRIPTION_API_KEY",
|
|
245
|
+
source: "explicit",
|
|
246
|
+
});
|
|
247
|
+
await transcribeAudio(audio);
|
|
248
|
+
// The explicit key honours TRANSCRIPTION_BASE_URL.
|
|
249
|
+
expect(calledUrls).toEqual([`http://127.0.0.1:${stt.port}/v1/audio/transcriptions`]);
|
|
250
|
+
expect(captured[0]).toMatchObject({
|
|
251
|
+
authorization: "Bearer stt-test-key-123456",
|
|
252
|
+
xiApiKey: null,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("an auto-detected key never goes to TRANSCRIPTION_BASE_URL", async () => {
|
|
257
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
258
|
+
process.env.TRANSCRIPTION_BASE_URL = "https://collector.example/v1";
|
|
259
|
+
await transcribeAudio(audio);
|
|
260
|
+
expect(calledUrls).toEqual(["https://api.elevenlabs.io/v1/speech-to-text"]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("a pinned provider uses only its own key", async () => {
|
|
264
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
265
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
266
|
+
process.env.TRANSCRIPTION_PROVIDER = "openai";
|
|
267
|
+
await transcribeAudio(audio);
|
|
268
|
+
expect(captured[0]).toMatchObject({ authorization: `Bearer ${OPENAI_FAKE}` });
|
|
269
|
+
|
|
270
|
+
delete process.env.OPENAI_API_KEY;
|
|
271
|
+
expect(isTranscriptionConfigured()).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("a detected key is scrubbed from errors under its own name", async () => {
|
|
275
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
276
|
+
reply = () => Response.json({ detail: `bad key ${ELEVEN_FAKE}` }, { status: 401 });
|
|
277
|
+
await expect(transcribeAudio(audio)).rejects.toThrow("bad key [REDACTED:ELEVENLABS_API_KEY]");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// The composer's mic button records webm in Chrome and Firefox; the upload
|
|
281
|
+
// route drops `;codecs=` before it gets here. Each provider reads the format
|
|
282
|
+
// from the file name, and all three list webm as supported.
|
|
283
|
+
test.each([
|
|
284
|
+
["elevenlabs", "ELEVENLABS_API_KEY", "https://api.elevenlabs.io/v1/speech-to-text"],
|
|
285
|
+
["openai", "OPENAI_API_KEY", "https://api.openai.com/v1/audio/transcriptions"],
|
|
286
|
+
["groq", "GROQ_API_KEY", "https://api.groq.com/openai/v1/audio/transcriptions"],
|
|
287
|
+
])("a recorded webm clip reaches %s as a .webm audio/webm file", async (name, keyEnv, url) => {
|
|
288
|
+
process.env[keyEnv] = fake(name);
|
|
289
|
+
const clip = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], { type: "audio/webm" });
|
|
290
|
+
const notes = await transcribeAudioNotes(
|
|
291
|
+
[{ key: "c", name: "recording-2026-10-21-143005.webm", data: clip, mimeType: "audio/webm" }],
|
|
292
|
+
"t",
|
|
293
|
+
);
|
|
294
|
+
expect(notes.get("c")).toBe("[Transcript of recording-2026-10-21-143005.webm]\nhello");
|
|
295
|
+
expect(calledUrls).toEqual([url]);
|
|
296
|
+
expect(captured[0]).toMatchObject({ filename: "recording-2026-10-21-143005.webm" });
|
|
297
|
+
expect(sentFileTypes).toEqual(["audio/webm"]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import { SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS, validateConfigValue } from "../be/swarm-config-guard";
|
|
4
5
|
import type { WorkflowNode } from "../types";
|
|
5
6
|
import { validateDefinition } from "../workflows/definition";
|
|
6
7
|
import { InProcessEventBus } from "../workflows/event-bus";
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
SystemOneDecisionExecutor,
|
|
12
13
|
type SystemOneDecisionExecutorOptions,
|
|
13
14
|
} from "../workflows/executors/system-one-decision";
|
|
15
|
+
import { SYSTEM_ONE_PROVIDER_IDS } from "../workflows/executors/system-one-providers";
|
|
14
16
|
import { interpolate } from "../workflows/template";
|
|
15
17
|
|
|
16
18
|
const TEST_DB_PATH = "./test-workflow-system-one-decision-providers.sqlite";
|
|
@@ -50,7 +52,7 @@ afterAll(async () => {
|
|
|
50
52
|
|
|
51
53
|
beforeEach(async () => {
|
|
52
54
|
await getDbClient().run(
|
|
53
|
-
"DELETE FROM swarm_config WHERE key IN ('OPENAI_API_KEY', 'OPENAI_DECISIONS_API_KEY', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN')",
|
|
55
|
+
"DELETE FROM swarm_config WHERE key IN ('OPENAI_API_KEY', 'OPENAI_DECISIONS_API_KEY', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN', 'SYSTEM_ONE_DEFAULT_PROVIDER')",
|
|
54
56
|
);
|
|
55
57
|
});
|
|
56
58
|
|
|
@@ -708,6 +710,101 @@ describe("system-one-decision openai provider", () => {
|
|
|
708
710
|
});
|
|
709
711
|
});
|
|
710
712
|
|
|
713
|
+
// ─── Default provider ───────────────────────────────────────
|
|
714
|
+
|
|
715
|
+
describe("system-one-decision default provider", () => {
|
|
716
|
+
const TYPESAFE_KEY = "typesafe-example-test-key-0123456789";
|
|
717
|
+
const jevBody = () =>
|
|
718
|
+
jsonResponse({
|
|
719
|
+
model: "jev",
|
|
720
|
+
answers: systemOneAnswers,
|
|
721
|
+
usage: { input_tokens: 1, output_tokens: 0 },
|
|
722
|
+
});
|
|
723
|
+
const setDefault = (value: string) =>
|
|
724
|
+
upsertSwarmConfig({ scope: "global", key: "SYSTEM_ONE_DEFAULT_PROVIDER", value });
|
|
725
|
+
const withTypesafeKey = {
|
|
726
|
+
getApiKey: async (id: string) =>
|
|
727
|
+
id === "typesafe" ? TYPESAFE_KEY : id === "cloudflare" ? CF_TOKEN : null,
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
test("unset, a node with no provider still calls typesafe", async () => {
|
|
731
|
+
const { executor, calls } = makeExecutor([jevBody()], withTypesafeKey);
|
|
732
|
+
const result = await run(executor, config());
|
|
733
|
+
expect(result.status).toBe("success");
|
|
734
|
+
expect(calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
|
|
735
|
+
expect(calls[0]?.body.model).toBe("jev-latest");
|
|
736
|
+
// The schema leaves the provider unset; the executor resolves it per run.
|
|
737
|
+
expect(SystemOneDecisionConfigSchema.parse(config()).provider).toBeUndefined();
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("set to cloudflare, a node with no provider calls Clef with no workflow edit", async () => {
|
|
741
|
+
await setDefault("cloudflare");
|
|
742
|
+
const { executor, calls } = makeExecutor([jsonResponse(clefEnvelope())], withTypesafeKey);
|
|
743
|
+
const result = await run(executor, config());
|
|
744
|
+
expect(result.status).toBe("success");
|
|
745
|
+
expect(calls[0]?.url).toBe(`${CF_BASE}/clef`);
|
|
746
|
+
expect((calls[0]?.init.headers as Record<string, string>).authorization).toBe(
|
|
747
|
+
`Bearer ${CF_TOKEN}`,
|
|
748
|
+
);
|
|
749
|
+
expect(result.output).toMatchObject({ model: "clef", answers: systemOneAnswers as never });
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("a node that names its provider keeps it", async () => {
|
|
753
|
+
await setDefault("cloudflare");
|
|
754
|
+
const { executor, calls } = makeExecutor([jevBody()], withTypesafeKey);
|
|
755
|
+
expect((await run(executor, config({ provider: "typesafe" }))).status).toBe("success");
|
|
756
|
+
expect(calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("readiness checks the default provider's credentials", async () => {
|
|
760
|
+
await setDefault("cloudflare");
|
|
761
|
+
const { executor } = makeExecutor([], { getApiKey: async () => null });
|
|
762
|
+
const problems = await executor.checkReadiness([{ id: "a", config: config() }]);
|
|
763
|
+
expect(problems).toHaveLength(1);
|
|
764
|
+
expect(problems[0]?.nodeIds).toEqual(["a"]);
|
|
765
|
+
expect(problems[0]?.message).toContain("CLOUDFLARE_API_TOKEN is not configured");
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("the environment is read when no row is stored; a stored row wins", async () => {
|
|
769
|
+
const env = { SYSTEM_ONE_DEFAULT_PROVIDER: "cloudflare" };
|
|
770
|
+
const fromEnv = makeExecutor([jsonResponse(clefEnvelope())], { ...withTypesafeKey, env });
|
|
771
|
+
expect((await run(fromEnv.executor, config())).status).toBe("success");
|
|
772
|
+
expect(fromEnv.calls[0]?.url).toBe(`${CF_BASE}/clef`);
|
|
773
|
+
|
|
774
|
+
await setDefault("typesafe");
|
|
775
|
+
const stored = makeExecutor([jevBody()], { ...withTypesafeKey, env });
|
|
776
|
+
expect((await run(stored.executor, config())).status).toBe("success");
|
|
777
|
+
expect(stored.calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("a defaulted provider's limits are checked at run, and nothing is sent", async () => {
|
|
781
|
+
await setDefault("cloudflare");
|
|
782
|
+
const qs: Record<string, unknown> = {};
|
|
783
|
+
const rs: Record<string, unknown> = {};
|
|
784
|
+
for (let i = 0; i < 65; i++) {
|
|
785
|
+
qs[`q${i}`] = { type: "noul", instructions: `Question ${i}?` };
|
|
786
|
+
rs[`q${i}`] = { type: "noul" };
|
|
787
|
+
}
|
|
788
|
+
const { executor, calls } = makeExecutor([jsonResponse(clefEnvelope())]);
|
|
789
|
+
const result = await run(executor, config({ questions: qs, returns: rs }));
|
|
790
|
+
expect(result.status).toBe("failed");
|
|
791
|
+
expect(result.error).toContain("at most 64 questions per node; this node has 65");
|
|
792
|
+
expect(calls).toHaveLength(0);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("the config guard accepts exactly the decide hosts", () => {
|
|
796
|
+
expect([...SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS].sort()).toEqual(
|
|
797
|
+
[...SYSTEM_ONE_PROVIDER_IDS].sort(),
|
|
798
|
+
);
|
|
799
|
+
for (const id of SYSTEM_ONE_PROVIDER_IDS) {
|
|
800
|
+
expect(validateConfigValue("SYSTEM_ONE_DEFAULT_PROVIDER", id)).toBeNull();
|
|
801
|
+
}
|
|
802
|
+
expect(validateConfigValue("SYSTEM_ONE_DEFAULT_PROVIDER", "jev")).toContain(
|
|
803
|
+
"Invalid SYSTEM_ONE_DEFAULT_PROVIDER value",
|
|
804
|
+
);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
711
808
|
// ─── Existing providers ─────────────────────────────────────
|
|
712
809
|
|
|
713
810
|
describe("system-one-decision SystemOne wire is unchanged", () => {
|
|
@@ -227,7 +227,8 @@ describe("system-one-decision config schema", () => {
|
|
|
227
227
|
questions: { fit: { type: "noul", instructions: "Is it real?" } },
|
|
228
228
|
returns: { fit: { type: "noul" } },
|
|
229
229
|
});
|
|
230
|
-
|
|
230
|
+
// Unset: the executor reads SYSTEM_ONE_DEFAULT_PROVIDER at run time (typesafe when unset).
|
|
231
|
+
expect(parsed.provider).toBeUndefined();
|
|
231
232
|
// The model default belongs to the provider, so the schema leaves it unset.
|
|
232
233
|
expect(parsed.model).toBeUndefined();
|
|
233
234
|
expect(parsed.timeoutMs).toBe(30_000);
|
package/src/tools/chat-access.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
1
2
|
import { getAgentById, getAgentCurrentTask, getTaskById } from "@/be/db";
|
|
2
3
|
import { can } from "@/rbac";
|
|
3
4
|
import { chatThreadOfContextKey } from "@/tasks/context-key";
|
|
4
5
|
import type { RequestInfo } from "@/tools/utils";
|
|
5
6
|
|
|
7
|
+
/** The `attachments` input of `chat-reply` and `chat-post`. */
|
|
8
|
+
export const chatAttachmentsSchema = z
|
|
9
|
+
.array(z.string().min(1))
|
|
10
|
+
.max(10)
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(
|
|
13
|
+
"Files to send after the text, up to 10: attachment ids of your current task (from get-task-details) or file paths in your agent-fs drive, such as thoughts/<agent-id>/report.pdf, read with your own agent-fs credential. Telegram shows JPEG and PNG as photos, MP4 as video, MP3 and M4A as audio, and anything else as a document. A file over the platform upload limit (Telegram 50 MB, Discord 10 MB) is not sent, and the thread gets a note naming it.",
|
|
14
|
+
);
|
|
15
|
+
|
|
6
16
|
/** Chat SDK verbs; the legacy policy grants them to the lead only. */
|
|
7
17
|
export type ChatVerb = "integration.chat.post" | "integration.chat.read" | "integration.chat.dm";
|
|
8
18
|
|
package/src/tools/chat-post.ts
CHANGED
|
@@ -2,8 +2,13 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { CHAT_SDK_ADAPTER_NAMES } from "@/integrations/chat-sdk/config";
|
|
4
4
|
import { errorMessage } from "@/integrations/chat-sdk/logger";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
adapterOfChatId,
|
|
7
|
+
postChatMessage,
|
|
8
|
+
unsentFilesSummary,
|
|
9
|
+
} from "@/integrations/chat-sdk/outbound";
|
|
10
|
+
import { resolveChatAttachmentRefs } from "@/integrations/chat-sdk/outbound-files";
|
|
11
|
+
import { canUseChatVerb, chatAttachmentsSchema } from "@/tools/chat-access";
|
|
7
12
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
8
13
|
|
|
9
14
|
export const registerChatPostTool = (server: McpServer) => {
|
|
@@ -27,6 +32,7 @@ export const registerChatPostTool = (server: McpServer) => {
|
|
|
27
32
|
.optional()
|
|
28
33
|
.describe("Full SDK channel id, such as slack:C123. Ignored when threadId is set."),
|
|
29
34
|
text: z.string().min(1).max(20_000).describe("Markdown text to post."),
|
|
35
|
+
attachments: chatAttachmentsSchema,
|
|
30
36
|
}),
|
|
31
37
|
outputSchema: swarmToolOutputSchema({
|
|
32
38
|
yourAgentId: z.string().optional(),
|
|
@@ -34,9 +40,12 @@ export const registerChatPostTool = (server: McpServer) => {
|
|
|
34
40
|
adapter: z.string().optional(),
|
|
35
41
|
threadId: z.string().optional(),
|
|
36
42
|
channelId: z.string().optional(),
|
|
43
|
+
unsentFiles: z
|
|
44
|
+
.array(z.looseObject({ name: z.string().optional(), reason: z.string().optional() }))
|
|
45
|
+
.optional(),
|
|
37
46
|
}),
|
|
38
47
|
},
|
|
39
|
-
async ({ adapter, threadId, channelId, text }, requestInfo) => {
|
|
48
|
+
async ({ adapter, threadId, channelId, text, attachments }, requestInfo) => {
|
|
40
49
|
const data = { yourAgentId: requestInfo.agentId };
|
|
41
50
|
const target = threadId ?? channelId;
|
|
42
51
|
if (!target) return toolErr("Give a threadId or a channelId.", { data });
|
|
@@ -49,22 +58,33 @@ export const registerChatPostTool = (server: McpServer) => {
|
|
|
49
58
|
{ data },
|
|
50
59
|
);
|
|
51
60
|
}
|
|
61
|
+
const resolved = await resolveChatAttachmentRefs(
|
|
62
|
+
attachments ?? [],
|
|
63
|
+
requestInfo.sourceTaskId,
|
|
64
|
+
requestInfo.agentId,
|
|
65
|
+
);
|
|
66
|
+
if ("error" in resolved) return toolErr(resolved.error, { data });
|
|
52
67
|
try {
|
|
53
68
|
const sent = await postChatMessage({
|
|
54
69
|
adapter,
|
|
55
70
|
...(threadId ? { threadId } : { channelId }),
|
|
56
71
|
text,
|
|
57
72
|
taskId: requestInfo.sourceTaskId,
|
|
73
|
+
files: resolved.files,
|
|
58
74
|
});
|
|
59
|
-
return toolOk(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
return toolOk(
|
|
76
|
+
`Posted to ${adapter} ${threadId ? "thread" : "channel"} ${target}.${unsentFilesSummary(sent.unsentFiles)}`,
|
|
77
|
+
{
|
|
78
|
+
data: {
|
|
79
|
+
...data,
|
|
80
|
+
messageId: sent.messageIds.at(-1),
|
|
81
|
+
adapter: sent.adapter,
|
|
82
|
+
threadId: sent.threadId,
|
|
83
|
+
channelId: sent.channelId,
|
|
84
|
+
unsentFiles: sent.unsentFiles,
|
|
85
|
+
},
|
|
66
86
|
},
|
|
67
|
-
|
|
87
|
+
);
|
|
68
88
|
} catch (error) {
|
|
69
89
|
const message = errorMessage(error);
|
|
70
90
|
return toolErr(`Could not post: ${message}`, { data });
|
package/src/tools/chat-reply.ts
CHANGED
|
@@ -2,8 +2,14 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { getAgentCurrentTask, getTaskById } from "@/be/db";
|
|
4
4
|
import { errorMessage } from "@/integrations/chat-sdk/logger";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ChatAdapterUnavailableError,
|
|
7
|
+
postChatMessage,
|
|
8
|
+
unsentFilesSummary,
|
|
9
|
+
} from "@/integrations/chat-sdk/outbound";
|
|
10
|
+
import { resolveChatAttachmentRefs } from "@/integrations/chat-sdk/outbound-files";
|
|
6
11
|
import { chatThreadOfContextKey } from "@/tasks/context-key";
|
|
12
|
+
import { chatAttachmentsSchema } from "@/tools/chat-access";
|
|
7
13
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
8
14
|
|
|
9
15
|
export const registerChatReplyTool = (server: McpServer) => {
|
|
@@ -12,19 +18,23 @@ export const registerChatReplyTool = (server: McpServer) => {
|
|
|
12
18
|
{
|
|
13
19
|
title: "Reply in the chat thread of this task",
|
|
14
20
|
description:
|
|
15
|
-
"Post a message to the Telegram, Slack, Discord or Teams thread your current task came from. Works only on a task whose contextKey starts with task:chat-sdk:. The task's final output is posted to the thread automatically when the task ends, so use this only for a question to the user
|
|
21
|
+
"Post a message to the Telegram, Slack, Discord or Teams thread your current task came from. Works only on a task whose contextKey starts with task:chat-sdk:. The task's final output is posted to the thread automatically when the task ends, so use this only for a question to the user, a short progress note on long work, or to send files (attachments). Markdown is supported; long text is split into several messages. When the final output of a task is posted, the files attached to that task go with it.",
|
|
16
22
|
annotations: { openWorldHint: true },
|
|
17
23
|
inputSchema: z.object({
|
|
18
24
|
text: z.string().min(1).max(20_000).describe("Markdown text to post to the thread."),
|
|
25
|
+
attachments: chatAttachmentsSchema,
|
|
19
26
|
}),
|
|
20
27
|
outputSchema: swarmToolOutputSchema({
|
|
21
28
|
yourAgentId: z.string().optional(),
|
|
22
29
|
messageId: z.string().optional(),
|
|
23
30
|
adapter: z.string().optional(),
|
|
24
31
|
threadId: z.string().optional(),
|
|
32
|
+
unsentFiles: z
|
|
33
|
+
.array(z.looseObject({ name: z.string().optional(), reason: z.string().optional() }))
|
|
34
|
+
.optional(),
|
|
25
35
|
}),
|
|
26
36
|
},
|
|
27
|
-
async ({ text }, requestInfo) => {
|
|
37
|
+
async ({ text, attachments }, requestInfo) => {
|
|
28
38
|
if (!requestInfo.agentId) return toolErr("Agent ID not found.");
|
|
29
39
|
const data = { yourAgentId: requestInfo.agentId };
|
|
30
40
|
|
|
@@ -46,21 +56,33 @@ export const registerChatReplyTool = (server: McpServer) => {
|
|
|
46
56
|
);
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
const resolved = await resolveChatAttachmentRefs(
|
|
60
|
+
attachments ?? [],
|
|
61
|
+
task.id,
|
|
62
|
+
requestInfo.agentId,
|
|
63
|
+
);
|
|
64
|
+
if ("error" in resolved) return toolErr(resolved.error, { data });
|
|
65
|
+
|
|
49
66
|
try {
|
|
50
67
|
const sent = await postChatMessage({
|
|
51
68
|
adapter: thread.adapter,
|
|
52
69
|
threadId: thread.threadId,
|
|
53
70
|
text,
|
|
54
71
|
taskId: task.id,
|
|
72
|
+
files: resolved.files,
|
|
55
73
|
});
|
|
56
|
-
return toolOk(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
return toolOk(
|
|
75
|
+
`Posted to ${thread.adapter} thread ${sent.threadId}.${unsentFilesSummary(sent.unsentFiles)}`,
|
|
76
|
+
{
|
|
77
|
+
data: {
|
|
78
|
+
...data,
|
|
79
|
+
messageId: sent.messageIds.at(-1),
|
|
80
|
+
adapter: sent.adapter,
|
|
81
|
+
threadId: sent.threadId,
|
|
82
|
+
unsentFiles: sent.unsentFiles,
|
|
83
|
+
},
|
|
62
84
|
},
|
|
63
|
-
|
|
85
|
+
);
|
|
64
86
|
} catch (error) {
|
|
65
87
|
const message = errorMessage(error);
|
|
66
88
|
return toolErr(
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
recordRetrievals,
|
|
12
12
|
} from "@/be/memory/raters/retrieval";
|
|
13
13
|
import { rerank } from "@/be/memory/reranker";
|
|
14
|
+
import { searchMemoryCandidates } from "@/be/memory/search-candidates";
|
|
14
15
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
15
16
|
import type { AgentMemorySource } from "@/types";
|
|
16
17
|
import { AgentMemoryScopeSchema, AgentMemorySourceSchema } from "@/types";
|
|
@@ -122,7 +123,8 @@ export const registerMemorySearchTool = (server: McpServer) => {
|
|
|
122
123
|
const queryEmbedding = await provider.embed(query);
|
|
123
124
|
|
|
124
125
|
const candidateLimit = limit * CANDIDATE_SET_MULTIPLIER;
|
|
125
|
-
const candidates = await
|
|
126
|
+
const candidates = await searchMemoryCandidates(
|
|
127
|
+
store,
|
|
126
128
|
queryEmbedding ?? new Float32Array(0),
|
|
127
129
|
requestInfo.agentId,
|
|
128
130
|
{
|