@desplega.ai/agent-swarm 1.157.0 → 1.158.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-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
- package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
- package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
- package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
- package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
- package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
- package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
- package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
- package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
- package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
- package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
- package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
- package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
- package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
- package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
- package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
- package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
- package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
- package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
- package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
- package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
- package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
- package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
- package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
- package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
- package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
- package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
- package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
- package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
- package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
- package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
- package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
- package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
- package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
- package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
- package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
- package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
- package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
- package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
- package/dist/cli-t1ze64h3.js +811 -0
- package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
- package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
- package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
- package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
- package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
- package/dist/cli.js +29 -15
- package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
- package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
- package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
- package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
- package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
- package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
- package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
- package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
- package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
- package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
- package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
- package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
- package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
- package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
- package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
- package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
- package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
- package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
- package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
- package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
- package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
- package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
- package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
- package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
- package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
- package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
- package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
- package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
- package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
- package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
- package/openapi.json +1626 -629
- package/package.json +2 -1
- package/src/apps/catalog.generated.json +229 -0
- package/src/apps/page-validator.ts +71 -16
- package/src/apps/row-store.ts +66 -11
- package/src/apps/sync.ts +59 -17
- package/src/artifact-sdk/browser-sdk.ts +436 -2
- package/src/be/approval-sweeps.ts +109 -0
- package/src/be/boot-seeds.ts +40 -0
- package/src/be/chunking.ts +1 -1
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/approvals.ts +78 -0
- package/src/be/db/tasks/read.ts +197 -39
- package/src/be/db/tasks/write.ts +41 -9
- package/src/be/db-client.ts +107 -48
- package/src/be/db.ts +219 -76
- package/src/be/events.ts +33 -1
- package/src/be/memory/graph-expansion.ts +10 -3
- package/src/be/memory/providers/sqlite-store.ts +55 -13
- package/src/be/memory/raters/retrieval.ts +5 -2
- package/src/be/memory/reranker.ts +26 -2
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
- package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
- package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
- package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +1857 -0
- package/src/be/modelsdev-cache.ts +14 -1
- package/src/be/pricing-normalize.ts +2 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/steering.ts +53 -3
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/task-citations.ts +29 -0
- package/src/cli.tsx +14 -1
- package/src/commands/onboard/compose-generator.ts +8 -3
- package/src/commands/onboard/container-engine.ts +220 -0
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/health-check.tsx +4 -1
- package/src/commands/onboard/steps/prereq-check.tsx +50 -54
- package/src/commands/onboard/steps/start.tsx +31 -16
- package/src/commands/onboard/types.ts +7 -2
- package/src/commands/onboard.tsx +39 -4
- package/src/commands/profile-sync.ts +79 -26
- package/src/commands/runner.ts +8 -1
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +263 -103
- package/src/hooks/hook.ts +21 -12
- package/src/http/agents.ts +35 -0
- package/src/http/approval-requests.ts +136 -64
- package/src/http/apps.ts +14 -3
- package/src/http/config.ts +15 -1
- package/src/http/events.ts +62 -2
- package/src/http/index.ts +6 -4
- package/src/http/memory.ts +21 -7
- package/src/http/schedules.ts +1 -1
- package/src/http/session-data.ts +9 -4
- package/src/http/sessions.ts +25 -18
- package/src/http/stats.ts +19 -7
- package/src/http/status.ts +11 -1
- package/src/http/tasks.ts +40 -8
- package/src/http/webhooks.ts +67 -46
- package/src/http/workflows.ts +15 -20
- package/src/linear/outbound.ts +37 -15
- package/src/otel-impl.ts +151 -1
- package/src/otel.ts +14 -3
- package/src/prompts/memories.ts +19 -2
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/modelsdev-reasoning.json +551 -0
- package/src/rbac/legacy-policy.ts +16 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/schedule-task.ts +3 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +12 -7
- package/src/slack/app.ts +35 -4
- package/src/slack/connection-state.ts +17 -0
- package/src/tasks/context-key.ts +1 -0
- package/src/tasks/worker-follow-up.ts +6 -2
- package/src/tests/agent-task-activity.test.ts +74 -0
- package/src/tests/agents-list-model-display.test.ts +6 -1
- package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
- package/src/tests/approval-requests.test.ts +395 -0
- package/src/tests/approval-sweeps-config.test.ts +47 -0
- package/src/tests/approval-sweeps.test.ts +508 -0
- package/src/tests/apps-kibo-blocks.test.ts +77 -0
- package/src/tests/apps-spike.test.ts +56 -1
- package/src/tests/apps-sync-engine.test.ts +20 -9
- package/src/tests/boot-seeds.test.ts +170 -0
- package/src/tests/cancel-approval-request-tool.test.ts +115 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/config-key-filter.test.ts +80 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/db-client-busy-retry.test.ts +96 -0
- package/src/tests/db-client.test.ts +60 -1
- package/src/tests/db-task-read-contract.test.ts +10 -0
- package/src/tests/db-task-write-contract.test.ts +9 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/events-http.test.ts +103 -0
- package/src/tests/extensions-catalog.test.ts +22 -17
- package/src/tests/extensions-pre-task.test.ts +107 -0
- package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
- package/src/tests/follow-up-config-inheritance.test.ts +260 -0
- package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
- package/src/tests/heartbeat-reboot-live.test.ts +95 -0
- package/src/tests/heartbeat-stall-cas.test.ts +80 -0
- package/src/tests/heartbeat.test.ts +99 -3
- package/src/tests/hook-mcp-config.test.ts +29 -0
- package/src/tests/http-api-integration.test.ts +16 -2
- package/src/tests/kv-storage.test.ts +62 -0
- package/src/tests/linear-outbound-sync.test.ts +120 -5
- package/src/tests/list-endpoint-slimming.test.ts +91 -0
- package/src/tests/memory-graph-organic-route.test.ts +155 -0
- package/src/tests/memory-http-recall-gating.test.ts +14 -4
- package/src/tests/memory-hybrid.test.ts +161 -1
- package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
- package/src/tests/memory.test.ts +22 -0
- package/src/tests/onboard-container-engine.test.ts +240 -0
- package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
- package/src/tests/pricing-routes.test.ts +14 -0
- package/src/tests/profile-sync-warning.test.ts +8 -4
- package/src/tests/promote-draft-task-route.test.ts +19 -0
- package/src/tests/provider-registration.test.ts +296 -42
- package/src/tests/rbac-engine.test.ts +30 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/script-connections.test.ts +7 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions-detail-batch.test.ts +250 -0
- package/src/tests/sessions.test.ts +118 -0
- package/src/tests/slack-config.test.ts +22 -0
- package/src/tests/sonnet-5-5-registry.test.ts +44 -0
- package/src/tests/status.test.ts +36 -1
- package/src/tests/swarm-diff.test.ts +316 -10
- package/src/tests/task-completion-idempotency.test.ts +2 -1
- package/src/tests/tasks-requested-by-filter.test.ts +3 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/ui-list-slim-projections.test.ts +158 -0
- package/src/tests/webhooks-task-blocked.test.ts +127 -0
- package/src/tests/workflow-approval-recovery.test.ts +203 -0
- package/src/tests/workflow-hitl-routing.test.ts +22 -1
- package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
- package/src/tests/workflow-http-v2.test.ts +99 -8
- package/src/tests/workflow-retry-validation.test.ts +56 -0
- package/src/tests/workflow-tla-races.test.ts +389 -0
- package/src/tests/workflow-validation-port-routing.test.ts +113 -1
- package/src/tools/cancel-approval-request.ts +57 -0
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/request-human-input.ts +4 -2
- package/src/tools/send-task.ts +4 -0
- package/src/tools/templates.ts +19 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/workflows/list-workflow-runs.ts +1 -0
- package/src/types.ts +38 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/approval-cancel.ts +152 -0
- package/src/workflows/approval-notifications.ts +84 -0
- package/src/workflows/checkpoint.ts +10 -5
- package/src/workflows/definition.ts +21 -0
- package/src/workflows/engine.ts +152 -21
- package/src/workflows/executors/human-in-the-loop.ts +1 -1
- package/src/workflows/recovery.ts +100 -45
- package/src/workflows/resume.ts +49 -18
- package/src/workflows/retry-poller.ts +71 -21
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/github-sender-allowlist/README.md +18 -0
- package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
- package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
- package/templates/skills/apps/content.md +20 -1
- package/templates/skills/code-quality/content.md +26 -1
- package/templates/skills/pages/content.md +16 -4
- package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
- package/dist/cli-2daksqps.js +0 -378
- package/dist/rbac-roles-3gxmnb20.js +0 -37
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import * as db from "../be/db";
|
|
5
|
+
import {
|
|
6
|
+
closeDb,
|
|
7
|
+
createWorkflow,
|
|
8
|
+
createWorkflowRun,
|
|
9
|
+
deleteWorkflow,
|
|
10
|
+
getWorkflowRun,
|
|
11
|
+
getWorkflowRunStepsByRunId,
|
|
12
|
+
initDb,
|
|
13
|
+
} from "../be/db";
|
|
14
|
+
import type { WorkflowDefinition, WorkflowRunStep } from "../types";
|
|
15
|
+
import { walkGraph } from "../workflows/engine";
|
|
16
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
17
|
+
import {
|
|
18
|
+
BaseExecutor,
|
|
19
|
+
type ExecutorDependencies,
|
|
20
|
+
type ExecutorResult,
|
|
21
|
+
} from "../workflows/executors/base";
|
|
22
|
+
import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
23
|
+
import { recoverIncompleteRuns } from "../workflows/recovery";
|
|
24
|
+
import { cancelWorkflowRun, retryFailedRun } from "../workflows/resume";
|
|
25
|
+
import { startRetryPoller, stopRetryPoller } from "../workflows/retry-poller";
|
|
26
|
+
import {
|
|
27
|
+
completeTaskStepAndResolveSuccessors,
|
|
28
|
+
failStepAndRunIfWaiting,
|
|
29
|
+
} from "../workflows/task-step-routing";
|
|
30
|
+
import { interpolate } from "../workflows/template";
|
|
31
|
+
|
|
32
|
+
// Counterexamples from the TLA+ model in specs/tla/workflows/. Each test calls
|
|
33
|
+
// the production functions in the order of the TLC trace named in its title
|
|
34
|
+
// (specs/tla/workflows/FINDINGS.md), against a temp SQLite DB. A barrier
|
|
35
|
+
// executor holds a node "in flight" so a trace step that interleaves with a
|
|
36
|
+
// running executor can be replayed deterministically.
|
|
37
|
+
|
|
38
|
+
const TEST_DB_PATH = `./test-workflow-tla-races-${crypto.randomUUID()}.sqlite`;
|
|
39
|
+
|
|
40
|
+
type Behavior =
|
|
41
|
+
| "ok"
|
|
42
|
+
| "fail"
|
|
43
|
+
| "async"
|
|
44
|
+
| { hold: Promise<void>; entered: () => void; async?: boolean };
|
|
45
|
+
|
|
46
|
+
/** Scripted executor: each call for a node consumes the next behavior. */
|
|
47
|
+
const plans = new Map<string, Behavior[]>();
|
|
48
|
+
const calls: string[] = [];
|
|
49
|
+
|
|
50
|
+
class ScriptedExecutor extends BaseExecutor<
|
|
51
|
+
typeof ScriptedExecutor.schema,
|
|
52
|
+
typeof ScriptedExecutor.outSchema
|
|
53
|
+
> {
|
|
54
|
+
static readonly schema = z.object({});
|
|
55
|
+
static readonly outSchema = z.object({ node: z.string() });
|
|
56
|
+
|
|
57
|
+
readonly type = "scripted";
|
|
58
|
+
readonly mode = "instant" as const;
|
|
59
|
+
readonly configSchema = ScriptedExecutor.schema;
|
|
60
|
+
readonly outputSchema = ScriptedExecutor.outSchema;
|
|
61
|
+
|
|
62
|
+
protected async execute(
|
|
63
|
+
_config: z.infer<typeof ScriptedExecutor.schema>,
|
|
64
|
+
_context: Readonly<Record<string, unknown>>,
|
|
65
|
+
meta: { nodeId: string },
|
|
66
|
+
): Promise<ExecutorResult<z.infer<typeof ScriptedExecutor.outSchema>>> {
|
|
67
|
+
calls.push(meta.nodeId);
|
|
68
|
+
let behavior = plans.get(meta.nodeId)?.shift() ?? "ok";
|
|
69
|
+
if (behavior === "fail") return { status: "failed", error: `${meta.nodeId} failed` };
|
|
70
|
+
if (typeof behavior === "object" && behavior.async) {
|
|
71
|
+
behavior.entered();
|
|
72
|
+
await behavior.hold;
|
|
73
|
+
behavior = "async";
|
|
74
|
+
}
|
|
75
|
+
if (behavior === "async") {
|
|
76
|
+
return {
|
|
77
|
+
status: "success",
|
|
78
|
+
async: true,
|
|
79
|
+
waitFor: "task.completed",
|
|
80
|
+
correlationId: crypto.randomUUID(),
|
|
81
|
+
} as ExecutorResult<z.infer<typeof ScriptedExecutor.outSchema>>;
|
|
82
|
+
}
|
|
83
|
+
if (typeof behavior === "object") {
|
|
84
|
+
behavior.entered();
|
|
85
|
+
await behavior.hold;
|
|
86
|
+
}
|
|
87
|
+
return { status: "success", output: { node: meta.nodeId } };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const deps: ExecutorDependencies = {
|
|
92
|
+
db: db as typeof import("../be/db"),
|
|
93
|
+
eventBus: workflowEventBus,
|
|
94
|
+
interpolate: (template, ctx) => interpolate(template, ctx).result,
|
|
95
|
+
};
|
|
96
|
+
const registry = new ExecutorRegistry();
|
|
97
|
+
registry.register(new ScriptedExecutor(deps));
|
|
98
|
+
|
|
99
|
+
const retry = { strategy: "static" as const, maxRetries: 1, baseDelayMs: 1, maxDelayMs: 1 };
|
|
100
|
+
|
|
101
|
+
/** T fans out to A and B; with `converge`, both join on M. */
|
|
102
|
+
function fanOut(converge: boolean): WorkflowDefinition {
|
|
103
|
+
return {
|
|
104
|
+
nodes: [
|
|
105
|
+
{ id: "T", type: "scripted", config: {}, next: ["A", "B"] },
|
|
106
|
+
{ id: "A", type: "scripted", config: {}, retry, ...(converge ? { next: "M" } : {}) },
|
|
107
|
+
{ id: "B", type: "scripted", config: {}, retry, ...(converge ? { next: "M" } : {}) },
|
|
108
|
+
...(converge ? [{ id: "M", type: "scripted", config: {} }] : []),
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const workflowIds: string[] = [];
|
|
114
|
+
|
|
115
|
+
async function newRun(def: WorkflowDefinition) {
|
|
116
|
+
const workflow = await createWorkflow({
|
|
117
|
+
name: `tla-race-${crypto.randomUUID()}`,
|
|
118
|
+
definition: def,
|
|
119
|
+
});
|
|
120
|
+
workflowIds.push(workflow.id);
|
|
121
|
+
const runId = crypto.randomUUID();
|
|
122
|
+
await createWorkflowRun({ id: runId, workflowId: workflow.id, triggerType: "manual" });
|
|
123
|
+
const ctx: Record<string, unknown> = {};
|
|
124
|
+
const walk = (nodeIds: string[]) =>
|
|
125
|
+
walkGraph(
|
|
126
|
+
def,
|
|
127
|
+
runId,
|
|
128
|
+
ctx,
|
|
129
|
+
def.nodes.filter((n) => nodeIds.includes(n.id)),
|
|
130
|
+
registry,
|
|
131
|
+
workflow.id,
|
|
132
|
+
);
|
|
133
|
+
return { runId, ctx, walk };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Holds the executor until released; with `async`, it then dispatches a task. */
|
|
137
|
+
function barrier(async = false) {
|
|
138
|
+
const entered = Promise.withResolvers<void>();
|
|
139
|
+
const release = Promise.withResolvers<void>();
|
|
140
|
+
const behavior: Behavior = { hold: release.promise, entered: entered.resolve, async };
|
|
141
|
+
return { behavior, entered: entered.promise, release: release.resolve };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function stepsOf(runId: string, nodeId: string): Promise<WorkflowRunStep[]> {
|
|
145
|
+
return (await getWorkflowRunStepsByRunId(runId)).filter((s) => s.nodeId === nodeId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function untilStatus(runId: string, nodeId: string, status: string) {
|
|
149
|
+
for (let i = 0; i < 200; i++) {
|
|
150
|
+
const [step] = await stepsOf(runId, nodeId);
|
|
151
|
+
if (step?.status === status) return step;
|
|
152
|
+
await sleep(2);
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`${nodeId} never reached ${status}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const callsOf = (nodeId: string) => calls.filter((n) => n === nodeId).length;
|
|
158
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
159
|
+
|
|
160
|
+
async function runPollerOnce() {
|
|
161
|
+
startRetryPoller(registry, 1);
|
|
162
|
+
try {
|
|
163
|
+
await sleep(60);
|
|
164
|
+
} finally {
|
|
165
|
+
stopRetryPoller();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
beforeAll(() => initDb(TEST_DB_PATH));
|
|
170
|
+
|
|
171
|
+
afterAll(async () => {
|
|
172
|
+
stopRetryPoller();
|
|
173
|
+
for (const id of workflowIds) await deleteWorkflow(id).catch(() => {});
|
|
174
|
+
closeDb();
|
|
175
|
+
for (const suffix of ["", "-wal", "-shm"]) await unlink(TEST_DB_PATH + suffix).catch(() => {});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("TLA+ workflow counterexamples", () => {
|
|
179
|
+
test("CX1: the retry poller does not re-execute a failed step of a cancelled run", async () => {
|
|
180
|
+
plans.clear();
|
|
181
|
+
calls.length = 0;
|
|
182
|
+
const def = fanOut(false);
|
|
183
|
+
plans.set("A", ["fail", "ok"]);
|
|
184
|
+
const { runId, walk } = await newRun(def);
|
|
185
|
+
|
|
186
|
+
// XDedup/XRun/XFail: A fails with a retry left; B completes. Run stays running.
|
|
187
|
+
await walk(["T"]);
|
|
188
|
+
expect((await stepsOf(runId, "A"))[0]?.nextRetryAt).toBeTruthy();
|
|
189
|
+
|
|
190
|
+
// Cancel: the failed A row counts as terminal, so its nextRetryAt survives.
|
|
191
|
+
await cancelWorkflowRun(runId);
|
|
192
|
+
await sleep(5);
|
|
193
|
+
|
|
194
|
+
// P1..P9: the poller selects A with no run filter and re-executes it.
|
|
195
|
+
await runPollerOnce();
|
|
196
|
+
|
|
197
|
+
expect(callsOf("A")).toBe(1);
|
|
198
|
+
expect((await getWorkflowRun(runId))?.status).toBe("cancelled");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("CX2: a user retry does not re-execute a branch the live walk is still running", async () => {
|
|
202
|
+
plans.clear();
|
|
203
|
+
calls.length = 0;
|
|
204
|
+
const def = fanOut(true);
|
|
205
|
+
const holdA = barrier();
|
|
206
|
+
plans.set("A", [holdA.behavior, "ok"]);
|
|
207
|
+
plans.set("B", ["async", "async"]);
|
|
208
|
+
const { runId, walk } = await newRun(def);
|
|
209
|
+
|
|
210
|
+
// Initial walk: B dispatches async (waiting), A is in flight.
|
|
211
|
+
const initial = walk(["T"]);
|
|
212
|
+
await holdA.entered;
|
|
213
|
+
const stepB = await untilStatus(runId, "B", "waiting");
|
|
214
|
+
|
|
215
|
+
// EF: B's task fails -> run failed.
|
|
216
|
+
await failStepAndRunIfWaiting(stepB!.id, runId, "task failed");
|
|
217
|
+
expect((await getWorkflowRun(runId))?.status).toBe("failed");
|
|
218
|
+
|
|
219
|
+
// U1/U2: the user retries; findReadyNodes returns A because it is not completed.
|
|
220
|
+
await retryFailedRun(runId, registry);
|
|
221
|
+
holdA.release();
|
|
222
|
+
await initial;
|
|
223
|
+
|
|
224
|
+
expect(callsOf("A")).toBe(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("CX2: a user retry does not re-dispatch a branch whose waiting checkpoint lost to the run failing", async () => {
|
|
228
|
+
plans.clear();
|
|
229
|
+
calls.length = 0;
|
|
230
|
+
const def = fanOut(true);
|
|
231
|
+
const holdA = barrier(true);
|
|
232
|
+
plans.set("A", [holdA.behavior, "async"]);
|
|
233
|
+
plans.set("B", ["async", "async"]);
|
|
234
|
+
const { runId, walk } = await newRun(def);
|
|
235
|
+
|
|
236
|
+
// XRun(A): A is dispatching its task while B waits on its own.
|
|
237
|
+
const initial = walk(["T"]);
|
|
238
|
+
await holdA.entered;
|
|
239
|
+
const stepB = await untilStatus(runId, "B", "waiting");
|
|
240
|
+
|
|
241
|
+
// EF: B's task fails -> run failed. XCkWait(A): the checkpoint CAS sees a
|
|
242
|
+
// failed run, so A's row stays `running` with its task out.
|
|
243
|
+
await failStepAndRunIfWaiting(stepB!.id, runId, "task failed");
|
|
244
|
+
holdA.release();
|
|
245
|
+
await initial;
|
|
246
|
+
expect((await stepsOf(runId, "A"))[0]?.status).toBe("running");
|
|
247
|
+
|
|
248
|
+
// U1/U2: the retry must not dispatch A a second time.
|
|
249
|
+
await retryFailedRun(runId, registry);
|
|
250
|
+
|
|
251
|
+
expect(callsOf("A")).toBe(1);
|
|
252
|
+
expect(await stepsOf(runId, "A")).toHaveLength(1);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("CX3: an async branch completing does not fire the join while a sibling branch is still executing", async () => {
|
|
256
|
+
plans.clear();
|
|
257
|
+
calls.length = 0;
|
|
258
|
+
const def = fanOut(true);
|
|
259
|
+
const holdA = barrier();
|
|
260
|
+
plans.set("A", [holdA.behavior]);
|
|
261
|
+
plans.set("B", ["async"]);
|
|
262
|
+
const { runId, ctx, walk } = await newRun(def);
|
|
263
|
+
|
|
264
|
+
const initial = walk(["T"]);
|
|
265
|
+
await holdA.entered;
|
|
266
|
+
const stepB = await untilStatus(runId, "B", "waiting");
|
|
267
|
+
|
|
268
|
+
// E2: B's task completes; the handler claims B and walks its successor M.
|
|
269
|
+
const routing = await completeTaskStepAndResolveSuccessors(
|
|
270
|
+
def,
|
|
271
|
+
runId,
|
|
272
|
+
stepB!,
|
|
273
|
+
{ node: "B" },
|
|
274
|
+
ctx,
|
|
275
|
+
);
|
|
276
|
+
expect(routing.claimed).toBe(true);
|
|
277
|
+
await walk(routing.successors.map((n) => n.id));
|
|
278
|
+
|
|
279
|
+
const mBeforeA = callsOf("M");
|
|
280
|
+
const statusBeforeA = (await getWorkflowRun(runId))?.status;
|
|
281
|
+
holdA.release();
|
|
282
|
+
await initial;
|
|
283
|
+
|
|
284
|
+
expect(mBeforeA).toBe(0);
|
|
285
|
+
expect(statusBeforeA).not.toBe("completed");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("CX4: two branch completions racing to the join execute it once", async () => {
|
|
289
|
+
plans.clear();
|
|
290
|
+
calls.length = 0;
|
|
291
|
+
const def = fanOut(true);
|
|
292
|
+
plans.set("A", ["async"]);
|
|
293
|
+
plans.set("B", ["async"]);
|
|
294
|
+
const { runId, ctx, walk } = await newRun(def);
|
|
295
|
+
|
|
296
|
+
await walk(["T"]);
|
|
297
|
+
const [stepA] = await stepsOf(runId, "A");
|
|
298
|
+
const [stepB] = await stepsOf(runId, "B");
|
|
299
|
+
|
|
300
|
+
// E2, E2: both handlers claim their step before either walks M.
|
|
301
|
+
const ra = await completeTaskStepAndResolveSuccessors(def, runId, stepA!, { node: "A" }, ctx);
|
|
302
|
+
const rb = await completeTaskStepAndResolveSuccessors(def, runId, stepB!, { node: "B" }, ctx);
|
|
303
|
+
await Promise.all([walk(ra.successors.map((n) => n.id)), walk(rb.successors.map((n) => n.id))]);
|
|
304
|
+
|
|
305
|
+
expect(callsOf("M")).toBe(1);
|
|
306
|
+
expect(await stepsOf(runId, "M")).toHaveLength(1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("CX5: heartbeat recovery leaves a step that is pending retry to the retry poller", async () => {
|
|
310
|
+
plans.clear();
|
|
311
|
+
calls.length = 0;
|
|
312
|
+
const def = fanOut(false);
|
|
313
|
+
plans.set("A", ["fail", "ok", "ok"]);
|
|
314
|
+
const { runId, walk } = await newRun(def);
|
|
315
|
+
|
|
316
|
+
// A fails with a retry pending; B completes; the walk ends with the run running.
|
|
317
|
+
await walk(["T"]);
|
|
318
|
+
expect((await getWorkflowRun(runId))?.status).toBe("running");
|
|
319
|
+
|
|
320
|
+
// H1..H3: recovery sees a running, idle run and re-walks A (findReadyNodes).
|
|
321
|
+
await recoverIncompleteRuns(registry);
|
|
322
|
+
// With A excluded nothing is ready, and recovery must not complete the run
|
|
323
|
+
// while A's retry is pending.
|
|
324
|
+
expect((await getWorkflowRun(runId))?.status).toBe("running");
|
|
325
|
+
await sleep(5);
|
|
326
|
+
// P1..P6: the poller retries the original failed A row.
|
|
327
|
+
await runPollerOnce();
|
|
328
|
+
|
|
329
|
+
// One original attempt plus one retry.
|
|
330
|
+
expect(callsOf("A")).toBe(2);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("CX6: heartbeat recovery does not walk a run whose trigger is still resolving inputs", async () => {
|
|
334
|
+
plans.clear();
|
|
335
|
+
calls.length = 0;
|
|
336
|
+
const def = fanOut(false);
|
|
337
|
+
const holdT = barrier();
|
|
338
|
+
plans.set("T", [holdT.behavior, "ok"]);
|
|
339
|
+
|
|
340
|
+
// IStart: startWorkflowExecution has committed the run as `running` and is
|
|
341
|
+
// awaiting resolveRenderedWorkflowInputs, so no walk is registered yet.
|
|
342
|
+
const { runId, walk } = await newRun(def);
|
|
343
|
+
|
|
344
|
+
// H1..H3: the sweep sees a running, idle run with no steps and walks T.
|
|
345
|
+
const recovery = recoverIncompleteRuns(registry);
|
|
346
|
+
await holdT.entered;
|
|
347
|
+
|
|
348
|
+
// IWalk: input resolution finishes and the trigger's own walk starts T.
|
|
349
|
+
await walk(["T"]);
|
|
350
|
+
holdT.release();
|
|
351
|
+
await recovery;
|
|
352
|
+
|
|
353
|
+
expect(callsOf("T")).toBe(1);
|
|
354
|
+
expect(await stepsOf(runId, "T")).toHaveLength(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("CX7: the walk finalizer does not complete a run while the retry poller is executing a step", async () => {
|
|
358
|
+
plans.clear();
|
|
359
|
+
calls.length = 0;
|
|
360
|
+
const def = fanOut(false);
|
|
361
|
+
const holdB = barrier();
|
|
362
|
+
const holdA = barrier();
|
|
363
|
+
plans.set("A", ["fail", holdA.behavior]);
|
|
364
|
+
plans.set("B", [holdB.behavior]);
|
|
365
|
+
const { runId, walk } = await newRun(def);
|
|
366
|
+
|
|
367
|
+
// XFail: A fails with a retry pending while B is still executing.
|
|
368
|
+
const initial = walk(["T"]);
|
|
369
|
+
await holdB.entered;
|
|
370
|
+
await untilStatus(runId, "A", "failed");
|
|
371
|
+
await sleep(5);
|
|
372
|
+
|
|
373
|
+
// P1..P5: the poller claims A and is executing it.
|
|
374
|
+
startRetryPoller(registry, 1);
|
|
375
|
+
try {
|
|
376
|
+
await holdA.entered;
|
|
377
|
+
// XCkOk/WFinal: B finishes and the walk finalizes with A running.
|
|
378
|
+
holdB.release();
|
|
379
|
+
await initial;
|
|
380
|
+
const statusWhileARuns = (await getWorkflowRun(runId))?.status;
|
|
381
|
+
holdA.release();
|
|
382
|
+
await sleep(20);
|
|
383
|
+
expect(statusWhileARuns).not.toBe("completed");
|
|
384
|
+
} finally {
|
|
385
|
+
holdA.release();
|
|
386
|
+
stopRetryPoller();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
getWorkflowRunStepsByRunId,
|
|
9
9
|
initDb,
|
|
10
10
|
} from "../be/db";
|
|
11
|
-
import type { Workflow, WorkflowDefinition } from "../types";
|
|
11
|
+
import type { Workflow, WorkflowDefinition, WorkflowNode } from "../types";
|
|
12
|
+
import { loadCompletedStepRouting } from "../workflows/completed-step-routing";
|
|
12
13
|
import { startWorkflowExecution } from "../workflows/engine";
|
|
13
14
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
14
15
|
import {
|
|
@@ -47,6 +48,28 @@ class ObjectOutputExecutor extends BaseExecutor<
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Mimics the `script` / `swarm-script` executors: always emits `nextPort: "success"`.
|
|
53
|
+
*/
|
|
54
|
+
class SuccessPortExecutor extends BaseExecutor<
|
|
55
|
+
typeof SuccessPortExecutor.schema,
|
|
56
|
+
typeof SuccessPortExecutor.outSchema
|
|
57
|
+
> {
|
|
58
|
+
static readonly schema = z.object({ approved: z.boolean() });
|
|
59
|
+
static readonly outSchema = z.object({ approved: z.boolean() });
|
|
60
|
+
|
|
61
|
+
readonly type = "success-port";
|
|
62
|
+
readonly mode = "instant" as const;
|
|
63
|
+
readonly configSchema = SuccessPortExecutor.schema;
|
|
64
|
+
readonly outputSchema = SuccessPortExecutor.outSchema;
|
|
65
|
+
|
|
66
|
+
protected async execute(
|
|
67
|
+
config: z.infer<typeof SuccessPortExecutor.schema>,
|
|
68
|
+
): Promise<ExecutorResult<z.infer<typeof SuccessPortExecutor.outSchema>>> {
|
|
69
|
+
return { status: "success", output: { approved: config.approved }, nextPort: "success" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
/**
|
|
51
74
|
* Executor that mimics an agent-task review output shape.
|
|
52
75
|
* Used for regression coverage around aliased property-match fields.
|
|
@@ -101,6 +124,7 @@ function createTestRegistry(): ExecutorRegistry {
|
|
|
101
124
|
const registry = new ExecutorRegistry();
|
|
102
125
|
registry.register(new ObjectOutputExecutor(mockDeps));
|
|
103
126
|
registry.register(new ReviewOutputExecutor(mockDeps));
|
|
127
|
+
registry.register(new SuccessPortExecutor(mockDeps));
|
|
104
128
|
registry.register(new NoopExecutor(mockDeps));
|
|
105
129
|
registry.register(new PropertyMatchExecutor(mockDeps));
|
|
106
130
|
return registry;
|
|
@@ -400,3 +424,91 @@ describe("Validation Port Routing", () => {
|
|
|
400
424
|
expect(nodeIds).toContain("after-check");
|
|
401
425
|
});
|
|
402
426
|
});
|
|
427
|
+
|
|
428
|
+
describe("Validation port routing when the executor emits its own port", () => {
|
|
429
|
+
function makeScriptLikeWorkflow(
|
|
430
|
+
approved: boolean,
|
|
431
|
+
next: WorkflowNode["next"],
|
|
432
|
+
withValidation = true,
|
|
433
|
+
): WorkflowDefinition {
|
|
434
|
+
return {
|
|
435
|
+
nodes: [
|
|
436
|
+
{
|
|
437
|
+
id: "check",
|
|
438
|
+
type: "success-port",
|
|
439
|
+
config: { approved },
|
|
440
|
+
next,
|
|
441
|
+
...(withValidation
|
|
442
|
+
? {
|
|
443
|
+
validation: {
|
|
444
|
+
executor: "property-match",
|
|
445
|
+
config: {
|
|
446
|
+
conditions: [{ field: "check.approved", op: "eq", value: true }],
|
|
447
|
+
},
|
|
448
|
+
mustPass: false,
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
: {}),
|
|
452
|
+
},
|
|
453
|
+
{ id: "on-pass", type: "noop", config: {} },
|
|
454
|
+
{ id: "on-fail", type: "noop", config: {} },
|
|
455
|
+
{ id: "on-success", type: "noop", config: {} },
|
|
456
|
+
],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function runAndCollect(def: WorkflowDefinition) {
|
|
461
|
+
const workflow = await makeWorkflow(def);
|
|
462
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
463
|
+
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
464
|
+
return { workflow, runId, steps, nodeIds: steps.map((s) => s.nodeId) };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
test('script-like node + validation pass → "pass" port, checkpointed for resume', async () => {
|
|
468
|
+
const { workflow, runId, steps, nodeIds } = await runAndCollect(
|
|
469
|
+
makeScriptLikeWorkflow(true, { pass: "on-pass", fail: "on-fail" }),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
expect(nodeIds).toContain("on-pass");
|
|
473
|
+
expect(nodeIds).not.toContain("on-fail");
|
|
474
|
+
expect(steps.find((s) => s.nodeId === "check")!.nextPort).toBe("pass");
|
|
475
|
+
|
|
476
|
+
// Recovery / resume rebuild active edges from the checkpointed nextPort.
|
|
477
|
+
const { activeEdges } = await loadCompletedStepRouting(
|
|
478
|
+
workflow.definition,
|
|
479
|
+
runId,
|
|
480
|
+
new Set(["check"]),
|
|
481
|
+
);
|
|
482
|
+
expect([...activeEdges]).toEqual(["check→on-pass"]);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test('script-like node + validation fail (mustPass: false) → "fail" port', async () => {
|
|
486
|
+
const { steps, nodeIds } = await runAndCollect(
|
|
487
|
+
makeScriptLikeWorkflow(false, { pass: "on-pass", fail: "on-fail" }),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
expect(nodeIds).toContain("on-fail");
|
|
491
|
+
expect(nodeIds).not.toContain("on-pass");
|
|
492
|
+
expect(steps.find((s) => s.nodeId === "check")!.nextPort).toBe("fail");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("executor port that is a key of next stays authoritative over validation", async () => {
|
|
496
|
+
const { steps, nodeIds } = await runAndCollect(
|
|
497
|
+
makeScriptLikeWorkflow(false, { success: "on-success", fail: "on-fail" }),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
expect(nodeIds).toContain("on-success");
|
|
501
|
+
expect(nodeIds).not.toContain("on-fail");
|
|
502
|
+
expect(steps.find((s) => s.nodeId === "check")!.nextPort).toBe("success");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("node without validation keeps the executor port", async () => {
|
|
506
|
+
const { steps, nodeIds } = await runAndCollect(
|
|
507
|
+
makeScriptLikeWorkflow(true, { pass: "on-pass", fail: "on-fail" }, false),
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
expect(nodeIds).not.toContain("on-pass");
|
|
511
|
+
expect(nodeIds).not.toContain("on-fail");
|
|
512
|
+
expect(steps.find((s) => s.nodeId === "check")!.nextPort).toBe("success");
|
|
513
|
+
});
|
|
514
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { getAgentById } from "@/be/db";
|
|
4
|
+
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
5
|
+
import { cancelApprovalRequest } from "@/workflows/approval-cancel";
|
|
6
|
+
|
|
7
|
+
export const cancelApprovalRequestInputSchema = z.object({
|
|
8
|
+
requestId: z.uuid().describe("The ID of the approval request to cancel."),
|
|
9
|
+
reason: z.string().max(500).optional().describe("Reason for cancellation."),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const cancelApprovalRequestOutputSchema = swarmToolOutputSchema({
|
|
13
|
+
yourAgentId: z.string().optional(),
|
|
14
|
+
approvalRequest: z.looseObject({}).optional(),
|
|
15
|
+
alreadyCancelled: z.boolean().optional(),
|
|
16
|
+
runCancelled: z.boolean().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const registerCancelApprovalRequestTool = (server: McpServer) => {
|
|
20
|
+
createToolRegistrar(server)(
|
|
21
|
+
"cancel-approval-request",
|
|
22
|
+
{
|
|
23
|
+
title: "Cancel approval request",
|
|
24
|
+
description:
|
|
25
|
+
"Cancel a pending approval request by id. Allowed for a lead agent, or for the agent that owns the request's source task. If the request gates a running or waiting workflow run, that run is cancelled too. A request whose explicit timeout passed is already 'timeout' and cannot be cancelled.",
|
|
26
|
+
annotations: { destructiveHint: true },
|
|
27
|
+
inputSchema: cancelApprovalRequestInputSchema,
|
|
28
|
+
outputSchema: cancelApprovalRequestOutputSchema,
|
|
29
|
+
},
|
|
30
|
+
async ({ requestId, reason }, info) => {
|
|
31
|
+
if (!info.agentId) {
|
|
32
|
+
return toolErr('Agent ID not found. Set the "X-Agent-ID" header.');
|
|
33
|
+
}
|
|
34
|
+
const agent = await getAgentById(info.agentId);
|
|
35
|
+
const result = await cancelApprovalRequest({
|
|
36
|
+
id: requestId,
|
|
37
|
+
reason,
|
|
38
|
+
principal: { kind: "agent", agentId: info.agentId, isLead: agent?.isLead ?? false },
|
|
39
|
+
resolvedBy: info.agentId,
|
|
40
|
+
source: "mcp",
|
|
41
|
+
});
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
return toolErr(result.message, { data: { yourAgentId: info.agentId } });
|
|
44
|
+
}
|
|
45
|
+
const data = {
|
|
46
|
+
yourAgentId: info.agentId,
|
|
47
|
+
approvalRequest: result.request,
|
|
48
|
+
alreadyCancelled: result.alreadyCancelled,
|
|
49
|
+
runCancelled: result.runCancelled,
|
|
50
|
+
};
|
|
51
|
+
const message = result.alreadyCancelled
|
|
52
|
+
? `Approval request "${requestId}" was already cancelled.`
|
|
53
|
+
: `Approval request "${requestId}" cancelled${result.runCancelled ? ", and its workflow run with it" : ""}.`;
|
|
54
|
+
return toolOk(message, { data, details: JSON.stringify(data, null, 2) });
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -37,7 +37,7 @@ export const registerRequestHumanInputTool = (server: McpServer) => {
|
|
|
37
37
|
title: "Request human input",
|
|
38
38
|
annotations: { destructiveHint: false },
|
|
39
39
|
description:
|
|
40
|
-
"Create an approval request
|
|
40
|
+
"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
41
|
"Supports multiple question types: approval (yes/no), text, single-select, " +
|
|
42
42
|
"multi-select, and boolean. Returns the request ID and URL for the human to respond.",
|
|
43
43
|
inputSchema: z.object({
|
|
@@ -48,7 +48,9 @@ export const registerRequestHumanInputTool = (server: McpServer) => {
|
|
|
48
48
|
.int()
|
|
49
49
|
.min(1)
|
|
50
50
|
.optional()
|
|
51
|
-
.describe(
|
|
51
|
+
.describe(
|
|
52
|
+
"Seconds until the request expires. After that the request becomes 'timeout' and you get a hitl-follow-up task. A request with no timeout is cancelled after APPROVAL_REQUEST_AUTO_CANCELLATION_DAYS days (default 7).",
|
|
53
|
+
),
|
|
52
54
|
}),
|
|
53
55
|
outputSchema: swarmToolOutputSchema({
|
|
54
56
|
yourAgentId: z.string().optional(),
|
package/src/tools/send-task.ts
CHANGED
|
@@ -409,6 +409,10 @@ export async function sendTaskHandler(
|
|
|
409
409
|
slackUserId,
|
|
410
410
|
overrideSlackContext,
|
|
411
411
|
followUpConfig,
|
|
412
|
+
// A delegation with parentTaskId is new work and starts without the
|
|
413
|
+
// parent's followUpConfig. A `resume` re-delegation (the reroute-decision
|
|
414
|
+
// template) continues the parent's work, so it keeps it.
|
|
415
|
+
inheritParentFollowUpConfig: taskType === "resume",
|
|
412
416
|
outputSchema,
|
|
413
417
|
routingReason: effectiveRoutingReason,
|
|
414
418
|
routingSource: effectiveRoutingSource,
|
package/src/tools/templates.ts
CHANGED
|
@@ -110,6 +110,25 @@ Continue your work based on the human's input.`,
|
|
|
110
110
|
category: "task_lifecycle",
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
registerTemplate({
|
|
114
|
+
eventType: "hitl.timeout",
|
|
115
|
+
header: "",
|
|
116
|
+
defaultBody: `Your approval request ({{request_id}}) timed out with no answer.
|
|
117
|
+
|
|
118
|
+
Title: {{title}}
|
|
119
|
+
Deadline: {{expires_at}}
|
|
120
|
+
Reason: {{reason}}
|
|
121
|
+
|
|
122
|
+
No human responded before the deadline. Decide how to continue without this input.`,
|
|
123
|
+
variables: [
|
|
124
|
+
{ name: "request_id", description: "The approval request ID" },
|
|
125
|
+
{ name: "title", description: "Title of the approval request" },
|
|
126
|
+
{ name: "expires_at", description: "The deadline that passed, ISO 8601" },
|
|
127
|
+
{ name: "reason", description: "The resolutionReason stored on the request" },
|
|
128
|
+
],
|
|
129
|
+
category: "task_lifecycle",
|
|
130
|
+
});
|
|
131
|
+
|
|
113
132
|
registerTemplate({
|
|
114
133
|
eventType: "task.worker.failed",
|
|
115
134
|
header: "",
|
package/src/tools/tool-config.ts
CHANGED
|
@@ -85,6 +85,7 @@ export async function listWorkflowRunsHandler({
|
|
|
85
85
|
status,
|
|
86
86
|
limit,
|
|
87
87
|
offset,
|
|
88
|
+
includeContext,
|
|
88
89
|
});
|
|
89
90
|
const resultRuns = includeContext ? runs : runs.map(slimRun);
|
|
90
91
|
return toolOk(`Found ${runs.length} run(s) at offset ${page.offset} (${page.total} total).`, {
|