@desplega.ai/agent-swarm 2.4.3 → 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-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
- package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
- package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
- package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
- package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
- package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
- package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
- package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
- package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
- package/dist/{claude-adapter-rp0jtnwr.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-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
- package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
- package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
- package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
- package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
- package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
- package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
- package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
- package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
- package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
- package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
- package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
- package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
- package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
- package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
- package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
- package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
- package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
- package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
- package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
- package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
- package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
- package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
- package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
- package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
- package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
- package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
- package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
- package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
- package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
- package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
- package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
- package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
- package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
- package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
- package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
- package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
- package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
- package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
- package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
- package/dist/cli.js +45 -20
- package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
- package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
- package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
- package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
- package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
- package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
- package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
- package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
- package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
- package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
- package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
- package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
- package/dist/handlers-7gw64dxw.js +39 -0
- package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
- package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
- package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
- package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
- package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
- package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
- package/dist/index-tfb26zhy.js +41 -0
- package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
- package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
- package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
- package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
- package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
- package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
- package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
- package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
- package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
- package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
- package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
- package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
- package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
- package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
- package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +303 -13
- package/package.json +8 -8
- package/src/apps/action-runner.ts +4 -0
- package/src/apps/catalog.generated.json +209 -4
- package/src/apps/page-validator.ts +21 -3
- 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/migrations/232_agent_session_tokens.sql +41 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/swarm-config-guard.ts +40 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/be/users.ts +169 -12
- package/src/cli.tsx +24 -0
- package/src/commands/worker-token.ts +164 -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/apps.ts +43 -5
- package/src/http/codex-oauth-keep-warm.ts +27 -9
- package/src/http/config.ts +5 -3
- package/src/http/core.ts +2 -0
- 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/session-access.ts +51 -2
- package/src/http/sessions.ts +249 -10
- package/src/http/tasks.ts +4 -32
- 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/acp-adapter.ts +33 -13
- package/src/providers/acp-swarm-events.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/codex-oauth/storage.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -1
- package/src/rbac/permissions.ts +4 -0
- 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/acp-adapter.test.ts +10 -23
- package/src/tests/acp-fake-agent.ts +36 -0
- package/src/tests/acp-swarm-events.test.ts +1 -1
- package/src/tests/agent-session-tokens.test.ts +452 -0
- package/src/tests/apps-action-parent-task.test.ts +295 -0
- 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/codex-oauth-keep-warm.test.ts +85 -1
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/config-session-auth.test.ts +14 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/grok-adapter.test.ts +3 -3
- 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 +265 -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/migration-232-agent-session-tokens.test.ts +67 -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/rbac-engine.test.ts +1 -0
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/session-route-posture.test.ts +2 -0
- 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/worker-token-cli.test.ts +76 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- 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/tests/workflow-system-one-extraction.test.ts +805 -0
- 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 +8 -1
- package/src/utils/config-env-export.ts +3 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/request-auth-context.ts +2 -1
- package/src/utils/transcription.ts +246 -0
- package/src/workflows/definition.ts +7 -1
- package/src/workflows/engine.ts +18 -12
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +84 -262
- package/src/workflows/executors/system-one-extract-wire.ts +57 -0
- package/src/workflows/executors/system-one-extraction.ts +830 -0
- package/src/workflows/executors/system-one-providers.ts +316 -114
- package/src/workflows/executors/system-one-transport.ts +264 -0
- 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/official/lead/HEARTBEAT.md +1 -1
- package/templates/skills/apps/SKILL.md +31 -7
- package/templates/skills/apps/content.md +31 -7
- 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 +8 -8
- 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/cli-v1sjhn52.js +0 -7
- package/dist/handlers-2qjw434w.js +0 -39
- package/dist/index-d0g18bax.js +0 -41
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import type { Attachment, FileUpload } from "chat";
|
|
1
2
|
import { scrubSecrets } from "@/utils/secret-scrubber";
|
|
2
3
|
import { workflowEventBus } from "@/workflows/event-bus";
|
|
3
|
-
import {
|
|
4
|
+
import { formatFileSize } from "./attachments";
|
|
5
|
+
import {
|
|
6
|
+
CHAT_FILES_PER_MESSAGE,
|
|
7
|
+
chatMessageLimit,
|
|
8
|
+
chatUploadLimit,
|
|
9
|
+
splitChatMessage,
|
|
10
|
+
TELEGRAM_PHOTO_LIMIT,
|
|
11
|
+
} from "./limits";
|
|
12
|
+
import { errorMessage } from "./logger";
|
|
4
13
|
import { getChatInstance } from "./runtime";
|
|
5
14
|
import { assertTrustedChatTarget } from "./teams-origin";
|
|
6
15
|
|
|
@@ -39,12 +48,40 @@ export interface ChatPostResult {
|
|
|
39
48
|
adapter: string;
|
|
40
49
|
threadId: string;
|
|
41
50
|
channelId: string;
|
|
42
|
-
/** One id per chunk, in send order. */
|
|
51
|
+
/** One id per chunk and per file message, in send order. */
|
|
43
52
|
messageIds: string[];
|
|
53
|
+
/** Files that did not reach the thread. The thread got a note naming each one. */
|
|
54
|
+
unsentFiles: UnsentChatFile[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A file to send after the text. `load` reads its bytes. */
|
|
58
|
+
export interface OutboundChatFile {
|
|
59
|
+
name: string;
|
|
60
|
+
mimeType?: string;
|
|
61
|
+
/** Size known before loading, so an oversized file is never read. */
|
|
62
|
+
sizeBytes?: number;
|
|
63
|
+
/** Throws `ChatFileTooLargeError` when the file is larger than `maxBytes`. */
|
|
64
|
+
load(maxBytes: number): Promise<{ data: Uint8Array; mimeType?: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface UnsentChatFile {
|
|
68
|
+
name: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class ChatFileTooLargeError extends Error {
|
|
73
|
+
constructor(readonly sizeBytes: number) {
|
|
74
|
+
super(`file is ${formatFileSize(sizeBytes)}`);
|
|
75
|
+
this.name = "ChatFileTooLargeError";
|
|
76
|
+
}
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
interface PostTarget {
|
|
47
|
-
post(message: {
|
|
80
|
+
post(message: {
|
|
81
|
+
markdown: string;
|
|
82
|
+
files?: FileUpload[];
|
|
83
|
+
attachments?: Attachment[];
|
|
84
|
+
}): Promise<{ id: string; threadId?: string }>;
|
|
48
85
|
}
|
|
49
86
|
|
|
50
87
|
async function postChunks(
|
|
@@ -62,10 +99,186 @@ async function postChunks(
|
|
|
62
99
|
return { messageIds, threadId };
|
|
63
100
|
}
|
|
64
101
|
|
|
102
|
+
type LoadedFile = { name: string; mimeType: string; data: Buffer };
|
|
103
|
+
|
|
104
|
+
/** First usable MIME type, else a guess from the file extension. */
|
|
105
|
+
function resolveMimeType(name: string, ...candidates: (string | null | undefined)[]): string {
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
const type = candidate?.split(";")[0]?.trim().toLowerCase();
|
|
108
|
+
if (type && type !== "application/octet-stream") return type;
|
|
109
|
+
}
|
|
110
|
+
return Bun.file(name).type.split(";")[0] ?? "application/octet-stream";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* How Telegram shows a file. Only types its media methods take are sent as
|
|
115
|
+
* photo, video or audio; everything else, and a photo over 10 MB, goes as a
|
|
116
|
+
* document.
|
|
117
|
+
*/
|
|
118
|
+
export function telegramAttachmentType(mimeType: string, sizeBytes: number): Attachment["type"] {
|
|
119
|
+
if (
|
|
120
|
+
(mimeType === "image/jpeg" || mimeType === "image/png") &&
|
|
121
|
+
sizeBytes <= TELEGRAM_PHOTO_LIMIT
|
|
122
|
+
) {
|
|
123
|
+
return "image";
|
|
124
|
+
}
|
|
125
|
+
if (mimeType === "video/mp4") return "video";
|
|
126
|
+
if (mimeType === "audio/mpeg" || mimeType === "audio/mp4" || mimeType === "audio/x-m4a") {
|
|
127
|
+
return "audio";
|
|
128
|
+
}
|
|
129
|
+
return "file";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Telegram media groups mix photos and videos only; documents and audio group
|
|
134
|
+
* with their own kind. Other adapters put any files in one message.
|
|
135
|
+
*/
|
|
136
|
+
function fileGroupKey(adapter: string, file: LoadedFile): string {
|
|
137
|
+
if (adapter !== "telegram") return "files";
|
|
138
|
+
const type = telegramAttachmentType(file.mimeType, file.data.byteLength);
|
|
139
|
+
return type === "image" || type === "video" ? "visual" : type;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fileMessage(adapter: string, files: LoadedFile[]) {
|
|
143
|
+
// Telegram uploads `files` only as documents; typed `attachments` become
|
|
144
|
+
// photos, videos and audio. A list of 2 to 10 is sent as one media group.
|
|
145
|
+
if (adapter === "telegram") {
|
|
146
|
+
return {
|
|
147
|
+
markdown: "",
|
|
148
|
+
attachments: files.map(
|
|
149
|
+
(file): Attachment => ({
|
|
150
|
+
type: telegramAttachmentType(file.mimeType, file.data.byteLength),
|
|
151
|
+
name: file.name,
|
|
152
|
+
mimeType: file.mimeType,
|
|
153
|
+
size: file.data.byteLength,
|
|
154
|
+
data: file.data,
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
markdown: "",
|
|
161
|
+
files: files.map((file) => ({ data: file.data, filename: file.name, mimeType: file.mimeType })),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function platformName(adapter: string): string {
|
|
166
|
+
return adapter.charAt(0).toUpperCase() + adapter.slice(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Send files as file messages, up to 10 per message, grouped the way the
|
|
171
|
+
* platform allows. Files are read one by one and a group is sent once full, so
|
|
172
|
+
* at most one group is held in memory. Never throws: a file that is too large,
|
|
173
|
+
* cannot be read, or is refused by the platform lands in `unsent`.
|
|
174
|
+
*/
|
|
175
|
+
async function postFiles(
|
|
176
|
+
adapter: string,
|
|
177
|
+
target: PostTarget,
|
|
178
|
+
files: OutboundChatFile[],
|
|
179
|
+
): Promise<{ messageIds: string[]; unsent: UnsentChatFile[] }> {
|
|
180
|
+
const limit = chatUploadLimit(adapter);
|
|
181
|
+
const tooLarge = (size: number) =>
|
|
182
|
+
`${formatFileSize(size)}, over the ${formatFileSize(limit)} upload limit of ${platformName(adapter)}`;
|
|
183
|
+
const messageIds: string[] = [];
|
|
184
|
+
const unsent: UnsentChatFile[] = [];
|
|
185
|
+
const groups = new Map<string, LoadedFile[]>();
|
|
186
|
+
|
|
187
|
+
const send = async (group: LoadedFile[]) => {
|
|
188
|
+
try {
|
|
189
|
+
const sent = await target.post(fileMessage(adapter, group));
|
|
190
|
+
messageIds.push(sent.id);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const reason = `refused by ${platformName(adapter)} (${scrubSecrets(errorMessage(error))})`;
|
|
193
|
+
for (const file of group) unsent.push({ name: file.name, reason });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
if (file.sizeBytes !== undefined && file.sizeBytes > limit) {
|
|
199
|
+
unsent.push({ name: file.name, reason: tooLarge(file.sizeBytes) });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
let loaded: LoadedFile;
|
|
203
|
+
try {
|
|
204
|
+
const { data, mimeType } = await file.load(limit);
|
|
205
|
+
if (data.byteLength > limit) throw new ChatFileTooLargeError(data.byteLength);
|
|
206
|
+
loaded = {
|
|
207
|
+
name: file.name,
|
|
208
|
+
mimeType: resolveMimeType(file.name, file.mimeType, mimeType),
|
|
209
|
+
data: Buffer.from(data.buffer, data.byteOffset, data.byteLength),
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
unsent.push({
|
|
213
|
+
name: file.name,
|
|
214
|
+
reason:
|
|
215
|
+
error instanceof ChatFileTooLargeError
|
|
216
|
+
? tooLarge(error.sizeBytes)
|
|
217
|
+
: `could not be read (${scrubSecrets(errorMessage(error))})`,
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const key = fileGroupKey(adapter, loaded);
|
|
222
|
+
const group = [...(groups.get(key) ?? []), loaded];
|
|
223
|
+
if (group.length === CHAT_FILES_PER_MESSAGE) {
|
|
224
|
+
groups.delete(key);
|
|
225
|
+
await send(group);
|
|
226
|
+
} else {
|
|
227
|
+
groups.set(key, group);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const group of groups.values()) await send(group);
|
|
231
|
+
return { messageIds, unsent };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Tool-result suffix naming the files that did not go out; empty when all did. */
|
|
235
|
+
export function unsentFilesSummary(unsent: UnsentChatFile[]): string {
|
|
236
|
+
return unsent.length === 0 ? "" : ` ${formatUnsentFilesNote(unsent)}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** One line of untrusted text: control characters and line breaks become spaces. */
|
|
240
|
+
function singleLine(text: string): string {
|
|
241
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point
|
|
242
|
+
return text.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").trim();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** A zero-width space after `@` and `<!` keeps adapters from turning text into mentions. */
|
|
246
|
+
function breakMentions(text: string): string {
|
|
247
|
+
return text.replace(/@/g, "@\u200b").replace(/<!/g, "<\u200b!");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* A file name as a Markdown code span, so it renders literally: no emphasis,
|
|
252
|
+
* links, or mentions. The fence is one backtick longer than the longest run
|
|
253
|
+
* inside, and the padding keeps an edge backtick from joining the fence.
|
|
254
|
+
*/
|
|
255
|
+
function codeSpan(text: string): string {
|
|
256
|
+
const value = breakMentions(singleLine(text)) || "(unnamed)";
|
|
257
|
+
const longestRun = Math.max(0, ...(value.match(/`+/g) ?? []).map((run) => run.length));
|
|
258
|
+
const fence = "`".repeat(longestRun + 1);
|
|
259
|
+
const pad = value.startsWith("`") || value.endsWith("`") ? " " : "";
|
|
260
|
+
return `${fence}${pad}${value}${pad}${fence}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The note posted after the files when some did not go out. File names come
|
|
265
|
+
* from senders and agents, so they render as code spans and cannot add
|
|
266
|
+
* formatting or mentions to the bot's message.
|
|
267
|
+
*/
|
|
268
|
+
export function formatUnsentFilesNote(unsent: UnsentChatFile[]): string {
|
|
269
|
+
const lines = unsent.map(
|
|
270
|
+
(file) => `- ${codeSpan(file.name)}: ${breakMentions(singleLine(file.reason))}`,
|
|
271
|
+
);
|
|
272
|
+
const count = unsent.length === 1 ? "1 file" : `${unsent.length} files`;
|
|
273
|
+
return [`Could not send ${count}:`, ...lines].join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
65
276
|
/**
|
|
66
277
|
* Post markdown to a Chat SDK thread or channel. Scrubs the text, splits it by
|
|
67
278
|
* the adapter limit, and emits one `chat.message.sent` for the whole message
|
|
68
|
-
* once every chunk was accepted.
|
|
279
|
+
* once every chunk was accepted. `files` go out after the text; a file that
|
|
280
|
+
* does not reach the thread is named in a note there and in `unsentFiles`,
|
|
281
|
+
* and does not fail the call. Throws `ChatAdapterUnavailableError` when the
|
|
69
282
|
* adapter is not running, `UntrustedTeamsServiceUrlError` before any send
|
|
70
283
|
* when a Teams id points at a host that is not Bot Framework, and the adapter
|
|
71
284
|
* error when a send fails.
|
|
@@ -80,6 +293,7 @@ export async function postChatMessage(args: {
|
|
|
80
293
|
channelId?: string;
|
|
81
294
|
text: string;
|
|
82
295
|
taskId?: string;
|
|
296
|
+
files?: OutboundChatFile[];
|
|
83
297
|
}): Promise<ChatPostResult> {
|
|
84
298
|
// Ids come from tools and stored context keys. Check the Teams origin
|
|
85
299
|
// before the adapter sends its token.
|
|
@@ -89,28 +303,44 @@ export async function postChatMessage(args: {
|
|
|
89
303
|
const text = scrubSecrets(args.text);
|
|
90
304
|
|
|
91
305
|
let result: ChatPostResult;
|
|
306
|
+
let target: PostTarget;
|
|
92
307
|
if (args.threadId) {
|
|
93
308
|
const thread = chat.thread(args.threadId);
|
|
309
|
+
target = thread;
|
|
94
310
|
const sent = await postChunks(args.adapter, thread, text);
|
|
95
311
|
result = {
|
|
96
312
|
adapter: args.adapter,
|
|
97
313
|
threadId: thread.id,
|
|
98
314
|
channelId: thread.channelId,
|
|
99
315
|
messageIds: sent.messageIds,
|
|
316
|
+
unsentFiles: [],
|
|
100
317
|
};
|
|
101
318
|
} else if (args.channelId) {
|
|
102
319
|
const channel = chat.channel(args.channelId);
|
|
320
|
+
target = channel;
|
|
103
321
|
const sent = await postChunks(args.adapter, channel, text);
|
|
104
322
|
result = {
|
|
105
323
|
adapter: args.adapter,
|
|
106
324
|
threadId: sent.threadId ?? channel.id,
|
|
107
325
|
channelId: channel.id,
|
|
108
326
|
messageIds: sent.messageIds,
|
|
327
|
+
unsentFiles: [],
|
|
109
328
|
};
|
|
110
329
|
} else {
|
|
111
330
|
throw new Error("postChatMessage needs a threadId or a channelId");
|
|
112
331
|
}
|
|
113
332
|
|
|
333
|
+
if (args.files?.length) {
|
|
334
|
+
const sent = await postFiles(args.adapter, target, args.files);
|
|
335
|
+
result.messageIds.push(...sent.messageIds);
|
|
336
|
+
result.unsentFiles = sent.unsent;
|
|
337
|
+
if (sent.unsent.length > 0) {
|
|
338
|
+
const note = formatUnsentFilesNote(sent.unsent);
|
|
339
|
+
const noted = await postChunks(args.adapter, target, scrubSecrets(note));
|
|
340
|
+
result.messageIds.push(...noted.messageIds);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
114
344
|
emitChatMessageSent({
|
|
115
345
|
adapter: result.adapter,
|
|
116
346
|
channelId: result.channelId,
|
|
@@ -12,6 +12,7 @@ import { scrubSecrets } from "@/utils/secret-scrubber";
|
|
|
12
12
|
import { workflowEventBus } from "@/workflows/event-bus";
|
|
13
13
|
import { errorMessage } from "./logger";
|
|
14
14
|
import { postChatMessage } from "./outbound";
|
|
15
|
+
import { taskOutputChatFiles } from "./outbound-files";
|
|
15
16
|
import { getChatInstance, getChatSdkHealth } from "./runtime";
|
|
16
17
|
import { sweepExpired } from "./state";
|
|
17
18
|
import { stopTypingKeepalive } from "./typing";
|
|
@@ -49,7 +50,8 @@ export function formatChatRelayText(task: AgentTask): string {
|
|
|
49
50
|
*
|
|
50
51
|
* Delivery is at-least-once: a chunk failure re-posts the earlier chunks on
|
|
51
52
|
* retry, and a crash between the send and `markChatRelayDelivered` re-sends the
|
|
52
|
-
* message.
|
|
53
|
+
* message. The task's output attachments go after the text; a file that cannot
|
|
54
|
+
* be sent gets a note in the thread and does not fail the delivery.
|
|
53
55
|
*
|
|
54
56
|
* Once `signal` aborts, no further row is claimed. A send already in flight is
|
|
55
57
|
* not cancelled (`postChatMessage` takes no signal); its row keeps the claim
|
|
@@ -99,6 +101,7 @@ export async function processPendingChatRelays(
|
|
|
99
101
|
threadId: target.threadId,
|
|
100
102
|
text: formatChatRelayText(task),
|
|
101
103
|
taskId: task.id,
|
|
104
|
+
files: await taskOutputChatFiles(task.id),
|
|
102
105
|
});
|
|
103
106
|
stopTypingKeepalive(target.threadId);
|
|
104
107
|
await markChatRelayDelivered(task.id, now);
|
|
@@ -83,6 +83,11 @@ export async function deleteChatChannelMapping(
|
|
|
83
83
|
return deleteKv(CHAT_CHANNELS_NAMESPACE, chatChannelKey(adapter, channelId));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** True when the text carries a `swarm#<uuid>` or `swarm#all` routing token. */
|
|
87
|
+
export function hasRoutingToken(text: string): boolean {
|
|
88
|
+
return /swarm#(?:[a-f0-9-]{36}|all)/i.test(text);
|
|
89
|
+
}
|
|
90
|
+
|
|
86
91
|
/** Remove `swarm#<uuid>` and `swarm#all` routing tokens from task text. */
|
|
87
92
|
export function stripRoutingTokens(text: string): string {
|
|
88
93
|
return text
|
|
@@ -91,6 +96,20 @@ export function stripRoutingTokens(text: string): string {
|
|
|
91
96
|
.trim();
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
const DEFAULT_OWNER_WINDOW_MINUTES = 15;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Start of the thread-owner window: a thread keeps its owner while the
|
|
103
|
+
* owner's task is open and for `CHAT_SDK_OWNER_WINDOW_MIN` (default 15,
|
|
104
|
+
* 0 = only while open) after it ends. Read per call, so a change applies at once.
|
|
105
|
+
*/
|
|
106
|
+
export function chatOwnerWindowStart(now: number = Date.now()): string {
|
|
107
|
+
const raw = process.env.CHAT_SDK_OWNER_WINDOW_MIN?.trim();
|
|
108
|
+
const value = raw ? Number(raw) : Number.NaN;
|
|
109
|
+
const minutes = Number.isFinite(value) && value >= 0 ? value : DEFAULT_OWNER_WINDOW_MINUTES;
|
|
110
|
+
return new Date(now - minutes * 60_000).toISOString();
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
async function leadMatch(reachable: Agent[], matchedText: string): Promise<ChatAgentMatch> {
|
|
95
114
|
const lead = reachable.find((agent) => agent.isLead) ?? (await getLeadAgent());
|
|
96
115
|
return { agent: lead, reason: "skill", matchedText };
|
|
@@ -99,7 +118,8 @@ async function leadMatch(reachable: Agent[], matchedText: string): Promise<ChatA
|
|
|
99
118
|
/**
|
|
100
119
|
* Port of `src/slack/router.ts` for Chat SDK threads. Order: `swarm#<uuid>`,
|
|
101
120
|
* `swarm#all`, KV channel mapping (agent, or workflow for a mention or DM), the
|
|
102
|
-
* thread owner (
|
|
121
|
+
* thread owner (inside its window in a Telegram group; lead when the owner is
|
|
122
|
+
* offline), the lead for a mention or DM.
|
|
103
123
|
* Returns null when the message is not addressed to the swarm.
|
|
104
124
|
*/
|
|
105
125
|
export async function routeChatMessage(
|
|
@@ -148,7 +168,13 @@ export async function routeChatMessage(
|
|
|
148
168
|
);
|
|
149
169
|
}
|
|
150
170
|
|
|
151
|
-
|
|
171
|
+
// Only a Telegram group expires its owner: one shared conversation, where a
|
|
172
|
+
// stale owner would catch the next topic. DMs and threads keep theirs.
|
|
173
|
+
const owner = await getAgentWorkingOnContextKey(
|
|
174
|
+
chatSdkContextKey(ctx.threadId),
|
|
175
|
+
"chat",
|
|
176
|
+
ctx.adapter === "telegram" && !ctx.isDirectMessage ? chatOwnerWindowStart() : undefined,
|
|
177
|
+
);
|
|
152
178
|
if (owner && (await isAgentReachable(owner))) {
|
|
153
179
|
return {
|
|
154
180
|
kind: "agents",
|
|
@@ -14,15 +14,23 @@ export interface ChatThreadSteeringResult {
|
|
|
14
14
|
* rule as the Slack default (`SLACK_THREAD_STEERING=lead`, queue mode) as
|
|
15
15
|
* constants: `isSteeringEnabled()` is the only switch. Returns null when the
|
|
16
16
|
* caller should buffer the message into a follow-up task instead.
|
|
17
|
+
*
|
|
18
|
+
* Only the task's own requester steers it: a steering row carries the task's
|
|
19
|
+
* attribution, and an undeliverable one is promoted to a follow-up of this
|
|
20
|
+
* task. `resolveSender` returns the sender's canonical user, undefined when
|
|
21
|
+
* unknown; it runs only when there is a task to steer.
|
|
17
22
|
*/
|
|
18
23
|
export async function requestChatThreadSteering(args: {
|
|
19
24
|
contextKey: string;
|
|
20
25
|
message: string;
|
|
26
|
+
resolveSender: () => Promise<string | undefined>;
|
|
21
27
|
}): Promise<ChatThreadSteeringResult | null> {
|
|
22
28
|
if (!isSteeringEnabled()) return null;
|
|
23
29
|
|
|
24
30
|
const task = await getLatestLeadTaskByContextKey(args.contextKey, "chat");
|
|
25
31
|
if (!task || task.status !== "in_progress") return null;
|
|
32
|
+
const senderUserId = await args.resolveSender();
|
|
33
|
+
if (senderUserId !== task.requestedByUserId) return null;
|
|
26
34
|
|
|
27
35
|
try {
|
|
28
36
|
const result = await requestSteering({
|
|
@@ -32,6 +40,7 @@ export async function requestChatThreadSteering(args: {
|
|
|
32
40
|
onUnsupported: "degrade",
|
|
33
41
|
source: "chat",
|
|
34
42
|
createdByKind: "user",
|
|
43
|
+
createdByUserId: senderUserId,
|
|
35
44
|
});
|
|
36
45
|
return { task, result };
|
|
37
46
|
} catch (error) {
|
|
@@ -14,3 +14,17 @@ registerTemplate({
|
|
|
14
14
|
],
|
|
15
15
|
category: "system",
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
registerTemplate({
|
|
19
|
+
eventType: "chat.message.header",
|
|
20
|
+
header: "",
|
|
21
|
+
defaultBody: "[chat] {{chat_facts}}",
|
|
22
|
+
variables: [
|
|
23
|
+
{
|
|
24
|
+
name: "chat_facts",
|
|
25
|
+
description:
|
|
26
|
+
"Adapter, chat type and title, forum topic, sender, why the message was taken (addressed) and the replied-to snippet, joined with ' · '",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
category: "system",
|
|
30
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Attachment } from "chat";
|
|
2
2
|
import { type AdditiveBuffer, createAdditiveBuffer } from "@/tasks/additive-buffer";
|
|
3
3
|
import { trackBackground } from "./background";
|
|
4
|
+
import type { ChatAddressedReason, ChatMessageFacts } from "./facts";
|
|
4
5
|
import { flushChatThread } from "./inbound";
|
|
5
6
|
import { stopTypingKeepalive } from "./typing";
|
|
6
7
|
|
|
@@ -14,6 +15,9 @@ export interface ChatBufferedMessage {
|
|
|
14
15
|
userName?: string;
|
|
15
16
|
botMentioned: boolean;
|
|
16
17
|
isDirectMessage: boolean;
|
|
18
|
+
/** Why the message was taken; `none` when it is unaddressed. */
|
|
19
|
+
addressed: ChatAddressedReason;
|
|
20
|
+
facts: ChatMessageFacts;
|
|
17
21
|
attachments: Attachment[];
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input caps shared by both MCP surfaces (`/mcp` and `/mcp-user`).
|
|
3
|
+
*
|
|
4
|
+
* The SDK leaves both bounds off when you build the server yourself
|
|
5
|
+
* (GHSA-46jr-4wx8-v453): `McpServer` only counts `tools/call` arguments when
|
|
6
|
+
* `maxToolInputElements` is set, and our HTTP handlers read the request body
|
|
7
|
+
* themselves, so the transport's own body cap never runs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Largest combined number of array elements and object members a single
|
|
12
|
+
* `tools/call` `arguments` payload may contain. Sized for the largest
|
|
13
|
+
* legitimate call: `app-rows-create` takes up to 500 rows, which is about
|
|
14
|
+
* 24,000 elements at 48 fields per row. Script SDK calls go through
|
|
15
|
+
* `/api/mcp-bridge`, which does not apply it.
|
|
16
|
+
*/
|
|
17
|
+
export const MCP_MAX_TOOL_INPUT_ELEMENTS = 25_000;
|
|
18
|
+
|
|
19
|
+
/** Largest MCP HTTP request body we read, in bytes. Matches the SDK default (4 MiB). */
|
|
20
|
+
export const MCP_MAX_REQUEST_BODY_BYTES = 4 * 1024 * 1024;
|
package/src/pages/browser-sdk.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Exposes a domain-grouped API on `window.SwarmSDK` (class) and a ready-to-use
|
|
4
4
|
// singleton `window.swarmSdk`. All calls route through the `/@swarm/api/*`
|
|
5
|
-
// proxy, which strips the page
|
|
5
|
+
// proxy, which strips the page's own session cookie and forwards to `/api/*` with
|
|
6
6
|
// a server-side bearer and signed page session. Memory operations use the
|
|
7
7
|
// page owner's agent scope. Viewer identity and permissions remain separate.
|
|
8
8
|
// The browser never handles the server bearer.
|
|
@@ -30,10 +30,15 @@ class SwarmSDK {
|
|
|
30
30
|
this.channel = async (name) => (await loadRealtime()).channel(name);
|
|
31
31
|
|
|
32
32
|
const base = '/@swarm/api';
|
|
33
|
+
// Each page has its own session cookie. Naming the page tells the proxy
|
|
34
|
+
// which one to read; the cookie's signed payload must still match it.
|
|
35
|
+
const pathParts = typeof location === 'undefined' ? [] : location.pathname.split('/');
|
|
36
|
+
const pageId = pathParts[1] === 'p' && pathParts[2] ? pathParts[2].split('.')[0] : '';
|
|
33
37
|
const call = async (method, path, body) => {
|
|
34
|
-
const init = { method };
|
|
38
|
+
const init = { method, headers: {} };
|
|
39
|
+
if (pageId) init.headers['X-Swarm-Page-Id'] = pageId;
|
|
35
40
|
if (body !== undefined) {
|
|
36
|
-
init.headers
|
|
41
|
+
init.headers['Content-Type'] = 'application/json';
|
|
37
42
|
init.body = JSON.stringify(body);
|
|
38
43
|
}
|
|
39
44
|
const res = await fetch(base + path, init);
|
|
@@ -415,7 +415,7 @@ const FEEDBACK_OVERLAY_JS = `
|
|
|
415
415
|
return fetch('/@swarm/api/pages/' + encodeURIComponent(pageId) + '/feedback', {
|
|
416
416
|
method: 'POST',
|
|
417
417
|
credentials: 'same-origin',
|
|
418
|
-
headers: { 'Content-Type': 'application/json' },
|
|
418
|
+
headers: { 'Content-Type': 'application/json', 'X-Swarm-Page-Id': pageId },
|
|
419
419
|
body: JSON.stringify(payload),
|
|
420
420
|
}).then(function (res) {
|
|
421
421
|
return res.text().then(function (text) {
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
// `/unstable` keeps the full ACP v1 surface the 1.x default import carried:
|
|
2
|
+
// turn `usage` on the prompt response is unstable, and the stable entrypoint
|
|
3
|
+
// does not type it. Both entrypoints speak ACP v1 on the wire.
|
|
1
4
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
client as acpClient,
|
|
6
|
+
type ClientContext,
|
|
4
7
|
type McpServer,
|
|
8
|
+
methods,
|
|
5
9
|
ndJsonStream,
|
|
6
10
|
PROTOCOL_VERSION,
|
|
7
11
|
type RequestPermissionRequest,
|
|
8
12
|
type RequestPermissionResponse,
|
|
9
13
|
type SessionConfigOption,
|
|
10
14
|
type SessionNotification,
|
|
15
|
+
type SetSessionConfigOptionRequest,
|
|
16
|
+
type SetSessionConfigOptionResponse,
|
|
11
17
|
type Usage,
|
|
12
|
-
} from "@agentclientprotocol/sdk";
|
|
18
|
+
} from "@agentclientprotocol/sdk/unstable";
|
|
13
19
|
import pkg from "../../package.json";
|
|
14
20
|
import type { AcpSessionConfigOption } from "../types";
|
|
15
21
|
import { mintAcpSessionToken, revokeAcpSessionToken } from "../utils/acp-session-token";
|
|
@@ -154,7 +160,7 @@ function boundedAcpLogScalar(value: unknown): unknown {
|
|
|
154
160
|
return typeof value === "string" ? value.slice(0, 500) : value;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
class SwarmAcpClient
|
|
163
|
+
class SwarmAcpClient {
|
|
158
164
|
/** Latest cumulative USD cost the target reported for the session. */
|
|
159
165
|
reportedCostUsd: number | null = null;
|
|
160
166
|
|
|
@@ -199,7 +205,7 @@ class ACPSession implements ProviderSession {
|
|
|
199
205
|
private ephemeralTokenId: string | null = null;
|
|
200
206
|
|
|
201
207
|
constructor(
|
|
202
|
-
private readonly
|
|
208
|
+
private readonly agent: ClientContext,
|
|
203
209
|
private readonly client: SwarmAcpClient,
|
|
204
210
|
private readonly process: Bun.Subprocess<"pipe", "pipe", "pipe">,
|
|
205
211
|
private readonly config: ProviderSessionConfig,
|
|
@@ -234,7 +240,7 @@ class ACPSession implements ProviderSession {
|
|
|
234
240
|
if (this.aborted) return;
|
|
235
241
|
this.aborted = true;
|
|
236
242
|
try {
|
|
237
|
-
await this.
|
|
243
|
+
await this.agent.notify(methods.agent.session.cancel, { sessionId: this.sessionId });
|
|
238
244
|
await Promise.race([this.completionPromise, Bun.sleep(ACP_CANCEL_SETTLE_MS)]);
|
|
239
245
|
} catch (err) {
|
|
240
246
|
this.emit({
|
|
@@ -282,7 +288,7 @@ class ACPSession implements ProviderSession {
|
|
|
282
288
|
private async runPrompt(): Promise<void> {
|
|
283
289
|
let result: ProviderResult;
|
|
284
290
|
try {
|
|
285
|
-
const response = await this.
|
|
291
|
+
const response = await this.agent.request(methods.agent.session.prompt, {
|
|
286
292
|
sessionId: this.sessionId,
|
|
287
293
|
prompt: [{ type: "text", text: this.config.prompt }],
|
|
288
294
|
});
|
|
@@ -441,11 +447,16 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
441
447
|
else preSessionEvents.push(event);
|
|
442
448
|
}, target);
|
|
443
449
|
const stream = ndJsonStream(fileSinkWritableStream(proc.stdin), proc.stdout);
|
|
444
|
-
const
|
|
450
|
+
const agent = acpClient()
|
|
451
|
+
.onRequest(methods.client.session.requestPermission, ({ params }) =>
|
|
452
|
+
client.requestPermission(params),
|
|
453
|
+
)
|
|
454
|
+
.onNotification(methods.client.session.update, ({ params }) => client.sessionUpdate(params))
|
|
455
|
+
.connect(stream).agent;
|
|
445
456
|
|
|
446
457
|
let ephemeralToken: { tokenId: string; plaintext: string } | null = null;
|
|
447
458
|
try {
|
|
448
|
-
await
|
|
459
|
+
await agent.request(methods.agent.initialize, {
|
|
449
460
|
protocolVersion: PROTOCOL_VERSION,
|
|
450
461
|
clientInfo: { name: "agent-swarm", version: pkg.version },
|
|
451
462
|
clientCapabilities: { session: { configOptions: { boolean: {} } } },
|
|
@@ -473,7 +484,7 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
473
484
|
// shape for an ACP agent with no network route to the swarm API. Gated on
|
|
474
485
|
// `mcpCapabilities.acp` and UNSTABLE; not adopted here.
|
|
475
486
|
const sessionMeta = target.sessionMeta?.(config);
|
|
476
|
-
const newSession = await
|
|
487
|
+
const newSession = await agent.request(methods.agent.session.new, {
|
|
477
488
|
...(sessionMeta ? { _meta: sessionMeta } : {}),
|
|
478
489
|
cwd: config.cwd,
|
|
479
490
|
mcpServers: [
|
|
@@ -491,13 +502,16 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
491
502
|
],
|
|
492
503
|
});
|
|
493
504
|
const configOptions = await applyConfiguredOptions(
|
|
494
|
-
|
|
505
|
+
{
|
|
506
|
+
setSessionConfigOption: (params) =>
|
|
507
|
+
agent.request(methods.agent.session.setConfigOption, params),
|
|
508
|
+
},
|
|
495
509
|
newSession.sessionId,
|
|
496
510
|
newSession.configOptions ?? [],
|
|
497
511
|
target.configuredOptions(config),
|
|
498
512
|
);
|
|
499
513
|
session = new ACPSession(
|
|
500
|
-
|
|
514
|
+
agent,
|
|
501
515
|
client,
|
|
502
516
|
proc,
|
|
503
517
|
config,
|
|
@@ -537,8 +551,14 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
537
551
|
}
|
|
538
552
|
}
|
|
539
553
|
|
|
554
|
+
export interface AcpConfigOptionSetter {
|
|
555
|
+
setSessionConfigOption(
|
|
556
|
+
params: SetSessionConfigOptionRequest,
|
|
557
|
+
): Promise<SetSessionConfigOptionResponse>;
|
|
558
|
+
}
|
|
559
|
+
|
|
540
560
|
export async function applyConfiguredOptions(
|
|
541
|
-
connection:
|
|
561
|
+
connection: AcpConfigOptionSetter,
|
|
542
562
|
sessionId: string,
|
|
543
563
|
advertised: SessionConfigOption[],
|
|
544
564
|
configured: Record<string, string | boolean>,
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
* default; the image installs exactly this version and the per-task settings
|
|
45
45
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
46
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
47
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1792584044-g15483c";
|
|
48
48
|
|
|
49
49
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
50
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|