@desplega.ai/agent-swarm 1.166.0 → 1.168.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-zhejfecx.js +34 -0
- package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
- package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
- package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.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-20q3nzvg.js} +9 -8
- package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
- package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
- package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
- package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.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-7x7chdrs.js} +7608 -7596
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
- package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
- 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-yshaazs1.js → cli-74ez9b66.js} +63 -20
- 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-fn697bnt.js → cli-9q0bh411.js} +21 -8
- package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
- package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
- package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
- package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
- package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
- package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
- package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
- package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
- 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-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
- package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
- package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
- package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- 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-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
- package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +28 -28
- 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-n63k33d3.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-xkxfd81a.js} +18 -18
- package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
- package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
- package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
- package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
- package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.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-phrndeb8.js} +18 -8
- package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
- package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
- package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
- package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
- package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
- package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
- package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
- package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
- package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
- package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
- package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.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-ngskthwh.js} +10 -9
- package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.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-hhm64r6m.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-35f91nx2.js} +11 -10
- package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
- package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
- package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.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-mxf2c842.js} +27 -27
- package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
- package/openapi.json +72 -1
- package/package.json +10 -10
- 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/script-connections.ts +40 -6
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +14 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/extensions/catalog.generated.json +17 -0
- 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/http/script-connections.ts +35 -0
- package/src/providers/acp-targets.ts +10 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +32 -0
- package/src/providers/claude-background-keepalive.ts +100 -0
- package/src/providers/claude-sdk-session.ts +6 -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 +11 -2
- 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/scheduler/scheduler.ts +82 -13
- package/src/scripts-runtime/executors/registry.ts +1 -1
- 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-background-keepalive-wiring.test.ts +220 -0
- package/src/tests/claude-background-keepalive.fixture.ts +162 -0
- package/src/tests/claude-background-keepalive.test.ts +139 -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/connection-embedded-auth.test.ts +51 -0
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +24 -2
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -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/schedule-target-type.test.ts +136 -0
- package/src/tests/script-connections-http.test.ts +45 -0
- package/src/tests/script-connections-mcp.test.ts +41 -0
- package/src/tests/script-executor-registry.test.ts +18 -0
- package/src/tests/send-task-self-send.test.ts +107 -0
- 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/tools/script-connections/tool.ts +35 -3
- package/src/tools/send-task.ts +9 -1
- 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/extensions/README.md +1 -0
- package/templates/extensions/slack-reply-gate/README.md +98 -0
- package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
- package/templates/extensions/slack-reply-gate/manifest.json +8 -0
- 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/swarm-scripts/SKILL.md +10 -0
- package/templates/skills/swarm-scripts/content.md +10 -0
- 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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// send-task refuses an agent session that targets itself, but a script runs as its owner (a
|
|
2
|
+
// scheduled script as the schedule's creator), so a Lead-owned script must be able to file
|
|
3
|
+
// work for the Lead. Script calls reach the tool through /api/mcp-bridge as `script-sdk`.
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
import { unlinkSync } from "node:fs";
|
|
7
|
+
import {
|
|
8
|
+
createServer as createHttpServer,
|
|
9
|
+
type IncomingMessage,
|
|
10
|
+
type Server,
|
|
11
|
+
type ServerResponse,
|
|
12
|
+
} from "node:http";
|
|
13
|
+
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
14
|
+
import { handleMcpBridge } from "../http/mcp-bridge";
|
|
15
|
+
import { getPathSegments } from "../http/utils";
|
|
16
|
+
import { sendTaskHandler } from "../tools/send-task";
|
|
17
|
+
import { ownerCtx } from "../tools/task-tool-ctx";
|
|
18
|
+
import { getRequestInfo } from "../tools/utils";
|
|
19
|
+
|
|
20
|
+
const TEST_DB_PATH = "./test-send-task-self-send.sqlite";
|
|
21
|
+
|
|
22
|
+
let server: Server;
|
|
23
|
+
let baseUrl: string;
|
|
24
|
+
let leadId = "";
|
|
25
|
+
|
|
26
|
+
const selfSendArgs = (agentId: string) => ({
|
|
27
|
+
task: "x mention to triage",
|
|
28
|
+
agentId,
|
|
29
|
+
routingReason: "skill" as const,
|
|
30
|
+
routingNote: "Lead triages mentions itself",
|
|
31
|
+
offerMode: false,
|
|
32
|
+
allowDuplicate: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function tasksFor(agentId: string): Promise<number> {
|
|
36
|
+
return (await getDbClient().get<{ c: number }>(
|
|
37
|
+
"SELECT COUNT(*) AS c FROM agent_tasks WHERE agentId = ?",
|
|
38
|
+
[agentId],
|
|
39
|
+
))!.c;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
initDb(TEST_DB_PATH);
|
|
44
|
+
leadId = (
|
|
45
|
+
await createAgent({ name: "self-send-lead", isLead: true, status: "idle", maxTasks: 5 })
|
|
46
|
+
).id;
|
|
47
|
+
server = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
48
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
49
|
+
const ok = await handleMcpBridge(
|
|
50
|
+
req,
|
|
51
|
+
res,
|
|
52
|
+
getPathSegments(req.url || ""),
|
|
53
|
+
undefined,
|
|
54
|
+
myAgentId,
|
|
55
|
+
);
|
|
56
|
+
if (!ok) {
|
|
57
|
+
res.writeHead(404);
|
|
58
|
+
res.end("Not Found");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
await new Promise<void>((resolve) => server.listen(0, resolve));
|
|
62
|
+
const addr = server.address();
|
|
63
|
+
if (!addr || typeof addr === "string") throw new Error("no port");
|
|
64
|
+
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
server.close();
|
|
69
|
+
closeDb();
|
|
70
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
71
|
+
try {
|
|
72
|
+
unlinkSync(TEST_DB_PATH + suffix);
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("send-task self-send guard", () => {
|
|
80
|
+
test("a script sending a task to its own agent succeeds", async () => {
|
|
81
|
+
const before = await tasksFor(leadId);
|
|
82
|
+
const res = await fetch(`${baseUrl}/api/mcp-bridge`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json", "x-agent-id": leadId },
|
|
85
|
+
body: JSON.stringify({ tool: "send-task", args: selfSendArgs(leadId) }),
|
|
86
|
+
});
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
const body = (await res.json()) as { success?: boolean; message?: string };
|
|
89
|
+
expect(body.message).not.toContain("Cannot send a task to yourself");
|
|
90
|
+
expect(body.success).toBe(true);
|
|
91
|
+
expect(await tasksFor(leadId)).toBe(before + 1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("an agent session sending a task to itself is still refused", async () => {
|
|
95
|
+
const before = await tasksFor(leadId);
|
|
96
|
+
// An MCP session's meta carries no origin mark, so getRequestInfo reports `mcp`.
|
|
97
|
+
const info = getRequestInfo({
|
|
98
|
+
sessionId: "session-1",
|
|
99
|
+
requestInfo: { headers: { "x-agent-id": leadId } },
|
|
100
|
+
} as unknown as Parameters<typeof getRequestInfo>[0]);
|
|
101
|
+
expect(info.callOrigin).toBe("mcp");
|
|
102
|
+
const result = await sendTaskHandler(ownerCtx(info), selfSendArgs(leadId));
|
|
103
|
+
expect(result.ok).toBe(false);
|
|
104
|
+
expect(result.message).toContain("Cannot send a task to yourself");
|
|
105
|
+
expect(await tasksFor(leadId)).toBe(before);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -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"),
|
|
@@ -4,6 +4,7 @@ import { getAgentById } from "@/be/db";
|
|
|
4
4
|
import {
|
|
5
5
|
ConnectionAuthInputSchema,
|
|
6
6
|
connectionAuthInputFromFlat,
|
|
7
|
+
deleteScriptConnection,
|
|
7
8
|
getScriptConnectionById,
|
|
8
9
|
listScriptConnections,
|
|
9
10
|
refreshScriptConnection,
|
|
@@ -16,13 +17,23 @@ import { resolveScopedResourceId, scopedResourceScopeIdSchema } from "@/utils/sc
|
|
|
16
17
|
|
|
17
18
|
const scriptConnectionsInputSchema = z.object({
|
|
18
19
|
action: z
|
|
19
|
-
.enum([
|
|
20
|
-
|
|
20
|
+
.enum([
|
|
21
|
+
"list",
|
|
22
|
+
"upsert-openapi",
|
|
23
|
+
"upsert-mcp",
|
|
24
|
+
"upsert-graphql",
|
|
25
|
+
"refresh",
|
|
26
|
+
"disable",
|
|
27
|
+
"delete",
|
|
28
|
+
])
|
|
29
|
+
.describe(
|
|
30
|
+
"List, create/update, refresh, disable, or delete a script connection. delete is permanent: it removes the connection, its managed credential binding, and any inline secret it owns.",
|
|
31
|
+
),
|
|
21
32
|
id: z
|
|
22
33
|
.string()
|
|
23
34
|
.uuid()
|
|
24
35
|
.optional()
|
|
25
|
-
.describe("Existing connection ID for update, refresh, or
|
|
36
|
+
.describe("Existing connection ID for update, refresh, disable, or delete."),
|
|
26
37
|
slug: z
|
|
27
38
|
.string()
|
|
28
39
|
.min(1)
|
|
@@ -193,6 +204,27 @@ export const registerScriptConnectionsTool = (server: McpServer) => {
|
|
|
193
204
|
});
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
if (args.action === "delete") {
|
|
208
|
+
if (!args.id) {
|
|
209
|
+
return toolErr("id is required for delete.", {
|
|
210
|
+
data: {
|
|
211
|
+
yourAgentId: requestInfo.agentId,
|
|
212
|
+
connections: listScriptConnections({ includeDisabled: true, allScopes: true }),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const deleted = await deleteScriptConnection(args.id);
|
|
217
|
+
const connections = listScriptConnections({ includeDisabled: true, allScopes: true });
|
|
218
|
+
if (!deleted) {
|
|
219
|
+
return toolErr(`Script connection ${args.id} not found.`, {
|
|
220
|
+
data: { yourAgentId: requestInfo.agentId, connections },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return toolOk(`Script connection ${deleted.slug} deleted.`, {
|
|
224
|
+
data: { yourAgentId: requestInfo.agentId, connections },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
196
228
|
if (args.action === "refresh") {
|
|
197
229
|
if (!args.id) {
|
|
198
230
|
return toolErr("id is required for refresh.", {
|
package/src/tools/send-task.ts
CHANGED
|
@@ -355,7 +355,15 @@ export async function sendTaskHandler(
|
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
|
|
358
|
+
// The guard stops an agent session from handing work back to itself. A script runs as its
|
|
359
|
+
// owner (a scheduled script as the schedule's creator), so a Lead-owned poller that files
|
|
360
|
+
// work for the Lead is legitimate. The bridge marks script calls `script-sdk`; a caller that
|
|
361
|
+
// spoofs it can only send itself a task, which grants nothing it could not already do.
|
|
362
|
+
if (
|
|
363
|
+
ctx.kind === "owner" &&
|
|
364
|
+
requestedAgentId === ctx.agentId &&
|
|
365
|
+
ctx.requestInfo?.callOrigin !== "script-sdk"
|
|
366
|
+
) {
|
|
359
367
|
return toolErr("Cannot send a task to yourself, are you drunk?", {
|
|
360
368
|
data: { yourAgentId: ctx.agentId },
|
|
361
369
|
});
|
|
@@ -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
|
}
|
|
@@ -13,6 +13,7 @@ Each folder holds exactly one `manifest.yaml`, `manifest.yml`, or `manifest.json
|
|
|
13
13
|
| `require-ticket-ref` | `pre.task.create` | Blocks REST, MCP, and Slack tasks that do not name a ticket |
|
|
14
14
|
| `notify-on-complete` | `post.task.completed`, `post.task.failed` | Posts a summary to a Slack channel |
|
|
15
15
|
| `deploy-awareness` | `pre.task.create` | Appends a one-line note to new tasks while a Dokploy deploy is in progress |
|
|
16
|
+
| `slack-reply-gate` | `pre.task.create` | Asks Jev (TypeSafe or OpenRouter) whether Lead should answer a Slack thread follow-up; in `enforce` drops the wake-up and marks the message :mute: |
|
|
16
17
|
| `require-verification-note` | `pre.tool.call` | Refuses completion without a `Verified:` line in the output |
|
|
17
18
|
| `task-digest` | `post.task.completed`, `post.task.failed` | Ships the `task-digest-collect` script, the `task-digest-daily` schedule (09:00 UTC), the `task-digest-report` workflow, and the `task-digest-guide` skill |
|
|
18
19
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# slack-reply-gate
|
|
2
|
+
|
|
3
|
+
Lead is woken by every human message in a Slack thread the swarm takes part in, including "lol", an emoji, or two people chatting with each other. This extension asks Jev, [TypeSafe](https://typesafe.ai)'s decision model, whether Lead should answer a new thread message, and drops the wake-up when it should not.
|
|
4
|
+
|
|
5
|
+
Install config:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{ "mode": "shadow" }
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Start in `shadow`, read the decisions in KV for a few days, then switch to `enforce`.
|
|
12
|
+
|
|
13
|
+
## Modes
|
|
14
|
+
|
|
15
|
+
| `mode` | Effect |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `off` | Never evaluates. |
|
|
18
|
+
| `shadow` (default) | Asks Jev and records the verdict. Never blocks. |
|
|
19
|
+
| `enforce` | Asks Jev, records the verdict, and blocks the Lead task when the answer is no. The message gets :mute: instead: no Lead task, no reply, no outcome card. |
|
|
20
|
+
|
|
21
|
+
## What it gates
|
|
22
|
+
|
|
23
|
+
Hook: `pre.task.create`, for tasks with origin `slack` whose description carries the thread buffer's header `[Thread follow-up — N message(s) buffered]`. Those are the non-mention thread replies that `src/slack/thread-buffer.ts` batches into a Lead task. A direct @mention of the bot creates a task without that header and is never gated.
|
|
24
|
+
|
|
25
|
+
The header counts only at the start of the description or right after the closing `</thread_context>`. Thread context quotes old messages verbatim, and an older Lead reply can quote the header itself, so a header inside the context is ignored. Only the text after the real header is treated as new.
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
|
|
29
|
+
1. A follow-up with downloaded files (status `draft`) passes without a Jev call.
|
|
30
|
+
2. **@mention pass-through.** A new message that @mentions Lead passes without a Jev call. A mention is the bot mention the engine rewrites to `<@U…> (that's you)`, a resolved `<@U…|lead>`, or a literal `@lead` (any name in `leadAliases`). Only the new messages are scanned, never the thread context, so an old mention earlier in the thread does not pass later banter.
|
|
31
|
+
3. Otherwise one Jev call (one `choice` question, see [Jev transport](#jev-transport)) with the new message(s), the sender kind, and the last `contextMessages` thread messages. Labels: `direct_ask`, `followup_to_lead`, `instruction_to_lead`, `reaction_only`, `banter_between_others`, `not_addressed_to_lead`, `other`.
|
|
32
|
+
4. The answer is no only when the top label is `reaction_only`, `banter_between_others`, or `not_addressed_to_lead`, and the summed probability of those three labels is at least `minConfidence`. A split between two skip labels is still a skip. An unsure verdict replies.
|
|
33
|
+
|
|
34
|
+
Several buffered messages are judged together. One mention in any of them passes the batch.
|
|
35
|
+
|
|
36
|
+
## Jev transport
|
|
37
|
+
|
|
38
|
+
Jev is reachable two ways. Both take the same request and return the same answer, so the decision is identical whichever one runs.
|
|
39
|
+
|
|
40
|
+
| `provider` | Endpoint | Key | Model |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `typesafe` | `POST https://api.typesafe.ai/v1/systemone` | `TYPESAFE_API_KEY` (or `apiKey`) | `model`, default `jev-latest` |
|
|
43
|
+
| `openrouter` | `POST https://openrouter.ai/api/alpha/decisions` | `OPENROUTER_API_KEY` | `openrouterModel`, default `typesafe/jev-1.13` |
|
|
44
|
+
| `auto` (default) | TypeSafe when its key is set, else OpenRouter | | |
|
|
45
|
+
|
|
46
|
+
Under `auto`, `TYPESAFE_API_KEY` wins when both keys are set. With neither key, every message fails open and the KV row names both secrets. Each decision row records the `provider` that answered.
|
|
47
|
+
|
|
48
|
+
## Fail open
|
|
49
|
+
|
|
50
|
+
Every error lets the task through: a missing key, a TypeSafe HTTP error, a network error, a timeout, a malformed answer, or an unknown label. The handler never throws, so an outage cannot auto-disable the extension. Each failure is recorded as `fail-open` in KV and logged as a warning.
|
|
51
|
+
|
|
52
|
+
## :mute: on drop (enforce)
|
|
53
|
+
|
|
54
|
+
No swarm tool or extension `ctx` method adds a Slack reaction, so the hook calls the Slack Web API with the bot token. It needs the `reactions:write` scope, which the Slack app already uses for its acceptance reactions.
|
|
55
|
+
|
|
56
|
+
1. `reactions.remove` for each acceptance reaction the engine may have put on the triggering message: `eyes`, `heavy_plus_sign`, `zap`, `speech_balloon`, or their `SLACK_REACTION_ACCEPTED`, `SLACK_REACTION_BUFFERED`, `SLACK_REACTION_NOW`, and `SLACK_REACTION_STEERED` overrides. Slack only removes the bot's own reaction, so a human's :eyes: stays.
|
|
57
|
+
2. `reactions.add` with `muteReaction` (default `mute`). `already_reacted` counts as success. Skipped when `muteReaction` is `""`.
|
|
58
|
+
|
|
59
|
+
The block returns first and the Slack calls run after it. A slow or failing Slack call never delays the drop or turns it back into a task. Set `muteReaction` to `""` for a true no-op: step 1 still clears the bot's acceptance reaction(s), and nothing is added.
|
|
60
|
+
|
|
61
|
+
## Config
|
|
62
|
+
|
|
63
|
+
| Key | Default | Notes |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `mode` | `shadow` | `off`, `shadow`, or `enforce` |
|
|
66
|
+
| `timeoutMs` | `3000` | Jev call timeout, capped at 4000 (the dispatcher cancels a handler at 5 s) |
|
|
67
|
+
| `provider` | `auto` | `auto`, `typesafe`, or `openrouter`. See [Jev transport](#jev-transport) |
|
|
68
|
+
| `model` | `jev-latest` | Jev model on TypeSafe |
|
|
69
|
+
| `openrouterModel` | `typesafe/jev-1.13` | Jev model on OpenRouter |
|
|
70
|
+
| `minConfidence` | `0.55` | Minimum summed skip probability to skip |
|
|
71
|
+
| `contextMessages` | `6` | Thread messages sent to Jev, 0 to 20 |
|
|
72
|
+
| `leadAliases` | `["lead"]` | A literal `@alias` in a new message counts as a mention |
|
|
73
|
+
| `botSlackUserIds` | `[]` | Slack user ids labelled `bot` as the sender for Jev |
|
|
74
|
+
| `apiKey` | unset | Explicit TypeSafe key. Prefer the `TYPESAFE_API_KEY` secret below |
|
|
75
|
+
| `muteReaction` | `mute` | Reaction for a dropped message; `""` only removes the acceptance reaction(s) |
|
|
76
|
+
| `slackTimeoutMs` | `5000` | Timeout per Slack call |
|
|
77
|
+
|
|
78
|
+
## Required secrets
|
|
79
|
+
|
|
80
|
+
All are read from the API process environment at call time. Set them as global `swarm_config` secrets: the API process loads global rows into `process.env` at boot and on config reload.
|
|
81
|
+
|
|
82
|
+
- `TYPESAFE_API_KEY` or `OPENROUTER_API_KEY`: one is required for any evaluation. Under `auto`, TypeSafe wins when both are set. Without either, every message fails open.
|
|
83
|
+
- `SLACK_BOT_TOKEN`: the Slack app's bot token. A swarm with Slack connected already has it. Without it, `enforce` still drops the message but cannot touch its reactions, and logs a `mute-error`.
|
|
84
|
+
|
|
85
|
+
No key is written to config, KV, logs, or a description.
|
|
86
|
+
|
|
87
|
+
## KV namespace `ext:slack-reply-gate`
|
|
88
|
+
|
|
89
|
+
- `d:<epochMs>:<channel>_<threadTs>`: one row per decision, with `action` (`pass`, `shadow-skip`, `skipped`, `fail-open`), `provider`, `reason`, `confidence`, `skipP`, `latencyMs`, `sender`, and a 300-character `preview`.
|
|
90
|
+
- `m:<epochMs>:<channel>_<threadTs>`: one row per mute attempt, with `action` (`muted`, `unreacted` when `muteReaction` is `""`, `mute-error`), `removed`, `added`, and `errors`.
|
|
91
|
+
- `last`: the latest decision. Mute rows never overwrite it.
|
|
92
|
+
- Counters: `total`, `mention`, `files`, `reply`, `shadow-skip`, `skipped`, `error`, `muted`, `unreacted`, `mute-error`, and `reason:<label>`.
|
|
93
|
+
|
|
94
|
+
## Known limits
|
|
95
|
+
|
|
96
|
+
- When Lead already has an active task in the thread, the buffer steers that task instead of creating one, so `pre.task.create` does not fire and the gate does not run.
|
|
97
|
+
- A blocked flush makes the thread buffer log a `TaskCreationBlockedError`. Nothing is lost: the next follow-up's `<thread_context>` carries the earlier messages.
|
|
98
|
+
- A flush of several messages mutes only the triggering (last) message. The others keep their acceptance reaction.
|