@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
|
@@ -6,8 +6,9 @@ import { z } from "zod";
|
|
|
6
6
|
*
|
|
7
7
|
* Every human message in a Slack thread the swarm takes part in is buffered and
|
|
8
8
|
* flushed into a Lead task ("[Thread follow-up — N message(s) buffered]"), even
|
|
9
|
-
* "lol", ":joy:" or two people bantering. This hook asks
|
|
10
|
-
* Lead should answer, and drops the wake-up when the
|
|
9
|
+
* "lol", ":joy:" or two people bantering. This hook asks a SystemOne decision
|
|
10
|
+
* model (Jev or Clef) whether Lead should answer, and drops the wake-up when the
|
|
11
|
+
* answer is no.
|
|
11
12
|
*
|
|
12
13
|
* Hook: `pre.task.create`, origin "slack", description carrying the
|
|
13
14
|
* thread-buffer header. `pre.slack.route` cannot be used: the buffered thread
|
|
@@ -15,17 +16,20 @@ import { z } from "zod";
|
|
|
15
16
|
* re-target a message, never drop it.
|
|
16
17
|
*
|
|
17
18
|
* Rules:
|
|
18
|
-
* - A message that @mentions Lead passes without a
|
|
19
|
-
* - Otherwise one
|
|
19
|
+
* - A message that @mentions Lead passes without a decision call.
|
|
20
|
+
* - Otherwise one `choice` question classifies the message into a reason;
|
|
20
21
|
* reply=false only for reaction_only | banter_between_others |
|
|
21
22
|
* not_addressed_to_lead, and only when the summed probability of those
|
|
22
23
|
* three labels is >= minConfidence (a banter/reaction split is still a skip).
|
|
23
24
|
* - Fails OPEN on every error, timeout or missing key.
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Provider (config.provider): the decision is made server-side by the
|
|
27
|
+
* `system-one-decide` tool (`ctx.swarm.systemOne_decide`), the same path as a
|
|
28
|
+
* `system-one-decision` workflow node, so every SystemOne host works and no key
|
|
29
|
+
* enters this hook. `auto` picks the first host with a configured credential:
|
|
30
|
+
* typesafe (Jev, TYPESAFE_API_KEY), openrouter (Jev, OPENROUTER_API_KEY), then
|
|
31
|
+
* cloudflare (Clef, CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY, plus
|
|
32
|
+
* CLOUDFLARE_ACCOUNT_ID). laya and openai are explicit choices only.
|
|
29
33
|
*
|
|
30
34
|
* modes (config.mode):
|
|
31
35
|
* off -> never evaluate
|
|
@@ -48,17 +52,19 @@ import { z } from "zod";
|
|
|
48
52
|
export const config = z.object({
|
|
49
53
|
mode: z.enum(["off", "shadow", "enforce"]).default("shadow"),
|
|
50
54
|
timeoutMs: z.number().int().positive().default(3000),
|
|
51
|
-
provider: z
|
|
52
|
-
|
|
55
|
+
provider: z
|
|
56
|
+
.enum(["auto", "typesafe", "openrouter", "cloudflare", "laya", "openai"])
|
|
57
|
+
.default("auto"),
|
|
58
|
+
/** Jev model on TypeSafe. */
|
|
53
59
|
model: z.string().default("jev-latest"),
|
|
54
|
-
/** Jev model on
|
|
60
|
+
/** Jev model on OpenRouter. */
|
|
55
61
|
openrouterModel: z.string().default("typesafe/jev-1.13"),
|
|
62
|
+
/** Clef model on Cloudflare Workers AI: clef or clef-flash. */
|
|
63
|
+
cloudflareModel: z.string().default("clef"),
|
|
56
64
|
minConfidence: z.number().min(0).max(1).default(0.55),
|
|
57
65
|
contextMessages: z.number().int().min(0).max(20).default(6),
|
|
58
66
|
leadAliases: z.array(z.string()).default(["lead"]),
|
|
59
67
|
botSlackUserIds: z.array(z.string()).default([]),
|
|
60
|
-
/** Explicit TypeSafe key; otherwise TYPESAFE_API_KEY from the API process env. */
|
|
61
|
-
apiKey: z.string().optional(),
|
|
62
68
|
/**
|
|
63
69
|
* Reaction for a dropped message in enforce mode. "" only removes the bot's
|
|
64
70
|
* acceptance reaction(s) and adds nothing.
|
|
@@ -71,13 +77,13 @@ type Cfg = z.infer<typeof config>;
|
|
|
71
77
|
type Swarm = ApiCtx["swarm"];
|
|
72
78
|
|
|
73
79
|
const NS = "ext:slack-reply-gate";
|
|
74
|
-
const TYPESAFE_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
|
|
75
|
-
const OPENROUTER_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
|
|
76
80
|
const HEADER_LINE_RE = /(?:^|\n)\[Thread follow-up — \d+ message\(s\) buffered\]\n/g;
|
|
77
81
|
const CTX_OPEN = "<thread_context>";
|
|
78
82
|
const CTX_CLOSE = "</thread_context>";
|
|
79
83
|
// The dispatcher cancels a handler after 5 s; leave room for the KV writes.
|
|
80
84
|
const MAX_TIMEOUT_MS = 4000;
|
|
85
|
+
// The smallest time budget system-one-decide accepts.
|
|
86
|
+
const MIN_DECIDE_TIMEOUT_MS = 1000;
|
|
81
87
|
|
|
82
88
|
export const REASONS = {
|
|
83
89
|
direct_ask:
|
|
@@ -179,14 +185,14 @@ const usable = (v: string): boolean => Boolean(v) && !v.startsWith("[REDACTED");
|
|
|
179
185
|
const envOf = (name: string): string => String(process.env[name] ?? "");
|
|
180
186
|
|
|
181
187
|
/**
|
|
182
|
-
* Resolve a secret
|
|
183
|
-
*
|
|
188
|
+
* Resolve a secret from the API process env. The API process loads global
|
|
189
|
+
* swarm_config rows into process.env at boot and on config reload.
|
|
184
190
|
* ctx.swarm.config_get cannot serve it: the SDK scrubs secrets to "[REDACTED:<name>]".
|
|
185
191
|
*/
|
|
186
|
-
const secret = (name: string
|
|
192
|
+
const secret = (name: string): string => {
|
|
187
193
|
const cached = secretCache.get(name);
|
|
188
194
|
if (cached && Date.now() - cached.at < KEY_TTL_MS) return cached.value;
|
|
189
|
-
const value =
|
|
195
|
+
const value = usable(envOf(name)) ? envOf(name) : "";
|
|
190
196
|
if (usable(value)) secretCache.set(name, { value, at: Date.now() });
|
|
191
197
|
return value;
|
|
192
198
|
};
|
|
@@ -196,46 +202,51 @@ export const resetKeyCache = (): void => {
|
|
|
196
202
|
secretCache.clear();
|
|
197
203
|
};
|
|
198
204
|
|
|
199
|
-
export type
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
+
export type ProviderId = Exclude<Cfg["provider"], "auto">;
|
|
206
|
+
|
|
207
|
+
/** Where the decision is made. No model means the provider's own default. */
|
|
208
|
+
export type DecisionTarget = { provider: ProviderId; model?: string };
|
|
209
|
+
|
|
210
|
+
/** What `auto` needs per provider: every group must have one usable value. */
|
|
211
|
+
const AUTO_CREDENTIALS: ReadonlyArray<[ProviderId, string[][]]> = [
|
|
212
|
+
["typesafe", [["TYPESAFE_API_KEY"]]],
|
|
213
|
+
["openrouter", [["OPENROUTER_API_KEY"]]],
|
|
214
|
+
["cloudflare", [["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_KEY"], ["CLOUDFLARE_ACCOUNT_ID"]]],
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const modelFor = (provider: ProviderId, cfg: Cfg): string | undefined =>
|
|
218
|
+
provider === "typesafe"
|
|
219
|
+
? cfg.model
|
|
220
|
+
: provider === "openrouter"
|
|
221
|
+
? cfg.openrouterModel
|
|
222
|
+
: provider === "cloudflare"
|
|
223
|
+
? cfg.cloudflareModel
|
|
224
|
+
: undefined;
|
|
205
225
|
|
|
206
226
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
227
|
+
* The decision providers to try, in order. An explicit one is used as is: the
|
|
228
|
+
* server checks its credential. `auto` lists every provider in `AUTO_CREDENTIALS`
|
|
229
|
+
* whose credential is set and throws when none is; the handler then fails open.
|
|
209
230
|
*/
|
|
210
|
-
export const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const key = secret("OPENROUTER_API_KEY");
|
|
219
|
-
return usable(key)
|
|
220
|
-
? { provider: "openrouter", endpoint: OPENROUTER_ENDPOINT, key, model: cfg.openrouterModel }
|
|
221
|
-
: null;
|
|
222
|
-
};
|
|
223
|
-
const transport =
|
|
224
|
-
cfg.provider === "typesafe"
|
|
225
|
-
? typesafe()
|
|
226
|
-
: cfg.provider === "openrouter"
|
|
227
|
-
? openrouter()
|
|
228
|
-
: (typesafe() ?? openrouter());
|
|
229
|
-
if (transport) return transport;
|
|
231
|
+
export const resolveProviders = (cfg: Cfg): DecisionTarget[] => {
|
|
232
|
+
if (cfg.provider !== "auto") {
|
|
233
|
+
return [{ provider: cfg.provider, model: modelFor(cfg.provider, cfg) }];
|
|
234
|
+
}
|
|
235
|
+
const targets = AUTO_CREDENTIALS.filter(([, groups]) =>
|
|
236
|
+
groups.every((keys) => keys.some((key) => usable(secret(key)))),
|
|
237
|
+
).map(([provider]) => ({ provider, model: modelFor(provider, cfg) }));
|
|
238
|
+
if (targets.length > 0) return targets;
|
|
230
239
|
throw new Error(
|
|
231
|
-
|
|
232
|
-
? "no TYPESAFE_API_KEY"
|
|
233
|
-
: cfg.provider === "openrouter"
|
|
234
|
-
? "no OPENROUTER_API_KEY"
|
|
235
|
-
: "no TYPESAFE_API_KEY or OPENROUTER_API_KEY",
|
|
240
|
+
"no TYPESAFE_API_KEY, OPENROUTER_API_KEY, or CLOUDFLARE_API_TOKEN with CLOUDFLARE_ACCOUNT_ID",
|
|
236
241
|
);
|
|
237
242
|
};
|
|
238
243
|
|
|
244
|
+
/** The first provider `resolveProviders` would try. */
|
|
245
|
+
export const resolveProvider = (cfg: Cfg): DecisionTarget => resolveProviders(cfg)[0]!;
|
|
246
|
+
|
|
247
|
+
/** The server refused the provider for a deployment setting and sent nothing. */
|
|
248
|
+
export class ProviderUnavailableError extends Error {}
|
|
249
|
+
|
|
239
250
|
const SLACK_API = "https://slack.com/api";
|
|
240
251
|
// The engine's acceptance reactions (src/slack/reaction-shortcode.ts), by config key.
|
|
241
252
|
const ACCEPTANCE_REACTIONS: ReadonlyArray<[string, string]> = [
|
|
@@ -323,7 +334,7 @@ export const settleMutes = async (): Promise<void> => {
|
|
|
323
334
|
await Promise.allSettled([...pendingMutes]);
|
|
324
335
|
};
|
|
325
336
|
|
|
326
|
-
export type
|
|
337
|
+
export type Verdict = {
|
|
327
338
|
reason: string;
|
|
328
339
|
confidence: number | null;
|
|
329
340
|
probabilities: Record<string, number> | null;
|
|
@@ -331,56 +342,73 @@ export type JevVerdict = {
|
|
|
331
342
|
};
|
|
332
343
|
|
|
333
344
|
/**
|
|
334
|
-
* One
|
|
335
|
-
*
|
|
345
|
+
* One decision: classify the newest message into a reason. Throws on any failure.
|
|
346
|
+
* The server makes the call (`system-one-decide`), so every provider returns the
|
|
347
|
+
* same `answers` shape. The time budget is enforced here and on the server.
|
|
336
348
|
*/
|
|
337
|
-
export const
|
|
349
|
+
export const askModel = async (
|
|
350
|
+
swarm: Swarm,
|
|
338
351
|
input: {
|
|
339
352
|
messages: string[];
|
|
340
353
|
context: string[];
|
|
341
354
|
sender: string;
|
|
342
355
|
leadNames: readonly string[];
|
|
343
356
|
},
|
|
344
|
-
|
|
357
|
+
target: DecisionTarget,
|
|
345
358
|
cfg: Cfg,
|
|
346
359
|
outer?: AbortSignal,
|
|
347
|
-
): Promise<
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
360
|
+
): Promise<Verdict> => {
|
|
361
|
+
const budgetMs = Math.min(cfg.timeoutMs, MAX_TIMEOUT_MS);
|
|
362
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
363
|
+
let onOuterAbort: (() => void) | undefined;
|
|
364
|
+
const stop = new Promise<never>((_resolve, reject) => {
|
|
365
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${budgetMs}ms`)), budgetMs);
|
|
366
|
+
onOuterAbort = () => reject(new Error("aborted"));
|
|
367
|
+
outer?.addEventListener("abort", onOuterAbort);
|
|
368
|
+
});
|
|
352
369
|
const t0 = Date.now();
|
|
353
370
|
try {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
371
|
+
const call = swarm.systemOne_decide({
|
|
372
|
+
provider: target.provider,
|
|
373
|
+
...(target.model ? { model: target.model } : {}),
|
|
374
|
+
state: {
|
|
375
|
+
role: "Decide whether Lead, the coordinating AI agent of a team's agent swarm, should answer a new Slack message in a thread it takes part in. Lead is woken by every message in the thread, including ones not meant for it. Answering noise wastes everyone's time; missing a real ask is worse.",
|
|
376
|
+
lead_names: [...input.leadNames],
|
|
377
|
+
recent_thread_messages: input.context,
|
|
378
|
+
new_message: input.messages.join("\n---\n"),
|
|
379
|
+
new_message_sender: input.sender,
|
|
359
380
|
},
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
new_message_sender: input.sender,
|
|
367
|
-
},
|
|
368
|
-
model: transport.model,
|
|
369
|
-
questions: {
|
|
370
|
-
reason: {
|
|
371
|
-
type: "choice",
|
|
372
|
-
instructions:
|
|
373
|
-
"Which label best describes new_message from Lead's point of view? Use recent_thread_messages to tell who it is talking to. Lines starting with [Agent] are swarm bots.",
|
|
374
|
-
criteria: REASONS,
|
|
375
|
-
},
|
|
381
|
+
questions: {
|
|
382
|
+
reason: {
|
|
383
|
+
type: "choice",
|
|
384
|
+
instructions:
|
|
385
|
+
"Which label best describes new_message from Lead's point of view? Use recent_thread_messages to tell who it is talking to. Lines starting with [Agent] are swarm bots.",
|
|
386
|
+
criteria: REASONS,
|
|
376
387
|
},
|
|
377
|
-
}
|
|
378
|
-
|
|
388
|
+
},
|
|
389
|
+
timeoutMs: Math.max(budgetMs, MIN_DECIDE_TIMEOUT_MS),
|
|
390
|
+
maxRetries: 0,
|
|
379
391
|
});
|
|
380
|
-
|
|
381
|
-
|
|
392
|
+
// After a timeout nobody awaits the call; its late failure must not go unhandled.
|
|
393
|
+
call.catch(() => {});
|
|
394
|
+
const res = (await Promise.race([call, stop])) as {
|
|
395
|
+
success?: boolean;
|
|
396
|
+
status?: number;
|
|
397
|
+
data?: unknown;
|
|
398
|
+
} | null;
|
|
399
|
+
const body = (res?.data ?? res) as {
|
|
400
|
+
success?: boolean;
|
|
401
|
+
message?: unknown;
|
|
402
|
+
error?: unknown;
|
|
403
|
+
providerUnavailable?: unknown;
|
|
382
404
|
answers?: { reason?: { choice?: unknown; confidence?: unknown; probabilities?: unknown } };
|
|
383
405
|
} | null;
|
|
406
|
+
if (res?.success === false || body?.success === false) {
|
|
407
|
+
const why = `${target.provider}: ${String(body?.message ?? body?.error ?? `HTTP ${res?.status}`).slice(0, 200)}`;
|
|
408
|
+
throw body?.providerUnavailable === true
|
|
409
|
+
? new ProviderUnavailableError(why)
|
|
410
|
+
: new Error(why);
|
|
411
|
+
}
|
|
384
412
|
const answer = body?.answers?.reason;
|
|
385
413
|
const reason = typeof answer?.choice === "string" ? answer.choice : "";
|
|
386
414
|
if (!(reason in REASONS)) throw new Error(`unexpected choice: ${String(reason).slice(0, 60)}`);
|
|
@@ -392,12 +420,34 @@ export const askJev = async (
|
|
|
392
420
|
return { reason, confidence, probabilities, latencyMs: Date.now() - t0 };
|
|
393
421
|
} finally {
|
|
394
422
|
clearTimeout(timer);
|
|
395
|
-
outer?.removeEventListener("abort", onOuterAbort);
|
|
423
|
+
if (onOuterAbort) outer?.removeEventListener("abort", onOuterAbort);
|
|
396
424
|
}
|
|
397
425
|
};
|
|
398
426
|
|
|
427
|
+
/**
|
|
428
|
+
* `askModel` on each target in turn, moving on only when the server refused the
|
|
429
|
+
* provider for a deployment setting (no request was sent). Any other failure, or
|
|
430
|
+
* a refusal of the last target, throws.
|
|
431
|
+
*/
|
|
432
|
+
export const askFirstAvailable = async (
|
|
433
|
+
swarm: Swarm,
|
|
434
|
+
input: Parameters<typeof askModel>[1],
|
|
435
|
+
targets: readonly DecisionTarget[],
|
|
436
|
+
cfg: Cfg,
|
|
437
|
+
outer?: AbortSignal,
|
|
438
|
+
): Promise<{ verdict: Verdict; target: DecisionTarget }> => {
|
|
439
|
+
for (const [i, target] of targets.entries()) {
|
|
440
|
+
try {
|
|
441
|
+
return { verdict: await askModel(swarm, input, target, cfg, outer), target };
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (!(err instanceof ProviderUnavailableError) || i === targets.length - 1) throw err;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
throw new Error("no decision provider");
|
|
447
|
+
};
|
|
448
|
+
|
|
399
449
|
/** Probability that the message needs no reply: the summed mass of the skip labels. */
|
|
400
|
-
export const skipProbability = (verdict:
|
|
450
|
+
export const skipProbability = (verdict: Verdict): number | null => {
|
|
401
451
|
if (verdict.probabilities) {
|
|
402
452
|
let sum = 0;
|
|
403
453
|
for (const [label, p] of Object.entries(verdict.probabilities)) {
|
|
@@ -408,9 +458,9 @@ export const skipProbability = (verdict: JevVerdict): number | null => {
|
|
|
408
458
|
return verdict.confidence;
|
|
409
459
|
};
|
|
410
460
|
|
|
411
|
-
/** Map a
|
|
461
|
+
/** Map a verdict to the typed decision. An unsure skip verdict replies. */
|
|
412
462
|
export const decide = (
|
|
413
|
-
verdict:
|
|
463
|
+
verdict: Verdict,
|
|
414
464
|
minConfidence: number,
|
|
415
465
|
): { reply: boolean; reason: Reason; skipP: number | null; lowConfidence: boolean } => {
|
|
416
466
|
const reason = verdict.reason as Reason;
|
|
@@ -507,7 +557,7 @@ const extension: SwarmExtension = (api) => {
|
|
|
507
557
|
return;
|
|
508
558
|
}
|
|
509
559
|
|
|
510
|
-
const
|
|
560
|
+
const targets = resolveProviders(cfg);
|
|
511
561
|
|
|
512
562
|
const sender =
|
|
513
563
|
opts.slackUserId && cfg.botSlackUserIds.includes(opts.slackUserId)
|
|
@@ -516,14 +566,15 @@ const extension: SwarmExtension = (api) => {
|
|
|
516
566
|
? "human (known teammate)"
|
|
517
567
|
: "unknown (not a known teammate; may be a bot or an external person)";
|
|
518
568
|
|
|
519
|
-
const verdict = await
|
|
569
|
+
const { verdict, target } = await askFirstAvailable(
|
|
570
|
+
ctx.swarm,
|
|
520
571
|
{
|
|
521
572
|
messages,
|
|
522
573
|
context: context.slice(-cfg.contextMessages).map((m) => m.slice(0, 500)),
|
|
523
574
|
sender,
|
|
524
575
|
leadNames: cfg.leadAliases,
|
|
525
576
|
},
|
|
526
|
-
|
|
577
|
+
targets,
|
|
527
578
|
cfg,
|
|
528
579
|
ctx.signal,
|
|
529
580
|
);
|
|
@@ -542,7 +593,7 @@ const extension: SwarmExtension = (api) => {
|
|
|
542
593
|
skipP: decision.skipP,
|
|
543
594
|
lowConfidence: decision.lowConfidence,
|
|
544
595
|
latencyMs: verdict.latencyMs,
|
|
545
|
-
provider:
|
|
596
|
+
provider: target.provider,
|
|
546
597
|
sender,
|
|
547
598
|
preview,
|
|
548
599
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "../manifest.schema.json",
|
|
3
3
|
"name": "slack-reply-gate",
|
|
4
|
-
"description": "Asks
|
|
5
|
-
"version": "1.
|
|
4
|
+
"description": "Asks a SystemOne decision model (Jev or Clef) whether Lead should answer a buffered Slack thread follow-up, and drops the wake-up when it should not",
|
|
5
|
+
"version": "1.3.0",
|
|
6
6
|
"runtime": "api",
|
|
7
7
|
"assets": { "hooks": "hooks.ts" }
|
|
8
8
|
}
|
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
## Tracked Items
|
|
12
12
|
|
|
13
13
|
- Live items only: active blockers and watch items. Cap 10, 20 max during a surge; prune before you add.
|
|
14
|
-
- Each item states its lift trigger and a calendar date (e.g. `lift when X; check
|
|
14
|
+
- Each item states its lift trigger and a calendar date as a bare `MM-DD` (e.g. `lift when X; check 08-22`) or `DELETE MM-DD`. A full `YYYY-MM-DD` stamp does not arm the gate.
|
|
15
15
|
- An item closes: move its durable facts to memory, then delete the line. Never strike it through.
|
|
@@ -475,7 +475,7 @@ Param `kind` is `string` by default, or `number` / `boolean`; `required` default
|
|
|
475
475
|
|
|
476
476
|
Within each page, `root` names one entry in its non-empty `elements` map. Elements are a flat, single-parent tree: ids are map keys, `children` contains ids (not nested elements), all elements must be reachable from `root`, and cycles, missing children, or shared children are invalid. Element and Form/UI ids are page-local. Element keys are `type`, `props`, `children`, `on`, `visible`, `repeat`, and `watch`; only components with a `default` child slot accept `children` (Stack, Grid, Split, Tabs, Container, Card, Section, and Drawer). `watch` maps state paths to an action step or chain.
|
|
477
477
|
|
|
478
|
-
## Component catalog (all
|
|
478
|
+
## Component catalog (all 34)
|
|
479
479
|
|
|
480
480
|
Props reject unknown keys. A `{"$state":"..."}` binding may replace a literal prop value at any depth.
|
|
481
481
|
|
|
@@ -485,9 +485,9 @@ Tones: Badge, Metric and Progress `tone` take `neutral`, `success`, `active`, `e
|
|
|
485
485
|
|
|
486
486
|
| Component | Required props | Optional props / values |
|
|
487
487
|
|---|---|---|
|
|
488
|
-
| `Stack` | none | Primary layout; `direction: "column", "row"`, `gap`, `padding: "none", "xs", "sm", "md", "lg", "xl"`; `align: "start", "center", "end", "stretch"`, `justify: "start", "center", "end", "between"`, `wrap`; `motion: "rise"` (default), `"fade"`, or `"none"` (see Motion below); has the `default` child slot. |
|
|
488
|
+
| `Stack` | none | Primary layout; `direction: "column", "row"`, `gap`, `padding: "none", "xs", "sm", "md", "lg", "xl"`; `align: "start", "center", "end", "stretch"`, `justify: "start", "center", "end", "between"`, `wrap`; `motion: "rise"` (default), `"fade"`, or `"none"` (see Motion below); `fill` (grow to fill a `fill`-aware parent; see Layout below); has the `default` child slot. |
|
|
489
489
|
| `Grid` | none | Responsive `columns`: integer 1 through 6, or `{ base, sm, md, lg }` counts 1 through 6; `gap` uses the Stack spacing values; has the `default` child slot. |
|
|
490
|
-
| `Split` | none | `ratio: "1-1", "1-2", "2-1", "1-3", "3-1"`, `gap`, `collapseBelow: "sm", "md", "lg"`, `reverse
|
|
490
|
+
| `Split` | none | `ratio: "1-1", "1-2", "2-1", "1-3", "3-1"`, `gap`, `collapseBelow: "sm", "md", "lg"`, `reverse`, `fill` (same contract as Stack); has the positional `default` child slot. |
|
|
491
491
|
| `Divider` | none | `label`; no children. |
|
|
492
492
|
| `Tabs` | `id`, `tabs` | `tabs` entries are `{ key, label?, icon?, count? }` (`count` is a number shown as a small pill); `defaultTab`; `variant: "line"` (default) or `"segmented"` (a compact segmented control, same state and warm panels); has the positional `default` child slot. |
|
|
493
493
|
| `Container` | none | Legacy layout primitive; prefer Stack. `direction: "row", "column"`, `gap: "none", "sm", "md", "lg"`; has the `default` child slot. |
|
|
@@ -496,7 +496,8 @@ Tones: Badge, Metric and Progress `tone` take `neutral`, `success`, `active`, `e
|
|
|
496
496
|
| `Text` | `content` | `tone: "default" or "muted"`. |
|
|
497
497
|
| `Markdown` | `content` | Rendered Markdown for help, instructions, and rich prose; no children. |
|
|
498
498
|
| `SearchInput` | `id` | `placeholder`, `label`, `shortcut` (one key, usually `"/"`: shows a keycap and focuses the field); writes debounced text to `/ui/<id>/value`; no children. |
|
|
499
|
-
| `Select` | `id`, `options` | `options` are strings or `{ value, label?, icon?, hint? }`; `placeholder`, `label`, `clearable`, `searchable` (type to filter; on by default past 8 options), `searchPlaceholder`, `multiple
|
|
499
|
+
| `Select` | `id`, `options` | `options` are strings or `{ value, label?, icon?, hint? }`; `placeholder`, `label`, `clearable`, `searchable` (type to filter; on by default past 8 options), `searchPlaceholder`, `multiple`, `defaultValue` (seeds `/ui/<id>/value` once, before any interaction; a one-entry array with `multiple`); writes a string or null to `/ui/<id>/value`, or with `multiple` a string array (empty = nothing picked); no children. |
|
|
500
|
+
| `NumberInput` | `id` | `label`, `placeholder`, `min`, `max`; a text field with a decimal keyboard that writes the parsed number to `/ui/<id>/value` on every change (no debounce), or null when the field is empty; non-numeric text (a bare `-` mid-edit, letters) leaves the stored value untouched; `min`/`max` clamp the stored value and the visible text snaps to it when the field loses focus; no children. |
|
|
500
501
|
| `Chips` | `id`, `options` | Toggle chips for quick filters. `options` are strings or `{ value, label?, icon? }`; `multiple`; `label` (group name for screen readers). Writes a string or null to `/ui/<id>/value`, or a string array with `multiple`; pressing the selected chip clears it. |
|
|
501
502
|
| `Button` | `label` | `variant: "default", "secondary", "outline", "ghost", "destructive", or "destructive-outline"` (use `destructive-outline` for standalone deletes); `disabled` (constant or bound boolean); `busyWith: "<actionName>"` swaps the label for a spinner while that custom action's `/actions/<name>/status` is `"running"` (presses are ignored), then shows a check and `successLabel` for about 1.5s when it turns `"ok"`; `icon`, `iconPosition: "start" or "end"`; `size: "sm", "default", or "lg"`; `shortcut` (one key, shown as a keycap, presses the button); `hint` (tooltip); dispatch with element-level `on.press`. |
|
|
502
503
|
| `Metric` | `label`, `value` | The dashboard stat tile. `value` is string or number (empty renders an em dash); bind `loading` to `/queries/<name>/loading` for a skeleton while the query loads; `icon` with `tone` tinting its tile; `detail` (a line under the value); `trend: { direction: "up", "down", or "flat", label }`; `info` (explanation tooltip next to the label). |
|
|
@@ -515,17 +516,20 @@ Tones: Badge, Metric and Progress `tone` take `neutral`, `success`, `active`, `e
|
|
|
515
516
|
| `Form` | `id`, `fields`, `onSubmit` | `title`, `submitLabel`; see below. |
|
|
516
517
|
| `Drawer` | `param` | `title`, `description`, `side: "right" or "left"`, `size: "sm", "md" (default), "lg", or "xl"` (about 480/660/900/1010px at 1440px; grows with the viewport up to 640/960/1280/1600px; full width on phones), `actions` (sticky footer buttons; see below); has the `default` child slot. |
|
|
517
518
|
| `DetailList` | `fields` | `data`, `emptyMessage`, `columns: 1 or 2`, `loading` (bind `/queries/<name>/loading` for skeleton fields while loading); renders one record without edit controls; see below for field kinds. |
|
|
519
|
+
| `CardGrid` | `titleKey` | `data`, `loading`, `error`, `emptyMessage`, `imageKey`, `fields`, `columns`, `onClick`; one card per row for record browsers; see below. |
|
|
518
520
|
| `Kanban` | `columnField`, `columns`, `titleField` | `data`, `loading`, `cardFields`, `idField` (default `id`), `onMove`, `onCardClick`; see below. |
|
|
519
521
|
| `Calendar` | `startField`, `titleField` | `data`, `loading`, `endField`, `idField`, `onSelect`; read-only month grid; see below. |
|
|
520
522
|
| `ContributionGraph` | none | `data`, `loading`, `dateField` (default `date`), `countField`, `days` (7-730, default 365); read-only activity heatmap. |
|
|
523
|
+
| `Tree` | `id`, `nodes` | `label`, `maxHeight` (default `480px`), `fill`; collapsible outline of a doc-shaped value that writes the clicked node key to `/ui/<id>/value`; see below. |
|
|
521
524
|
|
|
522
525
|
Table details:
|
|
523
526
|
|
|
524
|
-
- `columns[]`: `{ key, label?, kind?, tones?: {value: badgeTone}, width?: number, pinned?: "left" or "right", align?: "start" or "end", truncate?: "end" or "middle", icon?, tooltip?: "<row field>", max?: number, onChange?: chain }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, `badge`, `status`, `user`, `agent`, `relative`, `duration`, `progress`, `copy`, `switch`, `link`, or `json`; `string` and `enum` render as text, and `json` renders a truncated single-line `JSON.stringify` preview. Pin id/actions columns so they survive horizontal scroll on narrow viewports.
|
|
527
|
+
- `columns[]`: `{ key, label?, kind?, tones?: {value: badgeTone}, width?: number, pinned?: "left" or "right", align?: "start" or "end", truncate?: "end" or "middle", icon?, tooltip?: "<row field>", max?: number, format?: "<template>", onChange?: chain }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, `badge`, `status`, `user`, `agent`, `relative`, `duration`, `progress`, `copy`, `switch`, `link`, or `json`; `string` and `enum` render as text, and `json` renders a truncated single-line `JSON.stringify` preview. Pin id/actions columns so they survive horizontal scroll on narrow viewports.
|
|
525
528
|
- `status`: lifecycle values (`pending`, `in_progress`, `running`, `completed`, `failed`, `cancelled`, `paused`, `reviewing`, ...) render the dashboard status chip with its icon; other values use `tones` like `badge`.
|
|
526
529
|
- `user` / `agent`: avatar plus name. `relative` (and `date`): smart time, exact time on hover. `duration`: milliseconds as `1m 23s`. `progress`: a bar against `max` (default 100). `copy`: the value plus a copy button. `link`: an http(s) URL opening in a new tab (other values render as text).
|
|
527
530
|
- `switch`: a toggle. `onChange` runs with `$row` = the row with this column already set to the new value, e.g. `[{ "action": "app.mutate", "params": { "model": "m", "op": "update", "rowId": { "$row": "id" }, "values": { "enabled": { "$row": "enabled" } } } }]`. Without `onChange` the switch is read-only.
|
|
528
531
|
- Numbers, durations and progress right-align by default. `truncate: "middle"` keeps both ends of ids and paths. `icon` shows in the header; `tooltip` names a row field shown on hover.
|
|
532
|
+
- `format`: a `{{field}}` template that combines several row fields into one cell, e.g. `"{{name}} ({{status}})"`. Placeholder names match `[\w.]+` (letters, digits, `_` and `.`; no hyphens or spaces). Each placeholder reads that row field as-is (a dotted name such as `{{owner.name}}` is a literal column name, not a nested path); a missing or null field becomes an empty string and an object or array becomes a JSON preview cut to 120 characters. The whole cell is capped at 500 characters. A templated column ignores `kind`, shows the same text in the phone list, and sorts by the composed text. `search` and `filters` still read the column's `key`.
|
|
529
533
|
- `onRowClick`: action chain with `$row` / `$rowIndex`, run when a row is clicked (or Enter on a focused cell). Pair it with `app.navigate` that sets a Drawer param, and bind `selectedId` to `{ "$state": "/route/params/<param>" }` so the open row stays highlighted. `rowId` names the id field (default `id`).
|
|
530
534
|
- Phones (below 768px): the table renders as a card list. `mobileFields: { title, subtitle?, meta?: string or string[], status? }` picks the columns; by default the first text column is the title and the first `status`/`badge` column the chip. `mobile: "scroll"` keeps the grid; it is the default when the table has `rowActions` but no `onRowClick`.
|
|
531
535
|
- `emptyState: { icon?, title, body? }` replaces the plain empty message. A search or filter that hides every row shows its own "No rows match" state.
|
|
@@ -550,6 +554,12 @@ ContributionGraph details:
|
|
|
550
554
|
|
|
551
555
|
- Buckets `data` rows by the calendar day of `dateField` over the trailing `days`. Each row counts 1, or adds its numeric `countField` when set.
|
|
552
556
|
|
|
557
|
+
Tree details:
|
|
558
|
+
|
|
559
|
+
- `nodes` is a doc-shaped value: `{ root: string | string[], elements: { <key>: { type, children?: string[] } } }`, the json-render flat element map. Usually bind it to a json column: `{ "$state": "/queries/<name>/data/0/<column>" }`. A malformed or absent value renders an empty state; a `children` key missing from `elements` shows as `key — ?`. Outlines are cut at 2,000 rows with a notice; branches deeper than 32 levels keep a disabled chevron.
|
|
560
|
+
- Each row shows `key — type`. Clicking a row writes its key to `/ui/<id>/value`, and the row bound to that path renders selected; read the selection with `{ "$state": "/ui/<id>/value" }`. The Tree never writes `null`: when the selected key leaves the doc (a refetch, an edit) the stored key stays and no row renders selected. Selection and collapse are keyed by node key, so a key reachable through two parents selects and collapses in both places.
|
|
561
|
+
- Rows scroll inside `maxHeight` (default `480px`). Set `fill: true` inside a `fill` Stack or Split pane to grow to that height instead and scroll within it (ignores `maxHeight`).
|
|
562
|
+
|
|
553
563
|
Form details:
|
|
554
564
|
|
|
555
565
|
- `fields[]`: `{ name, label?, kind?, options?: string[], placeholder?, required? }`; `kind` is `string`, `text`, `number`, `boolean`, `date`, or `enum`, and enum fields need `options`.
|
|
@@ -569,6 +579,16 @@ Motion details:
|
|
|
569
579
|
|
|
570
580
|
- Every `Stack` fades its direct children in and lifts them 4px over 200ms on first paint, staggered 30ms (capped at the 10th child). It never replays on the 5s data poll. A Stack nested inside another one does not add its own entrance. `motion: "fade"` drops the lift; `motion: "none"` turns it off. Viewers with reduced motion get a 150ms fade.
|
|
571
581
|
|
|
582
|
+
CardGrid details — the visual counterpart to Table for record browsers (projects, documents, media):
|
|
583
|
+
|
|
584
|
+
- `titleKey` (required) picks the row field shown as the card title (an object value shows a JSON preview; titles are capped at 500 characters).
|
|
585
|
+
- `imageKey` (optional) picks a row field holding an absolute http(s) image URL and adds an image tile to every card. When it is set, a non-URL value, a missing value, or a failed load shows a placeholder tile. Omit it for text-only cards with no tile.
|
|
586
|
+
- `fields[]`: `{ key, kind?, tones? }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, `badge` (with `tones`), or `json` (no `label`, no `format`). A field with no `kind` whose value is an object renders like `json`. They render as an unlabeled `·`-joined meta line under the title, e.g. a `date` field for "created" and a `text` field for the author.
|
|
587
|
+
- `columns`: same responsive shorthand as `Grid.columns` (default `{ "base": 1, "sm": 2, "md": 3 }`).
|
|
588
|
+
- `onClick`: an action chain with `$row` / `$rowIndex`, run when a card is clicked or activated with Enter or Space. Commonly `app.navigate` into a detail page, the CardGrid equivalent of Table's `onRowClick`.
|
|
589
|
+
- Bind `data` / `loading` / `error` to a named query like Table. Loading shows skeleton cards, an error shows a callout, and no rows shows `emptyMessage`.
|
|
590
|
+
- It renders 200 cards at a time with a Show more button below the grid that reveals the next 200.
|
|
591
|
+
|
|
572
592
|
Shortcut details:
|
|
573
593
|
|
|
574
594
|
- `Button.shortcut` and `SearchInput.shortcut` are one key. Letters and digits match without Shift; symbols such as `/` and `?` match as typed. They do nothing while focus is in a text field, while Ctrl, Cmd, or Alt is held, or while a dialog or menu is open (unless the control is inside it, such as a Button in an open Drawer).
|
|
@@ -577,9 +597,11 @@ Shortcut details:
|
|
|
577
597
|
|
|
578
598
|
Use `Stack` as the primary page and section layout; `Container` is the legacy two-prop primitive retained for existing pages. Stack supports vertical or horizontal flow, shared spacing, alignment, justification, wrapping, and padding; row Stacks (filter bars, button rows) should set `collapseBelow: "sm"|"md"|"lg"` so they stack vertically on phones. Use `Grid` for responsive card or metric strips: a bare column count is responsive shorthand (1 column on phones, 2 from `sm`, the count from `md` up), or pin breakpoints explicitly with `{ "base": 1, "md": 2, "lg": 3 }`. `Grid` and `Split` also accept `padding` on the shared spacing scale.
|
|
579
599
|
|
|
600
|
+
`Stack` and `Split` size to their content by default, so a page scrolls as a whole. Set `fill: true` to make one grow to fill the remaining height of a `fill`-aware parent (another `fill` Stack or Split) instead; chain it from the page root down through every container on the path to the element that should scroll, and that element scrolls within whatever height is left while the page itself does not. A non-`fill` sibling (a toolbar, a header) keeps its natural height and the `fill` sibling absorbs the rest. A `fill` Split stretches both panes to the full row height, so nesting a `fill` Split inside one pane of another `fill` Split is the way to build a three-or-more column viewport-height layout (`ratio` is only ever two panes).
|
|
601
|
+
|
|
580
602
|
`Split` and `Tabs` children are positional. For Split, `children[0]` is the first pane, `children[1]` is the second, and extra children append inside the second pane. Below `collapseBelow`, panes stack; `reverse` changes only that narrow-layout stacking order. For Tabs, `children[i]` is the body for `tabs[i]`; keep both arrays in the same order and with the same count. Inactive tab children stay mounted but hidden, so Tables keep polling. Use `Divider` to separate sections and `Markdown` for richer explanatory content.
|
|
581
603
|
|
|
582
|
-
SearchInput, Select,
|
|
604
|
+
SearchInput, Select, NumberInput, Chips, and Tree are client-side controls: each needs a literal `id` and writes state under `/ui/<id>/value` (Tree writes the clicked node key); Tabs writes its active key to `/ui/<id>/tab`. Bind those values into a Table's `search` and `filters` props. This filters already-polled rows locally; it does not alter or re-run the named query.
|
|
583
605
|
|
|
584
606
|
```json
|
|
585
607
|
{
|
|
@@ -645,6 +667,8 @@ Only inside action-chain `params` (`on.<event>`, `Table.rowActions[].actions`, o
|
|
|
645
667
|
| `{ "$form": "title" }` | One current Form value. |
|
|
646
668
|
| `{ "$form": "" }` | All current Form values. |
|
|
647
669
|
| `{ "$state": "/queries/allIdeas/data" }` | Current value at a valid app state path, including action state such as `/actions/refreshIssues/status`. |
|
|
670
|
+
| `{ "$chain": "leafTaskId" }` | One chain-scope value, published by a component that runs a task chain; it carries `leafTaskId` and `newTaskId`. Where no chain scope is published, the sentinel is left in place and the action fails instead of silently dropping the parent. |
|
|
671
|
+
| `{ "$chain": "" }` | The whole chain scope. |
|
|
648
672
|
|
|
649
673
|
Each sentinel object must contain exactly the single key shown.
|
|
650
674
|
|
|
@@ -652,7 +676,7 @@ An action-chain step is `{ "action": "<type>", "params": {...} }`. Available act
|
|
|
652
676
|
|
|
653
677
|
- `app.mutate`: `{ model, op, rowId?, values?, formId? }`, where `op` is `"create"`, `"update"`, or `"delete"`. `update`/`delete` require `rowId` (usually `{ "$row": "id" }`, or `{ "$state": "/route/params/<param>" }` in a Drawer footer); literal `values` keys must be model columns. Successful mutation refetches all queries on that model.
|
|
654
678
|
- `app.refresh`: `{ query? }`; omit `query` to refetch all, or name a declared query.
|
|
655
|
-
- `app.action`: `{ name, input? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { "$row": "" } }`.
|
|
679
|
+
- `app.action`: `{ name, input?, parentTaskId? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { "$row": "" } }`. `parentTaskId` chains the created task under an existing one for task-kind actions (typically `{ "$chain": "leafTaskId" }`; a literal or `$state` task id also works); a script or sync action rejects it with 400.
|
|
656
680
|
- `app.navigate`: `{ page, params? }`; `page` must be declared, supplied keys must be params of the target page, and every required target param must be supplied. `params` replace the current route params wholesale and may contain action sentinels. Navigation pushes history, so browser Back returns to the previous page/params; only `mode` is preserved automatically.
|
|
657
681
|
- `ui.toast`: `{ message, description?, tone?, undo? }`, where `tone` is `"success"`, `"error"`, `"info"` (default), or `"warning"`. `undo: { label?, actions }` adds an Undo button (default label `Undo`) that runs `actions` as a chain; its steps are validated like any chain. Put it after the step it confirms, e.g. a row delete followed by `{ "action": "ui.toast", "params": { "message": "Order deleted", "undo": { "actions": [{ "action": "app.mutate", "params": { "model": "order", "op": "create", "values": { "title": { "$row": "title" } } } }] } } }`. Undo params resolve `$state` when Undo is pressed; `$row` / `$form` resolve when the toast is shown.
|
|
658
682
|
- `swarm.sdk`: `{ sdk, args? }`; invokes a catalog-supported Swarm browser SDK method with the viewer's bearer.
|