@desplega.ai/agent-swarm 2.4.0 → 2.4.1
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/README.md +1 -1
- package/dist/{acp-adapter-kedstxx6.js → acp-adapter-zvzr538d.js} +9 -9
- package/dist/{actions-vget6r1k.js → actions-jhtqdw69.js} +21 -21
- package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-bd84rhs2.js} +4 -4
- package/dist/{app-mqap8t8m.js → app-kreq1gwg.js} +3 -3
- package/dist/{assistant-h0gv97mv.js → assistant-3k22sbsq.js} +22 -22
- package/dist/{boot-reembed-qr8a894z.js → boot-reembed-e53x6b9v.js} +1 -1
- package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-f6z923ny.js} +1 -1
- package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-0538vsb7.js} +4 -4
- package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-6wxqkfk2.js} +4 -4
- package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gfts5044.js} +9 -9
- package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
- package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
- package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-56besmfz.js} +13 -13
- package/dist/{cli-nrrg0fvy.js → cli-0bh477zn.js} +18 -5
- package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
- package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
- package/dist/{cli-1cevz46c.js → cli-3kh4kpqy.js} +6 -6
- package/dist/{cli-tg9ksbn8.js → cli-3p8fqcnh.js} +2 -2
- package/dist/{cli-x1p5dhq2.js → cli-3r448dqn.js} +3 -3
- package/dist/{cli-ecshf504.js → cli-3tnpj2bh.js} +7 -7
- package/dist/{cli-ffbrkh1r.js → cli-471qk6s2.js} +46 -38
- package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
- package/dist/{cli-zvze9rbm.js → cli-6ncjvx55.js} +3 -3
- package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
- package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
- package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
- package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
- package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
- package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
- package/dist/{cli-mz1ezzzh.js → cli-ash6xxk7.js} +2 -2
- package/dist/{cli-rsd7ga7v.js → cli-axh29tyr.js} +11 -11
- package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
- package/dist/{cli-6ftrrwm1.js → cli-bn5kevnf.js} +5 -5
- package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
- package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
- package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
- package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
- package/dist/{cli-8cxkw1f5.js → cli-fz9kewbh.js} +2 -2
- package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
- package/dist/{cli-kz116kvw.js → cli-gf0kfg81.js} +7 -7
- package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
- package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
- package/dist/{cli-ywyrffff.js → cli-jh0qcrga.js} +1 -1
- package/dist/{cli-ttaz6c7m.js → cli-k9z4zd4p.js} +6 -5
- package/dist/{cli-nme7wery.js → cli-kmqgmcnb.js} +46 -13
- package/dist/{cli-x719j3vs.js → cli-kr2pmfqx.js} +392 -168
- package/dist/{cli-wrv09v7q.js → cli-m2qgh5sv.js} +1 -1
- package/dist/{cli-xfe63zx3.js → cli-mfe7f7b5.js} +2 -2
- package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
- package/dist/{cli-5s5hx57y.js → cli-nx462e4v.js} +1 -1
- package/dist/{cli-bfc3k1x5.js → cli-p0rakbrk.js} +9 -9
- package/dist/{cli-avmqhatn.js → cli-p6argxh9.js} +8 -8
- package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
- package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
- package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
- package/dist/{cli-bhwa8rzc.js → cli-rdq7tzwm.js} +13 -12
- package/dist/{cli-rg7ntypm.js → cli-rqqsfz30.js} +379 -87
- package/dist/{cli-kk2r80n5.js → cli-s48yvzb8.js} +4 -4
- package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
- package/dist/{cli-b80a1100.js → cli-sjyetj21.js} +2 -2
- package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
- package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
- package/dist/{cli-5ff2sjyt.js → cli-syhq378v.js} +2 -2
- package/dist/{cli-tj4j2ts4.js → cli-tbhwrxpt.js} +8 -8
- package/dist/{cli-4fhyq711.js → cli-vy0fay6k.js} +13 -13
- package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
- package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
- package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
- package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
- package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
- package/dist/{cli-tgvpmnwz.js → cli-xftrvxag.js} +5 -5
- package/dist/{cli-02vhhssk.js → cli-z071fb95.js} +5 -5
- package/dist/{cli-mvg2hf8p.js → cli-z1ew8vbq.js} +14 -7
- package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
- package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-p6rmm7vv.js} +6 -6
- package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
- package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
- package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-8vd5t9t0.js} +7 -7
- package/dist/{commands-psqhkttb.js → commands-7ems2p13.js} +2 -2
- package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
- package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-bg33ae53.js} +23 -23
- package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
- package/dist/{db-2xbyenjr.js → db-w505dwa8.js} +2 -2
- package/dist/{db-nc1rd869.js → db-ys2rbdft.js} +3 -3
- package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
- package/dist/{dispatcher-ewceb8ts.js → dispatcher-0s0mckqr.js} +20 -20
- package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
- package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
- package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
- package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
- package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
- package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
- package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
- package/dist/{external-channel-a5dsmxn4.js → external-channel-5b23fv8z.js} +2 -2
- package/dist/{grok-adapter-vj88xr23.js → grok-adapter-q9fghe5q.js} +10 -10
- package/dist/handlers-mqrym347.js +39 -0
- package/dist/{heartbeat-t2m9r3wa.js → heartbeat-tww6yx1w.js} +23 -23
- package/dist/{hook-xa677f8r.js → hook-dx0pkvar.js} +16 -9
- package/dist/{hook-8femx1bg.js → hook-pvmh9b6m.js} +6 -6
- package/dist/{http-xpmmq028.js → http-k2ejbt05.js} +199 -131
- package/dist/{index-s7ckdajq.js → index-2z710v4x.js} +10 -10
- package/dist/{index-dg2tmj99.js → index-dgz61nff.js} +20 -20
- package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
- package/dist/index-h2ek08d6.js +40 -0
- package/dist/index-pqfwh9nn.js +41 -0
- package/dist/{index-cdzyfrsp.js → index-x5aktmp8.js} +1 -1
- package/dist/{keepalive-3nr4st3r.js → keepalive-nhvpw10z.js} +4 -4
- package/dist/lead-jb6sf69z.js +78 -0
- package/dist/{maintenance-verwpe2c.js → maintenance-agakc519.js} +1 -1
- package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-4dcbhtn0.js} +3 -3
- package/dist/{onboard-aytqedmq.js → onboard-4n2kwa4v.js} +3 -3
- package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
- package/dist/{otel-impl-hckw4wt1.js → otel-impl-tfa12080.js} +2 -2
- package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
- package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-3cqx3f3x.js} +3 -3
- package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-9fae5rnv.js} +1 -1
- package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-y6zbfzve.js} +2 -2
- package/dist/{render-v2-7nqgvk2q.js → render-v2-emawjvc4.js} +3 -3
- package/dist/{resume-b6djcj3c.js → resume-zhgts7h7.js} +10 -8
- package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
- package/dist/{session-access-fc3f9y12.js → session-access-cm823e06.js} +4 -4
- package/dist/{setup-n39szm6b.js → setup-tkp99w1e.js} +1 -1
- package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
- package/dist/{work-objects-jem2fre0.js → work-objects-f722h263.js} +23 -23
- package/dist/worker-p7f336rq.js +78 -0
- package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
- package/openapi.json +99 -52
- package/package.json +2 -2
- package/src/apps/action-runner.ts +146 -0
- package/src/apps/row-store.ts +4 -2
- package/src/apps/script-types.ts +1 -1
- package/src/be/db.ts +14 -4
- package/src/be/fleet-benchmark-stats.ts +10 -5
- package/src/be/models-catalog.ts +4 -0
- package/src/be/onboarding.ts +6 -0
- package/src/be/scripts/typecheck.ts +13 -0
- package/src/be/swarm-config-guard.ts +10 -1
- package/src/be/telemetry-enable.ts +1 -1
- package/src/commands/provider-credentials.ts +12 -2
- package/src/commands/runner.ts +3 -0
- package/src/heartbeat/heartbeat.ts +2 -2
- package/src/hooks/codex-hook.ts +6 -2
- package/src/hooks/hook.ts +7 -0
- package/src/hooks/pr-body-guard.ts +38 -11
- package/src/hooks/pr-room-guard.ts +88 -0
- package/src/http/agents.ts +1 -0
- package/src/http/apps.ts +84 -181
- package/src/http/fleet-benchmark.ts +79 -30
- package/src/http/global-config.ts +7 -0
- package/src/http/ranking.ts +24 -40
- package/src/http/tasks.ts +2 -1
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/used-by.ts +55 -19
- package/src/http/users.ts +2 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +15 -0
- package/src/providers/reasoning-effort.ts +2 -1
- package/src/providers/types.ts +2 -0
- package/src/ranking/telemetry-ranking.ts +5 -33
- package/src/scripts-runtime/sdk-allowlist.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +31 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
- package/src/server.ts +4 -0
- package/src/telemetry-context.ts +19 -1
- package/src/telemetry-contract/EVENTS.md +42 -8
- package/src/telemetry-contract/SOURCE.json +6 -5
- package/src/telemetry-contract/catalog.json +225 -5
- package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
- package/src/telemetry-contract/types.gen.ts +225 -27
- package/src/telemetry-identity.ts +47 -9
- package/src/telemetry-snapshot.ts +9 -1
- package/src/telemetry-ui-events.ts +9 -2
- package/src/telemetry.ts +14 -3
- package/src/tests/app-action-run-tool.test.ts +178 -0
- package/src/tests/app-row-tools.test.ts +360 -0
- package/src/tests/check-pr-body.test.ts +0 -23
- package/src/tests/credential-check.test.ts +38 -0
- package/src/tests/fleet-benchmark.test.ts +122 -2
- package/src/tests/http-users.test.ts +62 -1
- package/src/tests/model-catalog-table.test.ts +63 -0
- package/src/tests/model-profiles-api.test.ts +19 -7
- package/src/tests/models-catalog.test.ts +50 -0
- package/src/tests/onboarding.test.ts +21 -0
- package/src/tests/org-website.test.ts +62 -0
- package/src/tests/pi-mono-adapter.test.ts +23 -0
- package/src/tests/pr-body-leaks.test.ts +4 -0
- package/src/tests/pr-room-guard.test.ts +242 -0
- package/src/tests/ranking-listing.test.ts +8 -23
- package/src/tests/ranking-route.test.ts +49 -47
- package/src/tests/reasoning-effort.test.ts +39 -3
- package/src/tests/runner-harness-telemetry-env.test.ts +56 -1
- package/src/tests/swarm-tool-result-gate.test.ts +2 -4
- package/src/tests/telemetry-context.test.ts +128 -2
- package/src/tests/telemetry-ui-route.test.ts +123 -0
- package/src/tests/tool-annotations.test.ts +3 -2
- package/src/tests/used-by-route.test.ts +144 -10
- package/src/tests/workflow-agent-task-retry.test.ts +130 -4
- package/src/tests/workflow-wait-recovery.test.ts +116 -4
- package/src/tools/app-action-run.ts +63 -0
- package/src/tools/app-rows.ts +234 -0
- package/src/tools/model-catalog/index.ts +1 -1
- package/src/tools/tool-config.ts +8 -0
- package/src/tools/utils.ts +1 -1
- package/src/tools/workflows/retry-workflow-run.ts +1 -1
- package/src/types.ts +17 -0
- package/src/utils/company-profile.ts +24 -0
- package/src/utils/org-website.ts +58 -21
- package/src/workflows/recovery.ts +10 -22
- package/src/workflows/resume.ts +16 -0
- package/src/workflows/task-step-routing.ts +47 -7
- package/templates/skills/apps/SKILL.md +41 -2
- package/templates/skills/apps/content.md +41 -2
- package/dist/handlers-zbs90bny.js +0 -39
- package/dist/index-8pwc1jc8.js +0 -40
- package/dist/index-nxa6me0q.js +0 -41
- package/dist/lead-td8h2sfn.js +0 -78
- package/dist/worker-wr6xgfr2.js +0 -78
- package/src/tests/check-ranking-entry.test.ts +0 -146
|
@@ -13,8 +13,14 @@ import {
|
|
|
13
13
|
getWorkflowRun,
|
|
14
14
|
getWorkflowRunStepsByRunId,
|
|
15
15
|
initDb,
|
|
16
|
+
updateWorkflowRunStep,
|
|
16
17
|
} from "../be/db";
|
|
17
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
type AgentTask,
|
|
20
|
+
SUPERSEDED_WORKFLOW_TASK_REASON,
|
|
21
|
+
type WorkflowDefinition,
|
|
22
|
+
type WorkflowRunStep,
|
|
23
|
+
} from "../types";
|
|
18
24
|
import { startWorkflowExecution } from "../workflows/engine";
|
|
19
25
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
20
26
|
import { AgentTaskExecutor } from "../workflows/executors/agent-task";
|
|
@@ -27,6 +33,7 @@ import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
|
27
33
|
import { recoverIncompleteRuns } from "../workflows/recovery";
|
|
28
34
|
import { setupWorkflowResumeListener } from "../workflows/resume";
|
|
29
35
|
import { startRetryPoller, stopRetryPoller } from "../workflows/retry-poller";
|
|
36
|
+
import { SUPERSEDE_RETRY_POLICY } from "../workflows/task-step-routing";
|
|
30
37
|
import { interpolate } from "../workflows/template";
|
|
31
38
|
|
|
32
39
|
const TEST_DB_PATH = "./test-workflow-agent-task-retry.sqlite";
|
|
@@ -217,18 +224,18 @@ describe("agent-task node.retry on task failure", () => {
|
|
|
217
224
|
expect((await getTaskByWorkflowRunStepId(step.id))!.id).toBe(second.id);
|
|
218
225
|
});
|
|
219
226
|
|
|
220
|
-
test("a node without retry fails the run exactly as before", async () => {
|
|
227
|
+
test("a node without retry fails the run exactly as before on a non-supersede failure", async () => {
|
|
221
228
|
const runId = await startRun({});
|
|
222
229
|
const step = await synthesizeStep(runId);
|
|
223
230
|
const first = (await getTaskByWorkflowRunStepId(step.id))!;
|
|
224
231
|
|
|
225
|
-
await failTask(first.id, "
|
|
232
|
+
await failTask(first.id, "agent crashed");
|
|
226
233
|
|
|
227
234
|
const run = await waitFor(async () => {
|
|
228
235
|
const r = await getWorkflowRun(runId);
|
|
229
236
|
return r?.status === "failed" ? r : null;
|
|
230
237
|
}, "run failed");
|
|
231
|
-
expect(run.error).toContain("
|
|
238
|
+
expect(run.error).toContain("agent crashed");
|
|
232
239
|
const failedStep = await synthesizeStep(runId);
|
|
233
240
|
expect(failedStep.status).toBe("failed");
|
|
234
241
|
expect(failedStep.retryCount).toBe(0);
|
|
@@ -281,6 +288,125 @@ describe("agent-task node.retry on task failure", () => {
|
|
|
281
288
|
});
|
|
282
289
|
});
|
|
283
290
|
|
|
291
|
+
/** Wait for the step to be queued for the poller, then make it due now. */
|
|
292
|
+
async function waitForQueuedRetry(runId: string): Promise<WorkflowRunStep> {
|
|
293
|
+
const queued = await waitFor(async () => {
|
|
294
|
+
const s = await synthesizeStep(runId);
|
|
295
|
+
return s.status === "failed" && s.nextRetryAt != null ? s : null;
|
|
296
|
+
}, "step queued for retry");
|
|
297
|
+
await updateWorkflowRunStep(queued.id, { nextRetryAt: new Date(Date.now() - 1).toISOString() });
|
|
298
|
+
return queued;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
describe("implicit retry for a superseded task on a node without retry", () => {
|
|
302
|
+
test("a cut is redriven once after the static delay; a second cut follows onNodeFailure", async () => {
|
|
303
|
+
const runId = await startRun({});
|
|
304
|
+
const step = await synthesizeStep(runId);
|
|
305
|
+
const first = (await getTaskByWorkflowRunStepId(step.id))!;
|
|
306
|
+
|
|
307
|
+
const cutAt = Date.now();
|
|
308
|
+
await failTask(first.id, SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
309
|
+
|
|
310
|
+
const queued = await waitFor(async () => {
|
|
311
|
+
const s = await synthesizeStep(runId);
|
|
312
|
+
return s.status === "failed" && s.nextRetryAt != null ? s : null;
|
|
313
|
+
}, "step queued for retry");
|
|
314
|
+
expect(queued.retryCount).toBe(1);
|
|
315
|
+
expect(queued.maxRetries).toBe(1);
|
|
316
|
+
expect(queued.error).toBe(SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
317
|
+
const delayMs = new Date(queued.nextRetryAt!).getTime() - cutAt;
|
|
318
|
+
expect(delayMs).toBeGreaterThanOrEqual(SUPERSEDE_RETRY_POLICY.baseDelayMs - 1000);
|
|
319
|
+
expect(delayMs).toBeLessThanOrEqual(SUPERSEDE_RETRY_POLICY.baseDelayMs + 5000);
|
|
320
|
+
expect((await getWorkflowRun(runId))!.status).toBe("waiting");
|
|
321
|
+
expect((await getTaskById(first.id))!.workflowRunStepId).toBeFalsy();
|
|
322
|
+
|
|
323
|
+
await waitForQueuedRetry(runId);
|
|
324
|
+
startRetryPoller(registry, 10);
|
|
325
|
+
const second = await waitForRedrive(step.id, first);
|
|
326
|
+
await waitFor(async () => {
|
|
327
|
+
const s = await synthesizeStep(runId);
|
|
328
|
+
return s.status === "waiting" ? s : null;
|
|
329
|
+
}, "step waiting on redriven task");
|
|
330
|
+
stopRetryPoller();
|
|
331
|
+
|
|
332
|
+
await failTask(second.id, SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
333
|
+
const run = await waitFor(async () => {
|
|
334
|
+
const r = await getWorkflowRun(runId);
|
|
335
|
+
return r?.status === "failed" ? r : null;
|
|
336
|
+
}, "run failed via onNodeFailure:fail");
|
|
337
|
+
expect(run.error).toContain(SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
338
|
+
const finalStep = await synthesizeStep(runId);
|
|
339
|
+
expect(finalStep.status).toBe("failed");
|
|
340
|
+
expect(finalStep.retryCount).toBe(1);
|
|
341
|
+
expect(finalStep.nextRetryAt).toBeFalsy();
|
|
342
|
+
expect((await getTaskByWorkflowRunStepId(step.id))!.id).toBe(second.id);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("the redriven task completing continues the run", async () => {
|
|
346
|
+
const runId = await startRun({ onNodeFailure: "continue" });
|
|
347
|
+
const step = await synthesizeStep(runId);
|
|
348
|
+
const first = (await getTaskByWorkflowRunStepId(step.id))!;
|
|
349
|
+
|
|
350
|
+
await failTask(first.id, SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
351
|
+
await waitForQueuedRetry(runId);
|
|
352
|
+
startRetryPoller(registry, 10);
|
|
353
|
+
const second = await waitForRedrive(step.id, first);
|
|
354
|
+
await waitFor(async () => {
|
|
355
|
+
const s = await synthesizeStep(runId);
|
|
356
|
+
return s.status === "waiting" ? s : null;
|
|
357
|
+
}, "step waiting on redriven task");
|
|
358
|
+
stopRetryPoller();
|
|
359
|
+
|
|
360
|
+
await completeTask(second.id, "synthesis done");
|
|
361
|
+
const run = await waitFor(async () => {
|
|
362
|
+
const r = await getWorkflowRun(runId);
|
|
363
|
+
return r?.status === "completed" ? r : null;
|
|
364
|
+
}, "run completed");
|
|
365
|
+
expect(run.status).toBe("completed");
|
|
366
|
+
const finalStep = await synthesizeStep(runId);
|
|
367
|
+
expect(JSON.stringify(finalStep.output)).not.toContain("[FAILED:");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("explicit retry {maxRetries:0} opts out: the cut fails the run", async () => {
|
|
371
|
+
const runId = await startRun({ retry: { ...RETRY_ONCE, maxRetries: 0 } });
|
|
372
|
+
const step = await synthesizeStep(runId);
|
|
373
|
+
const first = (await getTaskByWorkflowRunStepId(step.id))!;
|
|
374
|
+
|
|
375
|
+
await failTask(first.id, SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
376
|
+
|
|
377
|
+
const run = await waitFor(async () => {
|
|
378
|
+
const r = await getWorkflowRun(runId);
|
|
379
|
+
return r?.status === "failed" ? r : null;
|
|
380
|
+
}, "run failed");
|
|
381
|
+
expect(run.error).toContain(SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
382
|
+
const failedStep = await synthesizeStep(runId);
|
|
383
|
+
expect(failedStep.retryCount).toBe(0);
|
|
384
|
+
expect(failedStep.nextRetryAt).toBeFalsy();
|
|
385
|
+
expect((await getTaskByWorkflowRunStepId(step.id))!.id).toBe(first.id);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("the recovery sweep reads the cut from the task row", async () => {
|
|
389
|
+
const runId = await startRun({});
|
|
390
|
+
const step = await synthesizeStep(runId);
|
|
391
|
+
const first = (await getTaskByWorkflowRunStepId(step.id))!;
|
|
392
|
+
|
|
393
|
+
teardownResumeListener?.();
|
|
394
|
+
try {
|
|
395
|
+
await failTask(first.id, SUPERSEDED_WORKFLOW_TASK_REASON);
|
|
396
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
397
|
+
await recoverIncompleteRuns(registry);
|
|
398
|
+
} finally {
|
|
399
|
+
teardownResumeListener = setupWorkflowResumeListener(workflowEventBus, registry);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const queued = await synthesizeStep(runId);
|
|
403
|
+
expect(queued.status).toBe("failed");
|
|
404
|
+
expect(queued.nextRetryAt).toBeTruthy();
|
|
405
|
+
expect(queued.retryCount).toBe(1);
|
|
406
|
+
expect((await getWorkflowRun(runId))!.status).toBe("waiting");
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
284
410
|
describe("recovery sweep holding a stale failed-task snapshot", () => {
|
|
285
411
|
/**
|
|
286
412
|
* Recovery snapshots task A as failed, then pauses. Meanwhile the live
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import * as db from "../be/db";
|
|
4
4
|
import {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getWorkflowRunStep,
|
|
14
14
|
getWorkflowRunStepsByRunId,
|
|
15
15
|
initDb,
|
|
16
|
+
resolveWaitState,
|
|
16
17
|
updateWorkflowRun,
|
|
17
18
|
updateWorkflowRunStep,
|
|
18
19
|
} from "../be/db";
|
|
@@ -23,6 +24,10 @@ import { createExecutorRegistry } from "../workflows/executors/registry";
|
|
|
23
24
|
import { recoverIncompleteRuns } from "../workflows/recovery";
|
|
24
25
|
|
|
25
26
|
const TEST_DB_PATH = "./test-workflow-wait-recovery.sqlite";
|
|
27
|
+
const templateGlobals = globalThis as typeof globalThis & {
|
|
28
|
+
__testMigrationTemplate?: Uint8Array;
|
|
29
|
+
};
|
|
30
|
+
let savedTemplate: Uint8Array | undefined;
|
|
26
31
|
|
|
27
32
|
const eventBus = new InProcessEventBus();
|
|
28
33
|
const deps: ExecutorDependencies = {
|
|
@@ -42,7 +47,52 @@ async function makeWorkflow(name: string, def: WorkflowDefinition): Promise<Work
|
|
|
42
47
|
return wf;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
async function makeResolvedWait(
|
|
51
|
+
mode: "time" | "event",
|
|
52
|
+
status: "fired" | "timeout",
|
|
53
|
+
nextPort: string,
|
|
54
|
+
) {
|
|
55
|
+
const wf = await makeWorkflow("wait-recovery-resolved", {
|
|
56
|
+
nodes: [
|
|
57
|
+
{
|
|
58
|
+
id: "w1",
|
|
59
|
+
type: "wait",
|
|
60
|
+
config:
|
|
61
|
+
mode === "time"
|
|
62
|
+
? { mode, durationMs: 60_000 }
|
|
63
|
+
: { mode, eventName: "saved.signal", timeoutMs: 60_000 },
|
|
64
|
+
next: { [nextPort]: "done" },
|
|
65
|
+
},
|
|
66
|
+
{ id: "done", type: "notify", config: { channel: "swarm", template: "recovered" } },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
const run = await createWorkflowRun({ id: crypto.randomUUID(), workflowId: wf.id });
|
|
70
|
+
await updateWorkflowRun(run.id, { status: "waiting" });
|
|
71
|
+
const step = await createWorkflowRunStep({
|
|
72
|
+
id: crypto.randomUUID(),
|
|
73
|
+
runId: run.id,
|
|
74
|
+
nodeId: "w1",
|
|
75
|
+
nodeType: "wait",
|
|
76
|
+
});
|
|
77
|
+
await updateWorkflowRunStep(step.id, { status: "waiting" });
|
|
78
|
+
const future = new Date(Date.now() + 60_000).toISOString();
|
|
79
|
+
const wait = await createWaitState({
|
|
80
|
+
id: crypto.randomUUID(),
|
|
81
|
+
workflowRunId: run.id,
|
|
82
|
+
workflowRunStepId: step.id,
|
|
83
|
+
mode,
|
|
84
|
+
...(mode === "time" ? { wakeUpAt: future } : { eventName: "saved.signal", expiresAt: future }),
|
|
85
|
+
});
|
|
86
|
+
const payload = mode === "event" && status === "fired" ? { signal: "saved", ok: true } : null;
|
|
87
|
+
const resolution = await resolveWaitState(wait.id, { status, firedPayload: payload });
|
|
88
|
+
expect(resolution.updated).toBe(true);
|
|
89
|
+
return { run, step, wait, payload, resolution };
|
|
90
|
+
}
|
|
91
|
+
|
|
45
92
|
beforeAll(async () => {
|
|
93
|
+
// Use a file-backed DB so restart cases retain their committed wait resolutions.
|
|
94
|
+
savedTemplate = templateGlobals.__testMigrationTemplate;
|
|
95
|
+
templateGlobals.__testMigrationTemplate = undefined;
|
|
46
96
|
try {
|
|
47
97
|
await unlink(TEST_DB_PATH);
|
|
48
98
|
} catch {
|
|
@@ -60,12 +110,76 @@ afterAll(async () => {
|
|
|
60
110
|
}
|
|
61
111
|
}
|
|
62
112
|
closeDb();
|
|
113
|
+
templateGlobals.__testMigrationTemplate = savedTemplate;
|
|
63
114
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
64
115
|
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
65
116
|
}
|
|
66
117
|
});
|
|
67
118
|
|
|
68
119
|
describe("WaitExecutor — recovery on startup", () => {
|
|
120
|
+
test.each([
|
|
121
|
+
["time", "fired", "default"],
|
|
122
|
+
["event", "fired", "event"],
|
|
123
|
+
["event", "timeout", "timeout"],
|
|
124
|
+
] as const)("recovers a persisted %s/%s resolution after restart via '%s' exactly once", async (mode, status, nextPort) => {
|
|
125
|
+
const registry = createExecutorRegistry(deps);
|
|
126
|
+
const { run, step, wait, payload, resolution } = await makeResolvedWait(mode, status, nextPort);
|
|
127
|
+
|
|
128
|
+
// Reopen after the resolution commits, before the waiting step is checkpointed.
|
|
129
|
+
closeDb();
|
|
130
|
+
initDb(TEST_DB_PATH);
|
|
131
|
+
expect((await getWorkflowRunStep(step.id))?.status).toBe("waiting");
|
|
132
|
+
expect((await getWaitStateByStepId(step.id))?.status).toBe(status);
|
|
133
|
+
await Promise.all([recoverIncompleteRuns(registry), recoverIncompleteRuns(registry)]);
|
|
134
|
+
|
|
135
|
+
const recoveredStep = await getWorkflowRunStep(step.id);
|
|
136
|
+
expect(recoveredStep?.status).toBe("completed");
|
|
137
|
+
expect(recoveredStep?.nextPort).toBe(nextPort);
|
|
138
|
+
expect(recoveredStep?.output).toEqual({
|
|
139
|
+
waitId: wait.id,
|
|
140
|
+
mode,
|
|
141
|
+
firedAt: resolution.row!.resolvedAt,
|
|
142
|
+
payload,
|
|
143
|
+
});
|
|
144
|
+
expect(await getWaitStateByStepId(step.id)).toEqual(resolution.row);
|
|
145
|
+
expect((await getWorkflowRun(run.id))?.status).toBe("completed");
|
|
146
|
+
const doneSteps = (await getWorkflowRunStepsByRunId(run.id)).filter((s) => s.nodeId === "done");
|
|
147
|
+
expect(doneSteps).toHaveLength(1);
|
|
148
|
+
expect(doneSteps[0]?.status).toBe("completed");
|
|
149
|
+
|
|
150
|
+
await recoverIncompleteRuns(registry);
|
|
151
|
+
expect((await getWorkflowRunStepsByRunId(run.id)).filter((s) => s.nodeId === "done")).toEqual(
|
|
152
|
+
doneSteps,
|
|
153
|
+
);
|
|
154
|
+
expect((await getWorkflowRun(run.id))?.status).toBe("completed");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test.each([
|
|
158
|
+
"completed",
|
|
159
|
+
"failed",
|
|
160
|
+
"cancelled",
|
|
161
|
+
] as const)("does not reopen a %s run that becomes terminal during wait recovery", async (status) => {
|
|
162
|
+
const registry = createExecutorRegistry(deps);
|
|
163
|
+
const { run, step } = await makeResolvedWait("event", "fired", "event");
|
|
164
|
+
const getWorkflow = db.getWorkflow;
|
|
165
|
+
const snapshot = spyOn(db, "getWorkflow").mockImplementationOnce(async (id) => {
|
|
166
|
+
const workflow = await getWorkflow(id);
|
|
167
|
+
expect((await getWorkflowRun(run.id))?.status).toBe("waiting");
|
|
168
|
+
// A sibling can fail the run after the resume read, leaving this step waiting.
|
|
169
|
+
await updateWorkflowRun(run.id, { status });
|
|
170
|
+
return workflow;
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
await recoverIncompleteRuns(registry);
|
|
174
|
+
expect(snapshot).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect((await getWorkflowRun(run.id))?.status).toBe(status);
|
|
176
|
+
expect((await getWorkflowRunStep(step.id))?.status).toBe("waiting");
|
|
177
|
+
expect(await getWorkflowRunStepsByRunId(run.id)).toHaveLength(1);
|
|
178
|
+
} finally {
|
|
179
|
+
snapshot.mockRestore();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
69
183
|
test("server died while waiting: overdue time-mode wait recovers via 'default' port", async () => {
|
|
70
184
|
const registry = createExecutorRegistry(deps);
|
|
71
185
|
|
|
@@ -170,9 +284,7 @@ describe("WaitExecutor — recovery on startup", () => {
|
|
|
170
284
|
const doneCount1 = stepsAfter1.filter((s) => s.nodeId === "done").length;
|
|
171
285
|
expect(doneCount1).toBe(1);
|
|
172
286
|
|
|
173
|
-
//
|
|
174
|
-
// recovery query). resumeWaitState's atomic update returns updated=false,
|
|
175
|
-
// so the step shouldn't re-run.
|
|
287
|
+
// Completed runs and steps are no longer eligible for recovery.
|
|
176
288
|
await recoverIncompleteRuns(registry);
|
|
177
289
|
const stepsAfter2 = await getWorkflowRunStepsByRunId(run.id);
|
|
178
290
|
expect(stepsAfter2.filter((s) => s.nodeId === "done").length).toBe(1);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { runAppAction } from "../apps/action-runner";
|
|
4
|
+
import { AppNameSchema } from "../apps/definition";
|
|
5
|
+
import { appDefinitionNeedsRepair, getApp } from "../apps/store";
|
|
6
|
+
import { getAgentById } from "../be/db";
|
|
7
|
+
import { can } from "../rbac";
|
|
8
|
+
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "./utils";
|
|
9
|
+
|
|
10
|
+
export function registerAppActionRunTool(server: McpServer) {
|
|
11
|
+
createToolRegistrar(server)(
|
|
12
|
+
"app-action-run",
|
|
13
|
+
{
|
|
14
|
+
title: "Run an app action",
|
|
15
|
+
description:
|
|
16
|
+
"Run a declared app action as an agent. Scripts and extensions cannot invoke this tool.",
|
|
17
|
+
// An action runs a script, a sync, or a task: it can write rows and reach external sources.
|
|
18
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
19
|
+
rbac: { permission: "app.use" },
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
appId: z.string().min(1),
|
|
22
|
+
action: AppNameSchema,
|
|
23
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
24
|
+
}),
|
|
25
|
+
outputSchema: swarmToolOutputSchema({
|
|
26
|
+
ok: z.boolean().optional(),
|
|
27
|
+
result: z.unknown().optional(),
|
|
28
|
+
stdout: z.string().optional(),
|
|
29
|
+
error: z.string().optional(),
|
|
30
|
+
durationMs: z.number().optional(),
|
|
31
|
+
taskId: z.string().optional(),
|
|
32
|
+
status: z.string().optional(),
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
async ({ appId, action, input }, info) => {
|
|
36
|
+
if (info.callOrigin === "script-sdk" || info.callOrigin === "extension")
|
|
37
|
+
return toolErr("Scripts and extensions cannot run app actions.");
|
|
38
|
+
if (!info.agentId) return toolErr('Agent ID not found. Set the "X-Agent-ID" header.');
|
|
39
|
+
const agent = await getAgentById(info.agentId);
|
|
40
|
+
const decision = can({
|
|
41
|
+
principal: { kind: "agent", agentId: info.agentId, isLead: agent?.isLead ?? false },
|
|
42
|
+
verb: "app.use",
|
|
43
|
+
resource: { kind: "app", appId },
|
|
44
|
+
source: "mcp",
|
|
45
|
+
});
|
|
46
|
+
if (!decision.allow) return toolErr(decision.reason);
|
|
47
|
+
const app = await getApp(appId);
|
|
48
|
+
if (!app) return toolErr("App not found.");
|
|
49
|
+
if (appDefinitionNeedsRepair(app))
|
|
50
|
+
return toolErr("App definition needs repair.", { data: { issues: app.definitionError } });
|
|
51
|
+
const output = await runAppAction({
|
|
52
|
+
app,
|
|
53
|
+
name: action,
|
|
54
|
+
input,
|
|
55
|
+
actor: `agent:${info.agentId}`,
|
|
56
|
+
caller: { kind: "agent", id: info.agentId, ...(agent?.name ? { name: agent.name } : {}) },
|
|
57
|
+
});
|
|
58
|
+
if (output.status !== 200)
|
|
59
|
+
return toolErr(output.body.error ?? "Action failed.", { data: output.body });
|
|
60
|
+
return toolOk("Action invoked.", { data: output.body });
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { AppNameSchema, type ModelDef, SYSTEM_COLUMN_KINDS } from "../apps/definition";
|
|
4
|
+
import {
|
|
5
|
+
type AppRow,
|
|
6
|
+
AppRowAppNotFoundError,
|
|
7
|
+
AppRowValidationError,
|
|
8
|
+
createAppRow,
|
|
9
|
+
createAppRows,
|
|
10
|
+
deleteAppRow,
|
|
11
|
+
getAppRow,
|
|
12
|
+
patchAppRow,
|
|
13
|
+
validValue,
|
|
14
|
+
} from "../apps/row-store";
|
|
15
|
+
import { appDefinitionNeedsRepair, getApp } from "../apps/store";
|
|
16
|
+
import { getAgentById } from "../be/db";
|
|
17
|
+
import {
|
|
18
|
+
MAX_APP_BULK_ROWS_BODY_BYTES,
|
|
19
|
+
MAX_APP_ROW_BODY_BYTES,
|
|
20
|
+
rowMatchesFilters,
|
|
21
|
+
selectAppRows,
|
|
22
|
+
} from "../http/apps";
|
|
23
|
+
import { can } from "../rbac";
|
|
24
|
+
import {
|
|
25
|
+
createToolRegistrar,
|
|
26
|
+
type SwarmToolResult,
|
|
27
|
+
swarmToolOutputSchema,
|
|
28
|
+
toolErr,
|
|
29
|
+
toolOk,
|
|
30
|
+
} from "./utils";
|
|
31
|
+
|
|
32
|
+
const base = { appId: z.string().min(1), model: AppNameSchema };
|
|
33
|
+
const rowId = z.string().min(1);
|
|
34
|
+
const values = z.record(z.string(), z.unknown());
|
|
35
|
+
const equals = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]));
|
|
36
|
+
const limit = z.number().int().min(1).max(1000);
|
|
37
|
+
const rowOutput = { row: z.looseObject({}).optional() };
|
|
38
|
+
const rowsOutput = { rows: z.array(z.looseObject({})).optional() };
|
|
39
|
+
|
|
40
|
+
function checkSize(value: unknown, max: number): void {
|
|
41
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > max) {
|
|
42
|
+
throw new AppRowValidationError([
|
|
43
|
+
{ path: "values", message: `Row input exceeds ${max} bytes.` },
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rowFilters(model: ModelDef, equals: Record<string, string | number | boolean> = {}) {
|
|
49
|
+
return Object.entries(equals).map(([column, value]) => {
|
|
50
|
+
const declared = Object.hasOwn(model.columns, column) ? model.columns[column] : undefined;
|
|
51
|
+
if ((!declared && !Object.hasOwn(SYSTEM_COLUMN_KINDS, column)) || declared?.hidden) {
|
|
52
|
+
throw new AppRowValidationError([
|
|
53
|
+
{ path: `equals.${column}`, message: "Unknown or hidden column." },
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
if (!validValue(declared ?? { kind: SYSTEM_COLUMN_KINDS[column]! }, value)) {
|
|
57
|
+
throw new AppRowValidationError([
|
|
58
|
+
{ path: `equals.${column}`, message: "Filter value must match the column kind." },
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
return { column, value };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function registerAppRowTools(server: McpServer) {
|
|
66
|
+
function register<Shape extends z.ZodRawShape>(
|
|
67
|
+
name: string,
|
|
68
|
+
description: string,
|
|
69
|
+
shape: Shape,
|
|
70
|
+
output: z.ZodRawShape,
|
|
71
|
+
operation: (
|
|
72
|
+
args: z.infer<z.ZodObject<typeof base & Shape>>,
|
|
73
|
+
model: ModelDef,
|
|
74
|
+
actor: string,
|
|
75
|
+
) => Promise<SwarmToolResult>,
|
|
76
|
+
readOnly = false,
|
|
77
|
+
) {
|
|
78
|
+
createToolRegistrar(server)(
|
|
79
|
+
name,
|
|
80
|
+
{
|
|
81
|
+
title: description,
|
|
82
|
+
description,
|
|
83
|
+
annotations: { readOnlyHint: readOnly },
|
|
84
|
+
rbac: { permission: "app.use" },
|
|
85
|
+
inputSchema: z.object({ ...base, ...shape }),
|
|
86
|
+
outputSchema: swarmToolOutputSchema(output),
|
|
87
|
+
},
|
|
88
|
+
async (args, info) => {
|
|
89
|
+
if (!info.agentId) return toolErr('Agent ID not found. Set the "X-Agent-ID" header.');
|
|
90
|
+
const { appId, model: modelName } = z.object(base).parse(args);
|
|
91
|
+
const agent = await getAgentById(info.agentId);
|
|
92
|
+
const decision = can({
|
|
93
|
+
principal: { kind: "agent", agentId: info.agentId, isLead: agent?.isLead ?? false },
|
|
94
|
+
verb: "app.use",
|
|
95
|
+
resource: { kind: "app", appId },
|
|
96
|
+
source: "mcp",
|
|
97
|
+
});
|
|
98
|
+
if (!decision.allow) return toolErr(decision.reason);
|
|
99
|
+
const app = await getApp(appId);
|
|
100
|
+
if (!app) return toolErr("App not found.");
|
|
101
|
+
if (appDefinitionNeedsRepair(app))
|
|
102
|
+
return toolErr("App definition needs repair.", { data: { issues: app.definitionError } });
|
|
103
|
+
if (!Object.hasOwn(app.definition.models, modelName)) return toolErr("Model not found.");
|
|
104
|
+
try {
|
|
105
|
+
return await operation(args, app.definition.models[modelName]!, `agent:${info.agentId}`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof AppRowValidationError)
|
|
108
|
+
return toolErr(error.message, { data: { issues: error.issues } });
|
|
109
|
+
if (error instanceof AppRowAppNotFoundError) return toolErr(error.message);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
register(
|
|
117
|
+
"app-rows-list",
|
|
118
|
+
"List app rows with equality filters, sorting, and a limit.",
|
|
119
|
+
{
|
|
120
|
+
equals: equals.optional(),
|
|
121
|
+
sort: z.string().optional(),
|
|
122
|
+
limit: limit.optional(),
|
|
123
|
+
},
|
|
124
|
+
{ ...rowsOutput, total: z.number().optional() },
|
|
125
|
+
async (args, model) => {
|
|
126
|
+
const data = await selectAppRows(
|
|
127
|
+
args.appId,
|
|
128
|
+
args.model,
|
|
129
|
+
model,
|
|
130
|
+
rowFilters(model, args.equals),
|
|
131
|
+
args.sort,
|
|
132
|
+
args.limit,
|
|
133
|
+
true,
|
|
134
|
+
);
|
|
135
|
+
return toolOk(`Found ${data.total} row(s).`, { data });
|
|
136
|
+
},
|
|
137
|
+
true,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
register(
|
|
141
|
+
"app-row-get",
|
|
142
|
+
"Get an app row.",
|
|
143
|
+
{ rowId },
|
|
144
|
+
rowOutput,
|
|
145
|
+
async (args) => {
|
|
146
|
+
const row = await getAppRow(args.appId, args.model, args.rowId);
|
|
147
|
+
return row ? toolOk("Row retrieved.", { data: { row } }) : toolErr("Row not found.");
|
|
148
|
+
},
|
|
149
|
+
true,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
register(
|
|
153
|
+
"app-row-create",
|
|
154
|
+
"Create an app row.",
|
|
155
|
+
{ values },
|
|
156
|
+
rowOutput,
|
|
157
|
+
async (args, model, actor) => {
|
|
158
|
+
checkSize({ values: args.values }, MAX_APP_ROW_BODY_BYTES);
|
|
159
|
+
const row = await createAppRow(args.appId, args.model, model, args.values, { actor });
|
|
160
|
+
return toolOk("Row created.", { data: { row } });
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
register(
|
|
165
|
+
"app-rows-create",
|
|
166
|
+
"Create up to 500 app rows within 10 MiB.",
|
|
167
|
+
{
|
|
168
|
+
rows: z.array(values).min(1).max(500),
|
|
169
|
+
},
|
|
170
|
+
rowsOutput,
|
|
171
|
+
async (args, model, actor) => {
|
|
172
|
+
checkSize({ rows: args.rows }, MAX_APP_BULK_ROWS_BODY_BYTES);
|
|
173
|
+
for (const values of args.rows) checkSize({ values }, MAX_APP_ROW_BODY_BYTES);
|
|
174
|
+
const rows = await createAppRows(args.appId, args.model, model, args.rows, { actor });
|
|
175
|
+
return toolOk(`Created ${rows.length} row(s).`, { data: { rows } });
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
register(
|
|
180
|
+
"app-row-patch",
|
|
181
|
+
"Patch an app row.",
|
|
182
|
+
{ rowId, values },
|
|
183
|
+
rowOutput,
|
|
184
|
+
async (args, model, actor) => {
|
|
185
|
+
checkSize({ values: args.values }, MAX_APP_ROW_BODY_BYTES);
|
|
186
|
+
const row = await patchAppRow(args.appId, args.model, model, args.rowId, args.values, {
|
|
187
|
+
actor,
|
|
188
|
+
});
|
|
189
|
+
return row ? toolOk("Row updated.", { data: { row } }) : toolErr("Row not found.");
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
register(
|
|
194
|
+
"app-row-delete",
|
|
195
|
+
"Delete an app row.",
|
|
196
|
+
{ rowId },
|
|
197
|
+
{ ok: z.boolean().optional() },
|
|
198
|
+
async (args, model) => {
|
|
199
|
+
const deleted = await deleteAppRow(args.appId, args.model, model, args.rowId);
|
|
200
|
+
return deleted ? toolOk("Row deleted.", { data: { ok: true } }) : toolErr("Row not found.");
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
register(
|
|
205
|
+
"app-rows-delete",
|
|
206
|
+
"Delete matching app rows. Supply at least one equality filter.",
|
|
207
|
+
{
|
|
208
|
+
equals: equals.refine(
|
|
209
|
+
(value) => Object.keys(value).length > 0,
|
|
210
|
+
"Supply at least one equality filter.",
|
|
211
|
+
),
|
|
212
|
+
limit: limit.optional(),
|
|
213
|
+
},
|
|
214
|
+
{ deleted: z.number().optional() },
|
|
215
|
+
async (args, model) => {
|
|
216
|
+
const filters = rowFilters(model, args.equals);
|
|
217
|
+
const { rows } = await selectAppRows(
|
|
218
|
+
args.appId,
|
|
219
|
+
args.model,
|
|
220
|
+
model,
|
|
221
|
+
filters,
|
|
222
|
+
undefined,
|
|
223
|
+
args.limit ?? 1000,
|
|
224
|
+
true,
|
|
225
|
+
);
|
|
226
|
+
// Re-check under the row lock: a row patched out of the filter since the select is kept.
|
|
227
|
+
const matches = (row: AppRow) => rowMatchesFilters(row, filters);
|
|
228
|
+
let deleted = 0;
|
|
229
|
+
for (const row of rows)
|
|
230
|
+
if (await deleteAppRow(args.appId, args.model, model, row.id, matches)) deleted++;
|
|
231
|
+
return toolOk(`Deleted ${deleted} row(s).`, { data: { deleted } });
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -87,7 +87,7 @@ export const registerModelCatalogOverlayUpsertTool = (server: McpServer) => {
|
|
|
87
87
|
.string()
|
|
88
88
|
.min(1)
|
|
89
89
|
.describe(
|
|
90
|
-
`models.dev provider id: anthropic, openai, openrouter, amazon-bedrock, opencode, ${openAICompatibleProviderIds().join(", ")}.`,
|
|
90
|
+
`models.dev provider id: anthropic, openai, openrouter, amazon-bedrock, opencode, opencode-go, ${openAICompatibleProviderIds().join(", ")}.`,
|
|
91
91
|
),
|
|
92
92
|
modelId: z
|
|
93
93
|
.string()
|
package/src/tools/tool-config.ts
CHANGED
|
@@ -189,6 +189,14 @@ export const DEFERRED_TOOLS = new Set([
|
|
|
189
189
|
"app-list",
|
|
190
190
|
"app-patch",
|
|
191
191
|
"app-query",
|
|
192
|
+
"app-rows-list",
|
|
193
|
+
"app-row-get",
|
|
194
|
+
"app-row-create",
|
|
195
|
+
"app-rows-create",
|
|
196
|
+
"app-row-patch",
|
|
197
|
+
"app-row-delete",
|
|
198
|
+
"app-rows-delete",
|
|
199
|
+
"app-action-run",
|
|
192
200
|
"app-sync",
|
|
193
201
|
"app-upsert",
|
|
194
202
|
"app-history",
|
package/src/tools/utils.ts
CHANGED
|
@@ -347,7 +347,7 @@ export const NUDGES: Record<string, (result: SwarmToolResult) => string | undefi
|
|
|
347
347
|
const hasQueries = Object.keys(definition?.queries ?? {}).length > 0;
|
|
348
348
|
const hasActions = Object.keys(definition?.actions ?? {}).length > 0;
|
|
349
349
|
return hasQueries || hasActions
|
|
350
|
-
? "
|
|
350
|
+
? "Run `definition.queries` with `app-query` and supply each `$param` value. Invoke `definition.actions` with `app-action-run` through MCP or `POST /api/apps/<id>/actions/<name>` through REST. Sync actions refresh sources."
|
|
351
351
|
: undefined;
|
|
352
352
|
},
|
|
353
353
|
// kv-get is spill-exempt (retrieval path), so oversized values reach the
|
|
@@ -10,7 +10,7 @@ export const registerRetryWorkflowRunTool = (server: McpServer) => {
|
|
|
10
10
|
title: "Retry Workflow Run",
|
|
11
11
|
annotations: { destructiveHint: false },
|
|
12
12
|
description:
|
|
13
|
-
"
|
|
13
|
+
"Resume the same failed workflow run from its failed step(s); completed steps are not re-run. Retrying re-creates the failed node's side effects (e.g. an agent task), so check the target state first if that work was already done manually.",
|
|
14
14
|
inputSchema: z.object({
|
|
15
15
|
runId: z.string().uuid().describe("Workflow run ID to retry"),
|
|
16
16
|
}),
|