@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
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { SYSTEM_ONE_PROVIDER_IDS } from "@/workflows/executors/system-one-providers";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* One typed decision (Jev and the other SystemOne hosts) made in the API
|
|
13
|
+
* One typed decision (Jev, Clef and the other SystemOne hosts) made in the API
|
|
14
14
|
* process, the same way a `system-one-decision` workflow node makes it. The
|
|
15
15
|
* provider credential is read and sent here and never reaches the caller, so a
|
|
16
16
|
* script can ask for a decision without a credential binding in its own realm.
|
|
@@ -22,14 +22,16 @@ export const registerSystemOneDecideTool = (server: McpServer) => {
|
|
|
22
22
|
title: "Ask a SystemOne decision model",
|
|
23
23
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
24
24
|
description:
|
|
25
|
-
"Send a state and typed questions (noul, choice, score) to a SystemOne decision host such as Jev, and return the validated answers. " +
|
|
25
|
+
"Send a state and typed questions (noul, choice, score) to a SystemOne decision host such as Jev or Clef, and return the validated answers. " +
|
|
26
26
|
"Same request, provider list, credentials and validation as the `system-one-decision` workflow node, without its human review band. " +
|
|
27
27
|
"The provider credential is a global secret that stays on the server; this tool never returns it.",
|
|
28
28
|
inputSchema: z.object({
|
|
29
29
|
provider: z
|
|
30
30
|
.enum(SYSTEM_ONE_PROVIDER_IDS)
|
|
31
31
|
.optional()
|
|
32
|
-
.describe(
|
|
32
|
+
.describe(
|
|
33
|
+
"Decision host (default: the SYSTEM_ONE_DEFAULT_PROVIDER global config, typesafe when unset)",
|
|
34
|
+
),
|
|
33
35
|
model: z.string().min(1).optional().describe("Provider model id (default: the provider's)"),
|
|
34
36
|
state: z
|
|
35
37
|
.union([z.string(), z.array(z.string()), z.record(z.string(), z.unknown())])
|
|
@@ -55,6 +57,12 @@ export const registerSystemOneDecideTool = (server: McpServer) => {
|
|
|
55
57
|
answers: z.record(z.string(), z.unknown()).optional(),
|
|
56
58
|
usage: z.record(z.string(), z.unknown()).optional(),
|
|
57
59
|
requestId: z.string().optional(),
|
|
60
|
+
providerUnavailable: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe(
|
|
64
|
+
"On an error: true when this deployment refuses the provider (e.g. OpenRouter behind a gateway) and no request was sent, so a caller choosing automatically can try its next provider",
|
|
65
|
+
),
|
|
58
66
|
}),
|
|
59
67
|
},
|
|
60
68
|
async ({ provider, model, state, questions, timeoutMs, maxRetries }, requestInfo) => {
|
|
@@ -88,7 +96,12 @@ export const registerSystemOneDecideTool = (server: McpServer) => {
|
|
|
88
96
|
return toolErr("The system-one-decision executor is not available");
|
|
89
97
|
}
|
|
90
98
|
const decided = await executor.decide(parsed.data);
|
|
91
|
-
if (!decided.ok)
|
|
99
|
+
if (!decided.ok) {
|
|
100
|
+
return toolErr(
|
|
101
|
+
decided.error,
|
|
102
|
+
decided.deploymentRefused ? { data: { providerUnavailable: true } } : {},
|
|
103
|
+
);
|
|
104
|
+
}
|
|
92
105
|
|
|
93
106
|
const { output } = decided;
|
|
94
107
|
return toolOk(`Decision from ${output.model}`, {
|
package/src/types.ts
CHANGED
|
@@ -905,6 +905,12 @@ export const CreateTaskOptionsSchema = z.object({
|
|
|
905
905
|
* contract on completion (which would block the control task — DES-523).
|
|
906
906
|
*/
|
|
907
907
|
inheritParentOutputSchema: z.boolean().optional(),
|
|
908
|
+
/**
|
|
909
|
+
* When a `parentTaskId` is set and `requestedByUserId` is not, the child
|
|
910
|
+
* inherits the parent's requester by default. Set this to `false` when the
|
|
911
|
+
* child answers a different person (a chat message whose sender is unknown).
|
|
912
|
+
*/
|
|
913
|
+
inheritParentRequester: z.boolean().optional(),
|
|
908
914
|
/**
|
|
909
915
|
* Skip parent routing requirements only for a child with its own explicit
|
|
910
916
|
* Lead-only control-plane authorization.
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* binding instead.
|
|
9
9
|
*
|
|
10
10
|
* Keys below are read only by the API server (integration signing secrets, app
|
|
11
|
-
* private keys, OAuth client secrets, the API's own agent-fs
|
|
11
|
+
* private keys, OAuth client secrets, the API's own agent-fs and
|
|
12
|
+
* speech-to-text keys). They can
|
|
12
13
|
* never be exported: the config API rejects `exportToEnv: true` for them and
|
|
13
14
|
* every reader treats a stored true as false.
|
|
14
15
|
*/
|
|
@@ -25,6 +26,7 @@ export const ENV_EXPORT_RESTRICTED_KEYS: ReadonlySet<string> = new Set([
|
|
|
25
26
|
"AZURE_DEVOPS_WEBHOOK_SECRET",
|
|
26
27
|
"AGENTMAIL_WEBHOOK_SECRET",
|
|
27
28
|
"API_AGENT_FS_API_KEY",
|
|
29
|
+
"TRANSCRIPTION_API_KEY",
|
|
28
30
|
]);
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -246,23 +246,83 @@ export function parseCookieHeader(
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
* The single shared cookie name used before sessions became per-page. Every
|
|
250
|
+
* launch overwrote it, so only the newest page in a browser had a session.
|
|
251
|
+
* Still read for back-compat, but only for the page its payload names.
|
|
252
|
+
*/
|
|
253
|
+
export const LEGACY_PAGE_SESSION_COOKIE = "page_session";
|
|
254
|
+
|
|
255
|
+
/** Page ids are 32 hex chars today; anything outside the cookie-name-safe set is refused. */
|
|
256
|
+
const COOKIE_SAFE_PAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
257
|
+
|
|
258
|
+
export function isCookieSafePageId(pageId: string): boolean {
|
|
259
|
+
return COOKIE_SAFE_PAGE_ID.test(pageId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Per-page cookie name, so sessions for different pages coexist in one browser. */
|
|
263
|
+
export function pageSessionCookieName(pageId: string): string {
|
|
264
|
+
return `${LEGACY_PAGE_SESSION_COOKIE}_${pageId}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface VerifiedPageSession {
|
|
268
|
+
/** The raw signed token, forwarded upstream by the page proxy. */
|
|
269
|
+
token: string;
|
|
270
|
+
payload: PageSessionPayload;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
type CookieRequest = { headers: { cookie?: string | string[] | undefined } };
|
|
274
|
+
|
|
275
|
+
async function verifyNamedCookie(
|
|
276
|
+
req: CookieRequest,
|
|
277
|
+
name: string,
|
|
278
|
+
): Promise<VerifiedPageSession | null> {
|
|
279
|
+
const token = parseCookieHeader(req.headers.cookie, name);
|
|
280
|
+
const payload = await verifyPageSession(token);
|
|
281
|
+
return payload && token ? { token, payload } : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Find and verify the session for `pageId`: `page_session_<pageId>` first,
|
|
286
|
+
* then the legacy `page_session`. A cookie whose signed payload names a
|
|
287
|
+
* different page is ignored, so the result always satisfies
|
|
288
|
+
* `payload.pageId === pageId`. Returns `null` on any failure (no cookie,
|
|
289
|
+
* malformed, bad signature, expired, wrong page).
|
|
290
|
+
*/
|
|
291
|
+
export async function extractPageSession(
|
|
292
|
+
req: CookieRequest,
|
|
293
|
+
pageId: string,
|
|
294
|
+
): Promise<VerifiedPageSession | null> {
|
|
295
|
+
if (!isCookieSafePageId(pageId)) return null;
|
|
296
|
+
for (const name of [pageSessionCookieName(pageId), LEGACY_PAGE_SESSION_COOKIE]) {
|
|
297
|
+
const session = await verifyNamedCookie(req, name);
|
|
298
|
+
if (session?.payload.pageId === pageId) return session;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Verify the legacy `page_session` cookie for whatever page it names. Only for
|
|
305
|
+
* callers that send no page id (clients predating per-page cookies).
|
|
306
|
+
*/
|
|
307
|
+
export async function extractLegacyPageSession(
|
|
308
|
+
req: CookieRequest,
|
|
309
|
+
): Promise<VerifiedPageSession | null> {
|
|
310
|
+
return verifyNamedCookie(req, LEGACY_PAGE_SESSION_COOKIE);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Payload-only form of `extractPageSession`. Shared by the `/p/:id` handler
|
|
315
|
+
* (`src/http/pages-public.ts`) and the realtime upgrade
|
|
316
|
+
* (`src/realtime/transport.ts`) so they converge on the same semantics.
|
|
256
317
|
*
|
|
257
318
|
* Accepts an object with a `headers.cookie` field — duck-typed to keep this
|
|
258
319
|
* helper test-friendly without dragging `node:http` types into the utility.
|
|
259
320
|
*/
|
|
260
|
-
export async function extractAndVerifyCookie(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return verifyPageSession(token);
|
|
321
|
+
export async function extractAndVerifyCookie(
|
|
322
|
+
req: CookieRequest,
|
|
323
|
+
pageId: string,
|
|
324
|
+
): Promise<PageSessionPayload | null> {
|
|
325
|
+
return (await extractPageSession(req, pageId))?.payload ?? null;
|
|
266
326
|
}
|
|
267
327
|
|
|
268
328
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -284,6 +344,10 @@ const PAGE_SESSION_TTL_SECONDS = 3600;
|
|
|
284
344
|
* - `dev=true` → `SameSite=Lax` without `Secure` (works on http://localhost).
|
|
285
345
|
* - `dev=false` → `SameSite=None; Secure` (cross-site iframe embedding in prod).
|
|
286
346
|
*
|
|
347
|
+
* The cookie is named `page_session_<pageId>`, so launching page B leaves
|
|
348
|
+
* page A's session intact. Responses should go through `pageSessionSetCookies`,
|
|
349
|
+
* which also bounds how many of these a browser keeps.
|
|
350
|
+
*
|
|
287
351
|
* The caller is responsible for setting `Set-Cookie` on the response — this
|
|
288
352
|
* helper only builds the string. TTL is 1 hour; renewed on every issuance.
|
|
289
353
|
*/
|
|
@@ -299,13 +363,12 @@ export async function issuePageSessionCookie(
|
|
|
299
363
|
...(opts.name ? { name: opts.name } : {}),
|
|
300
364
|
...(opts.op ? { op: true as const } : {}),
|
|
301
365
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (opts.dev) {
|
|
366
|
+
return buildCookie(pageSessionCookieName(pageId), token, PAGE_SESSION_TTL_SECONDS, opts.dev);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildCookie(name: string, value: string, maxAge: number, dev: boolean): string {
|
|
370
|
+
const attrs = [`${name}=${value}`, "HttpOnly", "Path=/", `Max-Age=${maxAge}`];
|
|
371
|
+
if (dev) {
|
|
309
372
|
attrs.push("SameSite=Lax");
|
|
310
373
|
} else {
|
|
311
374
|
attrs.push("SameSite=None");
|
|
@@ -313,3 +376,53 @@ export async function issuePageSessionCookie(
|
|
|
313
376
|
}
|
|
314
377
|
return attrs.join("; ");
|
|
315
378
|
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Most per-page session cookies one browser keeps, counting the one being
|
|
382
|
+
* issued. Each is ~220 bytes on the wire, and `Path=/` sends all of them on
|
|
383
|
+
* every request to this host, so an unbounded jar grows past the server's
|
|
384
|
+
* 16 KB header limit (431 on every route, `/health` included) after ~75
|
|
385
|
+
* launches within the hour. 20 keeps the jar near 4.5 KB.
|
|
386
|
+
*/
|
|
387
|
+
export const MAX_PAGE_SESSION_COOKIES = 20;
|
|
388
|
+
|
|
389
|
+
const PAGE_SESSION_COOKIE_RE = new RegExp(
|
|
390
|
+
`(?:^|;\\s*)(${LEGACY_PAGE_SESSION_COOKIE}_[A-Za-z0-9_-]{1,64})=([^;]*)`,
|
|
391
|
+
"g",
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* `Set-Cookie` values for a launch of `pageId`: the new session, plus an
|
|
396
|
+
* expiring cookie for every other per-page session in the request's jar that
|
|
397
|
+
* is invalid or expired, or falls outside the `MAX_PAGE_SESSION_COOKIES - 1`
|
|
398
|
+
* newest. The browser then holds at most `MAX_PAGE_SESSION_COOKIES` of them.
|
|
399
|
+
* An evicted page's open tab loses its session, the same as after expiry.
|
|
400
|
+
*/
|
|
401
|
+
export async function pageSessionSetCookies(
|
|
402
|
+
req: CookieRequest,
|
|
403
|
+
pageId: string,
|
|
404
|
+
opts: { dev: boolean; uid?: string; name?: string; op?: true },
|
|
405
|
+
): Promise<string[]> {
|
|
406
|
+
const issued = await issuePageSessionCookie(pageId, opts);
|
|
407
|
+
const header = Array.isArray(req.headers.cookie)
|
|
408
|
+
? req.headers.cookie.join("; ")
|
|
409
|
+
: (req.headers.cookie ?? "");
|
|
410
|
+
const keep: { name: string; exp: number; order: number }[] = [];
|
|
411
|
+
const evict = new Set<string>();
|
|
412
|
+
const seen = new Set([pageSessionCookieName(pageId)]);
|
|
413
|
+
for (const [, name, token] of header.matchAll(PAGE_SESSION_COOKIE_RE)) {
|
|
414
|
+
if (!name || seen.has(name)) continue;
|
|
415
|
+
seen.add(name);
|
|
416
|
+
const payload = await verifyPageSession(token);
|
|
417
|
+
if (payload && name === pageSessionCookieName(payload.pageId)) {
|
|
418
|
+
keep.push({ name, exp: payload.exp, order: keep.length });
|
|
419
|
+
} else {
|
|
420
|
+
evict.add(name);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// `exp` has one-second granularity. Browsers list older cookies first, so a
|
|
424
|
+
// later position breaks ties toward the more recent launch.
|
|
425
|
+
keep.sort((a, b) => b.exp - a.exp || b.order - a.order);
|
|
426
|
+
for (const { name } of keep.slice(MAX_PAGE_SESSION_COOKIES - 1)) evict.add(name);
|
|
427
|
+
return [issued, ...[...evict].map((name) => buildCookie(name, "", 0, opts.dev))];
|
|
428
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech-to-text for inbound audio. `TRANSCRIPTION_PROVIDER` picks the request
|
|
3
|
+
* shape:
|
|
4
|
+
*
|
|
5
|
+
* - `openai`: `POST <base>/audio/transcriptions`, `Authorization: Bearer`, form
|
|
6
|
+
* field `model`. OpenAI Whisper and self-hosted whisper servers speak this
|
|
7
|
+
* shape.
|
|
8
|
+
* - `groq`: the `openai` shape against Groq's OpenAI-compatible endpoint.
|
|
9
|
+
* - `elevenlabs`: `POST <base>/speech-to-text`, `xi-api-key` header, form
|
|
10
|
+
* field `model_id`.
|
|
11
|
+
*
|
|
12
|
+
* The key: an explicit `TRANSCRIPTION_API_KEY` wins, with `TRANSCRIPTION_PROVIDER`
|
|
13
|
+
* (default `openai`). Without it, transcription reuses a provider key the
|
|
14
|
+
* swarm already has: the pinned provider's key when `TRANSCRIPTION_PROVIDER` is
|
|
15
|
+
* set, else the first of `ELEVENLABS_API_KEY`, `OPENAI_API_KEY`, `GROQ_API_KEY`.
|
|
16
|
+
* Off when none is set.
|
|
17
|
+
*
|
|
18
|
+
* `TRANSCRIPTION_MODEL` overrides the provider's default model.
|
|
19
|
+
* `TRANSCRIPTION_BASE_URL` overrides its base URL for an explicit key only: an
|
|
20
|
+
* auto-detected key always goes to its provider's own endpoint. Deepgram and AssemblyAI use other request shapes and are not
|
|
21
|
+
* supported. Read at call time, so a config reload applies without a restart.
|
|
22
|
+
*/
|
|
23
|
+
import { createValueScrubber, scrubSecrets } from "./secret-scrubber";
|
|
24
|
+
|
|
25
|
+
const TRANSCRIPTION_TIMEOUT_MS = 60_000;
|
|
26
|
+
|
|
27
|
+
/** The config key that switches transcription on explicitly. */
|
|
28
|
+
export const TRANSCRIPTION_API_KEY_ENV = "TRANSCRIPTION_API_KEY";
|
|
29
|
+
|
|
30
|
+
type ProviderSpec = {
|
|
31
|
+
/** The provider's own key, reused when `TRANSCRIPTION_API_KEY` is unset. */
|
|
32
|
+
keyEnv: string;
|
|
33
|
+
defaultBaseUrl: string;
|
|
34
|
+
defaultModel: string;
|
|
35
|
+
path: string;
|
|
36
|
+
modelField: string;
|
|
37
|
+
authHeaders: (apiKey: string) => Record<string, string>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const openAIShape = {
|
|
41
|
+
path: "/audio/transcriptions",
|
|
42
|
+
modelField: "model",
|
|
43
|
+
authHeaders: (apiKey: string) => ({ authorization: `Bearer ${apiKey}` }),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const PROVIDERS = {
|
|
47
|
+
openai: {
|
|
48
|
+
keyEnv: "OPENAI_API_KEY",
|
|
49
|
+
defaultBaseUrl: "https://api.openai.com/v1",
|
|
50
|
+
defaultModel: "whisper-1",
|
|
51
|
+
...openAIShape,
|
|
52
|
+
},
|
|
53
|
+
groq: {
|
|
54
|
+
keyEnv: "GROQ_API_KEY",
|
|
55
|
+
defaultBaseUrl: "https://api.groq.com/openai/v1",
|
|
56
|
+
defaultModel: "whisper-large-v3-turbo",
|
|
57
|
+
...openAIShape,
|
|
58
|
+
},
|
|
59
|
+
elevenlabs: {
|
|
60
|
+
keyEnv: "ELEVENLABS_API_KEY",
|
|
61
|
+
defaultBaseUrl: "https://api.elevenlabs.io/v1",
|
|
62
|
+
defaultModel: "scribe_v2",
|
|
63
|
+
path: "/speech-to-text",
|
|
64
|
+
modelField: "model_id",
|
|
65
|
+
authHeaders: (apiKey) => ({ "xi-api-key": apiKey }),
|
|
66
|
+
},
|
|
67
|
+
} satisfies Record<string, ProviderSpec>;
|
|
68
|
+
|
|
69
|
+
export type TranscriptionProvider = keyof typeof PROVIDERS;
|
|
70
|
+
|
|
71
|
+
/** Order in which an existing provider key is picked when nothing is pinned. */
|
|
72
|
+
const AUTO_DETECT_ORDER: TranscriptionProvider[] = ["elevenlabs", "openai", "groq"];
|
|
73
|
+
|
|
74
|
+
/** Every env key the provider and key resolution reads. */
|
|
75
|
+
export const TRANSCRIPTION_ENV_KEYS = [
|
|
76
|
+
TRANSCRIPTION_API_KEY_ENV,
|
|
77
|
+
"TRANSCRIPTION_PROVIDER",
|
|
78
|
+
"TRANSCRIPTION_BASE_URL",
|
|
79
|
+
"TRANSCRIPTION_MODEL",
|
|
80
|
+
...AUTO_DETECT_ORDER.map((name) => PROVIDERS[name].keyEnv),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export class TranscriptionError extends Error {
|
|
84
|
+
constructor(message: string) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "TranscriptionError";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The provider and key a transcription call uses. */
|
|
91
|
+
export type TranscriptionConfig = {
|
|
92
|
+
provider: string;
|
|
93
|
+
apiKey: string;
|
|
94
|
+
/** The env key the API key came from. */
|
|
95
|
+
keyEnv: string;
|
|
96
|
+
/** `explicit`: `TRANSCRIPTION_API_KEY`. `auto`: a provider key the swarm already had. */
|
|
97
|
+
source: "explicit" | "auto";
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function envValue(key: string): string | undefined {
|
|
101
|
+
return process.env[key]?.trim() || undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve provider and key, or null when no key is set. Unsupported provider names pass through. */
|
|
105
|
+
export function resolveTranscriptionConfig(): TranscriptionConfig | null {
|
|
106
|
+
const pinned = envValue("TRANSCRIPTION_PROVIDER")?.toLowerCase();
|
|
107
|
+
const explicitKey = envValue(TRANSCRIPTION_API_KEY_ENV);
|
|
108
|
+
if (explicitKey) {
|
|
109
|
+
return {
|
|
110
|
+
provider: pinned ?? "openai",
|
|
111
|
+
apiKey: explicitKey,
|
|
112
|
+
keyEnv: TRANSCRIPTION_API_KEY_ENV,
|
|
113
|
+
source: "explicit",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// A pinned provider only uses its own key, never another provider's.
|
|
117
|
+
const candidates = pinned
|
|
118
|
+
? AUTO_DETECT_ORDER.filter((name) => name === pinned)
|
|
119
|
+
: AUTO_DETECT_ORDER;
|
|
120
|
+
for (const name of candidates) {
|
|
121
|
+
const { keyEnv } = PROVIDERS[name];
|
|
122
|
+
const apiKey = envValue(keyEnv);
|
|
123
|
+
if (apiKey) return { provider: name, apiKey, keyEnv, source: "auto" };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isTranscriptionConfigured(): boolean {
|
|
129
|
+
return resolveTranscriptionConfig() !== null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function providerSpec(name: string): ProviderSpec {
|
|
133
|
+
if (!Object.hasOwn(PROVIDERS, name)) {
|
|
134
|
+
throw new TranscriptionError(
|
|
135
|
+
`TRANSCRIPTION_PROVIDER "${name}" is not supported (use ${Object.keys(PROVIDERS).join(", ")})`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return PROVIDERS[name as TranscriptionProvider];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** OpenAI puts the reason in `error.message`; ElevenLabs in `detail.message` or `detail`. */
|
|
142
|
+
function errorDetail(payload: Record<string, unknown> | null, status: number): string {
|
|
143
|
+
const error = payload?.error as { message?: unknown } | undefined;
|
|
144
|
+
if (typeof error?.message === "string") return error.message;
|
|
145
|
+
const detail = payload?.detail as { message?: unknown } | string | undefined;
|
|
146
|
+
if (typeof detail === "string") return detail;
|
|
147
|
+
if (typeof detail?.message === "string") return detail.message;
|
|
148
|
+
return `HTTP ${status}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Transcribe one audio file. Throws `TranscriptionError` when unconfigured or when the call fails. */
|
|
152
|
+
export async function transcribeAudio(input: {
|
|
153
|
+
data: Blob;
|
|
154
|
+
filename: string;
|
|
155
|
+
mimeType?: string;
|
|
156
|
+
}): Promise<string> {
|
|
157
|
+
const config = resolveTranscriptionConfig();
|
|
158
|
+
if (!config) throw new TranscriptionError("no transcription provider key is set");
|
|
159
|
+
const { apiKey } = config;
|
|
160
|
+
const provider = providerSpec(config.provider);
|
|
161
|
+
// An auto-detected key only goes to its own provider: a base URL set in
|
|
162
|
+
// config must not receive a key that was stored for another purpose.
|
|
163
|
+
const baseUrlOverride =
|
|
164
|
+
config.source === "explicit" ? process.env.TRANSCRIPTION_BASE_URL?.trim() : undefined;
|
|
165
|
+
const baseUrl = (baseUrlOverride || provider.defaultBaseUrl).replace(/\/+$/, "");
|
|
166
|
+
// scrubSecrets skips known values under 12 chars, and a self-hosted key can
|
|
167
|
+
// be shorter, so the submitted key is scrubbed by value from every error.
|
|
168
|
+
const keyScrubber = createValueScrubber([{ value: apiKey, name: config.keyEnv }]);
|
|
169
|
+
const scrub = (text: string) => scrubSecrets(keyScrubber.text(text));
|
|
170
|
+
const form = new FormData();
|
|
171
|
+
const type = input.mimeType ?? input.data.type;
|
|
172
|
+
form.append("file", type ? new Blob([input.data], { type }) : input.data, input.filename);
|
|
173
|
+
form.append(
|
|
174
|
+
provider.modelField,
|
|
175
|
+
process.env.TRANSCRIPTION_MODEL?.trim() || provider.defaultModel,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let response: Response;
|
|
179
|
+
try {
|
|
180
|
+
response = await fetch(`${baseUrl}${provider.path}`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: provider.authHeaders(apiKey),
|
|
183
|
+
body: form,
|
|
184
|
+
signal: AbortSignal.timeout(TRANSCRIPTION_TIMEOUT_MS),
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
188
|
+
throw new TranscriptionError(`no answer within ${TRANSCRIPTION_TIMEOUT_MS / 1000} s`);
|
|
189
|
+
}
|
|
190
|
+
throw new TranscriptionError(scrub(error instanceof Error ? error.message : String(error)));
|
|
191
|
+
}
|
|
192
|
+
const payload = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
193
|
+
if (!response.ok || typeof payload?.text !== "string") {
|
|
194
|
+
throw new TranscriptionError(scrub(errorDetail(payload, response.status)));
|
|
195
|
+
}
|
|
196
|
+
return payload.text.trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The task-text note for an audio file when no provider is configured. */
|
|
200
|
+
export const NOT_TRANSCRIBED_NOTE = `(not transcribed: no transcription provider configured, set ${TRANSCRIPTION_API_KEY_ENV} or one of ${AUTO_DETECT_ORDER.map((name) => PROVIDERS[name].keyEnv).join(", ")})`;
|
|
201
|
+
|
|
202
|
+
const AUDIO_EXTENSIONS: Record<string, string> = {
|
|
203
|
+
"audio/ogg": "ogg",
|
|
204
|
+
"audio/opus": "ogg",
|
|
205
|
+
"audio/mpeg": "mp3",
|
|
206
|
+
"audio/mp4": "m4a",
|
|
207
|
+
"audio/x-m4a": "m4a",
|
|
208
|
+
"audio/wav": "wav",
|
|
209
|
+
"audio/x-wav": "wav",
|
|
210
|
+
"audio/webm": "webm",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** One audio file to transcribe. `key` names its note in the returned map. */
|
|
214
|
+
export type AudioFile = { key: string; name: string; data: Blob; mimeType?: string };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Transcribe audio files into task-text notes, one per file: `[Transcript of
|
|
218
|
+
* <name>]` and the text, `(transcription failed: <reason>)`, or (when no
|
|
219
|
+
* provider is configured) `NOT_TRANSCRIBED_NOTE`. Never throws, so a failed
|
|
220
|
+
* call never blocks the task it belongs to. `logPrefix` tags the warning.
|
|
221
|
+
*/
|
|
222
|
+
export async function transcribeAudioNotes(
|
|
223
|
+
files: AudioFile[],
|
|
224
|
+
logPrefix: string,
|
|
225
|
+
): Promise<Map<string, string>> {
|
|
226
|
+
const notes = new Map<string, string>();
|
|
227
|
+
if (files.length === 0) return notes;
|
|
228
|
+
if (!isTranscriptionConfigured()) {
|
|
229
|
+
for (const file of files) notes.set(file.key, NOT_TRANSCRIBED_NOTE);
|
|
230
|
+
return notes;
|
|
231
|
+
}
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
// Voice notes can carry no file name; the endpoint reads the format from the extension.
|
|
234
|
+
const extension = AUDIO_EXTENSIONS[file.mimeType ?? ""] ?? "ogg";
|
|
235
|
+
const filename = /\.\w+$/.test(file.name) ? file.name : `${file.name}.${extension}`;
|
|
236
|
+
try {
|
|
237
|
+
const text = await transcribeAudio({ data: file.data, filename, mimeType: file.mimeType });
|
|
238
|
+
notes.set(file.key, `[Transcript of ${file.name}]\n${text || "(no speech recognized)"}`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const reason = scrubSecrets(error instanceof Error ? error.message : String(error));
|
|
241
|
+
console.warn(`${logPrefix} could not transcribe file ${file.key}: ${reason}`);
|
|
242
|
+
notes.set(file.key, `(transcription failed: ${reason})`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return notes;
|
|
246
|
+
}
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
} from "./human-in-the-loop";
|
|
19
19
|
import { SYSTEM_ONE_EXTRACTION_NODE_TYPE } from "./system-one-extraction";
|
|
20
20
|
import {
|
|
21
|
+
resolveSystemOneDefaultProvider,
|
|
21
22
|
resolveSystemOneTarget,
|
|
22
|
-
SYSTEM_ONE_DEFAULT_PROVIDER,
|
|
23
23
|
SYSTEM_ONE_PROVIDER_IDS,
|
|
24
24
|
SYSTEM_ONE_PROVIDERS,
|
|
25
25
|
type SystemOneProvider,
|
|
@@ -164,7 +164,11 @@ export type SystemOneHumanReview = z.infer<typeof HumanReviewSchema>;
|
|
|
164
164
|
|
|
165
165
|
export const SystemOneDecisionConfigSchema = z
|
|
166
166
|
.strictObject({
|
|
167
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Decision host. Unset means the `SYSTEM_ONE_DEFAULT_PROVIDER` global config
|
|
169
|
+
* value, read at run time (`typesafe` when that is unset too).
|
|
170
|
+
*/
|
|
171
|
+
provider: z.enum(SYSTEM_ONE_PROVIDER_IDS).optional(),
|
|
168
172
|
/** Provider-specific model id. Unset means the provider's default. */
|
|
169
173
|
model: z.string().min(1).optional(),
|
|
170
174
|
state: SystemOneStateSchema,
|
|
@@ -235,7 +239,9 @@ export const SystemOneDecisionConfigSchema = z
|
|
|
235
239
|
}
|
|
236
240
|
}
|
|
237
241
|
// Provider limits. A {{token}} model is skipped at save and checked again at run,
|
|
238
|
-
// where the interpolated config is parsed by this same schema.
|
|
242
|
+
// where the interpolated config is parsed by this same schema. With no provider
|
|
243
|
+
// the default is read at run time, so `decide` checks the limits there.
|
|
244
|
+
if (config.provider === undefined) return;
|
|
239
245
|
const modelProblem = systemOneModelProblem(
|
|
240
246
|
config.provider,
|
|
241
247
|
config.model ?? (SYSTEM_ONE_PROVIDERS[config.provider] as SystemOneProvider).defaultModel,
|
|
@@ -847,6 +853,11 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
|
|
|
847
853
|
return { db: this.deps.db, env: this.options.env ?? process.env };
|
|
848
854
|
}
|
|
849
855
|
|
|
856
|
+
/** The provider a config without `provider` uses, read now so a settings change applies at once. */
|
|
857
|
+
private defaultProvider(): Promise<SystemOneProviderId> {
|
|
858
|
+
return resolveSystemOneDefaultProvider(this.providerContext());
|
|
859
|
+
}
|
|
860
|
+
|
|
850
861
|
private resolveTarget(id: SystemOneProviderId, model?: string) {
|
|
851
862
|
return resolveSystemOneTarget(id, this.providerContext(), {
|
|
852
863
|
apiKey: this.options.getApiKey,
|
|
@@ -863,9 +874,10 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
|
|
|
863
874
|
nodes: readonly ExecutorReadinessNode[],
|
|
864
875
|
): Promise<ExecutorReadinessProblem[]> {
|
|
865
876
|
const nodeIdsByProvider = new Map<SystemOneProviderId, string[]>();
|
|
877
|
+
const defaultId = await this.defaultProvider();
|
|
866
878
|
for (const node of nodes) {
|
|
867
879
|
// An unknown provider is a config error the schema reports; it names no credential.
|
|
868
|
-
const id = systemOneProviderOf(node.config);
|
|
880
|
+
const id = systemOneProviderOf(node.config, defaultId);
|
|
869
881
|
if (id) nodeIdsByProvider.set(id, [...(nodeIdsByProvider.get(id) ?? []), node.id]);
|
|
870
882
|
}
|
|
871
883
|
const problems: ExecutorReadinessProblem[] = [];
|
|
@@ -903,16 +915,25 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
|
|
|
903
915
|
*/
|
|
904
916
|
async decide(
|
|
905
917
|
config: SystemOneDecisionConfig,
|
|
906
|
-
): Promise<
|
|
907
|
-
|
|
918
|
+
): Promise<
|
|
919
|
+
| { ok: true; output: SystemOneDecisionOutput }
|
|
920
|
+
| { ok: false; error: string; deploymentRefused?: true }
|
|
921
|
+
> {
|
|
922
|
+
const providerId = config.provider ?? (await this.defaultProvider());
|
|
908
923
|
const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[providerId];
|
|
909
924
|
|
|
910
925
|
// A provider with no default sends no `model` when the node sets none.
|
|
911
926
|
const model = config.model ?? provider.defaultModel;
|
|
912
927
|
const modelProblem = systemOneModelProblem(providerId, model);
|
|
913
928
|
if (modelProblem) return { ok: false, error: modelProblem };
|
|
929
|
+
// Checked at save only for a named provider; a defaulted one is checked here.
|
|
930
|
+
const limitProblem = systemOneQuestionLimitProblem(
|
|
931
|
+
providerId,
|
|
932
|
+
Object.keys(config.questions).length,
|
|
933
|
+
);
|
|
934
|
+
if (limitProblem) return { ok: false, error: limitProblem };
|
|
914
935
|
const target = await this.resolveTarget(providerId, model);
|
|
915
|
-
if (!target.ok) return
|
|
936
|
+
if (!target.ok) return target;
|
|
916
937
|
const { apiKey, endpoint, keyName } = target;
|
|
917
938
|
|
|
918
939
|
const wire = provider.wire ?? SYSTEM_ONE_WIRE;
|