@desplega.ai/agent-swarm 1.158.0 → 1.159.0
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-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
- package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
- package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
- package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
- package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
- package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
- package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
- package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
- package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
- package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
- package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
- package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
- package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
- package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
- package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
- package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
- package/dist/cli-0xqde53b.js +176 -0
- package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
- package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
- package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
- package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
- package/dist/cli-3gvrr8vj.js +1422 -0
- package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
- package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
- package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
- package/dist/cli-503ya656.js +455 -0
- package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
- package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
- package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
- package/dist/cli-7cxw8b60.js +14910 -0
- package/dist/cli-7kyk9jr3.js +22 -0
- package/dist/cli-7zttzetj.js +324 -0
- package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
- package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
- package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
- package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
- package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
- package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
- package/dist/cli-ca2w7zv7.js +115 -0
- package/dist/cli-cpvsaqsw.js +333 -0
- package/dist/cli-cygyqj3b.js +754 -0
- package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
- package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
- package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
- package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
- package/dist/cli-ejpygnqz.js +3815 -0
- package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
- package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
- package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
- package/dist/cli-gzyh0j0g.js +8455 -0
- package/dist/cli-hgqfttfc.js +183 -0
- package/dist/cli-hj65wsts.js +17755 -0
- package/dist/cli-j9z8mfx3.js +476 -0
- package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
- package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
- package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
- package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
- package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
- package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
- package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
- package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
- package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
- package/dist/cli-qvnnx4ct.js +1051 -0
- package/dist/cli-rb9m6rpe.js +4980 -0
- package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
- package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
- package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
- package/dist/cli-rykfmgwg.js +102 -0
- package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
- package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
- package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
- package/dist/cli-wd9hz40d.js +69 -0
- package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
- package/dist/cli-xgpf00sk.js +2149 -0
- package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
- package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
- package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
- package/dist/cli.js +38 -29
- package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
- package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
- package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
- package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
- package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
- package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
- package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
- package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
- package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
- package/dist/execute-71ysgv35.js +965 -0
- package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
- package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
- package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
- package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
- package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
- package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
- package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
- package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
- package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
- package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
- package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
- package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
- package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
- package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
- package/dist/llama-cpp-classify-t2tccams.js +335 -0
- package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
- package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
- package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
- package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
- package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
- package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
- package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
- package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
- package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
- package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
- package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
- package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
- package/dist/pricing-refresh-0v77385v.js +39 -0
- package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
- package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
- package/dist/runtime-ktjysacx.js +24 -0
- package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
- package/dist/typesafe-system-one-96960jxp.js +27 -0
- package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
- package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
- package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
- package/openapi.json +863 -14
- package/package.json +9 -8
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/tasks/human-free.ts +442 -0
- package/src/be/db/tasks/read.ts +56 -1
- package/src/be/db/tasks/write.ts +107 -11
- package/src/be/db.ts +89 -165
- package/src/be/harness-model-support.ts +182 -0
- package/src/be/human-free-drain.ts +67 -0
- package/src/be/migrations/182_task_human_free_flag.sql +63 -0
- package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
- package/src/be/migrations/184_model_catalog.sql +120 -0
- package/src/be/model-catalog-store.ts +644 -0
- package/src/be/model-tier-keys.ts +31 -0
- package/src/be/model-tier-resolution.ts +489 -0
- package/src/be/model-validation.ts +112 -0
- package/src/be/models-catalog.ts +61 -7
- package/src/be/modelsdev-cache.json +82 -0
- package/src/be/modelsdev-cache.ts +2 -0
- package/src/be/pricing-refresh.ts +154 -12
- package/src/be/scripts/typecheck.ts +15 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
- package/src/be/swarm-config-guard.ts +6 -0
- package/src/cli.tsx +10 -0
- package/src/commands/claude-managed-setup.ts +3 -1
- package/src/commands/runner.ts +76 -2
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +64 -15
- package/src/heartbeat/templates.ts +2 -2
- package/src/http/agents.ts +49 -1
- package/src/http/index.ts +8 -0
- package/src/http/models-catalog.ts +357 -8
- package/src/http/poll.ts +45 -2
- package/src/http/schedules.ts +36 -6
- package/src/http/session-data.ts +2 -5
- package/src/http/stats.ts +6 -8
- package/src/http/tasks.ts +26 -1
- package/src/http/usage-cache.ts +56 -15
- package/src/http/workflows.ts +35 -8
- package/src/prompts/session-templates.ts +1 -0
- package/src/providers/claude-managed-models.ts +47 -161
- package/src/providers/codex-adapter.ts +4 -4
- package/src/providers/codex-models.ts +65 -198
- package/src/providers/reasoning-effort.ts +44 -140
- package/src/rbac/legacy-policy.ts +25 -0
- package/src/rbac/permissions.ts +10 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +15 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
- package/src/server.ts +6 -0
- package/src/slack/message-text.ts +56 -2
- package/src/slack/thread-buffer.ts +10 -2
- package/src/tests/agents-list-model-display.test.ts +4 -6
- package/src/tests/base-prompt.test.ts +3 -0
- package/src/tests/claude-adapter.test.ts +10 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -18
- package/src/tests/codex-adapter.test.ts +2 -1
- package/src/tests/codex-gpt6-release.test.ts +66 -5
- package/src/tests/context-window.test.ts +7 -5
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +5 -1
- package/src/tests/db-task-write-contract.test.ts +4 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/explicit-model-validation.test.ts +332 -0
- package/src/tests/harness-model-support.test.ts +374 -0
- package/src/tests/heartbeat-checklist-config.test.ts +71 -0
- package/src/tests/heartbeat-checklist.test.ts +4 -2
- package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
- package/src/tests/human-free-flag.test.ts +279 -0
- package/src/tests/human-free-oracle.ts +57 -0
- package/src/tests/human-free-reclassify.test.ts +669 -0
- package/src/tests/migration-046-budgets.test.ts +3 -3
- package/src/tests/model-catalog-migration.test.ts +134 -0
- package/src/tests/model-catalog-table.test.ts +249 -0
- package/src/tests/model-catalog-write-access.test.ts +387 -0
- package/src/tests/model-tier-resolution.test.ts +519 -0
- package/src/tests/models-catalog.test.ts +1 -1
- package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
- package/src/tests/package-publish.test.ts +137 -36
- package/src/tests/pricing-routes.test.ts +2 -2
- package/src/tests/rbac-engine.test.ts +42 -2
- package/src/tests/reasoning-effort.test.ts +109 -0
- package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
- package/src/tests/runtime-model-catalog.test.ts +86 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/seed-scripts.test.ts +89 -0
- package/src/tests/session-costs.test.ts +14 -11
- package/src/tests/slack-message-text.test.ts +101 -0
- package/src/tests/slack-thread-buffer-context.test.ts +93 -0
- package/src/tests/task-cascade-fail.test.ts +148 -2
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/usage-cache.test.ts +166 -0
- package/src/tests/workflow-executors.test.ts +5 -2
- package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
- package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
- package/src/tests/workflow-system-one-decision.test.ts +2063 -0
- package/src/tools/model-catalog/index.ts +137 -0
- package/src/tools/schedules/create-schedule.ts +16 -1
- package/src/tools/schedules/patch-schedule.ts +17 -0
- package/src/tools/schedules/update-schedule.ts +17 -0
- package/src/tools/send-task.ts +14 -0
- package/src/tools/task-action.ts +12 -0
- package/src/tools/tool-config.ts +3 -1
- package/src/tools/workflows/create-workflow.ts +9 -2
- package/src/tools/workflows/patch-workflow-node.ts +17 -7
- package/src/tools/workflows/patch-workflow.ts +9 -2
- package/src/tools/workflows/update-workflow.ts +9 -2
- package/src/types.ts +30 -1
- package/src/utils/context-window.ts +47 -53
- package/src/utils/harness-cli-version.ts +115 -0
- package/src/utils/harness-model-error.ts +33 -0
- package/src/utils/runtime-model-catalog.ts +137 -0
- package/src/workflows/approval-resolution.ts +39 -0
- package/src/workflows/definition.ts +7 -0
- package/src/workflows/engine.ts +69 -1
- package/src/workflows/executors/agent-task.ts +9 -0
- package/src/workflows/executors/base.ts +53 -0
- package/src/workflows/executors/human-in-the-loop.ts +8 -8
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +1232 -0
- package/src/workflows/executors/system-one-providers.ts +301 -0
- package/src/workflows/executors/workflow-llm.ts +13 -1
- package/src/workflows/readiness.ts +86 -0
- package/src/workflows/recovery.ts +12 -12
- package/src/workflows/resume.ts +23 -9
- package/templates/schedules/daily-blocker-digest/content.md +4 -5
- package/templates/skills/agent-browser/SKILL.md +6 -5
- package/templates/skills/agent-browser/content.md +6 -5
- package/templates/skills/heartbeat-runbook/config.json +1 -1
- package/templates/skills/heartbeat-runbook/content.md +2 -2
- package/templates/skills/swarm-scripts/SKILL.md +1 -1
- package/templates/skills/swarm-scripts/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +81 -1
- package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
- package/templates/workflows/pr-review-status-sweep/content.md +2 -0
- package/dist/claude-sdk-session-t442qz6f.js +0 -39108
- package/dist/cli-0xjkj1j7.js +0 -61
- package/dist/cli-65hjmev5.js +0 -1141
- package/dist/cli-aw1jwp1r.js +0 -14075
- package/dist/cli-h967nb6r.js +0 -6603
- package/dist/cli-vapyk8sw.js +0 -20
- package/dist/cli-ws6rqycm.js +0 -247
- package/dist/pricing-refresh-v1cbr9te.js +0 -27
- package/src/providers/modelsdev-reasoning.json +0 -6453
- package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
- package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
|
@@ -1,21 +1,48 @@
|
|
|
1
1
|
// Live model catalog for the UI model picker.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// catalog
|
|
6
|
-
//
|
|
3
|
+
// GET serves the slim projection of the persistent `model_catalog` table
|
|
4
|
+
// merged with `model_catalog_overlay` (src/be/model-catalog-store.ts). The
|
|
5
|
+
// catalog refreshes server-side (boot + every 12h); POST .../refresh forces a
|
|
6
|
+
// refresh and PUT/DELETE .../overlay manage hand-verified overlay rows.
|
|
7
|
+
// See runbooks/model-catalog.md.
|
|
7
8
|
|
|
8
9
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
10
|
import { z } from "zod";
|
|
10
|
-
import {
|
|
11
|
+
import { getAgentById } from "../be/db";
|
|
12
|
+
import {
|
|
13
|
+
agentRanModel,
|
|
14
|
+
getAgentHarnessCliVersion,
|
|
15
|
+
listHarnessModelSupport,
|
|
16
|
+
recordHarnessModelSupport,
|
|
17
|
+
} from "../be/harness-model-support";
|
|
18
|
+
import {
|
|
19
|
+
deleteModelCatalogOverlay,
|
|
20
|
+
loadModelsCatalog,
|
|
21
|
+
upsertModelCatalogOverlay,
|
|
22
|
+
} from "../be/model-catalog-store";
|
|
23
|
+
import { previewModelTiers } from "../be/model-tier-resolution";
|
|
24
|
+
import { requestModelCatalogRefresh } from "../be/pricing-refresh";
|
|
25
|
+
import { can, type RbacPrincipal } from "../rbac";
|
|
26
|
+
import { MODEL_TIERS, ProviderNameSchema } from "../types";
|
|
27
|
+
import { isUnknownModelError } from "../utils/harness-model-error";
|
|
28
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
11
29
|
import { route } from "./route-def";
|
|
12
|
-
import {
|
|
30
|
+
import { jsonError } from "./utils";
|
|
13
31
|
|
|
14
32
|
const CatalogModelSchema = z.object({
|
|
15
33
|
id: z.string(),
|
|
16
34
|
name: z.string().optional(),
|
|
17
|
-
cost: z
|
|
35
|
+
cost: z
|
|
36
|
+
.object({
|
|
37
|
+
input: z.number().optional(),
|
|
38
|
+
output: z.number().optional(),
|
|
39
|
+
cache_read: z.number().optional(),
|
|
40
|
+
cache_write: z.number().optional(),
|
|
41
|
+
})
|
|
42
|
+
.optional(),
|
|
18
43
|
limit: z.object({ context: z.number().optional() }).optional(),
|
|
44
|
+
release_date: z.string().optional(),
|
|
45
|
+
status: z.string().optional(),
|
|
19
46
|
reasoning: z.boolean().optional(),
|
|
20
47
|
reasoning_options: z
|
|
21
48
|
.array(z.object({ type: z.string(), values: z.array(z.string()).optional() }))
|
|
@@ -28,6 +55,61 @@ const CatalogProviderSchema = z.object({
|
|
|
28
55
|
models: z.record(z.string(), CatalogModelSchema),
|
|
29
56
|
});
|
|
30
57
|
|
|
58
|
+
const ReasoningOptionSchema = z.object({
|
|
59
|
+
type: z.string(),
|
|
60
|
+
values: z.array(z.string()).optional(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const OverlayPricingSchema = z.object({
|
|
64
|
+
input: z.number().nonnegative().optional(),
|
|
65
|
+
output: z.number().nonnegative().optional(),
|
|
66
|
+
cache_read: z.number().nonnegative().optional(),
|
|
67
|
+
cache_write: z.number().nonnegative().optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const ModelCatalogRefreshResultSchema = z.object({
|
|
71
|
+
status: z.enum(["updated", "not-modified", "skipped-fresh", "skipped-cooldown", "error"]),
|
|
72
|
+
models: z.number(),
|
|
73
|
+
added: z.array(z.string()),
|
|
74
|
+
checkedAt: z.number().nullable(),
|
|
75
|
+
/** Set with `skipped-cooldown`: milliseconds until a forced refresh is accepted again. */
|
|
76
|
+
retryAfterMs: z.number().optional(),
|
|
77
|
+
error: z.string().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const ModelCatalogOverlayBodySchema = z.object({
|
|
81
|
+
provider: z.string().min(1),
|
|
82
|
+
modelId: z.string().min(1),
|
|
83
|
+
name: z.string().optional(),
|
|
84
|
+
family: z.string().optional(),
|
|
85
|
+
releaseDate: z.string().optional(),
|
|
86
|
+
contextWindow: z.number().int().positive().optional(),
|
|
87
|
+
maxOutput: z.number().int().positive().optional(),
|
|
88
|
+
reasoning: z.boolean().optional(),
|
|
89
|
+
reasoningOptions: z.array(ReasoningOptionSchema).optional(),
|
|
90
|
+
pricing: OverlayPricingSchema.optional(),
|
|
91
|
+
status: z.string().optional(),
|
|
92
|
+
reason: z.string().min(1),
|
|
93
|
+
verifiedBy: z.string().optional(),
|
|
94
|
+
expiresWhenUpstreamMatches: z.boolean().optional(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const OverlayEntrySchema = ModelCatalogOverlayBodySchema.extend({
|
|
98
|
+
name: z.string().nullish(),
|
|
99
|
+
family: z.string().nullish(),
|
|
100
|
+
releaseDate: z.string().nullish(),
|
|
101
|
+
contextWindow: z.number().nullish(),
|
|
102
|
+
maxOutput: z.number().nullish(),
|
|
103
|
+
reasoning: z.boolean().nullish(),
|
|
104
|
+
reasoningOptions: z.array(ReasoningOptionSchema).nullish(),
|
|
105
|
+
pricing: OverlayPricingSchema.nullish(),
|
|
106
|
+
status: z.string().nullish(),
|
|
107
|
+
verifiedBy: z.string().nullish(),
|
|
108
|
+
expiresWhenUpstreamMatches: z.boolean(),
|
|
109
|
+
createdAt: z.number(),
|
|
110
|
+
updatedAt: z.number(),
|
|
111
|
+
});
|
|
112
|
+
|
|
31
113
|
const getCatalog = route({
|
|
32
114
|
method: "get",
|
|
33
115
|
path: "/api/models-catalog",
|
|
@@ -48,6 +130,192 @@ const getCatalog = route({
|
|
|
48
130
|
},
|
|
49
131
|
});
|
|
50
132
|
|
|
133
|
+
const ModelTierPreviewSchema = z.object({
|
|
134
|
+
provider: ProviderNameSchema,
|
|
135
|
+
tier: z.enum(MODEL_TIERS),
|
|
136
|
+
key: z.string(),
|
|
137
|
+
defaultValue: z.string(),
|
|
138
|
+
configured: z.string().nullable(),
|
|
139
|
+
source: z.enum(["tier-config", "tier-default"]),
|
|
140
|
+
resolvedModel: z.string().nullable(),
|
|
141
|
+
alias: z.string().nullable(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const getTiers = route({
|
|
145
|
+
method: "get",
|
|
146
|
+
path: "/api/models-catalog/tiers",
|
|
147
|
+
pattern: ["api", "models-catalog", "tiers"],
|
|
148
|
+
summary: "Preview what each model tier resolves to per harness provider",
|
|
149
|
+
description:
|
|
150
|
+
"One row per provider and tier: the built-in default, the `MODEL_TIER_<PROVIDER>_<TIER>` value stored in swarm config (if any), which layer wins, and the concrete model it resolves to against the current catalog (`latest:` aliases resolved with the same soak and auto-upgrade rules as claim time, without recording a resolution). Ignores per-worker `MODEL_TIER_*` overrides and per-task models.",
|
|
151
|
+
tags: ["Pricing"],
|
|
152
|
+
responses: {
|
|
153
|
+
200: {
|
|
154
|
+
description: "Tier previews",
|
|
155
|
+
schema: z.object({ tiers: z.array(ModelTierPreviewSchema) }),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const refreshCatalog = route({
|
|
161
|
+
method: "post",
|
|
162
|
+
path: "/api/models-catalog/refresh",
|
|
163
|
+
pattern: ["api", "models-catalog", "refresh"],
|
|
164
|
+
summary: "Refresh the model catalog from models.dev",
|
|
165
|
+
description:
|
|
166
|
+
"Unforced calls skip the network when the last models.dev check is younger than 4h (`skipped-fresh`). `force: true` always fetches, still conditional on the stored ETag (`not-modified` on 304), but a forced call within a minute of the previous one returns `skipped-cooldown` with `retryAfterMs`. Concurrent refreshes share one fetch. Lead agent or operator only. `added` lists provider/modelId keys new since the previous fetch.",
|
|
167
|
+
tags: ["Pricing"],
|
|
168
|
+
rbac: { permission: "models.catalog.write" },
|
|
169
|
+
body: z.object({ force: z.boolean().optional() }),
|
|
170
|
+
responses: {
|
|
171
|
+
200: { description: "Refresh outcome", schema: ModelCatalogRefreshResultSchema },
|
|
172
|
+
403: { description: "Forbidden" },
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const upsertOverlay = route({
|
|
177
|
+
method: "put",
|
|
178
|
+
path: "/api/models-catalog/overlay",
|
|
179
|
+
pattern: ["api", "models-catalog", "overlay"],
|
|
180
|
+
summary: "Upsert one model-catalog overlay row",
|
|
181
|
+
description:
|
|
182
|
+
"Overlay facts win per non-null field over the models.dev row; overlay-only models are served too. Overlay prices fill pricing-table gaps (never override an active price). With `expiresWhenUpstreamMatches` (default true) the row is deleted once upstream matches every non-null fact.",
|
|
183
|
+
tags: ["Pricing"],
|
|
184
|
+
rbac: { permission: "models.catalog.write" },
|
|
185
|
+
body: ModelCatalogOverlayBodySchema,
|
|
186
|
+
responses: {
|
|
187
|
+
200: { description: "Overlay row upserted", schema: OverlayEntrySchema },
|
|
188
|
+
403: { description: "Forbidden" },
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const deleteOverlay = route({
|
|
193
|
+
method: "delete",
|
|
194
|
+
path: "/api/models-catalog/overlay",
|
|
195
|
+
pattern: ["api", "models-catalog", "overlay"],
|
|
196
|
+
summary: "Delete one model-catalog overlay row",
|
|
197
|
+
tags: ["Pricing"],
|
|
198
|
+
rbac: { permission: "models.catalog.write" },
|
|
199
|
+
body: z.object({ provider: z.string().min(1), modelId: z.string().min(1) }),
|
|
200
|
+
responses: {
|
|
201
|
+
200: { description: "Overlay row deleted", schema: z.object({ deleted: z.boolean() }) },
|
|
202
|
+
403: { description: "Forbidden" },
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const HarnessSupportRowSchema = z.object({
|
|
207
|
+
harness: z.string(),
|
|
208
|
+
cliVersion: z.string(),
|
|
209
|
+
modelId: z.string(),
|
|
210
|
+
status: z.enum(["ok", "unsupported", "unknown"]),
|
|
211
|
+
checkedAt: z.number(),
|
|
212
|
+
error: z.string().nullable(),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const listHarnessSupport = route({
|
|
216
|
+
method: "get",
|
|
217
|
+
path: "/api/models-catalog/harness-support",
|
|
218
|
+
pattern: ["api", "models-catalog", "harness-support"],
|
|
219
|
+
summary: "List harness CLI model support rows",
|
|
220
|
+
description:
|
|
221
|
+
"Whether a catalog model runs on a given `claude` / `codex` CLI version, as recorded by workers after a model's first run. No row means unknown (allowed at claim).",
|
|
222
|
+
tags: ["Pricing"],
|
|
223
|
+
query: z.object({ harness: z.string().optional(), cliVersion: z.string().optional() }),
|
|
224
|
+
responses: {
|
|
225
|
+
200: {
|
|
226
|
+
description: "Support rows, newest first",
|
|
227
|
+
schema: z.object({ rows: z.array(HarnessSupportRowSchema) }),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const recordHarnessSupport = route({
|
|
233
|
+
method: "put",
|
|
234
|
+
path: "/api/models-catalog/harness-support",
|
|
235
|
+
pattern: ["api", "models-catalog", "harness-support"],
|
|
236
|
+
summary: "Record whether a harness CLI version accepts a model",
|
|
237
|
+
description:
|
|
238
|
+
"Written by workers: `ok` after a model's first successful run, `unsupported` when the CLI rejects the model id. Claim-time resolution falls back (alias/tier) or fails fast (explicit model) on `unsupported`.",
|
|
239
|
+
tags: ["Pricing"],
|
|
240
|
+
rbac: { permission: "models.harness-support.write" },
|
|
241
|
+
body: z.object({
|
|
242
|
+
harness: z.string().min(1).max(32),
|
|
243
|
+
cliVersion: z.string().min(1).max(64),
|
|
244
|
+
modelId: z.string().min(1).max(200),
|
|
245
|
+
status: z.enum(["ok", "unsupported", "unknown"]),
|
|
246
|
+
error: z.string().max(4000).optional(),
|
|
247
|
+
}),
|
|
248
|
+
responses: {
|
|
249
|
+
200: { description: "Support row upserted", schema: HarnessSupportRowSchema },
|
|
250
|
+
403: { description: "Forbidden" },
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve the caller and gate on `verb`; writes a 403 on denial. Refresh and overlay writes
|
|
256
|
+
* (`models.catalog.write`) are lead or operator only. Harness support (`models.harness-support.write`)
|
|
257
|
+
* is a registered agent's report about its own CLI (bound to it by `callerOwnsSupportRow`) or an
|
|
258
|
+
* operator correction. Dashboard users can do neither.
|
|
259
|
+
*
|
|
260
|
+
* Trust boundary: a bound identity (an `aswt_` user token or an `aseph_` session token) is honoured
|
|
261
|
+
* as is. Beyond that, the shared swarm key is the operator credential and `X-Agent-ID` is
|
|
262
|
+
* self-declared, the same model `config.ts`, `mcp-servers.ts` and `extensions.ts` use. Every
|
|
263
|
+
* worker holds that key, so this gate stops a mistaken or ordinary worker call, not a worker that
|
|
264
|
+
* chooses to drop or forge the header; that needs per-agent credentials swarm-wide.
|
|
265
|
+
*/
|
|
266
|
+
async function ensureCatalogWriter(
|
|
267
|
+
req: IncomingMessage,
|
|
268
|
+
res: ServerResponse,
|
|
269
|
+
verb: "models.catalog.write" | "models.harness-support.write" = "models.catalog.write",
|
|
270
|
+
): Promise<{ userId: string | null; agentId: string | null } | null> {
|
|
271
|
+
const auth = getRequestAuth(req);
|
|
272
|
+
const header = req.headers["x-agent-id"];
|
|
273
|
+
const agentId =
|
|
274
|
+
auth?.kind === "agent" ? auth.agentId : Array.isArray(header) ? header[0] : header;
|
|
275
|
+
let principal: RbacPrincipal;
|
|
276
|
+
if (auth?.kind === "user") {
|
|
277
|
+
principal = { kind: "user", userId: auth.userId };
|
|
278
|
+
} else if (agentId) {
|
|
279
|
+
// An X-Agent-ID wins over the shared API key: a worker holds that key, so treating it as the
|
|
280
|
+
// operator would let any worker skip the verb's lead-only rule.
|
|
281
|
+
const agent = await getAgentById(agentId);
|
|
282
|
+
principal = { kind: "agent", agentId, isLead: agent?.isLead ?? false };
|
|
283
|
+
} else if (auth?.kind === "operator") {
|
|
284
|
+
principal = { kind: "operator" };
|
|
285
|
+
} else {
|
|
286
|
+
principal = { kind: "agent", agentId: "", isLead: false };
|
|
287
|
+
}
|
|
288
|
+
const decision = can({
|
|
289
|
+
principal,
|
|
290
|
+
verb,
|
|
291
|
+
resource: { kind: "none" },
|
|
292
|
+
source: "http",
|
|
293
|
+
});
|
|
294
|
+
if (!decision.allow) {
|
|
295
|
+
jsonError(
|
|
296
|
+
res,
|
|
297
|
+
verb === "models.catalog.write"
|
|
298
|
+
? "Writing the model catalog requires the lead agent or an operator"
|
|
299
|
+
: "Recording harness model support requires a registered agent or an operator",
|
|
300
|
+
403,
|
|
301
|
+
);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
userId: auth?.kind === "user" ? auth.userId : null,
|
|
306
|
+
agentId: principal.kind === "agent" && principal.agentId ? principal.agentId : null,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function callerOwnsSupportRow(
|
|
311
|
+
agentId: string,
|
|
312
|
+
body: { harness: string; cliVersion: string },
|
|
313
|
+
): Promise<boolean> {
|
|
314
|
+
const agent = await getAgentById(agentId);
|
|
315
|
+
if (!agent || agent.harnessProvider !== body.harness) return false;
|
|
316
|
+
return (await getAgentHarnessCliVersion(agentId)) === body.cliVersion;
|
|
317
|
+
}
|
|
318
|
+
|
|
51
319
|
export async function handleModelsCatalog(
|
|
52
320
|
req: IncomingMessage,
|
|
53
321
|
res: ServerResponse,
|
|
@@ -57,7 +325,88 @@ export async function handleModelsCatalog(
|
|
|
57
325
|
if (getCatalog.match(req.method, pathSegments)) {
|
|
58
326
|
const parsed = await getCatalog.parse(req, res, pathSegments, queryParams);
|
|
59
327
|
if (!parsed) return true;
|
|
60
|
-
|
|
328
|
+
getCatalog.respond(res, 200, await loadModelsCatalog());
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (getTiers.match(req.method, pathSegments)) {
|
|
333
|
+
const parsed = await getTiers.parse(req, res, pathSegments, queryParams);
|
|
334
|
+
if (!parsed) return true;
|
|
335
|
+
getTiers.respond(res, 200, { tiers: await previewModelTiers() });
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (refreshCatalog.match(req.method, pathSegments)) {
|
|
340
|
+
const parsed = await refreshCatalog.parse(req, res, pathSegments, queryParams);
|
|
341
|
+
if (!parsed) return true;
|
|
342
|
+
if (!(await ensureCatalogWriter(req, res))) return true;
|
|
343
|
+
refreshCatalog.respond(
|
|
344
|
+
res,
|
|
345
|
+
200,
|
|
346
|
+
await requestModelCatalogRefresh({ force: parsed.body.force }),
|
|
347
|
+
);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (upsertOverlay.match(req.method, pathSegments)) {
|
|
352
|
+
const parsed = await upsertOverlay.parse(req, res, pathSegments, queryParams);
|
|
353
|
+
if (!parsed) return true;
|
|
354
|
+
const writer = await ensureCatalogWriter(req, res);
|
|
355
|
+
if (!writer) return true;
|
|
356
|
+
const entry = await upsertModelCatalogOverlay(parsed.body, { userId: writer.userId });
|
|
357
|
+
upsertOverlay.respond(res, 200, entry);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (deleteOverlay.match(req.method, pathSegments)) {
|
|
362
|
+
const parsed = await deleteOverlay.parse(req, res, pathSegments, queryParams);
|
|
363
|
+
if (!parsed) return true;
|
|
364
|
+
if (!(await ensureCatalogWriter(req, res))) return true;
|
|
365
|
+
const deleted = await deleteModelCatalogOverlay(parsed.body.provider, parsed.body.modelId);
|
|
366
|
+
deleteOverlay.respond(res, 200, { deleted });
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (listHarnessSupport.match(req.method, pathSegments)) {
|
|
371
|
+
const parsed = await listHarnessSupport.parse(req, res, pathSegments, queryParams);
|
|
372
|
+
if (!parsed) return true;
|
|
373
|
+
listHarnessSupport.respond(res, 200, { rows: await listHarnessModelSupport(parsed.query) });
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (recordHarnessSupport.match(req.method, pathSegments)) {
|
|
378
|
+
const parsed = await recordHarnessSupport.parse(req, res, pathSegments, queryParams);
|
|
379
|
+
if (!parsed) return true;
|
|
380
|
+
const writer = await ensureCatalogWriter(req, res, "models.harness-support.write");
|
|
381
|
+
if (!writer) return true;
|
|
382
|
+
if (writer.agentId && !(await callerOwnsSupportRow(writer.agentId, parsed.body))) {
|
|
383
|
+
// Support rows are shared per (harness, CLI version), so a worker may only write the row
|
|
384
|
+
// for the harness and CLI version it registered, never an arbitrary tuple.
|
|
385
|
+
jsonError(
|
|
386
|
+
res,
|
|
387
|
+
"Harness support can only be recorded for the calling agent's own harness and registered CLI version",
|
|
388
|
+
403,
|
|
389
|
+
);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
writer.agentId &&
|
|
394
|
+
parsed.body.status === "unsupported" &&
|
|
395
|
+
!(
|
|
396
|
+
isUnknownModelError(parsed.body.error, parsed.body.modelId) &&
|
|
397
|
+
(await agentRanModel(writer.agentId, parsed.body.modelId))
|
|
398
|
+
)
|
|
399
|
+
) {
|
|
400
|
+
// An `unsupported` row changes claims on every peer with this CLI, so an agent may only
|
|
401
|
+
// report it for a model it was assigned, with the CLI's model-rejection text.
|
|
402
|
+
jsonError(
|
|
403
|
+
res,
|
|
404
|
+
"An unsupported report needs a model the calling agent ran and the CLI's model-rejection error",
|
|
405
|
+
403,
|
|
406
|
+
);
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
recordHarnessSupport.respond(res, 200, await recordHarnessModelSupport(parsed.body));
|
|
61
410
|
return true;
|
|
62
411
|
}
|
|
63
412
|
|
package/src/http/poll.ts
CHANGED
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
upsertChannelActivityCursor,
|
|
29
29
|
} from "../be/db";
|
|
30
30
|
import { renderIdentity, resolveIdentity } from "../be/identity";
|
|
31
|
+
import {
|
|
32
|
+
parseModelTierOverridesHeader,
|
|
33
|
+
recordClaimModelResolution,
|
|
34
|
+
setAgentModelTierOverrides,
|
|
35
|
+
} from "../be/model-tier-resolution";
|
|
31
36
|
import { touchRuntimeInstance } from "../be/multi-runtime";
|
|
32
37
|
import { hasCapability } from "../server";
|
|
33
38
|
import { fetchChannelActivity } from "../slack/channel-activity";
|
|
@@ -168,7 +173,15 @@ const pollTriggers = route({
|
|
|
168
173
|
summary: "Poll for triggers (tasks, mentions)",
|
|
169
174
|
tags: ["Poll"],
|
|
170
175
|
auth: { apiKey: true, agentId: true },
|
|
171
|
-
headers: runtimeInstanceHeader("poll for work")
|
|
176
|
+
headers: runtimeInstanceHeader("poll for work").extend({
|
|
177
|
+
"X-Model-Tier-Overrides": z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe(
|
|
181
|
+
"URL-encoded JSON {provider: {tier: model}} of the worker's MODEL_TIER_* env overrides. " +
|
|
182
|
+
"Stored on the agent row and applied at claim time (modelSource=worker-env).",
|
|
183
|
+
),
|
|
184
|
+
}),
|
|
172
185
|
responses: {
|
|
173
186
|
200: { description: "Trigger data or null", schema: pollResponseSchema },
|
|
174
187
|
400: { description: "Missing X-Agent-ID" },
|
|
@@ -230,6 +243,22 @@ async function attachmentsForTrigger(
|
|
|
230
243
|
}));
|
|
231
244
|
}
|
|
232
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Claim-time model resolution (runbooks/model-tiers.md). Never blocks a claim:
|
|
248
|
+
* a resolution failure leaves the worker on its own local resolution.
|
|
249
|
+
*/
|
|
250
|
+
async function claimModelFields(
|
|
251
|
+
task: { id: string; model?: string | null; modelTier?: string | null },
|
|
252
|
+
agent: Parameters<typeof recordClaimModelResolution>[1],
|
|
253
|
+
): Promise<Awaited<ReturnType<typeof recordClaimModelResolution>>> {
|
|
254
|
+
try {
|
|
255
|
+
return await recordClaimModelResolution(task, agent);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn(`[/api/poll] model resolution failed for task ${task.id}:`, error);
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
233
262
|
// ─── Cursor Commit Endpoint ─────────────────────────────────────────────────
|
|
234
263
|
|
|
235
264
|
const commitCursorsRoute = route({
|
|
@@ -268,6 +297,9 @@ export async function handlePoll(
|
|
|
268
297
|
const runtimeInstanceId = ((h) => (Array.isArray(h) ? h[0] : h))(
|
|
269
298
|
req.headers["x-runtime-instance-id"],
|
|
270
299
|
);
|
|
300
|
+
const modelTierOverrides = parseModelTierOverridesHeader(
|
|
301
|
+
((h) => (Array.isArray(h) ? h[0] : h))(req.headers["x-model-tier-overrides"]),
|
|
302
|
+
);
|
|
271
303
|
// Handle cursor commit endpoint
|
|
272
304
|
if (commitCursorsRoute.match(req.method, pathSegments)) {
|
|
273
305
|
const parsed = await commitCursorsRoute.parse(req, res, pathSegments, queryParams);
|
|
@@ -311,6 +343,12 @@ export async function handlePoll(
|
|
|
311
343
|
return { error: "Agent not found", status: 404 };
|
|
312
344
|
}
|
|
313
345
|
|
|
346
|
+
// The worker's parsed MODEL_TIER_* env overrides ride on every poll so
|
|
347
|
+
// claim-time resolution can honor them (no write when unchanged).
|
|
348
|
+
if (modelTierOverrides) {
|
|
349
|
+
await setAgentModelTierOverrides(agent.id, modelTierOverrides);
|
|
350
|
+
}
|
|
351
|
+
|
|
314
352
|
// Extension identities authenticate to the API but never execute
|
|
315
353
|
// work: no offers, no pending assignments, no pool claims.
|
|
316
354
|
if (isExtensionAgent(agent)) {
|
|
@@ -448,6 +486,7 @@ export async function handlePoll(
|
|
|
448
486
|
taskId: pendingTask.id,
|
|
449
487
|
task: {
|
|
450
488
|
...pendingTask,
|
|
489
|
+
...(await claimModelFields(pendingTask, agent)),
|
|
451
490
|
status: "in_progress",
|
|
452
491
|
attachments: await attachmentsForTrigger(pendingTask.id),
|
|
453
492
|
},
|
|
@@ -576,7 +615,11 @@ export async function handlePoll(
|
|
|
576
615
|
trigger: {
|
|
577
616
|
type: "task_assigned",
|
|
578
617
|
taskId: claimed.id,
|
|
579
|
-
task: {
|
|
618
|
+
task: {
|
|
619
|
+
...claimed,
|
|
620
|
+
...(await claimModelFields(claimed, agent)),
|
|
621
|
+
attachments: await attachmentsForTrigger(claimed.id),
|
|
622
|
+
},
|
|
580
623
|
...(claimedRequestedBy && { requestedBy: claimedRequestedBy }),
|
|
581
624
|
},
|
|
582
625
|
};
|
package/src/http/schedules.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
updateScheduledTask,
|
|
17
17
|
withFavoriteFlags,
|
|
18
18
|
} from "../be/db";
|
|
19
|
+
import { explicitModelErrorForAgent } from "../be/model-validation";
|
|
19
20
|
import { mergeScheduleTiming, validateRecurringTiming } from "../be/schedules/validate";
|
|
20
21
|
import { getScript } from "../be/scripts/db";
|
|
21
22
|
import { calculateNextRun, dispatchScheduleTarget } from "../scheduler/scheduler";
|
|
@@ -49,6 +50,8 @@ const scheduleUpdateBodySchema = z.object({
|
|
|
49
50
|
timezone: z.string().optional(),
|
|
50
51
|
model: z.string().nullable().optional(),
|
|
51
52
|
modelTier: ModelTierSchema.nullable().optional(),
|
|
53
|
+
/** Accept a `model` the catalog does not list (a custom id). Without it an unknown id is a 400. */
|
|
54
|
+
allowCustomModel: z.boolean().optional(),
|
|
52
55
|
nextRunAt: z.string().nullable().optional(),
|
|
53
56
|
targetType: ScheduledTaskTargetTypeSchema.optional(),
|
|
54
57
|
workflowId: z.string().uuid().nullable().optional(),
|
|
@@ -108,6 +111,8 @@ const createSchedule = route({
|
|
|
108
111
|
timezone: z.string().optional(),
|
|
109
112
|
model: z.string().optional(),
|
|
110
113
|
modelTier: ModelTierSchema.optional(),
|
|
114
|
+
/** Accept a `model` the catalog does not list (a custom id). Without it an unknown id is a 400. */
|
|
115
|
+
allowCustomModel: z.boolean().optional(),
|
|
111
116
|
scheduleType: z.enum(["recurring", "one_time"]).optional(),
|
|
112
117
|
targetType: ScheduledTaskTargetTypeSchema.optional(),
|
|
113
118
|
workflowId: z.string().uuid().optional(),
|
|
@@ -389,6 +394,16 @@ export async function handleSchedules(
|
|
|
389
394
|
}
|
|
390
395
|
}
|
|
391
396
|
|
|
397
|
+
const modelError = await explicitModelErrorForAgent({
|
|
398
|
+
model: splitLegacyModelAlias({ model: body.model, modelTier: body.modelTier }).model,
|
|
399
|
+
allowCustomModel: body.allowCustomModel,
|
|
400
|
+
agentId: body.targetAgentId,
|
|
401
|
+
});
|
|
402
|
+
if (modelError) {
|
|
403
|
+
jsonError(res, modelError, 400);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
392
407
|
try {
|
|
393
408
|
const trustedUserId = await resolveHttpAuditUserId(req, myAgentId);
|
|
394
409
|
let key: string | undefined;
|
|
@@ -535,23 +550,38 @@ export async function handleSchedules(
|
|
|
535
550
|
throw error;
|
|
536
551
|
}
|
|
537
552
|
}
|
|
553
|
+
// The escape hatch is a request flag, not a schedule column. `body` aliases
|
|
554
|
+
// `parsed.body`, so read the flag before stripping it.
|
|
555
|
+
const allowCustomModel = parsed.body.allowCustomModel;
|
|
556
|
+
delete body.allowCustomModel;
|
|
557
|
+
const existing = await getScheduledTaskById(parsed.params.id);
|
|
558
|
+
if (!existing) {
|
|
559
|
+
jsonError(res, "Schedule not found", 404);
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
538
562
|
if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
|
|
539
563
|
const normalizedModel = splitLegacyModelAlias({
|
|
540
564
|
model: parsed.body.model,
|
|
541
565
|
modelTier: parsed.body.modelTier,
|
|
542
566
|
});
|
|
567
|
+
// A model the schedule already stores is not re-judged, so an unrelated edit still saves.
|
|
568
|
+
if (normalizedModel.model !== existing.model) {
|
|
569
|
+
const modelError = await explicitModelErrorForAgent({
|
|
570
|
+
model: normalizedModel.model,
|
|
571
|
+
allowCustomModel,
|
|
572
|
+
agentId: parsed.body.targetAgentId ?? existing.targetAgentId,
|
|
573
|
+
});
|
|
574
|
+
if (modelError) {
|
|
575
|
+
jsonError(res, modelError, 400);
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
543
579
|
if (parsed.body.model !== undefined) body.model = normalizedModel.model ?? null;
|
|
544
580
|
if (parsed.body.modelTier !== undefined || normalizedModel.modelTier) {
|
|
545
581
|
body.modelTier = normalizedModel.modelTier ?? null;
|
|
546
582
|
}
|
|
547
583
|
}
|
|
548
584
|
|
|
549
|
-
const existing = await getScheduledTaskById(parsed.params.id);
|
|
550
|
-
if (!existing) {
|
|
551
|
-
jsonError(res, "Schedule not found", 404);
|
|
552
|
-
return true;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
585
|
// Reject updates on completed one-time schedules
|
|
556
586
|
if (existing.scheduleType === "one_time" && !existing.enabled && existing.lastRunAt) {
|
|
557
587
|
jsonError(res, "One-time schedule has already executed. Create a new one instead.", 400);
|
package/src/http/session-data.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
getSessionCostsFiltered,
|
|
14
14
|
getSessionLogsByTaskId,
|
|
15
15
|
getTaskById,
|
|
16
|
-
getUsageDataVersion,
|
|
17
16
|
} from "../be/db";
|
|
18
17
|
import { recordSessionCost } from "../otel";
|
|
19
18
|
import { incrementServerSessionsProcessed } from "../server-runtime-counters";
|
|
@@ -457,8 +456,7 @@ export async function handleSessionData(
|
|
|
457
456
|
userId: parsed.query.userId || undefined,
|
|
458
457
|
groupBy: parsed.query.groupBy || "both",
|
|
459
458
|
} as const;
|
|
460
|
-
const
|
|
461
|
-
const summary = await cachedUsageReport(`summary:${version}:${JSON.stringify(opts)}`, () =>
|
|
459
|
+
const summary = await cachedUsageReport(`summary:${JSON.stringify(opts)}`, () =>
|
|
462
460
|
getSessionCostSummary(opts),
|
|
463
461
|
);
|
|
464
462
|
getSessionCostSummaryRoute.respond(res, 200, summary);
|
|
@@ -478,8 +476,7 @@ export async function handleSessionData(
|
|
|
478
476
|
startDate: parsed.query.startDate || undefined,
|
|
479
477
|
endDate: parsed.query.endDate || undefined,
|
|
480
478
|
};
|
|
481
|
-
const
|
|
482
|
-
const rows = await cachedUsageReport(`attribution:${version}:${JSON.stringify(opts)}`, () =>
|
|
479
|
+
const rows = await cachedUsageReport(`attribution:${JSON.stringify(opts)}`, () =>
|
|
483
480
|
getAttributionByPerson(opts),
|
|
484
481
|
);
|
|
485
482
|
getAttributionByPersonRoute.respond(res, 200, { rows });
|
package/src/http/stats.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
getAgentStatusCounts,
|
|
5
5
|
getAllLogs,
|
|
6
6
|
getAllServices,
|
|
7
7
|
getConcurrentContext,
|
|
@@ -15,6 +15,7 @@ import { getDbRetentionStats } from "../be/db-retention";
|
|
|
15
15
|
import { isSteeringEnabled } from "../be/steering";
|
|
16
16
|
import type { AgentLog } from "../types";
|
|
17
17
|
import { AgentLogSchema, ScheduledTaskSchema, ServiceSchema } from "../types";
|
|
18
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
18
19
|
import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
|
|
19
20
|
import { resolveHttpFavoriteOwner } from "./favorite-owner";
|
|
20
21
|
import { route } from "./route-def";
|
|
@@ -51,6 +52,7 @@ const DashboardStatsSchema = z.object({
|
|
|
51
52
|
tasks: TaskCountsSchema,
|
|
52
53
|
steeringEnabled: z.boolean(),
|
|
53
54
|
multiRuntimeEnabled: z.boolean(),
|
|
55
|
+
devMode: z.boolean(),
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
/** Mirrors `DbRetentionTableStats` (src/be/db-retention.ts). */
|
|
@@ -247,16 +249,11 @@ export async function handleStats(
|
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
if (getStats.match(req.method, pathSegments)) {
|
|
250
|
-
const agents = await
|
|
252
|
+
const agents = await getAgentStatusCounts();
|
|
251
253
|
const taskStats = await getTaskStats();
|
|
252
254
|
|
|
253
255
|
const stats = {
|
|
254
|
-
agents
|
|
255
|
-
total: agents.length,
|
|
256
|
-
idle: agents.filter((a) => a.status === "idle").length,
|
|
257
|
-
busy: agents.filter((a) => a.status === "busy").length,
|
|
258
|
-
offline: agents.filter((a) => a.status === "offline").length,
|
|
259
|
-
},
|
|
256
|
+
agents,
|
|
260
257
|
tasks: {
|
|
261
258
|
total: taskStats.total,
|
|
262
259
|
unassigned: taskStats.unassigned,
|
|
@@ -271,6 +268,7 @@ export async function handleStats(
|
|
|
271
268
|
// Authenticated home for the steering feature flag — deliberately NOT on
|
|
272
269
|
// the unauthenticated /health endpoint (config must not leak).
|
|
273
270
|
steeringEnabled: isSteeringEnabled(),
|
|
271
|
+
devMode: isEnvFlagEnabled("SWARM_DEV_MODE", false),
|
|
274
272
|
// Same authenticated home, for the same reason. Read through the helper
|
|
275
273
|
// the server itself branches on, and read PER REQUEST rather than
|
|
276
274
|
// captured at module load, so the reported value tracks a `swarm_config`
|