@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.
- package/README.md +1 -1
- package/openapi.json +1264 -46
- package/package.json +2 -2
- package/src/be/db.ts +563 -9
- package/src/be/memory/edges-store.ts +69 -0
- package/src/be/memory/providers/sqlite-store.ts +4 -0
- package/src/be/memory/raters/explicit-self.ts +22 -0
- package/src/be/memory/raters/implicit-citation.ts +44 -0
- package/src/be/memory/raters/llm-client.ts +172 -0
- package/src/be/memory/raters/llm-summarizer.ts +218 -0
- package/src/be/memory/raters/llm.ts +375 -0
- package/src/be/memory/raters/noop.ts +14 -0
- package/src/be/memory/raters/registry.ts +86 -0
- package/src/be/memory/raters/retrieval.ts +88 -0
- package/src/be/memory/raters/run-server-raters.ts +97 -0
- package/src/be/memory/raters/store.ts +228 -0
- package/src/be/memory/raters/types.ts +101 -0
- package/src/be/memory/reranker.ts +32 -2
- package/src/be/memory/retrieval-store.ts +116 -0
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/051_memory_posteriors_and_retrieval.sql +67 -0
- package/src/be/migrations/052_memory_edges.sql +36 -0
- package/src/be/migrations/053_agent_waiting_for_credentials_status.sql +61 -0
- package/src/be/migrations/054_agent_harness_provider.sql +21 -0
- package/src/be/migrations/055_agent_cred_status.sql +15 -0
- package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
- package/src/be/migrations/057_inbox_item_state.sql +27 -0
- package/src/be/migrations/058_task_templates.sql +31 -0
- package/src/be/swarm-config-guard.ts +24 -0
- package/src/commands/credential-wait.ts +186 -0
- package/src/commands/provider-credentials.ts +434 -0
- package/src/commands/runner.ts +253 -21
- package/src/hooks/hook.ts +143 -66
- package/src/http/agents.ts +191 -1
- package/src/http/config.ts +11 -1
- package/src/http/core.ts +5 -0
- package/src/http/inbox-state.ts +89 -0
- package/src/http/index.ts +10 -0
- package/src/http/memory.ts +230 -1
- package/src/http/sessions.ts +86 -0
- package/src/http/status.ts +665 -0
- package/src/http/task-templates.ts +51 -0
- package/src/http/tasks.ts +85 -5
- package/src/http/users.ts +134 -0
- package/src/prompts/memories.ts +62 -0
- package/src/providers/claude-adapter.ts +22 -0
- package/src/providers/claude-managed-adapter.ts +24 -0
- package/src/providers/codex-adapter.ts +43 -1
- package/src/providers/devin-adapter.ts +18 -0
- package/src/providers/index.ts +7 -0
- package/src/providers/opencode-adapter.ts +60 -0
- package/src/providers/pi-mono-adapter.ts +71 -0
- package/src/providers/types.ts +34 -0
- package/src/server.ts +2 -0
- package/src/slack/handlers.ts +0 -1
- package/src/tests/agents-harness-provider.test.ts +333 -0
- package/src/tests/credential-check.test.ts +367 -0
- package/src/tests/credential-status-api.test.ts +223 -0
- package/src/tests/credential-status-routing.test.ts +150 -0
- package/src/tests/credential-wait.test.ts +282 -0
- package/src/tests/harness-provider-resolution.test.ts +242 -0
- package/src/tests/jira-sync.test.ts +1 -1
- package/src/tests/memory-edges.test.ts +722 -0
- package/src/tests/memory-rate-endpoint.test.ts +330 -0
- package/src/tests/memory-rate-tool.test.ts +252 -0
- package/src/tests/memory-rater-e2e.test.ts +578 -0
- package/src/tests/memory-rater-implicit-citation.test.ts +304 -0
- package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
- package/src/tests/memory-rater-llm.test.ts +964 -0
- package/src/tests/memory-rater-store.test.ts +249 -0
- package/src/tests/memory-reranker.test.ts +161 -2
- package/src/tests/migration-runner-regressions.test.ts +17 -2
- package/src/tests/mocks/mock-llm-rater-client.ts +35 -0
- package/src/tests/run-server-raters.test.ts +291 -0
- package/src/tests/sessions.test.ts +141 -0
- package/src/tests/status.test.ts +843 -0
- package/src/tests/stop-hook-task-resolution.test.ts +98 -0
- package/src/tests/template-recommendations.test.ts +148 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/use-dismissible-card.test.ts +140 -0
- package/src/tools/memory-rate.ts +166 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/store-progress.ts +37 -0
- package/src/tools/swarm-config/set-config.ts +17 -1
- package/src/tools/tool-config.ts +1 -0
- package/src/types.ts +122 -1
- package/src/utils/harness-provider.ts +32 -0
- package/tsconfig.json +0 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { listTaskTemplates } from "../be/db";
|
|
4
|
+
import { TaskTemplateKindSchema } from "../types";
|
|
5
|
+
import { route } from "./route-def";
|
|
6
|
+
import { json } from "./utils";
|
|
7
|
+
|
|
8
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const listTemplates = route({
|
|
11
|
+
method: "get",
|
|
12
|
+
path: "/api/task-templates",
|
|
13
|
+
pattern: ["api", "task-templates"],
|
|
14
|
+
summary: "List task templates ('To start' bucket)",
|
|
15
|
+
tags: ["Task Templates"],
|
|
16
|
+
query: z.object({
|
|
17
|
+
category: z.string().optional(),
|
|
18
|
+
/** v2 hook — v1 callers always pass `kind=task` (or omit). */
|
|
19
|
+
kind: TaskTemplateKindSchema.optional(),
|
|
20
|
+
/** Case-insensitive LIKE match against `title` OR `description`. */
|
|
21
|
+
query: z.string().optional(),
|
|
22
|
+
}),
|
|
23
|
+
responses: {
|
|
24
|
+
200: { description: "Task template list" },
|
|
25
|
+
401: { description: "Unauthorized" },
|
|
26
|
+
},
|
|
27
|
+
auth: { apiKey: true },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export async function handleTaskTemplates(
|
|
33
|
+
req: IncomingMessage,
|
|
34
|
+
res: ServerResponse,
|
|
35
|
+
pathSegments: string[],
|
|
36
|
+
queryParams: URLSearchParams,
|
|
37
|
+
): Promise<boolean> {
|
|
38
|
+
if (listTemplates.match(req.method, pathSegments)) {
|
|
39
|
+
const parsed = await listTemplates.parse(req, res, pathSegments, queryParams);
|
|
40
|
+
if (!parsed) return true;
|
|
41
|
+
const templates = listTaskTemplates({
|
|
42
|
+
category: parsed.query.category,
|
|
43
|
+
kind: parsed.query.kind,
|
|
44
|
+
query: parsed.query.query,
|
|
45
|
+
});
|
|
46
|
+
json(res, { templates });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
package/src/http/tasks.ts
CHANGED
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
failTask,
|
|
8
8
|
getAllTasks,
|
|
9
9
|
getDb,
|
|
10
|
+
getLeadAgent,
|
|
10
11
|
getLogsByTaskId,
|
|
11
12
|
getPausedTasksForAgent,
|
|
12
13
|
getTaskById,
|
|
13
14
|
getTasksCount,
|
|
15
|
+
getUserById,
|
|
14
16
|
pauseTask,
|
|
15
17
|
resumeTask,
|
|
16
18
|
updateAgentStatusFromCapacity,
|
|
@@ -20,7 +22,13 @@ import {
|
|
|
20
22
|
} from "../be/db";
|
|
21
23
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
22
24
|
import { telemetry } from "../telemetry";
|
|
23
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type AgentTaskSource,
|
|
27
|
+
AgentTaskSourceSchema,
|
|
28
|
+
type AgentTaskStatus,
|
|
29
|
+
AgentTaskStatusSchema,
|
|
30
|
+
ProviderNameSchema,
|
|
31
|
+
} from "../types";
|
|
24
32
|
import { route } from "./route-def";
|
|
25
33
|
import { json, jsonError } from "./utils";
|
|
26
34
|
|
|
@@ -33,16 +41,22 @@ const listTasks = route({
|
|
|
33
41
|
summary: "List tasks with filters",
|
|
34
42
|
tags: ["Tasks"],
|
|
35
43
|
query: z.object({
|
|
44
|
+
/** Single status, or comma-separated list (e.g. "failed,cancelled"). */
|
|
36
45
|
status: z.string().optional(),
|
|
37
46
|
agentId: z.string().optional(),
|
|
38
47
|
scheduleId: z.string().optional(),
|
|
39
48
|
search: z.string().optional(),
|
|
40
49
|
includeHeartbeat: z.enum(["true", "false"]).optional(),
|
|
50
|
+
/** ISO 8601 — return only tasks created on/after this timestamp. */
|
|
51
|
+
createdAfter: z.string().datetime().optional(),
|
|
52
|
+
/** Comma-separated source filter (e.g. `ui,slack`). Omit to include all. */
|
|
53
|
+
source: z.string().optional(),
|
|
41
54
|
limit: z.coerce.number().int().optional(),
|
|
42
55
|
offset: z.coerce.number().int().optional(),
|
|
43
56
|
}),
|
|
44
57
|
responses: {
|
|
45
58
|
200: { description: "Paginated task list" },
|
|
59
|
+
400: { description: "Validation error (e.g. unknown status token)" },
|
|
46
60
|
},
|
|
47
61
|
});
|
|
48
62
|
|
|
@@ -62,9 +76,10 @@ const createTask = route({
|
|
|
62
76
|
offeredTo: z.string().optional(),
|
|
63
77
|
dir: z.string().optional(),
|
|
64
78
|
parentTaskId: z.string().optional(),
|
|
65
|
-
source:
|
|
79
|
+
source: AgentTaskSourceSchema.optional(),
|
|
66
80
|
outputSchema: z.record(z.string(), z.unknown()).optional(),
|
|
67
81
|
contextKey: z.string().optional(),
|
|
82
|
+
requestedByUserId: z.string().optional(),
|
|
68
83
|
}),
|
|
69
84
|
responses: {
|
|
70
85
|
201: { description: "Task created" },
|
|
@@ -239,12 +254,53 @@ export async function handleTasks(
|
|
|
239
254
|
if (listTasks.match(req.method, pathSegments)) {
|
|
240
255
|
const parsed = await listTasks.parse(req, res, pathSegments, queryParams);
|
|
241
256
|
if (!parsed) return true;
|
|
257
|
+
|
|
258
|
+
// Multi-status CSV: split on `,` and validate each token against the
|
|
259
|
+
// canonical enum. Empty / single-status callers still work.
|
|
260
|
+
let status: AgentTaskStatus | AgentTaskStatus[] | undefined;
|
|
261
|
+
if (parsed.query.status) {
|
|
262
|
+
const tokens = parsed.query.status
|
|
263
|
+
.split(",")
|
|
264
|
+
.map((s) => s.trim())
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
const validated: AgentTaskStatus[] = [];
|
|
267
|
+
for (const tok of tokens) {
|
|
268
|
+
const result = AgentTaskStatusSchema.safeParse(tok);
|
|
269
|
+
if (!result.success) {
|
|
270
|
+
jsonError(res, `Invalid status token: ${tok}`, 400);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
validated.push(result.data);
|
|
274
|
+
}
|
|
275
|
+
status = validated.length === 1 ? validated[0] : validated;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let source: AgentTaskSource[] | undefined;
|
|
279
|
+
if (parsed.query.source) {
|
|
280
|
+
const tokens = parsed.query.source
|
|
281
|
+
.split(",")
|
|
282
|
+
.map((s) => s.trim())
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
const validated: AgentTaskSource[] = [];
|
|
285
|
+
for (const tok of tokens) {
|
|
286
|
+
const result = AgentTaskSourceSchema.safeParse(tok);
|
|
287
|
+
if (!result.success) {
|
|
288
|
+
jsonError(res, `Invalid source token: ${tok}`, 400);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
validated.push(result.data);
|
|
292
|
+
}
|
|
293
|
+
if (validated.length > 0) source = validated;
|
|
294
|
+
}
|
|
295
|
+
|
|
242
296
|
const filters = {
|
|
243
|
-
status
|
|
297
|
+
status,
|
|
244
298
|
agentId: parsed.query.agentId || undefined,
|
|
245
299
|
scheduleId: parsed.query.scheduleId || undefined,
|
|
246
300
|
search: parsed.query.search || undefined,
|
|
247
301
|
includeHeartbeat: parsed.query.includeHeartbeat === "true" || undefined,
|
|
302
|
+
createdAfter: parsed.query.createdAfter || undefined,
|
|
303
|
+
source,
|
|
248
304
|
limit: parsed.query.limit,
|
|
249
305
|
offset: parsed.query.offset,
|
|
250
306
|
};
|
|
@@ -258,9 +314,32 @@ export async function handleTasks(
|
|
|
258
314
|
const parsed = await createTask.parse(req, res, pathSegments, queryParams);
|
|
259
315
|
if (!parsed) return true;
|
|
260
316
|
|
|
317
|
+
// Tolerant `requestedByUserId`: prevent the deleted-user race from
|
|
318
|
+
// becoming a 500 — if the referenced user doesn't exist, log and drop
|
|
319
|
+
// the field rather than letting the FK fail at INSERT.
|
|
320
|
+
let requestedByUserId = parsed.body.requestedByUserId || undefined;
|
|
321
|
+
if (requestedByUserId && !getUserById(requestedByUserId)) {
|
|
322
|
+
console.warn(
|
|
323
|
+
`[tasks] requestedByUserId ${requestedByUserId} does not exist — coercing to NULL`,
|
|
324
|
+
);
|
|
325
|
+
requestedByUserId = undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Default agent for ingress-created tasks: when no explicit `agentId` is
|
|
329
|
+
// provided, route to the lead so the task has an owner immediately
|
|
330
|
+
// (regardless of whether it's a root or a follow-up under a parentTaskId).
|
|
331
|
+
// Without this, UI composer follow-ups land unassigned and never get
|
|
332
|
+
// picked up. Mirrors Slack's pattern (slack/actions.ts uses lead?.id when
|
|
333
|
+
// there's no working agent).
|
|
334
|
+
let defaultAgentId = parsed.body.agentId || undefined;
|
|
335
|
+
if (!defaultAgentId) {
|
|
336
|
+
const lead = getLeadAgent();
|
|
337
|
+
if (lead) defaultAgentId = lead.id;
|
|
338
|
+
}
|
|
339
|
+
|
|
261
340
|
try {
|
|
262
341
|
const task = createTaskWithSiblingAwareness(parsed.body.task, {
|
|
263
|
-
agentId:
|
|
342
|
+
agentId: defaultAgentId,
|
|
264
343
|
creatorAgentId: myAgentId || undefined,
|
|
265
344
|
taskType: parsed.body.taskType || undefined,
|
|
266
345
|
tags: parsed.body.tags || undefined,
|
|
@@ -269,9 +348,10 @@ export async function handleTasks(
|
|
|
269
348
|
offeredTo: parsed.body.offeredTo || undefined,
|
|
270
349
|
dir: parsed.body.dir || undefined,
|
|
271
350
|
parentTaskId: parsed.body.parentTaskId || undefined,
|
|
272
|
-
source:
|
|
351
|
+
source: parsed.body.source || "api",
|
|
273
352
|
outputSchema: parsed.body.outputSchema || undefined,
|
|
274
353
|
contextKey: parsed.body.contextKey || undefined,
|
|
354
|
+
requestedByUserId,
|
|
275
355
|
});
|
|
276
356
|
|
|
277
357
|
ensure({
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createUser, getAllUsers, getUserById, updateUser } from "../be/db";
|
|
4
|
+
import { route } from "./route-def";
|
|
5
|
+
import { json, jsonError } from "./utils";
|
|
6
|
+
|
|
7
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const listUsers = route({
|
|
10
|
+
method: "get",
|
|
11
|
+
path: "/api/users",
|
|
12
|
+
pattern: ["api", "users"],
|
|
13
|
+
summary: "List all users",
|
|
14
|
+
tags: ["Users"],
|
|
15
|
+
responses: {
|
|
16
|
+
200: { description: "List of users" },
|
|
17
|
+
401: { description: "Unauthorized" },
|
|
18
|
+
},
|
|
19
|
+
auth: { apiKey: true },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const createUserRoute = route({
|
|
23
|
+
method: "post",
|
|
24
|
+
path: "/api/users",
|
|
25
|
+
pattern: ["api", "users"],
|
|
26
|
+
summary: "Create a new user",
|
|
27
|
+
tags: ["Users"],
|
|
28
|
+
body: z.object({
|
|
29
|
+
name: z.string().min(1),
|
|
30
|
+
email: z.string().optional(),
|
|
31
|
+
role: z.string().optional(),
|
|
32
|
+
notes: z.string().optional(),
|
|
33
|
+
slackUserId: z.string().optional(),
|
|
34
|
+
linearUserId: z.string().optional(),
|
|
35
|
+
githubUsername: z.string().optional(),
|
|
36
|
+
gitlabUsername: z.string().optional(),
|
|
37
|
+
emailAliases: z.array(z.string()).optional(),
|
|
38
|
+
preferredChannel: z.string().optional(),
|
|
39
|
+
timezone: z.string().optional(),
|
|
40
|
+
}),
|
|
41
|
+
responses: {
|
|
42
|
+
200: { description: "User created" },
|
|
43
|
+
400: { description: "Validation error" },
|
|
44
|
+
401: { description: "Unauthorized" },
|
|
45
|
+
},
|
|
46
|
+
auth: { apiKey: true },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const updateUserRoute = route({
|
|
50
|
+
method: "put",
|
|
51
|
+
path: "/api/users/{id}",
|
|
52
|
+
pattern: ["api", "users", null],
|
|
53
|
+
summary: "Update an existing user (partial — at least one field required)",
|
|
54
|
+
tags: ["Users"],
|
|
55
|
+
params: z.object({ id: z.string() }),
|
|
56
|
+
body: z
|
|
57
|
+
.object({
|
|
58
|
+
name: z.string().min(1).optional(),
|
|
59
|
+
email: z.string().optional(),
|
|
60
|
+
role: z.string().optional(),
|
|
61
|
+
notes: z.string().optional(),
|
|
62
|
+
slackUserId: z.string().optional(),
|
|
63
|
+
linearUserId: z.string().optional(),
|
|
64
|
+
githubUsername: z.string().optional(),
|
|
65
|
+
gitlabUsername: z.string().optional(),
|
|
66
|
+
emailAliases: z.array(z.string()).optional(),
|
|
67
|
+
preferredChannel: z.string().optional(),
|
|
68
|
+
timezone: z.string().optional(),
|
|
69
|
+
})
|
|
70
|
+
.refine((v) => Object.keys(v).length > 0, {
|
|
71
|
+
message: "At least one field must be provided",
|
|
72
|
+
}),
|
|
73
|
+
responses: {
|
|
74
|
+
200: { description: "User updated" },
|
|
75
|
+
400: { description: "Validation error or empty body" },
|
|
76
|
+
401: { description: "Unauthorized" },
|
|
77
|
+
404: { description: "User not found" },
|
|
78
|
+
},
|
|
79
|
+
auth: { apiKey: true },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export async function handleUsers(
|
|
85
|
+
req: IncomingMessage,
|
|
86
|
+
res: ServerResponse,
|
|
87
|
+
pathSegments: string[],
|
|
88
|
+
queryParams: URLSearchParams,
|
|
89
|
+
): Promise<boolean> {
|
|
90
|
+
if (listUsers.match(req.method, pathSegments)) {
|
|
91
|
+
const parsed = await listUsers.parse(req, res, pathSegments, queryParams);
|
|
92
|
+
if (!parsed) return true;
|
|
93
|
+
const users = getAllUsers();
|
|
94
|
+
json(res, { users });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (createUserRoute.match(req.method, pathSegments)) {
|
|
99
|
+
const parsed = await createUserRoute.parse(req, res, pathSegments, queryParams);
|
|
100
|
+
if (!parsed) return true;
|
|
101
|
+
try {
|
|
102
|
+
const user = createUser(parsed.body);
|
|
103
|
+
json(res, { user });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to create user", 500);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (updateUserRoute.match(req.method, pathSegments)) {
|
|
111
|
+
const parsed = await updateUserRoute.parse(req, res, pathSegments, queryParams);
|
|
112
|
+
if (!parsed) return true;
|
|
113
|
+
|
|
114
|
+
// 404 if user not found before update — keeps the contract honest.
|
|
115
|
+
if (!getUserById(parsed.params.id)) {
|
|
116
|
+
jsonError(res, "User not found", 404);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const user = updateUser(parsed.params.id, parsed.body);
|
|
122
|
+
if (!user) {
|
|
123
|
+
jsonError(res, "User not found", 404);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
json(res, { user });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to update user", 500);
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-5.md §5
|
|
3
|
+
*
|
|
4
|
+
* Worker-side rendering of the "Relevant Past Knowledge" memories block that
|
|
5
|
+
* gets appended to a task's initial prompt. Pure string manipulation — no DB
|
|
6
|
+
* imports — so this file stays inside the worker-side boundary enforced by
|
|
7
|
+
* scripts/check-db-boundary.sh.
|
|
8
|
+
*
|
|
9
|
+
* The conditional hint at the end is gated on `MEMORY_RATERS` containing
|
|
10
|
+
* `explicit-self`. When the gate is closed (the default), the rendered
|
|
11
|
+
* prompt is byte-identical to pre-rater builds — strict backward compat.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type RelevantMemory = {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
content: string;
|
|
18
|
+
similarity: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SIMILARITY_THRESHOLD = 0.4;
|
|
22
|
+
|
|
23
|
+
const RATE_TOOL_HINT = `
|
|
24
|
+
|
|
25
|
+
When a memory above genuinely helps you solve this task — or actively
|
|
26
|
+
misleads you — call \`memory_rate\` with the memory id and useful=true/false.
|
|
27
|
+
This trains the swarm to surface better memories next time. Use sparingly:
|
|
28
|
+
2-5 ratings per task is plenty.`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render the memories prompt section. Returns `null` when there are no
|
|
32
|
+
* memories with `similarity > 0.4` — the caller should then skip the
|
|
33
|
+
* append entirely (matching pre-step-5 behaviour).
|
|
34
|
+
*/
|
|
35
|
+
export function renderMemoriesPrompt(memories: RelevantMemory[]): string | null {
|
|
36
|
+
const useful = memories.filter((m) => m.similarity > SIMILARITY_THRESHOLD);
|
|
37
|
+
if (useful.length === 0) return null;
|
|
38
|
+
|
|
39
|
+
const memoryContext = useful
|
|
40
|
+
.map((m) => `- **${m.name}** (id: ${m.id}): ${m.content.substring(0, 300)}`)
|
|
41
|
+
.join("\n");
|
|
42
|
+
|
|
43
|
+
let prompt = `\n\n### Relevant Past Knowledge\n\nThese memories from your previous sessions may be useful. Use \`memory-get\` with the memory ID to retrieve full details.\n\n${memoryContext}\n`;
|
|
44
|
+
|
|
45
|
+
if (isExplicitSelfRaterEnabled()) {
|
|
46
|
+
prompt += RATE_TOOL_HINT;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return prompt;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Exported for tests. Reads `MEMORY_RATERS` lazily so a test can flip the
|
|
54
|
+
* env var between renders without re-importing the module.
|
|
55
|
+
*/
|
|
56
|
+
export function isExplicitSelfRaterEnabled(): boolean {
|
|
57
|
+
const ratersEnabled = (process.env.MEMORY_RATERS ?? "")
|
|
58
|
+
.split(",")
|
|
59
|
+
.map((s) => s.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
return ratersEnabled.includes("explicit-self");
|
|
62
|
+
}
|
|
@@ -11,6 +11,7 @@ import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
|
11
11
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
12
12
|
import type {
|
|
13
13
|
CostData,
|
|
14
|
+
CredStatus,
|
|
14
15
|
ProviderAdapter,
|
|
15
16
|
ProviderEvent,
|
|
16
17
|
ProviderResult,
|
|
@@ -18,6 +19,22 @@ import type {
|
|
|
18
19
|
ProviderSessionConfig,
|
|
19
20
|
} from "./types";
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Predicate used by the worker boot loop and the credential-status endpoint.
|
|
24
|
+
* The claude harness needs EITHER `CLAUDE_CODE_OAUTH_TOKEN` (preferred) or
|
|
25
|
+
* `ANTHROPIC_API_KEY` — both are listed as missing when neither is present.
|
|
26
|
+
*/
|
|
27
|
+
export function checkClaudeCredentials(env: Record<string, string | undefined>): CredStatus {
|
|
28
|
+
if (env.CLAUDE_CODE_OAUTH_TOKEN || env.ANTHROPIC_API_KEY) {
|
|
29
|
+
return { ready: true, missing: [], satisfiedBy: "env" };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
ready: false,
|
|
33
|
+
missing: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
|
34
|
+
hint: "Set either CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY (one is enough).",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
/** Task file data written to /tmp for hook to read */
|
|
22
39
|
interface TaskFileData {
|
|
23
40
|
taskId: string;
|
|
@@ -177,6 +194,11 @@ class ClaudeSession implements ProviderSession {
|
|
|
177
194
|
ENABLE_PROMPT_CACHING_1H: "1",
|
|
178
195
|
...(config.env || process.env),
|
|
179
196
|
TASK_FILE: taskFilePath,
|
|
197
|
+
// Belt-and-braces: TASK_FILE on disk can disappear mid-session (race
|
|
198
|
+
// with task lifecycle), which silently drops the Stop-hook memory
|
|
199
|
+
// rater. The hook prefers these env vars when present. See PR #444.
|
|
200
|
+
AGENT_SWARM_TASK_ID: config.taskId,
|
|
201
|
+
AGENT_SWARM_AGENT_ID: config.agentId,
|
|
180
202
|
} as Record<string, string>,
|
|
181
203
|
stdout: "pipe",
|
|
182
204
|
stderr: "pipe",
|
|
@@ -64,6 +64,7 @@ import { computeClaudeManagedCostUsd } from "./claude-managed-models";
|
|
|
64
64
|
import { createClaudeManagedSwarmEventHandler } from "./claude-managed-swarm-events";
|
|
65
65
|
import type {
|
|
66
66
|
CostData,
|
|
67
|
+
CredStatus,
|
|
67
68
|
ProviderAdapter,
|
|
68
69
|
ProviderEvent,
|
|
69
70
|
ProviderResult,
|
|
@@ -71,6 +72,29 @@ import type {
|
|
|
71
72
|
ProviderSessionConfig,
|
|
72
73
|
} from "./types";
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Managed-agents needs all four bootstrap values. Unlike vanilla claude there
|
|
77
|
+
* is no oauth fallback — the SDK requires the API key, agent id, environment
|
|
78
|
+
* id, and a public MCP base URL the cloud sandbox can reach.
|
|
79
|
+
*/
|
|
80
|
+
export function checkClaudeManagedCredentials(env: Record<string, string | undefined>): CredStatus {
|
|
81
|
+
const required = [
|
|
82
|
+
"ANTHROPIC_API_KEY",
|
|
83
|
+
"MANAGED_AGENT_ID",
|
|
84
|
+
"MANAGED_ENVIRONMENT_ID",
|
|
85
|
+
"MCP_BASE_URL",
|
|
86
|
+
] as const;
|
|
87
|
+
const missing = required.filter((key) => !env[key]);
|
|
88
|
+
if (missing.length === 0) {
|
|
89
|
+
return { ready: true, missing: [], satisfiedBy: "env" };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
ready: false,
|
|
93
|
+
missing,
|
|
94
|
+
hint: "Run `bun run src/cli.tsx claude-managed-setup` once to provision MANAGED_AGENT_ID and MANAGED_ENVIRONMENT_ID, then set ANTHROPIC_API_KEY and MCP_BASE_URL (must be HTTPS-public).",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
// Re-export the type aliases at module level so adjacent files / tests can use
|
|
75
99
|
// the short names without re-discovering the long Beta-prefixed ones. Kept on
|
|
76
100
|
// `void` lines so unused-import lints stay quiet for the type imports above.
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* `Turn`, events, or items — the SDK already exports them as a tagged union.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
+
import { existsSync as nodeExistsSync } from "node:fs";
|
|
48
49
|
import os from "node:os";
|
|
49
50
|
import { join } from "node:path";
|
|
50
51
|
import {
|
|
@@ -73,6 +74,8 @@ import { resolveCodexPrompt } from "./codex-skill-resolver";
|
|
|
73
74
|
import { createCodexSwarmEventHandler } from "./codex-swarm-events";
|
|
74
75
|
import type {
|
|
75
76
|
CostData,
|
|
77
|
+
CredCheckOptions,
|
|
78
|
+
CredStatus,
|
|
76
79
|
ProviderAdapter,
|
|
77
80
|
ProviderEvent,
|
|
78
81
|
ProviderResult,
|
|
@@ -83,6 +86,45 @@ import type {
|
|
|
83
86
|
/** Alias for the SDK's (unexported) `CodexConfigObject` type. */
|
|
84
87
|
type CodexConfig = NonNullable<CodexOptions["config"]>;
|
|
85
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Codex satisfies its credential requirement by ANY of:
|
|
91
|
+
* 1. `~/.codex/auth.json` already exists on disk (the canonical state once
|
|
92
|
+
* `codex login` has run).
|
|
93
|
+
* 2. `OPENAI_API_KEY` is set — the entrypoint will run
|
|
94
|
+
* `codex login --with-api-key` to materialise auth.json on the next boot.
|
|
95
|
+
* 3. `CODEX_OAUTH` is set in the env (typically pulled from swarm_config) —
|
|
96
|
+
* the entrypoint restores it to disk.
|
|
97
|
+
*
|
|
98
|
+
* Cases 2/3 return `satisfiedBy: 'side-effect-pending'` because the worker
|
|
99
|
+
* process can't proceed until the entrypoint side-effect has materialised the
|
|
100
|
+
* file. The boot loop treats this as ready (the side-effect is the
|
|
101
|
+
* entrypoint's job, and re-running it is idempotent).
|
|
102
|
+
*/
|
|
103
|
+
export function checkCodexCredentials(
|
|
104
|
+
env: Record<string, string | undefined>,
|
|
105
|
+
opts: CredCheckOptions = {},
|
|
106
|
+
): CredStatus {
|
|
107
|
+
const homeDir = opts.homeDir ?? env.HOME ?? "/root";
|
|
108
|
+
const existsSync = opts.fs?.existsSync ?? nodeExistsSync;
|
|
109
|
+
const authFile = `${homeDir}/.codex/auth.json`;
|
|
110
|
+
if (existsSync(authFile)) {
|
|
111
|
+
return { ready: true, missing: [], satisfiedBy: "file" };
|
|
112
|
+
}
|
|
113
|
+
if (env.OPENAI_API_KEY || env.CODEX_OAUTH) {
|
|
114
|
+
return {
|
|
115
|
+
ready: true,
|
|
116
|
+
missing: [],
|
|
117
|
+
satisfiedBy: "side-effect-pending",
|
|
118
|
+
hint: "Credential present in env; entrypoint will materialise ~/.codex/auth.json on next boot.",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
ready: false,
|
|
123
|
+
missing: ["OPENAI_API_KEY", "CODEX_OAUTH", authFile],
|
|
124
|
+
hint: "Set OPENAI_API_KEY (entrypoint runs `codex login --with-api-key`), or store CODEX_OAUTH in swarm_config, or place a pre-authenticated `~/.codex/auth.json` in the worker home.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
86
128
|
/**
|
|
87
129
|
* Shape returned by `GET /api/agents/:id/mcp-servers?resolveSecrets=true`.
|
|
88
130
|
* Mirrors `pi-mono-adapter.ts:430-439` and `claude-adapter.ts:59-72`, plus
|
|
@@ -648,7 +690,7 @@ class CodexSession implements ProviderSession {
|
|
|
648
690
|
// `contextPercent` is on a 0-100 scale across all providers — claude
|
|
649
691
|
// emits `(used / total) * 100`, pi-mono passes through `usage.percent`
|
|
650
692
|
// which is already 0-100. The dashboard at
|
|
651
|
-
//
|
|
693
|
+
// ui/src/pages/tasks/[id]/page.tsx renders it via `.toFixed(0)`
|
|
652
694
|
// expecting an integer percent, so a 0-1 fraction would render as
|
|
653
695
|
// "0%" instead of e.g. "40%".
|
|
654
696
|
this.emit({
|
|
@@ -24,6 +24,7 @@ import { getOrCreatePlaybook } from "./devin-playbooks";
|
|
|
24
24
|
import { resolveDevinPrompt } from "./devin-skill-resolver";
|
|
25
25
|
import type {
|
|
26
26
|
CostData,
|
|
27
|
+
CredStatus,
|
|
27
28
|
ProviderAdapter,
|
|
28
29
|
ProviderEvent,
|
|
29
30
|
ProviderResult,
|
|
@@ -32,6 +33,23 @@ import type {
|
|
|
32
33
|
ProviderTraits,
|
|
33
34
|
} from "./types";
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Devin requires both an API key and an org id — there is no file-based
|
|
38
|
+
* fallback like the local-CLI providers offer.
|
|
39
|
+
*/
|
|
40
|
+
export function checkDevinCredentials(env: Record<string, string | undefined>): CredStatus {
|
|
41
|
+
const required = ["DEVIN_API_KEY", "DEVIN_ORG_ID"] as const;
|
|
42
|
+
const missing = required.filter((key) => !env[key]);
|
|
43
|
+
if (missing.length === 0) {
|
|
44
|
+
return { ready: true, missing: [], satisfiedBy: "env" };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
ready: false,
|
|
48
|
+
missing,
|
|
49
|
+
hint: "Set DEVIN_API_KEY and DEVIN_ORG_ID. Both come from app.devin.ai → Settings → API.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/** Default polling interval in milliseconds. */
|
|
36
54
|
const DEFAULT_POLL_INTERVAL_MS = 15_000;
|
|
37
55
|
|
package/src/providers/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
checkProviderCredentials,
|
|
3
|
+
REQUIRED_CRED_VARS_BY_PROVIDER,
|
|
4
|
+
type SupportedProvider,
|
|
5
|
+
} from "../commands/provider-credentials";
|
|
1
6
|
export type {
|
|
2
7
|
CostData,
|
|
8
|
+
CredCheckOptions,
|
|
9
|
+
CredStatus,
|
|
3
10
|
ProviderAdapter,
|
|
4
11
|
ProviderEvent,
|
|
5
12
|
ProviderResult,
|
|
@@ -18,6 +18,8 @@ import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
|
18
18
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
19
19
|
import type {
|
|
20
20
|
CostData,
|
|
21
|
+
CredCheckOptions,
|
|
22
|
+
CredStatus,
|
|
21
23
|
ProviderAdapter,
|
|
22
24
|
ProviderEvent,
|
|
23
25
|
ProviderResult,
|
|
@@ -26,6 +28,64 @@ import type {
|
|
|
26
28
|
ProviderTraits,
|
|
27
29
|
} from "./types";
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Map opencode model strings to the env var that satisfies them. Opencode
|
|
33
|
+
* uses the same `provider/model-id` shape as pi-mono — the prefix tells us
|
|
34
|
+
* which key the user must supply.
|
|
35
|
+
*/
|
|
36
|
+
function opencodeModelToCredKey(modelStr: string | undefined): string | null {
|
|
37
|
+
if (!modelStr) return null;
|
|
38
|
+
if (modelStr.includes("/")) {
|
|
39
|
+
const provider = modelStr.slice(0, modelStr.indexOf("/")).toLowerCase();
|
|
40
|
+
if (provider === "anthropic") return "ANTHROPIC_API_KEY";
|
|
41
|
+
if (provider === "openrouter") return "OPENROUTER_API_KEY";
|
|
42
|
+
if (provider === "openai") return "OPENAI_API_KEY";
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Opencode is satisfied by ANY of:
|
|
49
|
+
* 1. `~/.local/share/opencode/auth.json` exists (the file `opencode auth login`
|
|
50
|
+
* writes).
|
|
51
|
+
* 2. `MODEL_OVERRIDE` resolves to a provider-prefixed model — only that
|
|
52
|
+
* provider's key is required.
|
|
53
|
+
* 3. Otherwise any one of OPENROUTER_API_KEY / ANTHROPIC_API_KEY /
|
|
54
|
+
* OPENAI_API_KEY suffices.
|
|
55
|
+
*/
|
|
56
|
+
export function checkOpencodeCredentials(
|
|
57
|
+
env: Record<string, string | undefined>,
|
|
58
|
+
opts: CredCheckOptions = {},
|
|
59
|
+
): CredStatus {
|
|
60
|
+
const homeDir = opts.homeDir ?? env.HOME ?? "/root";
|
|
61
|
+
const probe = opts.fs?.existsSync ?? existsSync;
|
|
62
|
+
const authFile = `${homeDir}/.local/share/opencode/auth.json`;
|
|
63
|
+
if (probe(authFile)) {
|
|
64
|
+
return { ready: true, missing: [], satisfiedBy: "file" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const requiredKey = opencodeModelToCredKey(env.MODEL_OVERRIDE);
|
|
68
|
+
if (requiredKey) {
|
|
69
|
+
if (env[requiredKey]) {
|
|
70
|
+
return { ready: true, missing: [], satisfiedBy: "env" };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
ready: false,
|
|
74
|
+
missing: [requiredKey, authFile],
|
|
75
|
+
hint: `MODEL_OVERRIDE=${env.MODEL_OVERRIDE} requires ${requiredKey}; or run \`opencode auth login\` to create ${authFile}.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (env.OPENROUTER_API_KEY || env.ANTHROPIC_API_KEY || env.OPENAI_API_KEY) {
|
|
80
|
+
return { ready: true, missing: [], satisfiedBy: "env" };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ready: false,
|
|
84
|
+
missing: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", authFile],
|
|
85
|
+
hint: "Set one of OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY (any one suffices), or run `opencode auth login` to create ~/.local/share/opencode/auth.json.",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
29
89
|
function isAssistantMessage(msg: unknown): msg is AssistantMessage {
|
|
30
90
|
return (
|
|
31
91
|
typeof msg === "object" &&
|