@desplega.ai/agent-swarm 1.74.4 → 1.76.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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +1264 -46
  3. package/package.json +2 -2
  4. package/src/be/db.ts +563 -9
  5. package/src/be/memory/edges-store.ts +69 -0
  6. package/src/be/memory/providers/sqlite-store.ts +4 -0
  7. package/src/be/memory/raters/explicit-self.ts +22 -0
  8. package/src/be/memory/raters/implicit-citation.ts +44 -0
  9. package/src/be/memory/raters/llm-client.ts +172 -0
  10. package/src/be/memory/raters/llm-summarizer.ts +218 -0
  11. package/src/be/memory/raters/llm.ts +375 -0
  12. package/src/be/memory/raters/noop.ts +14 -0
  13. package/src/be/memory/raters/registry.ts +86 -0
  14. package/src/be/memory/raters/retrieval.ts +88 -0
  15. package/src/be/memory/raters/run-server-raters.ts +97 -0
  16. package/src/be/memory/raters/store.ts +228 -0
  17. package/src/be/memory/raters/types.ts +101 -0
  18. package/src/be/memory/reranker.ts +32 -2
  19. package/src/be/memory/retrieval-store.ts +116 -0
  20. package/src/be/memory/types.ts +3 -0
  21. package/src/be/migrations/051_memory_posteriors_and_retrieval.sql +67 -0
  22. package/src/be/migrations/052_memory_edges.sql +36 -0
  23. package/src/be/migrations/053_agent_waiting_for_credentials_status.sql +61 -0
  24. package/src/be/migrations/054_agent_harness_provider.sql +21 -0
  25. package/src/be/migrations/055_agent_cred_status.sql +15 -0
  26. package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
  27. package/src/be/migrations/057_inbox_item_state.sql +27 -0
  28. package/src/be/migrations/058_task_templates.sql +31 -0
  29. package/src/be/swarm-config-guard.ts +24 -0
  30. package/src/commands/credential-wait.ts +186 -0
  31. package/src/commands/provider-credentials.ts +434 -0
  32. package/src/commands/runner.ts +253 -21
  33. package/src/hooks/hook.ts +143 -66
  34. package/src/http/agents.ts +191 -1
  35. package/src/http/config.ts +11 -1
  36. package/src/http/core.ts +5 -0
  37. package/src/http/inbox-state.ts +89 -0
  38. package/src/http/index.ts +10 -0
  39. package/src/http/memory.ts +230 -1
  40. package/src/http/sessions.ts +86 -0
  41. package/src/http/status.ts +665 -0
  42. package/src/http/task-templates.ts +51 -0
  43. package/src/http/tasks.ts +85 -5
  44. package/src/http/users.ts +134 -0
  45. package/src/prompts/memories.ts +62 -0
  46. package/src/providers/claude-adapter.ts +22 -0
  47. package/src/providers/claude-managed-adapter.ts +24 -0
  48. package/src/providers/codex-adapter.ts +43 -1
  49. package/src/providers/devin-adapter.ts +18 -0
  50. package/src/providers/index.ts +7 -0
  51. package/src/providers/opencode-adapter.ts +60 -0
  52. package/src/providers/pi-mono-adapter.ts +71 -0
  53. package/src/providers/types.ts +34 -0
  54. package/src/server.ts +2 -0
  55. package/src/slack/handlers.ts +0 -1
  56. package/src/tests/agents-harness-provider.test.ts +333 -0
  57. package/src/tests/credential-check.test.ts +367 -0
  58. package/src/tests/credential-status-api.test.ts +223 -0
  59. package/src/tests/credential-status-routing.test.ts +150 -0
  60. package/src/tests/credential-wait.test.ts +282 -0
  61. package/src/tests/harness-provider-resolution.test.ts +242 -0
  62. package/src/tests/jira-sync.test.ts +1 -1
  63. package/src/tests/memory-edges.test.ts +722 -0
  64. package/src/tests/memory-rate-endpoint.test.ts +330 -0
  65. package/src/tests/memory-rate-tool.test.ts +252 -0
  66. package/src/tests/memory-rater-e2e.test.ts +578 -0
  67. package/src/tests/memory-rater-implicit-citation.test.ts +304 -0
  68. package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
  69. package/src/tests/memory-rater-llm.test.ts +964 -0
  70. package/src/tests/memory-rater-store.test.ts +249 -0
  71. package/src/tests/memory-reranker.test.ts +161 -2
  72. package/src/tests/migration-runner-regressions.test.ts +17 -2
  73. package/src/tests/mocks/mock-llm-rater-client.ts +35 -0
  74. package/src/tests/run-server-raters.test.ts +291 -0
  75. package/src/tests/sessions.test.ts +141 -0
  76. package/src/tests/status.test.ts +843 -0
  77. package/src/tests/stop-hook-task-resolution.test.ts +98 -0
  78. package/src/tests/template-recommendations.test.ts +148 -0
  79. package/src/tests/tool-annotations.test.ts +2 -2
  80. package/src/tests/use-dismissible-card.test.ts +140 -0
  81. package/src/tools/memory-rate.ts +166 -0
  82. package/src/tools/memory-search.ts +18 -0
  83. package/src/tools/store-progress.ts +37 -0
  84. package/src/tools/swarm-config/set-config.ts +17 -1
  85. package/src/tools/tool-config.ts +1 -0
  86. package/src/types.ts +122 -1
  87. package/src/utils/harness-provider.ts +32 -0
  88. package/tsconfig.json +0 -2
@@ -3,10 +3,19 @@ import { z } from "zod";
3
3
  import { chunkContent } from "../be/chunking";
4
4
  import { getEmbeddingProvider, getMemoryStore } from "../be/memory";
5
5
  import { CANDIDATE_SET_MULTIPLIER } from "../be/memory/constants";
6
+ import { listEdgesForAgent } from "../be/memory/edges-store";
7
+ import { recordRetrievals } from "../be/memory/raters/retrieval";
8
+ import { applyRating, ExplicitSelfDuplicateError } from "../be/memory/raters/store";
9
+ import {
10
+ type RatingEvent,
11
+ REFERENCES_SOURCE_MAX_LENGTH,
12
+ sanitizeReferencesSource,
13
+ } from "../be/memory/raters/types";
6
14
  import { rerank } from "../be/memory/reranker";
15
+ import { getRetrievalsForAgent, hasRetrievalForTask } from "../be/memory/retrieval-store";
7
16
  import { AgentMemoryScopeSchema, AgentMemorySourceSchema } from "../types";
8
17
  import { route } from "./route-def";
9
- import { json, jsonError } from "./utils";
18
+ import { json, jsonError, parseQueryParams } from "./utils";
10
19
 
11
20
  // ─── Route Definitions ───────────────────────────────────────────────────────
12
21
 
@@ -115,6 +124,105 @@ const deleteMemoryById = route({
115
124
  },
116
125
  });
117
126
 
127
+ // Memory rater v1.5 — worker-facing rating endpoints. Plan:
128
+ // thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-3.md
129
+ //
130
+ // `source` is restricted to `llm` and `explicit-self` at the HTTP boundary —
131
+ // `implicit-citation` runs in-process server-side via applyRating directly
132
+ // and must never arrive over HTTP (defence against worker spoofing).
133
+ // `referencesSource` (step-6 §4) — Q2 free-form contract: ≤512 chars,
134
+ // control-char strip, NUL byte rejection. Convention `<source>:<identifier>`
135
+ // (e.g. github:owner/repo#N, linear:KEY-N, customer:<slug>) is documented
136
+ // only in the OpenAPI description — server does NOT validate prefixes and
137
+ // does NOT enforce a closed enum. The transform throws via `z.NEVER` when
138
+ // sanitization rejects the input so the request fails with a clear 400.
139
+ const ReferencesSourceSchema = z
140
+ .string()
141
+ .min(1)
142
+ .max(REFERENCES_SOURCE_MAX_LENGTH)
143
+ .transform((value, ctx) => {
144
+ const cleaned = sanitizeReferencesSource(value);
145
+ if (cleaned === null) {
146
+ ctx.addIssue({
147
+ code: z.ZodIssueCode.custom,
148
+ message: "referencesSource must not contain NUL bytes or strip to empty",
149
+ });
150
+ return z.NEVER;
151
+ }
152
+ return cleaned;
153
+ })
154
+ .describe(
155
+ 'Optional external source ID this memory references. Free-form string, convention "<source>:<identifier>" (e.g. "github:owner/repo#N", "linear:KEY-N", "customer:<slug>", "slack:<channel>:<ts>", "agentmail:<thread-id>"). Pick any prefix that fits — no closed enum. When present, an edge from this memory to the external source is created/updated.',
156
+ );
157
+
158
+ const RateEventSchema = z.object({
159
+ memoryId: z.string().min(1),
160
+ signal: z.number().min(-1).max(1),
161
+ weight: z.number().min(0).max(1),
162
+ source: z.enum(["llm", "explicit-self"]),
163
+ reasoning: z.string().max(500).optional(),
164
+ taskId: z.string().uuid().optional(),
165
+ referencesSource: ReferencesSourceSchema.optional(),
166
+ });
167
+
168
+ const rateMemory = route({
169
+ method: "post",
170
+ path: "/api/memory/rate",
171
+ pattern: ["api", "memory", "rate"],
172
+ summary: "Submit RatingEvents to update memory usefulness posteriors",
173
+ tags: ["Memory"],
174
+ auth: { apiKey: true, agentId: true },
175
+ body: z.object({
176
+ events: z.array(RateEventSchema).min(1).max(50),
177
+ }),
178
+ responses: {
179
+ 200: { description: "Ratings applied; per-event rejections returned in body" },
180
+ 400: { description: "Validation error or explicit-self R6 spam-guard rejection" },
181
+ 409: { description: "Duplicate explicit-self rating for (taskId, memoryId)" },
182
+ },
183
+ });
184
+
185
+ const getRetrievals = route({
186
+ method: "get",
187
+ path: "/api/memory/retrievals",
188
+ pattern: ["api", "memory", "retrievals"],
189
+ summary: "List memories retrieved for a task or session (rater input)",
190
+ tags: ["Memory"],
191
+ auth: { apiKey: true, agentId: true },
192
+ query: z
193
+ .object({
194
+ taskId: z.string().uuid().optional(),
195
+ sessionId: z.string().optional(),
196
+ })
197
+ .refine((q) => q.taskId || q.sessionId, {
198
+ message: "taskId or sessionId required",
199
+ }),
200
+ responses: {
201
+ 200: { description: "Retrieval rows joined with agent_memory" },
202
+ 400: { description: "Missing taskId/sessionId or X-Agent-ID" },
203
+ },
204
+ });
205
+
206
+ // Memory rater v1.5 step-6 — the edges-list endpoint that powers the
207
+ // homepage demo ("this memory references PR #377"). Auth by X-Agent-ID +
208
+ // Bearer with defence-in-depth: the joined `agent_memory` row must either
209
+ // be swarm-scope or owned by the requesting agent. Plan §7.
210
+ const getMemoryEdges = route({
211
+ method: "get",
212
+ path: "/api/memory/edges",
213
+ pattern: ["api", "memory", "edges"],
214
+ summary: "List references-source edges for a memory",
215
+ tags: ["Memory"],
216
+ auth: { apiKey: true, agentId: true },
217
+ query: z.object({
218
+ memoryId: z.string().min(1),
219
+ }),
220
+ responses: {
221
+ 200: { description: "Edges with computed usefulness scores" },
222
+ 400: { description: "Missing memoryId or X-Agent-ID" },
223
+ },
224
+ });
225
+
118
226
  // ─── Handler ─────────────────────────────────────────────────────────────────
119
227
 
120
228
  export async function handleMemory(
@@ -211,6 +319,27 @@ export async function handleMemory(
211
319
  });
212
320
  const ranked = rerank(candidates, { limit: Math.min(limit, 20) });
213
321
 
322
+ // Retrieval bridge — when caller passed `X-Source-Task-ID`, record one
323
+ // `memory_retrieval` row per returned memory so server-side raters
324
+ // (ImplicitCitationRater, fired from store-progress on task completion)
325
+ // know which memories were surfaced. Best-effort: a logging failure must
326
+ // never poison search.
327
+ const sourceTaskIdHeader = req.headers["x-source-task-id"];
328
+ const sourceTaskId = Array.isArray(sourceTaskIdHeader)
329
+ ? sourceTaskIdHeader[0]
330
+ : sourceTaskIdHeader;
331
+ if (sourceTaskId) {
332
+ try {
333
+ recordRetrievals(
334
+ sourceTaskId,
335
+ myAgentId,
336
+ ranked.map((r) => ({ memoryId: r.id, similarity: r.similarity })),
337
+ );
338
+ } catch (err) {
339
+ console.error("[memory-search] recordRetrievals failed:", (err as Error).message);
340
+ }
341
+ }
342
+
214
343
  json(res, {
215
344
  results: ranked.map((r) => ({
216
345
  id: r.id,
@@ -389,5 +518,105 @@ export async function handleMemory(
389
518
  return true;
390
519
  }
391
520
 
521
+ if (rateMemory.match(req.method, pathSegments)) {
522
+ if (!myAgentId) {
523
+ jsonError(res, "Missing X-Agent-ID header", 400);
524
+ return true;
525
+ }
526
+
527
+ const parsed = await rateMemory.parse(req, res, pathSegments, new URLSearchParams());
528
+ if (!parsed) return true;
529
+
530
+ const { events } = parsed.body;
531
+
532
+ // R6 spam guard: explicit-self requires a matching memory_retrieval row.
533
+ // Reject the whole batch on first offender so the worker sees a clear 400.
534
+ for (const evt of events) {
535
+ if (evt.source !== "explicit-self") continue;
536
+ if (!evt.taskId) {
537
+ jsonError(res, `explicit-self rating for memoryId=${evt.memoryId} requires taskId`, 400);
538
+ return true;
539
+ }
540
+ if (!hasRetrievalForTask(evt.taskId, evt.memoryId)) {
541
+ jsonError(
542
+ res,
543
+ `explicit-self rating rejected: memoryId=${evt.memoryId} not present in memory_retrieval for task=${evt.taskId}`,
544
+ 400,
545
+ );
546
+ return true;
547
+ }
548
+ }
549
+
550
+ // applyRating's ctx carries a single taskId for the batch. Group events by
551
+ // taskId so each call gets a single coherent ctx (and one transaction).
552
+ const groups = new Map<string | undefined, typeof events>();
553
+ for (const evt of events) {
554
+ const list = groups.get(evt.taskId) ?? [];
555
+ list.push(evt);
556
+ groups.set(evt.taskId, list);
557
+ }
558
+
559
+ let applied = 0;
560
+ const rejected: { memoryId: string; reason: string }[] = [];
561
+ try {
562
+ for (const [taskId, batch] of groups) {
563
+ const ratingEvents: RatingEvent[] = batch.map((e) => ({
564
+ memoryId: e.memoryId,
565
+ signal: e.signal,
566
+ weight: e.weight,
567
+ source: e.source,
568
+ reasoning: e.reasoning,
569
+ ...(e.referencesSource !== undefined ? { referencesSource: e.referencesSource } : {}),
570
+ }));
571
+ const result = applyRating(ratingEvents, { taskId });
572
+ applied += result.applied;
573
+ for (const r of result.rejected) {
574
+ rejected.push({ memoryId: r.event.memoryId, reason: r.reason });
575
+ }
576
+ }
577
+ } catch (err) {
578
+ if (err instanceof ExplicitSelfDuplicateError) {
579
+ jsonError(res, `Duplicate explicit-self rating for memoryId=${err.event.memoryId}`, 409);
580
+ return true;
581
+ }
582
+ throw err;
583
+ }
584
+
585
+ json(res, { applied, rejected });
586
+ return true;
587
+ }
588
+
589
+ if (getRetrievals.match(req.method, pathSegments)) {
590
+ if (!myAgentId) {
591
+ jsonError(res, "Missing X-Agent-ID header", 400);
592
+ return true;
593
+ }
594
+
595
+ const queryParams = parseQueryParams(req.url || "");
596
+ const parsed = await getRetrievals.parse(req, res, pathSegments, queryParams);
597
+ if (!parsed) return true;
598
+
599
+ const { taskId, sessionId } = parsed.query;
600
+ const rows = getRetrievalsForAgent(myAgentId, { taskId, sessionId });
601
+ json(res, { results: rows });
602
+ return true;
603
+ }
604
+
605
+ if (getMemoryEdges.match(req.method, pathSegments)) {
606
+ if (!myAgentId) {
607
+ jsonError(res, "Missing X-Agent-ID header", 400);
608
+ return true;
609
+ }
610
+
611
+ const queryParams = parseQueryParams(req.url || "");
612
+ const parsed = await getMemoryEdges.parse(req, res, pathSegments, queryParams);
613
+ if (!parsed) return true;
614
+
615
+ const { memoryId } = parsed.query;
616
+ const edges = listEdgesForAgent(myAgentId, memoryId);
617
+ json(res, { edges });
618
+ return true;
619
+ }
620
+
392
621
  return false;
393
622
  }
@@ -0,0 +1,86 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import { getRootTaskChain, getTaskById, listRecentSessions } from "../be/db";
4
+ import { route } from "./route-def";
5
+ import { json, jsonError } from "./utils";
6
+
7
+ // ─── Route Definitions ───────────────────────────────────────────────────────
8
+
9
+ const listSessions = route({
10
+ method: "get",
11
+ path: "/api/sessions",
12
+ pattern: ["api", "sessions"],
13
+ summary: "List recent task sessions (root tasks + chain summary)",
14
+ tags: ["Sessions"],
15
+ query: z.object({
16
+ limit: z.coerce.number().int().optional(),
17
+ offset: z.coerce.number().int().optional(),
18
+ /** Comma-separated source filter (e.g. `ui,slack`). Omit to include all. */
19
+ source: z.string().optional(),
20
+ /** Case-insensitive substring match against the root task's text. */
21
+ q: z.string().optional(),
22
+ }),
23
+ responses: {
24
+ 200: { description: "Recent sessions ordered by chain-wide last activity" },
25
+ 401: { description: "Unauthorized" },
26
+ },
27
+ auth: { apiKey: true },
28
+ });
29
+
30
+ const getSession = route({
31
+ method: "get",
32
+ path: "/api/sessions/{rootTaskId}",
33
+ pattern: ["api", "sessions", null],
34
+ summary: "Get a session — root task + the entire descendant chain",
35
+ tags: ["Sessions"],
36
+ params: z.object({ rootTaskId: z.string() }),
37
+ responses: {
38
+ 200: { description: "Root task + chain (ordered by createdAt)" },
39
+ 401: { description: "Unauthorized" },
40
+ 404: { description: "Root task not found" },
41
+ },
42
+ auth: { apiKey: true },
43
+ });
44
+
45
+ // ─── Handler ─────────────────────────────────────────────────────────────────
46
+
47
+ export async function handleSessions(
48
+ req: IncomingMessage,
49
+ res: ServerResponse,
50
+ pathSegments: string[],
51
+ queryParams: URLSearchParams,
52
+ ): Promise<boolean> {
53
+ if (listSessions.match(req.method, pathSegments)) {
54
+ const parsed = await listSessions.parse(req, res, pathSegments, queryParams);
55
+ if (!parsed) return true;
56
+ const sources = parsed.query.source
57
+ ? parsed.query.source
58
+ .split(",")
59
+ .map((s) => s.trim())
60
+ .filter(Boolean)
61
+ : undefined;
62
+ const sessions = listRecentSessions({
63
+ limit: parsed.query.limit,
64
+ offset: parsed.query.offset,
65
+ source: sources,
66
+ q: parsed.query.q,
67
+ });
68
+ json(res, { sessions });
69
+ return true;
70
+ }
71
+
72
+ if (getSession.match(req.method, pathSegments)) {
73
+ const parsed = await getSession.parse(req, res, pathSegments, queryParams);
74
+ if (!parsed) return true;
75
+ const root = getTaskById(parsed.params.rootTaskId);
76
+ if (!root) {
77
+ jsonError(res, "Root task not found", 404);
78
+ return true;
79
+ }
80
+ const chain = getRootTaskChain(parsed.params.rootTaskId);
81
+ json(res, { root, chain });
82
+ return true;
83
+ }
84
+
85
+ return false;
86
+ }