@desplega.ai/agent-swarm 1.77.1 → 1.77.3
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/openapi.json +171 -5
- package/package.json +1 -1
- package/src/be/db.ts +14 -4
- package/src/commands/provider-credentials.ts +52 -1
- package/src/commands/runner.ts +33 -1
- package/src/http/agents.ts +113 -11
- package/src/tests/agents-harness-provider.test.ts +43 -2
- package/src/tests/credential-status-api.test.ts +40 -1
- package/src/types.ts +10 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.77.
|
|
5
|
+
"version": "1.77.3",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -448,6 +448,73 @@
|
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
450
|
},
|
|
451
|
+
"/api/agents/{id}/runtime": {
|
|
452
|
+
"patch": {
|
|
453
|
+
"summary": "Update an agent's runtime harness and default model",
|
|
454
|
+
"description": "Updates `agents.harness_provider` and upserts agent-scoped `swarm_config` rows for HARNESS_PROVIDER and MODEL_OVERRIDE. The settings apply to future provider sessions.",
|
|
455
|
+
"tags": [
|
|
456
|
+
"Agents"
|
|
457
|
+
],
|
|
458
|
+
"security": [
|
|
459
|
+
{
|
|
460
|
+
"bearerAuth": []
|
|
461
|
+
}
|
|
462
|
+
],
|
|
463
|
+
"parameters": [
|
|
464
|
+
{
|
|
465
|
+
"schema": {
|
|
466
|
+
"type": "string"
|
|
467
|
+
},
|
|
468
|
+
"required": true,
|
|
469
|
+
"name": "id",
|
|
470
|
+
"in": "path"
|
|
471
|
+
}
|
|
472
|
+
],
|
|
473
|
+
"requestBody": {
|
|
474
|
+
"content": {
|
|
475
|
+
"application/json": {
|
|
476
|
+
"schema": {
|
|
477
|
+
"type": "object",
|
|
478
|
+
"properties": {
|
|
479
|
+
"harness_provider": {
|
|
480
|
+
"type": "string",
|
|
481
|
+
"enum": [
|
|
482
|
+
"claude",
|
|
483
|
+
"codex",
|
|
484
|
+
"pi",
|
|
485
|
+
"opencode"
|
|
486
|
+
]
|
|
487
|
+
},
|
|
488
|
+
"model": {
|
|
489
|
+
"type": "string",
|
|
490
|
+
"minLength": 1
|
|
491
|
+
},
|
|
492
|
+
"allow_custom_model": {
|
|
493
|
+
"type": "boolean",
|
|
494
|
+
"default": false
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
"required": [
|
|
498
|
+
"harness_provider",
|
|
499
|
+
"model"
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
"responses": {
|
|
506
|
+
"200": {
|
|
507
|
+
"description": "Updated agent row"
|
|
508
|
+
},
|
|
509
|
+
"400": {
|
|
510
|
+
"description": "Validation error"
|
|
511
|
+
},
|
|
512
|
+
"404": {
|
|
513
|
+
"description": "Agent not found"
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
},
|
|
451
518
|
"/api/agents/{id}/name": {
|
|
452
519
|
"put": {
|
|
453
520
|
"summary": "Update agent name",
|
|
@@ -797,6 +864,59 @@
|
|
|
797
864
|
"testedAt"
|
|
798
865
|
]
|
|
799
866
|
},
|
|
867
|
+
"latestModel": {
|
|
868
|
+
"type": [
|
|
869
|
+
"object",
|
|
870
|
+
"null"
|
|
871
|
+
],
|
|
872
|
+
"properties": {
|
|
873
|
+
"model": {
|
|
874
|
+
"type": "string",
|
|
875
|
+
"minLength": 1
|
|
876
|
+
},
|
|
877
|
+
"source": {
|
|
878
|
+
"type": "string",
|
|
879
|
+
"enum": [
|
|
880
|
+
"task",
|
|
881
|
+
"agent_config",
|
|
882
|
+
"adapter_default",
|
|
883
|
+
"custom"
|
|
884
|
+
]
|
|
885
|
+
},
|
|
886
|
+
"taskId": {
|
|
887
|
+
"type": [
|
|
888
|
+
"string",
|
|
889
|
+
"null"
|
|
890
|
+
],
|
|
891
|
+
"default": null
|
|
892
|
+
},
|
|
893
|
+
"harnessProvider": {
|
|
894
|
+
"type": [
|
|
895
|
+
"string",
|
|
896
|
+
"null"
|
|
897
|
+
],
|
|
898
|
+
"enum": [
|
|
899
|
+
"claude",
|
|
900
|
+
"codex",
|
|
901
|
+
"pi",
|
|
902
|
+
"devin",
|
|
903
|
+
"claude-managed",
|
|
904
|
+
"opencode",
|
|
905
|
+
null
|
|
906
|
+
],
|
|
907
|
+
"default": null
|
|
908
|
+
},
|
|
909
|
+
"reportedAt": {
|
|
910
|
+
"type": "number"
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
"default": null,
|
|
914
|
+
"required": [
|
|
915
|
+
"model",
|
|
916
|
+
"source",
|
|
917
|
+
"reportedAt"
|
|
918
|
+
]
|
|
919
|
+
},
|
|
800
920
|
"reportedAt": {
|
|
801
921
|
"type": "number"
|
|
802
922
|
},
|
|
@@ -813,11 +933,57 @@
|
|
|
813
933
|
"ready",
|
|
814
934
|
"reportedAt"
|
|
815
935
|
]
|
|
936
|
+
},
|
|
937
|
+
"latest_model": {
|
|
938
|
+
"type": "object",
|
|
939
|
+
"properties": {
|
|
940
|
+
"model": {
|
|
941
|
+
"type": "string",
|
|
942
|
+
"minLength": 1
|
|
943
|
+
},
|
|
944
|
+
"source": {
|
|
945
|
+
"type": "string",
|
|
946
|
+
"enum": [
|
|
947
|
+
"task",
|
|
948
|
+
"agent_config",
|
|
949
|
+
"adapter_default",
|
|
950
|
+
"custom"
|
|
951
|
+
]
|
|
952
|
+
},
|
|
953
|
+
"taskId": {
|
|
954
|
+
"type": [
|
|
955
|
+
"string",
|
|
956
|
+
"null"
|
|
957
|
+
],
|
|
958
|
+
"default": null
|
|
959
|
+
},
|
|
960
|
+
"harnessProvider": {
|
|
961
|
+
"type": [
|
|
962
|
+
"string",
|
|
963
|
+
"null"
|
|
964
|
+
],
|
|
965
|
+
"enum": [
|
|
966
|
+
"claude",
|
|
967
|
+
"codex",
|
|
968
|
+
"pi",
|
|
969
|
+
"devin",
|
|
970
|
+
"claude-managed",
|
|
971
|
+
"opencode",
|
|
972
|
+
null
|
|
973
|
+
],
|
|
974
|
+
"default": null
|
|
975
|
+
},
|
|
976
|
+
"reportedAt": {
|
|
977
|
+
"type": "number"
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
"required": [
|
|
981
|
+
"model",
|
|
982
|
+
"source",
|
|
983
|
+
"reportedAt"
|
|
984
|
+
]
|
|
816
985
|
}
|
|
817
|
-
}
|
|
818
|
-
"required": [
|
|
819
|
-
"ready"
|
|
820
|
-
]
|
|
986
|
+
}
|
|
821
987
|
}
|
|
822
988
|
}
|
|
823
989
|
}
|
package/package.json
CHANGED
package/src/be/db.ts
CHANGED
|
@@ -1411,9 +1411,14 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1411
1411
|
params.push(filters.createdAfter);
|
|
1412
1412
|
}
|
|
1413
1413
|
|
|
1414
|
-
// Exclude heartbeat tasks by default
|
|
1414
|
+
// Exclude system/heartbeat tasks by default. The flag is still called
|
|
1415
|
+
// `includeHeartbeat` for backward compat with existing API callers, but we
|
|
1416
|
+
// also gate boot-triage + heartbeat-checklist behind it since those are
|
|
1417
|
+
// equally noisy in the dashboard task list.
|
|
1415
1418
|
if (!filters?.includeHeartbeat) {
|
|
1416
|
-
conditions.push(
|
|
1419
|
+
conditions.push(
|
|
1420
|
+
"(IFNULL(taskType, '') NOT IN ('heartbeat', 'heartbeat-checklist', 'boot-triage') AND tags NOT LIKE '%\"heartbeat\"%')",
|
|
1421
|
+
);
|
|
1417
1422
|
}
|
|
1418
1423
|
|
|
1419
1424
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -1509,9 +1514,14 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1509
1514
|
params.push(filters.createdAfter);
|
|
1510
1515
|
}
|
|
1511
1516
|
|
|
1512
|
-
// Exclude heartbeat tasks by default
|
|
1517
|
+
// Exclude system/heartbeat tasks by default. The flag is still called
|
|
1518
|
+
// `includeHeartbeat` for backward compat with existing API callers, but we
|
|
1519
|
+
// also gate boot-triage + heartbeat-checklist behind it since those are
|
|
1520
|
+
// equally noisy in the dashboard task list.
|
|
1513
1521
|
if (!filters?.includeHeartbeat) {
|
|
1514
|
-
conditions.push(
|
|
1522
|
+
conditions.push(
|
|
1523
|
+
"(IFNULL(taskType, '') NOT IN ('heartbeat', 'heartbeat-checklist', 'boot-triage') AND tags NOT LIKE '%\"heartbeat\"%')",
|
|
1524
|
+
);
|
|
1515
1525
|
}
|
|
1516
1526
|
|
|
1517
1527
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -25,7 +25,7 @@ import { checkDevinCredentials } from "../providers/devin-adapter";
|
|
|
25
25
|
import { checkOpencodeCredentials } from "../providers/opencode-adapter";
|
|
26
26
|
import { checkPiMonoCredentials } from "../providers/pi-mono-adapter";
|
|
27
27
|
import type { CredCheckOptions, CredStatus } from "../providers/types";
|
|
28
|
-
import type { AgentCredStatus } from "../types";
|
|
28
|
+
import type { AgentCredStatus, AgentLatestModel, ProviderName } from "../types";
|
|
29
29
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
30
30
|
|
|
31
31
|
export type SupportedProvider = "claude" | "claude-managed" | "codex" | "devin" | "opencode" | "pi";
|
|
@@ -392,6 +392,7 @@ export async function buildCredStatusReport(
|
|
|
392
392
|
satisfiedBy: presence.satisfiedBy ?? null,
|
|
393
393
|
hint: presence.hint ?? null,
|
|
394
394
|
liveTest,
|
|
395
|
+
latestModel: null,
|
|
395
396
|
reportedAt: Date.now(),
|
|
396
397
|
reportKind: kind,
|
|
397
398
|
};
|
|
@@ -432,3 +433,53 @@ export async function reportCredStatus(
|
|
|
432
433
|
console.warn(`[cred-status] POST failed (non-fatal): ${err}`);
|
|
433
434
|
}
|
|
434
435
|
}
|
|
436
|
+
|
|
437
|
+
export async function reportLatestModel(
|
|
438
|
+
apiUrl: string,
|
|
439
|
+
apiKey: string,
|
|
440
|
+
agentId: string,
|
|
441
|
+
latestModel: AgentLatestModel,
|
|
442
|
+
): Promise<void> {
|
|
443
|
+
try {
|
|
444
|
+
await fetch(`${apiUrl}/api/agents/${encodeURIComponent(agentId)}/credential-status`, {
|
|
445
|
+
method: "PUT",
|
|
446
|
+
headers: {
|
|
447
|
+
Authorization: `Bearer ${apiKey}`,
|
|
448
|
+
"X-Agent-ID": agentId,
|
|
449
|
+
"Content-Type": "application/json",
|
|
450
|
+
},
|
|
451
|
+
body: JSON.stringify({
|
|
452
|
+
latest_model: latestModel,
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.warn(`[latest-model] POST failed (non-fatal): ${err}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function buildLatestModelReport(opts: {
|
|
461
|
+
model: string;
|
|
462
|
+
taskModel?: string;
|
|
463
|
+
configModel?: string;
|
|
464
|
+
taskId?: string;
|
|
465
|
+
harnessProvider: ProviderName;
|
|
466
|
+
}): AgentLatestModel | null {
|
|
467
|
+
const model = opts.model.trim();
|
|
468
|
+
if (!model) return null;
|
|
469
|
+
const taskModel = opts.taskModel?.trim();
|
|
470
|
+
const configModel = opts.configModel?.trim();
|
|
471
|
+
return {
|
|
472
|
+
model,
|
|
473
|
+
source:
|
|
474
|
+
taskModel && model === taskModel
|
|
475
|
+
? "task"
|
|
476
|
+
: configModel && model === configModel
|
|
477
|
+
? "agent_config"
|
|
478
|
+
: taskModel || configModel
|
|
479
|
+
? "custom"
|
|
480
|
+
: "adapter_default",
|
|
481
|
+
taskId: opts.taskId ?? null,
|
|
482
|
+
harnessProvider: opts.harnessProvider,
|
|
483
|
+
reportedAt: Date.now(),
|
|
484
|
+
};
|
|
485
|
+
}
|
package/src/commands/runner.ts
CHANGED
|
@@ -33,8 +33,10 @@ import { interpolate } from "../workflows/template.ts";
|
|
|
33
33
|
import { awaitCredentials, BootMaxWaitExceededError, EX_CONFIG } from "./credential-wait.ts";
|
|
34
34
|
import {
|
|
35
35
|
buildCredStatusReport,
|
|
36
|
+
buildLatestModelReport,
|
|
36
37
|
isCredCheckDisabled,
|
|
37
38
|
reportCredStatus,
|
|
39
|
+
reportLatestModel,
|
|
38
40
|
} from "./provider-credentials.ts";
|
|
39
41
|
// Side-effect import: registers runner trigger/resumption templates
|
|
40
42
|
import "./templates.ts";
|
|
@@ -1752,6 +1754,7 @@ async function spawnProviderProcess(
|
|
|
1752
1754
|
iteration: number;
|
|
1753
1755
|
taskId?: string;
|
|
1754
1756
|
model?: string;
|
|
1757
|
+
harnessProvider: ProviderName;
|
|
1755
1758
|
cwd?: string;
|
|
1756
1759
|
vcsRepo?: string;
|
|
1757
1760
|
},
|
|
@@ -1783,7 +1786,8 @@ async function spawnProviderProcess(
|
|
|
1783
1786
|
process.env.AGENT_FS_SHARED_ORG_ID = freshEnv.AGENT_FS_SHARED_ORG_ID as string;
|
|
1784
1787
|
}
|
|
1785
1788
|
|
|
1786
|
-
const
|
|
1789
|
+
const configModel = (freshEnv.MODEL_OVERRIDE as string | undefined) || "";
|
|
1790
|
+
const model = opts.model || configModel || "";
|
|
1787
1791
|
|
|
1788
1792
|
const config: ProviderSessionConfig = {
|
|
1789
1793
|
prompt: opts.prompt,
|
|
@@ -1803,6 +1807,18 @@ async function spawnProviderProcess(
|
|
|
1803
1807
|
};
|
|
1804
1808
|
|
|
1805
1809
|
const session = await adapter.createSession(config);
|
|
1810
|
+
const initialModelReport = buildLatestModelReport({
|
|
1811
|
+
model,
|
|
1812
|
+
taskModel: opts.model,
|
|
1813
|
+
configModel,
|
|
1814
|
+
taskId: realTaskId,
|
|
1815
|
+
harnessProvider: opts.harnessProvider,
|
|
1816
|
+
});
|
|
1817
|
+
if (initialModelReport) {
|
|
1818
|
+
reportLatestModel(opts.apiUrl, opts.apiKey, opts.agentId, initialModelReport).catch((err) =>
|
|
1819
|
+
console.warn(`[runner] Failed to report latest model: ${err}`),
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1806
1822
|
|
|
1807
1823
|
let oauthSelection: CredentialSelection | undefined;
|
|
1808
1824
|
if (adapter.name === "codex" && credentialSelections.length === 0) {
|
|
@@ -1960,6 +1976,20 @@ async function spawnProviderProcess(
|
|
|
1960
1976
|
break;
|
|
1961
1977
|
}
|
|
1962
1978
|
case "result":
|
|
1979
|
+
{
|
|
1980
|
+
const latestModel = buildLatestModelReport({
|
|
1981
|
+
model: event.cost.model,
|
|
1982
|
+
taskModel: opts.model,
|
|
1983
|
+
configModel,
|
|
1984
|
+
taskId: realTaskId,
|
|
1985
|
+
harnessProvider: opts.harnessProvider,
|
|
1986
|
+
});
|
|
1987
|
+
if (latestModel) {
|
|
1988
|
+
reportLatestModel(opts.apiUrl, opts.apiKey, opts.agentId, latestModel).catch((err) =>
|
|
1989
|
+
console.warn(`[runner] Failed to report latest model: ${err}`),
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1963
1993
|
// Cost save is handled in waitForCompletion().then() to ensure
|
|
1964
1994
|
// it completes before the process exits (fire-and-forget here
|
|
1965
1995
|
// races with container shutdown).
|
|
@@ -3206,6 +3236,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3206
3236
|
iteration,
|
|
3207
3237
|
taskId: task.id,
|
|
3208
3238
|
model: (task as { model?: string }).model,
|
|
3239
|
+
harnessProvider: state.harnessProvider,
|
|
3209
3240
|
cwd: resumeCwd,
|
|
3210
3241
|
vcsRepo: task.vcsRepo,
|
|
3211
3242
|
},
|
|
@@ -3605,6 +3636,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3605
3636
|
iteration,
|
|
3606
3637
|
taskId: trigger.taskId,
|
|
3607
3638
|
model: taskModel,
|
|
3639
|
+
harnessProvider: state.harnessProvider,
|
|
3608
3640
|
cwd: effectiveCwd,
|
|
3609
3641
|
vcsRepo: taskVcsRepo,
|
|
3610
3642
|
},
|
package/src/http/agents.ts
CHANGED
|
@@ -21,7 +21,12 @@ import {
|
|
|
21
21
|
updateAgentStatus,
|
|
22
22
|
upsertSwarmConfig,
|
|
23
23
|
} from "../be/db";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
AgentCredStatusSchema,
|
|
26
|
+
AgentLatestModelSchema,
|
|
27
|
+
type ProviderName,
|
|
28
|
+
ProviderNameSchema,
|
|
29
|
+
} from "../types";
|
|
25
30
|
import { route } from "./route-def";
|
|
26
31
|
import { agentWithCapacity, json, jsonError } from "./utils";
|
|
27
32
|
|
|
@@ -74,6 +79,29 @@ const setAgentHarnessProviderRoute = route({
|
|
|
74
79
|
},
|
|
75
80
|
});
|
|
76
81
|
|
|
82
|
+
const LocalHarnessProviderSchema = z.enum(["claude", "codex", "pi", "opencode"]);
|
|
83
|
+
|
|
84
|
+
const updateAgentRuntimeRoute = route({
|
|
85
|
+
method: "patch",
|
|
86
|
+
path: "/api/agents/{id}/runtime",
|
|
87
|
+
pattern: ["api", "agents", null, "runtime"],
|
|
88
|
+
summary: "Update an agent's runtime harness and default model",
|
|
89
|
+
description:
|
|
90
|
+
"Updates `agents.harness_provider` and upserts agent-scoped `swarm_config` rows for HARNESS_PROVIDER and MODEL_OVERRIDE. The settings apply to future provider sessions.",
|
|
91
|
+
tags: ["Agents"],
|
|
92
|
+
params: z.object({ id: z.string() }),
|
|
93
|
+
body: z.object({
|
|
94
|
+
harness_provider: LocalHarnessProviderSchema,
|
|
95
|
+
model: z.string().trim().min(1),
|
|
96
|
+
allow_custom_model: z.boolean().optional().default(false),
|
|
97
|
+
}),
|
|
98
|
+
responses: {
|
|
99
|
+
200: { description: "Updated agent row" },
|
|
100
|
+
400: { description: "Validation error" },
|
|
101
|
+
404: { description: "Agent not found" },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
77
105
|
const listAgents = route({
|
|
78
106
|
method: "get",
|
|
79
107
|
path: "/api/agents",
|
|
@@ -175,7 +203,7 @@ const getAgent = route({
|
|
|
175
203
|
// ─── Credential-status (Phase 3 + 4 of the credential safe-loop plan) ───────
|
|
176
204
|
|
|
177
205
|
const credentialStatusBody = z.object({
|
|
178
|
-
ready: z.boolean(),
|
|
206
|
+
ready: z.boolean().optional(),
|
|
179
207
|
/** Env-var names (or absolute file paths) the worker is blocked on. Empty/null when ready. */
|
|
180
208
|
missing: z.array(z.string()).optional().nullable(),
|
|
181
209
|
/**
|
|
@@ -185,6 +213,11 @@ const credentialStatusBody = z.object({
|
|
|
185
213
|
* reads the row instead of running its own check.
|
|
186
214
|
*/
|
|
187
215
|
cred_status: AgentCredStatusSchema.optional().nullable(),
|
|
216
|
+
/**
|
|
217
|
+
* Worker-reported latest model telemetry. Optional and merge-only: when sent
|
|
218
|
+
* without `cred_status`, the API preserves existing readiness/live-test data.
|
|
219
|
+
*/
|
|
220
|
+
latest_model: AgentLatestModelSchema.optional(),
|
|
188
221
|
});
|
|
189
222
|
|
|
190
223
|
const updateAgentCredentialStatusRoute = route({
|
|
@@ -468,6 +501,41 @@ export async function handleAgentsRest(
|
|
|
468
501
|
return true;
|
|
469
502
|
}
|
|
470
503
|
|
|
504
|
+
if (updateAgentRuntimeRoute.match(req.method, pathSegments)) {
|
|
505
|
+
const parsed = await updateAgentRuntimeRoute.parse(req, res, pathSegments, queryParams);
|
|
506
|
+
if (!parsed) return true;
|
|
507
|
+
const agent = getDb().transaction(() => {
|
|
508
|
+
const updated = setAgentHarnessProvider(
|
|
509
|
+
parsed.params.id,
|
|
510
|
+
parsed.body.harness_provider as ProviderName,
|
|
511
|
+
);
|
|
512
|
+
if (!updated) return null;
|
|
513
|
+
upsertSwarmConfig({
|
|
514
|
+
scope: "agent",
|
|
515
|
+
scopeId: parsed.params.id,
|
|
516
|
+
key: "HARNESS_PROVIDER",
|
|
517
|
+
value: parsed.body.harness_provider,
|
|
518
|
+
description: "Set via PATCH /api/agents/{id}/runtime",
|
|
519
|
+
});
|
|
520
|
+
upsertSwarmConfig({
|
|
521
|
+
scope: "agent",
|
|
522
|
+
scopeId: parsed.params.id,
|
|
523
|
+
key: "MODEL_OVERRIDE",
|
|
524
|
+
value: parsed.body.model,
|
|
525
|
+
description: parsed.body.allow_custom_model
|
|
526
|
+
? "Custom model set via PATCH /api/agents/{id}/runtime"
|
|
527
|
+
: "Set via PATCH /api/agents/{id}/runtime",
|
|
528
|
+
});
|
|
529
|
+
return updated;
|
|
530
|
+
})();
|
|
531
|
+
if (!agent) {
|
|
532
|
+
jsonError(res, "Agent not found", 404);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
json(res, agentWithCapacity(agent));
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
471
539
|
// Bulk credential-status MUST be matched BEFORE single-agent routes — the
|
|
472
540
|
// path "api/agents/credential-status" otherwise looks like an agent id.
|
|
473
541
|
if (listCredentialStatusRoute.match(req.method, pathSegments)) {
|
|
@@ -498,11 +566,19 @@ export async function handleAgentsRest(
|
|
|
498
566
|
queryParams,
|
|
499
567
|
);
|
|
500
568
|
if (!parsed) return true;
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
569
|
+
const existing = getAgentById(parsed.params.id);
|
|
570
|
+
if (!existing) {
|
|
571
|
+
jsonError(res, "Agent not found", 404);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
const agent =
|
|
575
|
+
parsed.body.ready !== undefined
|
|
576
|
+
? (updateAgentCredentialState(
|
|
577
|
+
parsed.params.id,
|
|
578
|
+
parsed.body.ready,
|
|
579
|
+
parsed.body.missing ?? null,
|
|
580
|
+
) ?? existing)
|
|
581
|
+
: existing;
|
|
506
582
|
if (!agent) {
|
|
507
583
|
jsonError(res, "Agent not found", 404);
|
|
508
584
|
return true;
|
|
@@ -510,10 +586,36 @@ export async function handleAgentsRest(
|
|
|
510
586
|
// Phase 055: persist the richer worker-reported snapshot when sent.
|
|
511
587
|
// We accept `null` to explicitly clear (e.g. on harness change), and
|
|
512
588
|
// `undefined` to leave the existing row value untouched.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
589
|
+
let finalAgent = agent;
|
|
590
|
+
if (parsed.body.cred_status !== undefined) {
|
|
591
|
+
const nextStatus = parsed.body.cred_status
|
|
592
|
+
? {
|
|
593
|
+
...parsed.body.cred_status,
|
|
594
|
+
latestModel:
|
|
595
|
+
parsed.body.latest_model ??
|
|
596
|
+
parsed.body.cred_status.latestModel ??
|
|
597
|
+
agent.credStatus?.latestModel ??
|
|
598
|
+
null,
|
|
599
|
+
}
|
|
600
|
+
: null;
|
|
601
|
+
finalAgent = updateAgentCredStatus(parsed.params.id, nextStatus) ?? agent;
|
|
602
|
+
} else if (parsed.body.latest_model) {
|
|
603
|
+
const current = agent.credStatus ?? {
|
|
604
|
+
ready: parsed.body.ready ?? true,
|
|
605
|
+
missing: parsed.body.missing ?? [],
|
|
606
|
+
satisfiedBy: null,
|
|
607
|
+
hint: null,
|
|
608
|
+
liveTest: null,
|
|
609
|
+
latestModel: null,
|
|
610
|
+
reportedAt: parsed.body.latest_model.reportedAt,
|
|
611
|
+
reportKind: "post_task" as const,
|
|
612
|
+
};
|
|
613
|
+
finalAgent =
|
|
614
|
+
updateAgentCredStatus(parsed.params.id, {
|
|
615
|
+
...current,
|
|
616
|
+
latestModel: parsed.body.latest_model,
|
|
617
|
+
}) ?? agent;
|
|
618
|
+
}
|
|
517
619
|
json(res, agentWithCapacity(finalAgent));
|
|
518
620
|
return true;
|
|
519
621
|
}
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { handleAgentRegister, handleAgentsRest } from "../http/agents";
|
|
29
29
|
|
|
30
30
|
const TEST_DB_PATH = "./test-agents-harness-provider.sqlite";
|
|
31
|
-
const TEST_PORT = 13059;
|
|
31
|
+
const TEST_PORT = 13059 + (process.pid % 1000);
|
|
32
32
|
|
|
33
33
|
async function removeDbFiles(path: string): Promise<void> {
|
|
34
34
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
@@ -42,7 +42,7 @@ async function removeDbFiles(path: string): Promise<void> {
|
|
|
42
42
|
|
|
43
43
|
function makeTestServer(): Server {
|
|
44
44
|
return createHttpServer(async (req, res) => {
|
|
45
|
-
const url = new URL(req.url ?? "/",
|
|
45
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
46
46
|
const pathSegments = url.pathname.split("/").filter(Boolean);
|
|
47
47
|
const queryParams = url.searchParams;
|
|
48
48
|
const myAgentId = (req.headers["x-agent-id"] as string | undefined) ?? undefined;
|
|
@@ -331,3 +331,44 @@ describe("PATCH /api/agents/:id/harness-provider", () => {
|
|
|
331
331
|
expect(rows2.filter((r) => r.key === "HARNESS_PROVIDER")).toHaveLength(1);
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
|
+
|
|
335
|
+
describe("PATCH /api/agents/:id/runtime", () => {
|
|
336
|
+
test("updates harness_provider and agent-scoped runtime config rows", async () => {
|
|
337
|
+
const a = createAgent({
|
|
338
|
+
name: "runtime-target-1",
|
|
339
|
+
isLead: false,
|
|
340
|
+
status: "idle",
|
|
341
|
+
capabilities: [],
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const res = await fetch(`${baseUrl}/api/agents/${a.id}/runtime`, {
|
|
345
|
+
method: "PATCH",
|
|
346
|
+
headers: { "Content-Type": "application/json" },
|
|
347
|
+
body: JSON.stringify({ harness_provider: "codex", model: "gpt-5.4" }),
|
|
348
|
+
});
|
|
349
|
+
expect(res.status).toBe(200);
|
|
350
|
+
|
|
351
|
+
const row = getAgentById(a.id);
|
|
352
|
+
expect(row?.harnessProvider).toBe("codex");
|
|
353
|
+
|
|
354
|
+
const rows = getSwarmConfigs({ scope: "agent", scopeId: a.id });
|
|
355
|
+
expect(rows.find((r) => r.key === "HARNESS_PROVIDER")?.value).toBe("codex");
|
|
356
|
+
expect(rows.find((r) => r.key === "MODEL_OVERRIDE")?.value).toBe("gpt-5.4");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("rejects non-local harnesses for runtime editing", async () => {
|
|
360
|
+
const a = createAgent({
|
|
361
|
+
name: "runtime-target-2",
|
|
362
|
+
isLead: false,
|
|
363
|
+
status: "idle",
|
|
364
|
+
capabilities: [],
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const res = await fetch(`${baseUrl}/api/agents/${a.id}/runtime`, {
|
|
368
|
+
method: "PATCH",
|
|
369
|
+
headers: { "Content-Type": "application/json" },
|
|
370
|
+
body: JSON.stringify({ harness_provider: "devin", model: "devin" }),
|
|
371
|
+
});
|
|
372
|
+
expect(res.status).toBe(400);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
@@ -20,7 +20,7 @@ import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
const TEST_DB_PATH = "./test-credential-status-api.sqlite";
|
|
23
|
-
const TEST_PORT = 13041;
|
|
23
|
+
const TEST_PORT = 13041 + (process.pid % 1000);
|
|
24
24
|
|
|
25
25
|
function createTestServer(): Server {
|
|
26
26
|
return createHttpServer(async (req, res) => {
|
|
@@ -220,4 +220,43 @@ describe("Phase 4 — credential-status HTTP endpoints", () => {
|
|
|
220
220
|
});
|
|
221
221
|
expect(resp.status).toBe(400);
|
|
222
222
|
});
|
|
223
|
+
|
|
224
|
+
test("PUT /credential-status merges latest_model without clobbering readiness", async () => {
|
|
225
|
+
const snapshot = {
|
|
226
|
+
ready: true,
|
|
227
|
+
missing: [],
|
|
228
|
+
satisfiedBy: "env" as const,
|
|
229
|
+
hint: null,
|
|
230
|
+
liveTest: { ok: true, error: null, latency_ms: 45, testedAt: Date.now() },
|
|
231
|
+
reportedAt: Date.now(),
|
|
232
|
+
reportKind: "boot" as const,
|
|
233
|
+
};
|
|
234
|
+
await fetch(`${baseUrl}/api/agents/${readyAgentId}/credential-status`, {
|
|
235
|
+
method: "PUT",
|
|
236
|
+
headers: { "Content-Type": "application/json" },
|
|
237
|
+
body: JSON.stringify({ ready: true, missing: [], cred_status: snapshot }),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const modelReport = {
|
|
241
|
+
model: "gpt-5.4",
|
|
242
|
+
source: "agent_config" as const,
|
|
243
|
+
taskId: "task-123",
|
|
244
|
+
harnessProvider: "codex" as const,
|
|
245
|
+
reportedAt: Date.now(),
|
|
246
|
+
};
|
|
247
|
+
const put = await fetch(`${baseUrl}/api/agents/${readyAgentId}/credential-status`, {
|
|
248
|
+
method: "PUT",
|
|
249
|
+
headers: { "Content-Type": "application/json" },
|
|
250
|
+
body: JSON.stringify({ latest_model: modelReport }),
|
|
251
|
+
});
|
|
252
|
+
expect(put.status).toBe(200);
|
|
253
|
+
|
|
254
|
+
const get = await fetch(`${baseUrl}/api/agents/${readyAgentId}/credential-status`);
|
|
255
|
+
const body = (await get.json()) as {
|
|
256
|
+
credStatus: typeof snapshot & { latestModel?: typeof modelReport };
|
|
257
|
+
};
|
|
258
|
+
expect(body.credStatus.ready).toBe(true);
|
|
259
|
+
expect(body.credStatus.liveTest).toMatchObject({ ok: true, latency_ms: 45 });
|
|
260
|
+
expect(body.credStatus.latestModel).toMatchObject({ model: "gpt-5.4", source: "agent_config" });
|
|
261
|
+
});
|
|
223
262
|
});
|
package/src/types.ts
CHANGED
|
@@ -386,12 +386,22 @@ export const AgentCredStatusLiveTestSchema = z.object({
|
|
|
386
386
|
});
|
|
387
387
|
export type AgentCredStatusLiveTest = z.infer<typeof AgentCredStatusLiveTestSchema>;
|
|
388
388
|
|
|
389
|
+
export const AgentLatestModelSchema = z.object({
|
|
390
|
+
model: z.string().min(1),
|
|
391
|
+
source: z.enum(["task", "agent_config", "adapter_default", "custom"]),
|
|
392
|
+
taskId: z.string().nullable().default(null),
|
|
393
|
+
harnessProvider: ProviderNameSchema.nullable().default(null),
|
|
394
|
+
reportedAt: z.number(), // unix ms
|
|
395
|
+
});
|
|
396
|
+
export type AgentLatestModel = z.infer<typeof AgentLatestModelSchema>;
|
|
397
|
+
|
|
389
398
|
export const AgentCredStatusSchema = z.object({
|
|
390
399
|
ready: z.boolean(),
|
|
391
400
|
missing: z.array(z.string()).default([]),
|
|
392
401
|
satisfiedBy: z.enum(["env", "file", "side-effect-pending"]).nullable().default(null),
|
|
393
402
|
hint: z.string().nullable().default(null),
|
|
394
403
|
liveTest: AgentCredStatusLiveTestSchema.nullable().default(null),
|
|
404
|
+
latestModel: AgentLatestModelSchema.nullable().default(null),
|
|
395
405
|
reportedAt: z.number(), // unix ms
|
|
396
406
|
reportKind: z.enum(["boot", "post_task"]).default("boot"),
|
|
397
407
|
});
|