@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/commands/runner.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
generateDefaultSoulMd,
|
|
10
10
|
generateDefaultToolsMd,
|
|
11
11
|
} from "../prompts/defaults.ts";
|
|
12
|
+
import { renderMemoriesPrompt } from "../prompts/memories.ts";
|
|
12
13
|
import { configureHttpResolver, resolveTemplateAsync } from "../prompts/resolver.ts";
|
|
13
14
|
import { authJsonToCredentialSelection } from "../providers/codex-oauth/auth-json.js";
|
|
14
15
|
import {
|
|
@@ -24,10 +25,17 @@ import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
|
|
|
24
25
|
import { getContextWindowSize } from "../utils/context-window.ts";
|
|
25
26
|
import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
|
|
26
27
|
import { parseRateLimitResetTime } from "../utils/error-tracker.ts";
|
|
28
|
+
import { resolveHarnessProvider } from "../utils/harness-provider.ts";
|
|
27
29
|
import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
|
|
28
30
|
import { scrubSecrets } from "../utils/secret-scrubber.ts";
|
|
29
31
|
import { detectVcsProvider } from "../vcs/index.ts";
|
|
30
32
|
import { interpolate } from "../workflows/template.ts";
|
|
33
|
+
import { awaitCredentials, BootMaxWaitExceededError, EX_CONFIG } from "./credential-wait.ts";
|
|
34
|
+
import {
|
|
35
|
+
buildCredStatusReport,
|
|
36
|
+
isCredCheckDisabled,
|
|
37
|
+
reportCredStatus,
|
|
38
|
+
} from "./provider-credentials.ts";
|
|
31
39
|
// Side-effect import: registers runner trigger/resumption templates
|
|
32
40
|
import "./templates.ts";
|
|
33
41
|
|
|
@@ -202,6 +210,13 @@ async function closeAgent(config: ApiConfig, role: string): Promise<void> {
|
|
|
202
210
|
interface ResolvedEnvResult {
|
|
203
211
|
env: Record<string, string | undefined>;
|
|
204
212
|
credentialSelections: CredentialSelection[];
|
|
213
|
+
/**
|
|
214
|
+
* Effective `HARNESS_PROVIDER` after layering swarm_config over the base
|
|
215
|
+
* env. Callers should prefer this over `process.env.HARNESS_PROVIDER` so
|
|
216
|
+
* that an operator's swarm_config row (repo > agent > global) actually
|
|
217
|
+
* takes effect on the worker.
|
|
218
|
+
*/
|
|
219
|
+
resolvedProvider: ProviderName;
|
|
205
220
|
}
|
|
206
221
|
|
|
207
222
|
async function fetchResolvedEnv(
|
|
@@ -239,6 +254,8 @@ async function fetchResolvedEnv(
|
|
|
239
254
|
}
|
|
240
255
|
}
|
|
241
256
|
|
|
257
|
+
const resolvedProvider = resolveHarnessProvider(env, baseEnv);
|
|
258
|
+
|
|
242
259
|
const credentialSelections = await resolveCredentialPools(env, {
|
|
243
260
|
apiUrl,
|
|
244
261
|
apiKey,
|
|
@@ -246,10 +263,13 @@ async function fetchResolvedEnv(
|
|
|
246
263
|
// CLAUDE_CODE_OAUTH_TOKEN stamped on their task record (and vice
|
|
247
264
|
// versa) just because both env vars happen to be set in the worker
|
|
248
265
|
// container. See `PROVIDER_CREDENTIAL_VARS` in src/utils/credentials.ts.
|
|
249
|
-
|
|
266
|
+
//
|
|
267
|
+
// Use the resolved provider (swarm_config > env) so an operator can flip
|
|
268
|
+
// the worker's harness from the dashboard without restarting the container.
|
|
269
|
+
provider: resolvedProvider,
|
|
250
270
|
});
|
|
251
271
|
|
|
252
|
-
return { env, credentialSelections };
|
|
272
|
+
return { env, credentialSelections, resolvedProvider };
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
/** Tools that produce noise — skip auto-progress for these */
|
|
@@ -533,6 +553,12 @@ export async function ensureTaskFinished(
|
|
|
533
553
|
exitCode: number,
|
|
534
554
|
failureReason?: string,
|
|
535
555
|
providerOutput?: string,
|
|
556
|
+
/**
|
|
557
|
+
* Active provider for this task. When provided, gates the structured-output
|
|
558
|
+
* fallback path correctly even if `process.env.HARNESS_PROVIDER` differs
|
|
559
|
+
* from the resolved swarm_config value. Falls back to env when omitted.
|
|
560
|
+
*/
|
|
561
|
+
provider?: ProviderName,
|
|
536
562
|
): Promise<void> {
|
|
537
563
|
const headers: Record<string, string> = {
|
|
538
564
|
"X-Agent-ID": config.agentId,
|
|
@@ -558,7 +584,7 @@ export async function ensureTaskFinished(
|
|
|
558
584
|
body.output = providerOutput;
|
|
559
585
|
} else {
|
|
560
586
|
// Try structured output fallback if the task has an outputSchema
|
|
561
|
-
const adapterType = process.env.HARNESS_PROVIDER
|
|
587
|
+
const adapterType = provider ?? process.env.HARNESS_PROVIDER ?? "claude";
|
|
562
588
|
const fallback = await handleStructuredOutputFallback(config, taskId, adapterType);
|
|
563
589
|
|
|
564
590
|
console.log(`[${role}] Task ${taskId.slice(0, 8)} fallback result: ${fallback.kind}`);
|
|
@@ -890,7 +916,15 @@ function setupShutdownHandlers(
|
|
|
890
916
|
console.warn(
|
|
891
917
|
`[${role}] Failed to pause task ${taskId.slice(0, 8)}, marking as failed instead`,
|
|
892
918
|
);
|
|
893
|
-
await ensureTaskFinished(
|
|
919
|
+
await ensureTaskFinished(
|
|
920
|
+
apiConfig,
|
|
921
|
+
role,
|
|
922
|
+
taskId,
|
|
923
|
+
1,
|
|
924
|
+
undefined,
|
|
925
|
+
undefined,
|
|
926
|
+
state.harnessProvider,
|
|
927
|
+
);
|
|
894
928
|
}
|
|
895
929
|
}
|
|
896
930
|
}
|
|
@@ -958,6 +992,14 @@ interface RunningTask {
|
|
|
958
992
|
interface RunnerState {
|
|
959
993
|
activeTasks: Map<string, RunningTask>;
|
|
960
994
|
maxConcurrent: number;
|
|
995
|
+
/**
|
|
996
|
+
* Effective harness provider for this worker boot session — resolved
|
|
997
|
+
* from `swarm_config` (overlay) > `process.env.HARNESS_PROVIDER` > "claude".
|
|
998
|
+
* Used by error / cleanup paths so the structured-output fallback runs the
|
|
999
|
+
* right adapter even when env disagrees with swarm_config. Section 4
|
|
1000
|
+
* (per-task live re-resolution) will mutate this between tasks.
|
|
1001
|
+
*/
|
|
1002
|
+
harnessProvider: ProviderName;
|
|
961
1003
|
}
|
|
962
1004
|
|
|
963
1005
|
/** Buffer for session logs */
|
|
@@ -1335,6 +1377,13 @@ async function registerAgent(opts: {
|
|
|
1335
1377
|
role?: string;
|
|
1336
1378
|
capabilities?: string[];
|
|
1337
1379
|
maxTasks?: number;
|
|
1380
|
+
/**
|
|
1381
|
+
* Resolved harness provider (swarm_config > env > "claude"). Sent as both
|
|
1382
|
+
* the legacy `provider` field and the canonical `harness_provider` column.
|
|
1383
|
+
* Defaults to `process.env.HARNESS_PROVIDER || "claude"` for callers that
|
|
1384
|
+
* haven't migrated to passing it explicitly.
|
|
1385
|
+
*/
|
|
1386
|
+
harnessProvider?: ProviderName;
|
|
1338
1387
|
}): Promise<void> {
|
|
1339
1388
|
const headers: Record<string, string> = {
|
|
1340
1389
|
"Content-Type": "application/json",
|
|
@@ -1344,7 +1393,16 @@ async function registerAgent(opts: {
|
|
|
1344
1393
|
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
1345
1394
|
}
|
|
1346
1395
|
|
|
1347
|
-
const provider =
|
|
1396
|
+
const provider: ProviderName =
|
|
1397
|
+
opts.harnessProvider ?? ((process.env.HARNESS_PROVIDER || "claude") as ProviderName);
|
|
1398
|
+
|
|
1399
|
+
// Phase 1.5 (cloud-personalization): also push the canonical
|
|
1400
|
+
// `harness_provider` field so the API can persist it in its own column
|
|
1401
|
+
// (`agents.harness_provider`). Always send the resolved provider value
|
|
1402
|
+
// (defaulting to "claude" when HARNESS_PROVIDER is unset) so agents that
|
|
1403
|
+
// don't explicitly set the env var still self-report instead of leaving
|
|
1404
|
+
// the column NULL — matches how `provider` already defaults above.
|
|
1405
|
+
const harnessProvider: ProviderName = provider;
|
|
1348
1406
|
|
|
1349
1407
|
const response = await fetch(`${opts.apiUrl}/api/agents`, {
|
|
1350
1408
|
method: "POST",
|
|
@@ -1356,6 +1414,7 @@ async function registerAgent(opts: {
|
|
|
1356
1414
|
capabilities: opts.capabilities,
|
|
1357
1415
|
maxTasks: opts.maxTasks,
|
|
1358
1416
|
provider,
|
|
1417
|
+
harness_provider: harnessProvider,
|
|
1359
1418
|
}),
|
|
1360
1419
|
});
|
|
1361
1420
|
|
|
@@ -1543,6 +1602,7 @@ async function fetchRelevantMemories(
|
|
|
1543
1602
|
apiKey: string,
|
|
1544
1603
|
agentId: string,
|
|
1545
1604
|
taskDescription: string,
|
|
1605
|
+
taskId?: string,
|
|
1546
1606
|
): Promise<string | null> {
|
|
1547
1607
|
try {
|
|
1548
1608
|
const headers: Record<string, string> = {
|
|
@@ -1550,6 +1610,11 @@ async function fetchRelevantMemories(
|
|
|
1550
1610
|
"X-Agent-ID": agentId,
|
|
1551
1611
|
};
|
|
1552
1612
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
1613
|
+
// Memory rater v1.5: server uses this header to log `memory_retrieval`
|
|
1614
|
+
// rows so server-side raters (ImplicitCitationRater) can score the
|
|
1615
|
+
// memories they surface against this task's session_logs at completion.
|
|
1616
|
+
// Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-2.md §2
|
|
1617
|
+
if (taskId) headers["X-Source-Task-ID"] = taskId;
|
|
1553
1618
|
|
|
1554
1619
|
const response = await fetch(`${apiUrl}/api/memory/search`, {
|
|
1555
1620
|
method: "POST",
|
|
@@ -1563,14 +1628,7 @@ async function fetchRelevantMemories(
|
|
|
1563
1628
|
results: Array<{ id: string; name: string; content: string; similarity: number }>;
|
|
1564
1629
|
};
|
|
1565
1630
|
|
|
1566
|
-
|
|
1567
|
-
if (useful.length === 0) return null;
|
|
1568
|
-
|
|
1569
|
-
const memoryContext = useful
|
|
1570
|
-
.map((m) => `- **${m.name}** (id: ${m.id}): ${m.content.substring(0, 300)}`)
|
|
1571
|
-
.join("\n");
|
|
1572
|
-
|
|
1573
|
-
return `\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`;
|
|
1631
|
+
return renderMemoriesPrompt(data.results || []);
|
|
1574
1632
|
} catch {
|
|
1575
1633
|
// Non-blocking — don't fail task start because of memory search
|
|
1576
1634
|
return null;
|
|
@@ -2185,6 +2243,7 @@ async function checkCompletedProcesses(
|
|
|
2185
2243
|
result.exitCode,
|
|
2186
2244
|
failureReason,
|
|
2187
2245
|
result.output,
|
|
2246
|
+
state.harnessProvider,
|
|
2188
2247
|
);
|
|
2189
2248
|
|
|
2190
2249
|
ensure({
|
|
@@ -2295,9 +2354,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2295
2354
|
// Initialize Business-Use SDK for worker-side instrumentation
|
|
2296
2355
|
initialize();
|
|
2297
2356
|
|
|
2298
|
-
// Create provider adapter based on HARNESS_PROVIDER env var (default: claude)
|
|
2299
|
-
const adapter = createProviderAdapter(process.env.HARNESS_PROVIDER || "claude");
|
|
2300
|
-
|
|
2301
2357
|
const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
|
|
2302
2358
|
const baseLogDir = opts.logsDir || process.env.LOG_DIR || "/logs";
|
|
2303
2359
|
const logDir = `${baseLogDir}/${sessionId}`;
|
|
@@ -2312,6 +2368,30 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2312
2368
|
|
|
2313
2369
|
const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
|
|
2314
2370
|
const swarmUrl = process.env.SWARM_URL || "localhost";
|
|
2371
|
+
const apiKey = process.env.API_KEY || "";
|
|
2372
|
+
|
|
2373
|
+
// Resolve the boot harness provider from swarm_config (repo > agent > global,
|
|
2374
|
+
// overlaid on top of `process.env`). This is what selects the adapter for
|
|
2375
|
+
// this worker's lifetime. On a fresh worker (agentId="unknown") only global
|
|
2376
|
+
// swarm_config applies; once registered, an operator writing an agent-scoped
|
|
2377
|
+
// HARNESS_PROVIDER row takes effect on the next reconciliation cycle (Section 4)
|
|
2378
|
+
// or worker restart.
|
|
2379
|
+
//
|
|
2380
|
+
// Failures (network, API down, malformed value) fall back to env then "claude"
|
|
2381
|
+
// so a swarm_config outage cannot wedge boot.
|
|
2382
|
+
let bootProvider: ProviderName;
|
|
2383
|
+
try {
|
|
2384
|
+
bootProvider = (await fetchResolvedEnv(apiUrl, apiKey, agentId)).resolvedProvider;
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
console.warn(`[runner] fetchResolvedEnv failed at boot, falling back to env: ${err}`);
|
|
2387
|
+
bootProvider = resolveHarnessProvider({}, process.env);
|
|
2388
|
+
}
|
|
2389
|
+
console.log(`[runner] Resolved HARNESS_PROVIDER: ${bootProvider}`);
|
|
2390
|
+
|
|
2391
|
+
// Create provider adapter using the resolved value. `let` so the poll-loop
|
|
2392
|
+
// reconciliation block (Section 4) can swap it live when an operator changes
|
|
2393
|
+
// HARNESS_PROVIDER in swarm_config — call sites read the current binding.
|
|
2394
|
+
let adapter = createProviderAdapter(bootProvider);
|
|
2315
2395
|
|
|
2316
2396
|
// Configure HTTP-based template resolution (workers resolve via API, not local DB)
|
|
2317
2397
|
if (process.env.API_KEY) {
|
|
@@ -2382,9 +2462,11 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2382
2462
|
// Slack context for current task (gates Slack instructions in prompt)
|
|
2383
2463
|
let currentTaskSlackContext: BasePromptArgs["slackContext"] | undefined;
|
|
2384
2464
|
|
|
2385
|
-
// Generate base prompt (identity fields injected after profile fetch below)
|
|
2386
|
-
|
|
2465
|
+
// Generate base prompt (identity fields injected after profile fetch below).
|
|
2466
|
+
// Traits are read fresh on each call so a live adapter swap (Section 4)
|
|
2467
|
+
// produces a prompt matching the new provider's capabilities.
|
|
2387
2468
|
const buildSystemPrompt = async () => {
|
|
2469
|
+
const { traits } = adapter;
|
|
2388
2470
|
return getBasePrompt({
|
|
2389
2471
|
role,
|
|
2390
2472
|
agentId,
|
|
@@ -2460,7 +2542,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2460
2542
|
console.log(`[${role}] Total system prompt length: ${resolvedSystemPrompt.length} chars`);
|
|
2461
2543
|
|
|
2462
2544
|
const isAiLoop = opts.aiLoop || process.env.AI_LOOP === "true";
|
|
2463
|
-
const apiKey = process.env.API_KEY || "";
|
|
2464
2545
|
|
|
2465
2546
|
// Constants for polling
|
|
2466
2547
|
const PollIntervalMs = 2000; // 2 seconds between polls
|
|
@@ -2508,11 +2589,27 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2508
2589
|
const state: RunnerState = {
|
|
2509
2590
|
activeTasks: new Map(),
|
|
2510
2591
|
maxConcurrent,
|
|
2592
|
+
harnessProvider: bootProvider,
|
|
2511
2593
|
};
|
|
2512
2594
|
|
|
2513
2595
|
// Track tasks already signaled for cancellation to avoid repeated SIGTERM
|
|
2514
2596
|
const cancelledSignaled = new Set<string>();
|
|
2515
2597
|
|
|
2598
|
+
// Migration 055 — cache the harness_provider value used when we last
|
|
2599
|
+
// built a `cred_status` snapshot. Re-runs the post-task check only when
|
|
2600
|
+
// the resolved provider changes. Section 4 of the swarm_config-overrides-
|
|
2601
|
+
// HARNESS_PROVIDER work makes this dynamic: state.harnessProvider is
|
|
2602
|
+
// reconciled below from `swarm_config`, so an operator's change reaches
|
|
2603
|
+
// here without a worker restart.
|
|
2604
|
+
let cachedCredHarnessProvider: string | null = null;
|
|
2605
|
+
|
|
2606
|
+
// Throttle for live HARNESS_PROVIDER reconciliation. Each reconciliation
|
|
2607
|
+
// calls `fetchResolvedEnv` which also re-resolves credential pools — we
|
|
2608
|
+
// don't want that on every 2s poll. 10s gives operator changes a near-
|
|
2609
|
+
// immediate effect from a UX perspective without hammering the API.
|
|
2610
|
+
let lastHarnessReconcileAt = 0;
|
|
2611
|
+
const HARNESS_RECONCILE_INTERVAL_MS = 10_000;
|
|
2612
|
+
|
|
2516
2613
|
// Create API config for ping/close
|
|
2517
2614
|
const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
|
|
2518
2615
|
|
|
@@ -2534,6 +2631,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2534
2631
|
isLead,
|
|
2535
2632
|
capabilities,
|
|
2536
2633
|
maxTasks: maxConcurrent,
|
|
2634
|
+
harnessProvider: bootProvider,
|
|
2537
2635
|
});
|
|
2538
2636
|
console.log(`[${role}] Registered as "${agentName}" (ID: ${agentId})`);
|
|
2539
2637
|
} catch (error) {
|
|
@@ -2541,6 +2639,65 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2541
2639
|
process.exit(1);
|
|
2542
2640
|
}
|
|
2543
2641
|
|
|
2642
|
+
// Block until harness credentials are present in env. This loop replaces
|
|
2643
|
+
// the old bash-level fail-fast in `docker-entrypoint.sh` — the worker is
|
|
2644
|
+
// already registered (visible to the dashboard) and self-heals once
|
|
2645
|
+
// creds appear in `swarm_config`. See plans/2026-05-06-worker-credential-safe-loop.md.
|
|
2646
|
+
//
|
|
2647
|
+
// CRED_CHECK_DISABLE=1 opts out entirely: the worker trusts the operator
|
|
2648
|
+
// and starts polling immediately, with a NULL `cred_status` row that the
|
|
2649
|
+
// dashboard surfaces as "unreported."
|
|
2650
|
+
const harnessProvider = bootProvider;
|
|
2651
|
+
cachedCredHarnessProvider = harnessProvider;
|
|
2652
|
+
if (isCredCheckDisabled(process.env)) {
|
|
2653
|
+
console.log(`[${role}] CRED_CHECK_DISABLE=1, skipping credential checks`);
|
|
2654
|
+
} else {
|
|
2655
|
+
try {
|
|
2656
|
+
await awaitCredentials({
|
|
2657
|
+
provider: harnessProvider,
|
|
2658
|
+
refreshEnv: async () => {
|
|
2659
|
+
const { env } = await fetchResolvedEnv(apiUrl, apiKey, agentId);
|
|
2660
|
+
return env;
|
|
2661
|
+
},
|
|
2662
|
+
onTick: (status) => {
|
|
2663
|
+
// Best-effort status report — the dispatcher uses it to route
|
|
2664
|
+
// around blocked agents. Failures are non-fatal (the wait loop
|
|
2665
|
+
// already swallows onTick exceptions). We do NOT include
|
|
2666
|
+
// `cred_status` here — the live test runs once the worker is
|
|
2667
|
+
// ready (below), and intermediate ticks are presence-only.
|
|
2668
|
+
fetch(`${apiUrl}/api/agents/${encodeURIComponent(agentId)}/credential-status`, {
|
|
2669
|
+
method: "PUT",
|
|
2670
|
+
headers: {
|
|
2671
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2672
|
+
"X-Agent-ID": agentId,
|
|
2673
|
+
"Content-Type": "application/json",
|
|
2674
|
+
},
|
|
2675
|
+
body: JSON.stringify({ ready: status.ready, missing: status.missing }),
|
|
2676
|
+
}).catch(() => {
|
|
2677
|
+
// Swallowed — Phase 2 wait loop logs every tick anyway.
|
|
2678
|
+
});
|
|
2679
|
+
},
|
|
2680
|
+
});
|
|
2681
|
+
} catch (err) {
|
|
2682
|
+
if (err instanceof BootMaxWaitExceededError) {
|
|
2683
|
+
console.error(`[${role}] ${err.message}`);
|
|
2684
|
+
process.exit(EX_CONFIG);
|
|
2685
|
+
}
|
|
2686
|
+
throw err;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// Migration 055: build the full snapshot (presence + live test) once
|
|
2690
|
+
// creds are ready and POST it to the agent row. Status endpoint reads
|
|
2691
|
+
// this instead of running predicates server-side.
|
|
2692
|
+
try {
|
|
2693
|
+
const snapshot = await buildCredStatusReport(harnessProvider, process.env, {}, "boot");
|
|
2694
|
+
await reportCredStatus(apiUrl, apiKey, agentId, snapshot);
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
// Non-fatal — worker proceeds even if reporting fails.
|
|
2697
|
+
console.warn(`[${role}] cred_status boot report failed (non-fatal): ${err}`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2544
2701
|
// Clean up any stale active sessions from previous runs (crash recovery)
|
|
2545
2702
|
await cleanupActiveSessions(apiConfig);
|
|
2546
2703
|
console.log(`[${role}] Cleaned up stale active sessions`);
|
|
@@ -2865,6 +3022,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2865
3022
|
apiKey,
|
|
2866
3023
|
agentId,
|
|
2867
3024
|
task.task,
|
|
3025
|
+
task.id,
|
|
2868
3026
|
);
|
|
2869
3027
|
if (resumeMemoryContext) {
|
|
2870
3028
|
resumePrompt += resumeMemoryContext;
|
|
@@ -2970,7 +3128,15 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2970
3128
|
console.error(
|
|
2971
3129
|
`[${role}] Failed to spawn process for resumed task ${task.id.slice(0, 8)}: ${errMsg}`,
|
|
2972
3130
|
);
|
|
2973
|
-
await ensureTaskFinished(
|
|
3131
|
+
await ensureTaskFinished(
|
|
3132
|
+
apiConfig,
|
|
3133
|
+
role,
|
|
3134
|
+
task.id,
|
|
3135
|
+
1,
|
|
3136
|
+
`Spawn failed: ${errMsg}`,
|
|
3137
|
+
undefined,
|
|
3138
|
+
state.harnessProvider,
|
|
3139
|
+
);
|
|
2974
3140
|
continue;
|
|
2975
3141
|
}
|
|
2976
3142
|
|
|
@@ -3020,6 +3186,64 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3020
3186
|
// Check for completed processes first and ensure tasks are marked as finished
|
|
3021
3187
|
await checkCompletedProcesses(state, role, apiConfig);
|
|
3022
3188
|
|
|
3189
|
+
// Live HARNESS_PROVIDER reconciliation. Re-fetches `swarm_config` (overlaid
|
|
3190
|
+
// on env) and swaps the adapter if the resolved provider changed —
|
|
3191
|
+
// typically because an operator PATCH'd /api/agents/:id/harness-provider
|
|
3192
|
+
// (which writes a swarm_config row) or upserted a config row directly.
|
|
3193
|
+
//
|
|
3194
|
+
// Safety: in-flight sessions hold their own `ProviderSession` references
|
|
3195
|
+
// and continue on the old adapter unaffected. New spawns (below) read
|
|
3196
|
+
// the current `adapter` binding and pick up the swap. `basePrompt` is
|
|
3197
|
+
// rebuilt because traits (and therefore prompt content) may differ across
|
|
3198
|
+
// providers.
|
|
3199
|
+
if (Date.now() - lastHarnessReconcileAt > HARNESS_RECONCILE_INTERVAL_MS) {
|
|
3200
|
+
lastHarnessReconcileAt = Date.now();
|
|
3201
|
+
try {
|
|
3202
|
+
const { resolvedProvider } = await fetchResolvedEnv(apiUrl, apiKey, agentId);
|
|
3203
|
+
if (resolvedProvider !== state.harnessProvider) {
|
|
3204
|
+
const previous = state.harnessProvider;
|
|
3205
|
+
console.log(
|
|
3206
|
+
`[${role}] [harness] Reconciling adapter: ${previous} → ${resolvedProvider}`,
|
|
3207
|
+
);
|
|
3208
|
+
try {
|
|
3209
|
+
adapter = createProviderAdapter(resolvedProvider);
|
|
3210
|
+
state.harnessProvider = resolvedProvider;
|
|
3211
|
+
basePrompt = await buildSystemPrompt();
|
|
3212
|
+
resolvedSystemPrompt = additionalSystemPrompt
|
|
3213
|
+
? `${basePrompt}\n\n${additionalSystemPrompt}`
|
|
3214
|
+
: basePrompt;
|
|
3215
|
+
// Force a fresh cred_status report below for the new provider.
|
|
3216
|
+
cachedCredHarnessProvider = null;
|
|
3217
|
+
console.log(
|
|
3218
|
+
`[${role}] [harness] Swapped to ${resolvedProvider} (basePrompt rebuilt: ${basePrompt.length} chars)`,
|
|
3219
|
+
);
|
|
3220
|
+
} catch (err) {
|
|
3221
|
+
console.warn(
|
|
3222
|
+
`[${role}] [harness] Failed to swap to ${resolvedProvider} (staying on ${previous}): ${err}`,
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
} catch (err) {
|
|
3227
|
+
console.warn(`[${role}] [harness] Reconcile fetch failed (non-fatal): ${err}`);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// Migration 055 — post-task credential refresh, cache-keyed on the
|
|
3232
|
+
// *resolved* harness_provider. Re-runs the snapshot when the provider
|
|
3233
|
+
// changes (boot, or after a live swap above) so the dashboard shows
|
|
3234
|
+
// up-to-date credential status for the active adapter.
|
|
3235
|
+
if (!isCredCheckDisabled(process.env)) {
|
|
3236
|
+
const currentHarness = state.harnessProvider;
|
|
3237
|
+
if (currentHarness !== cachedCredHarnessProvider) {
|
|
3238
|
+
cachedCredHarnessProvider = currentHarness;
|
|
3239
|
+
buildCredStatusReport(currentHarness, process.env, {}, "post_task")
|
|
3240
|
+
.then((snap) => reportCredStatus(apiUrl, apiKey, agentId, snap))
|
|
3241
|
+
.catch((err) =>
|
|
3242
|
+
console.warn(`[${role}] cred_status post_task report failed (non-fatal): ${err}`),
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3023
3247
|
// Periodic VCS detection for running tasks (fire-and-forget, throttled per task)
|
|
3024
3248
|
const now = Date.now();
|
|
3025
3249
|
for (const [taskId, task] of state.activeTasks) {
|
|
@@ -3150,7 +3374,13 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3150
3374
|
? (trigger.task as { task: string; id?: string })
|
|
3151
3375
|
: null;
|
|
3152
3376
|
if (task?.task) {
|
|
3153
|
-
const memoryContext = await fetchRelevantMemories(
|
|
3377
|
+
const memoryContext = await fetchRelevantMemories(
|
|
3378
|
+
apiUrl,
|
|
3379
|
+
apiKey,
|
|
3380
|
+
agentId,
|
|
3381
|
+
task.task,
|
|
3382
|
+
task.id,
|
|
3383
|
+
);
|
|
3154
3384
|
if (memoryContext) {
|
|
3155
3385
|
triggerPrompt += memoryContext;
|
|
3156
3386
|
console.log(`[${role}] Injected relevant memories into task prompt`);
|
|
@@ -3320,6 +3550,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3320
3550
|
trigger.taskId,
|
|
3321
3551
|
1,
|
|
3322
3552
|
`Spawn failed: ${errMsg}`,
|
|
3553
|
+
undefined,
|
|
3554
|
+
state.harnessProvider,
|
|
3323
3555
|
);
|
|
3324
3556
|
}
|
|
3325
3557
|
continue;
|