@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for `summarizeSessionForPi` in `src/providers/pi-mono-extension.ts
|
|
2
|
+
* Unit tests for `summarizeSessionForPi` in `src/providers/pi-mono-extension.ts`,
|
|
3
|
+
* plus the extension's advisory nudges (delivered with `pi.sendMessage`).
|
|
3
4
|
*
|
|
4
5
|
* Plan: thoughts/taras/plans/2026-05-10-fix-session-summarization-workers.md
|
|
5
6
|
* → Phase 1 § "Test coverage"
|
|
@@ -19,8 +20,14 @@
|
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
23
|
+
import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { clearToolHistory } from "../hooks/tool-loop-detection";
|
|
22
25
|
import type { SummarizeSessionForPiDeps, SwarmHooksConfig } from "../providers/pi-mono-extension";
|
|
23
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
createSwarmHooksExtension,
|
|
28
|
+
SWARM_NUDGE_CUSTOM_TYPE,
|
|
29
|
+
summarizeSessionForPi,
|
|
30
|
+
} from "../providers/pi-mono-extension";
|
|
24
31
|
|
|
25
32
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
26
33
|
|
|
@@ -345,3 +352,155 @@ describe("summarizeSessionForPi", () => {
|
|
|
345
352
|
expect(consoleErrors.length).toBe(0);
|
|
346
353
|
});
|
|
347
354
|
});
|
|
355
|
+
|
|
356
|
+
describe("pi provider header attribution", () => {
|
|
357
|
+
test("uses request model URL and task env, replacing pi attribution", async () => {
|
|
358
|
+
let handler:
|
|
359
|
+
| ((event: { headers: Record<string, string> }, ctx: { model: { baseUrl: string } }) => void)
|
|
360
|
+
| undefined;
|
|
361
|
+
const pi = {
|
|
362
|
+
on: (event: string, callback: unknown) => {
|
|
363
|
+
if (event === "before_provider_headers") handler = callback as NonNullable<typeof handler>;
|
|
364
|
+
},
|
|
365
|
+
} as unknown as ExtensionAPI;
|
|
366
|
+
const config = { ...makeConfig(), env: {} as Record<string, string | undefined> };
|
|
367
|
+
await createSwarmHooksExtension(config)(pi);
|
|
368
|
+
expect(handler).toBeDefined();
|
|
369
|
+
const headers = {
|
|
370
|
+
"HTTP-Referer": "https://pi.dev",
|
|
371
|
+
"X-OpenRouter-Title": "pi",
|
|
372
|
+
"X-Custom": "keep",
|
|
373
|
+
};
|
|
374
|
+
handler!({ headers }, { model: { baseUrl: "https://openrouter.ai/api" } });
|
|
375
|
+
expect(headers).toEqual({
|
|
376
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
377
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
378
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
379
|
+
"X-Custom": "keep",
|
|
380
|
+
});
|
|
381
|
+
config.env.OPENROUTER_APP_ATTRIBUTION = "false";
|
|
382
|
+
const optedOut = { "X-Custom": "keep" };
|
|
383
|
+
handler!({ headers: optedOut }, { model: { baseUrl: "https://openrouter.ai/api" } });
|
|
384
|
+
expect(optedOut).toEqual({ "X-Custom": "keep" });
|
|
385
|
+
config.env = {};
|
|
386
|
+
const gateway = {};
|
|
387
|
+
handler!({ headers: gateway }, { model: { baseUrl: "http://localhost:1234/v1" } });
|
|
388
|
+
expect(gateway).toEqual({});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ── advisory nudges reach the model via pi.sendMessage ───────────────────────
|
|
393
|
+
|
|
394
|
+
type Handler = (event: unknown, ctx: unknown) => Promise<unknown> | unknown;
|
|
395
|
+
type SentMessage = { customType: string; content: Array<{ type: string; text: string }> };
|
|
396
|
+
|
|
397
|
+
/** Minimal stand-in for pi's ExtensionAPI: records handlers and sent messages. */
|
|
398
|
+
function loadExtension(config: SwarmHooksConfig) {
|
|
399
|
+
const handlers = new Map<string, Handler>();
|
|
400
|
+
const sent: SentMessage[] = [];
|
|
401
|
+
const pi = {
|
|
402
|
+
on: (event: string, handler: Handler) => {
|
|
403
|
+
handlers.set(event, handler);
|
|
404
|
+
},
|
|
405
|
+
sendMessage: (message: SentMessage) => {
|
|
406
|
+
sent.push(message);
|
|
407
|
+
},
|
|
408
|
+
registerMcpServer: () => {},
|
|
409
|
+
};
|
|
410
|
+
createSwarmHooksExtension(config)(pi as unknown as Parameters<ExtensionFactory>[0]);
|
|
411
|
+
const ctx = { getContextUsage: () => undefined };
|
|
412
|
+
const fire = (event: string, payload: unknown = {}) => {
|
|
413
|
+
const handler = handlers.get(event);
|
|
414
|
+
if (!handler) throw new Error(`no handler for ${event}`);
|
|
415
|
+
return handler({ type: event, ...(payload as object) }, ctx);
|
|
416
|
+
};
|
|
417
|
+
const texts = () =>
|
|
418
|
+
sent.map((m) => {
|
|
419
|
+
expect(m.customType).toBe(SWARM_NUDGE_CUSTOM_TYPE);
|
|
420
|
+
return m.content.map((c) => c.text).join("");
|
|
421
|
+
});
|
|
422
|
+
return { fire, texts };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function jsonResponse(body: unknown): FetchHandlerResp {
|
|
426
|
+
return { ok: true, status: 200, text: async () => "", json: async () => body };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
describe("createSwarmHooksExtension nudges", () => {
|
|
430
|
+
test("loop warning on tool_call is sent to the model", async () => {
|
|
431
|
+
const config = { ...makeConfig(), taskId: `task-pi-loop-${crypto.randomUUID()}` };
|
|
432
|
+
const { fire, texts } = loadExtension(config);
|
|
433
|
+
try {
|
|
434
|
+
for (let i = 0; i < 8; i++) {
|
|
435
|
+
const result = await fire("tool_call", { toolName: "bash", input: { command: "ls" } });
|
|
436
|
+
expect(result).toBeUndefined();
|
|
437
|
+
}
|
|
438
|
+
const warnings = texts().filter((t) => t.startsWith("Warning: "));
|
|
439
|
+
expect(warnings.length).toBe(1);
|
|
440
|
+
expect(warnings[0]).toContain("bash");
|
|
441
|
+
} finally {
|
|
442
|
+
await clearToolHistory(config.taskId);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("lead send-task reminder on tool_result is sent to the model", async () => {
|
|
447
|
+
const { fire, texts } = loadExtension({ ...makeConfig(), isLead: true });
|
|
448
|
+
await fire("tool_result", { toolName: "mcp__agent-swarm__send-task", input: {} });
|
|
449
|
+
expect(texts()).toEqual([
|
|
450
|
+
"Task sent successfully. Monitor progress using the get-task-details tool periodically.",
|
|
451
|
+
]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("lead concurrent context on session_start is sent to the model", async () => {
|
|
455
|
+
fetchHandler = async (url) =>
|
|
456
|
+
url.includes("/api/concurrent-context")
|
|
457
|
+
? jsonResponse({
|
|
458
|
+
processingInboxMessages: [
|
|
459
|
+
{ content: "deploy the thing", source: "slack", createdAt: "now" },
|
|
460
|
+
],
|
|
461
|
+
recentTaskDelegations: [],
|
|
462
|
+
activeSwarmTasks: [],
|
|
463
|
+
})
|
|
464
|
+
: jsonResponse({});
|
|
465
|
+
const { fire, texts } = loadExtension({ ...makeConfig(), isLead: true });
|
|
466
|
+
await fire("session_start");
|
|
467
|
+
expect(texts().length).toBe(1);
|
|
468
|
+
expect(texts()[0]).toContain("=== CONCURRENT SESSION AWARENESS ===");
|
|
469
|
+
expect(texts()[0]).toContain("deploy the thing");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("goal reminder is sent after compaction, not before every LLM call", async () => {
|
|
473
|
+
fetchHandler = async (url) =>
|
|
474
|
+
url.endsWith("/api/tasks/task-pi-1")
|
|
475
|
+
? jsonResponse({ id: "task-pi-1", task: "ship the fix", progress: "tests green" })
|
|
476
|
+
: jsonResponse({});
|
|
477
|
+
const { fire, texts } = loadExtension(makeConfig());
|
|
478
|
+
|
|
479
|
+
await fire("context", { messages: [] });
|
|
480
|
+
expect(texts()).toEqual([]);
|
|
481
|
+
|
|
482
|
+
await fire("session_compact");
|
|
483
|
+
expect(texts()).toEqual([
|
|
484
|
+
[
|
|
485
|
+
"=== GOAL REMINDER (injected before context compaction) ===",
|
|
486
|
+
"Task ID: task-pi-1",
|
|
487
|
+
"Task: ship the fix",
|
|
488
|
+
"Current Progress: tests green",
|
|
489
|
+
"=== Continue working on this task after compaction ===",
|
|
490
|
+
].join("\n"),
|
|
491
|
+
]);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("cancelled task on input skips the turn and leaves the nudge in the session", async () => {
|
|
495
|
+
fetchHandler = async (url) =>
|
|
496
|
+
url.includes("/cancelled-tasks")
|
|
497
|
+
? jsonResponse({ cancelled: [{ id: "task-pi-1", failureReason: "lead stopped it" }] })
|
|
498
|
+
: jsonResponse({});
|
|
499
|
+
const { fire, texts } = loadExtension(makeConfig());
|
|
500
|
+
const result = await fire("input", { text: "go", source: "rpc" });
|
|
501
|
+
expect(result).toEqual({ action: "handled" });
|
|
502
|
+
expect(texts()).toEqual([
|
|
503
|
+
"🛑 TASK CANCELLED: lead stopped it. Stop working and use store-progress to acknowledge.",
|
|
504
|
+
]);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
@@ -2,8 +2,9 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test
|
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, getActivePricingRow, getDbClient, getLogsByEventType, initDb } from "../be/db";
|
|
4
4
|
import { getModelsCatalog, resetModelsCatalogForTests } from "../be/models-catalog";
|
|
5
|
-
import type
|
|
5
|
+
import { loadModelsDevCache, type ModelsDevCache } from "../be/modelsdev-cache";
|
|
6
6
|
import { refreshPricingFromModelsDev } from "../be/pricing-refresh";
|
|
7
|
+
import { buildModelsDevSeedRows, buildPricingSeedRows } from "../be/seed-pricing";
|
|
7
8
|
|
|
8
9
|
const TEST_DB_PATH = "./test-pricing-refresh.sqlite";
|
|
9
10
|
|
|
@@ -201,6 +202,74 @@ describe("models.dev runtime pricing refresh", () => {
|
|
|
201
202
|
expect(rows.map((row) => row.effective_from)).toEqual([2_000, 3_000]);
|
|
202
203
|
});
|
|
203
204
|
|
|
205
|
+
test("refreshes when dated snapshots collapse onto one amp key at different prices", async () => {
|
|
206
|
+
// models.dev's live gpt-4o family. Amp strips the dated suffix, so all four
|
|
207
|
+
// land on amp|gpt-4o; the 2024-05-13 snapshot is priced higher. Its key
|
|
208
|
+
// comes first, as upstream serves it, so a first-wins builder would pick it.
|
|
209
|
+
const cache: ModelsDevCache = {
|
|
210
|
+
openai: {
|
|
211
|
+
models: {
|
|
212
|
+
"gpt-4o-2024-05-13": { cost: { input: 5, output: 15 } },
|
|
213
|
+
"gpt-4o": { cost: { input: 2.5, output: 10, cache_read: 1.25 } },
|
|
214
|
+
"gpt-4o-2024-08-06": { cost: { input: 2.5, output: 10, cache_read: 1.25 } },
|
|
215
|
+
"gpt-4o-2024-11-20": { cost: { input: 2.5, output: 10, cache_read: 1.25 } },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = await refreshPricingFromModelsDev({
|
|
221
|
+
now: 1_000,
|
|
222
|
+
fetchImpl: async () => responseFor(cache, '"etag-collapse"'),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result.status).toBe("refreshed");
|
|
226
|
+
expect(await getLogsByEventType("pricing.refresh.failed")).toHaveLength(0);
|
|
227
|
+
// The undated alias wins every token class.
|
|
228
|
+
expect((await getActivePricingRow("amp", "gpt-4o", "input", 1_000))?.pricePerMillionUsd).toBe(
|
|
229
|
+
2.5,
|
|
230
|
+
);
|
|
231
|
+
expect((await getActivePricingRow("amp", "gpt-4o", "output", 1_000))?.pricePerMillionUsd).toBe(
|
|
232
|
+
10,
|
|
233
|
+
);
|
|
234
|
+
expect(
|
|
235
|
+
(await getActivePricingRow("amp", "gpt-4o", "cached_input", 1_000))?.pricePerMillionUsd,
|
|
236
|
+
).toBe(1.25);
|
|
237
|
+
// Providers that keep the dated id still price each snapshot on its own.
|
|
238
|
+
expect(
|
|
239
|
+
(await getActivePricingRow("codex", "gpt-4o-2024-05-13", "input", 1_000))?.pricePerMillionUsd,
|
|
240
|
+
).toBe(5);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("picks the newest dated snapshot when no undated alias exists", () => {
|
|
244
|
+
const rows = buildModelsDevSeedRows({
|
|
245
|
+
openai: {
|
|
246
|
+
models: {
|
|
247
|
+
"gpt-x-2025-08-07": { cost: { input: 2, output: 8 } },
|
|
248
|
+
"gpt-x-2025-01-01": { cost: { input: 1, output: 4 } },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}).filter((row) => row.provider === "amp");
|
|
252
|
+
|
|
253
|
+
expect(rows).toEqual([
|
|
254
|
+
{ provider: "amp", model: "gpt-x", tokenClass: "input", pricePerMillionUsd: 2 },
|
|
255
|
+
{ provider: "amp", model: "gpt-x", tokenClass: "output", pricePerMillionUsd: 8 },
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("the vendored models.dev snapshot yields one row per (provider, model, tokenClass)", () => {
|
|
260
|
+
const cache = loadModelsDevCache();
|
|
261
|
+
expect(cache).not.toBeNull();
|
|
262
|
+
const rows = buildPricingSeedRows(cache);
|
|
263
|
+
const seen = new Set<string>();
|
|
264
|
+
const duplicates: string[] = [];
|
|
265
|
+
for (const row of rows) {
|
|
266
|
+
const key = `${row.provider}|${row.model}|${row.tokenClass}`;
|
|
267
|
+
if (seen.has(key)) duplicates.push(key);
|
|
268
|
+
seen.add(key);
|
|
269
|
+
}
|
|
270
|
+
expect(duplicates).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
|
|
204
273
|
test("writes scrubbed audit log entries for successful refreshes", async () => {
|
|
205
274
|
await refreshPricingFromModelsDev({
|
|
206
275
|
now: 1_000,
|
|
@@ -305,7 +305,9 @@ describe("Claude CLI shortnames (the tier defaults) take effort like the model t
|
|
|
305
305
|
expect(reasoningCapability("claude", "sonnet")).toEqual(
|
|
306
306
|
reasoningCapability("claude", "claude-sonnet-5-5"),
|
|
307
307
|
);
|
|
308
|
-
expect(reasoningCapability("claude", "haiku")
|
|
308
|
+
expect(reasoningCapability("claude", "haiku")).toEqual(
|
|
309
|
+
reasoningCapability("claude", "claude-haiku-5-5"),
|
|
310
|
+
);
|
|
309
311
|
expect(reasoningCapability("claude", "fable").supported).toBe(true);
|
|
310
312
|
});
|
|
311
313
|
|
|
@@ -127,7 +127,18 @@ describe("trigger_surface on task telemetry", () => {
|
|
|
127
127
|
// so each one is run for real and judged on the row it persists and the
|
|
128
128
|
// task.created event it emits.
|
|
129
129
|
const heartbeatCreators: Array<{ taskType: string; create: () => Promise<void> }> = [
|
|
130
|
-
{
|
|
130
|
+
{
|
|
131
|
+
taskType: "boot-triage",
|
|
132
|
+
create: async () => {
|
|
133
|
+
// A clean boot creates no task; force one to judge its source.
|
|
134
|
+
process.env.HEARTBEAT_BOOT_TRIAGE_ALWAYS = "true";
|
|
135
|
+
try {
|
|
136
|
+
await createBootTriageTask();
|
|
137
|
+
} finally {
|
|
138
|
+
delete process.env.HEARTBEAT_BOOT_TRIAGE_ALWAYS;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
},
|
|
131
142
|
{
|
|
132
143
|
taskType: "heartbeat-checklist",
|
|
133
144
|
create: async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { DEFAULT_MODEL } from "../utils/internal-ai/models";
|
|
3
|
+
import { OPENROUTER_APP_ATTRIBUTION_HEADERS } from "../utils/openrouter-base-url";
|
|
3
4
|
import { resolveWorkflowLlmConfig } from "../workflows/executors/workflow-llm";
|
|
4
5
|
|
|
5
6
|
describe("resolveWorkflowLlmConfig", () => {
|
|
@@ -12,10 +13,22 @@ describe("resolveWorkflowLlmConfig", () => {
|
|
|
12
13
|
expect(config).toEqual({
|
|
13
14
|
apiKey: "sk-or-test",
|
|
14
15
|
baseURL: "https://gateway.example.test/v1",
|
|
16
|
+
headers: {},
|
|
15
17
|
model: "deepseek/deepseek-v4.1-flash",
|
|
16
18
|
});
|
|
17
19
|
});
|
|
18
20
|
|
|
21
|
+
test("attributes direct openrouter.ai calls unless OPENROUTER_APP_ATTRIBUTION is off", async () => {
|
|
22
|
+
const direct = await resolveWorkflowLlmConfig(undefined, { OPENROUTER_API_KEY: "sk-or-test" });
|
|
23
|
+
expect(direct.headers).toEqual(OPENROUTER_APP_ATTRIBUTION_HEADERS);
|
|
24
|
+
|
|
25
|
+
const optedOut = await resolveWorkflowLlmConfig(undefined, {
|
|
26
|
+
OPENROUTER_API_KEY: "sk-or-test",
|
|
27
|
+
OPENROUTER_APP_ATTRIBUTION: "false",
|
|
28
|
+
});
|
|
29
|
+
expect(optedOut.headers).toEqual({});
|
|
30
|
+
});
|
|
31
|
+
|
|
19
32
|
test("routes OpenAI credentials to the SDK default endpoint", async () => {
|
|
20
33
|
const config = await resolveWorkflowLlmConfig(undefined, {
|
|
21
34
|
OPENAI_API_KEY: "example-sk-openai-test",
|
|
@@ -24,6 +37,7 @@ describe("resolveWorkflowLlmConfig", () => {
|
|
|
24
37
|
expect(config).toEqual({
|
|
25
38
|
apiKey: "example-sk-openai-test",
|
|
26
39
|
baseURL: undefined,
|
|
40
|
+
headers: {},
|
|
27
41
|
model: "gpt-6-luna",
|
|
28
42
|
});
|
|
29
43
|
});
|
|
@@ -734,4 +734,37 @@ describe("system-one-decision SystemOne wire is unchanged", () => {
|
|
|
734
734
|
expect(calls[0]?.raw).toBe(expected);
|
|
735
735
|
}
|
|
736
736
|
});
|
|
737
|
+
|
|
738
|
+
test("only the openrouter.ai provider carries OpenRouter app attribution", async () => {
|
|
739
|
+
const referer = async (provider: string, env: Record<string, string>) => {
|
|
740
|
+
const { executor, calls } = makeExecutor(
|
|
741
|
+
[
|
|
742
|
+
jsonResponse({
|
|
743
|
+
model: "jev",
|
|
744
|
+
answers: systemOneAnswers,
|
|
745
|
+
usage: { input_tokens: 1, output_tokens: 0 },
|
|
746
|
+
}),
|
|
747
|
+
],
|
|
748
|
+
{
|
|
749
|
+
getApiKey: async () => OPENAI_KEY,
|
|
750
|
+
getServerUrl: async () => "https://laya.example.test",
|
|
751
|
+
env: { OPENROUTER_API_KEY: OPENAI_KEY, ...env },
|
|
752
|
+
},
|
|
753
|
+
);
|
|
754
|
+
await run(executor, { ...config({ model: "jev-1.13.0" }), provider });
|
|
755
|
+
const headers = new Headers(calls[0]?.init.headers);
|
|
756
|
+
return [headers.get("http-referer"), headers.get("x-openrouter-categories")];
|
|
757
|
+
};
|
|
758
|
+
expect(await referer("openrouter", {})).toEqual([
|
|
759
|
+
"https://agent-swarm.dev",
|
|
760
|
+
"personal-agent,cloud-agent",
|
|
761
|
+
]);
|
|
762
|
+
expect(await referer("openrouter", { OPENROUTER_APP_ATTRIBUTION: "false" })).toEqual([
|
|
763
|
+
null,
|
|
764
|
+
null,
|
|
765
|
+
]);
|
|
766
|
+
for (const provider of ["typesafe", "laya"]) {
|
|
767
|
+
expect(await referer(provider, {})).toEqual([null, null]);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
737
770
|
});
|
|
@@ -12,7 +12,10 @@ const QuestionSchema = z.object({
|
|
|
12
12
|
.describe("Question type"),
|
|
13
13
|
label: z.string().describe("The question text displayed to the user"),
|
|
14
14
|
required: z.boolean().optional().describe("Whether this question is required (default: true)"),
|
|
15
|
-
description: z
|
|
15
|
+
description: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Optional help text, rendered as markdown (code fences, images)"),
|
|
16
19
|
placeholder: z.string().optional().describe("Placeholder text (for text type)"),
|
|
17
20
|
multiline: z.boolean().optional().describe("Use textarea instead of input (for text type)"),
|
|
18
21
|
options: z
|
|
@@ -39,7 +42,9 @@ export const registerRequestHumanInputTool = (server: McpServer) => {
|
|
|
39
42
|
description:
|
|
40
43
|
"Create an approval request and return at once with the request id and URL. The answer arrives later as a hitl-follow-up task. " +
|
|
41
44
|
"Supports multiple question types: approval (yes/no), text, single-select, " +
|
|
42
|
-
"multi-select, and boolean. Returns the request ID and URL for the human to respond."
|
|
45
|
+
"multi-select, and boolean. Returns the request ID and URL for the human to respond. " +
|
|
46
|
+
"The dashboard renders each label and description as markdown: put long content in description, fence code with a language, and put an image URL alone on its own line to show it. " +
|
|
47
|
+
"No Slack message is sent; share the URL yourself.",
|
|
43
48
|
inputSchema: z.object({
|
|
44
49
|
title: z.string().min(1).describe("Title of the approval request"),
|
|
45
50
|
questions: z.array(QuestionSchema).min(1).describe("Questions to ask the human"),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the `SWARM_ENABLED_TOOLS` MCP tool allowlist.
|
|
3
|
+
*
|
|
4
|
+
* Accepts a comma-separated list (`get-tasks, store-progress`) or a JSON array
|
|
5
|
+
* of strings. Returns `undefined` when the value is unset, blank, or holds no
|
|
6
|
+
* entries, so callers keep the capability-driven surface. Throws on a JSON
|
|
7
|
+
* array that does not parse to strings.
|
|
8
|
+
*/
|
|
9
|
+
export function parseEnabledTools(value: string | undefined): string[] | undefined {
|
|
10
|
+
if (value === undefined) return undefined;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
if (!trimmed) return undefined;
|
|
13
|
+
|
|
14
|
+
let entries: string[];
|
|
15
|
+
if (trimmed.startsWith("[")) {
|
|
16
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
17
|
+
if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) {
|
|
18
|
+
throw new Error("Expected a JSON array of tool names");
|
|
19
|
+
}
|
|
20
|
+
entries = parsed;
|
|
21
|
+
} else {
|
|
22
|
+
entries = trimmed.split(",");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tools = [...new Set(entries.map((entry) => entry.trim()).filter(Boolean))];
|
|
26
|
+
return tools.length > 0 ? tools : undefined;
|
|
27
|
+
}
|
|
@@ -20,7 +20,11 @@ import { complete } from "@earendil-works/pi-ai/compat";
|
|
|
20
20
|
import { getBuiltinModel as getModel } from "@earendil-works/pi-ai/providers/all";
|
|
21
21
|
import type { TSchema } from "typebox";
|
|
22
22
|
import { z } from "zod";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_OPENROUTER_BASE_URL,
|
|
25
|
+
getOpenRouterAttributionHeaders,
|
|
26
|
+
getOpenRouterBaseUrl,
|
|
27
|
+
} from "../openrouter-base-url.js";
|
|
24
28
|
import { scrubSecrets } from "../secret-scrubber.js";
|
|
25
29
|
import { type ResolvedCredential, resolveCredential } from "./credentials.js";
|
|
26
30
|
import { parseModelStr } from "./models.js";
|
|
@@ -319,10 +323,14 @@ export async function completeStructuredWithModel<TZod extends z.ZodTypeAny>(
|
|
|
319
323
|
// through a gateway (pi-ai's stream path uses `model.baseUrl` verbatim).
|
|
320
324
|
// Resolved from the caller's session env when provided — swarm_config
|
|
321
325
|
// overlays don't reach `process.env`.
|
|
322
|
-
const
|
|
326
|
+
const env = opts.env ?? process.env;
|
|
327
|
+
const openRouterBaseUrl = getOpenRouterBaseUrl(env as NodeJS.ProcessEnv);
|
|
323
328
|
if (provider === "openrouter" && openRouterBaseUrl !== DEFAULT_OPENROUTER_BASE_URL) {
|
|
324
329
|
model = { ...model, baseUrl: openRouterBaseUrl };
|
|
325
330
|
}
|
|
331
|
+
// pi-ai merges `options.headers` over the provider defaults.
|
|
332
|
+
const headers =
|
|
333
|
+
provider === "openrouter" ? getOpenRouterAttributionHeaders(model.baseUrl, env) : {};
|
|
326
334
|
|
|
327
335
|
const completeFn = opts._complete ?? complete;
|
|
328
336
|
const toolChoice =
|
|
@@ -348,7 +356,7 @@ export async function completeStructuredWithModel<TZod extends z.ZodTypeAny>(
|
|
|
348
356
|
},
|
|
349
357
|
// pi-ai's ProviderStreamOptions type only allows known providers;
|
|
350
358
|
// we pass the validated apiKey through verbatim.
|
|
351
|
-
{ apiKey: cred.apiKey, signal: opts.signal, toolChoice } as Parameters<
|
|
359
|
+
{ apiKey: cred.apiKey, signal: opts.signal, toolChoice, headers } as Parameters<
|
|
352
360
|
typeof completeFn
|
|
353
361
|
>[2],
|
|
354
362
|
);
|
|
@@ -26,3 +26,26 @@ export function getOpenRouterBaseUrl(env: NodeJS.ProcessEnv = process.env): stri
|
|
|
26
26
|
}
|
|
27
27
|
return DEFAULT_OPENROUTER_BASE_URL;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
export const OPENROUTER_APP_ATTRIBUTION_HEADERS = {
|
|
31
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
32
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
33
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/** Attribute direct OpenRouter requests unless the task opts out. */
|
|
37
|
+
export function getOpenRouterAttributionHeaders(
|
|
38
|
+
url: string,
|
|
39
|
+
env: Record<string, string | undefined> = process.env,
|
|
40
|
+
): Record<string, string> {
|
|
41
|
+
const enabled = env.OPENROUTER_APP_ATTRIBUTION?.toLowerCase();
|
|
42
|
+
if (enabled === "false" || enabled === "0") return {};
|
|
43
|
+
try {
|
|
44
|
+
const host = new URL(url).hostname;
|
|
45
|
+
return host === "openrouter.ai" || host.endsWith(".openrouter.ai")
|
|
46
|
+
? { ...OPENROUTER_APP_ATTRIBUTION_HEADERS }
|
|
47
|
+
: {};
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ExecutorMeta } from "../../types";
|
|
3
|
+
import { getOpenRouterAttributionHeaders } from "../../utils/openrouter-base-url";
|
|
3
4
|
import { scrubSecrets } from "../../utils/secret-scrubber";
|
|
4
5
|
import {
|
|
5
6
|
type ApprovalOutcome,
|
|
@@ -1170,6 +1171,7 @@ export class SystemOneDecisionExecutor extends BaseExecutor<
|
|
|
1170
1171
|
res = await doFetch(endpoint, {
|
|
1171
1172
|
method: "POST",
|
|
1172
1173
|
headers: {
|
|
1174
|
+
...getOpenRouterAttributionHeaders(endpoint, this.providerContext().env),
|
|
1173
1175
|
"content-type": "application/json",
|
|
1174
1176
|
accept: "application/json",
|
|
1175
1177
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { DEFAULT_MODEL, resolveCredential } from "../../utils/internal-ai/credentials";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getOpenRouterAttributionHeaders,
|
|
4
|
+
getOpenRouterBaseUrl,
|
|
5
|
+
} from "../../utils/openrouter-base-url";
|
|
3
6
|
|
|
4
7
|
export interface WorkflowLlmConfig {
|
|
5
8
|
apiKey: string;
|
|
6
9
|
baseURL?: string;
|
|
10
|
+
/** OpenRouter app attribution; empty for OpenAI and for gateway base URLs. */
|
|
11
|
+
headers: Record<string, string>;
|
|
7
12
|
model: string;
|
|
8
13
|
}
|
|
9
14
|
|
|
@@ -39,9 +44,11 @@ export async function resolveWorkflowLlmConfig(
|
|
|
39
44
|
// Workflow defaults are provider-owned; MEMORY_RATER_MODEL only configures memory work.
|
|
40
45
|
const model = requestedModel ?? DEFAULT_MODEL[credential.kind];
|
|
41
46
|
|
|
47
|
+
const baseURL = credential.kind === "openrouter" ? getOpenRouterBaseUrl(env) : undefined;
|
|
42
48
|
return {
|
|
43
49
|
apiKey: credential.apiKey,
|
|
44
|
-
baseURL
|
|
50
|
+
baseURL,
|
|
51
|
+
headers: baseURL ? getOpenRouterAttributionHeaders(baseURL, env) : {},
|
|
45
52
|
model: model.startsWith(providerPrefix) ? model.slice(providerPrefix.length) : model,
|
|
46
53
|
};
|
|
47
54
|
}
|
|
@@ -47,6 +47,8 @@ agent-browser close
|
|
|
47
47
|
|
|
48
48
|
## Recording
|
|
49
49
|
|
|
50
|
+
Pass `record start` an absolute path (`/tmp/<topic>/raw.webm`). A relative path resolves against the agent-browser daemon's cwd, not yours, and fails with `ffmpeg write failed: Broken pipe` while `doctor` stays green.
|
|
51
|
+
|
|
50
52
|
Default: record with `--cursor`, drive every supported pointer action (`click`, `mouse move`, `drag`) with `--human`, and ship the **1.5x video**. Keep the 1x original only if a reviewer asks for it. Human pointer movement follows an eased curve; use `--seed <n>` on `mouse move` to make the take reproducible instead of using a bare move.
|
|
51
53
|
|
|
52
54
|
Open the page first, then record the interaction:
|
|
@@ -42,6 +42,8 @@ agent-browser close
|
|
|
42
42
|
|
|
43
43
|
## Recording
|
|
44
44
|
|
|
45
|
+
Pass `record start` an absolute path (`/tmp/<topic>/raw.webm`). A relative path resolves against the agent-browser daemon's cwd, not yours, and fails with `ffmpeg write failed: Broken pipe` while `doctor` stays green.
|
|
46
|
+
|
|
45
47
|
Default: record with `--cursor`, drive every supported pointer action (`click`, `mouse move`, `drag`) with `--human`, and ship the **1.5x video**. Keep the 1x original only if a reviewer asks for it. Human pointer movement follows an eased curve; use `--seed <n>` on `mouse move` to make the take reproducible instead of using a bare move.
|
|
46
48
|
|
|
47
49
|
Open the page first, then record the interaction:
|
|
@@ -38,6 +38,7 @@ The Repository Guidelines carry `allowMerge` and `mergeChecks`.
|
|
|
38
38
|
|
|
39
39
|
## Review a PR
|
|
40
40
|
|
|
41
|
+
0. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop. New SHA: review the diff since that commit, then run the full list below. If the new head merged or rebased main, also read every conflict resolution (files both sides touched), check migration numbering and order against main, grep for stale migration numbers, and confirm CI is green at the exact head you review.
|
|
41
42
|
1. CI status first, including the fork checks below before treating CI as evidence. Failing CI is a REQUEST_CHANGES. Name the failing checks in the review.
|
|
42
43
|
2. Detect a GitHub fork PR with `gh pr view <number> --json isCrossRepository --jq .isCrossRepository`.
|
|
43
44
|
3. For a GitHub fork PR, inspect held runs with `SHA=$(gh pr view <number> --json headRefOid --jq .headRefOid); gh api --paginate "repos/<owner>/<repo>/actions/runs?head_sha=$SHA" --jq '.workflow_runs[] | "\(.conclusion // .status)|\(.name)"'`; `gh pr checks` and `statusCheckRollup` can omit them.
|
|
@@ -27,7 +27,7 @@ Two kinds of sections:
|
|
|
27
27
|
|
|
28
28
|
## Handle a heartbeat-checklist task
|
|
29
29
|
|
|
30
|
-
0. Quiet check. Take the fast path when three things hold: the system status
|
|
30
|
+
0. Quiet check. Take the fast path when three things hold: every failed, stalled or reboot-interrupted task in the system status was already named in the previous heartbeat-checklist task's output (newest completed one via `get-tasks`); no mention has gone unanswered since the last heartbeat; and no tracked item in the task's runbook snapshot is at or past its date. On the fast path, run the two step 3 scripts. If they report nothing the previous heartbeat had not already triaged, complete with one line citing the previous heartbeat task id and the counts, and skip steps 1, 2 and 4-7. Anything new means continue at step 1.
|
|
31
31
|
1. Read the latest runbook. The snapshot in the task may be stale.
|
|
32
32
|
2. Prune first. Check every tracked item against its lift trigger. Remove resolved, stale, and past-date items.
|
|
33
33
|
3. Run the seeded scripts: `script-run` with name `schedule-health`, then `task-failure-audit`, both with `args: { days: 1, publishPage: false }`. They report failing schedules and failure clusters. Check any other standing order in the runbook yourself.
|
|
@@ -13,4 +13,8 @@ When the task names a skill (`researching`, `planning`, `implementing`), use it.
|
|
|
13
13
|
|
|
14
14
|
Finish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.
|
|
15
15
|
|
|
16
|
+
Resuming (a resume task, or work cut by a graceful shutdown): read the parent's last progress line, then re-read live state once (PR state and head SHA, branch, `git status`) before new work. Re-run controls only if the head moved. Never redo a pushed commit or open a second PR for the same branch. Store a progress line with branch, PR number and head SHA each time you push.
|
|
17
|
+
|
|
18
|
+
Write times in replies, outputs and defer notes in the requester's timezone when the Requester Profile names one, never raw UTC.
|
|
19
|
+
|
|
16
20
|
If the user interrupts, follow their instructions. To resume, call `/work-on-task <taskId>` again.
|
|
@@ -8,4 +8,8 @@ When the task names a skill (`researching`, `planning`, `implementing`), use it.
|
|
|
8
8
|
|
|
9
9
|
Finish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.
|
|
10
10
|
|
|
11
|
+
Resuming (a resume task, or work cut by a graceful shutdown): read the parent's last progress line, then re-read live state once (PR state and head SHA, branch, `git status`) before new work. Re-run controls only if the head moved. Never redo a pushed commit or open a second PR for the same branch. Store a progress line with branch, PR number and head SHA each time you push.
|
|
12
|
+
|
|
13
|
+
Write times in replies, outputs and defer notes in the requester's timezone when the Requester Profile names one, never raw UTC.
|
|
14
|
+
|
|
11
15
|
If the user interrupts, follow their instructions. To resume, call `/work-on-task <taskId>` again.
|
|
@@ -173,6 +173,15 @@ Afterwards, a `warnings` entry on `create-workflow` or a patch tool means the ke
|
|
|
173
173
|
- Do not set `retry` or `validation.retry` on a `system-one-decision` node. It retries connection errors, 408, 429, and 5xx itself (`config.maxRetries`, 0 to 3, default 2) inside `config.timeoutMs` (default 30000). A 401, a 422, or an answer that fails validation ends after one attempt.
|
|
174
174
|
- The node reads its provider's key (`TYPESAFE_API_KEY`, `OPENROUTER_API_KEY`, `LAYA_API_KEY`, `OPENAI_DECISIONS_API_KEY`, or `CLOUDFLARE_API_TOKEN`, plus `LAYA_URL` for `laya` and `CLOUDFLARE_ACCOUNT_ID` for `cloudflare`) server-side. The key never appears in the definition, the step output, or an error. A run whose key or `LAYA_URL` is missing fails before any node executes, and a 401 or 403 fails the step as `<KEY> was rejected`. An unresolved `{{token}}` fails the step before any request.
|
|
175
175
|
|
|
176
|
+
### `human-in-the-loop` (how approvals render)
|
|
177
|
+
|
|
178
|
+
The same rules apply to a standalone `request-human-input` call.
|
|
179
|
+
|
|
180
|
+
- **Dashboard card.** `title` is plain text. Each question's `label` is its heading and renders as markdown. `description` renders as full markdown and is the only field that shows images, so put drafts, diffs, and logs there.
|
|
181
|
+
- **Code.** Fence code, logs, and JSON with a language (```` ```json ````): the block keeps its lines and scrolls. Fence a prose draft with no language: it wraps. A single newline outside a fence becomes a paragraph break.
|
|
182
|
+
- **Images.** Put an `http(s)` URL ending in `.png`, `.jpg`, `.jpeg`, `.gif`, or `.webp` alone on its own line in `description`, or use ``. The link must outlive the request: an expired presigned URL shows an "image unavailable" note.
|
|
183
|
+
- **Slack.** A node's `notifications` post the title, the question labels (each collapsed to one line, capped at 3000 characters in total), a Review button, and the timeout. Descriptions, options, code, and images are not shown, so the title and labels must make sense alone. `request-human-input` posts nothing to Slack: share the returned URL yourself.
|
|
184
|
+
|
|
176
185
|
### `swarm-script`
|
|
177
186
|
|
|
178
187
|
```json
|