@desplega.ai/agent-swarm 2.5.2 → 2.5.3
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-csh0zz3b.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-6xx079d1.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-ft6r6v23.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-fkbyk66x.js} +2 -2
- package/dist/{cli-d172svcw.js → cli-1fc85mvt.js} +4 -4
- package/dist/{cli-3e9vs93g.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-vaxw3bg0.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-7ccf7akh.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-b1tv22wr.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-f55b6ky0.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-86zpfje5.js → cli-99xgw26r.js} +1 -1
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-h5d22pfs.js → cli-aecdcr77.js} +119 -282
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-15xd0j7w.js → cli-ce0ak9ms.js} +11 -11
- package/dist/{cli-y3dzwyy3.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-b6tnjdvx.js → cli-e8jyq7d8.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-bpbcpd8b.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-qrjv7t18.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-14v1sxxy.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-wadzapwb.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-jmcjbjkt.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-j3dwd4kz.js → cli-jjznk3f7.js} +718 -108
- package/dist/{cli-csh8fnbg.js → cli-kxawjrxg.js} +147 -28
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-0vgy5wfw.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-aaqnqpyw.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-e91vshb1.js → cli-p9rafdrb.js} +4 -4
- package/dist/{cli-edtm741g.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-ha22h0eg.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-xzge46sj.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-cyfgxnvb.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-4qkg9968.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-f3pfn4x3.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-kcnwn85g.js → cli-y561h5a9.js} +4 -4
- package/dist/{cli-n2918sed.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +16 -16
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-1cnvseqx.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-v5ytg5pw.js → db-24ck09xy.js} +1 -1
- package/dist/{db-4vq88jkc.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-trtcrqae.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-n9bymc2n.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-ptsraqa8.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ngtvh729.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-z8xy72gp.js → http-mxcqhhcs.js} +2757 -2448
- package/dist/{index-a9nsavhx.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-06qkmcx6.js → index-n1vbvta9.js} +17 -9
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-1cscynkb.js → index-spwxjgay.js} +17 -17
- package/dist/{index-r70q0xjx.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-0er16h2y.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-vk32s884.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-682k2mt5.js} +1 -1
- package/dist/{rbac-roles-452665y0.js → rbac-roles-gfn30fb8.js} +2 -2
- package/dist/{render-v2-0ttsk2j4.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3045 -2456
- package/package.json +4 -4
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +73 -15
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +29 -2
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +25 -5
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/swarm-config-guard.ts +23 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +22 -37
- package/src/http/sessions.ts +27 -2
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/chat-sdk-inbound.test.ts +20 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/sessions-tree.test.ts +168 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/types.ts +7 -0
- package/src/workflows/approval-notifications.ts +10 -3
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
|
@@ -83,6 +83,8 @@ const makeCtx = (
|
|
|
83
83
|
dbThrows?: boolean;
|
|
84
84
|
/** The caller's agents.isLead; null means no agents row. */
|
|
85
85
|
callerIsLead?: boolean | null;
|
|
86
|
+
/** user_external_ids x users rows for the mention lookup. */
|
|
87
|
+
addressees?: Record<string, unknown>[];
|
|
86
88
|
} = {},
|
|
87
89
|
) => {
|
|
88
90
|
const kv: Record<string, unknown> = {};
|
|
@@ -99,6 +101,10 @@ const makeCtx = (
|
|
|
99
101
|
db_query: async (args: any) => {
|
|
100
102
|
queries.push(args);
|
|
101
103
|
if (opts.dbThrows) throw new Error("db down");
|
|
104
|
+
if (String(args.sql).includes("user_external_ids")) {
|
|
105
|
+
const rows = (opts.addressees ?? []).filter((r) => args.params.includes(r.externalId));
|
|
106
|
+
return { success: true, data: { rows } };
|
|
107
|
+
}
|
|
102
108
|
if (String(args.sql).includes("FROM agents")) {
|
|
103
109
|
const lead = opts.callerIsLead ?? false;
|
|
104
110
|
return {
|
|
@@ -157,6 +163,8 @@ describe("defaults", () => {
|
|
|
157
163
|
timeoutMs: 4000,
|
|
158
164
|
maxBlocksPerTask: 1,
|
|
159
165
|
provider: "auto",
|
|
166
|
+
plainMode: "shadow",
|
|
167
|
+
plainThreshold: 0.25,
|
|
160
168
|
});
|
|
161
169
|
});
|
|
162
170
|
|
|
@@ -198,11 +206,17 @@ describe("pass", () => {
|
|
|
198
206
|
const call = jevCalls[0]!;
|
|
199
207
|
expect(call.url).toBe("https://api.typesafe.ai/v1/systemone");
|
|
200
208
|
expect(call.auth).toBe("Bearer tsk-test");
|
|
201
|
-
expect(Object.keys(call.body.questions).sort()).toEqual([
|
|
209
|
+
expect(Object.keys(call.body.questions).sort()).toEqual([
|
|
210
|
+
"language",
|
|
211
|
+
"plain",
|
|
212
|
+
"tone",
|
|
213
|
+
"verbosity",
|
|
214
|
+
]);
|
|
215
|
+
expect(call.body.state.plain_language_rules).toEqual(mod.PLAIN_RULES);
|
|
202
216
|
expect(call.body.state.latest_request).toBe("which option did you pick?");
|
|
203
217
|
});
|
|
204
218
|
|
|
205
|
-
test("non-Slack tasks, requesters without prefs, non-completions and other agents' tasks skip Jev", async () => {
|
|
219
|
+
test("with plain off: non-Slack tasks, requesters without prefs, non-completions and other agents' tasks skip Jev", async () => {
|
|
206
220
|
jevScores(0.1);
|
|
207
221
|
const cases: Array<[any, Row]> = [
|
|
208
222
|
[completion("x"), taskRow({ source: "mcp", slackThreadTs: null })],
|
|
@@ -220,7 +234,7 @@ describe("pass", () => {
|
|
|
220
234
|
[completion("x", { taskId: "not-a-uuid" }), taskRow()],
|
|
221
235
|
];
|
|
222
236
|
for (const [event, row] of cases) {
|
|
223
|
-
const { ctx } = makeCtx({ mode: "enforce" }, { row });
|
|
237
|
+
const { ctx } = makeCtx({ mode: "enforce", plainMode: "off" }, { row });
|
|
224
238
|
expect(await handler(event, ctx)).toBeUndefined();
|
|
225
239
|
}
|
|
226
240
|
expect(jevCalls).toHaveLength(0);
|
|
@@ -264,9 +278,9 @@ describe("pass", () => {
|
|
|
264
278
|
expect(queries).toHaveLength(1);
|
|
265
279
|
});
|
|
266
280
|
|
|
267
|
-
test("mode off never
|
|
281
|
+
test("mode off and plainMode off never read the task", async () => {
|
|
268
282
|
jevScores(0.1);
|
|
269
|
-
const { ctx, queries } = makeCtx({ mode: "off" });
|
|
283
|
+
const { ctx, queries } = makeCtx({ mode: "off", plainMode: "off" });
|
|
270
284
|
expect(await handler(completion("x"), ctx)).toBeUndefined();
|
|
271
285
|
expect(queries).toHaveLength(0);
|
|
272
286
|
});
|
|
@@ -387,13 +401,12 @@ describe("fail open", () => {
|
|
|
387
401
|
|
|
388
402
|
describe("self-test", () => {
|
|
389
403
|
test("records both controls without judging real traffic", async () => {
|
|
390
|
-
mockJev((body) =>
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
);
|
|
404
|
+
mockJev((body) => {
|
|
405
|
+
const msg = String(body.state.agent_message);
|
|
406
|
+
const bad = msg.startsWith("Great") || msg.startsWith("Prompt cache monitor");
|
|
407
|
+
const [dim] = Object.keys(body.questions);
|
|
408
|
+
return Response.json({ answers: { [dim!]: { noul: bad ? 0.1 : 0.9 } } });
|
|
409
|
+
});
|
|
397
410
|
const { ctx, kv, queries, counters } = makeCtx();
|
|
398
411
|
const event = {
|
|
399
412
|
tool: "store-progress",
|
|
@@ -405,9 +418,305 @@ describe("self-test", () => {
|
|
|
405
418
|
positiveControlBadScore: 0.1,
|
|
406
419
|
negativeControlGoodScore: 0.9,
|
|
407
420
|
controlsOk: true,
|
|
421
|
+
plainPositiveControlBadScore: 0.1,
|
|
422
|
+
plainNegativeControlGoodScore: 0.9,
|
|
423
|
+
plainControlsOk: true,
|
|
408
424
|
provider: "typesafe",
|
|
409
425
|
});
|
|
426
|
+
// Two verbosity calls and two plain calls, each asking one question.
|
|
427
|
+
expect(jevCalls.map((c) => Object.keys(c.body.questions)[0]).sort()).toEqual([
|
|
428
|
+
"plain",
|
|
429
|
+
"plain",
|
|
430
|
+
"verbosity",
|
|
431
|
+
"verbosity",
|
|
432
|
+
]);
|
|
410
433
|
expect(queries).toHaveLength(0);
|
|
411
434
|
expect(counters.total).toBeUndefined();
|
|
412
435
|
});
|
|
413
436
|
});
|
|
437
|
+
|
|
438
|
+
describe("plain", () => {
|
|
439
|
+
const slackReply = (message: string, extra: Record<string, unknown> = {}) => ({
|
|
440
|
+
tool: "slack-reply",
|
|
441
|
+
args: { taskId: TASK_ID, message, ...extra },
|
|
442
|
+
requestInfo: { callOrigin: "mcp", agentId: "agent-1" },
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("a requester without prefs still gets the plain question", async () => {
|
|
446
|
+
jevScores(0.9);
|
|
447
|
+
const { ctx, counters } = makeCtx({}, { row: taskRow({ comms: null }) });
|
|
448
|
+
expect(await handler(completion("Picked B."), ctx)).toBeUndefined();
|
|
449
|
+
expect(Object.keys(jevCalls[0]!.body.questions)).toEqual(["plain"]);
|
|
450
|
+
expect(counters).toMatchObject({ total: 1, pass: 1 });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("mode off with plainMode shadow asks only plain", async () => {
|
|
454
|
+
jevScores(0.9);
|
|
455
|
+
const { ctx } = makeCtx({ mode: "off" });
|
|
456
|
+
await handler(completion("Picked B."), ctx);
|
|
457
|
+
expect(Object.keys(jevCalls[0]!.body.questions)).toEqual(["plain"]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("plain uses plainThreshold, not threshold", async () => {
|
|
461
|
+
jevScores({ plain: 0.3 });
|
|
462
|
+
const passing = makeCtx({ mode: "enforce", plainMode: "enforce", threshold: 0.75 });
|
|
463
|
+
expect(await handler(completion("Picked B."), passing.ctx)).toBeUndefined();
|
|
464
|
+
expect(passing.counters).toMatchObject({ pass: 1 });
|
|
465
|
+
|
|
466
|
+
jevScores({ plain: 0.2 });
|
|
467
|
+
const failing = makeCtx({ mode: "enforce", plainMode: "enforce", threshold: 0.75 });
|
|
468
|
+
const result = await handler(completion("p50 Δ +120ms (n=37)"), failing.ctx);
|
|
469
|
+
expect(result).toMatchObject({ action: "block" });
|
|
470
|
+
expect(result.reason).toContain("plain (0.20 vs 0.25)");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("plainMode shadow records a plain failure and never blocks, even with mode enforce", async () => {
|
|
474
|
+
jevScores({ plain: 0.05 });
|
|
475
|
+
const { ctx, counters, kv } = makeCtx({ mode: "enforce" });
|
|
476
|
+
expect(await handler(completion("p50 Δ +120ms (n=37)"), ctx)).toBeUndefined();
|
|
477
|
+
expect(counters).toMatchObject({ fail: 1, shadowed: 1, "dim:plain": 1 });
|
|
478
|
+
expect(counters.blocked).toBeUndefined();
|
|
479
|
+
expect(kv.last).toMatchObject({ failed: ["plain"], plainMode: "shadow" });
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("plainMode enforce blocks and points the agent at the comms skill", async () => {
|
|
483
|
+
jevScores({ plain: 0.05 });
|
|
484
|
+
const { ctx, counters } = makeCtx({ mode: "shadow", plainMode: "enforce" });
|
|
485
|
+
const result = await handler(completion("p50 Δ +120ms (n=37)"), ctx);
|
|
486
|
+
expect(result).toMatchObject({ action: "block" });
|
|
487
|
+
expect(result.reason).toContain("plain (0.05 vs 0.25)");
|
|
488
|
+
expect(result.reason).toContain("`comms` skill (Simpler mode)");
|
|
489
|
+
expect(result.reason).toContain("Keep every number, name, link and hedge verbatim");
|
|
490
|
+
expect(result.reason).toContain("call store-progress again");
|
|
491
|
+
expect(counters).toMatchObject({ blocked: 1, "dim:plain": 1 });
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("a failing preference in shadow is left out of a plain block", async () => {
|
|
495
|
+
jevScores({ plain: 0.05, verbosity: 0.1 });
|
|
496
|
+
const { ctx } = makeCtx({ mode: "shadow", plainMode: "enforce" });
|
|
497
|
+
const result = await handler(completion("x"), ctx);
|
|
498
|
+
expect(result.reason).toContain("plain (");
|
|
499
|
+
expect(result.reason).not.toContain("verbosity (");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("structured JSON output skips the plain question", async () => {
|
|
503
|
+
jevScores(0.9);
|
|
504
|
+
const { ctx } = makeCtx({ mode: "enforce", plainMode: "enforce" });
|
|
505
|
+
await handler(completion('{"prUrl":"https://example.com/pull/1","n":3}'), ctx);
|
|
506
|
+
expect(Object.keys(jevCalls[0]!.body.questions)).not.toContain("plain");
|
|
507
|
+
expect(mod.looksLikeJson("{not json")).toBe(false);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("a JSON message on any Slack tool is still judged plain, even with mode off", async () => {
|
|
511
|
+
const json = '{"p50":"+120ms","n":37}';
|
|
512
|
+
for (const tool of ["slack-reply", "slack-post", "slack-start-thread", "slack-update"]) {
|
|
513
|
+
jevCalls = [];
|
|
514
|
+
jevScores({ plain: 0.05 });
|
|
515
|
+
const { ctx } = makeCtx({ mode: "off", plainMode: "enforce" });
|
|
516
|
+
const result = await handler(
|
|
517
|
+
{
|
|
518
|
+
tool,
|
|
519
|
+
args: { taskId: TASK_ID, channelId: "C1", message: json },
|
|
520
|
+
requestInfo: { callOrigin: "mcp", agentId: "agent-1", sourceTaskId: TASK_ID },
|
|
521
|
+
},
|
|
522
|
+
ctx,
|
|
523
|
+
);
|
|
524
|
+
expect(jevCalls).toHaveLength(1);
|
|
525
|
+
expect(Object.keys(jevCalls[0]!.body.questions)).toEqual(["plain"]);
|
|
526
|
+
expect(result).toMatchObject({ action: "block" });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("slack-reply is judged on its message with the named task", async () => {
|
|
531
|
+
jevScores({ plain: 0.05 });
|
|
532
|
+
const { ctx, queries } = makeCtx({ plainMode: "enforce" });
|
|
533
|
+
const result = await handler(slackReply("p50 Δ +120ms (n=37)"), ctx);
|
|
534
|
+
expect(result).toMatchObject({ action: "block" });
|
|
535
|
+
expect(result.reason).toContain("call slack-reply again");
|
|
536
|
+
expect(queries[0].params).toEqual([TASK_ID]);
|
|
537
|
+
expect(jevCalls[0]!.body.state.agent_message).toBe("p50 Δ +120ms (n=37)");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("slack-post, slack-start-thread and slack-update use the caller's task context", async () => {
|
|
541
|
+
for (const tool of ["slack-post", "slack-start-thread", "slack-update"]) {
|
|
542
|
+
jevCalls = [];
|
|
543
|
+
jevScores(0.9);
|
|
544
|
+
const { ctx, queries } = makeCtx();
|
|
545
|
+
await handler(
|
|
546
|
+
{
|
|
547
|
+
tool,
|
|
548
|
+
args: { channelId: "C1", message: "Deploys take 4 minutes now." },
|
|
549
|
+
requestInfo: { callOrigin: "mcp", agentId: "agent-1", sourceTaskId: TASK_ID },
|
|
550
|
+
},
|
|
551
|
+
ctx,
|
|
552
|
+
);
|
|
553
|
+
expect(queries[0].params).toEqual([TASK_ID]);
|
|
554
|
+
expect(jevCalls).toHaveLength(1);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("a Slack post from a non-Slack task is still judged", async () => {
|
|
559
|
+
jevScores(0.9);
|
|
560
|
+
const { ctx } = makeCtx({}, { row: taskRow({ source: "mcp", slackThreadTs: null }) });
|
|
561
|
+
await handler(slackReply("Deploys take 4 minutes now."), ctx);
|
|
562
|
+
expect(jevCalls).toHaveLength(1);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("a Slack post without a task, or on another agent's task, skips Jev", async () => {
|
|
566
|
+
jevScores(0.05);
|
|
567
|
+
const noTask = makeCtx({ plainMode: "enforce" });
|
|
568
|
+
expect(
|
|
569
|
+
await handler(
|
|
570
|
+
{
|
|
571
|
+
tool: "slack-post",
|
|
572
|
+
args: { channelId: "C1", message: "x" },
|
|
573
|
+
requestInfo: { callOrigin: "mcp", agentId: "agent-1" },
|
|
574
|
+
},
|
|
575
|
+
noTask.ctx,
|
|
576
|
+
),
|
|
577
|
+
).toBeUndefined();
|
|
578
|
+
const other = makeCtx({ plainMode: "enforce" }, { row: taskRow({ agentId: "agent-2" }) });
|
|
579
|
+
expect(await handler(slackReply("x"), other.ctx)).toBeUndefined();
|
|
580
|
+
expect(jevCalls).toHaveLength(0);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("a Slack post on an unassigned task the caller did not create skips Jev", async () => {
|
|
584
|
+
// slack-reply itself rejects this task, so nothing from it may reach the judge,
|
|
585
|
+
// even though store-progress may write to an unassigned task.
|
|
586
|
+
jevScores(0.05);
|
|
587
|
+
const unassigned = makeCtx({ plainMode: "enforce" }, { row: taskRow({ agentId: null }) });
|
|
588
|
+
expect(await handler(slackReply("x"), unassigned.ctx)).toBeUndefined();
|
|
589
|
+
const lead = makeCtx(
|
|
590
|
+
{ plainMode: "enforce" },
|
|
591
|
+
{ row: taskRow({ agentId: "agent-2" }), callerIsLead: true },
|
|
592
|
+
);
|
|
593
|
+
expect(await handler(slackReply("x"), lead.ctx)).toBeUndefined();
|
|
594
|
+
expect(jevCalls).toHaveLength(0);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("the task's creator may post and is judged", async () => {
|
|
598
|
+
jevScores(0.9);
|
|
599
|
+
const { ctx } = makeCtx(
|
|
600
|
+
{},
|
|
601
|
+
{ row: taskRow({ agentId: "agent-2", creatorAgentId: "agent-1" }) },
|
|
602
|
+
);
|
|
603
|
+
await handler(slackReply("Deploys take 4 minutes now."), ctx);
|
|
604
|
+
expect(jevCalls).toHaveLength(1);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("store-progress and slack-reply share one block budget per task", async () => {
|
|
608
|
+
jevScores({ plain: 0.05 });
|
|
609
|
+
const { ctx, counters } = makeCtx({ plainMode: "enforce" });
|
|
610
|
+
expect(await handler(slackReply("p50 Δ"), ctx)).toMatchObject({ action: "block" });
|
|
611
|
+
expect(await handler(completion("p50 Δ"), ctx)).toBeUndefined();
|
|
612
|
+
expect(counters).toMatchObject({ blocked: 1, escaped: 1 });
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("a script SDK call is recorded and never blocked", async () => {
|
|
616
|
+
jevScores({ plain: 0.05 });
|
|
617
|
+
const { ctx, counters } = makeCtx({ mode: "enforce", plainMode: "enforce" });
|
|
618
|
+
const event = {
|
|
619
|
+
...slackReply("p50 Δ"),
|
|
620
|
+
requestInfo: { callOrigin: "script-sdk", agentId: "agent-1" },
|
|
621
|
+
};
|
|
622
|
+
expect(await handler(event, ctx)).toBeUndefined();
|
|
623
|
+
expect(counters).toMatchObject({ fail: 1, shadowed: 1 });
|
|
624
|
+
expect(counters.blocked).toBeUndefined();
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
describe("multi-person threads", () => {
|
|
629
|
+
const BOB_PREFS = { language: "Spanish", tone: "warm" };
|
|
630
|
+
const bob = { externalId: "UBOB", id: "user-2", name: "Bob", comms: JSON.stringify(BOB_PREFS) };
|
|
631
|
+
const post = (message: string) => ({
|
|
632
|
+
tool: "slack-reply",
|
|
633
|
+
args: { taskId: TASK_ID, message },
|
|
634
|
+
requestInfo: { callOrigin: "mcp", agentId: "agent-1" },
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("mentionedSlackIds keeps first-appearance order and drops duplicates", () => {
|
|
638
|
+
expect(mod.mentionedSlackIds("<@UBOB|bob> and <@U1> then <@UBOB>")).toEqual(["UBOB", "U1"]);
|
|
639
|
+
expect(mod.mentionedSlackIds("no mentions, @bob")).toEqual([]);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("a message that mentions another person is judged against that person's prefs", async () => {
|
|
643
|
+
jevScores({ language: 0.1 });
|
|
644
|
+
const { ctx, kv } = makeCtx({ mode: "enforce" }, { addressees: [bob] });
|
|
645
|
+
const result = await handler(post("<@UBOB> hola, el deploy tarda 4 minutos."), ctx);
|
|
646
|
+
const state = jevCalls[0]!.body.state;
|
|
647
|
+
expect(state.requester).toBe("Bob");
|
|
648
|
+
expect(state.requester_preferences).toEqual(BOB_PREFS);
|
|
649
|
+
expect(Object.keys(jevCalls[0]!.body.questions).sort()).toEqual(["language", "plain", "tone"]);
|
|
650
|
+
expect(result).toMatchObject({ action: "block" });
|
|
651
|
+
expect(result.reason).toContain("Bob's stated preferences");
|
|
652
|
+
expect(kv.last).toMatchObject({
|
|
653
|
+
judgedFor: "mention",
|
|
654
|
+
audienceId: "user-2",
|
|
655
|
+
requester: "Alice",
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("the first mentioned person with prefs wins; one without prefs is skipped", async () => {
|
|
660
|
+
jevScores(0.9);
|
|
661
|
+
const carol = { externalId: "UCAROL", id: "user-3", name: "Carol", comms: null };
|
|
662
|
+
const { ctx } = makeCtx({}, { addressees: [carol, bob] });
|
|
663
|
+
await handler(post("<@UCAROL> <@UBOB> deploys take 4 minutes."), ctx);
|
|
664
|
+
expect(jevCalls[0]!.body.state.requester).toBe("Bob");
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("no mention, or an unknown one, falls back to the requester", async () => {
|
|
668
|
+
for (const message of ["Deploys take 4 minutes.", "<@UNOBODY> deploys take 4 minutes."]) {
|
|
669
|
+
jevCalls = [];
|
|
670
|
+
jevScores(0.9);
|
|
671
|
+
const { ctx, kv } = makeCtx({}, { addressees: [bob] });
|
|
672
|
+
await handler(post(message), ctx);
|
|
673
|
+
expect(jevCalls[0]!.body.state.requester_preferences).toEqual(PREFS);
|
|
674
|
+
expect(kv.last).toMatchObject({ judgedFor: "requester", audience: "Alice" });
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("mentioning the requester keeps the requester's prefs", async () => {
|
|
679
|
+
jevScores(0.9);
|
|
680
|
+
const alice = {
|
|
681
|
+
externalId: "U1",
|
|
682
|
+
id: "user-1",
|
|
683
|
+
name: "Alice",
|
|
684
|
+
comms: JSON.stringify(BOB_PREFS),
|
|
685
|
+
};
|
|
686
|
+
const { ctx } = makeCtx({}, { addressees: [alice] });
|
|
687
|
+
await handler(post("<@U1> deploys take 4 minutes."), ctx);
|
|
688
|
+
expect(jevCalls[0]!.body.state.requester_preferences).toEqual(PREFS);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test.each([
|
|
692
|
+
null,
|
|
693
|
+
"{}",
|
|
694
|
+
])("a self-mention without requester prefs (%s) skips to the next mentioned person with prefs", async (comms) => {
|
|
695
|
+
jevScores(0.9);
|
|
696
|
+
const alice = { externalId: "U1", id: "user-1", name: "Alice", comms };
|
|
697
|
+
const { ctx, kv } = makeCtx(
|
|
698
|
+
{ mode: "enforce", plainMode: "off" },
|
|
699
|
+
{ row: taskRow({ comms }), addressees: [alice, bob] },
|
|
700
|
+
);
|
|
701
|
+
await handler(post("<@U1> <@UBOB> deploys take 4 minutes."), ctx);
|
|
702
|
+
expect(jevCalls).toHaveLength(1);
|
|
703
|
+
expect(jevCalls[0]!.body.state.requester).toBe("Bob");
|
|
704
|
+
expect(jevCalls[0]!.body.state.requester_preferences).toEqual(BOB_PREFS);
|
|
705
|
+
expect(kv.last).toMatchObject({ judgedFor: "mention", audienceId: "user-2" });
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("a completion that mentions someone is judged for them too", async () => {
|
|
709
|
+
jevScores(0.9);
|
|
710
|
+
const { ctx } = makeCtx({}, { addressees: [bob] });
|
|
711
|
+
await handler(completion("<@UBOB> done, PR #12."), ctx);
|
|
712
|
+
expect(jevCalls[0]!.body.state.requester).toBe("Bob");
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("with mode off, plain alone skips the mention lookup", async () => {
|
|
716
|
+
jevScores(0.9);
|
|
717
|
+
const { ctx, queries } = makeCtx({ mode: "off" }, { addressees: [bob] });
|
|
718
|
+
await handler(post("<@UBOB> deploys take 4 minutes."), ctx);
|
|
719
|
+
expect(queries.some((q) => String(q.sql).includes("user_external_ids"))).toBe(false);
|
|
720
|
+
expect(jevCalls).toHaveLength(1);
|
|
721
|
+
});
|
|
722
|
+
});
|
|
@@ -266,6 +266,39 @@ describe("list-endpoint slimming", () => {
|
|
|
266
266
|
}
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
+
test("getAllTasks — the preview starts after a leading sibling block", async () => {
|
|
270
|
+
// `prependSiblingBlock` output: a long block, a blank line, then the ask.
|
|
271
|
+
const agentId = "preview-sibling-agent";
|
|
272
|
+
const sibling = (n: number) =>
|
|
273
|
+
`- [in_progress] task:${n} — agent:Picateclas — started 2m ago, last active 10s ago\n "${"é".repeat(190)}"`;
|
|
274
|
+
const block = `<sibling_tasks_in_progress>\nThe following tasks are already running in the same context (contextKey: slack:C1:1.2). The user has submitted new input while they were in flight — coordinate with them, do not duplicate:\n\n${[1, 2, 3].map(sibling).join("\n")}\n</sibling_tasks_in_progress>`;
|
|
275
|
+
const ask = `also bump the version ${"v".repeat(400)}`;
|
|
276
|
+
const texts = [
|
|
277
|
+
`${block}\n\n${ask}`,
|
|
278
|
+
`${block}\nno blank line`,
|
|
279
|
+
// No closing tag: nothing to skip.
|
|
280
|
+
`<sibling_tasks_in_progress>\n${"x".repeat(400)}`,
|
|
281
|
+
// Not at the start: nothing to skip.
|
|
282
|
+
`lead in\n\n${block}\n\nask`,
|
|
283
|
+
];
|
|
284
|
+
for (const text of texts) await createTaskExtended(text, { agentId });
|
|
285
|
+
|
|
286
|
+
const expected = [
|
|
287
|
+
`${ask.slice(0, 300)}…`,
|
|
288
|
+
"\nno blank line",
|
|
289
|
+
`<sibling_tasks_in_progress>\n${"x".repeat(272)}…`,
|
|
290
|
+
`${`lead in\n\n${block}\n\nask`.slice(0, 300)}…`,
|
|
291
|
+
].sort();
|
|
292
|
+
for (const opts of [{ slim: true }, { fields: "timeline" as const }]) {
|
|
293
|
+
const rows = await getAllTasks({ agentId, limit: 50 }, opts);
|
|
294
|
+
expect(rows.map((t) => t.task).sort()).toEqual(expected);
|
|
295
|
+
}
|
|
296
|
+
// Session roots map the full text, so the JS skip must agree with the SQL one.
|
|
297
|
+
const sessions = await listRecentSessions({ limit: 200, slim: true });
|
|
298
|
+
const roots = sessions.filter((s) => s.root.agentId === agentId).map((s) => s.root.task);
|
|
299
|
+
expect(roots.sort()).toEqual(expected);
|
|
300
|
+
});
|
|
301
|
+
|
|
269
302
|
test("getAllTasks — slim and full pages hold the same rows in the same order", async () => {
|
|
270
303
|
for (let i = 0; i < 12; i++) {
|
|
271
304
|
await createTaskExtended(`page-parity ${i}`, { agentId: "slim-agent-1" });
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
test("migration 240 hands each legacy child to the attempt that owns it now", async () => {
|
|
5
|
+
const db = new Database(":memory:");
|
|
6
|
+
try {
|
|
7
|
+
db.exec("CREATE TABLE workflow_runs (id TEXT PRIMARY KEY, workflowId TEXT, status TEXT)");
|
|
8
|
+
db.exec(
|
|
9
|
+
`CREATE TABLE workflow_run_steps (
|
|
10
|
+
id TEXT PRIMARY KEY, status TEXT, retryCount INTEGER NOT NULL DEFAULT 0, nextRetryAt TEXT
|
|
11
|
+
)`,
|
|
12
|
+
);
|
|
13
|
+
db.exec(await Bun.file("src/be/migrations/203_workflow_run_parent_step.sql").text());
|
|
14
|
+
const step = db.query(
|
|
15
|
+
"INSERT INTO workflow_run_steps (id, status, retryCount, nextRetryAt) VALUES (?, ?, ?, ?)",
|
|
16
|
+
);
|
|
17
|
+
// Created on attempt 1 (attempt 0 failed before any child existed) and parked.
|
|
18
|
+
step.run("parked-retry", "waiting", 1, null);
|
|
19
|
+
// Its child failed; attempt 2 is queued and must start a new child.
|
|
20
|
+
step.run("queued-retry", "failed", 2, "2030-01-01T00:00:00.000Z");
|
|
21
|
+
step.run("first-attempt", "completed", 0, null);
|
|
22
|
+
step.run("exhausted", "failed", 3, null);
|
|
23
|
+
const run = db.query(
|
|
24
|
+
"INSERT INTO workflow_runs (id, workflowId, status, parentStepId) VALUES (?, 'wf', ?, ?)",
|
|
25
|
+
);
|
|
26
|
+
run.run("child-parked", "waiting", "parked-retry");
|
|
27
|
+
run.run("child-queued", "failed", "queued-retry");
|
|
28
|
+
run.run("child-first", "completed", "first-attempt");
|
|
29
|
+
run.run("child-exhausted", "failed", "exhausted");
|
|
30
|
+
run.run("child-orphan", "completed", "missing-step");
|
|
31
|
+
run.run("top-level", "completed", null);
|
|
32
|
+
|
|
33
|
+
db.exec(await Bun.file("src/be/migrations/240_workflow_run_parent_step_attempt.sql").text());
|
|
34
|
+
|
|
35
|
+
expect(
|
|
36
|
+
db.prepare("SELECT id, parentStepAttempt AS attempt FROM workflow_runs ORDER BY id").all(),
|
|
37
|
+
).toEqual([
|
|
38
|
+
{ id: "child-exhausted", attempt: 3 },
|
|
39
|
+
{ id: "child-first", attempt: 0 },
|
|
40
|
+
{ id: "child-orphan", attempt: 0 },
|
|
41
|
+
{ id: "child-parked", attempt: 1 },
|
|
42
|
+
{ id: "child-queued", attempt: 1 },
|
|
43
|
+
{ id: "top-level", attempt: 0 },
|
|
44
|
+
]);
|
|
45
|
+
// One child per attempt: a second child for the parked attempt is rejected,
|
|
46
|
+
// one for the queued attempt is the new invocation.
|
|
47
|
+
const insert = db.query(
|
|
48
|
+
"INSERT INTO workflow_runs (id, workflowId, status, parentStepId, parentStepAttempt) VALUES (?, 'wf', 'running', ?, ?)",
|
|
49
|
+
);
|
|
50
|
+
expect(() => insert.run("dup", "parked-retry", 1)).toThrow();
|
|
51
|
+
insert.run("next", "queued-retry", 2);
|
|
52
|
+
} finally {
|
|
53
|
+
db.close();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -642,6 +642,12 @@ describe("script connections", () => {
|
|
|
642
642
|
markMigrationApplied(database, "235_chat_relay_defer.sql");
|
|
643
643
|
// 237 reads approval_requests and inbox_item_state, which this migration-112-only fixture does not create.
|
|
644
644
|
markMigrationApplied(database, "237_session_inbox_cleanup.sql");
|
|
645
|
+
// 239 indexes skills, which this migration-112-only fixture does not create.
|
|
646
|
+
markMigrationApplied(database, "239_skills_fts.sql");
|
|
647
|
+
// 240 alters workflow_runs, which this migration-112-only fixture does not create.
|
|
648
|
+
markMigrationApplied(database, "240_workflow_run_parent_step_attempt.sql");
|
|
649
|
+
// 241 indexes skills, which this migration-112-only fixture does not create.
|
|
650
|
+
markMigrationApplied(database, "241_skills_meta_fts.sql");
|
|
645
651
|
|
|
646
652
|
const id = crypto.randomUUID();
|
|
647
653
|
const now = new Date().toISOString();
|