@desplega.ai/agent-swarm 1.91.0 → 1.92.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/openapi.json +585 -5
- package/package.json +1 -1
- package/src/be/db.ts +337 -1
- package/src/be/migrations/083_script_workflows.sql +51 -0
- package/src/be/modelsdev-cache.json +42352 -38595
- package/src/be/scripts/typecheck.ts +49 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +216 -6
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
- package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
- package/src/be/seed-scripts/index.ts +20 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +17 -0
- package/src/commands/runner.ts +43 -2
- package/src/http/db-query.ts +20 -5
- package/src/http/index.ts +10 -0
- package/src/http/script-runs.ts +555 -0
- package/src/prompts/session-templates.ts +24 -4
- package/src/providers/claude-adapter.ts +60 -13
- package/src/script-workflows/executor.ts +110 -0
- package/src/script-workflows/harness.ts +73 -0
- package/src/script-workflows/label-lint.ts +51 -0
- package/src/script-workflows/limits.ts +22 -0
- package/src/script-workflows/supervisor.ts +139 -0
- package/src/script-workflows/workflow-ctx.ts +205 -0
- package/src/scripts-runtime/sdk-allowlist.ts +3 -0
- package/src/scripts-runtime/types/stdlib.d.ts +60 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +60 -0
- package/src/server.ts +2 -0
- package/src/slack/handlers.ts +11 -4
- package/src/slack/message-text.ts +98 -0
- package/src/slack/thread-buffer.ts +5 -3
- package/src/tests/claude-adapter-binary.test.ts +147 -4
- package/src/tests/db-query.test.ts +28 -0
- package/src/tests/error-tracker.test.ts +121 -0
- package/src/tests/harness-provider-resolution.test.ts +33 -0
- package/src/tests/mcp-tools.test.ts +6 -0
- package/src/tests/prompt-template-session.test.ts +34 -5
- package/src/tests/script-runs-http.test.ts +278 -0
- package/src/tests/script-workflows-label-lint.test.ts +43 -0
- package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
- package/src/tests/scripts-mcp-e2e.test.ts +49 -2
- package/src/tests/seed-scripts.test.ts +347 -2
- package/src/tests/slack-message-text.test.ts +250 -0
- package/src/tests/system-default-skills.test.ts +40 -0
- package/src/tools/db-query.ts +16 -6
- package/src/tools/script-runs.ts +123 -0
- package/src/tools/slack-read.ts +12 -3
- package/src/tools/tool-config.ts +4 -1
- package/src/types.ts +52 -0
- package/src/utils/error-tracker.ts +40 -1
- package/src/utils/internal-ai/complete-structured.ts +10 -4
- package/src/workflows/executors/raw-llm.ts +76 -59
- package/templates/skills/pages/content.md +205 -55
- package/templates/skills/script-workflows/config.json +14 -0
- package/templates/skills/script-workflows/content.md +68 -0
- package/templates/skills/swarm-scripts/content.md +2 -3
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { stdlib } from "../scripts-runtime/stdlib";
|
|
2
|
+
|
|
3
|
+
type StepStatusResponse =
|
|
4
|
+
| { stepKey: string; stepType: string; result: unknown }
|
|
5
|
+
| { error: string };
|
|
6
|
+
|
|
7
|
+
type StepWriteResponse = { ok: true } | { error: string };
|
|
8
|
+
|
|
9
|
+
type RawLlmConfig = {
|
|
10
|
+
prompt: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
schema?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type AgentTaskConfig = {
|
|
16
|
+
template?: string;
|
|
17
|
+
task?: string;
|
|
18
|
+
agentId?: string;
|
|
19
|
+
tags?: string[];
|
|
20
|
+
priority?: number;
|
|
21
|
+
offerMode?: boolean;
|
|
22
|
+
dir?: string;
|
|
23
|
+
vcsRepo?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
parentTaskId?: string;
|
|
26
|
+
requestedByUserId?: string;
|
|
27
|
+
outputSchema?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SwarmScriptConfig = {
|
|
31
|
+
name?: string;
|
|
32
|
+
scriptName?: string;
|
|
33
|
+
source?: string;
|
|
34
|
+
args?: unknown;
|
|
35
|
+
scope?: "agent" | "global";
|
|
36
|
+
fsMode?: "none" | "workspace-rw";
|
|
37
|
+
intent?: string;
|
|
38
|
+
idempotencyKey?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type WorkflowRunInfo = {
|
|
42
|
+
id: string;
|
|
43
|
+
agentId: string;
|
|
44
|
+
args: unknown;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type WorkflowCtx = {
|
|
48
|
+
run: WorkflowRunInfo;
|
|
49
|
+
step: {
|
|
50
|
+
rawLlm: (label: string, config: RawLlmConfig) => Promise<unknown>;
|
|
51
|
+
agentTask: (label: string, config: AgentTaskConfig) => Promise<unknown>;
|
|
52
|
+
swarmScript: (label: string, config: SwarmScriptConfig) => Promise<unknown>;
|
|
53
|
+
humanInTheLoop: () => Promise<never>;
|
|
54
|
+
};
|
|
55
|
+
swarm: Record<string, (args?: unknown) => Promise<unknown>>;
|
|
56
|
+
stdlib: typeof stdlib;
|
|
57
|
+
logger: Console;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function encodeStepKey(label: string): string {
|
|
61
|
+
return encodeURIComponent(label);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function headers(apiKey: string, agentId: string): Record<string, string> {
|
|
65
|
+
return {
|
|
66
|
+
Authorization: `Bearer ${apiKey}`,
|
|
67
|
+
"X-Agent-ID": agentId,
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readJson(res: Response): Promise<unknown> {
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
return text ? JSON.parse(text) : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function apiError(prefix: string, status: number, body: unknown): Error {
|
|
78
|
+
const message =
|
|
79
|
+
body && typeof body === "object" && "error" in body
|
|
80
|
+
? String((body as { error: unknown }).error)
|
|
81
|
+
: JSON.stringify(body);
|
|
82
|
+
return new Error(`${prefix} failed with ${status}: ${message}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildWorkflowCtx(input: {
|
|
86
|
+
runId: string;
|
|
87
|
+
agentId: string;
|
|
88
|
+
apiKey: string;
|
|
89
|
+
baseUrl: string;
|
|
90
|
+
args: unknown;
|
|
91
|
+
}): WorkflowCtx {
|
|
92
|
+
const baseUrl = input.baseUrl.replace(/\/$/, "");
|
|
93
|
+
const authHeaders = headers(input.apiKey, input.agentId);
|
|
94
|
+
|
|
95
|
+
async function fetchJson(path: string, init: RequestInit = {}): Promise<unknown> {
|
|
96
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
97
|
+
...init,
|
|
98
|
+
headers: { ...authHeaders, ...((init.headers as Record<string, string>) ?? {}) },
|
|
99
|
+
});
|
|
100
|
+
const body = await readJson(res);
|
|
101
|
+
if (!res.ok) throw apiError(path, res.status, body);
|
|
102
|
+
return body;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function completedStep(
|
|
106
|
+
label: string,
|
|
107
|
+
): Promise<{ found: true; result: unknown } | { found: false }> {
|
|
108
|
+
const res = await fetch(
|
|
109
|
+
`${baseUrl}/api/internal/script-runs/${input.runId}/steps/${encodeStepKey(label)}`,
|
|
110
|
+
{
|
|
111
|
+
headers: authHeaders,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
if (res.status === 404) return { found: false };
|
|
115
|
+
const body = (await readJson(res)) as StepStatusResponse;
|
|
116
|
+
if (!res.ok) throw apiError(`step ${label}`, res.status, body);
|
|
117
|
+
return { found: true, result: "result" in body ? body.result : undefined };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function writeStep(
|
|
121
|
+
label: string,
|
|
122
|
+
stepType: string,
|
|
123
|
+
config: unknown,
|
|
124
|
+
status: "completed" | "failed",
|
|
125
|
+
result?: unknown,
|
|
126
|
+
error?: string,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const body = (await fetchJson(`/api/internal/script-runs/${input.runId}/steps`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: JSON.stringify({ stepKey: label, stepType, config, status, result, error }),
|
|
131
|
+
})) as StepWriteResponse;
|
|
132
|
+
if (!("ok" in body)) throw new Error(`Failed to write journal step ${label}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function durableStep(
|
|
136
|
+
label: string,
|
|
137
|
+
stepType: string,
|
|
138
|
+
config: unknown,
|
|
139
|
+
execute: () => Promise<unknown>,
|
|
140
|
+
): Promise<unknown> {
|
|
141
|
+
const replayed = await completedStep(label);
|
|
142
|
+
if (replayed.found) return replayed.result;
|
|
143
|
+
try {
|
|
144
|
+
const result = await execute();
|
|
145
|
+
await writeStep(label, stepType, config, "completed", result);
|
|
146
|
+
return result;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
149
|
+
await writeStep(label, stepType, config, "failed", undefined, error);
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const swarm = new Proxy({} as Record<string, (args?: unknown) => Promise<unknown>>, {
|
|
155
|
+
get(_target, prop) {
|
|
156
|
+
if (typeof prop !== "string") return undefined;
|
|
157
|
+
return (args?: unknown) =>
|
|
158
|
+
fetchJson("/api/mcp-bridge", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: JSON.stringify({ tool: prop.replaceAll("_", "-"), args: args ?? {} }),
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
run: { id: input.runId, agentId: input.agentId, args: input.args },
|
|
167
|
+
step: {
|
|
168
|
+
rawLlm: (label, config) =>
|
|
169
|
+
durableStep(label, "raw-llm", config, async () =>
|
|
170
|
+
fetchJson("/api/internal/raw-llm", {
|
|
171
|
+
method: "POST",
|
|
172
|
+
body: JSON.stringify(config),
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
agentTask: (label, config) =>
|
|
176
|
+
durableStep(label, "agent-task", config, async () =>
|
|
177
|
+
fetchJson(`/api/internal/script-runs/${input.runId}/agent-task`, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
body: JSON.stringify({ stepKey: label, ...config }),
|
|
180
|
+
}),
|
|
181
|
+
),
|
|
182
|
+
swarmScript: (label, config) =>
|
|
183
|
+
durableStep(label, "swarm-script", config, async () =>
|
|
184
|
+
fetchJson("/api/scripts/run", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
name: config.name ?? config.scriptName,
|
|
188
|
+
source: config.source,
|
|
189
|
+
args: config.args,
|
|
190
|
+
scope: config.scope,
|
|
191
|
+
fsMode: config.fsMode ?? "none",
|
|
192
|
+
intent: config.intent ?? `script-run:${input.runId}:${label}`,
|
|
193
|
+
idempotencyKey: config.idempotencyKey,
|
|
194
|
+
}),
|
|
195
|
+
}),
|
|
196
|
+
),
|
|
197
|
+
humanInTheLoop: async () => {
|
|
198
|
+
throw new Error("ctx.step.humanInTheLoop is stubbed in Script Workflows v1");
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
swarm,
|
|
202
|
+
stdlib,
|
|
203
|
+
logger: console,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -39,6 +39,9 @@ export const SDK_TOOL_NAME_MAP = {
|
|
|
39
39
|
script_upsert: "script-upsert",
|
|
40
40
|
script_delete: "script-delete", // destructive
|
|
41
41
|
script_queryTypes: "script-query-types",
|
|
42
|
+
script_launchRun: "launch-script-run",
|
|
43
|
+
script_getRun: "get-script-run",
|
|
44
|
+
script_listRuns: "list-script-runs",
|
|
42
45
|
|
|
43
46
|
// ── swarm / agent ──
|
|
44
47
|
swarm_get: "get-swarm",
|
|
@@ -262,6 +262,20 @@ declare module "swarm-sdk" {
|
|
|
262
262
|
}): Promise<unknown>;
|
|
263
263
|
script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
|
|
264
264
|
script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
|
|
265
|
+
script_launchRun(args: {
|
|
266
|
+
source: string;
|
|
267
|
+
args?: unknown;
|
|
268
|
+
idempotencyKey?: string;
|
|
269
|
+
scriptName?: string;
|
|
270
|
+
requestedByUserId?: string;
|
|
271
|
+
}): Promise<unknown>;
|
|
272
|
+
script_getRun(args: { id: string }): Promise<unknown>;
|
|
273
|
+
script_listRuns(args?: {
|
|
274
|
+
status?: "running" | "paused" | "completed" | "failed" | "cancelled" | "aborted_limit";
|
|
275
|
+
agentId?: string;
|
|
276
|
+
limit?: number;
|
|
277
|
+
offset?: number;
|
|
278
|
+
}): Promise<unknown>;
|
|
265
279
|
|
|
266
280
|
// --- write: repos ---
|
|
267
281
|
repo_update(args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -320,7 +334,53 @@ declare module "swarm-sdk" {
|
|
|
320
334
|
|
|
321
335
|
export interface ScriptLogger extends Console {}
|
|
322
336
|
|
|
337
|
+
export interface ScriptRunContext {
|
|
338
|
+
id: string;
|
|
339
|
+
agentId: string;
|
|
340
|
+
args: unknown;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface ScriptWorkflowSteps {
|
|
344
|
+
rawLlm(
|
|
345
|
+
label: string,
|
|
346
|
+
config: { prompt: string; model?: string; schema?: Record<string, unknown> },
|
|
347
|
+
): Promise<unknown>;
|
|
348
|
+
agentTask(
|
|
349
|
+
label: string,
|
|
350
|
+
config: {
|
|
351
|
+
template?: string;
|
|
352
|
+
task?: string;
|
|
353
|
+
agentId?: string;
|
|
354
|
+
tags?: string[];
|
|
355
|
+
priority?: number;
|
|
356
|
+
offerMode?: boolean;
|
|
357
|
+
dir?: string;
|
|
358
|
+
vcsRepo?: string;
|
|
359
|
+
model?: string;
|
|
360
|
+
parentTaskId?: string;
|
|
361
|
+
requestedByUserId?: string;
|
|
362
|
+
outputSchema?: Record<string, unknown>;
|
|
363
|
+
},
|
|
364
|
+
): Promise<unknown>;
|
|
365
|
+
swarmScript(
|
|
366
|
+
label: string,
|
|
367
|
+
config: {
|
|
368
|
+
name?: string;
|
|
369
|
+
scriptName?: string;
|
|
370
|
+
source?: string;
|
|
371
|
+
args?: unknown;
|
|
372
|
+
scope?: ScriptScope;
|
|
373
|
+
fsMode?: ScriptFsMode;
|
|
374
|
+
intent?: string;
|
|
375
|
+
idempotencyKey?: string;
|
|
376
|
+
},
|
|
377
|
+
): Promise<unknown>;
|
|
378
|
+
humanInTheLoop(): Promise<never>;
|
|
379
|
+
}
|
|
380
|
+
|
|
323
381
|
export interface ScriptContext {
|
|
382
|
+
run?: ScriptRunContext;
|
|
383
|
+
step?: ScriptWorkflowSteps;
|
|
324
384
|
swarm: SwarmSdk & { config: SwarmConfig };
|
|
325
385
|
stdlib: ScriptStdlib;
|
|
326
386
|
logger: ScriptLogger;
|
|
@@ -244,6 +244,20 @@ declare module "swarm-sdk" {
|
|
|
244
244
|
}): Promise<unknown>;
|
|
245
245
|
script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
|
|
246
246
|
script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
|
|
247
|
+
script_launchRun(args: {
|
|
248
|
+
source: string;
|
|
249
|
+
args?: unknown;
|
|
250
|
+
idempotencyKey?: string;
|
|
251
|
+
scriptName?: string;
|
|
252
|
+
requestedByUserId?: string;
|
|
253
|
+
}): Promise<unknown>;
|
|
254
|
+
script_getRun(args: { id: string }): Promise<unknown>;
|
|
255
|
+
script_listRuns(args?: {
|
|
256
|
+
status?: "running" | "paused" | "completed" | "failed" | "cancelled" | "aborted_limit";
|
|
257
|
+
agentId?: string;
|
|
258
|
+
limit?: number;
|
|
259
|
+
offset?: number;
|
|
260
|
+
}): Promise<unknown>;
|
|
247
261
|
|
|
248
262
|
// --- write: repos ---
|
|
249
263
|
repo_update(args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -302,7 +316,53 @@ declare module "swarm-sdk" {
|
|
|
302
316
|
|
|
303
317
|
export interface ScriptLogger extends Console {}
|
|
304
318
|
|
|
319
|
+
export interface ScriptRunContext {
|
|
320
|
+
id: string;
|
|
321
|
+
agentId: string;
|
|
322
|
+
args: unknown;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface ScriptWorkflowSteps {
|
|
326
|
+
rawLlm(
|
|
327
|
+
label: string,
|
|
328
|
+
config: { prompt: string; model?: string; schema?: Record<string, unknown> },
|
|
329
|
+
): Promise<unknown>;
|
|
330
|
+
agentTask(
|
|
331
|
+
label: string,
|
|
332
|
+
config: {
|
|
333
|
+
template?: string;
|
|
334
|
+
task?: string;
|
|
335
|
+
agentId?: string;
|
|
336
|
+
tags?: string[];
|
|
337
|
+
priority?: number;
|
|
338
|
+
offerMode?: boolean;
|
|
339
|
+
dir?: string;
|
|
340
|
+
vcsRepo?: string;
|
|
341
|
+
model?: string;
|
|
342
|
+
parentTaskId?: string;
|
|
343
|
+
requestedByUserId?: string;
|
|
344
|
+
outputSchema?: Record<string, unknown>;
|
|
345
|
+
},
|
|
346
|
+
): Promise<unknown>;
|
|
347
|
+
swarmScript(
|
|
348
|
+
label: string,
|
|
349
|
+
config: {
|
|
350
|
+
name?: string;
|
|
351
|
+
scriptName?: string;
|
|
352
|
+
source?: string;
|
|
353
|
+
args?: unknown;
|
|
354
|
+
scope?: ScriptScope;
|
|
355
|
+
fsMode?: ScriptFsMode;
|
|
356
|
+
intent?: string;
|
|
357
|
+
idempotencyKey?: string;
|
|
358
|
+
},
|
|
359
|
+
): Promise<unknown>;
|
|
360
|
+
humanInTheLoop(): Promise<never>;
|
|
361
|
+
}
|
|
362
|
+
|
|
305
363
|
export interface ScriptContext {
|
|
364
|
+
run?: ScriptRunContext;
|
|
365
|
+
step?: ScriptWorkflowSteps;
|
|
306
366
|
swarm: SwarmSdk & { config: SwarmConfig };
|
|
307
367
|
stdlib: ScriptStdlib;
|
|
308
368
|
logger: ScriptLogger;
|
package/src/server.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
import { registerScriptDeleteTool } from "./tools/script-delete";
|
|
79
79
|
import { registerScriptQueryTypesTool } from "./tools/script-query-types";
|
|
80
80
|
import { registerScriptRunTool } from "./tools/script-run";
|
|
81
|
+
import { registerScriptRunsTools } from "./tools/script-runs";
|
|
81
82
|
import { registerScriptSearchTool } from "./tools/script-search";
|
|
82
83
|
import { registerScriptUpsertTool } from "./tools/script-upsert";
|
|
83
84
|
import { registerSendTaskTool } from "./tools/send-task";
|
|
@@ -227,6 +228,7 @@ export function createServer() {
|
|
|
227
228
|
registerScriptUpsertTool(server);
|
|
228
229
|
registerScriptDeleteTool(server);
|
|
229
230
|
registerScriptQueryTypesTool(server);
|
|
231
|
+
registerScriptRunsTools(server);
|
|
230
232
|
|
|
231
233
|
// External command routes - mirrors the `agent-swarm x ...` CLI surface.
|
|
232
234
|
registerSwarmXTool(server);
|
package/src/slack/handlers.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { SlackFile } from "./files";
|
|
|
18
18
|
import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
|
|
19
19
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
20
20
|
import "./templates";
|
|
21
|
+
import { extractSlackMessageText } from "./message-text";
|
|
21
22
|
import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "./thread-buffer";
|
|
22
23
|
import { registerTreeMessage } from "./watcher";
|
|
23
24
|
|
|
@@ -173,6 +174,8 @@ interface ThreadMessage {
|
|
|
173
174
|
subtype?: string;
|
|
174
175
|
text?: string;
|
|
175
176
|
ts: string;
|
|
177
|
+
attachments?: Array<{ fallback?: string; text?: string; title?: string; pretext?: string }>;
|
|
178
|
+
blocks?: unknown[];
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
// Cache for bot's own user ID (avoids redundant auth.test calls)
|
|
@@ -286,8 +289,11 @@ async function getThreadContext(
|
|
|
286
289
|
});
|
|
287
290
|
|
|
288
291
|
const messages = (result.messages || []) as ThreadMessage[];
|
|
289
|
-
// Filter out the current message
|
|
290
|
-
const previousMessages = messages.filter((m) =>
|
|
292
|
+
// Filter out the current message; include any message with extractable text
|
|
293
|
+
const previousMessages = messages.filter((m) => {
|
|
294
|
+
if (m.ts === currentTs) return false;
|
|
295
|
+
return extractSlackMessageText(m) !== "";
|
|
296
|
+
});
|
|
291
297
|
|
|
292
298
|
if (previousMessages.length === 0) return "";
|
|
293
299
|
|
|
@@ -298,13 +304,14 @@ async function getThreadContext(
|
|
|
298
304
|
const isBotMessage =
|
|
299
305
|
m.user === botUserId || m.bot_id !== undefined || m.subtype === "bot_message";
|
|
300
306
|
|
|
307
|
+
const text = extractSlackMessageText(m);
|
|
301
308
|
if (isBotMessage) {
|
|
302
309
|
// Bot/agent message - truncate if too long
|
|
303
|
-
const truncatedText =
|
|
310
|
+
const truncatedText = text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
304
311
|
formattedMessages.push(`[Agent]: ${truncatedText}`);
|
|
305
312
|
} else {
|
|
306
313
|
const userName = m.user ? await getUserDisplayName(client, m.user) : "Unknown";
|
|
307
|
-
formattedMessages.push(`${userName}: ${
|
|
314
|
+
formattedMessages.push(`${userName}: ${text}`);
|
|
308
315
|
}
|
|
309
316
|
}
|
|
310
317
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility for extracting displayable text from a Slack message.
|
|
3
|
+
*
|
|
4
|
+
* Slack integration/alert apps (Datadog, PagerDuty, GitHub) often post with
|
|
5
|
+
* an empty top-level `text` field and put all content in legacy `attachments`
|
|
6
|
+
* or Block Kit `blocks`. This helper falls back through those layers so callers
|
|
7
|
+
* never silently drop those messages.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface SlackAttachment {
|
|
11
|
+
fallback?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
pretext?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Internal shape used only inside the helper; callers pass `blocks?: unknown[]`
|
|
18
|
+
interface SlackBlockInternal {
|
|
19
|
+
type?: string;
|
|
20
|
+
text?: { type?: string; text?: string };
|
|
21
|
+
/** section blocks may use fields[] instead of (or alongside) a top-level text object */
|
|
22
|
+
fields?: Array<{ type?: string; text?: string }>;
|
|
23
|
+
elements?: unknown[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SlackMessageLike {
|
|
27
|
+
text?: string;
|
|
28
|
+
attachments?: SlackAttachment[];
|
|
29
|
+
/** Typed as unknown[] so any Slack SDK block variant is accepted without casting. */
|
|
30
|
+
blocks?: unknown[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recursively collect plain text from a Slack rich_text node tree.
|
|
35
|
+
*
|
|
36
|
+
* Handles text leaf nodes plus all container types that carry child elements:
|
|
37
|
+
* rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted.
|
|
38
|
+
*/
|
|
39
|
+
function collectRichTextParts(node: unknown, parts: string[]): void {
|
|
40
|
+
if (node == null || typeof node !== "object") return;
|
|
41
|
+
const n = node as { type?: string; text?: string; elements?: unknown[] };
|
|
42
|
+
if (n.type === "text" && n.text) {
|
|
43
|
+
parts.push(n.text);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(n.elements)) {
|
|
46
|
+
for (const child of n.elements) {
|
|
47
|
+
collectRichTextParts(child, parts);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the best displayable text for a Slack message.
|
|
54
|
+
*
|
|
55
|
+
* Priority:
|
|
56
|
+
* 1. `msg.text` (non-empty)
|
|
57
|
+
* 2. `msg.attachments[]` — joins `fallback || text || title || pretext` for each
|
|
58
|
+
* 3. `msg.blocks[]` — extracts text from section (text + fields) and rich_text blocks
|
|
59
|
+
* 4. `""` if nothing found
|
|
60
|
+
*/
|
|
61
|
+
export function extractSlackMessageText(msg: SlackMessageLike): string {
|
|
62
|
+
if (msg.text?.trim()) return msg.text;
|
|
63
|
+
|
|
64
|
+
// Legacy attachments (Datadog, PagerDuty, GitHub alert apps)
|
|
65
|
+
if (Array.isArray(msg.attachments) && msg.attachments.length > 0) {
|
|
66
|
+
const parts = msg.attachments
|
|
67
|
+
.filter((a): a is SlackAttachment => a != null && typeof a === "object")
|
|
68
|
+
.map((a) => a.fallback || a.text || a.title || a.pretext || "")
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
if (parts.length > 0) return parts.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Block Kit blocks
|
|
74
|
+
if (Array.isArray(msg.blocks) && msg.blocks.length > 0) {
|
|
75
|
+
const parts: string[] = [];
|
|
76
|
+
for (const rawBlock of msg.blocks) {
|
|
77
|
+
if (rawBlock == null || typeof rawBlock !== "object") continue;
|
|
78
|
+
const block = rawBlock as SlackBlockInternal;
|
|
79
|
+
if (block.type === "section") {
|
|
80
|
+
if (block.text?.text) parts.push(block.text.text);
|
|
81
|
+
if (Array.isArray(block.fields)) {
|
|
82
|
+
for (const field of block.fields) {
|
|
83
|
+
if (field != null && typeof field === "object" && field.text) {
|
|
84
|
+
parts.push(field.text);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (block.type === "rich_text" && Array.isArray(block.elements)) {
|
|
89
|
+
for (const el of block.elements) {
|
|
90
|
+
collectRichTextParts(el, parts);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (parts.length > 0) return parts.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
@@ -4,6 +4,7 @@ import { slackContextKey } from "../tasks/context-key";
|
|
|
4
4
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
5
5
|
import { getSlackApp } from "./app";
|
|
6
6
|
import { buildBufferFlushBlocks } from "./blocks";
|
|
7
|
+
import { extractSlackMessageText } from "./message-text";
|
|
7
8
|
import { registerTreeMessage } from "./watcher";
|
|
8
9
|
|
|
9
10
|
interface BufferedMessage {
|
|
@@ -93,15 +94,16 @@ async function getThreadContextForBuffer(channelId: string, threadTs: string): P
|
|
|
93
94
|
if (messages.length === 0) return "";
|
|
94
95
|
|
|
95
96
|
const formatted = messages
|
|
96
|
-
.filter((m) => m
|
|
97
|
+
.filter((m) => extractSlackMessageText(m) !== "")
|
|
97
98
|
.map((m) => {
|
|
98
99
|
const msg = m as Record<string, unknown>;
|
|
99
100
|
const isBotMessage = msg.bot_id !== undefined || msg.subtype === "bot_message";
|
|
101
|
+
const text = extractSlackMessageText(m);
|
|
100
102
|
if (isBotMessage) {
|
|
101
|
-
const truncated =
|
|
103
|
+
const truncated = text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
102
104
|
return `[Agent]: ${truncated}`;
|
|
103
105
|
}
|
|
104
|
-
return `<@${m.user}>: ${
|
|
106
|
+
return `<@${m.user}>: ${text}`;
|
|
105
107
|
})
|
|
106
108
|
.join("\n");
|
|
107
109
|
|