@desplega.ai/agent-swarm 1.146.0 → 1.147.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 +1 -1
- package/dist/{acp-adapter-jnq4612m.js → acp-adapter-9n319wqc.js} +42 -4
- package/dist/{actions-s41xcn3f.js → actions-2vxqvpr9.js} +9 -8
- package/dist/{app-j8bfw3f6.js → app-w066xfy0.js} +6 -5
- package/dist/assistant-kt31dj2h.js +236 -0
- package/dist/{boot-reembed-jxdz46jp.js → boot-reembed-az5rassp.js} +7 -6
- package/dist/{boot-reembed-x1476agm.js → boot-reembed-j3mm3rfz.js} +8 -7
- package/dist/{boot-scrub-logs-rab9h5bt.js → boot-scrub-logs-b6h54817.js} +4 -4
- package/dist/{claude-adapter-ep6qekbm.js → claude-adapter-w1z04ann.js} +8 -6
- package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
- package/dist/{claude-sdk-session-0wp3qrcy.js → claude-sdk-session-f0cc1kec.js} +8 -6
- package/dist/{cli-bafd6xnf.js → cli-0b6y7kxm.js} +1 -1
- package/dist/{cli-jyy3e193.js → cli-12fz972k.js} +4 -4
- package/dist/{cli-pzfc7j7g.js → cli-1th1728g.js} +2 -2
- package/dist/{cli-68h2mfrn.js → cli-3tgwnf43.js} +3 -3
- package/dist/{cli-q3qr220d.js → cli-4nr988h6.js} +1 -1
- package/dist/{cli-m4xsy03s.js → cli-53s590z8.js} +1 -1
- package/dist/{cli-c8q7292f.js → cli-54wwve05.js} +5 -4
- package/dist/{cli-rymrjneb.js → cli-719k9j2c.js} +2 -2
- package/dist/{cli-zn65x0w8.js → cli-89xfd7f9.js} +21 -47
- package/dist/{cli-506z6jqa.js → cli-8v0g1yc8.js} +1 -1
- package/dist/cli-9d8ntetj.js +42 -0
- package/dist/{cli-dhg6j8h3.js → cli-atcve9y8.js} +826 -737
- package/dist/{cli-46yck6nj.js → cli-b5bq9crk.js} +14 -31
- package/dist/cli-d8brzxjb.js +190 -0
- package/dist/{cli-a960nkfb.js → cli-de14znh8.js} +2 -2
- package/dist/{cli-s8sbe32s.js → cli-dttph1bw.js} +2 -2
- package/dist/cli-ep19k3mb.js +19 -0
- package/dist/{cli-d9pnxgth.js → cli-f146mn5s.js} +7 -50
- package/dist/{cli-qar1f940.js → cli-f7dsy61y.js} +5 -3
- package/dist/{cli-d4xqde2b.js → cli-ftht3zzj.js} +2780 -2587
- package/dist/{cli-pmwbdw36.js → cli-hbv0kq6w.js} +2 -2
- package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
- package/dist/{cli-2zvtzevh.js → cli-kpv03zhj.js} +13 -11
- package/dist/{cli-a9bn9svs.js → cli-m8yrsg97.js} +2 -2
- package/dist/cli-m9sxbkhm.js +779 -0
- package/dist/cli-mmemxxdc.js +823 -0
- package/dist/{cli-6xwhtc64.js → cli-n8508nre.js} +1 -1
- package/dist/{cli-q0qq1sxw.js → cli-p1d5b073.js} +37 -2
- package/dist/{cli-yqeb27t1.js → cli-rw51bq3j.js} +13 -10
- package/dist/{cli-zww3vcnm.js → cli-smby5m17.js} +7 -6
- package/dist/{cli-ah5hjk49.js → cli-tzvk9haz.js} +94 -372
- package/dist/{cli-yxwe913z.js → cli-w9kb3bk1.js} +1 -1
- package/dist/{cli-skd674mt.js → cli-y6b6fb76.js} +25 -3
- package/dist/{cli-3k2g3kg2.js → cli-yz3djrm0.js} +9 -9
- package/dist/{cli-4secrq32.js → cli-zhjmqxzh.js} +1 -1
- package/dist/cli.js +13 -13
- package/dist/{codex-adapter-d6qajxj3.js → codex-adapter-zw4dwsx7.js} +2 -2
- package/dist/{codex-hook-n7m5k7cr.js → codex-hook-bw8p5nh4.js} +2 -2
- package/dist/{codex-session-runner-v9krqe4x.js → codex-session-runner-k981s65v.js} +2 -2
- package/dist/{commands-923wsas7.js → commands-sykrxx7e.js} +4 -4
- package/dist/{db-m1hy4zh5.js → db-sfbsc8wt.js} +6 -4
- package/dist/handlers-0aspcejm.js +603 -0
- package/dist/{hook-ee4hff56.js → hook-4nvqj2sb.js} +7 -5
- package/dist/{hook-bxe3bkdy.js → hook-hxccs7m5.js} +24 -49
- package/dist/{http-a8rx91me.js → http-y06z517d.js} +223 -92
- package/dist/{index-697zfzv1.js → index-hssww7kj.js} +15 -14
- package/dist/{index-3x9g3nbg.js → index-k8znfx0z.js} +13 -12
- package/dist/{index-1j9va48r.js → index-tce1rss4.js} +371 -110
- package/dist/{index-jj4q3x59.js → index-x5j894zf.js} +14 -13
- package/dist/{keepalive-sds4e4ce.js → keepalive-w733ax66.js} +6 -6
- package/dist/{lead-ggq6szk8.js → lead-75bjqa9d.js} +29 -27
- package/dist/{maintenance-093p41d4.js → maintenance-w4f1zjck.js} +8 -7
- package/dist/{oauth-refresh-sweep-dvzq4ckj.js → oauth-refresh-sweep-wdetky26.js} +5 -5
- package/dist/{onboard-ydr9tzw5.js → onboard-bb5net8s.js} +2 -2
- package/dist/{otel-impl-zx3tdf3j.js → otel-impl-0w7c14ap.js} +1 -1
- package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
- package/dist/{pricing-refresh-7e6pqnzp.js → pricing-refresh-9nzsrgcq.js} +6 -6
- package/dist/{rbac-roles-p2rh6d0r.js → rbac-roles-r3hqjp09.js} +5 -5
- package/dist/{rbac-roles-0dv1wt0w.js → rbac-roles-z1jrffbt.js} +4 -4
- package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
- package/dist/{render-v2-4k3jt4r2.js → render-v2-vbr89fnb.js} +6 -5
- package/dist/{seed-pricing-2cc1qmcd.js → seed-pricing-g62hy1pk.js} +7 -5
- package/dist/{setup-c5yss30k.js → setup-gnfqnptp.js} +2 -2
- package/dist/{worker-7srgeqcr.js → worker-kznxp4qm.js} +29 -27
- package/openapi.json +263 -12
- package/package.json +1 -1
- package/plugin/commands/start-worker.md +4 -9
- package/plugin/pi-skills/start-worker/SKILL.md +4 -9
- package/src/be/db/agents.ts +556 -0
- package/src/be/db/context-versions.ts +117 -0
- package/src/be/db/runtime.ts +406 -0
- package/src/be/db/tasks/read.ts +1167 -0
- package/src/be/db/tasks/write.ts +953 -0
- package/src/be/db.ts +583 -3428
- package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
- package/src/be/migrations/148_pages_svg.sql +123 -0
- package/src/be/migrations/149_session_tokens.sql +24 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +13 -8
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
- package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
- package/src/be/seed-scripts/index.ts +17 -0
- package/src/be/seed-skills/index.ts +14 -0
- package/src/be/swarm-config-guard.ts +1 -1
- package/src/be/task-attachment-store.ts +70 -0
- package/src/be/users.ts +77 -0
- package/src/commands/attachments-section.ts +37 -0
- package/src/commands/claude-managed-setup.ts +45 -3
- package/src/commands/claude-md-session.ts +408 -0
- package/src/commands/context-preamble.ts +15 -3
- package/src/commands/profile-sync.ts +205 -5
- package/src/commands/provider-credentials.ts +60 -27
- package/src/commands/runner.ts +63 -58
- package/src/commands/templates.ts +25 -2
- package/src/hooks/hook.ts +26 -67
- package/src/http/agents.ts +40 -1
- package/src/http/auth.ts +13 -1
- package/src/http/fs.ts +21 -51
- package/src/http/mcp.ts +32 -0
- package/src/http/pages-public.ts +20 -4
- package/src/http/poll.ts +4 -0
- package/src/http/repos.ts +3 -3
- package/src/http/sessions.ts +82 -0
- package/src/http/tasks.ts +10 -2
- package/src/prompts/session-templates.ts +4 -2
- package/src/providers/acp-adapter.ts +42 -6
- package/src/providers/pi-mono-adapter.ts +23 -6
- package/src/scheduler/scheduler.test.ts +49 -2
- package/src/scheduler/scheduler.ts +4 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +87 -46
- package/src/slack/handlers.test.ts +1 -3
- package/src/slack/handlers.ts +102 -87
- package/src/slack/inbound-files.ts +466 -0
- package/src/slack/templates.ts +14 -0
- package/src/tasks/task-terminal-effects.ts +158 -0
- package/src/tests/acp-adapter.test.ts +327 -192
- package/src/tests/budget-claim-gate.test.ts +1 -0
- package/src/tests/build-pi-skills.test.ts +5 -7
- package/src/tests/claude-managed-setup.test.ts +52 -4
- package/src/tests/claude-md-session.test.ts +603 -0
- package/src/tests/codex-skill-resolver.test.ts +19 -0
- package/src/tests/context-versioning.test.ts +107 -0
- package/src/tests/credential-check.test.ts +157 -14
- package/src/tests/db-agents.test.ts +91 -0
- package/src/tests/db-context-versions.test.ts +37 -0
- package/src/tests/db-extraction-contract.test.ts +45 -0
- package/src/tests/db-runtime.test.ts +70 -0
- package/src/tests/db-task-read-contract.test.ts +624 -0
- package/src/tests/db-task-write-contract.test.ts +653 -0
- package/src/tests/db-tasks-read.test.ts +183 -0
- package/src/tests/db-tasks-write.test.ts +230 -0
- package/src/tests/defer-task.test.ts +389 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/e2e-visuals-stage.test.ts +151 -0
- package/src/tests/file-lock.test.ts +144 -0
- package/src/tests/fixtures/file-lock-holder.ts +13 -0
- package/src/tests/fixtures/hold-file-lock.ts +25 -0
- package/src/tests/heartbeat-expiry.test.ts +188 -0
- package/src/tests/http-api-integration.test.ts +78 -1
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-svg-migration.test.ts +42 -0
- package/src/tests/preload.ts +4 -1
- package/src/tests/profile-sync-warning.test.ts +16 -0
- package/src/tests/profile-sync.test.ts +126 -0
- package/src/tests/prompt-template-session.test.ts +20 -4
- package/src/tests/provider-registration.test.ts +93 -0
- package/src/tests/rbac-e2e-helpers.test.ts +53 -0
- package/src/tests/rbac-e2e-helpers.ts +19 -2
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/runner-attachments-section.test.ts +22 -2
- package/src/tests/runner-context-preamble.test.ts +30 -0
- package/src/tests/runner-deployment-resume.test.ts +107 -0
- package/src/tests/runner-repo-autostash.test.ts +12 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/session-tokens.test.ts +279 -0
- package/src/tests/slack-file-tools-attachments.test.ts +408 -0
- package/src/tests/slack-inbound-files.test.ts +847 -0
- package/src/tests/star-history-refresh.test.ts +102 -0
- package/src/tests/swarm-repos.test.ts +20 -0
- package/src/tests/tool-annotations.test.ts +4 -3
- package/src/tests/ui-logs-parser.test.ts +176 -0
- package/src/tools/create-page.ts +4 -2
- package/src/tools/defer-task.ts +239 -0
- package/src/tools/repos/update-repo.ts +2 -2
- package/src/tools/schedules/create-schedule.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +1 -0
- package/src/tools/schedules/patch-schedule.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +1 -0
- package/src/tools/slack-download-file.ts +60 -29
- package/src/tools/slack-read.ts +73 -32
- package/src/tools/store-progress.ts +16 -146
- package/src/tools/tool-config.ts +1 -0
- package/src/tools/utils.ts +21 -0
- package/src/types.ts +37 -4
- package/src/utils/acp-session-token.ts +56 -0
- package/src/utils/file-lock.ts +111 -0
- package/src/utils/heartbeat-expiry.ts +78 -0
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/task-attachment-links.ts +22 -0
- package/templates/skills/code-quality/content.md +8 -5
- package/templates/skills/download-task-attachment/content.md +2 -1
- package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
- package/templates/skills/review-offered-task/config.json +14 -0
- package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
- package/templates/skills/scheduling/content.md +4 -0
- package/templates/skills/slack-interaction/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +16 -0
- package/templates/skills/work-on-task/config.json +14 -0
- package/templates/skills/work-on-task/content.md +11 -0
- package/dist/assistant-dx3ew4d8.js +0 -212
- package/dist/cli-00kyrp1a.js +0 -317
- package/dist/cli-n1q8q14d.js +0 -177
- package/dist/cli-ss6vybmt.js +0 -269
- package/dist/handlers-e9vp2fr8.js +0 -592
- package/plugin/commands/work-on-task.md +0 -39
- package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
package/src/http/sessions.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { countSessions, getRootTaskChain, getTaskById, listRecentSessions } from "../be/db";
|
|
4
4
|
import { getTaskSteeringFields } from "../be/steering";
|
|
5
|
+
import { mintSessionToken, revokeSessionToken } from "../be/users";
|
|
5
6
|
import { AgentTaskSchema, AgentTaskStatusSchema, SteerModeSchema } from "../types";
|
|
7
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
6
8
|
import { route } from "./route-def";
|
|
7
9
|
import { jsonError } from "./utils";
|
|
8
10
|
|
|
@@ -68,6 +70,57 @@ const TaskWithSteeringSchema = AgentTaskSchema.extend({
|
|
|
68
70
|
|
|
69
71
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
70
72
|
|
|
73
|
+
/** Maximum TTL for an ephemeral session token: 7 days. */
|
|
74
|
+
const MAX_SESSION_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
75
|
+
|
|
76
|
+
const mintSessionTokenRoute = route({
|
|
77
|
+
method: "post",
|
|
78
|
+
path: "/api/sessions/tokens",
|
|
79
|
+
pattern: ["api", "sessions", "tokens"],
|
|
80
|
+
summary: "Mint an ephemeral session token for an ACP provider session",
|
|
81
|
+
description:
|
|
82
|
+
"Returns a short-lived aseph_ bearer for use by the ACP adapter in place of the full " +
|
|
83
|
+
"operator key. The token expires at the requested TTL and is actively revoked when the " +
|
|
84
|
+
"session ends. Only the operator key may mint session tokens.",
|
|
85
|
+
tags: ["Sessions"],
|
|
86
|
+
body: z.object({
|
|
87
|
+
agentId: z.string().min(1),
|
|
88
|
+
taskId: z.string().min(1),
|
|
89
|
+
ttlMs: z.number().int().positive().max(MAX_SESSION_TOKEN_TTL_MS),
|
|
90
|
+
}),
|
|
91
|
+
responses: {
|
|
92
|
+
200: {
|
|
93
|
+
description: "Minted token plaintext (returned once) and its stable token ID",
|
|
94
|
+
schema: z.object({
|
|
95
|
+
tokenId: z.string(),
|
|
96
|
+
plaintext: z.string(),
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
400: { description: "Validation error" },
|
|
100
|
+
401: { description: "Unauthorized" },
|
|
101
|
+
},
|
|
102
|
+
auth: { apiKey: true },
|
|
103
|
+
rbac: { ungated: "operator-only: minting is gated on the full operator key in auth middleware" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const revokeSessionTokenRoute = route({
|
|
107
|
+
method: "delete",
|
|
108
|
+
path: "/api/sessions/tokens/{tokenId}",
|
|
109
|
+
pattern: ["api", "sessions", "tokens", null],
|
|
110
|
+
summary: "Revoke an ephemeral session token",
|
|
111
|
+
description: "Revokes an aseph_ bearer token by ID. No-op if already revoked.",
|
|
112
|
+
tags: ["Sessions"],
|
|
113
|
+
params: z.object({ tokenId: z.string() }),
|
|
114
|
+
responses: {
|
|
115
|
+
204: { description: "Token revoked (or was already revoked)" },
|
|
116
|
+
401: { description: "Unauthorized" },
|
|
117
|
+
},
|
|
118
|
+
auth: { apiKey: true },
|
|
119
|
+
rbac: {
|
|
120
|
+
ungated: "operator-only: revocation is gated on the full operator key in auth middleware",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
71
124
|
const listSessions = route({
|
|
72
125
|
method: "get",
|
|
73
126
|
path: "/api/sessions",
|
|
@@ -136,6 +189,35 @@ export async function handleSessions(
|
|
|
136
189
|
pathSegments: string[],
|
|
137
190
|
queryParams: URLSearchParams,
|
|
138
191
|
): Promise<boolean> {
|
|
192
|
+
if (mintSessionTokenRoute.match(req.method, pathSegments)) {
|
|
193
|
+
// Restrict to the full operator key — aseph_ and aswt_ bearers must not be
|
|
194
|
+
// able to mint new session tokens.
|
|
195
|
+
if (getRequestAuth(req)?.kind !== "operator") {
|
|
196
|
+
jsonError(res, "Unauthorized", 401);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
const parsed = await mintSessionTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
200
|
+
if (!parsed) return true;
|
|
201
|
+
const { agentId, taskId, ttlMs } = parsed.body;
|
|
202
|
+
const token = await mintSessionToken(agentId, taskId, ttlMs);
|
|
203
|
+
mintSessionTokenRoute.respond(res, 200, { tokenId: token.tokenId, plaintext: token.plaintext });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (revokeSessionTokenRoute.match(req.method, pathSegments)) {
|
|
208
|
+
// Restrict revocation to the operator key as well.
|
|
209
|
+
if (getRequestAuth(req)?.kind !== "operator") {
|
|
210
|
+
jsonError(res, "Unauthorized", 401);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
const parsed = await revokeSessionTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
214
|
+
if (!parsed) return true;
|
|
215
|
+
await revokeSessionToken(parsed.params.tokenId);
|
|
216
|
+
res.writeHead(204);
|
|
217
|
+
res.end();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
139
221
|
if (listSessions.match(req.method, pathSegments)) {
|
|
140
222
|
const parsed = await listSessions.parse(req, res, pathSegments, queryParams);
|
|
141
223
|
if (!parsed) return true;
|
package/src/http/tasks.ts
CHANGED
|
@@ -515,7 +515,9 @@ const listPausedTasks = route({
|
|
|
515
515
|
responses: {
|
|
516
516
|
200: {
|
|
517
517
|
description: "Paused task list",
|
|
518
|
-
schema: z.object({
|
|
518
|
+
schema: z.object({
|
|
519
|
+
tasks: z.array(AgentTaskSchema.extend({ attachments: z.array(TaskAttachmentSchema) })),
|
|
520
|
+
}),
|
|
519
521
|
},
|
|
520
522
|
},
|
|
521
523
|
});
|
|
@@ -1449,7 +1451,13 @@ export async function handleTasks(
|
|
|
1449
1451
|
return true;
|
|
1450
1452
|
}
|
|
1451
1453
|
const pausedTasks = await getPausedTasksForAgent(myAgentId);
|
|
1452
|
-
|
|
1454
|
+
const tasks = await Promise.all(
|
|
1455
|
+
pausedTasks.map(async (task) => ({
|
|
1456
|
+
...task,
|
|
1457
|
+
attachments: await getTaskAttachments(task.id),
|
|
1458
|
+
})),
|
|
1459
|
+
);
|
|
1460
|
+
listPausedTasks.respond(res, 200, { tasks });
|
|
1453
1461
|
return true;
|
|
1454
1462
|
}
|
|
1455
1463
|
|
|
@@ -74,7 +74,8 @@ Choose the path by the shape of the work:
|
|
|
74
74
|
Store progress with \`store-progress\` at each milestone. A milestone is a result the lead could act on.
|
|
75
75
|
The task is done when \`store-progress\` carries status \`completed\` and an \`output\` that names the result and every artifact link. On failure, status \`failed\` and a \`failureReason\` that names what you tried.
|
|
76
76
|
When the task carries an \`outputSchema\`, \`output\` is JSON that matches it.
|
|
77
|
-
When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can.
|
|
77
|
+
When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can.
|
|
78
|
+
The task has four endings. When you are done: \`completed\`. When the answer needs time (a build, a deploy, a reply): \`defer-task\` with a summary of what you did, when to wake up, and what to check. It completes the task now; the wake-up task continues it. When a person must decide: \`request-human-input\`. When nothing else is possible: \`failed\` with the blocker.
|
|
78
79
|
`,
|
|
79
80
|
variables: [],
|
|
80
81
|
category: "system",
|
|
@@ -89,9 +90,10 @@ registerTemplate({
|
|
|
89
90
|
Your output is delegation and review. Workers implement, research, analyze, and write. Data gathering, even a quick query, goes to a worker. You answer simple factual questions yourself.
|
|
90
91
|
|
|
91
92
|
\`get-swarm\` is the roster. Route by capability and load.
|
|
93
|
+
Delegate with \`send-task\`. Read a child's result with \`get-task-details\`.
|
|
92
94
|
A task states the goal, the repo URL when there is one, and the constraints. Workers know git, the skills, and \`store-progress\`.
|
|
93
95
|
Delegate by the shape of the work: a workflow for multi-step or fan-out work, a schedule for recurring work, a script for bulk data, an inline \`script-run\` for a one-off bulk job you can run yourself. The \`workflow-iterate\`, \`scheduling\`, and \`swarm-scripts\` skills build them.
|
|
94
|
-
Research or exploration: tell the worker to use
|
|
96
|
+
Research or exploration: tell the worker to use the \`researching\` skill. A large feature: a task for the \`planning\` skill first, then a task for the \`implementing\` skill with \`parentTaskId\`. A small fix: direct implementation.
|
|
95
97
|
A follow-up that continues earlier work carries \`parentTaskId\`. The worker receives the prior context.
|
|
96
98
|
A task whose result depends on the workers' output: wait for the children with the \`wait-for-task\` script, then merge and complete the task yourself. A turn that ends with children still running leaves the task unfinished.
|
|
97
99
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@agentclientprotocol/sdk";
|
|
13
13
|
import pkg from "../../package.json";
|
|
14
14
|
import type { AcpSessionConfigOption } from "../types";
|
|
15
|
+
import { mintAcpSessionToken, revokeAcpSessionToken } from "../utils/acp-session-token";
|
|
15
16
|
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
16
17
|
import {
|
|
17
18
|
detachedProcessGroup,
|
|
@@ -176,6 +177,10 @@ class ACPSession implements ProviderSession {
|
|
|
176
177
|
private output = "";
|
|
177
178
|
private completionPromise: Promise<ProviderResult>;
|
|
178
179
|
private completionResolve!: (result: ProviderResult) => void;
|
|
180
|
+
/** ID of the ephemeral aseph_ token minted for this session, or null when
|
|
181
|
+
* the fallback operator key is in use (e.g. during tests or if the API is
|
|
182
|
+
* not yet upgraded). Revoked in finish(). */
|
|
183
|
+
private ephemeralTokenId: string | null = null;
|
|
179
184
|
|
|
180
185
|
constructor(
|
|
181
186
|
private readonly connection: ClientSideConnection,
|
|
@@ -183,8 +188,10 @@ class ACPSession implements ProviderSession {
|
|
|
183
188
|
private readonly config: ProviderSessionConfig,
|
|
184
189
|
sessionId: string,
|
|
185
190
|
providerMeta?: Record<string, unknown>,
|
|
191
|
+
ephemeralTokenId?: string,
|
|
186
192
|
) {
|
|
187
193
|
this.sessionId = sessionId;
|
|
194
|
+
this.ephemeralTokenId = ephemeralTokenId ?? null;
|
|
188
195
|
this.completionPromise = new Promise((resolve) => {
|
|
189
196
|
this.completionResolve = resolve;
|
|
190
197
|
});
|
|
@@ -338,6 +345,11 @@ class ACPSession implements ProviderSession {
|
|
|
338
345
|
if (this.completed) return;
|
|
339
346
|
this.completed = true;
|
|
340
347
|
this.completionResolve(result);
|
|
348
|
+
// Revoke the ephemeral token now that the session is done. Best-effort:
|
|
349
|
+
// the token expires on its own, so a failure here is not critical.
|
|
350
|
+
if (this.ephemeralTokenId) {
|
|
351
|
+
void revokeAcpSessionToken(this.config.apiUrl, this.config.apiKey, this.ephemeralTokenId);
|
|
352
|
+
}
|
|
341
353
|
}
|
|
342
354
|
}
|
|
343
355
|
|
|
@@ -373,12 +385,23 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
373
385
|
const stream = ndJsonStream(fileSinkWritableStream(proc.stdin), proc.stdout);
|
|
374
386
|
const connection = new ClientSideConnection(() => client, stream);
|
|
375
387
|
|
|
388
|
+
let ephemeralToken: { tokenId: string; plaintext: string } | null = null;
|
|
376
389
|
try {
|
|
377
390
|
await connection.initialize({
|
|
378
391
|
protocolVersion: PROTOCOL_VERSION,
|
|
379
392
|
clientInfo: { name: "agent-swarm", version: pkg.version },
|
|
380
393
|
clientCapabilities: { session: { configOptions: { boolean: {} } } },
|
|
381
394
|
});
|
|
395
|
+
// Mint a short-lived session-scoped bearer so the ACP target receives
|
|
396
|
+
// an aseph_ token rather than the full operator key. Throws on failure
|
|
397
|
+
// so we fail closed instead of falling back to the operator key.
|
|
398
|
+
ephemeralToken = await mintAcpSessionToken(
|
|
399
|
+
config.apiUrl,
|
|
400
|
+
config.apiKey,
|
|
401
|
+
config.agentId,
|
|
402
|
+
config.taskId,
|
|
403
|
+
);
|
|
404
|
+
|
|
382
405
|
const installedServers = await fetchInstalledMcpServers(
|
|
383
406
|
config.apiUrl,
|
|
384
407
|
config.apiKey,
|
|
@@ -388,7 +411,7 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
388
411
|
// The v2 draft (@agentclientprotocol/sdk/experimental/v2) adds a fourth
|
|
389
412
|
// `{ type: "acp", name, serverId }` variant where the client hosts the MCP
|
|
390
413
|
// server itself and the agent tunnels MCP over the existing ACP connection
|
|
391
|
-
// (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop
|
|
414
|
+
// (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop -- the
|
|
392
415
|
// shape for an ACP agent with no network route to the swarm API. Gated on
|
|
393
416
|
// `mcpCapabilities.acp` and UNSTABLE; not adopted here.
|
|
394
417
|
const newSession = await connection.newSession({
|
|
@@ -399,7 +422,7 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
399
422
|
name: "swarm",
|
|
400
423
|
url: `${config.apiUrl.replace(/\/+$/, "")}/mcp`,
|
|
401
424
|
headers: [
|
|
402
|
-
{ name: "Authorization", value: `Bearer ${
|
|
425
|
+
{ name: "Authorization", value: `Bearer ${ephemeralToken.plaintext}` },
|
|
403
426
|
{ name: "X-Agent-ID", value: config.agentId },
|
|
404
427
|
{ name: "X-Source-Task-Id", value: config.taskId },
|
|
405
428
|
],
|
|
@@ -413,13 +436,26 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
413
436
|
newSession.configOptions ?? [],
|
|
414
437
|
target.configuredOptions(config),
|
|
415
438
|
);
|
|
416
|
-
session = new ACPSession(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
439
|
+
session = new ACPSession(
|
|
440
|
+
connection,
|
|
441
|
+
proc,
|
|
442
|
+
config,
|
|
443
|
+
newSession.sessionId,
|
|
444
|
+
{
|
|
445
|
+
target: target.target,
|
|
446
|
+
configOptions: sanitizeAcpConfigOptions(configOptions),
|
|
447
|
+
},
|
|
448
|
+
ephemeralToken.tokenId,
|
|
449
|
+
);
|
|
420
450
|
for (const event of preSessionEvents) session.emitFromAcp(event);
|
|
421
451
|
return session;
|
|
422
452
|
} catch (err) {
|
|
453
|
+
// Revoke the ephemeral token before re-throwing if ACPSession has not yet
|
|
454
|
+
// taken ownership of it (i.e. setup failed after mint but before the
|
|
455
|
+
// ACPSession constructor ran).
|
|
456
|
+
if (ephemeralToken && !session) {
|
|
457
|
+
void revokeAcpSessionToken(config.apiUrl, config.apiKey, ephemeralToken.tokenId);
|
|
458
|
+
}
|
|
423
459
|
await terminateProcessGroup(proc.pid);
|
|
424
460
|
throw new Error(`ACP target failed during startup: ${scrubSecrets(formatError(err))}`);
|
|
425
461
|
}
|
|
@@ -191,19 +191,36 @@ export async function checkPiMonoCredentials(
|
|
|
191
191
|
env: Record<string, string | undefined>,
|
|
192
192
|
opts: CredCheckOptions = {},
|
|
193
193
|
): Promise<CredStatus> {
|
|
194
|
-
// Determine Bedrock
|
|
195
|
-
// - Explicit: BEDROCK_AUTH_MODE=sdk
|
|
194
|
+
// Determine the Bedrock mode:
|
|
195
|
+
// - Explicit: BEDROCK_AUTH_MODE=sdk — the AWS SDK default credential chain
|
|
196
|
+
// - Explicit: BEDROCK_AUTH_MODE=bearer — an explicit Bedrock API key in
|
|
197
|
+
// AWS_BEARER_TOKEN_BEDROCK, which the AWS SDK picks up as the
|
|
198
|
+
// bearer identity for the Bedrock clients
|
|
196
199
|
// - Fallback: BEDROCK_AUTH_MODE absent AND MODEL_OVERRIDE starts with
|
|
197
200
|
// "amazon-bedrock/" (preserves today's prefix-inference semantics)
|
|
198
|
-
//
|
|
199
|
-
//
|
|
201
|
+
// Both explicit modes share the region check and the enumeration probe; they
|
|
202
|
+
// differ in what has to be present up front and in how readiness is reported.
|
|
200
203
|
const bedrockAuthMode = env.BEDROCK_AUTH_MODE?.toLowerCase();
|
|
204
|
+
const isBedrockBearer = bedrockAuthMode === "bearer";
|
|
201
205
|
const isBedrockSdk =
|
|
202
206
|
bedrockAuthMode === "sdk" ||
|
|
203
207
|
(bedrockAuthMode === undefined &&
|
|
204
208
|
env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/"));
|
|
205
209
|
|
|
206
|
-
if (
|
|
210
|
+
if (isBedrockBearer && !env.AWS_BEARER_TOKEN_BEDROCK) {
|
|
211
|
+
// The token is the whole point of this mode; without it there is nothing
|
|
212
|
+
// the probe could authenticate with, and the standard API keys do not
|
|
213
|
+
// apply to Bedrock, so do not fall through to them.
|
|
214
|
+
return {
|
|
215
|
+
ready: false,
|
|
216
|
+
missing: ["AWS_BEARER_TOKEN_BEDROCK"],
|
|
217
|
+
hint: "BEDROCK_AUTH_MODE=bearer requires AWS_BEARER_TOKEN_BEDROCK (a Bedrock API key); set it, or use BEDROCK_AUTH_MODE=sdk to authenticate through the AWS credential chain.",
|
|
218
|
+
bedrockModels: [],
|
|
219
|
+
bedrockRegion: env.AWS_REGION ?? "",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isBedrockSdk || isBedrockBearer) {
|
|
207
224
|
const region = env.AWS_REGION;
|
|
208
225
|
if (!region) {
|
|
209
226
|
// Do NOT fabricate a region. A guessed `us-east-1` can differ from where
|
|
@@ -231,7 +248,7 @@ export async function checkPiMonoCredentials(
|
|
|
231
248
|
return {
|
|
232
249
|
ready: true,
|
|
233
250
|
missing: [],
|
|
234
|
-
satisfiedBy: "sdk-delegated",
|
|
251
|
+
satisfiedBy: isBedrockBearer ? "env" : "sdk-delegated",
|
|
235
252
|
hint: `Bedrock models invocable in ${region} enumerated (${bedrockModels.length} usable; ListFoundationModels + ListInferenceProfiles).`,
|
|
236
253
|
bedrockModels,
|
|
237
254
|
bedrockRegion: region,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlinkSync } from "node:fs";
|
|
3
|
-
import { closeDb, createScheduledTask, initDb } from "../be/db";
|
|
4
|
-
import { calculateNextRun } from "./scheduler";
|
|
3
|
+
import { closeDb, createAgent, createScheduledTask, createTaskExtended, initDb } from "../be/db";
|
|
4
|
+
import { calculateNextRun, createStandaloneScheduleTask } from "./scheduler";
|
|
5
5
|
|
|
6
6
|
const TEST_DB_PATH = "./test-scheduler.sqlite";
|
|
7
7
|
|
|
@@ -151,3 +151,50 @@ describe("calculateNextRun", () => {
|
|
|
151
151
|
expect(nextRun).toBe("2026-01-19T09:00:00.000Z");
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
describe("createStandaloneScheduleTask", () => {
|
|
156
|
+
test("passes the schedule's parentTaskId through to the created task", async () => {
|
|
157
|
+
const agent = await createAgent({
|
|
158
|
+
name: "Deferred Wake-up Worker",
|
|
159
|
+
description: "Agent that owns the deferred task",
|
|
160
|
+
role: "worker",
|
|
161
|
+
isLead: false,
|
|
162
|
+
status: "idle",
|
|
163
|
+
maxTasks: 1,
|
|
164
|
+
capabilities: [],
|
|
165
|
+
});
|
|
166
|
+
const parent = await createTaskExtended("original work that needed time", {
|
|
167
|
+
agentId: agent.id,
|
|
168
|
+
source: "mcp",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const schedule = await createScheduledTask({
|
|
172
|
+
name: "test-deferred-parent",
|
|
173
|
+
taskTemplate: `Resume task ${parent.id}: the deploy should be green now`,
|
|
174
|
+
scheduleType: "one_time",
|
|
175
|
+
nextRunAt: new Date(Date.now() + 60_000).toISOString(),
|
|
176
|
+
targetAgentId: agent.id,
|
|
177
|
+
createdByAgentId: agent.id,
|
|
178
|
+
taskType: "deferred",
|
|
179
|
+
tags: ["deferred"],
|
|
180
|
+
parentTaskId: parent.id,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const task = await createStandaloneScheduleTask(schedule);
|
|
184
|
+
expect(task.parentTaskId).toBe(parent.id);
|
|
185
|
+
expect(task.taskType).toBe("deferred");
|
|
186
|
+
expect(task.tags).toContain("deferred");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("a schedule without parentTaskId creates a parentless task", async () => {
|
|
190
|
+
const schedule = await createScheduledTask({
|
|
191
|
+
name: "test-no-parent",
|
|
192
|
+
taskTemplate: "Ordinary scheduled work",
|
|
193
|
+
cronExpression: "0 9 * * *",
|
|
194
|
+
timezone: "UTC",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const task = await createStandaloneScheduleTask(schedule);
|
|
198
|
+
expect(task.parentTaskId).toBeFalsy();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -73,6 +73,10 @@ export async function createStandaloneScheduleTask(
|
|
|
73
73
|
source: "schedule",
|
|
74
74
|
requestedByUserId: schedule.createdBy,
|
|
75
75
|
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
76
|
+
// Set only by `defer-task`. An explicit parent wins over the sibling-awareness
|
|
77
|
+
// auto-wiring (see withSiblingAwareness in src/tasks/sibling-awareness.ts), so
|
|
78
|
+
// the wake-up run continues the deferred task rather than a random sibling.
|
|
79
|
+
parentTaskId: schedule.parentTaskId,
|
|
76
80
|
});
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -12,6 +12,7 @@ export const SDK_TOOL_NAME_MAP = {
|
|
|
12
12
|
task_list: "get-tasks",
|
|
13
13
|
task_get: "get-task-details",
|
|
14
14
|
task_storeProgress: "store-progress",
|
|
15
|
+
task_defer: "defer-task",
|
|
15
16
|
task_poll: "poll-task",
|
|
16
17
|
task_send: "send-task",
|
|
17
18
|
task_cancel: "cancel-task", // destructive
|
|
@@ -143,6 +143,14 @@ declare module "swarm-sdk" {
|
|
|
143
143
|
task_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
144
144
|
task_get(args: { taskId: string }): Promise<unknown>;
|
|
145
145
|
task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
|
|
146
|
+
task_defer(args: {
|
|
147
|
+
taskId: string;
|
|
148
|
+
delayMs?: number;
|
|
149
|
+
runAt?: string;
|
|
150
|
+
summary: string;
|
|
151
|
+
note: string;
|
|
152
|
+
checks?: string[];
|
|
153
|
+
}): Promise<unknown>;
|
|
146
154
|
task_poll(args?: Record<string, unknown>): Promise<unknown>;
|
|
147
155
|
// --- kv ---
|
|
148
156
|
kv_get<T = unknown>(args: {
|
|
@@ -125,6 +125,14 @@ declare module "swarm-sdk" {
|
|
|
125
125
|
task_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
126
126
|
task_get(args: { taskId: string }): Promise<unknown>;
|
|
127
127
|
task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
|
|
128
|
+
task_defer(args: {
|
|
129
|
+
taskId: string;
|
|
130
|
+
delayMs?: number;
|
|
131
|
+
runAt?: string;
|
|
132
|
+
summary: string;
|
|
133
|
+
note: string;
|
|
134
|
+
checks?: string[];
|
|
135
|
+
}): Promise<unknown>;
|
|
128
136
|
task_poll(args?: Record<string, unknown>): Promise<unknown>;
|
|
129
137
|
// --- kv ---
|
|
130
138
|
kv_get<T = unknown>(args: {
|
package/src/server.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { registerCreateMetricTool } from "./tools/create-metric";
|
|
|
26
26
|
import { registerCreatePageTool } from "./tools/create-page";
|
|
27
27
|
import { registerCredentialBindingsTool } from "./tools/credential-bindings";
|
|
28
28
|
import { registerDbQueryTool } from "./tools/db-query";
|
|
29
|
+
import { registerDeferTaskTool } from "./tools/defer-task";
|
|
29
30
|
import { registerDeleteChannelTool } from "./tools/delete-channel";
|
|
30
31
|
import { registerDeletePageTool } from "./tools/delete-page";
|
|
31
32
|
import { registerGetMetricsTool } from "./tools/get-metrics";
|
|
@@ -421,6 +422,7 @@ export async function createServer(opts: { scriptsOnly?: boolean; fullSurface?:
|
|
|
421
422
|
if (hasCapability("scheduling")) {
|
|
422
423
|
registerListSchedulesTool(server);
|
|
423
424
|
registerCreateScheduleTool(server);
|
|
425
|
+
registerDeferTaskTool(server);
|
|
424
426
|
registerUpdateScheduleTool(server);
|
|
425
427
|
registerPatchScheduleTool(server);
|
|
426
428
|
registerDeleteScheduleTool(server);
|
package/src/slack/assistant.ts
CHANGED
|
@@ -2,10 +2,17 @@ import { Assistant } from "@slack/bolt";
|
|
|
2
2
|
import { getAgentWorkingOnThread, getLeadAgent, getMostRecentTaskInThread } from "../be/db";
|
|
3
3
|
import { resolveTemplate } from "../prompts/resolver";
|
|
4
4
|
import { slackContextKey } from "../tasks/context-key";
|
|
5
|
-
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
6
5
|
import { ackSlackMessage, reactionName } from "./ack";
|
|
7
6
|
import { resolveSlackUserId, rewriteSlackMentions } from "./enrich";
|
|
8
7
|
import { wasEventSeen } from "./event-dedup";
|
|
8
|
+
import type { SlackFile } from "./files";
|
|
9
|
+
import {
|
|
10
|
+
bufferedFileFailures,
|
|
11
|
+
buildEffectiveText,
|
|
12
|
+
createSlackTaskWithFiles,
|
|
13
|
+
fetchSlackFiles,
|
|
14
|
+
notifySlackFileFailures,
|
|
15
|
+
} from "./inbound-files";
|
|
9
16
|
import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
|
|
10
17
|
import { hasOtherUserMention } from "./router";
|
|
11
18
|
import { bufferThreadMessage, getBufferMessageCount } from "./thread-buffer";
|
|
@@ -80,12 +87,10 @@ export function createAssistant(): Assistant {
|
|
|
80
87
|
const threadTs = (msg.thread_ts as string) || message.ts;
|
|
81
88
|
const channelId = message.channel;
|
|
82
89
|
const messageText = (msg.text as string) || "";
|
|
90
|
+
// `file_share` messages land here too: an image sent with no caption
|
|
91
|
+
// arrives with empty `text` and everything in `files`.
|
|
92
|
+
const files = msg.files as SlackFile[] | undefined;
|
|
83
93
|
const userId = (msg.user as string) || "";
|
|
84
|
-
// Any in-body `<@U…>` mention the requester typed is rewritten via
|
|
85
|
-
// the identity primitive before it reaches agent-visible task text —
|
|
86
|
-
// never a raw Slack ID. Bot-mention routing checks below use the
|
|
87
|
-
// raw `messageText`, not this rendered copy.
|
|
88
|
-
const renderedMessageText = await rewriteSlackMentions(messageText);
|
|
89
94
|
|
|
90
95
|
// Resolve the bot's own Slack user ID (cached after first call) so we can
|
|
91
96
|
// check whether this message is actually addressed to us.
|
|
@@ -123,30 +128,55 @@ export function createAssistant(): Assistant {
|
|
|
123
128
|
// 1. Check if an agent is already working in this thread
|
|
124
129
|
const workingAgent = await getAgentWorkingOnThread(channelId, threadTs);
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// Follow-up message → route to the same agent. Buffered follow-ups
|
|
132
|
+
// carry the `[File: …]` lines only; their files are not attached.
|
|
133
|
+
if (workingAgent && workingAgent.status !== "offline" && isAdditiveSlack()) {
|
|
134
|
+
const unattached = bufferedFileFailures(files);
|
|
135
|
+
bufferThreadMessage(
|
|
136
|
+
channelId,
|
|
137
|
+
threadTs,
|
|
138
|
+
buildEffectiveText(messageText, files, unattached),
|
|
139
|
+
userId,
|
|
140
|
+
message.ts,
|
|
141
|
+
);
|
|
142
|
+
const count = getBufferMessageCount(`${channelId}:${threadTs}`);
|
|
143
|
+
const event = count === 1 ? "accepted" : "buffered";
|
|
144
|
+
await ackSlackMessage(client, channelId, message.ts, reactionName(event), event);
|
|
145
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
146
|
+
await safeSetStatus("Queuing follow-up...");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fetch shared files before the task text is final: a file that can't be
|
|
151
|
+
// downloaded is flagged in it.
|
|
152
|
+
if (files?.length) await safeSetStatus("Downloading attachments...");
|
|
153
|
+
await using inbound = await fetchSlackFiles(client, files);
|
|
154
|
+
// Any in-body `<@U…>` mention the requester typed is rewritten via
|
|
155
|
+
// the identity primitive before it reaches agent-visible task text —
|
|
156
|
+
// never a raw Slack ID. Bot-mention routing checks above use the
|
|
157
|
+
// raw `messageText`, not this rendered copy.
|
|
158
|
+
const renderedMessageText = await rewriteSlackMentions(
|
|
159
|
+
buildEffectiveText(messageText, inbound.files, inbound.failed),
|
|
160
|
+
);
|
|
136
161
|
|
|
162
|
+
if (workingAgent && workingAgent.status !== "offline") {
|
|
137
163
|
// Otherwise, create a follow-up task for the working agent
|
|
138
164
|
const latestTask = await getMostRecentTaskInThread(channelId, threadTs);
|
|
139
|
-
const task = await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
const { task, unattached } = await createSlackTaskWithFiles(
|
|
166
|
+
renderedMessageText,
|
|
167
|
+
{
|
|
168
|
+
agentId: workingAgent.id,
|
|
169
|
+
source: "slack",
|
|
170
|
+
slackChannelId: channelId,
|
|
171
|
+
slackThreadTs: threadTs,
|
|
172
|
+
slackTriggerMessageTs: message.ts,
|
|
173
|
+
slackUserId: userId,
|
|
174
|
+
parentTaskId: latestTask?.id,
|
|
175
|
+
requestedByUserId,
|
|
176
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
177
|
+
},
|
|
178
|
+
inbound,
|
|
179
|
+
);
|
|
150
180
|
await ackSlackMessage(
|
|
151
181
|
client,
|
|
152
182
|
channelId,
|
|
@@ -154,6 +184,7 @@ export function createAssistant(): Assistant {
|
|
|
154
184
|
reactionName("accepted"),
|
|
155
185
|
"accepted",
|
|
156
186
|
);
|
|
187
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
157
188
|
|
|
158
189
|
if (isSlackRenderV2Enabled()) await ensureSlackThreadTree([task.id]);
|
|
159
190
|
|
|
@@ -182,15 +213,19 @@ export function createAssistant(): Assistant {
|
|
|
182
213
|
const lead = await getLeadAgent();
|
|
183
214
|
if (!lead) {
|
|
184
215
|
// No lead — still queue the task
|
|
185
|
-
const task = await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
const { task, unattached } = await createSlackTaskWithFiles(
|
|
217
|
+
renderedMessageText + channelContext,
|
|
218
|
+
{
|
|
219
|
+
source: "slack",
|
|
220
|
+
slackChannelId: channelId,
|
|
221
|
+
slackThreadTs: threadTs,
|
|
222
|
+
slackTriggerMessageTs: message.ts,
|
|
223
|
+
slackUserId: userId,
|
|
224
|
+
requestedByUserId,
|
|
225
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
226
|
+
},
|
|
227
|
+
inbound,
|
|
228
|
+
);
|
|
194
229
|
await ackSlackMessage(
|
|
195
230
|
client,
|
|
196
231
|
channelId,
|
|
@@ -198,6 +233,7 @@ export function createAssistant(): Assistant {
|
|
|
198
233
|
reactionName("accepted"),
|
|
199
234
|
"accepted",
|
|
200
235
|
);
|
|
236
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
201
237
|
if (isSlackRenderV2Enabled()) {
|
|
202
238
|
await ensureSlackThreadTree([task.id]);
|
|
203
239
|
} else {
|
|
@@ -207,17 +243,22 @@ export function createAssistant(): Assistant {
|
|
|
207
243
|
return;
|
|
208
244
|
}
|
|
209
245
|
|
|
210
|
-
const task = await
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
246
|
+
const { task, unattached } = await createSlackTaskWithFiles(
|
|
247
|
+
renderedMessageText + channelContext,
|
|
248
|
+
{
|
|
249
|
+
agentId: lead.id,
|
|
250
|
+
source: "slack",
|
|
251
|
+
slackChannelId: channelId,
|
|
252
|
+
slackThreadTs: threadTs,
|
|
253
|
+
slackTriggerMessageTs: message.ts,
|
|
254
|
+
slackUserId: userId,
|
|
255
|
+
requestedByUserId,
|
|
256
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
257
|
+
},
|
|
258
|
+
inbound,
|
|
259
|
+
);
|
|
220
260
|
await ackSlackMessage(client, channelId, message.ts, reactionName("accepted"), "accepted");
|
|
261
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
221
262
|
if (isSlackRenderV2Enabled()) await ensureSlackThreadTree([task.id]);
|
|
222
263
|
// setStatus shows typing indicator — watcher will post final result when done
|
|
223
264
|
} catch (error) {
|
|
@@ -17,15 +17,13 @@ import { createAssistant } from "./assistant";
|
|
|
17
17
|
import * as slackEnrichModule from "./enrich";
|
|
18
18
|
import type { SlackFile } from "./files";
|
|
19
19
|
import {
|
|
20
|
-
buildAttachmentText,
|
|
21
|
-
buildEffectiveText,
|
|
22
20
|
checkUserAccess,
|
|
23
|
-
formatFileSize,
|
|
24
21
|
isBotMessage,
|
|
25
22
|
isSwarmThreadRoot,
|
|
26
23
|
registerMessageHandler,
|
|
27
24
|
type UserFilterConfig,
|
|
28
25
|
} from "./handlers";
|
|
26
|
+
import { buildAttachmentText, buildEffectiveText, formatFileSize } from "./inbound-files";
|
|
29
27
|
import { SLACK_REACTION_CONFIG_KEYS } from "./reaction-shortcode";
|
|
30
28
|
|
|
31
29
|
describe("checkUserAccess", () => {
|