@desplega.ai/agent-swarm 1.166.0 → 1.167.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-zpvy6321.js +34 -0
- package/dist/{actions-qz3m692w.js → actions-fq2zrwtf.js} +25 -25
- package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-5gbk7501.js} +4 -4
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-43vrdmv8.js → app-nctetn1e.js} +10 -9
- package/dist/{assistant-ertffmka.js → assistant-0aq8gjvj.js} +26 -26
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-998jtpdg.js → boot-reembed-1ze5853n.js} +9 -8
- package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-fmp5ww69.js} +9 -8
- package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-cbp8xd4e.js} +10 -9
- package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-22tx8a53.js} +12 -11
- package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-0jhgeh1d.js} +24 -22
- package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
- package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
- package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-wh77rh83.js} +22 -22
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-dw2q9f7g.js → cli-1yrcfa5s.js} +4 -4
- package/dist/{cli-jvj6a0cv.js → cli-577bxkf6.js} +27 -17
- package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
- package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
- package/dist/{cli-cpfmqz3n.js → cli-6qxmjpem.js} +5 -5
- package/dist/{cli-aqcey741.js → cli-6vfz44sr.js} +1 -1
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-gt8azyet.js → cli-c2tpw9ck.js} +1 -1
- package/dist/{cli-ecknp7p1.js → cli-c8r99akr.js} +10 -10
- package/dist/{cli-3qn4r334.js → cli-cexctgn5.js} +20 -14
- package/dist/{cli-vzc73wet.js → cli-cf2vgn9d.js} +87 -69
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/{cli-j2jbr8af.js → cli-kw0jcfje.js} +1 -1
- package/dist/{cli-8t229cmr.js → cli-m7vmeepg.js} +30 -17
- package/dist/{cli-yshaazs1.js → cli-mkdsy1nq.js} +62 -20
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-0qjcsxgz.js → cli-mxry91sa.js} +30 -9
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-rggdq8wq.js → cli-p8rt8spp.js} +3 -3
- package/dist/{cli-v2a8x45d.js → cli-pkj1w46r.js} +10 -5
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-66zh26sp.js → cli-qzmzwegq.js} +57 -17
- package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-0nhdwxd5.js → cli-tx2zvn23.js} +3 -3
- package/dist/{cli-mwegs4af.js → cli-ty4bzkqr.js} +3 -3
- package/dist/{cli-ysnyhws9.js → cli-v1pwxreq.js} +54 -51
- package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/{cli-df56tqdw.js → cli-wydz3feg.js} +108 -57
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-x7kpr72a.js → cli-y0kjpfm9.js} +2 -2
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-fn697bnt.js → cli-ykt8f7va.js} +21 -8
- package/dist/{cli-4r1sjfc4.js → cli-ymmc1zxg.js} +8 -8
- package/dist/{cli-8kgvmkak.js → cli-ypgba1a8.js} +1 -1
- package/dist/{cli-rsy933yz.js → cli-z1dqhc9y.js} +30 -19
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +26 -26
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-cgesnjkr.js} +18 -18
- package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
- package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-8kd7ddhr.js} +18 -18
- package/dist/{commands-mhxam6z2.js → commands-h6zaaxym.js} +9 -8
- package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
- package/dist/{db-sjks418k.js → db-qg3myn53.js} +9 -8
- package/dist/{dispatcher-1es759rn.js → dispatcher-7vnwj9qw.js} +24 -24
- package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-tz6hfp5r.js} +5 -5
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-jdt2dz2s.js} +18 -8
- package/dist/{handlers-1gkbhktr.js → handlers-2qtkpncr.js} +27 -27
- package/dist/{hook-ds2zxq9v.js → hook-51yfcgsv.js} +42 -31
- package/dist/{hook-vxa3tcsy.js → hook-qnbdgwvd.js} +14 -14
- package/dist/{http-3h21z9md.js → http-jsmtag08.js} +320 -98
- package/dist/{index-9b65j1m5.js → index-2t9kesn1.js} +9 -8
- package/dist/{index-dkfxpkmm.js → index-bt8wkv1w.js} +25 -25
- package/dist/{index-f8xnqrye.js → index-cx1sb9bq.js} +33 -24
- package/dist/{index-bn6cy8ps.js → index-ekywyv2v.js} +24 -24
- package/dist/{index-n99fycq7.js → index-t4g06w93.js} +24 -24
- package/dist/{keepalive-3xspa9jw.js → keepalive-4aezx4bj.js} +11 -10
- package/dist/{lead-6qphc124.js → lead-r44bkpm5.js} +47 -47
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-0teem1bb.js → maintenance-grz5jrzh.js} +9 -8
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
- package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-wm4ctjfj.js} +10 -9
- package/dist/{onboard-54qmnqxe.js → onboard-v60zgqf0.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-ycqzp2a1.js → otel-impl-ghh4dyf8.js} +1 -1
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
- package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-mjrqvnvw.js} +11 -10
- package/dist/{rbac-roles-j78wdksm.js → rbac-roles-axv4d74x.js} +10 -9
- package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-fd5tskj9.js} +9 -8
- package/dist/{render-v2-pxjs49fx.js → render-v2-k50j33qd.js} +10 -9
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-ymej0xtx.js → setup-4syh0pd2.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-3nh4ymzb.js → work-objects-jcdbg8rr.js} +27 -27
- package/dist/{worker-qa9fkz5s.js → worker-x8j8c9sx.js} +47 -47
- package/openapi.json +1 -1
- package/package.json +8 -8
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +13 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/providers/acp-targets.ts +9 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +26 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/dsh-adapter.ts +10 -1
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/server.ts +63 -3
- package/src/tests/acp-adapter.test.ts +152 -12
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/dsh-adapter.test.ts +23 -1
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/request-human-input.ts +7 -2
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-yzfxn8vj.js +0 -23
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
- package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
|
@@ -3,7 +3,10 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { normalizeGrokModel } from "@desplega/model-catalog";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getOpenRouterAttributionHeaders,
|
|
8
|
+
getOpenRouterBaseUrl,
|
|
9
|
+
} from "../utils/openrouter-base-url";
|
|
7
10
|
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
8
11
|
import { ACPAdapter } from "./acp-adapter";
|
|
9
12
|
import { grokTargetProfile, isGrokOpenRouterModel } from "./acp-targets";
|
|
@@ -64,6 +67,8 @@ export interface GrokCustomModel {
|
|
|
64
67
|
baseUrl: string;
|
|
65
68
|
/** Env var holding the endpoint's API key. */
|
|
66
69
|
envKey: string;
|
|
70
|
+
/** Sent verbatim on every request to the endpoint (`extra_headers`). */
|
|
71
|
+
headers?: Record<string, string>;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
/** `openrouter/<vendor>/<id>` -> a `[model."openrouter/<vendor>/<id>"]` block on OpenRouter. */
|
|
@@ -73,14 +78,24 @@ export function grokOpenRouterModel(
|
|
|
73
78
|
): GrokCustomModel | null {
|
|
74
79
|
const id = model.trim();
|
|
75
80
|
if (!isGrokOpenRouterModel(id)) return null;
|
|
81
|
+
const baseUrl = getOpenRouterBaseUrl(env);
|
|
76
82
|
return {
|
|
77
83
|
id,
|
|
78
84
|
model: id.slice("openrouter/".length),
|
|
79
|
-
baseUrl
|
|
85
|
+
baseUrl,
|
|
80
86
|
envKey: "OPENROUTER_API_KEY",
|
|
87
|
+
headers: getOpenRouterAttributionHeaders(baseUrl, env),
|
|
81
88
|
};
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
/** `key = { "k" = "v", ... }`, or nothing for an empty map. JSON strings are valid TOML basic strings. */
|
|
92
|
+
function tomlInlineTable(key: string, entries: Record<string, string> | undefined): string[] {
|
|
93
|
+
const pairs = Object.entries(entries ?? {});
|
|
94
|
+
if (pairs.length === 0) return [];
|
|
95
|
+
const body = pairs.map(([k, v]) => `${JSON.stringify(k)} = ${JSON.stringify(v)}`).join(", ");
|
|
96
|
+
return [`${key} = { ${body} }`];
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
/** `$GROK_HOME/config.toml` for one swarm session. */
|
|
85
100
|
export function buildGrokConfigToml(
|
|
86
101
|
disabledPlugins: readonly string[],
|
|
@@ -95,6 +110,7 @@ export function buildGrokConfigToml(
|
|
|
95
110
|
`base_url = ${JSON.stringify(customModel.baseUrl)}`,
|
|
96
111
|
`env_key = ${JSON.stringify(customModel.envKey)}`,
|
|
97
112
|
`api_backend = "chat_completions"`,
|
|
113
|
+
...tomlInlineTable("extra_headers", customModel.headers),
|
|
98
114
|
]
|
|
99
115
|
: [];
|
|
100
116
|
return [
|
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
import { validateOpencodeCredentials } from "../utils/credentials";
|
|
21
21
|
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
22
22
|
import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_OPENROUTER_BASE_URL,
|
|
25
|
+
getOpenRouterAttributionHeaders,
|
|
26
|
+
getOpenRouterBaseUrl,
|
|
27
|
+
} from "../utils/openrouter-base-url";
|
|
24
28
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
25
29
|
import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
|
|
26
30
|
import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
|
|
@@ -177,22 +181,34 @@ function formatUnknownError(err: unknown): string {
|
|
|
177
181
|
* Route OpenRouter traffic through the configured gateway (see
|
|
178
182
|
* src/utils/openrouter-base-url.ts). opencode's bundled openrouter provider
|
|
179
183
|
* honors `provider.openrouter.options.baseURL`. Mutates `opencodeConfig` in
|
|
180
|
-
* place
|
|
181
|
-
* openrouter.ai behavior is preserved. Exported for tests.
|
|
184
|
+
* place, and attribute direct OpenRouter requests to Agent Swarm.
|
|
182
185
|
*/
|
|
183
186
|
export function applyOpenRouterBaseUrlOverride(
|
|
184
187
|
opencodeConfig: Config,
|
|
185
188
|
env: Record<string, string | undefined> = process.env,
|
|
186
189
|
): void {
|
|
187
190
|
const openRouterBaseUrl = getOpenRouterBaseUrl(env as NodeJS.ProcessEnv);
|
|
188
|
-
|
|
191
|
+
const headers = getOpenRouterAttributionHeaders(openRouterBaseUrl, env);
|
|
192
|
+
const overrideBaseUrl = openRouterBaseUrl !== DEFAULT_OPENROUTER_BASE_URL;
|
|
193
|
+
if (!overrideBaseUrl && Object.keys(headers).length === 0) return;
|
|
189
194
|
opencodeConfig.provider = {
|
|
190
195
|
...opencodeConfig.provider,
|
|
191
196
|
openrouter: {
|
|
192
197
|
...opencodeConfig.provider?.openrouter,
|
|
193
198
|
options: {
|
|
194
199
|
...opencodeConfig.provider?.openrouter?.options,
|
|
195
|
-
baseURL: openRouterBaseUrl,
|
|
200
|
+
...(overrideBaseUrl ? { baseURL: openRouterBaseUrl } : {}),
|
|
201
|
+
...(Object.keys(headers).length > 0
|
|
202
|
+
? {
|
|
203
|
+
headers: {
|
|
204
|
+
...(opencodeConfig.provider?.openrouter?.options?.headers as
|
|
205
|
+
| Record<string, string>
|
|
206
|
+
| undefined),
|
|
207
|
+
...headers,
|
|
208
|
+
"X-Title": "Agent Swarm",
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
: {}),
|
|
196
212
|
},
|
|
197
213
|
},
|
|
198
214
|
};
|
|
@@ -11,6 +11,7 @@ import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/
|
|
|
11
11
|
import { checkToolLoop, clearToolHistory } from "../hooks/tool-loop-detection";
|
|
12
12
|
import { summarizeSession as runSummarize } from "../utils/internal-ai";
|
|
13
13
|
import { getMemoryRaterNames } from "../utils/memory-raters";
|
|
14
|
+
import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
|
|
14
15
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
15
16
|
|
|
16
17
|
export interface SwarmHooksConfig {
|
|
@@ -421,6 +422,24 @@ export async function summarizeSessionForPi(
|
|
|
421
422
|
}
|
|
422
423
|
}
|
|
423
424
|
|
|
425
|
+
/** `customType` of the advisory messages this extension sends to the model. */
|
|
426
|
+
export const SWARM_NUDGE_CUSTOM_TYPE = "swarm-nudge";
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Deliver an advisory nudge to the model. pi runs extensions in-process and
|
|
430
|
+
* never feeds their stdout to the model, so a console.log here only reaches
|
|
431
|
+
* the worker log. `sendMessage` steers the text in after the current tool
|
|
432
|
+
* batch while the agent is streaming, and otherwise appends it to the session
|
|
433
|
+
* so the next prompt carries it.
|
|
434
|
+
*/
|
|
435
|
+
function sendNudge(pi: Parameters<ExtensionFactory>[0], text: string): void {
|
|
436
|
+
pi.sendMessage({
|
|
437
|
+
customType: SWARM_NUDGE_CUSTOM_TYPE,
|
|
438
|
+
content: [{ type: "text", text }],
|
|
439
|
+
display: false,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
424
443
|
/**
|
|
425
444
|
* Create the swarm hooks extension factory for pi-mono.
|
|
426
445
|
*
|
|
@@ -431,6 +450,13 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
|
|
|
431
450
|
return (pi) => {
|
|
432
451
|
let lastContextPostTime = 0;
|
|
433
452
|
|
|
453
|
+
pi.on("before_provider_headers", (event, ctx) => {
|
|
454
|
+
Object.assign(
|
|
455
|
+
event.headers,
|
|
456
|
+
getOpenRouterAttributionHeaders(ctx.model?.baseUrl ?? "", config.env),
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
434
460
|
// === session_start → SessionStart ===
|
|
435
461
|
pi.on("session_start", async (_event, _ctx) => {
|
|
436
462
|
// Installed MCP servers: pi's MCP extension connects them in the
|
|
@@ -458,7 +484,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
|
|
|
458
484
|
if (config.isLead) {
|
|
459
485
|
const ctx = await fetchConcurrentContext(config);
|
|
460
486
|
if (ctx) {
|
|
461
|
-
|
|
487
|
+
sendNudge(pi, ctx);
|
|
462
488
|
}
|
|
463
489
|
}
|
|
464
490
|
});
|
|
@@ -498,7 +524,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
|
|
|
498
524
|
}
|
|
499
525
|
|
|
500
526
|
if (loopResult.severity === "warning" && loopResult.reason) {
|
|
501
|
-
|
|
527
|
+
sendNudge(pi, `Warning: ${loopResult.reason}`);
|
|
502
528
|
}
|
|
503
529
|
}
|
|
504
530
|
|
|
@@ -586,7 +612,8 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
|
|
|
586
612
|
|
|
587
613
|
// Reminders
|
|
588
614
|
if (config.isLead && event.toolName?.endsWith("send-task")) {
|
|
589
|
-
|
|
615
|
+
sendNudge(
|
|
616
|
+
pi,
|
|
590
617
|
"Task sent successfully. Monitor progress using the get-task-details tool periodically.",
|
|
591
618
|
);
|
|
592
619
|
}
|
|
@@ -594,29 +621,30 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
|
|
|
594
621
|
return undefined;
|
|
595
622
|
});
|
|
596
623
|
|
|
597
|
-
// ===
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
pi.on("
|
|
601
|
-
if (!config.taskId) return
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
reminder.push(`Current Progress: ${taskDetails.progress}`);
|
|
613
|
-
}
|
|
614
|
-
reminder.push("=== Continue working on this task after compaction ===");
|
|
615
|
-
console.log(reminder.join("\n"));
|
|
624
|
+
// === session_compact → PreCompact ===
|
|
625
|
+
// Re-anchor the model on its task once compaction has summarized the
|
|
626
|
+
// history. `context` fires before every LLM call, so it cannot carry this.
|
|
627
|
+
pi.on("session_compact", async () => {
|
|
628
|
+
if (!config.taskId) return;
|
|
629
|
+
|
|
630
|
+
const taskDetails = await fetchTaskDetails(config);
|
|
631
|
+
if (taskDetails) {
|
|
632
|
+
const reminder = [
|
|
633
|
+
"=== GOAL REMINDER (injected before context compaction) ===",
|
|
634
|
+
`Task ID: ${taskDetails.id}`,
|
|
635
|
+
`Task: ${taskDetails.task}`,
|
|
636
|
+
];
|
|
637
|
+
if (taskDetails.progress) {
|
|
638
|
+
reminder.push(`Current Progress: ${taskDetails.progress}`);
|
|
616
639
|
}
|
|
617
|
-
|
|
618
|
-
|
|
640
|
+
reminder.push("=== Continue working on this task after compaction ===");
|
|
641
|
+
sendNudge(pi, reminder.join("\n"));
|
|
619
642
|
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// === context (before every LLM call) ===
|
|
646
|
+
pi.on("context", async (_event, ctx) => {
|
|
647
|
+
if (!config.taskId) return undefined;
|
|
620
648
|
|
|
621
649
|
// Report context usage as a compaction event
|
|
622
650
|
const usage = ctx.getContextUsage?.();
|
|
@@ -644,7 +672,10 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
|
|
|
644
672
|
const { cancelled, reason } = await isTaskCancelled(config);
|
|
645
673
|
if (cancelled) {
|
|
646
674
|
const cancelReason = reason || "Task cancelled by lead or creator";
|
|
647
|
-
|
|
675
|
+
// "handled" skips the turn; the nudge lands in the session for the
|
|
676
|
+
// next one instead of paying for a turn on a cancelled task.
|
|
677
|
+
sendNudge(
|
|
678
|
+
pi,
|
|
648
679
|
`🛑 TASK CANCELLED: ${cancelReason}. Stop working and use store-progress to acknowledge.`,
|
|
649
680
|
);
|
|
650
681
|
return { action: "handled" as const };
|
|
@@ -178,6 +178,8 @@ and heartbeat-classified roots are omitted, and the metrics remain separate.
|
|
|
178
178
|
- **Codex usage:** the app-server reports cumulative token counters, including cache writes.
|
|
179
179
|
The adapter calculates each turn's delta before adding it to the session total.
|
|
180
180
|
This prevents repeated billing of earlier turns when the adapter processes queued input.
|
|
181
|
+
Context occupancy is separate: it comes from the per-request `tokenUsage.last`
|
|
182
|
+
snapshot (and `modelContextWindow` when present), never from the turn delta.
|
|
181
183
|
- **Claude breakdown validity is all-or-nothing:** the claude adapter drops the
|
|
182
184
|
entire `modelUsage` breakdown when any entry carries a missing, non-finite,
|
|
183
185
|
or negative token counter — zero-filling would let the server price a
|
package/src/server.ts
CHANGED
|
@@ -196,6 +196,7 @@ import {
|
|
|
196
196
|
registerTriggerWorkflowTool,
|
|
197
197
|
registerUpdateWorkflowTool,
|
|
198
198
|
} from "./tools/workflows";
|
|
199
|
+
import { parseEnabledTools } from "./utils/enabled-tools";
|
|
199
200
|
import { resolveScriptsOnlyMode } from "./utils/scripts-only-mode";
|
|
200
201
|
|
|
201
202
|
// Every known capability, including the ones disabled by default. Exported for
|
|
@@ -293,18 +294,75 @@ export function isScriptsOnlyMcp(): boolean {
|
|
|
293
294
|
return resolveScriptsOnlyMode({ env: process.env.SCRIPTS_ONLY_MCP });
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
const loggedToolAllowlistWarnings = new Set<string>();
|
|
298
|
+
|
|
299
|
+
function warnToolAllowlistOnce(message: string): void {
|
|
300
|
+
if (loggedToolAllowlistWarnings.has(message)) return;
|
|
301
|
+
loggedToolAllowlistWarnings.add(message);
|
|
302
|
+
console.warn(message);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* SWARM_ENABLED_TOOLS: an optional explicit allowlist for the externally
|
|
307
|
+
* exposed MCP surface. Returns undefined (capability-driven surface) when the
|
|
308
|
+
* value is unset, blank, or invalid. Never logs the raw value.
|
|
309
|
+
*/
|
|
310
|
+
function resolveToolAllowlist(value: string | undefined): string[] | undefined {
|
|
311
|
+
if (value === undefined) return undefined;
|
|
312
|
+
try {
|
|
313
|
+
const tools = parseEnabledTools(value);
|
|
314
|
+
if (!tools) {
|
|
315
|
+
warnToolAllowlistOnce("[MCP] SWARM_ENABLED_TOOLS is set but lists no tools; ignoring it");
|
|
316
|
+
}
|
|
317
|
+
return tools;
|
|
318
|
+
} catch {
|
|
319
|
+
warnToolAllowlistOnce("[MCP] Invalid SWARM_ENABLED_TOOLS; ignoring it");
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Drop every registered tool the allowlist does not name; warn on unknown names. */
|
|
325
|
+
function applyToolAllowlist(server: McpServer, allowlist: readonly string[]): void {
|
|
326
|
+
const tools = (server as unknown as { _registeredTools: Record<string, { remove(): void }> })
|
|
327
|
+
._registeredTools;
|
|
328
|
+
const allowed = new Set(allowlist);
|
|
329
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
330
|
+
if (!allowed.has(name)) tool.remove();
|
|
331
|
+
}
|
|
332
|
+
const unknown = allowlist.filter((name) => !Object.hasOwn(tools, name));
|
|
333
|
+
if (unknown.length > 0) {
|
|
334
|
+
warnToolAllowlistOnce(
|
|
335
|
+
`[MCP] SWARM_ENABLED_TOOLS names unknown tools, ignoring them: ${unknown.join(", ")}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
296
340
|
export async function createServer(
|
|
297
|
-
opts: {
|
|
341
|
+
opts: {
|
|
342
|
+
scriptsOnly?: boolean;
|
|
343
|
+
fullSurface?: boolean;
|
|
344
|
+
preloadedTools?: readonly string[];
|
|
345
|
+
/** Raw SWARM_ENABLED_TOOLS value for this session; defaults to the env. */
|
|
346
|
+
enabledTools?: string;
|
|
347
|
+
} = {},
|
|
298
348
|
) {
|
|
299
349
|
// Reload env
|
|
300
350
|
await loadGlobalConfigsIntoEnv(true);
|
|
301
351
|
|
|
352
|
+
// The tool allowlist replaces CAPABILITIES and SCRIPTS_ONLY_MCP when set.
|
|
353
|
+
// Full-surface consumers (the scripts SDK bridge) ignore it.
|
|
354
|
+
const toolAllowlist =
|
|
355
|
+
opts.fullSurface === true
|
|
356
|
+
? undefined
|
|
357
|
+
: resolveToolAllowlist(opts.enabledTools ?? process.env.SWARM_ENABLED_TOOLS);
|
|
358
|
+
|
|
302
359
|
// Capability flags shape the externally exposed MCP tool list only. Internal
|
|
303
360
|
// full-surface consumers (the scripts SDK bridge, drift-check tests) pass
|
|
304
361
|
// fullSurface to register every tool group regardless of CAPABILITIES.
|
|
362
|
+
// An allowlist also registers every group, then prunes to the listed names.
|
|
305
363
|
// This shadows the module-level hasCapability for the registrations below.
|
|
306
364
|
const hasCapability = (cap: CAPABILITIES_T): boolean =>
|
|
307
|
-
opts.fullSurface === true || getCapabilities().has(cap);
|
|
365
|
+
opts.fullSurface === true || toolAllowlist !== undefined || getCapabilities().has(cap);
|
|
308
366
|
|
|
309
367
|
// Initialize database with WAL mode
|
|
310
368
|
// Uses DATABASE_PATH env var for Docker volume compatibility (WAL needs .sqlite, .sqlite-wal, .sqlite-shm on same filesystem)
|
|
@@ -355,7 +413,7 @@ export async function createServer(
|
|
|
355
413
|
// Scripts-only surface (experimental code-mode): register just the script
|
|
356
414
|
// catalog tools and stop. script-connections / script-apis stay out — they
|
|
357
415
|
// are lead-only security admin and excluded from the scripts SDK too.
|
|
358
|
-
if (opts.scriptsOnly ?? isScriptsOnlyMcp()) {
|
|
416
|
+
if (toolAllowlist === undefined && (opts.scriptsOnly ?? isScriptsOnlyMcp())) {
|
|
359
417
|
registerScriptSearchTool(server);
|
|
360
418
|
registerScriptRunTool(server);
|
|
361
419
|
registerScriptUpsertTool(server);
|
|
@@ -630,5 +688,7 @@ export async function createServer(
|
|
|
630
688
|
registerUpdateServiceStatusTool(server);
|
|
631
689
|
}
|
|
632
690
|
|
|
691
|
+
if (toolAllowlist) applyToolAllowlist(server, toolAllowlist);
|
|
692
|
+
|
|
633
693
|
return server;
|
|
634
694
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { createServer, type IncomingMessage, type
|
|
3
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import type { Usage } from "@agentclientprotocol/sdk";
|
|
@@ -64,20 +64,29 @@ function baseConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSes
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Start a minimal swarm API stub that handles POST /api/sessions/tokens and
|
|
67
|
-
* DELETE /api/sessions/tokens/:id. Returns
|
|
68
|
-
*
|
|
67
|
+
* DELETE /api/sessions/tokens/:id. Returns its base URL and `close()`, which
|
|
68
|
+
* the caller must await. The adapter revokes a minted token fire-and-forget,
|
|
69
|
+
* so closing before that DELETE lands fails it and leaks a revoke warning
|
|
70
|
+
* into a later test.
|
|
69
71
|
*/
|
|
70
72
|
async function startTokenStubServer(
|
|
71
73
|
tokenId: string,
|
|
72
74
|
plaintext: string,
|
|
73
|
-
): Promise<{
|
|
75
|
+
): Promise<{ apiUrl: string; close: () => Promise<void> }> {
|
|
76
|
+
let minted = false;
|
|
77
|
+
let resolveRevoked!: () => void;
|
|
78
|
+
const revoked = new Promise<void>((resolve) => {
|
|
79
|
+
resolveRevoked = resolve;
|
|
80
|
+
});
|
|
74
81
|
const stub = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
75
82
|
if (req.method === "POST" && req.url === "/api/sessions/tokens") {
|
|
83
|
+
minted = true;
|
|
76
84
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
77
85
|
res.end(JSON.stringify({ tokenId, plaintext }));
|
|
78
86
|
} else if (req.method === "DELETE" && req.url?.startsWith("/api/sessions/tokens/")) {
|
|
79
87
|
res.writeHead(204);
|
|
80
88
|
res.end();
|
|
89
|
+
resolveRevoked();
|
|
81
90
|
} else {
|
|
82
91
|
res.writeHead(404);
|
|
83
92
|
res.end();
|
|
@@ -85,7 +94,13 @@ async function startTokenStubServer(
|
|
|
85
94
|
});
|
|
86
95
|
await new Promise<void>((resolve) => stub.listen(0, "127.0.0.1", resolve));
|
|
87
96
|
const addr = stub.address() as import("net").AddressInfo;
|
|
88
|
-
|
|
97
|
+
const close = async () => {
|
|
98
|
+
// A minted token is always revoked; wait for it, bounded so a failed test
|
|
99
|
+
// still tears down.
|
|
100
|
+
if (minted) await Promise.race([revoked, Bun.sleep(5_000)]);
|
|
101
|
+
await new Promise<void>((resolve) => stub.close(() => resolve()));
|
|
102
|
+
};
|
|
103
|
+
return { apiUrl: `http://127.0.0.1:${addr.port}`, close };
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
describe("ACPAdapter", () => {
|
|
@@ -305,7 +320,7 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
305
320
|
`,
|
|
306
321
|
);
|
|
307
322
|
|
|
308
|
-
const {
|
|
323
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
309
324
|
"stub-token-id",
|
|
310
325
|
"aseph_stubtokenfortest1234567890",
|
|
311
326
|
);
|
|
@@ -503,7 +518,7 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
503
518
|
closeDb();
|
|
504
519
|
}
|
|
505
520
|
} finally {
|
|
506
|
-
await
|
|
521
|
+
await closeTokenStub();
|
|
507
522
|
}
|
|
508
523
|
});
|
|
509
524
|
|
|
@@ -537,10 +552,132 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
537
552
|
),
|
|
538
553
|
).toMatchObject({
|
|
539
554
|
OPENAI_API_KEY: "example-test-key",
|
|
540
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
555
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
556
|
+
theme: "dark",
|
|
557
|
+
model: "opencode/model",
|
|
558
|
+
provider: {
|
|
559
|
+
openrouter: {
|
|
560
|
+
options: {
|
|
561
|
+
headers: {
|
|
562
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
563
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
564
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
565
|
+
"X-Title": "Agent Swarm",
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
}),
|
|
541
571
|
});
|
|
542
572
|
});
|
|
543
573
|
|
|
574
|
+
test("OpenCode preset merges the OpenRouter gateway with model and provider config", () => {
|
|
575
|
+
const config = baseConfig({
|
|
576
|
+
model: " openrouter/openai/gpt-4o-mini ",
|
|
577
|
+
env: {
|
|
578
|
+
ACP_TARGET: "opencode",
|
|
579
|
+
OPENROUTER_BASE_URL: " http://127.0.0.1:8080/api/v1/ ",
|
|
580
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
581
|
+
theme: "dark",
|
|
582
|
+
provider: { openrouter: { options: { apiKey: "dummy" } } },
|
|
583
|
+
}),
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
const env = resolveAcpTarget(config).env(config);
|
|
587
|
+
expect(JSON.parse(env.OPENCODE_CONFIG_CONTENT!)).toEqual({
|
|
588
|
+
theme: "dark",
|
|
589
|
+
model: "openrouter/openai/gpt-4o-mini",
|
|
590
|
+
provider: {
|
|
591
|
+
openrouter: { options: { apiKey: "dummy", baseURL: "http://127.0.0.1:8080/api/v1" } },
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("OpenCode preset omits gateway override for unset, blank, or default URL", () => {
|
|
597
|
+
for (const url of [undefined, "", "https://openrouter.ai/api/v1/"]) {
|
|
598
|
+
const config = baseConfig({
|
|
599
|
+
env: { ACP_TARGET: "opencode", OPENROUTER_BASE_URL: url },
|
|
600
|
+
});
|
|
601
|
+
const env = resolveAcpTarget(config).env(config);
|
|
602
|
+
const content = JSON.parse(env.OPENCODE_CONFIG_CONTENT!);
|
|
603
|
+
expect(content).toEqual({
|
|
604
|
+
model: config.model,
|
|
605
|
+
provider: {
|
|
606
|
+
openrouter: {
|
|
607
|
+
options: {
|
|
608
|
+
headers: {
|
|
609
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
610
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
611
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
612
|
+
"X-Title": "Agent Swarm",
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
expect(content.provider.openrouter.options.baseURL).toBeUndefined();
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("OpenCode preset adds attribution headers to OPENCODE_CONFIG_CONTENT for an OpenRouter host", () => {
|
|
623
|
+
const config = baseConfig({
|
|
624
|
+
model: "",
|
|
625
|
+
env: {
|
|
626
|
+
ACP_TARGET: "opencode",
|
|
627
|
+
OPENROUTER_BASE_URL: "https://openrouter.ai/api/v1",
|
|
628
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
629
|
+
theme: "dark",
|
|
630
|
+
provider: { openrouter: { options: { headers: { "X-Custom": "preserved" } } } },
|
|
631
|
+
}),
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
const env = resolveAcpTarget(config).env(config);
|
|
635
|
+
expect(JSON.parse(env.OPENCODE_CONFIG_CONTENT!)).toEqual({
|
|
636
|
+
theme: "dark",
|
|
637
|
+
provider: {
|
|
638
|
+
openrouter: {
|
|
639
|
+
options: {
|
|
640
|
+
headers: {
|
|
641
|
+
"X-Custom": "preserved",
|
|
642
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
643
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
644
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
645
|
+
"X-Title": "Agent Swarm",
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("OpenCode preset leaves OPENCODE_CONFIG_CONTENT unchanged when attribution is disabled", () => {
|
|
654
|
+
const content = JSON.stringify({
|
|
655
|
+
theme: "dark",
|
|
656
|
+
provider: { openrouter: { options: { headers: { "X-Custom": "preserved" } } } },
|
|
657
|
+
});
|
|
658
|
+
const config = baseConfig({
|
|
659
|
+
model: "",
|
|
660
|
+
env: {
|
|
661
|
+
ACP_TARGET: "opencode",
|
|
662
|
+
OPENROUTER_BASE_URL: "https://openrouter.ai/api/v1",
|
|
663
|
+
OPENROUTER_APP_ATTRIBUTION: "false",
|
|
664
|
+
OPENCODE_CONFIG_CONTENT: content,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
expect(resolveAcpTarget(config).env(config).OPENCODE_CONFIG_CONTENT).toBe(content);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("OpenCode preset injects gateway even without a model fallback", () => {
|
|
671
|
+
const config = baseConfig({
|
|
672
|
+
model: "",
|
|
673
|
+
env: { ACP_TARGET: "opencode", OPENROUTER_BASE_URL: "http://127.0.0.1:8080/api/v1" },
|
|
674
|
+
});
|
|
675
|
+
const env = resolveAcpTarget(config).env(config);
|
|
676
|
+
expect(JSON.parse(env.OPENCODE_CONFIG_CONTENT!).provider.openrouter.options.baseURL).toBe(
|
|
677
|
+
"http://127.0.0.1:8080/api/v1",
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
544
681
|
test("Gemini preset supplies command, credentials, trust, and a model environment fallback", () => {
|
|
545
682
|
const config = baseConfig({
|
|
546
683
|
model: "gemini-3-flash-preview",
|
|
@@ -687,7 +824,10 @@ new AgentSideConnection((connection) => new FakeCopilot(connection), stream);
|
|
|
687
824
|
chmodSync(copilotStub, 0o755);
|
|
688
825
|
mkdirSync(join(binDir, "home"));
|
|
689
826
|
|
|
690
|
-
const {
|
|
827
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
828
|
+
"copilot-token-id",
|
|
829
|
+
"example-aseph_x",
|
|
830
|
+
);
|
|
691
831
|
try {
|
|
692
832
|
const config = baseConfig({
|
|
693
833
|
cwd,
|
|
@@ -721,7 +861,7 @@ new AgentSideConnection((connection) => new FakeCopilot(connection), stream);
|
|
|
721
861
|
while (existsSync(captured.dir) && Date.now() < deadline) await Bun.sleep(10);
|
|
722
862
|
expect(existsSync(captured.dir)).toBe(false);
|
|
723
863
|
} finally {
|
|
724
|
-
await
|
|
864
|
+
await closeTokenStub();
|
|
725
865
|
}
|
|
726
866
|
});
|
|
727
867
|
|
|
@@ -1077,7 +1217,7 @@ const stream = ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(proce
|
|
|
1077
1217
|
new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
1078
1218
|
`,
|
|
1079
1219
|
);
|
|
1080
|
-
const {
|
|
1220
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
1081
1221
|
"stub-token-id",
|
|
1082
1222
|
"aseph_stubtokenfortest1234567890",
|
|
1083
1223
|
);
|
|
@@ -1140,7 +1280,7 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
1140
1280
|
closeDb();
|
|
1141
1281
|
}
|
|
1142
1282
|
} finally {
|
|
1143
|
-
await
|
|
1283
|
+
await closeTokenStub();
|
|
1144
1284
|
}
|
|
1145
1285
|
});
|
|
1146
1286
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildPayload, parseJunit } from "../../scripts/ci-unit-test-report";
|
|
3
|
+
|
|
4
|
+
// Shape emitted by `bun test --reporter=junit` (bun 1.4): one testsuite per file,
|
|
5
|
+
// nested testsuites per describe, and attribute values XML-escaped.
|
|
6
|
+
const JUNIT = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7
|
+
<testsuites name="bun test" tests="6" assertions="5" failures="1" skipped="2" time="0.3">
|
|
8
|
+
<testsuite name="src/a.test.ts" file="src/a.test.ts" tests="4" failures="1" skipped="1" time="0.2">
|
|
9
|
+
<testsuite name="outer" file="src/a.test.ts" line="3" tests="4" time="0">
|
|
10
|
+
<testsuite name="inner & "nested"" file="src/a.test.ts" line="4" tests="2" time="0">
|
|
11
|
+
<testcase name="slow one" classname="inner" time="0.120" file="src/a.test.ts" line="5" assertions="1" />
|
|
12
|
+
<testcase name="breaks <sometimes>" classname="inner" time="0.004" file="src/a.test.ts" line="6" assertions="1">
|
|
13
|
+
<failure type="AssertionError" message="expect(received).toBe(expected) ">AssertionError </failure>
|
|
14
|
+
</testcase>
|
|
15
|
+
</testsuite>
|
|
16
|
+
<testcase name="fast" classname="outer" time="0.000010" file="src/a.test.ts" line="8" assertions="1" />
|
|
17
|
+
<testcase name="later" classname="outer" time="0" file="src/a.test.ts" line="9" assertions="0">
|
|
18
|
+
<skipped message="TODO" />
|
|
19
|
+
</testcase>
|
|
20
|
+
</testsuite>
|
|
21
|
+
</testsuite>
|
|
22
|
+
<testsuite name="src/b.test.ts" file="src/b.test.ts" tests="2" failures="0" skipped="1" time="0.1">
|
|
23
|
+
<testcase name="top level" classname="" time="0.06" file="src/b.test.ts" line="2" assertions="2" />
|
|
24
|
+
<testcase name="skipped" classname="" time="0" file="src/b.test.ts" line="3" assertions="0">
|
|
25
|
+
<skipped />
|
|
26
|
+
</testcase>
|
|
27
|
+
</testsuite>
|
|
28
|
+
</testsuites>`;
|
|
29
|
+
|
|
30
|
+
describe("ci-unit-test-report", () => {
|
|
31
|
+
test("parses describe chains, statuses, durations and XML entities", () => {
|
|
32
|
+
expect(parseJunit(JUNIT)).toEqual([
|
|
33
|
+
{
|
|
34
|
+
file: "src/a.test.ts",
|
|
35
|
+
name: 'outer > inner & "nested" > slow one',
|
|
36
|
+
ms: 120,
|
|
37
|
+
status: "pass",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
file: "src/a.test.ts",
|
|
41
|
+
name: 'outer > inner & "nested" > breaks <sometimes>',
|
|
42
|
+
ms: 4,
|
|
43
|
+
status: "fail",
|
|
44
|
+
},
|
|
45
|
+
{ file: "src/a.test.ts", name: "outer > fast", ms: 0, status: "pass" },
|
|
46
|
+
{ file: "src/a.test.ts", name: "outer > later", ms: 0, status: "skip" },
|
|
47
|
+
{ file: "src/b.test.ts", name: "top level", ms: 60, status: "pass" },
|
|
48
|
+
{ file: "src/b.test.ts", name: "skipped", ms: 0, status: "skip" },
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("keeps every non-pass and every test at or above minMs, with per-file totals", () => {
|
|
53
|
+
const payload = buildPayload(parseJunit(JUNIT), new Map([["src/a.test.ts", "abc123"]]), 50);
|
|
54
|
+
expect(payload.totals).toEqual({ tests: 6, pass: 3, fail: 1, skip: 2, testMs: 184 });
|
|
55
|
+
expect(payload.files).toEqual([
|
|
56
|
+
["src/a.test.ts", "abc123", 4, 1, 124],
|
|
57
|
+
["src/b.test.ts", null, 2, 0, 60],
|
|
58
|
+
]);
|
|
59
|
+
expect(payload.tests).toEqual([
|
|
60
|
+
[0, 'outer > inner & "nested" > slow one', 120, "p"],
|
|
61
|
+
[0, 'outer > inner & "nested" > breaks <sometimes>', 4, "f"],
|
|
62
|
+
[0, "outer > later", 0, "s"],
|
|
63
|
+
[1, "top level", 60, "p"],
|
|
64
|
+
[1, "skipped", 0, "s"],
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("an empty report parses to no tests", () => {
|
|
69
|
+
expect(
|
|
70
|
+
parseJunit('<?xml version="1.0"?><testsuites name="bun test" tests="0"></testsuites>'),
|
|
71
|
+
).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|