@desplega.ai/agent-swarm 2.0.0 → 2.1.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-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
- package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
- package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
- package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
- package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
- package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
- package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
- package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
- package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
- package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
- package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
- package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
- package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
- package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
- package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
- package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
- package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
- package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
- package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
- package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
- package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
- package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
- package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
- package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
- package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
- package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
- package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
- package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
- package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
- package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
- package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
- package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
- package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
- package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
- package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
- package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
- package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
- package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
- package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
- package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
- package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
- package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
- package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
- package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
- package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
- package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
- package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
- package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
- package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
- package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
- package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
- package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
- package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
- package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
- package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
- package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
- package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
- package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
- package/dist/cli-wfnemr1t.js +86 -0
- package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
- package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
- package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
- package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
- package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
- package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
- package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
- package/dist/cli.js +24 -23
- package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
- package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
- package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
- package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
- package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
- package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
- package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
- package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
- package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
- package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
- package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
- package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
- package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
- package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
- package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
- package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
- package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
- package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
- package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
- package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
- package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
- package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
- package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
- package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
- package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
- package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
- package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
- package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
- package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
- package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
- package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
- package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
- package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
- package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
- package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
- package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
- package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
- package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
- package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
- package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
- package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
- package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
- package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
- package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
- package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
- package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
- package/openapi.json +777 -6
- package/package.json +8 -7
- package/src/agentmail/handlers.ts +21 -11
- package/src/apps/definition.ts +16 -0
- package/src/azure-devops/api.ts +52 -2
- package/src/azure-devops/boards-sync.ts +50 -20
- package/src/azure-devops/index.ts +1 -0
- package/src/azure-devops/outbound.ts +95 -0
- package/src/be/boot-seeds.ts +11 -0
- package/src/be/db/model-profiles.ts +315 -0
- package/src/be/db/tasks/read.ts +7 -0
- package/src/be/db.ts +53 -47
- package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
- package/src/be/migrations/220_model_profiles.sql +137 -0
- package/src/be/model-tier-resolution.ts +145 -11
- package/src/be/model-validation.ts +8 -1
- package/src/be/scripts/typecheck.ts +12 -0
- package/src/be/secret-registry.ts +6 -2
- package/src/be/seed-model-profiles.ts +41 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/runner.ts +3 -17
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/loader.ts +10 -5
- package/src/heartbeat/heartbeat.ts +2 -1
- package/src/http/agents.ts +49 -2
- package/src/http/all-routes.ts +1 -0
- package/src/http/apps.ts +30 -3
- package/src/http/config.ts +26 -2
- package/src/http/core.ts +4 -0
- package/src/http/index.ts +11 -1
- package/src/http/model-profiles.ts +303 -0
- package/src/http/pages-public.ts +11 -5
- package/src/http/poll.ts +16 -5
- package/src/http/script-runs.ts +1 -1
- package/src/http/scripts.ts +25 -2
- package/src/http/tasks.ts +18 -0
- package/src/http/webhooks.ts +1 -2
- package/src/pages/feedback-overlay.ts +65 -6
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-extension.ts +17 -3
- package/src/providers/reasoning-effort.ts +10 -0
- package/src/rbac/legacy-policy.ts +8 -0
- package/src/rbac/permissions.ts +5 -0
- package/src/scheduler/deferred-task-waits.ts +18 -5
- package/src/scheduler/scheduler.ts +7 -1
- package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
- package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
- package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
- package/src/scripts-runtime/extract-args-schema.ts +9 -8
- package/src/scripts-runtime/quickjs-compat.ts +464 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +12 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
- package/src/server.ts +10 -1
- package/src/slack/blocks.ts +4 -2
- package/src/slack/reaction-shortcode.ts +14 -0
- package/src/slack/render-v2.ts +12 -7
- package/src/slack/responses.ts +5 -4
- package/src/slack/thread-buffer.ts +41 -22
- package/src/tasks/worker-follow-up.ts +11 -2
- package/src/tests/agentmail-filters.test.ts +47 -0
- package/src/tests/apps-spike2.test.ts +35 -0
- package/src/tests/azure-devops-outbound.test.ts +420 -0
- package/src/tests/db-task-read-contract.test.ts +2 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/deferred-task-wake.test.ts +78 -1
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
- package/src/tests/extensions-loader.test.ts +15 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
- package/src/tests/heartbeat-jev-gate.test.ts +385 -0
- package/src/tests/model-profile-backcompat.test.ts +327 -0
- package/src/tests/model-profile-providers.test.ts +302 -0
- package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
- package/src/tests/model-profiles-api.test.ts +614 -0
- package/src/tests/pages-feedback-keys.test.ts +121 -0
- package/src/tests/pages-feedback.test.ts +3 -0
- package/src/tests/pi-context-compaction.test.ts +193 -0
- package/src/tests/profile-claim.test.ts +269 -0
- package/src/tests/quickjs-compat.test.ts +213 -0
- package/src/tests/rbac-engine.test.ts +21 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +252 -1
- package/src/tests/script-executor-quickjs.test.ts +96 -0
- package/src/tests/script-runs-http.test.ts +89 -0
- package/src/tests/scripts-http.test.ts +16 -0
- package/src/tests/secret-scrubber.test.ts +10 -0
- package/src/tests/seed-model-profiles.test.ts +174 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/skill-invocations.test.ts +4 -2
- package/src/tests/slack-blocks.test.ts +36 -0
- package/src/tests/slack-inline-output.test.ts +14 -0
- package/src/tests/slack-reaction-config.test.ts +20 -0
- package/src/tests/slack-render-v2.test.ts +37 -0
- package/src/tests/slack-thread-buffer.test.ts +98 -1
- package/src/tests/workflow-llm.test.ts +61 -0
- package/src/tools/model-profiles/list-model-profiles.ts +54 -0
- package/src/tools/request-human-input.ts +11 -3
- package/src/tools/script-apis.ts +5 -1
- package/src/tools/script-upsert.ts +4 -1
- package/src/tools/send-task.ts +24 -1
- package/src/tools/system-one-decide.ts +104 -0
- package/src/tools/task-action.ts +22 -1
- package/src/tools/tool-config.ts +6 -0
- package/src/types.ts +36 -1
- package/src/utils/secret-scrubber.ts +19 -12
- package/src/utils/vcs-repo-match.ts +29 -0
- package/src/workflows/executors/raw-llm.ts +8 -3
- package/src/workflows/executors/system-one-decision.ts +21 -11
- package/src/workflows/executors/workflow-llm.ts +22 -0
- package/src/workflows/index.ts +21 -12
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/comms-adherence-gate/README.md +88 -0
- package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
- package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
- package/dist/cli-5t4f29ez.js +0 -33
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model profiles: a named (harness, model, effort) picked per task (`profile`)
|
|
3
|
+
* or applied as an agent's default (agent-scoped `MODEL_PROFILE` config row).
|
|
4
|
+
* Claim resolves them (`src/be/model-tier-resolution.ts`). See
|
|
5
|
+
* runbooks/model-tiers.md § Model profiles.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
type ModelProfile,
|
|
9
|
+
type ProviderName,
|
|
10
|
+
ProviderNameSchema,
|
|
11
|
+
type ReasoningEffort,
|
|
12
|
+
ReasoningEffortSchema,
|
|
13
|
+
} from "../../types";
|
|
14
|
+
import { vcsRepoName, vcsRepoUrlMatches } from "../../utils/vcs-repo-match";
|
|
15
|
+
import { getDbClient } from "./runtime";
|
|
16
|
+
import { SUPERSEDED_RESUME_REPAIR_WINDOW_MS } from "./tasks/read";
|
|
17
|
+
|
|
18
|
+
/** Agent-scoped swarm_config key holding the agent's default profile name. */
|
|
19
|
+
export const MODEL_PROFILE_CONFIG_KEY = "MODEL_PROFILE";
|
|
20
|
+
|
|
21
|
+
type ModelProfileRow = {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
harness: string;
|
|
25
|
+
model: string;
|
|
26
|
+
effort: string | null;
|
|
27
|
+
allowCustomModel: number;
|
|
28
|
+
description: string | null;
|
|
29
|
+
seededFrom: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
lastUpdatedAt: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function rowToModelProfile(row: ModelProfileRow): ModelProfile {
|
|
35
|
+
return {
|
|
36
|
+
id: row.id,
|
|
37
|
+
name: row.name,
|
|
38
|
+
harness: ProviderNameSchema.parse(row.harness),
|
|
39
|
+
model: row.model,
|
|
40
|
+
effort: ReasoningEffortSchema.safeParse(row.effort).success
|
|
41
|
+
? (row.effort as ReasoningEffort)
|
|
42
|
+
: null,
|
|
43
|
+
allowCustomModel: row.allowCustomModel === 1,
|
|
44
|
+
description: row.description,
|
|
45
|
+
seededFrom: row.seededFrom,
|
|
46
|
+
createdAt: row.createdAt,
|
|
47
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function listModelProfiles(
|
|
52
|
+
filter: { harness?: ProviderName } = {},
|
|
53
|
+
): Promise<ModelProfile[]> {
|
|
54
|
+
const rows = filter.harness
|
|
55
|
+
? await getDbClient().query<ModelProfileRow>(
|
|
56
|
+
"SELECT * FROM model_profiles WHERE harness = ? ORDER BY harness, name",
|
|
57
|
+
[filter.harness],
|
|
58
|
+
)
|
|
59
|
+
: await getDbClient().query<ModelProfileRow>(
|
|
60
|
+
"SELECT * FROM model_profiles ORDER BY harness, name",
|
|
61
|
+
);
|
|
62
|
+
return rows.map(rowToModelProfile);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getModelProfileByName(name: string): Promise<ModelProfile | null> {
|
|
66
|
+
const row = await getDbClient().get<ModelProfileRow>(
|
|
67
|
+
"SELECT * FROM model_profiles WHERE name = ?",
|
|
68
|
+
[name.trim()],
|
|
69
|
+
);
|
|
70
|
+
return row ? rowToModelProfile(row) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ModelProfileInput {
|
|
74
|
+
name: string;
|
|
75
|
+
harness: ProviderName;
|
|
76
|
+
model: string;
|
|
77
|
+
effort?: ReasoningEffort | null;
|
|
78
|
+
allowCustomModel?: boolean;
|
|
79
|
+
description?: string | null;
|
|
80
|
+
seededFrom?: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Insert a profile. Returns null when the name is taken. */
|
|
84
|
+
export async function createModelProfile(
|
|
85
|
+
input: ModelProfileInput,
|
|
86
|
+
audit: { userId?: string | null } = {},
|
|
87
|
+
): Promise<ModelProfile | null> {
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
const row = await getDbClient().get<ModelProfileRow>(
|
|
90
|
+
`INSERT INTO model_profiles
|
|
91
|
+
(id, name, harness, model, effort, allowCustomModel, description, seededFrom, createdAt, lastUpdatedAt, created_by, updated_by)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
93
|
+
ON CONFLICT(name) DO NOTHING
|
|
94
|
+
RETURNING *`,
|
|
95
|
+
[
|
|
96
|
+
crypto.randomUUID(),
|
|
97
|
+
input.name,
|
|
98
|
+
input.harness,
|
|
99
|
+
input.model,
|
|
100
|
+
input.effort ?? null,
|
|
101
|
+
input.allowCustomModel ? 1 : 0,
|
|
102
|
+
input.description ?? null,
|
|
103
|
+
input.seededFrom ?? null,
|
|
104
|
+
now,
|
|
105
|
+
now,
|
|
106
|
+
audit.userId ?? null,
|
|
107
|
+
audit.userId ?? null,
|
|
108
|
+
],
|
|
109
|
+
);
|
|
110
|
+
return row ? rowToModelProfile(row) : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ModelProfilePatch = Partial<
|
|
114
|
+
Pick<ModelProfileInput, "model" | "effort" | "allowCustomModel" | "description">
|
|
115
|
+
>;
|
|
116
|
+
|
|
117
|
+
/** Update a profile's model, effort or description. The name and harness are fixed. */
|
|
118
|
+
export async function updateModelProfile(
|
|
119
|
+
name: string,
|
|
120
|
+
patch: ModelProfilePatch,
|
|
121
|
+
audit: { userId?: string | null } = {},
|
|
122
|
+
): Promise<ModelProfile | null> {
|
|
123
|
+
const sets: string[] = [];
|
|
124
|
+
const params: (string | number | null)[] = [];
|
|
125
|
+
if (patch.model !== undefined) {
|
|
126
|
+
sets.push("model = ?");
|
|
127
|
+
params.push(patch.model);
|
|
128
|
+
}
|
|
129
|
+
if (patch.effort !== undefined) {
|
|
130
|
+
sets.push("effort = ?");
|
|
131
|
+
params.push(patch.effort);
|
|
132
|
+
}
|
|
133
|
+
if (patch.allowCustomModel !== undefined) {
|
|
134
|
+
sets.push("allowCustomModel = ?");
|
|
135
|
+
params.push(patch.allowCustomModel ? 1 : 0);
|
|
136
|
+
}
|
|
137
|
+
if (patch.description !== undefined) {
|
|
138
|
+
sets.push("description = ?");
|
|
139
|
+
params.push(patch.description);
|
|
140
|
+
}
|
|
141
|
+
sets.push("lastUpdatedAt = ?", "updated_by = ?");
|
|
142
|
+
params.push(new Date().toISOString(), audit.userId ?? null);
|
|
143
|
+
const row = await getDbClient().get<ModelProfileRow>(
|
|
144
|
+
`UPDATE model_profiles SET ${sets.join(", ")} WHERE name = ? RETURNING *`,
|
|
145
|
+
[...params, name],
|
|
146
|
+
);
|
|
147
|
+
return row ? rowToModelProfile(row) : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Tasks (alias `t`) that still hold their profile: unfinished ones, and
|
|
152
|
+
* superseded ones still owed a resume. The resume copies the profile in a
|
|
153
|
+
* later write than the supersede (HTTP supersede, heartbeat remediation), and
|
|
154
|
+
* after a restart the heartbeat's orphan repair makes it, inside the same
|
|
155
|
+
* window (`SUPERSEDED_RESUME_REPAIR_WINDOW_MS`). Binds one parameter: the
|
|
156
|
+
* oldest `finishedAt` the repair still covers.
|
|
157
|
+
*/
|
|
158
|
+
const TASK_HOLDS_PROFILE_SQL = `(
|
|
159
|
+
t.status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
160
|
+
OR (t.status = 'superseded'
|
|
161
|
+
AND t.workflowRunStepId IS NULL
|
|
162
|
+
AND (t.finishedAt IS NULL OR t.finishedAt > ?)
|
|
163
|
+
AND NOT EXISTS (
|
|
164
|
+
SELECT 1 FROM agent_tasks c WHERE c.parentTaskId = t.id AND c.taskType = 'resume')))`;
|
|
165
|
+
|
|
166
|
+
function repairWindowStart(): string {
|
|
167
|
+
return new Date(Date.now() - SUPERSEDED_RESUME_REPAIR_WINDOW_MS).toISOString();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Who still points at a profile: agents with it as default, and tasks that still hold it. */
|
|
171
|
+
export async function modelProfileReferences(
|
|
172
|
+
name: string,
|
|
173
|
+
): Promise<{ agentIds: string[]; openTaskCount: number }> {
|
|
174
|
+
const agents = await getDbClient().query<{ scopeId: string }>(
|
|
175
|
+
"SELECT scopeId FROM swarm_config WHERE scope = 'agent' AND key = ? AND value = ?",
|
|
176
|
+
[MODEL_PROFILE_CONFIG_KEY, name],
|
|
177
|
+
);
|
|
178
|
+
const tasks = await getDbClient().get<{ n: number }>(
|
|
179
|
+
`SELECT COUNT(*) AS n FROM agent_tasks t WHERE t.profile = ? AND ${TASK_HOLDS_PROFILE_SQL}`,
|
|
180
|
+
[name, repairWindowStart()],
|
|
181
|
+
);
|
|
182
|
+
return { agentIds: agents.map((a) => a.scopeId), openTaskCount: tasks?.n ?? 0 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Which migration-220 reference trigger refused a write: `missing` when a task
|
|
187
|
+
* or an agent default names a profile that does not exist.
|
|
188
|
+
*/
|
|
189
|
+
export function modelProfileRefError(error: unknown): "missing" | null {
|
|
190
|
+
const message = error instanceof Error ? error.message : "";
|
|
191
|
+
return message.includes("model-profile-missing:") ? "missing" : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Delete a profile unless a task still holds it (`TASK_HOLDS_PROFILE_SQL`) or
|
|
196
|
+
* an agent default names it. The check is part of the DELETE statement, so a
|
|
197
|
+
* reference written after the caller's own count still makes it `in-use`; the
|
|
198
|
+
* triggers in migration 220 refuse the opposite order (a reference written
|
|
199
|
+
* after this delete).
|
|
200
|
+
*/
|
|
201
|
+
export async function deleteModelProfile(name: string): Promise<"deleted" | "missing" | "in-use"> {
|
|
202
|
+
const row = await getDbClient().get<{ id: string }>(
|
|
203
|
+
`DELETE FROM model_profiles
|
|
204
|
+
WHERE name = ?
|
|
205
|
+
AND NOT EXISTS (
|
|
206
|
+
SELECT 1 FROM agent_tasks t
|
|
207
|
+
WHERE t.profile = model_profiles.name AND ${TASK_HOLDS_PROFILE_SQL})
|
|
208
|
+
AND NOT EXISTS (
|
|
209
|
+
SELECT 1 FROM swarm_config
|
|
210
|
+
WHERE scope = 'agent' AND key = ? AND trim(value) = model_profiles.name)
|
|
211
|
+
RETURNING id`,
|
|
212
|
+
[name, repairWindowStart(), MODEL_PROFILE_CONFIG_KEY],
|
|
213
|
+
);
|
|
214
|
+
if (row) return "deleted";
|
|
215
|
+
return (await getModelProfileByName(name)) ? "in-use" : "missing";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The agent's default profile name (agent-scoped MODEL_PROFILE), or null. */
|
|
219
|
+
export async function getAgentDefaultProfileName(agentId: string): Promise<string | null> {
|
|
220
|
+
const row = await getDbClient().get<{ value: string }>(
|
|
221
|
+
"SELECT value FROM swarm_config WHERE scope = 'agent' AND scopeId = ? AND key = ?",
|
|
222
|
+
[agentId, MODEL_PROFILE_CONFIG_KEY],
|
|
223
|
+
);
|
|
224
|
+
return row?.value.trim() || null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Which of MODEL_OVERRIDE / REASONING_EFFORT_OVERRIDE the task's repo sets at
|
|
229
|
+
* repo scope, exported or not. The worker env takes a repo row over the
|
|
230
|
+
* agent's own (repo > agent), and a withheld one removes the key, so the
|
|
231
|
+
* agent's default profile must not supply that axis. Finds the repo the way
|
|
232
|
+
* the worker does (`fetchRepoConfig`). `includeSecrets: false` leaves out
|
|
233
|
+
* secret rows, as `/api/config/resolved` does for a worker that cannot read
|
|
234
|
+
* them (`readsRepoSecrets`): that worker's env never gets them.
|
|
235
|
+
*/
|
|
236
|
+
export async function repoModelOverrideKeys(
|
|
237
|
+
vcsRepo: string | null | undefined,
|
|
238
|
+
{ includeSecrets = true }: { includeSecrets?: boolean } = {},
|
|
239
|
+
): Promise<Set<string>> {
|
|
240
|
+
if (!vcsRepo?.trim()) return new Set();
|
|
241
|
+
const repos = await getDbClient().query<{ id: string; url: string }>(
|
|
242
|
+
"SELECT id, url FROM swarm_repos WHERE name = ? ORDER BY name ASC",
|
|
243
|
+
[vcsRepoName(vcsRepo)],
|
|
244
|
+
);
|
|
245
|
+
const repo = repos.find((r) => vcsRepoUrlMatches(r.url, vcsRepo));
|
|
246
|
+
if (!repo) return new Set();
|
|
247
|
+
const rows = await getDbClient().query<{ key: string }>(
|
|
248
|
+
`SELECT key FROM swarm_config
|
|
249
|
+
WHERE scope = 'repo' AND scopeId = ? AND key IN ('MODEL_OVERRIDE', 'REASONING_EFFORT_OVERRIDE')
|
|
250
|
+
${includeSecrets ? "" : "AND isSecret = 0"}`,
|
|
251
|
+
[repo.id],
|
|
252
|
+
);
|
|
253
|
+
return new Set(rows.map((row) => row.key));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Why a task cannot ask for `profile`, or null when it can. PR 1 validity is
|
|
258
|
+
* the harness only: the profile must exist and, when the target agent is known
|
|
259
|
+
* and has a harness, run on that harness. `profile` is exclusive with `model`
|
|
260
|
+
* and `modelTier`, which pick the model another way.
|
|
261
|
+
*/
|
|
262
|
+
export async function taskProfileError(check: {
|
|
263
|
+
profile?: string | null;
|
|
264
|
+
model?: string | null;
|
|
265
|
+
modelTier?: string | null;
|
|
266
|
+
agentId?: string | null;
|
|
267
|
+
}): Promise<string | null> {
|
|
268
|
+
const name = check.profile?.trim();
|
|
269
|
+
if (!name) return null;
|
|
270
|
+
if (check.model?.trim() || check.modelTier?.trim()) {
|
|
271
|
+
return "Pass `profile`, or `model`/`modelTier`, not both.";
|
|
272
|
+
}
|
|
273
|
+
const profile = await getModelProfileByName(name);
|
|
274
|
+
if (!profile) {
|
|
275
|
+
return `Unknown model profile "${name}". List profiles with list-model-profiles.`;
|
|
276
|
+
}
|
|
277
|
+
if (!check.agentId) return null;
|
|
278
|
+
const agent = await getDbClient().get<{ harness: string | null }>(
|
|
279
|
+
"SELECT COALESCE(harness_provider, provider) AS harness FROM agents WHERE id = ?",
|
|
280
|
+
[check.agentId],
|
|
281
|
+
);
|
|
282
|
+
if (agent?.harness && agent.harness !== profile.harness) {
|
|
283
|
+
return `Model profile "${name}" runs on the ${profile.harness} harness; the target agent runs ${agent.harness}.`;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type OverrideRowState = { value: string; exportToEnv?: number } | null;
|
|
289
|
+
|
|
290
|
+
/** What a config row does to the worker env: its value, withheld (exportToEnv off), or absent. */
|
|
291
|
+
function effectiveOverride(row: OverrideRowState): string | null {
|
|
292
|
+
if (!row) return null;
|
|
293
|
+
return row.exportToEnv === 0 ? "\u0000withheld" : row.value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* An agent's default model profile (MODEL_PROFILE) beats its MODEL_OVERRIDE and
|
|
298
|
+
* REASONING_EFFORT_OVERRIDE at claim. An effective change to either override, by
|
|
299
|
+
* any path, drops the profile default, so a stale default never beats a fresh
|
|
300
|
+
* override. Effective means what the worker env sees: the value, and whether
|
|
301
|
+
* the row is exported (`exportToEnv`).
|
|
302
|
+
*/
|
|
303
|
+
export async function clearAgentModelProfileOnOverrideChange(
|
|
304
|
+
agentId: string,
|
|
305
|
+
key: string,
|
|
306
|
+
before: OverrideRowState,
|
|
307
|
+
after: OverrideRowState,
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
if (key !== "MODEL_OVERRIDE" && key !== "REASONING_EFFORT_OVERRIDE") return;
|
|
310
|
+
if (effectiveOverride(before) === effectiveOverride(after)) return;
|
|
311
|
+
await getDbClient().run(
|
|
312
|
+
"DELETE FROM swarm_config WHERE scope = 'agent' AND scopeId = ? AND key = ?",
|
|
313
|
+
[agentId, MODEL_PROFILE_CONFIG_KEY],
|
|
314
|
+
);
|
|
315
|
+
}
|
package/src/be/db/tasks/read.ts
CHANGED
|
@@ -92,6 +92,8 @@ export type AgentTaskRow = {
|
|
|
92
92
|
resolvedModel?: string | null;
|
|
93
93
|
modelSource?: string | null;
|
|
94
94
|
modelAlias?: string | null;
|
|
95
|
+
profile?: string | null;
|
|
96
|
+
resolvedProfile?: string | null;
|
|
95
97
|
scheduleId: string | null;
|
|
96
98
|
workflowRunId: string | null;
|
|
97
99
|
workflowRunStepId: string | null;
|
|
@@ -322,6 +324,8 @@ export function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
322
324
|
resolvedEffort: ReasoningEffortSchema.safeParse(row.resolvedEffort).success
|
|
323
325
|
? (row.resolvedEffort as ReasoningEffort)
|
|
324
326
|
: undefined,
|
|
327
|
+
profile: row.profile ?? undefined,
|
|
328
|
+
resolvedProfile: row.resolvedProfile ?? undefined,
|
|
325
329
|
scheduleId: row.scheduleId ?? undefined,
|
|
326
330
|
workflowRunId: row.workflowRunId ?? undefined,
|
|
327
331
|
workflowRunStepId: row.workflowRunStepId ?? undefined,
|
|
@@ -456,6 +460,9 @@ export async function hasNonTerminalResumeChild(parentId: string): Promise<boole
|
|
|
456
460
|
return row !== undefined && row !== null;
|
|
457
461
|
}
|
|
458
462
|
|
|
463
|
+
/** How far back the heartbeat repairs a superseded task that never got a resume. */
|
|
464
|
+
export const SUPERSEDED_RESUME_REPAIR_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
465
|
+
|
|
459
466
|
/**
|
|
460
467
|
* Superseded tasks that never got a resume child. Both supersede paths
|
|
461
468
|
* (heartbeat remediation and `POST /api/tasks/:id/supersede`) supersede
|
package/src/be/db.ts
CHANGED
|
@@ -193,6 +193,7 @@ import {
|
|
|
193
193
|
createContextVersion,
|
|
194
194
|
getLatestContextVersion,
|
|
195
195
|
} from "./db/context-versions";
|
|
196
|
+
import { clearAgentModelProfileOnOverrideChange } from "./db/model-profiles";
|
|
196
197
|
import { getDb, getDbClient } from "./db/runtime";
|
|
197
198
|
import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
|
|
198
199
|
import {
|
|
@@ -342,6 +343,7 @@ export {
|
|
|
342
343
|
type PendingChatRelay,
|
|
343
344
|
recordChatRelayError,
|
|
344
345
|
resetTasksNotified,
|
|
346
|
+
SUPERSEDED_RESUME_REPAIR_WINDOW_MS,
|
|
345
347
|
type TaskFilters,
|
|
346
348
|
} from "./db/tasks/read";
|
|
347
349
|
|
|
@@ -2857,9 +2859,9 @@ export async function createTaskExtended(
|
|
|
2857
2859
|
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
2858
2860
|
vcsInstallationId, vcsNodeId,
|
|
2859
2861
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
2860
|
-
mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, scheduleId,
|
|
2862
|
+
mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, profile, scheduleId,
|
|
2861
2863
|
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by, isHumanFree
|
|
2862
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
2864
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
2863
2865
|
[
|
|
2864
2866
|
id,
|
|
2865
2867
|
assetKey,
|
|
@@ -2901,6 +2903,7 @@ export async function createTaskExtended(
|
|
|
2901
2903
|
options?.model ?? null,
|
|
2902
2904
|
options?.modelTier ?? null,
|
|
2903
2905
|
options?.effort ?? null,
|
|
2906
|
+
options?.profile?.trim() || null,
|
|
2904
2907
|
options?.scheduleId ?? null,
|
|
2905
2908
|
options?.workflowRunId ?? null,
|
|
2906
2909
|
options?.workflowRunStepId ?? null,
|
|
@@ -6662,55 +6665,55 @@ export async function upsertSwarmConfig(data: {
|
|
|
6662
6665
|
const row = await getDbClient().transaction(async () => {
|
|
6663
6666
|
const existing =
|
|
6664
6667
|
scopeId === null
|
|
6665
|
-
? await getDbClient().get<
|
|
6666
|
-
"SELECT
|
|
6668
|
+
? await getDbClient().get<SwarmConfigRow>(
|
|
6669
|
+
"SELECT * FROM swarm_config WHERE scope = ? AND scopeId IS NULL AND key = ?",
|
|
6667
6670
|
[data.scope, data.key],
|
|
6668
6671
|
)
|
|
6669
|
-
: await getDbClient().get<
|
|
6670
|
-
"SELECT
|
|
6672
|
+
: await getDbClient().get<SwarmConfigRow>(
|
|
6673
|
+
"SELECT * FROM swarm_config WHERE scope = ? AND scopeId = ? AND key = ?",
|
|
6671
6674
|
[data.scope, scopeId, data.key],
|
|
6672
6675
|
);
|
|
6673
6676
|
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6677
|
+
// COALESCE keeps the stored flag when the caller did not send one.
|
|
6678
|
+
const written = existing
|
|
6679
|
+
? await getDbClient().get<SwarmConfigRow>(
|
|
6680
|
+
`UPDATE swarm_config SET value = ?, isSecret = ?, envPath = ?, description = ?, encrypted = ?, lastUpdatedAt = ?,
|
|
6681
|
+
exportToEnv = CASE WHEN ? THEN 0 ELSE COALESCE(?, exportToEnv) END
|
|
6682
|
+
WHERE id = ? RETURNING *`,
|
|
6683
|
+
[
|
|
6684
|
+
storedValue,
|
|
6685
|
+
isSecret,
|
|
6686
|
+
envPath,
|
|
6687
|
+
description,
|
|
6688
|
+
encryptedFlag,
|
|
6689
|
+
now,
|
|
6690
|
+
restricted ? 1 : 0,
|
|
6691
|
+
data.exportToEnv === undefined ? null : data.exportToEnv ? 1 : 0,
|
|
6692
|
+
existing.id,
|
|
6693
|
+
],
|
|
6694
|
+
)
|
|
6695
|
+
: await getDbClient().get<SwarmConfigRow>(
|
|
6696
|
+
`INSERT INTO swarm_config (id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt, encrypted, exportToEnv)
|
|
6697
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
6698
|
+
[
|
|
6699
|
+
crypto.randomUUID(),
|
|
6700
|
+
data.scope,
|
|
6701
|
+
scopeId,
|
|
6702
|
+
data.key,
|
|
6703
|
+
storedValue,
|
|
6704
|
+
isSecret,
|
|
6705
|
+
envPath,
|
|
6706
|
+
description,
|
|
6707
|
+
now,
|
|
6708
|
+
now,
|
|
6709
|
+
encryptedFlag,
|
|
6710
|
+
restricted ? 0 : (data.exportToEnv ?? defaultExportToEnv(data.key)) ? 1 : 0,
|
|
6711
|
+
],
|
|
6712
|
+
);
|
|
6713
|
+
if (data.scope === "agent" && scopeId) {
|
|
6714
|
+
await clearAgentModelProfileOnOverrideChange(scopeId, data.key, existing, written);
|
|
6692
6715
|
}
|
|
6693
|
-
|
|
6694
|
-
const id = crypto.randomUUID();
|
|
6695
|
-
const exportToEnv = restricted ? 0 : (data.exportToEnv ?? defaultExportToEnv(data.key)) ? 1 : 0;
|
|
6696
|
-
return await getDbClient().get<SwarmConfigRow>(
|
|
6697
|
-
`INSERT INTO swarm_config (id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt, encrypted, exportToEnv)
|
|
6698
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
6699
|
-
[
|
|
6700
|
-
id,
|
|
6701
|
-
data.scope,
|
|
6702
|
-
scopeId,
|
|
6703
|
-
data.key,
|
|
6704
|
-
storedValue,
|
|
6705
|
-
isSecret,
|
|
6706
|
-
envPath,
|
|
6707
|
-
description,
|
|
6708
|
-
now,
|
|
6709
|
-
now,
|
|
6710
|
-
encryptedFlag,
|
|
6711
|
-
exportToEnv,
|
|
6712
|
-
],
|
|
6713
|
-
);
|
|
6716
|
+
return written;
|
|
6714
6717
|
});
|
|
6715
6718
|
|
|
6716
6719
|
if (!row) throw new Error("Failed to upsert swarm config");
|
|
@@ -6759,10 +6762,13 @@ export async function emitBuiltInIntegrationConnectedOnce(
|
|
|
6759
6762
|
* predate hardening must remain removable through remediation paths.
|
|
6760
6763
|
*/
|
|
6761
6764
|
export async function deleteSwarmConfig(id: string): Promise<boolean> {
|
|
6762
|
-
const row = await getDbClient().get<
|
|
6763
|
-
"DELETE FROM swarm_config WHERE id = ? RETURNING
|
|
6765
|
+
const row = await getDbClient().get<SwarmConfigRow>(
|
|
6766
|
+
"DELETE FROM swarm_config WHERE id = ? RETURNING *",
|
|
6764
6767
|
[id],
|
|
6765
6768
|
);
|
|
6769
|
+
if (row?.scope === "agent" && row.scopeId) {
|
|
6770
|
+
await clearAgentModelProfileOnOverrideChange(row.scopeId, row.key, row, null);
|
|
6771
|
+
}
|
|
6766
6772
|
if (row) getDbClient().afterCommit(() => publishWorkerConfigNudge(row.scope, row.scopeId));
|
|
6767
6773
|
return row !== null;
|
|
6768
6774
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- The pi extension posted its per-LLM-call `context` usage snapshot as
|
|
2
|
+
-- eventType 'compaction', so every pi turn drew an AUTO COMPACTION divider and
|
|
3
|
+
-- bumped agent_tasks.compactionCount. Those rows never carry compactTrigger or
|
|
4
|
+
-- preCompactTokens (real compactions from every harness do). Relabel them as
|
|
5
|
+
-- 'progress' and recompute the counter for the pi tasks.
|
|
6
|
+
|
|
7
|
+
UPDATE task_context_snapshots
|
|
8
|
+
SET eventType = 'progress'
|
|
9
|
+
WHERE eventType = 'compaction'
|
|
10
|
+
AND compactTrigger IS NULL
|
|
11
|
+
AND preCompactTokens IS NULL
|
|
12
|
+
AND sessionId LIKE 'pi-%'
|
|
13
|
+
AND taskId IN (SELECT id FROM agent_tasks WHERE provider = 'pi');
|
|
14
|
+
|
|
15
|
+
UPDATE agent_tasks
|
|
16
|
+
SET compactionCount = (
|
|
17
|
+
SELECT COUNT(*)
|
|
18
|
+
FROM task_context_snapshots s
|
|
19
|
+
WHERE s.taskId = agent_tasks.id AND s.eventType = 'compaction'
|
|
20
|
+
)
|
|
21
|
+
WHERE provider = 'pi'
|
|
22
|
+
AND COALESCE(compactionCount, 0) > 0;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
-- Model profiles (runbooks/model-tiers.md § Model profiles): a named
|
|
2
|
+
-- (harness, model, effort) that Lead picks per task or that applies as an
|
|
3
|
+
-- agent's default. Claim stamps the profile on the task. The tier seeds
|
|
4
|
+
-- (`<harness>-<tier>`) are written at boot by src/be/seed-model-profiles.ts.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS model_profiles (
|
|
6
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
7
|
+
name TEXT NOT NULL UNIQUE,
|
|
8
|
+
harness TEXT NOT NULL,
|
|
9
|
+
model TEXT NOT NULL,
|
|
10
|
+
effort TEXT,
|
|
11
|
+
allowCustomModel INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
description TEXT,
|
|
13
|
+
-- `tier:<tier>` for a boot seed, `agent-config` for a profile this migration
|
|
14
|
+
-- made from an agent's MODEL_OVERRIDE, NULL for one an operator created.
|
|
15
|
+
seededFrom TEXT,
|
|
16
|
+
createdAt TEXT NOT NULL,
|
|
17
|
+
lastUpdatedAt TEXT NOT NULL,
|
|
18
|
+
created_by TEXT REFERENCES users(id),
|
|
19
|
+
updated_by TEXT REFERENCES users(id)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
-- `profile` is the creator's ask; `resolvedProfile` is the name claim applied
|
|
23
|
+
-- (the task's ask or the agent's default), mirroring model / resolvedModel.
|
|
24
|
+
ALTER TABLE agent_tasks ADD COLUMN profile TEXT;
|
|
25
|
+
ALTER TABLE agent_tasks ADD COLUMN resolvedProfile TEXT;
|
|
26
|
+
|
|
27
|
+
-- A profile name a task asks for, or an agent has as its default, must exist
|
|
28
|
+
-- when the row is written. Enforced here so the check and the write are one
|
|
29
|
+
-- statement: an app-level check before a separate insert races a concurrent
|
|
30
|
+
-- delete. The delete side is the matching single-statement guard in
|
|
31
|
+
-- deleteModelProfile (src/be/db/model-profiles.ts). It is not a trigger on
|
|
32
|
+
-- model_profiles because one reading agent_tasks would break every later
|
|
33
|
+
-- migration that rebuilds agent_tasks (DROP + RENAME re-parses that trigger).
|
|
34
|
+
CREATE TRIGGER IF NOT EXISTS model_profiles_task_ref_insert
|
|
35
|
+
BEFORE INSERT ON agent_tasks
|
|
36
|
+
WHEN NEW.profile IS NOT NULL
|
|
37
|
+
AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = NEW.profile)
|
|
38
|
+
BEGIN
|
|
39
|
+
SELECT RAISE(ABORT, 'model-profile-missing: the task''s model profile does not exist');
|
|
40
|
+
END;
|
|
41
|
+
|
|
42
|
+
CREATE TRIGGER IF NOT EXISTS model_profiles_task_ref_update
|
|
43
|
+
BEFORE UPDATE OF profile ON agent_tasks
|
|
44
|
+
WHEN NEW.profile IS NOT NULL
|
|
45
|
+
AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = NEW.profile)
|
|
46
|
+
BEGIN
|
|
47
|
+
SELECT RAISE(ABORT, 'model-profile-missing: the task''s model profile does not exist');
|
|
48
|
+
END;
|
|
49
|
+
|
|
50
|
+
CREATE TRIGGER IF NOT EXISTS model_profiles_default_ref_insert
|
|
51
|
+
BEFORE INSERT ON swarm_config
|
|
52
|
+
WHEN NEW.scope = 'agent' AND NEW.key = 'MODEL_PROFILE'
|
|
53
|
+
AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = trim(NEW.value))
|
|
54
|
+
BEGIN
|
|
55
|
+
SELECT RAISE(ABORT, 'model-profile-missing: the agent''s default model profile does not exist');
|
|
56
|
+
END;
|
|
57
|
+
|
|
58
|
+
CREATE TRIGGER IF NOT EXISTS model_profiles_default_ref_update
|
|
59
|
+
BEFORE UPDATE OF scope, key, value ON swarm_config
|
|
60
|
+
WHEN NEW.scope = 'agent' AND NEW.key = 'MODEL_PROFILE'
|
|
61
|
+
AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = trim(NEW.value))
|
|
62
|
+
BEGIN
|
|
63
|
+
SELECT RAISE(ABORT, 'model-profile-missing: the agent''s default model profile does not exist');
|
|
64
|
+
END;
|
|
65
|
+
|
|
66
|
+
-- Each agent's MODEL_OVERRIDE + REASONING_EFFORT_OVERRIDE pair becomes a
|
|
67
|
+
-- profile, deduped by (harness, model, effort), and that agent's default
|
|
68
|
+
-- (agent-scoped MODEL_PROFILE, never exported to the worker env). Claim then
|
|
69
|
+
-- supplies the same model and effort the worker read from its env before.
|
|
70
|
+
-- A row with exportToEnv = 0 never reached the worker, so it is not carried
|
|
71
|
+
-- over: a withheld model makes no default, a withheld effort no profile effort.
|
|
72
|
+
-- ACP keeps MODEL_OVERRIDE (no portable mapping); `latest:` aliases are not
|
|
73
|
+
-- valid agent defaults, so they are left alone too.
|
|
74
|
+
CREATE TEMP TABLE _model_profile_overrides AS
|
|
75
|
+
SELECT
|
|
76
|
+
a.id AS agentId,
|
|
77
|
+
COALESCE(a.harness_provider, a.provider) AS harness,
|
|
78
|
+
trim(m.value) AS model,
|
|
79
|
+
CASE
|
|
80
|
+
WHEN lower(trim(e.value)) IN ('off', 'low', 'medium', 'high', 'xhigh', 'max')
|
|
81
|
+
THEN lower(trim(e.value))
|
|
82
|
+
END AS effort
|
|
83
|
+
FROM agents a
|
|
84
|
+
JOIN swarm_config m
|
|
85
|
+
ON m.scope = 'agent' AND m.scopeId = a.id AND m.key = 'MODEL_OVERRIDE'
|
|
86
|
+
AND m.isSecret = 0 AND m.encrypted = 0 AND m.exportToEnv = 1
|
|
87
|
+
LEFT JOIN swarm_config e
|
|
88
|
+
ON e.scope = 'agent' AND e.scopeId = a.id AND e.key = 'REASONING_EFFORT_OVERRIDE'
|
|
89
|
+
AND e.isSecret = 0 AND e.encrypted = 0 AND e.exportToEnv = 1
|
|
90
|
+
WHERE COALESCE(a.harness_provider, a.provider) IS NOT NULL
|
|
91
|
+
AND COALESCE(a.harness_provider, a.provider) <> 'acp'
|
|
92
|
+
AND trim(m.value) <> ''
|
|
93
|
+
AND lower(trim(m.value)) NOT LIKE 'latest:%';
|
|
94
|
+
|
|
95
|
+
-- Name: `<harness>-<model>[-<effort>]`, without repeating a harness prefix the
|
|
96
|
+
-- model already carries, e.g. `codex-gpt-6.1-sol-xhigh`, `claude-opus-5-5-high`.
|
|
97
|
+
INSERT OR IGNORE INTO model_profiles
|
|
98
|
+
(name, harness, model, effort, allowCustomModel, description, seededFrom, createdAt, lastUpdatedAt)
|
|
99
|
+
SELECT
|
|
100
|
+
lower(replace(replace(
|
|
101
|
+
CASE WHEN lower(model) LIKE lower(harness) || '-%' THEN model ELSE harness || '-' || model END,
|
|
102
|
+
'/', '-'), ':', '-')) || COALESCE('-' || effort, ''),
|
|
103
|
+
harness,
|
|
104
|
+
model,
|
|
105
|
+
effort,
|
|
106
|
+
1,
|
|
107
|
+
'Created from an agent''s MODEL_OVERRIDE and REASONING_EFFORT_OVERRIDE.',
|
|
108
|
+
'agent-config',
|
|
109
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
110
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
111
|
+
FROM _model_profile_overrides
|
|
112
|
+
GROUP BY harness, model, effort;
|
|
113
|
+
|
|
114
|
+
INSERT OR IGNORE INTO swarm_config
|
|
115
|
+
(id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt, encrypted, exportToEnv)
|
|
116
|
+
SELECT
|
|
117
|
+
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
|
|
118
|
+
substr(lower(hex(randomblob(2))), 2) || '-' ||
|
|
119
|
+
substr('89ab', 1 + (abs(random()) % 4), 1) || substr(lower(hex(randomblob(2))), 2) || '-' ||
|
|
120
|
+
lower(hex(randomblob(6))),
|
|
121
|
+
'agent',
|
|
122
|
+
o.agentId,
|
|
123
|
+
'MODEL_PROFILE',
|
|
124
|
+
p.name,
|
|
125
|
+
0,
|
|
126
|
+
NULL,
|
|
127
|
+
'Default model profile, created from this agent''s MODEL_OVERRIDE.',
|
|
128
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
129
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
130
|
+
0,
|
|
131
|
+
0
|
|
132
|
+
FROM _model_profile_overrides o
|
|
133
|
+
JOIN model_profiles p
|
|
134
|
+
ON p.harness = o.harness AND p.model = o.model AND p.effort IS o.effort
|
|
135
|
+
AND p.seededFrom = 'agent-config';
|
|
136
|
+
|
|
137
|
+
DROP TABLE _model_profile_overrides;
|