@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Message, Thread } from "chat";
|
|
2
|
+
|
|
3
|
+
/** Why a message counts as meant for the swarm. `none`: it reached the router unaddressed. */
|
|
4
|
+
export type ChatAddressedReason =
|
|
5
|
+
| "dm"
|
|
6
|
+
| "mention"
|
|
7
|
+
| "reply-to-bot"
|
|
8
|
+
| "requester-follow-up"
|
|
9
|
+
| "none";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Facts about one inbound message that only core can read: the chat it came
|
|
13
|
+
* from (Telegram `message.raw.chat`) and whether it answers the bot.
|
|
14
|
+
*/
|
|
15
|
+
export interface ChatMessageFacts {
|
|
16
|
+
/** Platform chat type, such as Telegram `private`, `group` or `supergroup`. */
|
|
17
|
+
chatType?: string;
|
|
18
|
+
isForum?: boolean;
|
|
19
|
+
/** Group or channel title. */
|
|
20
|
+
title?: string;
|
|
21
|
+
/** Forum topic: `"<name>"` when the update carries it, else `#<thread id>`. */
|
|
22
|
+
topic?: string;
|
|
23
|
+
/** The sender as people know them: `Name (@username)`. */
|
|
24
|
+
from: string;
|
|
25
|
+
/** The message replies to one of the bot's own messages. */
|
|
26
|
+
replyToBot: boolean;
|
|
27
|
+
/** Start of the message it replies to. */
|
|
28
|
+
replyToSnippet?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SNIPPET_MAX = 80;
|
|
32
|
+
|
|
33
|
+
interface TelegramRawMessage {
|
|
34
|
+
message_id?: number;
|
|
35
|
+
message_thread_id?: number;
|
|
36
|
+
is_topic_message?: boolean;
|
|
37
|
+
chat?: { type?: string; title?: string; is_forum?: boolean };
|
|
38
|
+
from?: { first_name?: string; last_name?: string; username?: string };
|
|
39
|
+
reply_to_message?: {
|
|
40
|
+
message_id?: number;
|
|
41
|
+
from?: { id?: number | string };
|
|
42
|
+
text?: string;
|
|
43
|
+
caption?: string;
|
|
44
|
+
forum_topic_created?: { name?: string };
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function snippet(text: string | undefined): string | undefined {
|
|
49
|
+
const flat = text?.replace(/\s+/g, " ").trim();
|
|
50
|
+
if (!flat) return undefined;
|
|
51
|
+
return flat.length > SNIPPET_MAX ? `${flat.slice(0, SNIPPET_MAX)}...` : flat;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function personLabel(name: string | undefined, username: string | undefined, fallback: string) {
|
|
55
|
+
if (name && username && name !== username) return `${name} (@${username})`;
|
|
56
|
+
if (username) return `@${username}`;
|
|
57
|
+
return name || fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function telegramFacts(raw: TelegramRawMessage, message: Message, botUserId?: string) {
|
|
61
|
+
const reply = raw.reply_to_message;
|
|
62
|
+
// In a forum topic every plain message replies to the topic's creation
|
|
63
|
+
// message, so that reply is the topic, not an answer to anyone.
|
|
64
|
+
const replyIsTopicRoot =
|
|
65
|
+
reply !== undefined &&
|
|
66
|
+
(reply.forum_topic_created !== undefined ||
|
|
67
|
+
(raw.is_topic_message === true && reply.message_id === raw.message_thread_id));
|
|
68
|
+
const realReply = reply && !replyIsTopicRoot ? reply : undefined;
|
|
69
|
+
const topicName = replyIsTopicRoot ? reply?.forum_topic_created?.name : undefined;
|
|
70
|
+
const name = [raw.from?.first_name, raw.from?.last_name].filter(Boolean).join(" ").trim();
|
|
71
|
+
return {
|
|
72
|
+
chatType: raw.chat?.type,
|
|
73
|
+
isForum: raw.chat?.is_forum === true ? true : undefined,
|
|
74
|
+
title: raw.chat?.title,
|
|
75
|
+
topic:
|
|
76
|
+
raw.is_topic_message === true && raw.message_thread_id !== undefined
|
|
77
|
+
? topicName
|
|
78
|
+
? `"${topicName}"`
|
|
79
|
+
: `#${raw.message_thread_id}`
|
|
80
|
+
: undefined,
|
|
81
|
+
from: personLabel(name || undefined, raw.from?.username, message.author.userId),
|
|
82
|
+
replyToBot:
|
|
83
|
+
realReply?.from?.id !== undefined &&
|
|
84
|
+
botUserId !== undefined &&
|
|
85
|
+
String(realReply.from.id) === botUserId,
|
|
86
|
+
replyToSnippet: snippet(realReply?.text ?? realReply?.caption),
|
|
87
|
+
} satisfies ChatMessageFacts;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Read the chat facts of one message. Telegram reads the raw update; other adapters the SDK message. */
|
|
91
|
+
export function readChatMessageFacts(thread: Thread, message: Message): ChatMessageFacts {
|
|
92
|
+
const raw = message.raw as TelegramRawMessage | undefined;
|
|
93
|
+
if (thread.adapter.name === "telegram" && raw && typeof raw === "object") {
|
|
94
|
+
return telegramFacts(raw, message, thread.adapter.botUserId);
|
|
95
|
+
}
|
|
96
|
+
const { fullName, userName, userId } = message.author;
|
|
97
|
+
return {
|
|
98
|
+
chatType: thread.isDM ? "dm" : undefined,
|
|
99
|
+
from: personLabel(fullName || undefined, userName || undefined, userId),
|
|
100
|
+
replyToBot: message.replyTo?.author.isMe === true,
|
|
101
|
+
replyToSnippet: snippet(message.replyTo?.text),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One line of facts for the task header: `telegram · supergroup "Life" · from: ... · addressed: ...`. */
|
|
106
|
+
export function formatChatHeaderFacts(
|
|
107
|
+
adapter: string,
|
|
108
|
+
facts: ChatMessageFacts,
|
|
109
|
+
addressed: ChatAddressedReason,
|
|
110
|
+
): string {
|
|
111
|
+
// Title, topic and sender are user-set: one line each, so none can forge a header line.
|
|
112
|
+
const oneLine = (text: string) => text.replace(/\s+/g, " ").trim();
|
|
113
|
+
const parts = [adapter];
|
|
114
|
+
if (facts.chatType)
|
|
115
|
+
parts.push(facts.title ? `${facts.chatType} "${oneLine(facts.title)}"` : facts.chatType);
|
|
116
|
+
if (facts.isForum) parts.push(facts.topic ? `forum topic ${oneLine(facts.topic)}` : "forum");
|
|
117
|
+
parts.push(`from: ${oneLine(facts.from)}`, `addressed: ${addressed}`);
|
|
118
|
+
if (facts.replyToSnippet) parts.push(`reply-to: "${facts.replyToSnippet}"`);
|
|
119
|
+
return parts.join(" · ");
|
|
120
|
+
}
|
|
@@ -2,12 +2,15 @@ import type { Adapter, Message, Thread } from "chat";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
getAgentById,
|
|
5
|
+
getAgentWorkingOnContextKey,
|
|
5
6
|
getAllAgents,
|
|
6
7
|
getLatestActiveTaskByContextKey,
|
|
7
8
|
getLatestTaskByContextKey,
|
|
8
9
|
getLeadAgent,
|
|
9
10
|
isExtensionAgent,
|
|
10
11
|
} from "@/be/db";
|
|
12
|
+
import { resolveIdentity } from "@/be/identity";
|
|
13
|
+
import { recordUnmappedIdentity } from "@/be/unmapped-identities";
|
|
11
14
|
import { dispatchPre } from "@/extensions/dispatcher";
|
|
12
15
|
import { resolveTemplate } from "@/prompts/resolver";
|
|
13
16
|
import { chatSdkContextKey } from "@/tasks/context-key";
|
|
@@ -21,19 +24,25 @@ import {
|
|
|
21
24
|
buildChatAttachmentText,
|
|
22
25
|
createChatTaskWithFiles,
|
|
23
26
|
fetchChatFiles,
|
|
27
|
+
formatTooLargeReply,
|
|
24
28
|
toChatFiles,
|
|
29
|
+
transcribeChatAudio,
|
|
25
30
|
} from "./attachments";
|
|
26
31
|
import { getChatThreadResetAt } from "./commands";
|
|
32
|
+
import { type ChatAddressedReason, formatChatHeaderFacts, readChatMessageFacts } from "./facts";
|
|
27
33
|
import { errorMessage } from "./logger";
|
|
28
|
-
import { emitChatMessageSent } from "./outbound";
|
|
34
|
+
import { emitChatMessageSent, postChatMessage } from "./outbound";
|
|
29
35
|
import {
|
|
30
36
|
type ChatAgentMatch,
|
|
31
37
|
type ChatRouteTarget,
|
|
38
|
+
chatOwnerWindowStart,
|
|
39
|
+
getChatChannelMapping,
|
|
40
|
+
hasRoutingToken,
|
|
32
41
|
routeChatMessage,
|
|
33
42
|
stripRoutingTokens,
|
|
34
43
|
} from "./router";
|
|
35
44
|
import { getChatInstance } from "./runtime";
|
|
36
|
-
import { readPersistedThreadHistory } from "./state";
|
|
45
|
+
import { readPersistedThreadHistory, SqliteChatState } from "./state";
|
|
37
46
|
import { formatChatSteeringAck, requestChatThreadSteering } from "./steering";
|
|
38
47
|
import {
|
|
39
48
|
bufferChatMessage,
|
|
@@ -109,11 +118,37 @@ export async function handleChatInbound(
|
|
|
109
118
|
if (immediate) text = text.replace(/^!now\s*/i, "").trim();
|
|
110
119
|
const attachments = message.attachments ?? [];
|
|
111
120
|
|
|
121
|
+
const facts = readChatMessageFacts(thread, message);
|
|
122
|
+
const isDirectMessage = kind === "dm" || thread.isDM;
|
|
123
|
+
const mentioned = kind === "mention" || message.isMention === true;
|
|
124
|
+
let addressed: ChatAddressedReason = isDirectMessage
|
|
125
|
+
? "dm"
|
|
126
|
+
: facts.replyToBot
|
|
127
|
+
? "reply-to-bot"
|
|
128
|
+
: mentioned
|
|
129
|
+
? "mention"
|
|
130
|
+
: "none";
|
|
131
|
+
// A Telegram group is one shared conversation, not a thread about the task:
|
|
132
|
+
// an unaddressed message there is side chatter unless its sender asked for
|
|
133
|
+
// the thread's open task. Chatter neither steers nor reaches the router.
|
|
134
|
+
if (addressed === "none" && (facts.chatType === "group" || facts.chatType === "supergroup")) {
|
|
135
|
+
const unaddressed = await classifyUnaddressedGroupMessage(thread, message.author.userId, text);
|
|
136
|
+
if (unaddressed === "chatter") {
|
|
137
|
+
console.log(
|
|
138
|
+
`[chat-sdk] ${thread.id}: unaddressed group message from a non-requester, ignored`,
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (unaddressed === "requester-follow-up") addressed = unaddressed;
|
|
143
|
+
}
|
|
144
|
+
|
|
112
145
|
// A thread with messages already waiting keeps its order: append, do not steer past them.
|
|
146
|
+
// Another sender's message takes the buffer, so it gets its own header and requester.
|
|
113
147
|
if (!immediate && attachments.length === 0 && text && !isChatThreadBuffered(thread.id)) {
|
|
114
148
|
const steering = await requestChatThreadSteering({
|
|
115
149
|
contextKey: chatSdkContextKey(thread.id),
|
|
116
150
|
message: text,
|
|
151
|
+
resolveSender: () => resolveChatSender(adapter, message.author.userId),
|
|
117
152
|
});
|
|
118
153
|
if (steering) {
|
|
119
154
|
console.log(
|
|
@@ -148,8 +183,10 @@ export async function handleChatInbound(
|
|
|
148
183
|
text,
|
|
149
184
|
userId: message.author.userId,
|
|
150
185
|
userName: message.author.userName || undefined,
|
|
151
|
-
botMentioned:
|
|
152
|
-
isDirectMessage
|
|
186
|
+
botMentioned: mentioned || facts.replyToBot,
|
|
187
|
+
isDirectMessage,
|
|
188
|
+
addressed,
|
|
189
|
+
facts,
|
|
153
190
|
attachments,
|
|
154
191
|
});
|
|
155
192
|
// Subscribed-thread messages may be dropped by the router at flush time;
|
|
@@ -159,6 +196,76 @@ export async function handleChatInbound(
|
|
|
159
196
|
if (immediate) await instantFlushChat(thread.id);
|
|
160
197
|
}
|
|
161
198
|
|
|
199
|
+
const REQUESTER_KEY_PREFIX = "swarm:requester:";
|
|
200
|
+
const REQUESTER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
201
|
+
|
|
202
|
+
/** Platform user id of whoever asked for the thread's latest task. */
|
|
203
|
+
async function rememberChatThreadRequester(threadId: string, userId: string): Promise<void> {
|
|
204
|
+
try {
|
|
205
|
+
await new SqliteChatState().set(`${REQUESTER_KEY_PREFIX}${threadId}`, userId, REQUESTER_TTL_MS);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`[chat-sdk] Could not store the thread requester: ${errorMessage(error)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* An unaddressed message outside a DM. `routable`: a routing token or an
|
|
213
|
+
* agent channel mapping routes it whoever sent it. `requester-follow-up`: its
|
|
214
|
+
* sender asked for the thread's task and the owner window is open. Anything
|
|
215
|
+
* else is `chatter`.
|
|
216
|
+
*/
|
|
217
|
+
async function classifyUnaddressedGroupMessage(
|
|
218
|
+
thread: Thread,
|
|
219
|
+
senderId: string,
|
|
220
|
+
text: string,
|
|
221
|
+
): Promise<"routable" | "requester-follow-up" | "chatter"> {
|
|
222
|
+
if (hasRoutingToken(text)) return "routable";
|
|
223
|
+
if ((await getChatChannelMapping(thread.adapter.name, thread.channelId))?.agentId) {
|
|
224
|
+
return "routable";
|
|
225
|
+
}
|
|
226
|
+
const owner = await getAgentWorkingOnContextKey(
|
|
227
|
+
chatSdkContextKey(thread.id),
|
|
228
|
+
"chat",
|
|
229
|
+
chatOwnerWindowStart(),
|
|
230
|
+
);
|
|
231
|
+
if (!owner) return "chatter";
|
|
232
|
+
const requester = await new SqliteChatState().get<string>(`${REQUESTER_KEY_PREFIX}${thread.id}`);
|
|
233
|
+
return requester === senderId ? "requester-follow-up" : "chatter";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Canonical user behind a chat sender, through the same identity registry as
|
|
238
|
+
* Slack: `(<adapter>, <platform user id>)` in `user_external_ids`. An unknown
|
|
239
|
+
* sender goes to the Unmapped tracker and the task has no requester.
|
|
240
|
+
*/
|
|
241
|
+
async function resolveChatRequester(
|
|
242
|
+
adapter: string,
|
|
243
|
+
item: ChatBufferedMessage,
|
|
244
|
+
): Promise<string | undefined> {
|
|
245
|
+
return resolveChatSender(adapter, item.userId, {
|
|
246
|
+
sampleEventType: "chat.message",
|
|
247
|
+
sampleContext: `${item.facts.from}: ${item.text}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** {@link resolveChatRequester} for a bare sender id; records it as unmapped only when `sample` is set. */
|
|
252
|
+
async function resolveChatSender(
|
|
253
|
+
adapter: string,
|
|
254
|
+
userId: string,
|
|
255
|
+
sample?: { sampleEventType: string; sampleContext: string },
|
|
256
|
+
): Promise<string | undefined> {
|
|
257
|
+
// Telegram `sender_chat` posts (anonymous admins, linked channels) are no person.
|
|
258
|
+
if (!userId || userId.startsWith("chat:")) return undefined;
|
|
259
|
+
try {
|
|
260
|
+
const resolution = await resolveIdentity(adapter, userId);
|
|
261
|
+
if (resolution.status === "resolved") return resolution.userId;
|
|
262
|
+
if (sample) await recordUnmappedIdentity(adapter, userId, sample);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.warn(`[chat-sdk] Could not resolve the requester: ${errorMessage(error)}`);
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
162
269
|
/**
|
|
163
270
|
* Earlier messages of the thread for the task description, minus the flushed
|
|
164
271
|
* ones and anything before the last `/reset`. The adapter's view is merged with
|
|
@@ -265,6 +372,11 @@ export async function flushChatThread(
|
|
|
265
372
|
.join("\n---\n");
|
|
266
373
|
const botMentioned = items.some((item) => item.botMentioned);
|
|
267
374
|
const isDirectMessage = items.some((item) => item.isDirectMessage);
|
|
375
|
+
// The task's requester: the first sender who addressed the swarm.
|
|
376
|
+
const requester = items.find((item) => item.addressed !== "none") ?? first;
|
|
377
|
+
// One task carries every sender's text, so a batch from several people
|
|
378
|
+
// gets no requester: one sender's identity must not vouch for another's words.
|
|
379
|
+
const otherSenders = new Set(items.map((item) => item.userId)).size - 1;
|
|
268
380
|
|
|
269
381
|
// After `/reset` the thread's earlier tasks are not context: the new task gets
|
|
270
382
|
// no parent and no thread history older than the reset.
|
|
@@ -285,6 +397,9 @@ export async function flushChatThread(
|
|
|
285
397
|
text: rawText,
|
|
286
398
|
botMentioned,
|
|
287
399
|
isDirectMessage,
|
|
400
|
+
...(first.facts.chatType ? { chatType: first.facts.chatType } : {}),
|
|
401
|
+
...(first.facts.isForum ? { isForum: true } : {}),
|
|
402
|
+
replyToBot: items.some((item) => item.facts.replyToBot),
|
|
288
403
|
...(mostRecentTask ? { threadContext: { adapter, threadId } } : {}),
|
|
289
404
|
},
|
|
290
405
|
{
|
|
@@ -354,7 +469,19 @@ export async function flushChatThread(
|
|
|
354
469
|
const body = stripRoutingTokens(rawText);
|
|
355
470
|
const files = items.flatMap((item) => toChatFiles(adapter, item.messageId, item.attachments));
|
|
356
471
|
await using inbound = await fetchChatFiles(files);
|
|
357
|
-
const
|
|
472
|
+
const tooLarge = inbound.failed.filter((failure) => failure.tooLarge);
|
|
473
|
+
if (tooLarge.length > 0) {
|
|
474
|
+
// The user would otherwise never learn the file was dropped.
|
|
475
|
+
try {
|
|
476
|
+
await postChatMessage({ adapter, threadId, text: formatTooLargeReply(tooLarge) });
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.warn(
|
|
479
|
+
`[chat-sdk] ${threadId}: could not post the too-large reply: ${errorMessage(error)}`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const notes = await transcribeChatAudio(inbound.fetched);
|
|
484
|
+
const fileText = buildChatAttachmentText(inbound.files, inbound.failed, notes);
|
|
358
485
|
const combined = [body, fileText].filter(Boolean).join("\n\n");
|
|
359
486
|
const marked =
|
|
360
487
|
items.length > 1
|
|
@@ -364,9 +491,26 @@ export async function flushChatThread(
|
|
|
364
491
|
mostRecentTask && thread
|
|
365
492
|
? await getChatThreadContext(thread, new Set(items.map((item) => item.messageId)), resetAt)
|
|
366
493
|
: "";
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
494
|
+
const header = resolveTemplate("chat.message.header", {
|
|
495
|
+
chat_facts: formatChatHeaderFacts(
|
|
496
|
+
adapter,
|
|
497
|
+
otherSenders > 0
|
|
498
|
+
? { ...requester.facts, from: `${requester.facts.from} +${otherSenders} more` }
|
|
499
|
+
: requester.facts,
|
|
500
|
+
requester.addressed,
|
|
501
|
+
),
|
|
502
|
+
}).text;
|
|
503
|
+
const description = [
|
|
504
|
+
header,
|
|
505
|
+
threadContext
|
|
506
|
+
? resolveTemplate("chat.message.thread_context", { thread_messages: threadContext }).text
|
|
507
|
+
: "",
|
|
508
|
+
marked,
|
|
509
|
+
]
|
|
510
|
+
.filter(Boolean)
|
|
511
|
+
.join("\n\n");
|
|
512
|
+
const requestedByUserId =
|
|
513
|
+
otherSenders > 0 ? undefined : await resolveChatRequester(adapter, requester);
|
|
370
514
|
|
|
371
515
|
const latestActive = await getLatestActiveTaskByContextKey(contextKey, "chat");
|
|
372
516
|
const dependsOn = !immediate && latestActive ? [latestActive.id] : undefined;
|
|
@@ -382,7 +526,10 @@ export async function flushChatThread(
|
|
|
382
526
|
priority: 70,
|
|
383
527
|
contextKey,
|
|
384
528
|
dependsOn,
|
|
529
|
+
requestedByUserId,
|
|
385
530
|
parentTaskId: mostRecentTask?.id,
|
|
531
|
+
// The parent's requester asked an earlier message: an unknown sender stays unattributed.
|
|
532
|
+
inheritParentRequester: false,
|
|
386
533
|
// A human message in the thread: never answer it in the previous task's
|
|
387
534
|
// structured-output contract.
|
|
388
535
|
inheritParentOutputSchema: false,
|
|
@@ -404,6 +551,7 @@ export async function flushChatThread(
|
|
|
404
551
|
}
|
|
405
552
|
}
|
|
406
553
|
if (created > 0) {
|
|
554
|
+
await rememberChatThreadRequester(threadId, requester.userId);
|
|
407
555
|
await subscribe(thread);
|
|
408
556
|
// Subscribed-thread messages start typing only here, once a task exists.
|
|
409
557
|
startTypingKeepalive(adapter, threadId);
|
|
@@ -19,6 +19,30 @@ export function chatMessageLimit(adapter: string): number {
|
|
|
19
19
|
return CHAT_MESSAGE_LIMITS[adapter] ?? FALLBACK_LIMIT;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const MB = 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per-file outbound upload ceiling, in bytes. Every adapter is capped at
|
|
26
|
+
* 50 MB, the task attachment cap, because a file is read into memory before
|
|
27
|
+
* it is sent. Telegram's Bot API takes 50 MB per upload (photos 10 MB, see
|
|
28
|
+
* `TELEGRAM_PHOTO_LIMIT`); Discord takes 10 MB without a server boost.
|
|
29
|
+
*/
|
|
30
|
+
const CHAT_UPLOAD_LIMITS: Record<string, number> = {
|
|
31
|
+
telegram: 50 * MB,
|
|
32
|
+
discord: 10 * MB,
|
|
33
|
+
};
|
|
34
|
+
const FALLBACK_UPLOAD_LIMIT = 50 * MB;
|
|
35
|
+
|
|
36
|
+
/** Telegram sends a larger image with `sendDocument` instead of `sendPhoto`. */
|
|
37
|
+
export const TELEGRAM_PHOTO_LIMIT = 10 * MB;
|
|
38
|
+
|
|
39
|
+
export function chatUploadLimit(adapter: string): number {
|
|
40
|
+
return CHAT_UPLOAD_LIMITS[adapter] ?? FALLBACK_UPLOAD_LIMIT;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Most files one platform message can carry (Telegram media groups, Discord). */
|
|
44
|
+
export const CHAT_FILES_PER_MESSAGE = 10;
|
|
45
|
+
|
|
22
46
|
/**
|
|
23
47
|
* Split `text` into chunks of at most `limit` characters. A chunk ends at the
|
|
24
48
|
* last line break inside the window, else at the last space, else at the
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files an agent sends to a chat thread: task attachments, read through the
|
|
3
|
+
* active file provider, and agent-fs paths, read with the caller's own agent-fs
|
|
4
|
+
* credential. `postChatMessage` sends them.
|
|
5
|
+
*/
|
|
6
|
+
import { getResolvedConfig, getTaskAttachments } from "@/be/db";
|
|
7
|
+
import { backingProviderId, scopeFromAttachment } from "@/be/task-attachment-store";
|
|
8
|
+
import { AgentFsProvider } from "@/fs/agent-fs-provider";
|
|
9
|
+
import {
|
|
10
|
+
type FileScope,
|
|
11
|
+
type FileStorageProvider,
|
|
12
|
+
FilesError,
|
|
13
|
+
normalizeStoredKey,
|
|
14
|
+
} from "@/fs/provider";
|
|
15
|
+
import { getFileStorageProvider } from "@/fs/registry";
|
|
16
|
+
import type { TaskAttachment } from "@/types";
|
|
17
|
+
import { createValueScrubber } from "@/utils/secret-scrubber";
|
|
18
|
+
import { SHARED_FILE_INTENT } from "./attachments";
|
|
19
|
+
import { errorMessage } from "./logger";
|
|
20
|
+
import { ChatFileTooLargeError, type OutboundChatFile } from "./outbound";
|
|
21
|
+
|
|
22
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
23
|
+
|
|
24
|
+
/** The caller's own agent-fs credential and default drive. Never the API's `API_AGENT_FS_API_KEY`. */
|
|
25
|
+
export type CallerAgentFs = { apiUrl: string; apiKey: string; orgId: string; driveId: string };
|
|
26
|
+
|
|
27
|
+
async function download(
|
|
28
|
+
scope: FileScope,
|
|
29
|
+
maxBytes: number,
|
|
30
|
+
provider: Pick<FileStorageProvider, "download"> = getFileStorageProvider(),
|
|
31
|
+
): Promise<{ data: Uint8Array; mimeType?: string }> {
|
|
32
|
+
const response = await provider.download(scope);
|
|
33
|
+
const length = Number(response.headers.get("content-length"));
|
|
34
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
35
|
+
await response.body?.cancel();
|
|
36
|
+
throw new ChatFileTooLargeError(length);
|
|
37
|
+
}
|
|
38
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
39
|
+
if (data.byteLength > maxBytes) throw new ChatFileTooLargeError(data.byteLength);
|
|
40
|
+
return { data, mimeType: response.headers.get("content-type") ?? undefined };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A task attachment as a file to send, or undefined when the active provider does not hold its bytes. */
|
|
44
|
+
export function chatFileOfTaskAttachment(attachment: TaskAttachment): OutboundChatFile | undefined {
|
|
45
|
+
if (backingProviderId(attachment) !== getFileStorageProvider().id) return undefined;
|
|
46
|
+
return {
|
|
47
|
+
name: attachment.name,
|
|
48
|
+
mimeType: attachment.mimeType,
|
|
49
|
+
sizeBytes: attachment.sizeBytes,
|
|
50
|
+
load: (maxBytes) => download(scopeFromAttachment(attachment), maxBytes),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The segments of a tool-supplied path, each decoded once and checked, so an
|
|
56
|
+
* encoded `..`, separator, `?` or `#` cannot leave the drive or change the endpoint.
|
|
57
|
+
*/
|
|
58
|
+
function pathSegments(path: string): string[] {
|
|
59
|
+
return normalizeStoredKey(path)
|
|
60
|
+
.split("/")
|
|
61
|
+
.map((segment) => {
|
|
62
|
+
let decoded = segment;
|
|
63
|
+
try {
|
|
64
|
+
decoded = decodeURIComponent(segment);
|
|
65
|
+
} catch {
|
|
66
|
+
// A lone `%` is part of the name; it is encoded below.
|
|
67
|
+
}
|
|
68
|
+
if (!decoded || decoded === "." || decoded === ".." || /[/\\\0]/.test(decoded)) {
|
|
69
|
+
throw new FilesError(
|
|
70
|
+
"Provider",
|
|
71
|
+
"Path must not contain empty, `.` or `..` segments, or encoded separators",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return decoded;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The agent-fs credential and drive the caller itself holds, or undefined when it has none. */
|
|
79
|
+
async function callerAgentFs(agentId: string | undefined): Promise<CallerAgentFs | undefined> {
|
|
80
|
+
if (!agentId) return undefined;
|
|
81
|
+
const configs = await getResolvedConfig(agentId);
|
|
82
|
+
const value = (key: string) => configs.find((c) => c.key === key)?.value?.trim();
|
|
83
|
+
const apiUrl = value("AGENT_FS_API_URL") || process.env.AGENT_FS_API_URL?.trim();
|
|
84
|
+
const apiKey = value("AGENT_FS_API_KEY");
|
|
85
|
+
const orgId = value("AGENT_FS_DEFAULT_ORG_ID");
|
|
86
|
+
const driveId = value("AGENT_FS_DEFAULT_DRIVE_ID");
|
|
87
|
+
return apiUrl && apiKey && orgId && driveId ? { apiUrl, apiKey, orgId, driveId } : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A file at a path of the caller's agent-fs drive, read with the caller's own
|
|
92
|
+
* credential: a path the caller cannot read is not sent, and the thread gets a note.
|
|
93
|
+
*/
|
|
94
|
+
export function chatFileOfPath(path: string, access: CallerAgentFs): OutboundChatFile {
|
|
95
|
+
const segments = pathSegments(path);
|
|
96
|
+
const name = segments.at(-1)!;
|
|
97
|
+
const key = segments.map(encodeURIComponent).join("/");
|
|
98
|
+
// The caller's key can be shorter than the global scrubber's floor, so a
|
|
99
|
+
// storage error body or transport error is scrubbed of it by exact value.
|
|
100
|
+
const keyScrubber = createValueScrubber([{ value: access.apiKey, name: "AGENT_FS_API_KEY" }]);
|
|
101
|
+
return {
|
|
102
|
+
name,
|
|
103
|
+
load: async (maxBytes) => {
|
|
104
|
+
try {
|
|
105
|
+
return await download(
|
|
106
|
+
{ taskId: "chat", name, key, orgId: access.orgId, driveId: access.driveId },
|
|
107
|
+
maxBytes,
|
|
108
|
+
new AgentFsProvider(access),
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof ChatFileTooLargeError) throw error;
|
|
112
|
+
if (error instanceof FilesError && error.code === "Unauthorized") {
|
|
113
|
+
throw new Error("your agent-fs credential cannot read this path");
|
|
114
|
+
}
|
|
115
|
+
throw new Error(keyScrubber.text(error instanceof Error ? error.message : String(error)));
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the `attachments` of `chat-reply` and `chat-post`: an attachment id
|
|
123
|
+
* of `taskId`, or an agent-fs path read as `agentId`. Returns an error naming
|
|
124
|
+
* the first entry that cannot be sent, so the tool refuses before posting anything.
|
|
125
|
+
*/
|
|
126
|
+
export async function resolveChatAttachmentRefs(
|
|
127
|
+
refs: string[],
|
|
128
|
+
taskId: string | undefined,
|
|
129
|
+
agentId: string | undefined,
|
|
130
|
+
): Promise<{ files: OutboundChatFile[] } | { error: string }> {
|
|
131
|
+
let taskAttachments: TaskAttachment[] | undefined;
|
|
132
|
+
let access: CallerAgentFs | undefined | null = null;
|
|
133
|
+
const files: OutboundChatFile[] = [];
|
|
134
|
+
for (const ref of refs) {
|
|
135
|
+
if (UUID_PATTERN.test(ref)) {
|
|
136
|
+
if (!taskId) return { error: `Attachment ${ref}: no current task to look it up in.` };
|
|
137
|
+
taskAttachments ??= await getTaskAttachments(taskId);
|
|
138
|
+
const attachment = taskAttachments.find((a) => a.id === ref);
|
|
139
|
+
if (!attachment)
|
|
140
|
+
return { error: `Attachment ${ref} is not an attachment of task ${taskId}.` };
|
|
141
|
+
const file = chatFileOfTaskAttachment(attachment);
|
|
142
|
+
if (!file) {
|
|
143
|
+
return {
|
|
144
|
+
error: `Attachment ${ref} (${attachment.kind}) is not stored in the swarm file provider, so it cannot be sent as a file. Post its link instead.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
files.push(file);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (access === null) access = await callerAgentFs(agentId);
|
|
151
|
+
if (!access) {
|
|
152
|
+
return {
|
|
153
|
+
error: `Path ${ref}: you have no agent-fs credential and drive to read it with. Attach the file to your task and send its attachment id.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
files.push(chatFileOfPath(ref, access));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { error: `Path ${ref}: ${errorMessage(error)}` };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { files };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A Slack file an agent fetched into its task; recorded with that agent's id. */
|
|
166
|
+
const FETCHED_SLACK_FILE_INTENT = "slack-file";
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The output files of a task, for the result relay: attachments an agent
|
|
170
|
+
* added that the active provider holds. Input files (a user's upload, a Slack
|
|
171
|
+
* file an agent fetched) are left out, as the Slack relay does, so they are not
|
|
172
|
+
* echoed back or carried into another chat thread.
|
|
173
|
+
*/
|
|
174
|
+
export async function taskOutputChatFiles(taskId: string): Promise<OutboundChatFile[]> {
|
|
175
|
+
return (await getTaskAttachments(taskId)).flatMap((attachment) => {
|
|
176
|
+
if (attachment.agentId === null) return [];
|
|
177
|
+
if (
|
|
178
|
+
attachment.intent === SHARED_FILE_INTENT ||
|
|
179
|
+
attachment.intent === FETCHED_SLACK_FILE_INTENT
|
|
180
|
+
) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const file = chatFileOfTaskAttachment(attachment);
|
|
184
|
+
return file ? [file] : [];
|
|
185
|
+
});
|
|
186
|
+
}
|