@desplega.ai/agent-swarm 2.4.0 → 2.4.2

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.
Files changed (239) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
  3. package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
  4. package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
  5. package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
  6. package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
  7. package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
  8. package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
  9. package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
  10. package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
  11. package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
  12. package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
  13. package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
  15. package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
  16. package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
  17. package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
  20. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  21. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  22. package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
  23. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  24. package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
  25. package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
  26. package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
  27. package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
  28. package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
  29. package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
  30. package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
  31. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  32. package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
  33. package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
  34. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  35. package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
  36. package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
  37. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  38. package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
  39. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  40. package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
  41. package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
  42. package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
  43. package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
  44. package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
  45. package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
  46. package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
  47. package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
  49. package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
  50. package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
  51. package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
  52. package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
  53. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  54. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  55. package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
  56. package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
  57. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  58. package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
  59. package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
  60. package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
  61. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  62. package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
  63. package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
  64. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  65. package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
  66. package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
  67. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  68. package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
  69. package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
  70. package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
  71. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  72. package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
  73. package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
  74. package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
  75. package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
  76. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  77. package/dist/cli.js +20 -20
  78. package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
  79. package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
  80. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  81. package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
  82. package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
  83. package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
  84. package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
  85. package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
  86. package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
  87. package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
  88. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  89. package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
  90. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  91. package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
  92. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  93. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  94. package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
  95. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  96. package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
  97. package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
  98. package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
  99. package/dist/handlers-dkcarkd0.js +39 -0
  100. package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
  101. package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
  102. package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
  103. package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
  104. package/dist/index-5jecgp3n.js +41 -0
  105. package/dist/index-aex0eba9.js +40 -0
  106. package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
  107. package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
  108. package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
  109. package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
  110. package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
  111. package/dist/lead-b1da0f97.js +78 -0
  112. package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
  114. package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
  115. package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
  116. package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
  117. package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
  118. package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
  119. package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
  120. package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
  121. package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
  122. package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
  123. package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
  124. package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
  125. package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
  126. package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
  127. package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
  128. package/dist/worker-c1e68hpa.js +78 -0
  129. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  130. package/openapi.json +491 -52
  131. package/package.json +3 -3
  132. package/src/apps/action-runner.ts +146 -0
  133. package/src/apps/catalog.generated.json +3 -2
  134. package/src/apps/definition.ts +152 -4
  135. package/src/apps/row-store.ts +23 -9
  136. package/src/apps/schema-migrate.ts +14 -6
  137. package/src/apps/script-types.ts +2 -1
  138. package/src/apps/sync.ts +20 -19
  139. package/src/be/db/tasks/read.ts +112 -0
  140. package/src/be/db.ts +16 -4
  141. package/src/be/fleet-benchmark-stats.ts +10 -5
  142. package/src/be/models-catalog.ts +4 -0
  143. package/src/be/onboarding.ts +6 -0
  144. package/src/be/scripts/typecheck.ts +13 -0
  145. package/src/be/swarm-config-guard.ts +25 -1
  146. package/src/be/telemetry-enable.ts +1 -1
  147. package/src/commands/provider-credentials.ts +12 -2
  148. package/src/commands/runner.ts +3 -0
  149. package/src/heartbeat/heartbeat.ts +2 -2
  150. package/src/hooks/codex-hook.ts +6 -2
  151. package/src/hooks/hook.ts +7 -0
  152. package/src/hooks/pr-body-guard.ts +38 -11
  153. package/src/hooks/pr-room-guard.ts +88 -0
  154. package/src/http/agents.ts +1 -0
  155. package/src/http/apps.ts +99 -183
  156. package/src/http/fleet-benchmark.ts +79 -30
  157. package/src/http/global-config.ts +7 -0
  158. package/src/http/ranking.ts +24 -40
  159. package/src/http/status.ts +49 -43
  160. package/src/http/tasks.ts +49 -1
  161. package/src/http/telemetry-ui.ts +1 -1
  162. package/src/http/used-by.ts +55 -19
  163. package/src/http/users.ts +2 -2
  164. package/src/providers/acp-targets.ts +1 -1
  165. package/src/providers/amp-adapter.ts +1 -1
  166. package/src/providers/pi-mono-adapter.ts +15 -0
  167. package/src/providers/reasoning-effort.ts +2 -1
  168. package/src/providers/types.ts +2 -0
  169. package/src/ranking/telemetry-ranking.ts +5 -33
  170. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  173. package/src/server.ts +4 -0
  174. package/src/telemetry-context.ts +19 -1
  175. package/src/telemetry-contract/EVENTS.md +42 -8
  176. package/src/telemetry-contract/SOURCE.json +6 -5
  177. package/src/telemetry-contract/catalog.json +225 -5
  178. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  179. package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
  180. package/src/telemetry-contract/types.gen.ts +225 -27
  181. package/src/telemetry-identity.ts +47 -9
  182. package/src/telemetry-snapshot.ts +9 -1
  183. package/src/telemetry-ui-events.ts +9 -2
  184. package/src/telemetry.ts +29 -6
  185. package/src/tests/app-action-run-tool.test.ts +178 -0
  186. package/src/tests/app-row-tools.test.ts +385 -0
  187. package/src/tests/apps-json-column.test.ts +611 -0
  188. package/src/tests/apps-script-types.test.ts +2 -0
  189. package/src/tests/apps-sync-engine.test.ts +42 -0
  190. package/src/tests/check-pr-body.test.ts +0 -23
  191. package/src/tests/credential-check.test.ts +38 -0
  192. package/src/tests/db-task-read-contract.test.ts +4 -0
  193. package/src/tests/db-task-write-contract.test.ts +2 -0
  194. package/src/tests/docs-index-footprint.test.js +2 -2
  195. package/src/tests/fleet-benchmark.test.ts +122 -2
  196. package/src/tests/http-users.test.ts +62 -1
  197. package/src/tests/model-catalog-table.test.ts +63 -0
  198. package/src/tests/model-profiles-api.test.ts +19 -7
  199. package/src/tests/models-catalog.test.ts +50 -0
  200. package/src/tests/onboarding.test.ts +21 -0
  201. package/src/tests/org-website.test.ts +62 -0
  202. package/src/tests/pi-mono-adapter.test.ts +23 -0
  203. package/src/tests/pr-body-leaks.test.ts +4 -0
  204. package/src/tests/pr-room-guard.test.ts +242 -0
  205. package/src/tests/ranking-listing.test.ts +8 -23
  206. package/src/tests/ranking-route.test.ts +49 -47
  207. package/src/tests/reasoning-effort.test.ts +39 -3
  208. package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
  209. package/src/tests/status.test.ts +158 -9
  210. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  211. package/src/tests/task-lineage.test.ts +180 -0
  212. package/src/tests/telemetry-context.test.ts +128 -2
  213. package/src/tests/telemetry-init.test.ts +62 -5
  214. package/src/tests/telemetry-ui-route.test.ts +123 -0
  215. package/src/tests/tool-annotations.test.ts +3 -2
  216. package/src/tests/used-by-route.test.ts +144 -10
  217. package/src/tests/user-token-rest-auth.test.ts +38 -0
  218. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  219. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  220. package/src/tools/app-action-run.ts +63 -0
  221. package/src/tools/app-rows.ts +239 -0
  222. package/src/tools/model-catalog/index.ts +1 -1
  223. package/src/tools/tool-config.ts +8 -0
  224. package/src/tools/utils.ts +1 -1
  225. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  226. package/src/types.ts +17 -0
  227. package/src/utils/company-profile.ts +24 -0
  228. package/src/utils/org-website.ts +58 -21
  229. package/src/workflows/recovery.ts +10 -22
  230. package/src/workflows/resume.ts +16 -0
  231. package/src/workflows/task-step-routing.ts +47 -7
  232. package/templates/skills/apps/SKILL.md +44 -4
  233. package/templates/skills/apps/content.md +44 -4
  234. package/dist/handlers-zbs90bny.js +0 -39
  235. package/dist/index-8pwc1jc8.js +0 -40
  236. package/dist/index-nxa6me0q.js +0 -41
  237. package/dist/lead-td8h2sfn.js +0 -78
  238. package/dist/worker-wr6xgfr2.js +0 -78
  239. package/src/tests/check-ranking-entry.test.ts +0 -146
package/src/http/apps.ts CHANGED
@@ -1,16 +1,17 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import * as z from "zod";
3
3
  import type { AppActionCaller } from "../apps/action-caller";
4
+ import { runAppAction } from "../apps/action-runner";
4
5
  import {
5
6
  type AppDefinition,
6
7
  AppNameSchema,
7
8
  type AppQueryDef,
8
9
  type AppValidationIssue,
9
10
  applyAppDefinitionPatch,
10
- ColumnKindSchema,
11
11
  isIso8601Date,
12
12
  type ModelDef,
13
13
  parseAppDefinition,
14
+ ScalarColumnKindSchema,
14
15
  SORTABLE_SYSTEM_DATE_COLUMNS,
15
16
  SYSTEM_COLUMN_KINDS,
16
17
  scriptCompatWarnings,
@@ -61,14 +62,9 @@ import {
61
62
  rollbackApp,
62
63
  snapshotApp,
63
64
  } from "../apps/version";
64
- import { normalizeAssetKey } from "../assets/key";
65
- import { getAgentById, getAppVersion, getAppVersions, getLeadAgent } from "../be/db";
66
- import { getScriptById } from "../be/scripts/db";
67
- import { getSavedScriptOwnerAgentId, runSavedScriptAsAgent } from "../be/scripts/run-saved";
68
- import { resolveTemplate } from "../prompts/resolver";
65
+ import { getAgentById, getAppVersion, getAppVersions } from "../be/db";
69
66
  import type { RbacPrincipal, RbacResource } from "../rbac";
70
67
  import { can } from "../rbac";
71
- import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
72
68
  import { AgentTaskStatusSchema } from "../types";
73
69
  import { getRequestAuth } from "../utils/request-auth-context";
74
70
  import { scrubObject } from "../utils/secret-scrubber";
@@ -77,8 +73,8 @@ import { route } from "./route-def";
77
73
  import { BODY_TOO_LARGE, enforceContentLengthCap, json, jsonError } from "./utils";
78
74
 
79
75
  const MAX_APP_BODY_BYTES = 5 * 1024 * 1024;
80
- const MAX_APP_ROW_BODY_BYTES = 1 * 1024 * 1024;
81
- const MAX_APP_BULK_ROWS_BODY_BYTES = 10 * 1024 * 1024;
76
+ export const MAX_APP_ROW_BODY_BYTES = 1 * 1024 * 1024;
77
+ export const MAX_APP_BULK_ROWS_BODY_BYTES = 10 * 1024 * 1024;
82
78
  const MAX_USER_CONFIG_BODY_BYTES = 64 * 1024;
83
79
  const MAX_USER_CONFIG_VALUES_BYTES = 16 * 1024;
84
80
  const DECIMAL_NUMBER_PATTERN = /^[+-]?\d+(?:\.\d+)?$/;
@@ -220,7 +216,7 @@ const UserConfigValuesSchema = z.record(
220
216
  * on the wire anyway — the key can only ever be absent.
221
217
  */
222
218
  const AppUserConfigFieldSchema = z.object({
223
- kind: ColumnKindSchema,
219
+ kind: ScalarColumnKindSchema,
224
220
  default: z.union([z.string(), z.number(), z.boolean()]).optional(),
225
221
  enum: z.array(z.string()).optional(),
226
222
  label: z.string().optional(),
@@ -756,17 +752,6 @@ function definitionNeedsRepair(
756
752
  return true;
757
753
  }
758
754
 
759
- /**
760
- * One `error` string for a sync result, or undefined when every pass succeeded.
761
- * Each failed pair is named so a multi-pair sync says which one broke; the pass
762
- * list in `result.passes` carries the rest.
763
- */
764
- function syncPassError(sync: Awaited<ReturnType<typeof runAppSync>>): string | undefined {
765
- const failed = sync.passes.filter((pass) => pass.error !== undefined);
766
- if (failed.length === 0) return undefined;
767
- return failed.map((pass) => `${pass.model}.${pass.source}: ${pass.error}`).join("; ");
768
- }
769
-
770
755
  function snapshotFailure(res: ServerResponse): void {
771
756
  jsonError(res, "failed to snapshot app", 500);
772
757
  }
@@ -800,6 +785,7 @@ async function resolveModel(
800
785
  }
801
786
 
802
787
  function parseFilterValue(raw: string, column: ModelDef["columns"][string]): unknown {
788
+ if (column.kind === "json") throw new Error("json columns cannot be filtered");
803
789
  if (column.kind === "number") {
804
790
  if (!DECIMAL_NUMBER_PATTERN.test(raw)) throw new Error("must be a decimal number");
805
791
  const value = Number(raw);
@@ -991,6 +977,10 @@ function rowValue(row: AppRow, column: string): unknown {
991
977
  return Object.hasOwn(row, column) ? row[column] : undefined;
992
978
  }
993
979
 
980
+ export function rowMatchesFilters(row: AppRow, filters: RowFilter[]): boolean {
981
+ return filters.every((filter) => rowValue(row, filter.column) === filter.value);
982
+ }
983
+
994
984
  export function applyQuery(
995
985
  rows: AppRow[],
996
986
  query: AppQueryDef,
@@ -1019,6 +1009,71 @@ export function applyQuery(
1019
1009
  return selected.slice(0, query.limit ?? 200);
1020
1010
  }
1021
1011
 
1012
+ export async function selectAppRows(
1013
+ appId: string,
1014
+ modelName: string,
1015
+ model: ModelDef,
1016
+ filters: RowFilter[],
1017
+ sortRaw?: string,
1018
+ limit = 200,
1019
+ systemColumns = false,
1020
+ ) {
1021
+ // Backstop for every caller: the REST route (`filter.<col>`) and the MCP
1022
+ // tools (`equals.<col>`) reject json filters with their own paths first.
1023
+ const jsonFilterIssues = filters.flatMap((filter) =>
1024
+ Object.hasOwn(model.columns, filter.column) && model.columns[filter.column]!.kind === "json"
1025
+ ? [{ path: `filter.${filter.column}`, message: "json columns cannot be filtered" }]
1026
+ : [],
1027
+ );
1028
+ if (jsonFilterIssues.length > 0) throw new AppRowValidationError(jsonFilterIssues);
1029
+
1030
+ // SQL narrows the scan; the `===` re-check keeps the exact filter semantics.
1031
+ const rows = (
1032
+ await listAppRows(
1033
+ appId,
1034
+ modelName,
1035
+ filters.flatMap((filter) =>
1036
+ typeof filter.value === "string" ||
1037
+ typeof filter.value === "number" ||
1038
+ typeof filter.value === "boolean"
1039
+ ? [{ column: filter.column, value: filter.value }]
1040
+ : [],
1041
+ ),
1042
+ )
1043
+ ).filter((row) => rowMatchesFilters(row, filters));
1044
+
1045
+ if (sortRaw) {
1046
+ const [column, dir, extra] = sortRaw.split(":");
1047
+ if (
1048
+ extra !== undefined ||
1049
+ !column ||
1050
+ (dir !== "asc" && dir !== "desc") ||
1051
+ (!SORTABLE_SYSTEM_DATE_COLUMNS.has(column) &&
1052
+ !(systemColumns && Object.hasOwn(SYSTEM_COLUMN_KINDS, column)) &&
1053
+ (!Object.hasOwn(model.columns, column) || model.columns[column]!.hidden === true))
1054
+ ) {
1055
+ throw new AppRowValidationError([
1056
+ { path: "sort", message: "must be <column>:<asc|desc> for a known column" },
1057
+ ]);
1058
+ }
1059
+ if (model.columns[column]?.kind === "json") {
1060
+ throw new AppRowValidationError([{ path: "sort", message: "json columns cannot be sorted" }]);
1061
+ }
1062
+ rows.sort((a, b) => {
1063
+ const result = compareValues(
1064
+ rowValue(a, column),
1065
+ rowValue(b, column),
1066
+ dir,
1067
+ SORTABLE_SYSTEM_DATE_COLUMNS.has(column) ||
1068
+ (Object.hasOwn(model.columns, column) && model.columns[column]!.kind === "date"),
1069
+ );
1070
+ return result;
1071
+ });
1072
+ }
1073
+ const total = rows.length;
1074
+ return { rows: rows.slice(0, limit), total };
1075
+ }
1076
+
1022
1077
  export async function handleApps(
1023
1078
  req: IncomingMessage,
1024
1079
  res: ServerResponse,
@@ -1189,55 +1244,23 @@ export async function handleApps(
1189
1244
  json(res, { error: "invalid row query", issues }, 400);
1190
1245
  return true;
1191
1246
  }
1192
- // SQL narrows the scan; the `===` re-check keeps the exact filter semantics.
1193
- const rows = (
1194
- await listAppRows(
1195
- parsed.params.id,
1196
- parsed.params.model,
1197
- filters.flatMap((filter) =>
1198
- typeof filter.value === "string" ||
1199
- typeof filter.value === "number" ||
1200
- typeof filter.value === "boolean"
1201
- ? [{ column: filter.column, value: filter.value }]
1202
- : [],
1247
+ try {
1248
+ listRowsRoute.respond(
1249
+ res,
1250
+ 200,
1251
+ await selectAppRows(
1252
+ parsed.params.id,
1253
+ parsed.params.model,
1254
+ resolved.model,
1255
+ filters,
1256
+ parsed.query.sort,
1257
+ parsed.query.limit,
1203
1258
  ),
1204
- )
1205
- ).filter((row) => filters.every((filter) => rowValue(row, filter.column) === filter.value));
1206
- const sortRaw = parsed.query.sort;
1207
- if (sortRaw) {
1208
- const [column, dir, extra] = sortRaw.split(":");
1209
- if (
1210
- extra !== undefined ||
1211
- !column ||
1212
- (dir !== "asc" && dir !== "desc") ||
1213
- (!SORTABLE_SYSTEM_DATE_COLUMNS.has(column) &&
1214
- (!Object.hasOwn(resolved.model.columns, column) ||
1215
- resolved.model.columns[column]!.hidden === true))
1216
- ) {
1217
- json(
1218
- res,
1219
- {
1220
- error: "invalid sort",
1221
- issues: [{ path: "sort", message: "must be <column>:<asc|desc> for a known column" }],
1222
- },
1223
- 400,
1224
- );
1225
- return true;
1226
- }
1227
- rows.sort((a, b) => {
1228
- const result = compareValues(
1229
- rowValue(a, column),
1230
- rowValue(b, column),
1231
- dir,
1232
- SORTABLE_SYSTEM_DATE_COLUMNS.has(column) ||
1233
- (Object.hasOwn(resolved.model.columns, column) &&
1234
- resolved.model.columns[column]!.kind === "date"),
1235
- );
1236
- return result;
1237
- });
1259
+ );
1260
+ } catch (error) {
1261
+ if (!(error instanceof AppRowValidationError)) throw error;
1262
+ json(res, { error: "invalid sort", issues: error.issues }, 400);
1238
1263
  }
1239
- const total = rows.length;
1240
- listRowsRoute.respond(res, 200, { rows: rows.slice(0, parsed.query.limit ?? 200), total });
1241
1264
  return true;
1242
1265
  }
1243
1266
 
@@ -1359,122 +1382,15 @@ export async function handleApps(
1359
1382
  return true;
1360
1383
  }
1361
1384
  if (definitionNeedsRepair(res, app)) return true;
1362
- const actions = app.definition.actions;
1363
- if (!actions || !Object.hasOwn(actions, parsed.params.name)) {
1364
- jsonError(res, "app action not found", 404);
1365
- return true;
1366
- }
1367
-
1368
- const action = actions[parsed.params.name]!;
1369
- const input = parsed.body.input ?? {};
1370
- if (action.kind === "script") {
1371
- const script = await getScriptById(action.scriptId);
1372
- if (!script) {
1373
- json(
1374
- res,
1375
- {
1376
- error: "invalid app action",
1377
- issues: [
1378
- {
1379
- path: `actions.${parsed.params.name}.scriptId`,
1380
- message: `script "${action.scriptId}" no longer exists`,
1381
- },
1382
- ],
1383
- },
1384
- 400,
1385
- );
1386
- return true;
1387
- }
1388
-
1389
- // Script actions run with the OWNER's bindings by design: wiring a script
1390
- // into an app is the owner's delegation. parseAppDefinition enforces at
1391
- // write time that an agent may only wire scripts it owns (or global ones),
1392
- // so a foreign script id cannot be smuggled in here. Invoker-brokered
1393
- // credentials remain a possible future tightening.
1394
- const runAsAgentId = getSavedScriptOwnerAgentId(script);
1395
- if (!runAsAgentId) {
1396
- jsonError(res, "agentId is required: this script has no owning agent to run as", 400);
1397
- return true;
1398
- }
1399
-
1400
- const output = await runSavedScriptAsAgent({
1401
- script,
1402
- input: { ...action.args, ...input, app: { id: app.id }, caller },
1403
- agentId: runAsAgentId,
1404
- });
1405
- const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
1406
- const error = ok
1407
- ? undefined
1408
- : output.runtimeError
1409
- ? `${output.runtimeError.name}: ${output.runtimeError.message}`
1410
- : (output.error ?? `Script exited with code ${output.exitCode}`);
1411
- runActionRoute.respond(
1412
- res,
1413
- 200,
1414
- scrubObject({
1415
- ok,
1416
- result: output.result,
1417
- stdout: output.stdout,
1418
- ...(error === undefined ? {} : { error }),
1419
- durationMs: output.durationMs,
1420
- }),
1421
- );
1422
- return true;
1423
- }
1424
-
1425
- if (action.kind === "sync") {
1426
- const startedAt = Date.now();
1427
- const sync = await runAppSync({
1428
- appId: app.id,
1429
- ...(action.model === undefined ? {} : { model: action.model }),
1430
- ...(action.source === undefined ? {} : { source: action.source }),
1431
- invokedBy: actor,
1432
- });
1433
- const durationMs = Date.now() - startedAt;
1434
- if (sync.issues && sync.issues.length > 0) {
1435
- json(res, { error: "invalid sync action", issues: sync.issues }, 400);
1436
- return true;
1437
- }
1438
- // Deliberately the SCRIPT-action response shape, minus `taskId`:
1439
- // app-surface.tsx branches on `taskId` first, so omitting it gives the
1440
- // dashboard running -> ok/error + refetchAll() with zero UI changes.
1441
- const error = syncPassError(sync);
1442
- runActionRoute.respond(
1443
- res,
1444
- 200,
1445
- scrubObject({
1446
- ok: sync.ok,
1447
- result: { passes: sync.passes },
1448
- ...(error === undefined ? {} : { error }),
1449
- durationMs,
1450
- }),
1451
- );
1452
- return true;
1453
- }
1454
-
1455
- const lead = action.agentId ? null : await getLeadAgent();
1456
- const taskPrompt = resolveTemplate("task.app.action", {
1457
- prompt: action.prompt,
1458
- app_id: app.id,
1459
- action_name: parsed.params.name,
1460
- input_json: JSON.stringify(input),
1461
- caller_json: JSON.stringify(caller),
1385
+ const output = await runAppAction({
1386
+ app,
1387
+ name: parsed.params.name,
1388
+ input: parsed.body.input,
1389
+ caller,
1390
+ actor,
1462
1391
  });
1463
- const task = await createTaskWithSiblingAwareness(
1464
- taskPrompt.text,
1465
- {
1466
- source: "api",
1467
- agentId: action.agentId ?? lead?.id,
1468
- routingReason: action.agentId ? "human_pinned" : lead ? "skill" : undefined,
1469
- routingSource: action.agentId || lead ? "engine_default" : undefined,
1470
- // App-spawned tasks group under the app's asset namespace so a
1471
- // swarm-tasks source can pull them back via config.assetKey.
1472
- key: normalizeAssetKey(`shared/app:${app.id}/action:${parsed.params.name}/`),
1473
- ...(actor.startsWith("user:") ? { requestedByUserId: actor.slice("user:".length) } : {}),
1474
- },
1475
- { origin: "app" },
1476
- );
1477
- runActionRoute.respond(res, 200, { ok: true, taskId: task.id, status: task.status });
1392
+ if (output.status === 200) runActionRoute.respond(res, 200, output.body);
1393
+ else json(res, output.body, output.status);
1478
1394
  return true;
1479
1395
  }
1480
1396
 
@@ -1,6 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import { getSwarmConfigs } from "../be/db";
4
3
  import { getLocalFleetStats } from "../be/fleet-benchmark-stats";
5
4
  import {
6
5
  compareWithFleet,
@@ -12,6 +11,9 @@ import {
12
11
  localTasksPerWeek,
13
12
  } from "../fleet-benchmark";
14
13
  import { isTelemetryEnabled } from "../telemetry";
14
+ import { COMPANY_TEAM_SIZES, COMPANY_USE_CASES } from "../utils/company-profile";
15
+ import { normalizeOrgWebsite } from "../utils/org-website";
16
+ import { globalConfig } from "./global-config";
15
17
  import { rejectGuest } from "./request-principal";
16
18
  import { route } from "./route-def";
17
19
  import { telemetryProxyEndpoint } from "./used-by";
@@ -34,6 +36,14 @@ const FleetStateSchema = z.enum([
34
36
 
35
37
  const FleetBenchmarkResponseSchema = z.object({
36
38
  state: FleetStateSchema,
39
+ level: z.enum(["basic", "full"]),
40
+ peerGroup: z
41
+ .object({
42
+ teamSize: z.string().nullable(),
43
+ useCase: z.string().nullable(),
44
+ orgs: z.number().int(),
45
+ })
46
+ .nullable(),
37
47
  local: z
38
48
  .object({
39
49
  tasksPerWeek: z.number(),
@@ -67,7 +77,7 @@ const fleetBenchmark = route({
67
77
  pattern: ["api", "fleet-benchmark"],
68
78
  summary: "Compare this swarm with similar swarms",
69
79
  description:
70
- "Places this swarm's tasks per week and task success rate (last 30 days) in the fleet percentiles published by the telemetry proxy, against swarms with the same agent count when enough exist. Needs telemetry on and an organization name; otherwise `state` says what is missing and the proxy is not called. `unavailable` also covers an install whose first event the proxy saw cannot be established locally (until 30 days after the first accepted event). The fleet numbers are cached for a day.",
80
+ "Compares local task volume and success rate with fleet percentiles over the last 30 days. Comparisons require telemetry, an organization name, and a known proxy ingest date. A website, team size, and use case enable full comparisons with peer filters. The response includes the proxy's peer group when available. With telemetry off, it returns local DB counts without a proxy request. The fleet cache key includes the peer filters.",
71
81
  tags: ["Telemetry"],
72
82
  responses: {
73
83
  200: {
@@ -81,8 +91,12 @@ const fleetBenchmark = route({
81
91
 
82
92
  // ─── Caches ──────────────────────────────────────────────────────────────────
83
93
 
84
- let fleetCache: { value: FleetBenchmarks | null; until: number } | null = null;
85
- let localCache: { value: LocalFleetStats | null; until: number } | null = null;
94
+ let fleetCache: { key: string; value: ProxyFleetBenchmarks | null; until: number } | null = null;
95
+ let localCache: {
96
+ requireIngestAnchor: boolean;
97
+ value: LocalFleetStats | null;
98
+ until: number;
99
+ } | null = null;
86
100
 
87
101
  const ProxyMetricSchema = z.object({
88
102
  n: z.number(),
@@ -112,19 +126,34 @@ const ProxyFleetBenchmarksSchema = z.object({
112
126
  "2-5": ProxyBlockSchema.optional(),
113
127
  "6+": ProxyBlockSchema.optional(),
114
128
  }),
129
+ peer_group: z
130
+ .object({
131
+ team_size: z.string().nullable(),
132
+ use_case: z.string().nullable(),
133
+ orgs: z.number().int().nonnegative(),
134
+ })
135
+ .nullable()
136
+ .optional(),
115
137
  }) satisfies z.ZodType<FleetBenchmarks>;
116
138
 
139
+ type ProxyFleetBenchmarks = z.infer<typeof ProxyFleetBenchmarksSchema>;
140
+
117
141
  /** The proxy's fleet percentiles. The request carries no install ID. */
118
- async function fleetBenchmarks(now: number): Promise<FleetBenchmarks | null> {
119
- if (fleetCache && fleetCache.until > now) return fleetCache.value;
120
- let value: FleetBenchmarks | null = null;
142
+ async function fleetBenchmarks(
143
+ now: number,
144
+ peerGroup: { teamSize: string; useCase: string } | null,
145
+ ): Promise<ProxyFleetBenchmarks | null> {
146
+ const endpoint = new URL(await telemetryProxyEndpoint("/v1/fleet/benchmarks"));
147
+ endpoint.searchParams.set("window", "30d");
148
+ if (peerGroup) {
149
+ endpoint.searchParams.set("team_size", peerGroup.teamSize);
150
+ endpoint.searchParams.set("use_case", peerGroup.useCase);
151
+ }
152
+ const key = endpoint.toString();
153
+ if (fleetCache?.key === key && fleetCache.until > now) return fleetCache.value;
154
+ let value: ProxyFleetBenchmarks | null = null;
121
155
  try {
122
- const response = await fetch(
123
- `${await telemetryProxyEndpoint("/v1/fleet/benchmarks")}?window=30d`,
124
- {
125
- signal: AbortSignal.timeout(TIMEOUT_MS),
126
- },
127
- );
156
+ const response = await fetch(key, { signal: AbortSignal.timeout(TIMEOUT_MS) });
128
157
  if (response.ok) {
129
158
  const parsed = ProxyFleetBenchmarksSchema.safeParse(await response.json());
130
159
  if (parsed.success) value = parsed.data;
@@ -132,38 +161,46 @@ async function fleetBenchmarks(now: number): Promise<FleetBenchmarks | null> {
132
161
  } catch {
133
162
  // Unreachable proxy: the card says the comparison is unavailable.
134
163
  }
135
- fleetCache = { value, until: now + (value ? FLEET_TTL_MS : FLEET_FAILURE_TTL_MS) };
164
+ fleetCache = { key, value, until: now + (value ? FLEET_TTL_MS : FLEET_FAILURE_TTL_MS) };
136
165
  return value;
137
166
  }
138
167
 
139
- async function localStats(now: Date): Promise<LocalFleetStats | null> {
140
- if (localCache && localCache.until > now.getTime()) return localCache.value;
141
- const value = await getLocalFleetStats(now);
142
- localCache = { value, until: now.getTime() + LOCAL_TTL_MS };
168
+ async function localStats(now: Date, requireIngestAnchor = true): Promise<LocalFleetStats | null> {
169
+ if (localCache?.requireIngestAnchor === requireIngestAnchor && localCache.until > now.getTime())
170
+ return localCache.value;
171
+ const value = await getLocalFleetStats(now, { requireIngestAnchor });
172
+ localCache = { requireIngestAnchor, value, until: now.getTime() + LOCAL_TTL_MS };
143
173
  return value;
144
174
  }
145
175
 
146
- async function orgName(): Promise<string | undefined> {
147
- const stored = (
148
- await getSwarmConfigs({ scope: "global", key: "SWARM_ORG_NAME" })
149
- )?.[0]?.value?.trim();
150
- return stored || process.env.SWARM_ORG_NAME?.trim() || undefined;
151
- }
152
-
153
176
  export async function buildFleetBenchmarkResponse(
154
177
  now = new Date(),
155
178
  ): Promise<FleetBenchmarkResponse> {
179
+ const [name, website, teamSize, useCase] = await Promise.all([
180
+ globalConfig("SWARM_ORG_NAME"),
181
+ globalConfig("SWARM_ORG_WEBSITE"),
182
+ globalConfig("SWARM_ORG_TEAM_SIZE"),
183
+ globalConfig("SWARM_ORG_USE_CASE"),
184
+ ]);
185
+ const team = z.enum(COMPANY_TEAM_SIZES).safeParse(teamSize);
186
+ const use = z.enum(COMPANY_USE_CASES).safeParse(useCase);
187
+ const peerGroup =
188
+ normalizeOrgWebsite(website) && team.success && use.success
189
+ ? { teamSize: team.data, useCase: use.data }
190
+ : null;
156
191
  const empty = {
192
+ level: peerGroup ? ("full" as const) : ("basic" as const),
193
+ peerGroup: null,
157
194
  local: null,
158
195
  minFinishedTasks: FLEET_MIN_FINISHED_TASKS,
159
196
  generatedAt: null,
160
197
  comparisons: [],
161
198
  };
162
199
  // Telemetry off means no call to our proxy at all, not even an anonymous one.
163
- if (!isTelemetryEnabled()) return { state: "telemetry_off", ...empty };
164
- if (!(await orgName())) return { state: "no_org_name", ...empty };
200
+ const telemetryEnabled = isTelemetryEnabled();
201
+ if (telemetryEnabled && !name) return { state: "no_org_name", ...empty };
165
202
 
166
- const stats = await localStats(now);
203
+ const stats = await localStats(now, telemetryEnabled);
167
204
  // Where the proxy started counting this install is unknown: no comparison.
168
205
  if (!stats) return { state: "unavailable", ...empty };
169
206
  const finishedTasks = stats.tasksCompleted + stats.tasksFailed;
@@ -174,14 +211,26 @@ export async function buildFleetBenchmarkResponse(
174
211
  agents: stats.agents,
175
212
  bucket: fleetBucketFor(stats.agents),
176
213
  };
214
+ if (!telemetryEnabled) return { ...empty, state: "telemetry_off", local };
177
215
  if (finishedTasks < FLEET_MIN_FINISHED_TASKS)
178
216
  return { ...empty, state: "not_enough_tasks", local };
179
217
 
180
- const fleet = await fleetBenchmarks(now.getTime());
218
+ const fleet = await fleetBenchmarks(now.getTime(), peerGroup);
219
+ const responsePeerGroup =
220
+ peerGroup && fleet?.peer_group
221
+ ? {
222
+ teamSize: fleet.peer_group.team_size,
223
+ useCase: fleet.peer_group.use_case,
224
+ orgs: fleet.peer_group.orgs,
225
+ }
226
+ : null;
181
227
  const comparisons = fleet ? compareWithFleet(stats, fleet, now) : [];
182
- if (!fleet || comparisons.length === 0) return { ...empty, state: "unavailable", local };
228
+ if (!fleet || comparisons.length === 0)
229
+ return { ...empty, state: "unavailable", local, peerGroup: responsePeerGroup };
183
230
  return {
231
+ ...empty,
184
232
  state: "ready",
233
+ peerGroup: responsePeerGroup,
185
234
  local,
186
235
  minFinishedTasks: FLEET_MIN_FINISHED_TASKS,
187
236
  generatedAt: fleet.generatedAt,
@@ -0,0 +1,7 @@
1
+ import { getSwarmConfigs } from "../be/db";
2
+
3
+ /** Stored values, including empty strings, override env values, as on config reload. */
4
+ export async function globalConfig(key: string): Promise<string | undefined> {
5
+ const stored = (await getSwarmConfigs({ scope: "global", key }))?.[0]?.value;
6
+ return (stored ?? process.env[key])?.trim() || undefined;
7
+ }