@desplega.ai/agent-swarm 1.83.2 → 1.84.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +48 -8
  2. package/openapi.json +24 -3
  3. package/package.json +1 -1
  4. package/src/be/migrations/076_kapso_sender_user_backfill.sql +43 -0
  5. package/src/commands/context-preamble.ts +178 -0
  6. package/src/commands/runner.ts +28 -1
  7. package/src/http/users.ts +11 -3
  8. package/src/http/webhooks.ts +101 -0
  9. package/src/integrations/kapso/client.ts +198 -0
  10. package/src/integrations/kapso/config.ts +104 -0
  11. package/src/integrations/kapso/inbound.ts +147 -0
  12. package/src/prompts/base-prompt.ts +15 -2
  13. package/src/prompts/session-templates.ts +26 -12
  14. package/src/server.ts +14 -0
  15. package/src/tests/agentmail-sending-skill.test.ts +75 -0
  16. package/src/tests/agents-list-model-display.test.ts +33 -0
  17. package/src/tests/base-prompt.test.ts +90 -1
  18. package/src/tests/http-users.test.ts +53 -0
  19. package/src/tests/kapso-client.test.ts +94 -0
  20. package/src/tests/kapso-inbound.test.ts +257 -0
  21. package/src/tests/kv-page-proxy.test.ts +1 -0
  22. package/src/tests/pagination-metrics.test.ts +4 -4
  23. package/src/tests/prompt-template-session.test.ts +13 -3
  24. package/src/tests/runner-context-preamble.test.ts +202 -0
  25. package/src/tests/tool-annotations.test.ts +3 -2
  26. package/src/tools/cancel-task.ts +13 -5
  27. package/src/tools/get-task-details.ts +18 -10
  28. package/src/tools/get-tasks.ts +9 -4
  29. package/src/tools/register-kapso-number.ts +210 -0
  30. package/src/tools/send-task.ts +9 -5
  31. package/src/tools/task-action.ts +20 -10
  32. package/src/tools/templates.ts +35 -0
  33. package/src/tools/tool-config.ts +6 -0
  34. package/src/tools/whatsapp-message.ts +135 -0
  35. package/templates/skills/agentmail-sending/SKILL.md +169 -0
  36. package/templates/skills/kapso-whatsapp/SKILL.md +383 -0
@@ -0,0 +1,202 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createServer, type Server } from "node:http";
3
+ import {
4
+ buildContextPreamble,
5
+ CONTEXT_PREAMBLE_MAX_CHARS,
6
+ CONTEXT_PREAMBLE_MAX_TOKENS,
7
+ fetchTaskContextForPreamble,
8
+ type TaskContextForPreamble,
9
+ } from "../commands/context-preamble";
10
+
11
+ const TEST_PORT = 19091;
12
+ const API_URL = `http://localhost:${TEST_PORT}`;
13
+ const API_KEY = "test-key";
14
+
15
+ // In-memory task store for the mock server
16
+ const mockTasks: Record<string, TaskContextForPreamble> = {};
17
+
18
+ let server: Server;
19
+
20
+ beforeAll(async () => {
21
+ server = createServer((req, res) => {
22
+ const url = req.url ?? "";
23
+ const match = url.match(/^\/api\/tasks\/([^/?]+)/);
24
+ if (match) {
25
+ const id = match[1];
26
+ const task = mockTasks[id];
27
+ if (task) {
28
+ res.writeHead(200, { "Content-Type": "application/json" });
29
+ res.end(JSON.stringify(task));
30
+ } else {
31
+ res.writeHead(404, { "Content-Type": "application/json" });
32
+ res.end(JSON.stringify({ error: "not found" }));
33
+ }
34
+ return;
35
+ }
36
+ res.writeHead(404);
37
+ res.end();
38
+ });
39
+
40
+ await new Promise<void>((resolve) => server.listen(TEST_PORT, resolve));
41
+ });
42
+
43
+ afterAll(() => {
44
+ server.close();
45
+ // Clear mocks
46
+ for (const k of Object.keys(mockTasks)) delete mockTasks[k];
47
+ });
48
+
49
+ function seedTask(task: TaskContextForPreamble): void {
50
+ mockTasks[task.id] = task;
51
+ }
52
+
53
+ describe("fetchTaskContextForPreamble", () => {
54
+ test("returns null on 404", async () => {
55
+ const result = await fetchTaskContextForPreamble(API_URL, API_KEY, "missing-id");
56
+ expect(result).toBeNull();
57
+ });
58
+
59
+ test("fetches task context fields", async () => {
60
+ seedTask({
61
+ id: "task-a",
62
+ task: "Build the widget",
63
+ output: "Widget built successfully",
64
+ status: "completed",
65
+ attachments: [{ kind: "url", name: "Report", url: "https://example.com/report" }],
66
+ });
67
+
68
+ const result = await fetchTaskContextForPreamble(API_URL, API_KEY, "task-a");
69
+ expect(result).not.toBeNull();
70
+ expect(result?.id).toBe("task-a");
71
+ expect(result?.task).toBe("Build the widget");
72
+ expect(result?.output).toBe("Widget built successfully");
73
+ expect(result?.attachments).toHaveLength(1);
74
+ expect(result?.attachments?.[0].name).toBe("Report");
75
+ });
76
+ });
77
+
78
+ describe("buildContextPreamble", () => {
79
+ test("returns null when parent task not found", async () => {
80
+ const result = await buildContextPreamble(API_URL, API_KEY, "nonexistent-parent");
81
+ expect(result).toBeNull();
82
+ });
83
+
84
+ test("includes parent task subject and output in preamble", async () => {
85
+ seedTask({
86
+ id: "parent-1",
87
+ task: "Fix the auth bug in login flow",
88
+ output: "Fixed by patching jwt validation in auth.ts:42",
89
+ status: "completed",
90
+ });
91
+
92
+ const preamble = await buildContextPreamble(API_URL, API_KEY, "parent-1");
93
+ expect(preamble).not.toBeNull();
94
+ expect(preamble).toContain("parent-1");
95
+ expect(preamble).toContain("Fix the auth bug in login flow");
96
+ expect(preamble).toContain("Fixed by patching jwt validation");
97
+ expect(preamble).toContain("get-task-details");
98
+ expect(preamble).toContain("Prior Conversation Context");
99
+ });
100
+
101
+ test("includes attachment pointers in preamble", async () => {
102
+ seedTask({
103
+ id: "parent-2",
104
+ task: "Generate a report",
105
+ output: "Report generated",
106
+ status: "completed",
107
+ attachments: [
108
+ { kind: "url", name: "Final Report", url: "https://example.com/report.pdf" },
109
+ {
110
+ kind: "agent-fs",
111
+ name: "Raw Data",
112
+ path: "thoughts/agent/research/data.md",
113
+ orgId: "org-123",
114
+ driveId: "drv-456",
115
+ },
116
+ ],
117
+ });
118
+
119
+ const preamble = await buildContextPreamble(API_URL, API_KEY, "parent-2");
120
+ expect(preamble).toContain("Final Report");
121
+ expect(preamble).toContain("https://example.com/report.pdf");
122
+ expect(preamble).toContain("Raw Data");
123
+ expect(preamble).toContain("live.agent-fs.dev");
124
+ expect(preamble).toContain("org-123");
125
+ expect(preamble).toContain("drv-456");
126
+ });
127
+
128
+ test("shows 'no output recorded' when task has no output or progress", async () => {
129
+ seedTask({
130
+ id: "parent-no-output",
131
+ task: "A task with no output yet",
132
+ status: "in_progress",
133
+ });
134
+
135
+ const preamble = await buildContextPreamble(API_URL, API_KEY, "parent-no-output");
136
+ expect(preamble).toContain("no output recorded");
137
+ });
138
+
139
+ test("walks ancestor chain and includes older ancestors as pointers", async () => {
140
+ seedTask({
141
+ id: "grandparent-1",
142
+ task: "Initial research task",
143
+ output: "Research complete",
144
+ status: "completed",
145
+ });
146
+ seedTask({
147
+ id: "child-of-grandparent",
148
+ task: "Second task referencing research",
149
+ output: "Second task done",
150
+ status: "completed",
151
+ parentTaskId: "grandparent-1",
152
+ });
153
+
154
+ const preamble = await buildContextPreamble(API_URL, API_KEY, "child-of-grandparent");
155
+ expect(preamble).not.toBeNull();
156
+ // Immediate parent (child-of-grandparent) gets inline detail
157
+ expect(preamble).toContain("child-of-grandparent");
158
+ expect(preamble).toContain("Second task done");
159
+ // Grandparent gets pointer-only entry
160
+ expect(preamble).toContain("grandparent-1");
161
+ expect(preamble).toContain("Older Ancestor Tasks");
162
+ expect(preamble).toContain("Initial research task");
163
+ });
164
+
165
+ test("enforces token budget — truncates oversized output", async () => {
166
+ // Generate output that exceeds the budget
167
+ const hugeOutput = "x".repeat(CONTEXT_PREAMBLE_MAX_CHARS + 5000);
168
+ seedTask({
169
+ id: "parent-big",
170
+ task: "Task with very large output",
171
+ output: hugeOutput,
172
+ status: "completed",
173
+ });
174
+
175
+ const preamble = await buildContextPreamble(API_URL, API_KEY, "parent-big");
176
+ expect(preamble).not.toBeNull();
177
+ // Preamble must be within budget (some slack for the truncation suffix)
178
+ expect(preamble?.length ?? 0).toBeLessThanOrEqual(
179
+ CONTEXT_PREAMBLE_MAX_CHARS + 300, // 300 chars slack for the truncation message
180
+ );
181
+ });
182
+
183
+ test("preamble starts with context section and ends with separator", async () => {
184
+ seedTask({
185
+ id: "parent-structure",
186
+ task: "A well-structured task",
187
+ output: "Done",
188
+ status: "completed",
189
+ });
190
+
191
+ const preamble = await buildContextPreamble(API_URL, API_KEY, "parent-structure");
192
+ expect(preamble).toContain("---");
193
+ expect(preamble).toContain("Prior Conversation Context");
194
+ // Should end with trailing separator
195
+ expect(preamble?.trimEnd()).toMatch(/---\s*$/);
196
+ });
197
+
198
+ test("CONTEXT_PREAMBLE_MAX_TOKENS is 2000 by default", () => {
199
+ expect(CONTEXT_PREAMBLE_MAX_TOKENS).toBe(2000);
200
+ expect(CONTEXT_PREAMBLE_MAX_CHARS).toBe(8000);
201
+ });
202
+ });
@@ -323,9 +323,10 @@ describe("Tool Annotations & Classification", () => {
323
323
  test("registered tool count matches expected total", () => {
324
324
  const count = Object.keys(tools).length;
325
325
  // We expect all tools to be registered when all capabilities are enabled (default)
326
- // Includes 11 skill tools, 7 MCP server tools, and reusable script tools
326
+ // Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
327
+ // native Kapso/WhatsApp tools (register/unregister number + send/reply message).
327
328
  expect(count).toBeGreaterThanOrEqual(45);
328
- expect(count).toBeLessThanOrEqual(100);
329
+ expect(count).toBeLessThanOrEqual(120);
329
330
  });
330
331
 
331
332
  test("core tools are fewer than deferred tools", () => {
@@ -142,12 +142,20 @@ export async function cancelTaskHandler(
142
142
 
143
143
  if ("content" in result) return result;
144
144
 
145
+ const structuredContent = {
146
+ yourAgentId: agentId,
147
+ ...result,
148
+ };
149
+
145
150
  return {
146
- content: [{ type: "text", text: result.message }],
147
- structuredContent: {
148
- yourAgentId: agentId,
149
- ...result,
150
- },
151
+ content: [
152
+ { type: "text", text: result.message },
153
+ {
154
+ type: "text",
155
+ text: JSON.stringify(structuredContent),
156
+ },
157
+ ],
158
+ structuredContent,
151
159
  };
152
160
  }
153
161
 
@@ -65,17 +65,25 @@ export async function getTaskDetailsHandler(
65
65
  ? { name: requestedByUser.name, email: requestedByUser.email }
66
66
  : undefined;
67
67
 
68
+ const structuredContent = {
69
+ yourAgentId: agentId,
70
+ success: true,
71
+ message: `Task "${taskId}" details retrieved.`,
72
+ task,
73
+ requestedBy,
74
+ logs,
75
+ attachments,
76
+ };
77
+
68
78
  return {
69
- content: [{ type: "text", text: `Task "${taskId}" details retrieved.` }],
70
- structuredContent: {
71
- yourAgentId: agentId,
72
- success: true,
73
- message: `Task "${taskId}" details retrieved.`,
74
- task,
75
- requestedBy,
76
- logs,
77
- attachments,
78
- },
79
+ content: [
80
+ { type: "text", text: `Task "${taskId}" details retrieved.` },
81
+ {
82
+ type: "text",
83
+ text: JSON.stringify(structuredContent),
84
+ },
85
+ ],
86
+ structuredContent,
79
87
  };
80
88
  }
81
89
 
@@ -140,6 +140,10 @@ export async function getTasksHandler(
140
140
  if (scheduleId) filters.push(`scheduleId='${scheduleId}'`);
141
141
 
142
142
  const filterMsg = filters.length > 0 ? ` (${filters.join(", ")})` : "";
143
+ const structuredContent = {
144
+ yourAgentId: agentId,
145
+ tasks: taskSummaries,
146
+ };
143
147
 
144
148
  return {
145
149
  content: [
@@ -147,11 +151,12 @@ export async function getTasksHandler(
147
151
  type: "text",
148
152
  text: `Found ${taskSummaries.length} task(s)${filterMsg}.`,
149
153
  },
154
+ {
155
+ type: "text",
156
+ text: JSON.stringify(structuredContent),
157
+ },
150
158
  ],
151
- structuredContent: {
152
- yourAgentId: agentId,
153
- tasks: taskSummaries,
154
- },
159
+ structuredContent,
155
160
  };
156
161
  }
157
162
 
@@ -0,0 +1,210 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById, getLeadAgent } from "@/be/db";
4
+ import { registerKapsoWebhook } from "@/integrations/kapso/client";
5
+ import {
6
+ deleteKapsoNumberMapping,
7
+ getKapsoConfig,
8
+ getKapsoNumberMapping,
9
+ type KapsoNumberMapping,
10
+ putKapsoNumberMapping,
11
+ } from "@/integrations/kapso/config";
12
+ import { createToolRegistrar } from "@/tools/utils";
13
+
14
+ /** Build the native inbound webhook URL the swarm exposes for Kapso deliveries. */
15
+ function nativeWebhookUrl(): string {
16
+ const base = (
17
+ process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`
18
+ ).replace(/\/+$/, "");
19
+ return `${base}/api/integrations/kapso/webhook`;
20
+ }
21
+
22
+ export const registerRegisterKapsoNumberTool = (server: McpServer) => {
23
+ createToolRegistrar(server)(
24
+ "register-kapso-number",
25
+ {
26
+ title: "Register Kapso WhatsApp Number",
27
+ annotations: { idempotentHint: true, openWorldHint: true },
28
+ description:
29
+ "Provision a Kapso WhatsApp phone number for native inbound routing. Lead-only. Points the number's Kapso webhook at the swarm's native handler (signed with KAPSO_WEBHOOK_HMAC_SECRET) and stores a KV mapping so inbound messages route to an agent (defaults to the lead, or a workflow if workflowId is given). Returns the stored mapping + the registered webhook URL.",
30
+ inputSchema: z.object({
31
+ phoneNumberId: z
32
+ .string()
33
+ .min(1)
34
+ .describe("Kapso/Meta phone-number ID to provision (KAPSO_PHONE_NUMBER_ID)."),
35
+ agentId: z
36
+ .string()
37
+ .uuid()
38
+ .optional()
39
+ .describe(
40
+ "Agent to route inbound messages to as a `kapso-inbound` task. Defaults to the lead agent when omitted.",
41
+ ),
42
+ workflowId: z
43
+ .string()
44
+ .uuid()
45
+ .optional()
46
+ .describe(
47
+ "Advanced override: dispatch inbound via this workflow's webhook trigger instead of a task.",
48
+ ),
49
+ name: z.string().optional().describe("Human-friendly display name for the number."),
50
+ }),
51
+ outputSchema: z.object({
52
+ yourAgentId: z.string().uuid().optional(),
53
+ success: z.boolean(),
54
+ message: z.string(),
55
+ webhookUrl: z.string().optional(),
56
+ webhookRegistered: z.boolean().optional(),
57
+ mapping: z
58
+ .object({
59
+ phoneNumberId: z.string(),
60
+ agentId: z.string().optional(),
61
+ workflowId: z.string().optional(),
62
+ name: z.string().optional(),
63
+ createdAt: z.string(),
64
+ })
65
+ .optional(),
66
+ }),
67
+ },
68
+ async ({ phoneNumberId, agentId, workflowId, name }, requestInfo) => {
69
+ try {
70
+ // Lead-only: provisioning a number rewires inbound routing for the
71
+ // whole swarm, so restrict it to the lead agent.
72
+ const callerAgent = requestInfo.agentId ? getAgentById(requestInfo.agentId) : null;
73
+ if (!callerAgent?.isLead) {
74
+ const msg = "Permission denied. Only the lead can register a Kapso number.";
75
+ return {
76
+ content: [{ type: "text", text: msg }],
77
+ structuredContent: { yourAgentId: requestInfo.agentId, success: false, message: msg },
78
+ };
79
+ }
80
+
81
+ // Default the routing target to the lead when no agent/workflow is given.
82
+ const ownerAgentId = agentId ?? (workflowId ? undefined : getLeadAgent()?.id);
83
+
84
+ const config = getKapsoConfig();
85
+ const webhookUrl = nativeWebhookUrl();
86
+
87
+ // Best-effort: point the Kapso webhook at our native handler. The KV
88
+ // mapping (the durable routing record the inbound handler reads) is
89
+ // written regardless, so a manually-configured number still routes.
90
+ let webhookRegistered = false;
91
+ let webhookNote = "";
92
+ if (!config.apiKey) {
93
+ webhookNote =
94
+ " (KAPSO_API_KEY not configured — skipped provider webhook registration; configure the webhook in the Kapso dashboard)";
95
+ } else {
96
+ const result = await registerKapsoWebhook({
97
+ apiBaseUrl: config.apiBaseUrl,
98
+ apiKey: config.apiKey,
99
+ phoneNumberId,
100
+ webhookUrl,
101
+ secret: config.webhookHmacSecret,
102
+ });
103
+ webhookRegistered = result.ok;
104
+ if (!result.ok) {
105
+ webhookNote = ` (provider webhook registration failed: ${result.errorMessage})`;
106
+ } else if (result.alreadyRegistered) {
107
+ webhookNote = " (webhook already registered — skipped re-creation)";
108
+ }
109
+ }
110
+
111
+ const mapping: KapsoNumberMapping = {
112
+ phoneNumberId,
113
+ ...(ownerAgentId ? { agentId: ownerAgentId } : {}),
114
+ ...(workflowId ? { workflowId } : {}),
115
+ ...(name ? { name } : {}),
116
+ createdAt: new Date().toISOString(),
117
+ };
118
+ putKapsoNumberMapping(mapping);
119
+
120
+ const text = `Registered Kapso number ${phoneNumberId} → ${
121
+ workflowId
122
+ ? `workflow ${workflowId}`
123
+ : ownerAgentId
124
+ ? `agent ${ownerAgentId}`
125
+ : "task pool"
126
+ }${webhookNote}`;
127
+ return {
128
+ content: [{ type: "text", text }],
129
+ structuredContent: {
130
+ yourAgentId: requestInfo.agentId,
131
+ success: true,
132
+ message: text,
133
+ webhookUrl,
134
+ webhookRegistered,
135
+ mapping,
136
+ },
137
+ };
138
+ } catch (err) {
139
+ const errorMessage = err instanceof Error ? err.message : String(err);
140
+ return {
141
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
142
+ structuredContent: {
143
+ yourAgentId: requestInfo.agentId,
144
+ success: false,
145
+ message: errorMessage,
146
+ },
147
+ };
148
+ }
149
+ },
150
+ );
151
+ };
152
+
153
+ export const registerUnregisterKapsoNumberTool = (server: McpServer) => {
154
+ createToolRegistrar(server)(
155
+ "unregister-kapso-number",
156
+ {
157
+ title: "Unregister Kapso WhatsApp Number",
158
+ annotations: { idempotentHint: true },
159
+ description:
160
+ "Remove a Kapso phone number's native routing mapping from the KV store. Lead-only. Inbound messages for the number stop routing through the native handler. The Kapso-side webhook is not deleted automatically — remove it in the Kapso dashboard if you want deliveries to stop.",
161
+ inputSchema: z.object({
162
+ phoneNumberId: z
163
+ .string()
164
+ .min(1)
165
+ .describe("Kapso/Meta phone-number ID whose mapping should be removed."),
166
+ }),
167
+ outputSchema: z.object({
168
+ yourAgentId: z.string().uuid().optional(),
169
+ success: z.boolean(),
170
+ message: z.string(),
171
+ }),
172
+ },
173
+ async ({ phoneNumberId }, requestInfo) => {
174
+ try {
175
+ const callerAgent = requestInfo.agentId ? getAgentById(requestInfo.agentId) : null;
176
+ if (!callerAgent?.isLead) {
177
+ const msg = "Permission denied. Only the lead can unregister a Kapso number.";
178
+ return {
179
+ content: [{ type: "text", text: msg }],
180
+ structuredContent: { yourAgentId: requestInfo.agentId, success: false, message: msg },
181
+ };
182
+ }
183
+
184
+ const existing = getKapsoNumberMapping(phoneNumberId);
185
+ const deleted = deleteKapsoNumberMapping(phoneNumberId);
186
+ const text = existing
187
+ ? `Unregistered Kapso number ${phoneNumberId}`
188
+ : `No mapping found for Kapso number ${phoneNumberId}`;
189
+ return {
190
+ content: [{ type: "text", text }],
191
+ structuredContent: {
192
+ yourAgentId: requestInfo.agentId,
193
+ success: deleted,
194
+ message: text,
195
+ },
196
+ };
197
+ } catch (err) {
198
+ const errorMessage = err instanceof Error ? err.message : String(err);
199
+ return {
200
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
201
+ structuredContent: {
202
+ yourAgentId: requestInfo.agentId,
203
+ success: false,
204
+ message: errorMessage,
205
+ },
206
+ };
207
+ }
208
+ },
209
+ );
210
+ };
@@ -326,13 +326,17 @@ export async function sendTaskHandler(
326
326
  });
327
327
 
328
328
  const result = txn();
329
+ const structuredContent = {
330
+ yourAgentId: creatorAgentId,
331
+ ...result,
332
+ };
329
333
 
330
334
  return {
331
- content: [{ type: "text", text: result.message }],
332
- structuredContent: {
333
- yourAgentId: creatorAgentId,
334
- ...result,
335
- },
335
+ content: [
336
+ { type: "text", text: result.message },
337
+ { type: "text", text: JSON.stringify(result) },
338
+ ],
339
+ structuredContent,
336
340
  };
337
341
  }
338
342
 
@@ -108,24 +108,34 @@ type TaskActionResult = {
108
108
 
109
109
  function agentOnlyActionResult(): CallToolResult {
110
110
  const message = "This action is only available to worker agents.";
111
+ const structuredContent = {
112
+ success: false,
113
+ message,
114
+ };
115
+
111
116
  return {
112
117
  isError: true,
113
- content: [{ type: "text", text: message }],
114
- structuredContent: {
115
- success: false,
116
- message,
117
- },
118
+ content: [
119
+ { type: "text", text: message },
120
+ { type: "text", text: JSON.stringify(structuredContent) },
121
+ ],
122
+ structuredContent,
118
123
  };
119
124
  }
120
125
 
121
126
  function taskActionCallResult(result: TaskActionResult, agentId?: string): CallToolResult {
122
127
  const { refusalSideEffects: _omit, ...publicResult } = result;
128
+ const structuredContent = {
129
+ yourAgentId: agentId,
130
+ ...publicResult,
131
+ };
132
+
123
133
  return {
124
- content: [{ type: "text", text: result.message }],
125
- structuredContent: {
126
- yourAgentId: agentId,
127
- ...publicResult,
128
- },
134
+ content: [
135
+ { type: "text", text: result.message },
136
+ { type: "text", text: JSON.stringify(structuredContent) },
137
+ ],
138
+ structuredContent,
129
139
  };
130
140
  }
131
141
 
@@ -7,6 +7,41 @@
7
7
 
8
8
  import { registerTemplate } from "../prompts/registry";
9
9
 
10
+ // ============================================================================
11
+ // Kapso/WhatsApp inbound (native handler creates a kapso-inbound task)
12
+ // ============================================================================
13
+
14
+ registerTemplate({
15
+ eventType: "kapso.message.received",
16
+ header: "",
17
+ defaultBody: `# WhatsApp inbound (Kapso)
18
+
19
+ A Kapso webhook fired on the swarm's provisioned WhatsApp number. Load the \`kapso-whatsapp\` skill, then triage this like any other interaction and reply on WhatsApp by quote-replying the inbound WAMID (\`context.message_id\`).
20
+
21
+ ## Source: WhatsApp (Kapso)
22
+ - conversation_id: {{conversation_id}}
23
+ - inbound_wamid: {{inbound_wamid}}
24
+ - sender_phone: {{sender_phone}}
25
+ - contact_name: {{contact_name}}
26
+ - phone_number_id: {{phone_number_id}}{{test_note}}
27
+
28
+ ## Message
29
+ {{message_text}}`,
30
+ variables: [
31
+ { name: "conversation_id", description: "Kapso conversation id, or 'unknown'" },
32
+ { name: "inbound_wamid", description: "Inbound message WAMID, or 'unknown'" },
33
+ { name: "sender_phone", description: "Sender phone (E.164 no +), or 'unknown'" },
34
+ { name: "contact_name", description: "Contact display name, or 'unknown'" },
35
+ { name: "phone_number_id", description: "Provisioned phone-number id, or 'unknown'" },
36
+ {
37
+ name: "test_note",
38
+ description: "Appended note when the payload is a Kapso test delivery (else empty)",
39
+ },
40
+ { name: "message_text", description: "Inbound message text or a non-text placeholder" },
41
+ ],
42
+ category: "event",
43
+ });
44
+
10
45
  // ============================================================================
11
46
  // Worker task follow-ups (created by store-progress for the lead)
12
47
  // ============================================================================
@@ -98,6 +98,12 @@ export const DEFERRED_TOOLS = new Set([
98
98
  // AgentMail (1)
99
99
  "register-agentmail-inbox",
100
100
 
101
+ // Kapso/WhatsApp (4)
102
+ "register-kapso-number",
103
+ "unregister-kapso-number",
104
+ "send-whatsapp-message",
105
+ "reply-whatsapp-message",
106
+
101
107
  // Tracker (6)
102
108
  "tracker-status",
103
109
  "tracker-link-task",