@desplega.ai/agent-swarm 1.77.0 → 1.77.2
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/commands/provider-credentials.ts +52 -1
- package/src/commands/runner.ts +56 -8
- 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/tests/tool-call-progress.test.ts +44 -0
- 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.2",
|
|
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
|
@@ -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";
|
|
@@ -415,13 +417,19 @@ export function humanizeToolName(name: string): string {
|
|
|
415
417
|
export function toolCallToProgress(toolName: string, args: unknown): string | null {
|
|
416
418
|
if (SKIP_PROGRESS_TOOLS.has(toolName)) return null;
|
|
417
419
|
|
|
420
|
+
const a = args as Record<string, unknown>;
|
|
421
|
+
const maybeMcpServer = typeof a?.server === "string" ? a.server : undefined;
|
|
422
|
+
const maybeMcpTool = typeof a?.tool === "string" ? a.tool : undefined;
|
|
423
|
+
const effectiveToolName =
|
|
424
|
+
maybeMcpServer && maybeMcpTool ? `mcp__${maybeMcpServer}__${maybeMcpTool}` : toolName;
|
|
425
|
+
if (SKIP_PROGRESS_TOOLS.has(effectiveToolName)) return null;
|
|
426
|
+
|
|
418
427
|
// Normalize: pi-mono uses lowercase ("read"), Claude uses PascalCase ("Read")
|
|
419
428
|
const normalized =
|
|
420
|
-
|
|
421
|
-
?
|
|
422
|
-
:
|
|
429
|
+
effectiveToolName.startsWith("mcp__") || effectiveToolName.includes("_")
|
|
430
|
+
? effectiveToolName
|
|
431
|
+
: effectiveToolName.charAt(0).toUpperCase() + effectiveToolName.slice(1);
|
|
423
432
|
|
|
424
|
-
const a = args as Record<string, unknown>;
|
|
425
433
|
const shortPath = (p: unknown) => {
|
|
426
434
|
if (typeof p !== "string") return "";
|
|
427
435
|
// Show last 2 path segments for readability
|
|
@@ -450,8 +458,8 @@ export function toolCallToProgress(toolName: string, args: unknown): string | nu
|
|
|
450
458
|
return `⚙️ Running /${a.skill}`;
|
|
451
459
|
default: {
|
|
452
460
|
// MCP tools: mcp__server__tool
|
|
453
|
-
if (
|
|
454
|
-
const parts =
|
|
461
|
+
if (effectiveToolName.startsWith("mcp__")) {
|
|
462
|
+
const parts = effectiveToolName.split("__");
|
|
455
463
|
if (parts.length >= 3) {
|
|
456
464
|
const server = parts[1];
|
|
457
465
|
const tool = parts.slice(2).join("__");
|
|
@@ -465,8 +473,18 @@ export function toolCallToProgress(toolName: string, args: unknown): string | nu
|
|
|
465
473
|
// Other MCP servers: "🔌 server: Humanized tool"
|
|
466
474
|
return `🔌 ${server}: ${humanizeToolName(tool)}`;
|
|
467
475
|
}
|
|
468
|
-
return `🔌 ${
|
|
476
|
+
return `🔌 ${effectiveToolName}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Pi-mono exposes tools from the built-in swarm MCP endpoint as bare
|
|
480
|
+
// names ("store-progress", "send-task", ...), not as mcp__ names.
|
|
481
|
+
// Treat those names as agent-swarm tools so activity stays readable.
|
|
482
|
+
if (toolName.includes("-")) {
|
|
483
|
+
const label = SWARM_TOOL_LABELS[toolName];
|
|
484
|
+
if (label === null) return null;
|
|
485
|
+
if (label) return label;
|
|
469
486
|
}
|
|
487
|
+
|
|
470
488
|
return `🔧 ${toolName}`;
|
|
471
489
|
}
|
|
472
490
|
}
|
|
@@ -1736,6 +1754,7 @@ async function spawnProviderProcess(
|
|
|
1736
1754
|
iteration: number;
|
|
1737
1755
|
taskId?: string;
|
|
1738
1756
|
model?: string;
|
|
1757
|
+
harnessProvider: ProviderName;
|
|
1739
1758
|
cwd?: string;
|
|
1740
1759
|
vcsRepo?: string;
|
|
1741
1760
|
},
|
|
@@ -1767,7 +1786,8 @@ async function spawnProviderProcess(
|
|
|
1767
1786
|
process.env.AGENT_FS_SHARED_ORG_ID = freshEnv.AGENT_FS_SHARED_ORG_ID as string;
|
|
1768
1787
|
}
|
|
1769
1788
|
|
|
1770
|
-
const
|
|
1789
|
+
const configModel = (freshEnv.MODEL_OVERRIDE as string | undefined) || "";
|
|
1790
|
+
const model = opts.model || configModel || "";
|
|
1771
1791
|
|
|
1772
1792
|
const config: ProviderSessionConfig = {
|
|
1773
1793
|
prompt: opts.prompt,
|
|
@@ -1787,6 +1807,18 @@ async function spawnProviderProcess(
|
|
|
1787
1807
|
};
|
|
1788
1808
|
|
|
1789
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
|
+
}
|
|
1790
1822
|
|
|
1791
1823
|
let oauthSelection: CredentialSelection | undefined;
|
|
1792
1824
|
if (adapter.name === "codex" && credentialSelections.length === 0) {
|
|
@@ -1944,6 +1976,20 @@ async function spawnProviderProcess(
|
|
|
1944
1976
|
break;
|
|
1945
1977
|
}
|
|
1946
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
|
+
}
|
|
1947
1993
|
// Cost save is handled in waitForCompletion().then() to ensure
|
|
1948
1994
|
// it completes before the process exits (fire-and-forget here
|
|
1949
1995
|
// races with container shutdown).
|
|
@@ -3190,6 +3236,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3190
3236
|
iteration,
|
|
3191
3237
|
taskId: task.id,
|
|
3192
3238
|
model: (task as { model?: string }).model,
|
|
3239
|
+
harnessProvider: state.harnessProvider,
|
|
3193
3240
|
cwd: resumeCwd,
|
|
3194
3241
|
vcsRepo: task.vcsRepo,
|
|
3195
3242
|
},
|
|
@@ -3589,6 +3636,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3589
3636
|
iteration,
|
|
3590
3637
|
taskId: trigger.taskId,
|
|
3591
3638
|
model: taskModel,
|
|
3639
|
+
harnessProvider: state.harnessProvider,
|
|
3592
3640
|
cwd: effectiveCwd,
|
|
3593
3641
|
vcsRepo: taskVcsRepo,
|
|
3594
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
|
});
|
|
@@ -175,6 +175,50 @@ describe("toolCallToProgress", () => {
|
|
|
175
175
|
expect(result).toBe("🔌 context7: Query docs");
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
// --- Provider-normalized MCP variants ---
|
|
179
|
+
|
|
180
|
+
test("bare agent-swarm store-progress is skipped", () => {
|
|
181
|
+
expect(toolCallToProgress("store-progress", {})).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("bare agent-swarm send-task has pretty label", () => {
|
|
185
|
+
const result = toolCallToProgress("send-task", {});
|
|
186
|
+
expect(result).toBe("📤 Delegating task");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("bare agent-swarm db-query has pretty label", () => {
|
|
190
|
+
const result = toolCallToProgress("db-query", {});
|
|
191
|
+
expect(result).toBe("🗃️ Querying database");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("codex MCP args with agent-swarm server use pretty labels", () => {
|
|
195
|
+
const result = toolCallToProgress("send-task", {
|
|
196
|
+
server: "agent-swarm",
|
|
197
|
+
tool: "send-task",
|
|
198
|
+
arguments: { task: "ping" },
|
|
199
|
+
});
|
|
200
|
+
expect(result).toBe("📤 Delegating task");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("codex MCP args with agent-swarm store-progress are skipped", () => {
|
|
204
|
+
expect(
|
|
205
|
+
toolCallToProgress("store-progress", {
|
|
206
|
+
server: "agent-swarm",
|
|
207
|
+
tool: "store-progress",
|
|
208
|
+
arguments: { status: "completed" },
|
|
209
|
+
}),
|
|
210
|
+
).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("codex MCP args with external server keep server prefix", () => {
|
|
214
|
+
const result = toolCallToProgress("list-issues", {
|
|
215
|
+
server: "linear",
|
|
216
|
+
tool: "list-issues",
|
|
217
|
+
arguments: {},
|
|
218
|
+
});
|
|
219
|
+
expect(result).toBe("🔌 linear: List issues");
|
|
220
|
+
});
|
|
221
|
+
|
|
178
222
|
// --- Short path helper (tested implicitly) ---
|
|
179
223
|
|
|
180
224
|
test("Read with short path (<=2 segments) keeps full path", () => {
|
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
|
});
|