@desplega.ai/agent-swarm 1.123.0 → 1.124.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/README.md +2 -1
- package/dist/{actions-e4c9f0xy.js → actions-z3d0fk7z.js} +6 -6
- package/dist/{app-vmvmj2v3.js → app-q2rc9tdr.js} +4 -4
- package/dist/{assistant-yn7a792a.js → assistant-3stjq7fz.js} +9 -9
- package/dist/{boot-reembed-xg1r86h2.js → boot-reembed-5867whnb.js} +3 -3
- package/dist/{boot-reembed-chxty50a.js → boot-reembed-v6r1df32.js} +4 -4
- package/dist/{boot-scrub-logs-exwddvp5.js → boot-scrub-logs-8b8jegk3.js} +2 -2
- package/dist/{cli-1592nfd6.js → cli-0925phzv.js} +1 -1
- package/dist/{cli-hbbxmhec.js → cli-2yr6edr7.js} +4 -4
- package/dist/{cli-8cq4z4x7.js → cli-3z4q09tn.js} +182 -19
- package/dist/{cli-vz9drmce.js → cli-5jn6wnd9.js} +17 -8
- package/dist/{cli-5arfywtm.js → cli-5ncdb7ff.js} +19 -9
- package/dist/{cli-3tv9z4wx.js → cli-92bqgbgy.js} +1 -1
- package/dist/{cli-fjf04hpa.js → cli-dq2ssyf0.js} +48 -56
- package/dist/{cli-mngsj9fx.js → cli-gkvf8d83.js} +1 -1
- package/dist/{cli-mepk1j0t.js → cli-gzjsamwc.js} +2 -2
- package/dist/{cli-c9m59f83.js → cli-h89g0qrd.js} +1 -1
- package/dist/{cli-n8mf7x4b.js → cli-jp4mdj9d.js} +2 -2
- package/dist/{cli-822d4bjk.js → cli-kk58tg33.js} +7 -7
- package/dist/{cli-9hj1adgp.js → cli-mcez7z4p.js} +1 -1
- package/dist/{cli-byv3720g.js → cli-mmk5vagg.js} +2 -2
- package/dist/{cli-zdp11t8m.js → cli-p0ec4vv6.js} +1 -1
- package/dist/{cli-h2say6xk.js → cli-p0zgv8pj.js} +3 -1
- package/dist/{cli-8ef5ky75.js → cli-perb20ma.js} +1 -1
- package/dist/{cli-zkj6f2sk.js → cli-qf7acvyr.js} +90 -4
- package/dist/{cli-kmm1rwmp.js → cli-qwsagsmg.js} +1 -1
- package/dist/{cli-we91ywvy.js → cli-sqv6c9b8.js} +6 -6
- package/dist/{cli-np0yhmmk.js → cli-vm20sh8d.js} +3 -3
- package/dist/{cli-p11rf59d.js → cli-wn3m6q54.js} +3 -3
- package/dist/{cli-mkpf4ndf.js → cli-x268say8.js} +2976 -5621
- package/dist/{cli-ngbsk4yx.js → cli-y43n987v.js} +1 -1
- package/dist/{cli-4509mwm5.js → cli-ykthjw29.js} +8 -8
- package/dist/{cli-9eka6jwv.js → cli-zxb72ctk.js} +2 -2
- package/dist/cli.js +12 -10
- package/dist/{commands-sjk3zfyr.js → commands-sda76yn8.js} +2 -2
- package/dist/{db-2je5agjb.js → db-vn9e4rqj.js} +6 -2
- package/dist/{e2b-2s4psnbf.js → e2b-q345y8s4.js} +0 -4
- package/dist/{handlers-yzv369hf.js → handlers-xfja18ac.js} +9 -9
- package/dist/{hook-rfsj2r8x.js → hook-2medrp47.js} +1 -40
- package/dist/{http-v9rr144m.js → http-937pyjfc.js} +99 -44
- package/dist/{index-nr2199m6.js → index-2bqd2q9v.js} +11 -11
- package/dist/{index-3xtk777g.js → index-gw4en7jg.js} +9 -9
- package/dist/{index-c3tan44f.js → index-knqczmnm.js} +10 -10
- package/dist/{index-smjdc3h9.js → index-vah60vxp.js} +8 -8
- package/dist/{keepalive-5ds53bjd.js → keepalive-20rd6hxh.js} +5 -5
- package/dist/{lead-5xhez25b.js → lead-7w2k3a49.js} +21 -21
- package/dist/{maintenance-b43d24q6.js → maintenance-p3x918yw.js} +4 -4
- package/dist/{oauth-refresh-sweep-qe286vn2.js → oauth-refresh-sweep-shm0nzm4.js} +4 -4
- package/dist/{onboard-c4r3ds3z.js → onboard-py9dnf54.js} +6 -2
- package/dist/{otel-impl-nv3rjw61.js → otel-impl-7yjkfg68.js} +1 -1
- package/dist/{pi-mono-adapter-2syx1x4d.js → pi-mono-adapter-z6h98fva.js} +4 -38
- package/dist/{pricing-refresh-5ct2wp4k.js → pricing-refresh-3snb6370.js} +4 -4
- package/dist/{rbac-roles-m502w0qv.js → rbac-roles-5e72bbpk.js} +3 -3
- package/dist/{rbac-roles-b82cxpxn.js → rbac-roles-gqxdp8st.js} +4 -4
- package/dist/{seed-pricing-jhjphbf0.js → seed-pricing-bg42d7h2.js} +3 -3
- package/dist/{setup-zbaspydq.js → setup-nzk3kbbq.js} +2 -2
- package/dist/{worker-snvhpax5.js → worker-0p1jdzkm.js} +21 -21
- package/openapi.json +146 -1
- package/package.json +3 -1
- package/src/be/db.ts +81 -4
- package/src/be/models-catalog.ts +156 -0
- package/src/be/modelsdev-cache.ts +14 -1
- package/src/be/pricing-refresh.ts +2 -0
- package/src/be/scripts/typecheck.ts +17 -4
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +1 -1
- package/src/commands/onboard/env-generator.ts +12 -0
- package/src/e2b/env.ts +0 -4
- package/src/hooks/hook.ts +0 -67
- package/src/http/all-routes.ts +1 -0
- package/src/http/index.ts +2 -0
- package/src/http/models-catalog.ts +65 -0
- package/src/http/workflows.ts +16 -4
- package/src/prompts/base-prompt.ts +8 -7
- package/src/prompts/session-templates.ts +106 -12
- package/src/providers/pi-mono-adapter.ts +12 -2
- package/src/providers/pi-mono-extension.ts +0 -64
- package/src/providers/pricing-sources.md +16 -1
- package/src/scripts-runtime/eval-harness.ts +18 -1
- package/src/scripts-runtime/swarm-sdk.ts +10 -0
- package/src/scripts-runtime/types/stdlib.d.ts +13 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +13 -0
- package/src/server-user.ts +4 -7
- package/src/slack/blocks.ts +21 -8
- package/src/slack/responses.ts +27 -51
- package/src/slack/watcher.ts +22 -5
- package/src/telemetry.ts +169 -3
- package/src/tests/base-prompt.test.ts +79 -0
- package/src/tests/mcp-tools-user.test.ts +62 -16
- package/src/tests/mcp-tools.test.ts +6 -3
- package/src/tests/memory-edit.test.ts +21 -4
- package/src/tests/model-groups-live-catalog.test.ts +65 -0
- package/src/tests/models-catalog.test.ts +119 -0
- package/src/tests/oauth-access-token-tool.test.ts +71 -1
- package/src/tests/pricing-refresh.test.ts +25 -0
- package/src/tests/prompt-template-session.test.ts +142 -5
- package/src/tests/rbac-charact-skills.test.ts +81 -14
- package/src/tests/script-apis-mcp.test.ts +56 -8
- package/src/tests/scripts-mcp-e2e.test.ts +157 -8
- package/src/tests/scripts-typecheck.test.ts +17 -0
- package/src/tests/sdk-allowlist.test.ts +34 -1
- package/src/tests/seed-scripts.test.ts +43 -0
- package/src/tests/slack-blocks.test.ts +2 -1
- package/src/tests/slack-inline-output.test.ts +24 -30
- package/src/tests/slack-watcher.test.ts +42 -0
- package/src/tests/steer-task-tool.test.ts +37 -8
- package/src/tests/steering-transport.test.ts +18 -3
- package/src/tests/swarm-tool-result-gate.test.ts +399 -0
- package/src/tests/task-tools-ctx.test.ts +49 -17
- package/src/tests/task-tools-ownership.test.ts +41 -13
- package/src/tests/telemetry-init.test.ts +376 -2
- package/src/tests/tool-output-agent-id.test.ts +14 -2
- package/src/tests/workflow-http-v2.test.ts +116 -0
- package/src/tools/accept-steer.ts +45 -53
- package/src/tools/cancel-task.ts +31 -41
- package/src/tools/context-diff.ts +19 -63
- package/src/tools/context-history.ts +22 -51
- package/src/tools/create-channel.ts +24 -39
- package/src/tools/create-metric.ts +19 -82
- package/src/tools/create-page.ts +20 -81
- package/src/tools/credential-bindings/tool.ts +125 -246
- package/src/tools/db-query.ts +13 -24
- package/src/tools/delete-channel.ts +25 -71
- package/src/tools/delete-page.ts +27 -75
- package/src/tools/get-metrics.ts +29 -22
- package/src/tools/get-swarm.ts +44 -15
- package/src/tools/get-task-details.ts +150 -32
- package/src/tools/get-tasks.ts +63 -29
- package/src/tools/inject-learning.ts +8 -38
- package/src/tools/join-swarm.ts +45 -47
- package/src/tools/kv/kv-delete.ts +13 -37
- package/src/tools/kv/kv-get.ts +38 -29
- package/src/tools/kv/kv-incr.ts +28 -56
- package/src/tools/kv/kv-list.ts +40 -31
- package/src/tools/kv/kv-set.ts +34 -76
- package/src/tools/list-channels.ts +24 -17
- package/src/tools/list-services.ts +37 -45
- package/src/tools/manage-user.ts +53 -66
- package/src/tools/mcp-servers/mcp-server-create.ts +19 -55
- package/src/tools/mcp-servers/mcp-server-delete.ts +9 -39
- package/src/tools/mcp-servers/mcp-server-get.ts +9 -35
- package/src/tools/mcp-servers/mcp-server-install.ts +13 -60
- package/src/tools/mcp-servers/mcp-server-list.ts +28 -26
- package/src/tools/mcp-servers/mcp-server-uninstall.ts +10 -36
- package/src/tools/mcp-servers/mcp-server-update.ts +13 -57
- package/src/tools/memory-delete.ts +16 -46
- package/src/tools/memory-edit.ts +48 -49
- package/src/tools/memory-get.ts +40 -36
- package/src/tools/memory-rate.ts +14 -44
- package/src/tools/memory-search.ts +46 -52
- package/src/tools/my-agent-info.ts +39 -33
- package/src/tools/oauth-access-token.ts +9 -24
- package/src/tools/poll-task.ts +50 -77
- package/src/tools/post-message.ts +24 -38
- package/src/tools/prompt-templates/delete.ts +16 -48
- package/src/tools/prompt-templates/get.ts +45 -42
- package/src/tools/prompt-templates/list.ts +29 -44
- package/src/tools/prompt-templates/preview.ts +14 -40
- package/src/tools/prompt-templates/set.ts +30 -52
- package/src/tools/read-messages.ts +45 -54
- package/src/tools/register-agentmail-inbox.ts +28 -84
- package/src/tools/register-kapso-number.ts +20 -50
- package/src/tools/register-service.ts +36 -44
- package/src/tools/repos/get-repos.ts +29 -21
- package/src/tools/repos/update-repo.ts +30 -27
- package/src/tools/request-human-input.ts +10 -33
- package/src/tools/resolve-user.ts +59 -16
- package/src/tools/schedules/create-schedule.ts +58 -173
- package/src/tools/schedules/delete-schedule.ts +16 -56
- package/src/tools/schedules/list-schedules.ts +41 -67
- package/src/tools/schedules/patch-schedule.ts +57 -153
- package/src/tools/schedules/run-schedule-now.ts +18 -59
- package/src/tools/schedules/update-schedule.ts +57 -153
- package/src/tools/script-apis.ts +46 -28
- package/src/tools/script-common.ts +129 -34
- package/src/tools/script-connections/tool.ts +59 -154
- package/src/tools/script-query-types.ts +43 -3
- package/src/tools/script-run.ts +41 -2
- package/src/tools/script-runs.ts +69 -1
- package/src/tools/script-search.ts +24 -1
- package/src/tools/script-upsert.ts +18 -3
- package/src/tools/send-task.ts +32 -81
- package/src/tools/skills/skill-create.ts +14 -39
- package/src/tools/skills/skill-delete.ts +14 -42
- package/src/tools/skills/skill-get-file.ts +10 -43
- package/src/tools/skills/skill-get.ts +9 -32
- package/src/tools/skills/skill-install-remote.ts +16 -52
- package/src/tools/skills/skill-install.ts +14 -52
- package/src/tools/skills/skill-list.ts +24 -26
- package/src/tools/skills/skill-publish.ts +18 -54
- package/src/tools/skills/skill-search.ts +21 -18
- package/src/tools/skills/skill-sync-remote.ts +12 -24
- package/src/tools/skills/skill-uninstall.ts +10 -26
- package/src/tools/skills/skill-update.ts +24 -81
- package/src/tools/slack-delete.ts +8 -32
- package/src/tools/slack-download-file.ts +17 -57
- package/src/tools/slack-list-channels.ts +16 -43
- package/src/tools/slack-post.ts +11 -40
- package/src/tools/slack-read.ts +33 -103
- package/src/tools/slack-reply.ts +16 -53
- package/src/tools/slack-start-thread.ts +14 -55
- package/src/tools/slack-update.ts +8 -35
- package/src/tools/slack-upload-file.ts +22 -122
- package/src/tools/steer-task.ts +22 -35
- package/src/tools/store-progress.ts +11 -26
- package/src/tools/swarm-config/delete-config.ts +19 -57
- package/src/tools/swarm-config/get-config.ts +36 -44
- package/src/tools/swarm-config/list-config.ts +36 -44
- package/src/tools/swarm-config/set-config.ts +35 -73
- package/src/tools/swarm-x.ts +20 -26
- package/src/tools/task-action.ts +26 -46
- package/src/tools/task-tool-ctx.ts +5 -13
- package/src/tools/tracker/tracker-link-task.ts +8 -22
- package/src/tools/tracker/tracker-map-agent.ts +8 -22
- package/src/tools/tracker/tracker-status.ts +27 -19
- package/src/tools/tracker/tracker-sync-status.ts +8 -18
- package/src/tools/tracker/tracker-unlink.ts +4 -16
- package/src/tools/unregister-service.ts +19 -59
- package/src/tools/update-profile.ts +60 -100
- package/src/tools/update-service-status.ts +42 -67
- package/src/tools/utils.ts +189 -8
- package/src/tools/whatsapp-message.ts +19 -32
- package/src/tools/workflows/cancel-workflow-run.ts +4 -16
- package/src/tools/workflows/create-workflow.ts +9 -38
- package/src/tools/workflows/delete-workflow.ts +5 -17
- package/src/tools/workflows/get-workflow-run.ts +41 -31
- package/src/tools/workflows/get-workflow.ts +10 -21
- package/src/tools/workflows/list-workflow-runs.ts +107 -34
- package/src/tools/workflows/list-workflows.ts +5 -17
- package/src/tools/workflows/patch-workflow-node.ts +11 -39
- package/src/tools/workflows/patch-workflow.ts +11 -36
- package/src/tools/workflows/retry-workflow-run.ts +4 -16
- package/src/tools/workflows/trigger-workflow.ts +22 -58
- package/src/tools/workflows/update-workflow.ts +10 -42
- package/templates/skills/swarm-scripts/SKILL.md +1 -1
package/src/slack/watcher.ts
CHANGED
|
@@ -301,6 +301,24 @@ export async function processTreeMessages(): Promise<void> {
|
|
|
301
301
|
// Check if tree is fully terminal — do one final render then clean up
|
|
302
302
|
const fullyTerminal = isTreeFullyTerminal(nodes);
|
|
303
303
|
|
|
304
|
+
// Deliver any output that the compact tree would truncate before the tree
|
|
305
|
+
// is finalized and removed from tracking. Previously cleanup marked these
|
|
306
|
+
// tasks notified first, so the completion loop below never posted them.
|
|
307
|
+
let outputDeliverySucceeded = true;
|
|
308
|
+
for (const node of nodes.flatMap((root) => [root, ...root.children])) {
|
|
309
|
+
const task = getTaskById(node.taskId);
|
|
310
|
+
if (!task || !shouldPostInlineCompletionOutput(task)) continue;
|
|
311
|
+
|
|
312
|
+
const sent = await sendInlineTaskOutput(task);
|
|
313
|
+
if (sent) {
|
|
314
|
+
node.slackReplySent = true;
|
|
315
|
+
notifiedCompletions.set(task.id, now);
|
|
316
|
+
console.log(`[Slack] Sent full output for tree-tracked completion ${task.id.slice(0, 8)}`);
|
|
317
|
+
} else {
|
|
318
|
+
outputDeliverySucceeded = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
304
322
|
// Build blocks
|
|
305
323
|
let blocks: unknown[];
|
|
306
324
|
try {
|
|
@@ -315,7 +333,7 @@ export async function processTreeMessages(): Promise<void> {
|
|
|
315
333
|
const lastSerialized = lastRenderedTree.get(messageTs);
|
|
316
334
|
if (serialized === lastSerialized) {
|
|
317
335
|
// No changes — skip update
|
|
318
|
-
if (fullyTerminal) {
|
|
336
|
+
if (fullyTerminal && outputDeliverySucceeded) {
|
|
319
337
|
// Even though nothing changed visually, clean up if terminal
|
|
320
338
|
cleanupCompletedTree(messageTs, tree, nodes);
|
|
321
339
|
}
|
|
@@ -380,7 +398,7 @@ export async function processTreeMessages(): Promise<void> {
|
|
|
380
398
|
}
|
|
381
399
|
|
|
382
400
|
// Clean up fully terminal trees after final render
|
|
383
|
-
if (fullyTerminal && success) {
|
|
401
|
+
if (fullyTerminal && success && outputDeliverySucceeded) {
|
|
384
402
|
cleanupCompletedTree(messageTs, tree, nodes);
|
|
385
403
|
}
|
|
386
404
|
}
|
|
@@ -711,11 +729,10 @@ export function startTaskWatcher(intervalMs = 3000): void {
|
|
|
711
729
|
if (lastSent && now - lastSent < MIN_SEND_INTERVAL) continue;
|
|
712
730
|
|
|
713
731
|
// Tasks tracked in a tree are still rendered by processTreeMessages().
|
|
714
|
-
// For
|
|
732
|
+
// For truncated completions, add one inline output reply so the user
|
|
715
733
|
// sees the actual answer instead of only the compact tree preview.
|
|
716
734
|
if (taskToTree.has(task.id)) {
|
|
717
|
-
|
|
718
|
-
if (!shouldPostInlineCompletionOutput(task, attachments)) {
|
|
735
|
+
if (!shouldPostInlineCompletionOutput(task)) {
|
|
719
736
|
notifiedCompletions.set(task.id, now);
|
|
720
737
|
continue;
|
|
721
738
|
}
|
package/src/telemetry.ts
CHANGED
|
@@ -15,9 +15,15 @@ const PRODUCT = "agent-swarm";
|
|
|
15
15
|
const TIMEOUT_MS = 5_000;
|
|
16
16
|
|
|
17
17
|
let installationId: string | null = null;
|
|
18
|
+
let installedAt: string | null = null;
|
|
18
19
|
let source = "unknown";
|
|
19
20
|
let cachedIsCloud = false;
|
|
20
21
|
let cachedIsE2b = false;
|
|
22
|
+
let cachedHasEmbedding = false;
|
|
23
|
+
let cachedHasSlackChannel = false;
|
|
24
|
+
let cachedHasEmailChannel = false;
|
|
25
|
+
let cachedInstallMethod = "manual";
|
|
26
|
+
let cachedInstallPreset: string | undefined;
|
|
21
27
|
|
|
22
28
|
function isEnabled(): boolean {
|
|
23
29
|
return isEnvFlagEnabled("ANONYMIZED_TELEMETRY", true);
|
|
@@ -80,6 +86,92 @@ export function _resolveCloudMode(mcpBaseUrl: string | undefined | null): {
|
|
|
80
86
|
return { isCloud: isCloudHostname(hostname) };
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
const KNOWN_INSTALL_METHODS = new Set(["onboard_interactive", "onboard_noninteractive"]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the "which entry point produced this install" cohort.
|
|
93
|
+
*
|
|
94
|
+
* `raw` comes from `INSTALL_METHOD`, written into `.env` by the onboard
|
|
95
|
+
* wizard's `generateEnv()` (`onboard_interactive` / `onboard_noninteractive`
|
|
96
|
+
* per `--yes`/`-y`). Installs that never ran the wizard — hand-written
|
|
97
|
+
* docker-compose, a bare `bun run start:http` clone — never set this var, so
|
|
98
|
+
* an unrecognized/missing value falls back to `"e2b"` (detected independently
|
|
99
|
+
* via `E2B_SANDBOX_ID`, since the `e2b start-stack` CLI path doesn't go
|
|
100
|
+
* through `generateEnv()`) or `"manual"`.
|
|
101
|
+
*
|
|
102
|
+
* Exported for tests; not part of the public API.
|
|
103
|
+
*/
|
|
104
|
+
export function _resolveInstallMethod(raw: string | undefined | null, isE2b: boolean): string {
|
|
105
|
+
const normalized = raw?.trim();
|
|
106
|
+
if (normalized && KNOWN_INSTALL_METHODS.has(normalized)) return normalized;
|
|
107
|
+
if (isE2b) return "e2b";
|
|
108
|
+
return "manual";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Preset IDs defined in `src/commands/onboard/presets.ts` (`PRESETS`).
|
|
113
|
+
* Duplicated as a literal set here — rather than imported — to preserve this
|
|
114
|
+
* file's zero-dependency contract (see file header); bump this list in the
|
|
115
|
+
* same PR that adds/removes/renames a preset.
|
|
116
|
+
*/
|
|
117
|
+
const KNOWN_INSTALL_PRESETS = new Set(["dev", "content", "research", "solo", "custom"]);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate `INSTALL_PRESET` against the wizard's known preset IDs.
|
|
121
|
+
*
|
|
122
|
+
* Unlike `_resolveInstallMethod`, an unrecognized value is omitted entirely
|
|
123
|
+
* rather than mapped to a fallback sentinel: `INSTALL_PRESET` is free-form
|
|
124
|
+
* operator-set environment text, so forwarding an unrecognized value (an
|
|
125
|
+
* email address, a customer name, anything placed in that env var by
|
|
126
|
+
* mistake) would violate telemetry's enum-only / no-PII contract. Returning
|
|
127
|
+
* `undefined` here means the `install_preset` metadata key is omitted from
|
|
128
|
+
* the event entirely (see the spread-guard in `track()`).
|
|
129
|
+
*
|
|
130
|
+
* Exported for tests; not part of the public API.
|
|
131
|
+
*/
|
|
132
|
+
export function _resolveInstallPreset(raw: string | undefined | null): string | undefined {
|
|
133
|
+
const normalized = raw?.trim();
|
|
134
|
+
return normalized && KNOWN_INSTALL_PRESETS.has(normalized) ? normalized : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether semantic memory is likely enabled — i.e. an embedding-capable key
|
|
139
|
+
* is present. The onboard wizard's `generateEnv()` currently only writes
|
|
140
|
+
* Anthropic credentials (`src/commands/onboard/env-generator.ts`), so this
|
|
141
|
+
* lets us measure how many installs are silently running with memory search
|
|
142
|
+
* degraded/off. Boolean only — never the key value.
|
|
143
|
+
*
|
|
144
|
+
* Exported for tests; not part of the public API.
|
|
145
|
+
*/
|
|
146
|
+
export function _hasEmbeddingKey(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
147
|
+
return !!(env.EMBEDDING_API_KEY || env.OPENAI_API_KEY);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Mirrors `initSlackApp()`'s enablement check (`src/slack/app.ts`) without
|
|
152
|
+
* importing the Slack module — this file is intentionally dependency-free
|
|
153
|
+
* (see file header) so both the api-server and workers can import it cheaply.
|
|
154
|
+
* If Slack's disable/credential logic changes, update this in the same PR.
|
|
155
|
+
*
|
|
156
|
+
* Exported for tests; not part of the public API.
|
|
157
|
+
*/
|
|
158
|
+
export function _hasSlackChannel(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
159
|
+
return (
|
|
160
|
+
!!(env.SLACK_BOT_TOKEN && env.SLACK_APP_TOKEN) && !isEnvFlagEnabled("SLACK_DISABLE", false, env)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Mirrors `isAgentMailEnabled()` (`src/agentmail/app.ts`) without importing
|
|
166
|
+
* the AgentMail module — see `_hasSlackChannel` for why. AgentMail is the
|
|
167
|
+
* swarm's only outbound email channel today.
|
|
168
|
+
*
|
|
169
|
+
* Exported for tests; not part of the public API.
|
|
170
|
+
*/
|
|
171
|
+
export function _hasEmailChannel(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
172
|
+
return !!env.AGENTMAIL_WEBHOOK_SECRET && !isEnvFlagEnabled("AGENTMAIL_DISABLE", false, env);
|
|
173
|
+
}
|
|
174
|
+
|
|
83
175
|
interface InitTelemetryOptions {
|
|
84
176
|
/**
|
|
85
177
|
* Whether to mint and persist a new install ID when the config read returns
|
|
@@ -113,15 +205,38 @@ export async function initTelemetry(
|
|
|
113
205
|
const resolved = _resolveCloudMode(process.env.MCP_BASE_URL);
|
|
114
206
|
cachedIsCloud = resolved.isCloud;
|
|
115
207
|
cachedIsE2b = _isE2bSandbox();
|
|
116
|
-
|
|
208
|
+
cachedHasEmbedding = _hasEmbeddingKey();
|
|
209
|
+
cachedHasSlackChannel = _hasSlackChannel();
|
|
210
|
+
cachedHasEmailChannel = _hasEmailChannel();
|
|
211
|
+
cachedInstallMethod = _resolveInstallMethod(process.env.INSTALL_METHOD, cachedIsE2b);
|
|
212
|
+
cachedInstallPreset = _resolveInstallPreset(process.env.INSTALL_PRESET);
|
|
213
|
+
console.log(
|
|
214
|
+
`telemetry: cloud=${cachedIsCloud} e2b=${cachedIsE2b} install_method=${cachedInstallMethod}`,
|
|
215
|
+
);
|
|
117
216
|
|
|
118
217
|
try {
|
|
119
218
|
const existing = await getConfig("telemetry_installation_id");
|
|
120
219
|
if (existing) {
|
|
121
220
|
installationId = existing;
|
|
221
|
+
// A pre-existing installation ID with no stored anchor means this
|
|
222
|
+
// install predates `telemetry_installed_at` tracking. Do NOT mint
|
|
223
|
+
// now() as a stand-in "install date" here — that back-fills a date
|
|
224
|
+
// that's wrong by however long the install has actually existed
|
|
225
|
+
// (seen: 110 days on our own production install after a routine
|
|
226
|
+
// upgrade). Leave installedAt null so the field is omitted from the
|
|
227
|
+
// payload: absence unambiguously means "pre-existing install, anchor
|
|
228
|
+
// unknown", and consumers can fall back to min(occurred_at) per
|
|
229
|
+
// installation_id in ClickHouse for the real anchor. Only a
|
|
230
|
+
// genuinely new installationId (the branch below) mints one.
|
|
231
|
+
const existingInstalledAt = await getConfig("telemetry_installed_at");
|
|
232
|
+
if (existingInstalledAt) {
|
|
233
|
+
installedAt = existingInstalledAt;
|
|
234
|
+
}
|
|
122
235
|
} else if (generateIfMissing) {
|
|
123
|
-
|
|
124
|
-
await setConfig("telemetry_installation_id",
|
|
236
|
+
const candidateId = `install_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
237
|
+
await setConfig("telemetry_installation_id", candidateId);
|
|
238
|
+
installationId = candidateId;
|
|
239
|
+
await tryPersistInstalledAt(setConfig);
|
|
125
240
|
}
|
|
126
241
|
// else: leave installationId = null; track() will no-op
|
|
127
242
|
} catch {
|
|
@@ -129,11 +244,39 @@ export async function initTelemetry(
|
|
|
129
244
|
if (generateIfMissing) {
|
|
130
245
|
// Generate ephemeral ID so telemetry still works this session.
|
|
131
246
|
installationId = `ephemeral_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
247
|
+
// Not persisted — config access is failing, so there's nowhere durable
|
|
248
|
+
// to write it. Leave installedAt null rather than a fake fresh mint.
|
|
132
249
|
}
|
|
133
250
|
// else: leave installationId = null; track() will no-op
|
|
134
251
|
}
|
|
135
252
|
}
|
|
136
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Mint + persist the `telemetry_installed_at` anchor, but only commit it to
|
|
256
|
+
* module state (and therefore only emit it on this session's events) once
|
|
257
|
+
* the write actually lands.
|
|
258
|
+
*
|
|
259
|
+
* The anchor's entire value is being a stable, one-time identity for "when
|
|
260
|
+
* did this install first appear" — a failed write here must not fabricate
|
|
261
|
+
* that stability. If `setConfig` throws, this process emits no anchor at
|
|
262
|
+
* all (rather than an unpersisted one that the next boot won't see and will
|
|
263
|
+
* mint a *different* replacement for), and — critically — the failure is
|
|
264
|
+
* swallowed locally so it can't unwind into the caller's `catch`, which
|
|
265
|
+
* would otherwise discard an already-resolved `installationId` that has
|
|
266
|
+
* nothing to do with this write.
|
|
267
|
+
*/
|
|
268
|
+
async function tryPersistInstalledAt(
|
|
269
|
+
setConfig: (key: string, value: string) => Promise<void> | void,
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
const candidate = new Date().toISOString();
|
|
272
|
+
try {
|
|
273
|
+
await setConfig("telemetry_installed_at", candidate);
|
|
274
|
+
installedAt = candidate;
|
|
275
|
+
} catch {
|
|
276
|
+
// Leave installedAt null for this session.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
137
280
|
interface TrackOptions {
|
|
138
281
|
event: string;
|
|
139
282
|
properties?: Record<string, unknown>;
|
|
@@ -208,6 +351,15 @@ export function track(options: TrackOptions): void {
|
|
|
208
351
|
is_cloud: cachedIsCloud || isCloudDeployment(),
|
|
209
352
|
is_e2b: cachedIsE2b,
|
|
210
353
|
swarmVersion: pkg.version,
|
|
354
|
+
// Instrumentation-gap closure (2026-07-29): activation-funnel signals.
|
|
355
|
+
// All boolean/enum, resolved once at init from process.env — never a
|
|
356
|
+
// secret, URL, channel ID, address, or other identifier. Spread LAST
|
|
357
|
+
// (same rule as is_cloud/is_e2b above) so callers can't spoof them.
|
|
358
|
+
has_embedding_key: cachedHasEmbedding,
|
|
359
|
+
has_slack_channel: cachedHasSlackChannel,
|
|
360
|
+
has_email_channel: cachedHasEmailChannel,
|
|
361
|
+
has_notification_channel: cachedHasSlackChannel || cachedHasEmailChannel,
|
|
362
|
+
install_method: cachedInstallMethod,
|
|
211
363
|
},
|
|
212
364
|
metadata: {
|
|
213
365
|
transport: "https",
|
|
@@ -215,6 +367,9 @@ export function track(options: TrackOptions): void {
|
|
|
215
367
|
environment: getTelemetryEnvironment(),
|
|
216
368
|
is_cloud: isCloudDeployment(),
|
|
217
369
|
...getOrgIdentity(),
|
|
370
|
+
// Optional — only present when known, same pattern as organization_*.
|
|
371
|
+
...(cachedInstallPreset ? { install_preset: cachedInstallPreset } : {}),
|
|
372
|
+
...(installedAt ? { install_created_at: installedAt } : {}),
|
|
218
373
|
...options.metadata,
|
|
219
374
|
},
|
|
220
375
|
};
|
|
@@ -235,9 +390,15 @@ export function track(options: TrackOptions): void {
|
|
|
235
390
|
*/
|
|
236
391
|
export function _resetTelemetryStateForTests(): void {
|
|
237
392
|
installationId = null;
|
|
393
|
+
installedAt = null;
|
|
238
394
|
source = "unknown";
|
|
239
395
|
cachedIsCloud = false;
|
|
240
396
|
cachedIsE2b = false;
|
|
397
|
+
cachedHasEmbedding = false;
|
|
398
|
+
cachedHasSlackChannel = false;
|
|
399
|
+
cachedHasEmailChannel = false;
|
|
400
|
+
cachedInstallMethod = "manual";
|
|
401
|
+
cachedInstallPreset = undefined;
|
|
241
402
|
}
|
|
242
403
|
|
|
243
404
|
/** Test-only: read the resolved install ID. */
|
|
@@ -245,6 +406,11 @@ export function _getInstallationIdForTests(): string | null {
|
|
|
245
406
|
return installationId;
|
|
246
407
|
}
|
|
247
408
|
|
|
409
|
+
/** Test-only: read the resolved install-created-at anchor. */
|
|
410
|
+
export function _getInstalledAtForTests(): string | null {
|
|
411
|
+
return installedAt;
|
|
412
|
+
}
|
|
413
|
+
|
|
248
414
|
export const telemetry = {
|
|
249
415
|
taskEvent(
|
|
250
416
|
event: string,
|
|
@@ -42,6 +42,51 @@ function disableSlackPromptTools() {
|
|
|
42
42
|
delete process.env.SLACK_APP_TOKEN;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Heading of `system.agent.script_authoring_contract` — used for dedupe checks */
|
|
46
|
+
const AUTHORING_CONTRACT_MARKER = "### Script Authoring Contract";
|
|
47
|
+
|
|
48
|
+
/** How many times the authoring contract appears in a rendered prompt */
|
|
49
|
+
function countAuthoringContract(text: string): number {
|
|
50
|
+
return text.split(AUTHORING_CONTRACT_MARKER).length - 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Script authoring contract — must reach every script-carrying composite, and
|
|
55
|
+
// must never be duplicated (scripts-only mode renders after a composite that
|
|
56
|
+
// already carries the rubric).
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
describe("getBasePrompt — script authoring contract", () => {
|
|
59
|
+
const combos: { role: string; provider?: "claude" | "pi"; scriptsOnly?: boolean }[] = [
|
|
60
|
+
{ role: "worker" },
|
|
61
|
+
{ role: "lead" },
|
|
62
|
+
{ role: "worker", provider: "pi" },
|
|
63
|
+
{ role: "lead", provider: "pi" },
|
|
64
|
+
{ role: "worker", scriptsOnly: true },
|
|
65
|
+
{ role: "lead", scriptsOnly: true },
|
|
66
|
+
{ role: "worker", provider: "pi", scriptsOnly: true },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const combo of combos) {
|
|
70
|
+
const label = `${combo.role}/${combo.provider ?? "default"}${
|
|
71
|
+
combo.scriptsOnly ? "/scripts-only" : ""
|
|
72
|
+
}`;
|
|
73
|
+
test(`renders exactly once for ${label}`, async () => {
|
|
74
|
+
const result = await getBasePrompt({ ...minimalArgs, ...combo });
|
|
75
|
+
expect(countAuthoringContract(result)).toBe(1);
|
|
76
|
+
expect(result).toContain("`args` FIRST, `ctx` SECOND");
|
|
77
|
+
expect(result).toContain("[REDACTED:<CONFIG_KEY>]");
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test("remote provider (no MCP) gets no script authoring contract", async () => {
|
|
82
|
+
const result = await getBasePrompt({
|
|
83
|
+
...minimalArgs,
|
|
84
|
+
traits: { hasMcp: false, hasLocalEnvironment: false },
|
|
85
|
+
});
|
|
86
|
+
expect(countAuthoringContract(result)).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
45
90
|
// ---------------------------------------------------------------------------
|
|
46
91
|
// Basic fields
|
|
47
92
|
// ---------------------------------------------------------------------------
|
|
@@ -663,6 +708,40 @@ describe("getBasePrompt — context-mode provider gating", () => {
|
|
|
663
708
|
expect(result).not.toContain("batch_execute");
|
|
664
709
|
expect(result).toContain("Agent Scripts");
|
|
665
710
|
expect(result).toContain("Pre-built Seed Scripts");
|
|
711
|
+
// Scheduling rules live outside the ctx_* advertisement — pi keeps them.
|
|
712
|
+
expect(result).toContain("Scheduling — Pick the Right targetType");
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("pi LEAD also skips the context-mode block (lead.pi composite)", async () => {
|
|
716
|
+
const result = await getBasePrompt({
|
|
717
|
+
...minimalArgs,
|
|
718
|
+
role: "lead",
|
|
719
|
+
traits: localTraits,
|
|
720
|
+
provider: "pi",
|
|
721
|
+
});
|
|
722
|
+
// Lead content is present
|
|
723
|
+
expect(result).toContain("CRITICAL: You are a coordinator");
|
|
724
|
+
// ...but no phantom ctx_* tooling
|
|
725
|
+
expect(result).not.toContain("Context Window Management");
|
|
726
|
+
expect(result).not.toContain("batch_execute");
|
|
727
|
+
expect(result).not.toContain("execute_file");
|
|
728
|
+
// Script guidance (incl. the authoring contract) survives
|
|
729
|
+
expect(result).toContain("Agent Scripts");
|
|
730
|
+
expect(result).toContain(AUTHORING_CONTRACT_MARKER);
|
|
731
|
+
expect(result).toContain("Pre-built Seed Scripts");
|
|
732
|
+
// Scheduling rules live outside the ctx_* advertisement — pi keeps them.
|
|
733
|
+
expect(result).toContain("Scheduling — Pick the Right targetType");
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("claude LEAD keeps the context-mode block", async () => {
|
|
737
|
+
const result = await getBasePrompt({
|
|
738
|
+
...minimalArgs,
|
|
739
|
+
role: "lead",
|
|
740
|
+
traits: localTraits,
|
|
741
|
+
provider: "claude",
|
|
742
|
+
});
|
|
743
|
+
expect(result).toContain("Context Window Management");
|
|
744
|
+
expect(result).toContain("batch_execute");
|
|
666
745
|
});
|
|
667
746
|
|
|
668
747
|
for (const provider of ["claude", "codex", "opencode"] as const) {
|
|
@@ -45,6 +45,18 @@ function textOf(result: CallToolResult): string {
|
|
|
45
45
|
return "";
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Structured payload the registrar composes: `{ ...data, success, message,
|
|
50
|
+
* details?, nudge? }` (see finalizeSwarmToolResult in src/tools/utils.ts).
|
|
51
|
+
* Tool-specific data keys (status/unknown/ambiguous/candidates/externalIds/id/...)
|
|
52
|
+
* are spread at the TOP LEVEL alongside the envelope keys — this is where the
|
|
53
|
+
* machine-readable payload now lives ("never prose" = never ONLY in the text
|
|
54
|
+
* channel; structuredContent is always populated).
|
|
55
|
+
*/
|
|
56
|
+
function structuredOf(result: CallToolResult): Record<string, unknown> {
|
|
57
|
+
return (result.structuredContent ?? {}) as Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
function eventsFor(userId: string): Array<{ eventType: string; afterJson: string | null }> {
|
|
49
61
|
return getDb()
|
|
50
62
|
.prepare<{ eventType: string; afterJson: string | null }, string>(
|
|
@@ -90,6 +102,12 @@ describe("resolve-user MCP tool (new {kind, externalId, email} shape)", () => {
|
|
|
90
102
|
const text = textOf(result);
|
|
91
103
|
expect(text).toContain(user.id);
|
|
92
104
|
expect(text).toContain("Slack User One");
|
|
105
|
+
expect(result.isError).toBe(false);
|
|
106
|
+
expect(structuredOf(result)).toMatchObject({
|
|
107
|
+
success: true,
|
|
108
|
+
id: user.id,
|
|
109
|
+
name: "Slack User One",
|
|
110
|
+
});
|
|
93
111
|
});
|
|
94
112
|
|
|
95
113
|
test("matches by email → findUserByEmail hit (primary + alias)", async () => {
|
|
@@ -101,22 +119,34 @@ describe("resolve-user MCP tool (new {kind, externalId, email} shape)", () => {
|
|
|
101
119
|
|
|
102
120
|
const byPrimary = await callTool(server, "resolve-user", { email: "primary@example.com" });
|
|
103
121
|
expect(textOf(byPrimary)).toContain(user.id);
|
|
122
|
+
expect(byPrimary.isError).toBe(false);
|
|
123
|
+
expect(structuredOf(byPrimary)).toMatchObject({ success: true, id: user.id });
|
|
104
124
|
|
|
105
125
|
const byAlias = await callTool(server, "resolve-user", { email: "alias@example.com" });
|
|
106
126
|
expect(textOf(byAlias)).toContain(user.id);
|
|
127
|
+
expect(byAlias.isError).toBe(false);
|
|
128
|
+
expect(structuredOf(byAlias)).toMatchObject({ success: true, id: user.id });
|
|
107
129
|
});
|
|
108
130
|
|
|
109
|
-
test("returns a structured {status: 'unknown', ...} payload when nothing matches —
|
|
131
|
+
test("returns a structured {status: 'unknown', ...} payload when nothing matches — machine-readable via structuredContent, not prose-only", async () => {
|
|
110
132
|
const result = await callTool(server, "resolve-user", {
|
|
111
133
|
kind: "slack",
|
|
112
134
|
externalId: "U_DOES_NOT_EXIST",
|
|
113
135
|
});
|
|
114
|
-
|
|
115
|
-
expect(
|
|
136
|
+
// A miss is a successful, honest lookup outcome — not a tool failure.
|
|
137
|
+
expect(result.isError).toBe(false);
|
|
138
|
+
// NEW CONTRACT (SwarmToolResult): the {status, kind, externalId} payload is
|
|
139
|
+
// spread at the top level of structuredContent alongside the envelope
|
|
140
|
+
// (success/message), not embedded as a JSON blob inside content[0].text.
|
|
141
|
+
expect(structuredOf(result)).toMatchObject({
|
|
142
|
+
success: true,
|
|
116
143
|
status: "unknown",
|
|
117
144
|
kind: "slack",
|
|
118
145
|
externalId: "U_DOES_NOT_EXIST",
|
|
119
146
|
});
|
|
147
|
+
// Prose summary still shows up in the text channel for harnesses that
|
|
148
|
+
// only read content.text.
|
|
149
|
+
expect(textOf(result)).toContain('No user found for slack="U_DOES_NOT_EXIST"');
|
|
120
150
|
});
|
|
121
151
|
|
|
122
152
|
// Schema-level validation tests. The MCP SDK runs Zod at the transport
|
|
@@ -183,18 +213,21 @@ describe("resolve-user MCP tool (new {kind, externalId, email} shape)", () => {
|
|
|
183
213
|
kind: "github",
|
|
184
214
|
externalId: "extid-gh-handle",
|
|
185
215
|
});
|
|
186
|
-
const parsed =
|
|
216
|
+
const parsed = structuredOf(result);
|
|
187
217
|
expect(parsed.id).toBe(user.id);
|
|
188
218
|
expect(Array.isArray(parsed.externalIds)).toBe(true);
|
|
189
|
-
const kinds = parsed.externalIds
|
|
219
|
+
const kinds = (parsed.externalIds as Array<{ kind: string }>).map((e) => e.kind).sort();
|
|
190
220
|
expect(kinds).toEqual(["github", "slack"]);
|
|
221
|
+
// Both channels must be independently self-sufficient — the same payload
|
|
222
|
+
// is also present (via `details`) in the text channel.
|
|
223
|
+
expect(textOf(result)).toContain(user.id);
|
|
191
224
|
});
|
|
192
225
|
|
|
193
226
|
test("externalIds is empty array when user has no identities", async () => {
|
|
194
227
|
const user = createUser({ name: "No Identities User", email: "noid@example.com" });
|
|
195
228
|
|
|
196
229
|
const result = await callTool(server, "resolve-user", { email: "noid@example.com" });
|
|
197
|
-
const parsed =
|
|
230
|
+
const parsed = structuredOf(result);
|
|
198
231
|
expect(parsed.id).toBe(user.id);
|
|
199
232
|
expect(parsed.externalIds).toEqual([]);
|
|
200
233
|
});
|
|
@@ -204,17 +237,21 @@ describe("resolve-user MCP tool (new {kind, externalId, email} shape)", () => {
|
|
|
204
237
|
linkIdentity(user.id, "linear", "L_UIDLOOKUP", SYSTEM_ACTOR);
|
|
205
238
|
|
|
206
239
|
const result = await callTool(server, "resolve-user", { userId: user.id });
|
|
207
|
-
const parsed =
|
|
240
|
+
const parsed = structuredOf(result);
|
|
208
241
|
expect(parsed.id).toBe(user.id);
|
|
209
242
|
expect(parsed.name).toBe("User ID Lookup");
|
|
210
243
|
expect(parsed.externalIds).toHaveLength(1);
|
|
211
|
-
expect(parsed.externalIds[0]).toMatchObject({
|
|
244
|
+
expect((parsed.externalIds as unknown[])[0]).toMatchObject({
|
|
245
|
+
kind: "linear",
|
|
246
|
+
externalId: "L_UIDLOOKUP",
|
|
247
|
+
});
|
|
212
248
|
});
|
|
213
249
|
|
|
214
250
|
test("userId lookup returns a structured {status: 'unknown', ...} payload for an unknown ID", async () => {
|
|
215
251
|
const result = await callTool(server, "resolve-user", { userId: "nonexistent-user-id-xyz" });
|
|
216
|
-
|
|
217
|
-
expect(
|
|
252
|
+
expect(result.isError).toBe(false);
|
|
253
|
+
expect(structuredOf(result)).toMatchObject({
|
|
254
|
+
success: true,
|
|
218
255
|
status: "unknown",
|
|
219
256
|
kind: "userId",
|
|
220
257
|
externalId: "nonexistent-user-id-xyz",
|
|
@@ -225,7 +262,7 @@ describe("resolve-user MCP tool (new {kind, externalId, email} shape)", () => {
|
|
|
225
262
|
const user = createUser({ name: "Zbigniew Solo", email: "zbigniew@example.com" });
|
|
226
263
|
|
|
227
264
|
const result = await callTool(server, "resolve-user", { name: "Zbigniew Solo" });
|
|
228
|
-
const parsed =
|
|
265
|
+
const parsed = structuredOf(result);
|
|
229
266
|
expect(parsed.id).toBe(user.id);
|
|
230
267
|
expect(parsed.name).toBe("Zbigniew Solo");
|
|
231
268
|
});
|
|
@@ -234,7 +271,7 @@ describe("resolve-user MCP tool (new {kind, externalId, email} shape)", () => {
|
|
|
234
271
|
const user = createUser({ name: "Priyanka Unique Prefix", email: "priyanka@example.com" });
|
|
235
272
|
|
|
236
273
|
const result = await callTool(server, "resolve-user", { name: "Priyanka" });
|
|
237
|
-
const parsed =
|
|
274
|
+
const parsed = structuredOf(result);
|
|
238
275
|
expect(parsed.id).toBe(user.id);
|
|
239
276
|
});
|
|
240
277
|
|
|
@@ -243,17 +280,22 @@ describe("resolve-user MCP tool (new {kind, externalId, email} shape)", () => {
|
|
|
243
280
|
const b = createUser({ name: "Alberto Dubois", email: "alberto.dubois@example.com" });
|
|
244
281
|
|
|
245
282
|
const result = await callTool(server, "resolve-user", { name: "Alberto" });
|
|
246
|
-
const parsed =
|
|
283
|
+
const parsed = structuredOf(result);
|
|
247
284
|
expect(parsed.status).toBe("ambiguous");
|
|
248
285
|
expect(parsed.message).toMatch(/AMBIGUOUS/);
|
|
249
|
-
const candidateIds = parsed.candidates
|
|
286
|
+
const candidateIds = (parsed.candidates as Array<{ userId: string }>)
|
|
287
|
+
.map((c) => c.userId)
|
|
288
|
+
.sort();
|
|
250
289
|
expect(candidateIds).toEqual([a.id, b.id].sort());
|
|
290
|
+
// The "never guess" message must also reach harnesses that only read text.
|
|
291
|
+
expect(textOf(result)).toMatch(/AMBIGUOUS/);
|
|
251
292
|
});
|
|
252
293
|
|
|
253
294
|
test("name lookup: zero matches return a structured {status: 'unknown', kind: 'name', ...} payload", async () => {
|
|
254
295
|
const result = await callTool(server, "resolve-user", { name: "Nobody Registered Xyz" });
|
|
255
|
-
|
|
256
|
-
expect(
|
|
296
|
+
expect(result.isError).toBe(false);
|
|
297
|
+
expect(structuredOf(result)).toMatchObject({
|
|
298
|
+
success: true,
|
|
257
299
|
status: "unknown",
|
|
258
300
|
kind: "name",
|
|
259
301
|
externalId: "Nobody Registered Xyz",
|
|
@@ -364,7 +406,11 @@ describe("manage-user MCP tool (identities array)", () => {
|
|
|
364
406
|
|
|
365
407
|
test("non-lead caller is rejected", async () => {
|
|
366
408
|
const result = await callTool(server, "manage-user", { action: "list" }, WORKER_ID);
|
|
409
|
+
// NEW CONTRACT: failures set isError=true (derived from ok=false) and
|
|
410
|
+
// structuredContent.success=false — not just prose in content.text.
|
|
411
|
+
expect(result.isError).toBe(true);
|
|
367
412
|
expect(textOf(result)).toContain("Only the lead agent");
|
|
413
|
+
expect(structuredOf(result)).toMatchObject({ success: false });
|
|
368
414
|
});
|
|
369
415
|
|
|
370
416
|
test("create no longer accepts old top-level slackUserId / linearUserId / githubUsername / gitlabUsername", async () => {
|
|
@@ -48,13 +48,16 @@ describe("script MCP tools", () => {
|
|
|
48
48
|
"script-search":
|
|
49
49
|
"Semantic search over swarm-shared TypeScript scripts (catalog persisted in the agent-swarm DB; callable from agents and workflows). For ephemeral throwaway TS on your local machine, use code-mode instead.",
|
|
50
50
|
"script-run":
|
|
51
|
-
"Run a named swarm-shared script (callable across agents and from workflow `swarm-script` nodes), OR inline source (auto-saved as scratch to the catalog). Use for swarm-visible, durable scripts. For local-only throwaway TS, use code-mode `run`.",
|
|
51
|
+
"Run a named swarm-shared script (callable across agents and from workflow `swarm-script` nodes), OR inline source (auto-saved as scratch to the catalog). Inline source executes without the `script-upsert` compile-time typecheck. Use for swarm-visible, durable scripts. For local-only throwaway TS, use code-mode `run`.",
|
|
52
52
|
"script-upsert":
|
|
53
|
-
|
|
53
|
+
'Typecheck and persist a TypeScript script to the swarm catalog under your agent scope (or global if you\'re a lead). Import `ScriptContext` from "swarm-sdk" for a real context type. Other agents and workflow nodes will be able to find and run it. For local-only scripts, use code-mode `save`.',
|
|
54
54
|
"script-delete":
|
|
55
55
|
"Remove a swarm-shared script from the catalog. Versions table preserves history.",
|
|
56
56
|
"script-query-types":
|
|
57
|
-
|
|
57
|
+
// Updated for the frozen SwarmToolResult contract refactor: `name` is
|
|
58
|
+
// now optional — omit it to get the swarm-wide type surface instead
|
|
59
|
+
// of a single script's signature.
|
|
60
|
+
"Fetch the auto-generated `swarm-sdk.d.ts` (derived from the live MCP tool registry) + the `stdlib.d.ts` blobs — for IDE-style introspection before authoring or running a script. Pass `name` to also get that script's signature; omit it for the swarm-wide type surface. The same types are used by `script-upsert`'s typecheck pass, so they are authoritative.",
|
|
58
61
|
"launch-script-run":
|
|
59
62
|
"Launch a durable one-off script workflow run. The run executes in the background and can be inspected with get-script-run for terminal status and journal entries.",
|
|
60
63
|
"get-script-run":
|
|
@@ -254,13 +254,30 @@ describe("memory editing", () => {
|
|
|
254
254
|
intent: "test stale-link pruning",
|
|
255
255
|
},
|
|
256
256
|
meta(),
|
|
257
|
-
)) as {
|
|
257
|
+
)) as {
|
|
258
|
+
isError?: boolean;
|
|
259
|
+
content: Array<{ type: string; text: string }>;
|
|
260
|
+
structuredContent: {
|
|
261
|
+
success: boolean;
|
|
262
|
+
message: string;
|
|
263
|
+
changed?: boolean;
|
|
264
|
+
version?: number;
|
|
265
|
+
memory?: { id?: string };
|
|
266
|
+
};
|
|
267
|
+
};
|
|
258
268
|
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
269
|
+
// New envelope contract (src/tools/utils.ts finalizeSwarmToolResult):
|
|
270
|
+
// isError = !ok, structuredContent = { ...data, success, message }, and
|
|
271
|
+
// content[0].text carries the same message. Assert on the message first
|
|
272
|
+
// — on failure it carries the tool's error (a bare success:false told us
|
|
273
|
+
// nothing when this flaked in CI).
|
|
274
|
+
expect(result.isError).toBe(false);
|
|
275
|
+
expect(result.structuredContent.message).toStartWith(`Memory "${a.id}" edited to version`);
|
|
276
|
+
expect(result.content[0]?.text).toStartWith(`Memory "${a.id}" edited to version`);
|
|
262
277
|
expect(result.structuredContent.success).toBe(true);
|
|
263
278
|
expect(result.structuredContent.changed).toBe(true);
|
|
279
|
+
expect(result.structuredContent.version).toBe(2);
|
|
280
|
+
expect(result.structuredContent.memory?.id).toBe(a.id);
|
|
264
281
|
|
|
265
282
|
const rows = linkRowsFor(a.id);
|
|
266
283
|
expect(rows).toHaveLength(2);
|