@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
package/src/hooks/hook.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import pkg from "../../package.json";
|
|
4
|
+
import {
|
|
5
|
+
buildRatingsFromLlm,
|
|
6
|
+
buildSummaryWithRatingsPrompt,
|
|
7
|
+
dedupeRetrievalsForRater,
|
|
8
|
+
fetchRetrievalsForTask,
|
|
9
|
+
isLlmRaterEnabled,
|
|
10
|
+
postRatings,
|
|
11
|
+
type RetrievalRow,
|
|
12
|
+
} from "../be/memory/raters/llm";
|
|
13
|
+
import { runMemoryRater } from "../be/memory/raters/llm-summarizer";
|
|
4
14
|
import type { Agent } from "../types";
|
|
5
15
|
import { checkToolLoop, clearToolHistory } from "./tool-loop-detection";
|
|
6
16
|
|
|
@@ -195,6 +205,32 @@ async function restoreClaudeMdBackup(): Promise<void> {
|
|
|
195
205
|
}
|
|
196
206
|
}
|
|
197
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Resolve task context for the Stop-hook session-summary / memory-rater
|
|
210
|
+
* piggyback. Prefers the AGENT_SWARM_TASK_ID env var (set by the harness in
|
|
211
|
+
* `claude-adapter.ts`) so the rater still works when the on-disk TASK_FILE
|
|
212
|
+
* was already cleaned up — the silent-drop bug PR #444 traced.
|
|
213
|
+
*/
|
|
214
|
+
export async function resolveStopHookTaskContext(
|
|
215
|
+
env: Record<string, string | undefined> = process.env,
|
|
216
|
+
): Promise<{ taskContext: string; taskId: string | undefined }> {
|
|
217
|
+
let taskContext = "";
|
|
218
|
+
let taskId: string | undefined = env.AGENT_SWARM_TASK_ID || undefined;
|
|
219
|
+
const taskFile = env.TASK_FILE;
|
|
220
|
+
if (taskFile) {
|
|
221
|
+
try {
|
|
222
|
+
const taskData = JSON.parse(await Bun.file(taskFile).text());
|
|
223
|
+
taskContext = `Task: ${taskData.task || "Unknown"}`;
|
|
224
|
+
if (!taskId) taskId = taskData.id;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// Don't blackhole the read failure — log it once so future regressions
|
|
227
|
+
// are visible. Same one-line debug pattern as PR #444's gate trace.
|
|
228
|
+
console.error("[memory-rater:llm] TASK_FILE read failed:", (err as Error).message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { taskContext, taskId };
|
|
232
|
+
}
|
|
233
|
+
|
|
198
234
|
/**
|
|
199
235
|
* Main hook handler - processes Claude Code hook events
|
|
200
236
|
*/
|
|
@@ -1043,9 +1079,19 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
|
|
|
1043
1079
|
}
|
|
1044
1080
|
}
|
|
1045
1081
|
|
|
1046
|
-
// Session summarization via
|
|
1047
|
-
//
|
|
1048
|
-
|
|
1082
|
+
// Session summarization + LLM rater piggyback via OpenRouter (Vercel AI SDK).
|
|
1083
|
+
//
|
|
1084
|
+
// No-op when OPENROUTER_API_KEY is unset — self-hosters / OSS users
|
|
1085
|
+
// without OpenRouter skip session summary + LLM ratings entirely. The
|
|
1086
|
+
// previous `claude -p` path silently produced "Not logged in · Please
|
|
1087
|
+
// run /login" rows after the 2026-05-05 CLAUDE_CODE_VERSION bump
|
|
1088
|
+
// stopped propagating CLAUDE_CODE_OAUTH_TOKEN to hook subprocesses.
|
|
1089
|
+
if (
|
|
1090
|
+
agentInfo?.id &&
|
|
1091
|
+
msg.transcript_path &&
|
|
1092
|
+
!process.env.SKIP_SESSION_SUMMARY &&
|
|
1093
|
+
process.env.OPENROUTER_API_KEY
|
|
1094
|
+
) {
|
|
1049
1095
|
try {
|
|
1050
1096
|
let transcript = "";
|
|
1051
1097
|
try {
|
|
@@ -1057,22 +1103,33 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
|
|
|
1057
1103
|
}
|
|
1058
1104
|
|
|
1059
1105
|
if (transcript.length > 100) {
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1106
|
+
// Prefer AGENT_SWARM_TASK_ID env var; fall back to TASK_FILE on
|
|
1107
|
+
// disk. PR #444 gate-trace showed the file disappears mid-session
|
|
1108
|
+
// and the silent catch dropped every LLM rater piggyback.
|
|
1109
|
+
const { taskContext, taskId } = await resolveStopHookTaskContext();
|
|
1110
|
+
|
|
1111
|
+
const apiUrl =
|
|
1112
|
+
process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
|
|
1113
|
+
const apiKey = process.env.API_KEY || "";
|
|
1114
|
+
|
|
1115
|
+
// Memory-rater v1.5 step-4: piggyback per-memory ratings on the
|
|
1116
|
+
// existing summary call when MEMORY_RATERS includes `llm`.
|
|
1117
|
+
// Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-4.md §3
|
|
1118
|
+
const llmRaterEnabled = isLlmRaterEnabled();
|
|
1119
|
+
let retrievals: RetrievalRow[] = [];
|
|
1120
|
+
if (llmRaterEnabled && taskId) {
|
|
1121
|
+
const rawRetrievals = await fetchRetrievalsForTask({
|
|
1122
|
+
apiUrl,
|
|
1123
|
+
apiKey,
|
|
1124
|
+
agentId: agentInfo.id,
|
|
1125
|
+
taskId,
|
|
1126
|
+
});
|
|
1127
|
+
// Dedup self-similar cron-task memories before sending to the
|
|
1128
|
+
// rater — see `dedupeRetrievalsForRater` doc for the why.
|
|
1129
|
+
retrievals = dedupeRetrievalsForRater(rawRetrievals);
|
|
1072
1130
|
}
|
|
1073
1131
|
|
|
1074
|
-
|
|
1075
|
-
const summarizePrompt = `You are summarizing an AI agent's work session. Extract ONLY high-value learnings.
|
|
1132
|
+
const baseSummarizePrompt = `You are summarizing an AI agent's work session. Extract ONLY high-value learnings.
|
|
1076
1133
|
|
|
1077
1134
|
DO NOT include:
|
|
1078
1135
|
- Generic descriptions of what was done ("worked on task X")
|
|
@@ -1091,57 +1148,77 @@ ${taskContext ? `\nTask context: ${taskContext}` : ""}
|
|
|
1091
1148
|
Transcript:
|
|
1092
1149
|
${transcript}`;
|
|
1093
1150
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
stdout: "pipe",
|
|
1100
|
-
stderr: "pipe",
|
|
1101
|
-
env: { ...process.env, SKIP_SESSION_SUMMARY: "1" },
|
|
1102
|
-
},
|
|
1103
|
-
);
|
|
1104
|
-
const timeoutId = setTimeout(() => proc.kill(), 30000);
|
|
1105
|
-
const result = { stdout: await new Response(proc.stdout).text() };
|
|
1106
|
-
clearTimeout(timeoutId);
|
|
1107
|
-
await Bun.$`rm -f ${tmpFile}`.quiet();
|
|
1108
|
-
|
|
1109
|
-
let summary: string;
|
|
1110
|
-
try {
|
|
1111
|
-
const summaryOutput = JSON.parse(result.stdout);
|
|
1112
|
-
summary = summaryOutput.result ?? result.stdout;
|
|
1113
|
-
} catch {
|
|
1114
|
-
summary = result.stdout;
|
|
1115
|
-
}
|
|
1151
|
+
// Always ask for the structured (summary + ratings) payload — same
|
|
1152
|
+
// cost as the unstructured path. Empty retrievals → empty memory
|
|
1153
|
+
// block → ratings: []; the postRatings gate below still skips the
|
|
1154
|
+
// POST when there's nothing to send.
|
|
1155
|
+
const summarizePrompt = buildSummaryWithRatingsPrompt(baseSummarizePrompt, retrievals);
|
|
1116
1156
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const apiKey = process.env.API_KEY || "";
|
|
1126
|
-
|
|
1127
|
-
await fetch(`${apiUrl}/api/memory/index`, {
|
|
1128
|
-
method: "POST",
|
|
1129
|
-
headers: {
|
|
1130
|
-
"Content-Type": "application/json",
|
|
1131
|
-
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
1132
|
-
"X-Agent-ID": agentInfo.id,
|
|
1133
|
-
},
|
|
1134
|
-
body: JSON.stringify({
|
|
1135
|
-
agentId: agentInfo.id,
|
|
1136
|
-
content: summary,
|
|
1137
|
-
name: taskContext
|
|
1138
|
-
? `Session: ${taskContext.slice(0, 80)}`
|
|
1139
|
-
: `Session: ${new Date().toISOString().slice(0, 16)}`,
|
|
1140
|
-
scope: "agent",
|
|
1141
|
-
source: "session_summary",
|
|
1142
|
-
...(taskId ? { sourceTaskId: taskId } : {}),
|
|
1143
|
-
}),
|
|
1157
|
+
const raterResult = await runMemoryRater({
|
|
1158
|
+
prompt: summarizePrompt,
|
|
1159
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
1160
|
+
});
|
|
1161
|
+
if (!raterResult.ok) {
|
|
1162
|
+
console.error("[memory-rater:llm] runMemoryRater returned non-ok", {
|
|
1163
|
+
reason: raterResult.reason,
|
|
1164
|
+
...(raterResult.status !== undefined ? { status: raterResult.status } : {}),
|
|
1144
1165
|
});
|
|
1166
|
+
} else {
|
|
1167
|
+
const summary = raterResult.data.summary;
|
|
1168
|
+
const ratings = raterResult.data.ratings;
|
|
1169
|
+
|
|
1170
|
+
// Skip indexing if the session had no significant learnings
|
|
1171
|
+
if (
|
|
1172
|
+
summary &&
|
|
1173
|
+
summary.length > 20 &&
|
|
1174
|
+
!summary.trim().toLowerCase().includes("no significant learnings")
|
|
1175
|
+
) {
|
|
1176
|
+
await fetch(`${apiUrl}/api/memory/index`, {
|
|
1177
|
+
method: "POST",
|
|
1178
|
+
headers: {
|
|
1179
|
+
"Content-Type": "application/json",
|
|
1180
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
1181
|
+
"X-Agent-ID": agentInfo.id,
|
|
1182
|
+
},
|
|
1183
|
+
body: JSON.stringify({
|
|
1184
|
+
agentId: agentInfo.id,
|
|
1185
|
+
content: summary,
|
|
1186
|
+
name: taskContext
|
|
1187
|
+
? `Session: ${taskContext.slice(0, 80)}`
|
|
1188
|
+
: `Session: ${new Date().toISOString().slice(0, 16)}`,
|
|
1189
|
+
scope: "agent",
|
|
1190
|
+
source: "session_summary",
|
|
1191
|
+
...(taskId ? { sourceTaskId: taskId } : {}),
|
|
1192
|
+
}),
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Best-effort: post LLM ratings. Never blocks summary indexing.
|
|
1197
|
+
if (llmRaterEnabled && taskId && retrievals.length > 0 && ratings.length === 0) {
|
|
1198
|
+
console.error("[memory-rater:llm] piggyback produced no ratings", {
|
|
1199
|
+
retrievalsLen: retrievals.length,
|
|
1200
|
+
ratingsLen: 0,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
if (llmRaterEnabled && taskId && ratings.length > 0) {
|
|
1204
|
+
try {
|
|
1205
|
+
const events = buildRatingsFromLlm(ratings, retrievals);
|
|
1206
|
+
if (events.length > 0) {
|
|
1207
|
+
await postRatings({
|
|
1208
|
+
apiUrl,
|
|
1209
|
+
apiKey,
|
|
1210
|
+
agentId: agentInfo.id,
|
|
1211
|
+
taskId,
|
|
1212
|
+
events,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
console.error(
|
|
1217
|
+
"[memory-rater:llm] piggyback rating emission failed:",
|
|
1218
|
+
(err as Error).message,
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1145
1222
|
}
|
|
1146
1223
|
}
|
|
1147
1224
|
} catch {
|
package/src/http/agents.ts
CHANGED
|
@@ -10,14 +10,18 @@ import {
|
|
|
10
10
|
getDb,
|
|
11
11
|
getSwarmConfigs,
|
|
12
12
|
resetEmptyPollCount,
|
|
13
|
+
setAgentHarnessProvider,
|
|
13
14
|
updateAgentActivity,
|
|
15
|
+
updateAgentCredentialState,
|
|
16
|
+
updateAgentCredStatus,
|
|
14
17
|
updateAgentMaxTasks,
|
|
15
18
|
updateAgentName,
|
|
16
19
|
updateAgentProfile,
|
|
17
20
|
updateAgentProvider,
|
|
18
21
|
updateAgentStatus,
|
|
22
|
+
upsertSwarmConfig,
|
|
19
23
|
} from "../be/db";
|
|
20
|
-
import { ProviderNameSchema } from "../types";
|
|
24
|
+
import { AgentCredStatusSchema, ProviderNameSchema } from "../types";
|
|
21
25
|
import { route } from "./route-def";
|
|
22
26
|
import { agentWithCapacity, json, jsonError } from "./utils";
|
|
23
27
|
|
|
@@ -37,6 +41,12 @@ const registerAgent = route({
|
|
|
37
41
|
capabilities: z.array(z.string()).optional(),
|
|
38
42
|
maxTasks: z.number().int().optional(),
|
|
39
43
|
provider: ProviderNameSchema.optional(),
|
|
44
|
+
/**
|
|
45
|
+
* Phase 1.5 (cloud-personalization): worker-pushed canonical harness
|
|
46
|
+
* provider. Persists to `agents.harness_provider`. Validated against
|
|
47
|
+
* the canonical list — unknown values reject the request with 400.
|
|
48
|
+
*/
|
|
49
|
+
harness_provider: ProviderNameSchema.optional(),
|
|
40
50
|
}),
|
|
41
51
|
responses: {
|
|
42
52
|
200: { description: "Agent re-registered (already existed)" },
|
|
@@ -45,6 +55,25 @@ const registerAgent = route({
|
|
|
45
55
|
},
|
|
46
56
|
});
|
|
47
57
|
|
|
58
|
+
const setAgentHarnessProviderRoute = route({
|
|
59
|
+
method: "patch",
|
|
60
|
+
path: "/api/agents/{id}/harness-provider",
|
|
61
|
+
pattern: ["api", "agents", null, "harness-provider"],
|
|
62
|
+
summary: "Re-assign an agent's harness_provider (live)",
|
|
63
|
+
description:
|
|
64
|
+
"Updates `agents.harness_provider` and upserts `swarm_config` (scope=agent, key=HARNESS_PROVIDER) so the worker's poll-loop reconciliation picks up the new provider within ~10s. No restart required. The swarm_config row is what actually drives the worker; the column mirrors the latest set value for dashboards.",
|
|
65
|
+
tags: ["Agents"],
|
|
66
|
+
params: z.object({ id: z.string() }),
|
|
67
|
+
body: z.object({
|
|
68
|
+
harness_provider: ProviderNameSchema,
|
|
69
|
+
}),
|
|
70
|
+
responses: {
|
|
71
|
+
200: { description: "Updated agent row" },
|
|
72
|
+
400: { description: "Validation error (unknown provider)" },
|
|
73
|
+
404: { description: "Agent not found" },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
48
77
|
const listAgents = route({
|
|
49
78
|
method: "get",
|
|
50
79
|
path: "/api/agents",
|
|
@@ -143,6 +172,62 @@ const getAgent = route({
|
|
|
143
172
|
},
|
|
144
173
|
});
|
|
145
174
|
|
|
175
|
+
// ─── Credential-status (Phase 3 + 4 of the credential safe-loop plan) ───────
|
|
176
|
+
|
|
177
|
+
const credentialStatusBody = z.object({
|
|
178
|
+
ready: z.boolean(),
|
|
179
|
+
/** Env-var names (or absolute file paths) the worker is blocked on. Empty/null when ready. */
|
|
180
|
+
missing: z.array(z.string()).optional().nullable(),
|
|
181
|
+
/**
|
|
182
|
+
* Migration 055: full credential snapshot (presence + live test). Optional
|
|
183
|
+
* for backward compat — older workers may only POST `{ready, missing}`.
|
|
184
|
+
* When present, written to `agents.cred_status` as JSON; the dashboard
|
|
185
|
+
* reads the row instead of running its own check.
|
|
186
|
+
*/
|
|
187
|
+
cred_status: AgentCredStatusSchema.optional().nullable(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const updateAgentCredentialStatusRoute = route({
|
|
191
|
+
method: "put",
|
|
192
|
+
path: "/api/agents/{id}/credential-status",
|
|
193
|
+
pattern: ["api", "agents", null, "credential-status"],
|
|
194
|
+
summary: "Worker self-report of credential readiness (Phase 3 boot loop)",
|
|
195
|
+
tags: ["Agents"],
|
|
196
|
+
params: z.object({ id: z.string() }),
|
|
197
|
+
body: credentialStatusBody,
|
|
198
|
+
responses: {
|
|
199
|
+
200: { description: "State updated; returns the agent row." },
|
|
200
|
+
404: { description: "Agent not found" },
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const getAgentCredentialStatusRoute = route({
|
|
205
|
+
method: "get",
|
|
206
|
+
path: "/api/agents/{id}/credential-status",
|
|
207
|
+
pattern: ["api", "agents", null, "credential-status"],
|
|
208
|
+
summary: "Single-agent credential-status snapshot for the dashboard",
|
|
209
|
+
tags: ["Agents"],
|
|
210
|
+
params: z.object({ id: z.string() }),
|
|
211
|
+
responses: {
|
|
212
|
+
200: { description: "Credential status payload" },
|
|
213
|
+
404: { description: "Agent not found" },
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const listCredentialStatusRoute = route({
|
|
218
|
+
method: "get",
|
|
219
|
+
path: "/api/agents/credential-status",
|
|
220
|
+
pattern: ["api", "agents", "credential-status"],
|
|
221
|
+
summary: "Bulk credential-status across all agents (powers the dashboard)",
|
|
222
|
+
tags: ["Agents"],
|
|
223
|
+
query: z.object({
|
|
224
|
+
status: z.enum(["idle", "busy", "offline", "waiting_for_credentials"]).optional(),
|
|
225
|
+
}),
|
|
226
|
+
responses: {
|
|
227
|
+
200: { description: "List of {agentId, status, missing[], lastCheckedAt}" },
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
146
231
|
// ─── Handlers ────────────────────────────────────────────────────────────────
|
|
147
232
|
|
|
148
233
|
export async function handleAgentRegister(
|
|
@@ -169,6 +254,17 @@ export async function handleAgentRegister(
|
|
|
169
254
|
if (parsed.body.provider && parsed.body.provider !== existingAgent.provider) {
|
|
170
255
|
updateAgentProvider(existingAgent.id, parsed.body.provider);
|
|
171
256
|
}
|
|
257
|
+
// Phase 1.5: worker-pushed harness_provider always wins on
|
|
258
|
+
// re-registration. Env-driven, by design (per-agent live override
|
|
259
|
+
// belongs to DES-359). NULL => leave existing column untouched
|
|
260
|
+
// so PATCH /harness-provider doesn't get clobbered by re-register
|
|
261
|
+
// payloads from older workers.
|
|
262
|
+
if (
|
|
263
|
+
parsed.body.harness_provider &&
|
|
264
|
+
parsed.body.harness_provider !== existingAgent.harnessProvider
|
|
265
|
+
) {
|
|
266
|
+
setAgentHarnessProvider(existingAgent.id, parsed.body.harness_provider);
|
|
267
|
+
}
|
|
172
268
|
resetEmptyPollCount(existingAgent.id);
|
|
173
269
|
return { agent: getAgentById(agentId), created: false };
|
|
174
270
|
}
|
|
@@ -183,6 +279,7 @@ export async function handleAgentRegister(
|
|
|
183
279
|
capabilities: parsed.body.capabilities ?? [],
|
|
184
280
|
maxTasks: parsed.body.maxTasks ?? 1,
|
|
185
281
|
provider: parsed.body.provider,
|
|
282
|
+
harnessProvider: parsed.body.harness_provider ?? null,
|
|
186
283
|
});
|
|
187
284
|
|
|
188
285
|
return { agent, created: true };
|
|
@@ -349,6 +446,99 @@ export async function handleAgentsRest(
|
|
|
349
446
|
return true;
|
|
350
447
|
}
|
|
351
448
|
|
|
449
|
+
if (setAgentHarnessProviderRoute.match(req.method, pathSegments)) {
|
|
450
|
+
const parsed = await setAgentHarnessProviderRoute.parse(req, res, pathSegments, queryParams);
|
|
451
|
+
if (!parsed) return true;
|
|
452
|
+
const agent = setAgentHarnessProvider(parsed.params.id, parsed.body.harness_provider);
|
|
453
|
+
if (!agent) {
|
|
454
|
+
jsonError(res, "Agent not found", 404);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
// Mirror to swarm_config (scope=agent) so the worker's reconciliation
|
|
458
|
+
// loop actually reads the new value. The column above is for dashboard
|
|
459
|
+
// visibility; this row is the live override.
|
|
460
|
+
upsertSwarmConfig({
|
|
461
|
+
scope: "agent",
|
|
462
|
+
scopeId: parsed.params.id,
|
|
463
|
+
key: "HARNESS_PROVIDER",
|
|
464
|
+
value: parsed.body.harness_provider,
|
|
465
|
+
description: "Set via PATCH /api/agents/{id}/harness-provider",
|
|
466
|
+
});
|
|
467
|
+
json(res, agentWithCapacity(agent));
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Bulk credential-status MUST be matched BEFORE single-agent routes — the
|
|
472
|
+
// path "api/agents/credential-status" otherwise looks like an agent id.
|
|
473
|
+
if (listCredentialStatusRoute.match(req.method, pathSegments)) {
|
|
474
|
+
const parsed = await listCredentialStatusRoute.parse(req, res, pathSegments, queryParams);
|
|
475
|
+
if (!parsed) return true;
|
|
476
|
+
const filter = parsed.query.status;
|
|
477
|
+
const agents = getAllAgents()
|
|
478
|
+
.filter((a) => (filter ? a.status === filter : true))
|
|
479
|
+
.map((a) => ({
|
|
480
|
+
agentId: a.id,
|
|
481
|
+
name: a.name,
|
|
482
|
+
status: a.status,
|
|
483
|
+
missing: a.credentialMissing ?? [],
|
|
484
|
+
provider: a.provider ?? null,
|
|
485
|
+
harnessProvider: a.harnessProvider ?? null,
|
|
486
|
+
credStatus: a.credStatus ?? null,
|
|
487
|
+
lastCheckedAt: a.lastUpdatedAt,
|
|
488
|
+
}));
|
|
489
|
+
json(res, { agents });
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (updateAgentCredentialStatusRoute.match(req.method, pathSegments)) {
|
|
494
|
+
const parsed = await updateAgentCredentialStatusRoute.parse(
|
|
495
|
+
req,
|
|
496
|
+
res,
|
|
497
|
+
pathSegments,
|
|
498
|
+
queryParams,
|
|
499
|
+
);
|
|
500
|
+
if (!parsed) return true;
|
|
501
|
+
const agent = updateAgentCredentialState(
|
|
502
|
+
parsed.params.id,
|
|
503
|
+
parsed.body.ready,
|
|
504
|
+
parsed.body.missing ?? null,
|
|
505
|
+
);
|
|
506
|
+
if (!agent) {
|
|
507
|
+
jsonError(res, "Agent not found", 404);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
// Phase 055: persist the richer worker-reported snapshot when sent.
|
|
511
|
+
// We accept `null` to explicitly clear (e.g. on harness change), and
|
|
512
|
+
// `undefined` to leave the existing row value untouched.
|
|
513
|
+
const finalAgent =
|
|
514
|
+
parsed.body.cred_status !== undefined
|
|
515
|
+
? (updateAgentCredStatus(parsed.params.id, parsed.body.cred_status ?? null) ?? agent)
|
|
516
|
+
: agent;
|
|
517
|
+
json(res, agentWithCapacity(finalAgent));
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (getAgentCredentialStatusRoute.match(req.method, pathSegments)) {
|
|
522
|
+
const parsed = await getAgentCredentialStatusRoute.parse(req, res, pathSegments, queryParams);
|
|
523
|
+
if (!parsed) return true;
|
|
524
|
+
const agent = getAgentById(parsed.params.id);
|
|
525
|
+
if (!agent) {
|
|
526
|
+
jsonError(res, "Agent not found", 404);
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
json(res, {
|
|
530
|
+
agentId: agent.id,
|
|
531
|
+
name: agent.name,
|
|
532
|
+
status: agent.status,
|
|
533
|
+
missing: agent.credentialMissing ?? [],
|
|
534
|
+
provider: agent.provider ?? null,
|
|
535
|
+
harnessProvider: agent.harnessProvider ?? null,
|
|
536
|
+
credStatus: agent.credStatus ?? null,
|
|
537
|
+
lastCheckedAt: agent.lastUpdatedAt,
|
|
538
|
+
});
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
352
542
|
if (getAgent.match(req.method, pathSegments)) {
|
|
353
543
|
const parsed = await getAgent.parse(req, res, pathSegments, queryParams);
|
|
354
544
|
if (!parsed) return true;
|
package/src/http/config.ts
CHANGED
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
maskSecrets,
|
|
10
10
|
upsertSwarmConfig,
|
|
11
11
|
} from "../be/db";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isReservedConfigKey,
|
|
14
|
+
reservedKeyError,
|
|
15
|
+
validateConfigValue,
|
|
16
|
+
} from "../be/swarm-config-guard";
|
|
13
17
|
import { reloadGlobalConfigsAndIntegrations } from "./core";
|
|
14
18
|
import { route } from "./route-def";
|
|
15
19
|
import { json, jsonError } from "./utils";
|
|
@@ -230,6 +234,12 @@ export async function handleConfig(
|
|
|
230
234
|
return true;
|
|
231
235
|
}
|
|
232
236
|
|
|
237
|
+
const validationError = validateConfigValue(key, value);
|
|
238
|
+
if (validationError) {
|
|
239
|
+
jsonError(res, validationError, 400);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
233
243
|
try {
|
|
234
244
|
const includeSecrets = queryParams.get("includeSecrets") === "true";
|
|
235
245
|
const config = upsertSwarmConfig({
|
package/src/http/core.ts
CHANGED
|
@@ -272,6 +272,11 @@ export async function handleCore(
|
|
|
272
272
|
|
|
273
273
|
if (agent.status === "busy") {
|
|
274
274
|
status = "busy";
|
|
275
|
+
} else if (agent.status === "waiting_for_credentials") {
|
|
276
|
+
// Preserve the waiting state — only the worker's own credential-wait
|
|
277
|
+
// tick (POST /api/agents/:id/credential-status) clears it once creds
|
|
278
|
+
// resolve. The pinger must not stomp it back to idle.
|
|
279
|
+
status = "waiting_for_credentials";
|
|
275
280
|
}
|
|
276
281
|
|
|
277
282
|
updateAgentStatus(agent.id, status);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { listInboxState, upsertInboxState } from "../be/db";
|
|
4
|
+
import { InboxItemStatusSchema, InboxItemTypeSchema } from "../types";
|
|
5
|
+
import { route } from "./route-def";
|
|
6
|
+
import { json, jsonError } from "./utils";
|
|
7
|
+
|
|
8
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const listState = route({
|
|
11
|
+
method: "get",
|
|
12
|
+
path: "/api/inbox-state",
|
|
13
|
+
pattern: ["api", "inbox-state"],
|
|
14
|
+
summary: "List inbox-item state rows for a user",
|
|
15
|
+
tags: ["Inbox State"],
|
|
16
|
+
query: z.object({
|
|
17
|
+
userId: z.string(),
|
|
18
|
+
status: InboxItemStatusSchema.optional(),
|
|
19
|
+
itemType: InboxItemTypeSchema.optional(),
|
|
20
|
+
}),
|
|
21
|
+
responses: {
|
|
22
|
+
200: { description: "Inbox state rows" },
|
|
23
|
+
400: { description: "Validation error" },
|
|
24
|
+
401: { description: "Unauthorized" },
|
|
25
|
+
},
|
|
26
|
+
auth: { apiKey: true },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const upsertState = route({
|
|
30
|
+
method: "patch",
|
|
31
|
+
path: "/api/inbox-state",
|
|
32
|
+
pattern: ["api", "inbox-state"],
|
|
33
|
+
summary: "Upsert per-user dismiss/snooze/done state for an inbox item",
|
|
34
|
+
tags: ["Inbox State"],
|
|
35
|
+
body: z.object({
|
|
36
|
+
userId: z.string(),
|
|
37
|
+
itemType: InboxItemTypeSchema,
|
|
38
|
+
itemId: z.string().min(1),
|
|
39
|
+
status: InboxItemStatusSchema,
|
|
40
|
+
snoozeUntil: z.string().datetime().optional(),
|
|
41
|
+
}),
|
|
42
|
+
responses: {
|
|
43
|
+
200: { description: "Upserted inbox state row" },
|
|
44
|
+
400: { description: "Validation error" },
|
|
45
|
+
401: { description: "Unauthorized" },
|
|
46
|
+
},
|
|
47
|
+
auth: { apiKey: true },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export async function handleInboxState(
|
|
53
|
+
req: IncomingMessage,
|
|
54
|
+
res: ServerResponse,
|
|
55
|
+
pathSegments: string[],
|
|
56
|
+
queryParams: URLSearchParams,
|
|
57
|
+
): Promise<boolean> {
|
|
58
|
+
if (listState.match(req.method, pathSegments)) {
|
|
59
|
+
const parsed = await listState.parse(req, res, pathSegments, queryParams);
|
|
60
|
+
if (!parsed) return true;
|
|
61
|
+
const items = listInboxState({
|
|
62
|
+
userId: parsed.query.userId,
|
|
63
|
+
status: parsed.query.status,
|
|
64
|
+
itemType: parsed.query.itemType,
|
|
65
|
+
});
|
|
66
|
+
json(res, { items });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (upsertState.match(req.method, pathSegments)) {
|
|
71
|
+
const parsed = await upsertState.parse(req, res, pathSegments, queryParams);
|
|
72
|
+
if (!parsed) return true;
|
|
73
|
+
try {
|
|
74
|
+
const item = upsertInboxState({
|
|
75
|
+
userId: parsed.body.userId,
|
|
76
|
+
itemType: parsed.body.itemType,
|
|
77
|
+
itemId: parsed.body.itemId,
|
|
78
|
+
status: parsed.body.status,
|
|
79
|
+
snoozeUntil: parsed.body.snoozeUntil,
|
|
80
|
+
});
|
|
81
|
+
json(res, { item });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to upsert inbox state", 500);
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return false;
|
|
89
|
+
}
|
package/src/http/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { handleDbQuery } from "./db-query";
|
|
|
29
29
|
import { handleEcosystem } from "./ecosystem";
|
|
30
30
|
import { handleEvents } from "./events";
|
|
31
31
|
import { handleHeartbeat } from "./heartbeat";
|
|
32
|
+
import { handleInboxState } from "./inbox-state";
|
|
32
33
|
import { handleIntegrations } from "./integrations";
|
|
33
34
|
import { handleMcp } from "./mcp";
|
|
34
35
|
import { handleMcpOAuth, startMcpOAuthPendingGc, stopMcpOAuthPendingGc } from "./mcp-oauth";
|
|
@@ -40,10 +41,14 @@ import { handlePromptTemplates } from "./prompt-templates";
|
|
|
40
41
|
import { handleRepos } from "./repos";
|
|
41
42
|
import { handleSchedules } from "./schedules";
|
|
42
43
|
import { handleSessionData } from "./session-data";
|
|
44
|
+
import { handleSessions } from "./sessions";
|
|
43
45
|
import { handleSkills } from "./skills";
|
|
44
46
|
import { handleStats } from "./stats";
|
|
47
|
+
import { handleStatus } from "./status";
|
|
48
|
+
import { handleTaskTemplates } from "./task-templates";
|
|
45
49
|
import { handleTasks } from "./tasks";
|
|
46
50
|
import { handleTrackers } from "./trackers";
|
|
51
|
+
import { handleUsers } from "./users";
|
|
47
52
|
import { getPathSegments, parseQueryParams, setCorsHeaders } from "./utils";
|
|
48
53
|
import { handleWebhooks } from "./webhooks";
|
|
49
54
|
import { handleWorkflowEvents } from "./workflow-events";
|
|
@@ -126,6 +131,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
126
131
|
() => handleContext(req, res, pathSegments, queryParams, myAgentId),
|
|
127
132
|
() => handleTasks(req, res, pathSegments, queryParams, myAgentId),
|
|
128
133
|
() => handleStats(req, res, pathSegments, queryParams),
|
|
134
|
+
() => handleStatus(req, res, pathSegments, queryParams),
|
|
129
135
|
() => handleActiveSessions(req, res, pathSegments, queryParams, myAgentId),
|
|
130
136
|
() => handlePricing(req, res, pathSegments, queryParams, myAgentId),
|
|
131
137
|
() => handleSchedules(req, res, pathSegments, queryParams, myAgentId),
|
|
@@ -144,6 +150,10 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
144
150
|
() => handleApiKeys(req, res, pathSegments, queryParams),
|
|
145
151
|
() => handleHeartbeat(req, res, pathSegments),
|
|
146
152
|
() => handleEvents(req, res, pathSegments, queryParams, myAgentId),
|
|
153
|
+
() => handleUsers(req, res, pathSegments, queryParams),
|
|
154
|
+
() => handleSessions(req, res, pathSegments, queryParams),
|
|
155
|
+
() => handleInboxState(req, res, pathSegments, queryParams),
|
|
156
|
+
() => handleTaskTemplates(req, res, pathSegments, queryParams),
|
|
147
157
|
() => handleMcp(req, res, transports),
|
|
148
158
|
];
|
|
149
159
|
|