@desplega.ai/agent-swarm 1.73.4 → 1.74.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 +22 -3
- package/package.json +8 -6
- package/src/be/db.ts +29 -3
- package/src/be/migrations/048_agent_provider.sql +1 -0
- package/src/commands/runner.ts +10 -0
- package/src/hooks/hook.ts +0 -10
- package/src/http/agents.ts +7 -0
- package/src/http/session-data.ts +3 -3
- package/src/http/tasks.ts +3 -0
- package/src/providers/claude-adapter.ts +2 -65
- package/src/providers/index.ts +4 -1
- package/src/providers/opencode-adapter.ts +539 -0
- package/src/providers/pi-mono-adapter.ts +4 -1
- package/src/providers/pi-mono-extension.ts +0 -4
- package/src/providers/types.ts +1 -1
- package/src/tests/credentials.test.ts +85 -0
- package/src/tests/opencode-adapter.test.ts +422 -0
- package/src/tests/provider-adapter.test.ts +8 -1
- package/src/tools/db-query.ts +2 -18
- package/src/tools/store-progress.ts +31 -5
- package/src/types.ts +16 -4
- package/src/utils/credentials.ts +21 -0
- package/src/utils/mcp-server-fetcher.ts +72 -0
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ flowchart LR
|
|
|
89
89
|
- **Multi-channel inputs** — Slack, GitHub, GitLab, email, Linear, Jira, and the HTTP API all create tasks. [Integrations](#integrations)
|
|
90
90
|
- **Workflow engine with Human-in-the-Loop** — DAG-based automation with approval gates, retries, and structured I/O. [Workflows →](https://docs.agent-swarm.dev/docs/concepts/workflows)
|
|
91
91
|
- **Scheduled & recurring tasks** — cron-based automation for standing work. [Scheduling →](https://docs.agent-swarm.dev/docs/concepts/scheduling)
|
|
92
|
-
- **Multi-provider** — run with Claude Code, OpenAI Codex, pi-mono, Devin,
|
|
92
|
+
- **Multi-provider** — run with Claude Code, OpenAI Codex, pi-mono, Devin, Claude Managed Agents, or opencode. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
|
|
93
93
|
- **Skills & MCP servers** — reusable procedural knowledge and per-agent MCP servers with scope cascade. [MCP tools →](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
|
|
94
94
|
- **Real-time dashboard** — monitor agents, tasks, and inter-agent chat. [app.agent-swarm.dev →](https://app.agent-swarm.dev)
|
|
95
95
|
|
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.73.
|
|
5
|
+
"version": "1.73.5",
|
|
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": [
|
|
@@ -315,6 +315,17 @@
|
|
|
315
315
|
},
|
|
316
316
|
"maxTasks": {
|
|
317
317
|
"type": "integer"
|
|
318
|
+
},
|
|
319
|
+
"provider": {
|
|
320
|
+
"type": "string",
|
|
321
|
+
"enum": [
|
|
322
|
+
"claude",
|
|
323
|
+
"codex",
|
|
324
|
+
"pi",
|
|
325
|
+
"devin",
|
|
326
|
+
"claude-managed",
|
|
327
|
+
"opencode"
|
|
328
|
+
]
|
|
318
329
|
}
|
|
319
330
|
},
|
|
320
331
|
"required": [
|
|
@@ -5127,7 +5138,8 @@
|
|
|
5127
5138
|
"enum": [
|
|
5128
5139
|
"claude",
|
|
5129
5140
|
"codex",
|
|
5130
|
-
"pi"
|
|
5141
|
+
"pi",
|
|
5142
|
+
"opencode"
|
|
5131
5143
|
]
|
|
5132
5144
|
},
|
|
5133
5145
|
"createdAt": {
|
|
@@ -6847,6 +6859,9 @@
|
|
|
6847
6859
|
"devin"
|
|
6848
6860
|
]
|
|
6849
6861
|
},
|
|
6862
|
+
"model": {
|
|
6863
|
+
"type": "string"
|
|
6864
|
+
},
|
|
6850
6865
|
"providerMeta": {
|
|
6851
6866
|
"type": "object",
|
|
6852
6867
|
"properties": {
|
|
@@ -6884,9 +6899,13 @@
|
|
|
6884
6899
|
"claude",
|
|
6885
6900
|
"codex",
|
|
6886
6901
|
"pi",
|
|
6887
|
-
"claude-managed"
|
|
6902
|
+
"claude-managed",
|
|
6903
|
+
"opencode"
|
|
6888
6904
|
]
|
|
6889
6905
|
},
|
|
6906
|
+
"model": {
|
|
6907
|
+
"type": "string"
|
|
6908
|
+
},
|
|
6890
6909
|
"providerMeta": {
|
|
6891
6910
|
"type": "object",
|
|
6892
6911
|
"properties": {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.0",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@biomejs/biome": "^2.3.10",
|
|
89
89
|
"@faker-js/faker": "^10.4.0",
|
|
90
|
+
"@opencode-ai/plugin": "1.14.30",
|
|
90
91
|
"@types/bun": "latest"
|
|
91
92
|
},
|
|
92
93
|
"peerDependencies": {
|
|
@@ -97,17 +98,18 @@
|
|
|
97
98
|
},
|
|
98
99
|
"dependencies": {
|
|
99
100
|
"@ai-sdk/openai": "^3.0.41",
|
|
100
|
-
"@anthropic-ai/sdk": "
|
|
101
|
+
"@anthropic-ai/sdk": "^0.93.0",
|
|
101
102
|
"@asteasolutions/zod-to-openapi": "^8.0.0",
|
|
102
103
|
"@desplega.ai/business-use": "^0.4.2",
|
|
103
104
|
"@desplega.ai/localtunnel": "^2.2.0",
|
|
104
105
|
"@inkjs/ui": "^2.0.0",
|
|
105
106
|
"@linear/sdk": "^77.0.0",
|
|
106
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
107
|
-
"@mariozechner/pi-ai": "^0.
|
|
108
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
107
|
+
"@mariozechner/pi-agent-core": "^0.73.0",
|
|
108
|
+
"@mariozechner/pi-ai": "^0.73.0",
|
|
109
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
109
110
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
110
|
-
"@openai/codex-sdk": "^0.
|
|
111
|
+
"@openai/codex-sdk": "^0.125.0",
|
|
112
|
+
"@opencode-ai/sdk": "^1.14.30",
|
|
111
113
|
"@openfort/openfort-node": "^0.9.1",
|
|
112
114
|
"@slack/bolt": "^4.6.0",
|
|
113
115
|
"@types/react": "^19.2.7",
|
package/src/be/db.ts
CHANGED
|
@@ -549,6 +549,7 @@ type AgentRow = {
|
|
|
549
549
|
toolsMd: string | null;
|
|
550
550
|
heartbeatMd: string | null;
|
|
551
551
|
lastActivityAt: string | null;
|
|
552
|
+
provider: string | null;
|
|
552
553
|
createdAt: string;
|
|
553
554
|
lastUpdatedAt: string;
|
|
554
555
|
};
|
|
@@ -571,6 +572,7 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
571
572
|
toolsMd: row.toolsMd ?? undefined,
|
|
572
573
|
heartbeatMd: row.heartbeatMd ?? undefined,
|
|
573
574
|
lastActivityAt: row.lastActivityAt ?? undefined,
|
|
575
|
+
provider: (row.provider as ProviderName | null) ?? undefined,
|
|
574
576
|
createdAt: row.createdAt,
|
|
575
577
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
576
578
|
};
|
|
@@ -578,8 +580,8 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
578
580
|
|
|
579
581
|
export const agentQueries = {
|
|
580
582
|
insert: () =>
|
|
581
|
-
getDb().prepare<AgentRow, [string, string, number, AgentStatus, number]>(
|
|
582
|
-
"INSERT INTO agents (id, name, isLead, status, maxTasks, createdAt, lastUpdatedAt) VALUES (?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *",
|
|
583
|
+
getDb().prepare<AgentRow, [string, string, number, AgentStatus, number, string | null]>(
|
|
584
|
+
"INSERT INTO agents (id, name, isLead, status, maxTasks, provider, createdAt, lastUpdatedAt) VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *",
|
|
583
585
|
),
|
|
584
586
|
|
|
585
587
|
getById: () => getDb().prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?"),
|
|
@@ -601,7 +603,7 @@ export function createAgent(
|
|
|
601
603
|
const maxTasks = agent.maxTasks ?? 1;
|
|
602
604
|
const row = agentQueries
|
|
603
605
|
.insert()
|
|
604
|
-
.get(id, agent.name, agent.isLead ? 1 : 0, agent.status, maxTasks);
|
|
606
|
+
.get(id, agent.name, agent.isLead ? 1 : 0, agent.status, maxTasks, agent.provider ?? null);
|
|
605
607
|
if (!row) throw new Error("Failed to create agent");
|
|
606
608
|
try {
|
|
607
609
|
createLogEntry({ eventType: "agent_joined", agentId: id, newValue: agent.status });
|
|
@@ -649,6 +651,16 @@ export function updateAgentMaxTasks(id: string, maxTasks: number): Agent | null
|
|
|
649
651
|
return row ? rowToAgent(row) : null;
|
|
650
652
|
}
|
|
651
653
|
|
|
654
|
+
export function updateAgentProvider(id: string, provider: ProviderName): Agent | null {
|
|
655
|
+
const row = getDb()
|
|
656
|
+
.prepare<AgentRow, [string, string]>(
|
|
657
|
+
`UPDATE agents SET provider = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
658
|
+
WHERE id = ? RETURNING *`,
|
|
659
|
+
)
|
|
660
|
+
.get(provider, id);
|
|
661
|
+
return row ? rowToAgent(row) : null;
|
|
662
|
+
}
|
|
663
|
+
|
|
652
664
|
export function updateAgentActivity(id: string): void {
|
|
653
665
|
getDb()
|
|
654
666
|
.prepare<null, [string]>(
|
|
@@ -1078,6 +1090,7 @@ export function updateTaskClaudeSessionId(
|
|
|
1078
1090
|
claudeSessionId: string,
|
|
1079
1091
|
provider?: ProviderName,
|
|
1080
1092
|
providerMeta?: Record<string, unknown>,
|
|
1093
|
+
model?: string,
|
|
1081
1094
|
): AgentTask | null {
|
|
1082
1095
|
const setClauses = ["claudeSessionId = ?", "lastUpdatedAt = ?"];
|
|
1083
1096
|
const params: (string | null)[] = [claudeSessionId, new Date().toISOString()];
|
|
@@ -1090,6 +1103,10 @@ export function updateTaskClaudeSessionId(
|
|
|
1090
1103
|
setClauses.push("providerMeta = ?");
|
|
1091
1104
|
params.push(JSON.stringify(providerMeta));
|
|
1092
1105
|
}
|
|
1106
|
+
if (model !== undefined) {
|
|
1107
|
+
setClauses.push("model = ?");
|
|
1108
|
+
params.push(model);
|
|
1109
|
+
}
|
|
1093
1110
|
|
|
1094
1111
|
params.push(taskId);
|
|
1095
1112
|
|
|
@@ -2016,6 +2033,15 @@ export interface CreateTaskOptions {
|
|
|
2016
2033
|
workflowRunId?: string;
|
|
2017
2034
|
workflowRunStepId?: string;
|
|
2018
2035
|
sourceTaskId?: string;
|
|
2036
|
+
/**
|
|
2037
|
+
* Optional JSON Schema the agent's final output must conform to.
|
|
2038
|
+
*
|
|
2039
|
+
* Enforced via the MCP `store-progress` tool (validated in
|
|
2040
|
+
* `src/tools/store-progress.ts`). NOT enforced when the task runs on
|
|
2041
|
+
* default-mode Devin (no MCP) — see runbooks/harness-providers.md
|
|
2042
|
+
* ("Per-task outputSchema support"). Callers reading `task.output` for
|
|
2043
|
+
* a schema'd task should be defensive about JSON parsing.
|
|
2044
|
+
*/
|
|
2019
2045
|
outputSchema?: Record<string, unknown>;
|
|
2020
2046
|
requestedByUserId?: string;
|
|
2021
2047
|
contextKey?: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE agents ADD COLUMN provider TEXT;
|
package/src/commands/runner.ts
CHANGED
|
@@ -551,6 +551,10 @@ export async function ensureTaskFinished(
|
|
|
551
551
|
body.failureReason = failureReason || `Claude process exited with code ${exitCode}`;
|
|
552
552
|
} else if (providerOutput) {
|
|
553
553
|
// Provider already supplied structured output (e.g. Devin) — use directly.
|
|
554
|
+
// NOTE: providerOutput is NOT validated against task.outputSchema here.
|
|
555
|
+
// Known gap for default-mode Devin; see runbooks/harness-providers.md
|
|
556
|
+
// ("Per-task outputSchema support"). Schema enforcement only happens on
|
|
557
|
+
// the MCP path via store-progress.
|
|
554
558
|
body.output = providerOutput;
|
|
555
559
|
} else {
|
|
556
560
|
// Try structured output fallback if the task has an outputSchema
|
|
@@ -1050,12 +1054,14 @@ async function saveProviderSessionId(
|
|
|
1050
1054
|
claudeSessionId: string,
|
|
1051
1055
|
provider?: ProviderName,
|
|
1052
1056
|
providerMeta?: Record<string, unknown>,
|
|
1057
|
+
model?: string,
|
|
1053
1058
|
): Promise<void> {
|
|
1054
1059
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1055
1060
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
1056
1061
|
const body: Record<string, unknown> = { claudeSessionId };
|
|
1057
1062
|
if (provider !== undefined) body.provider = provider;
|
|
1058
1063
|
if (providerMeta !== undefined) body.providerMeta = providerMeta;
|
|
1064
|
+
if (model !== undefined && model !== "") body.model = model;
|
|
1059
1065
|
await fetch(`${apiUrl}/api/tasks/${taskId}/claude-session`, {
|
|
1060
1066
|
method: "PUT",
|
|
1061
1067
|
headers,
|
|
@@ -1338,6 +1344,8 @@ async function registerAgent(opts: {
|
|
|
1338
1344
|
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
1339
1345
|
}
|
|
1340
1346
|
|
|
1347
|
+
const provider = (process.env.HARNESS_PROVIDER || "claude") as ProviderName;
|
|
1348
|
+
|
|
1341
1349
|
const response = await fetch(`${opts.apiUrl}/api/agents`, {
|
|
1342
1350
|
method: "POST",
|
|
1343
1351
|
headers,
|
|
@@ -1347,6 +1355,7 @@ async function registerAgent(opts: {
|
|
|
1347
1355
|
role: opts.role,
|
|
1348
1356
|
capabilities: opts.capabilities,
|
|
1349
1357
|
maxTasks: opts.maxTasks,
|
|
1358
|
+
provider,
|
|
1350
1359
|
}),
|
|
1351
1360
|
});
|
|
1352
1361
|
|
|
@@ -1745,6 +1754,7 @@ async function spawnProviderProcess(
|
|
|
1745
1754
|
event.sessionId,
|
|
1746
1755
|
event.provider,
|
|
1747
1756
|
event.providerMeta,
|
|
1757
|
+
model,
|
|
1748
1758
|
).catch((err) => console.warn(`[runner] Failed to save session ID: ${err}`));
|
|
1749
1759
|
} else {
|
|
1750
1760
|
// Pool task: save provider session ID on active session so it can be
|
package/src/hooks/hook.ts
CHANGED
|
@@ -691,12 +691,6 @@ export async function handleHook(): Promise<void> {
|
|
|
691
691
|
console.log(tray);
|
|
692
692
|
}
|
|
693
693
|
}
|
|
694
|
-
|
|
695
|
-
if (!agentInfo.isLead && agentInfo.status === "busy") {
|
|
696
|
-
console.log(
|
|
697
|
-
`Remember to call store-progress periodically to update the lead agent on your progress as you are currently marked as busy. The comments you leave will be helpful for the lead agent to monitor your work.`,
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
694
|
} else {
|
|
701
695
|
console.log(
|
|
702
696
|
`You are not registered in the agent swarm yet. Use the join-swarm tool to register yourself, then check your status with my-agent-info.
|
|
@@ -995,10 +989,6 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
|
|
|
995
989
|
`Task sent successfully.${maybeTaskId ? ` Task ID: ${maybeTaskId}.` : ""} Monitor progress using the get-task-details tool periodically.`,
|
|
996
990
|
);
|
|
997
991
|
}
|
|
998
|
-
} else {
|
|
999
|
-
console.log(
|
|
1000
|
-
`Remember to call store-progress periodically to update the lead agent on your progress.`,
|
|
1001
|
-
);
|
|
1002
992
|
}
|
|
1003
993
|
}
|
|
1004
994
|
break;
|
package/src/http/agents.ts
CHANGED
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
updateAgentMaxTasks,
|
|
15
15
|
updateAgentName,
|
|
16
16
|
updateAgentProfile,
|
|
17
|
+
updateAgentProvider,
|
|
17
18
|
updateAgentStatus,
|
|
18
19
|
} from "../be/db";
|
|
20
|
+
import { ProviderNameSchema } from "../types";
|
|
19
21
|
import { route } from "./route-def";
|
|
20
22
|
import { agentWithCapacity, json, jsonError } from "./utils";
|
|
21
23
|
|
|
@@ -34,6 +36,7 @@ const registerAgent = route({
|
|
|
34
36
|
role: z.string().optional(),
|
|
35
37
|
capabilities: z.array(z.string()).optional(),
|
|
36
38
|
maxTasks: z.number().int().optional(),
|
|
39
|
+
provider: ProviderNameSchema.optional(),
|
|
37
40
|
}),
|
|
38
41
|
responses: {
|
|
39
42
|
200: { description: "Agent re-registered (already existed)" },
|
|
@@ -163,6 +166,9 @@ export async function handleAgentRegister(
|
|
|
163
166
|
if (parsed.body.maxTasks !== undefined && parsed.body.maxTasks !== existingAgent.maxTasks) {
|
|
164
167
|
updateAgentMaxTasks(existingAgent.id, parsed.body.maxTasks);
|
|
165
168
|
}
|
|
169
|
+
if (parsed.body.provider && parsed.body.provider !== existingAgent.provider) {
|
|
170
|
+
updateAgentProvider(existingAgent.id, parsed.body.provider);
|
|
171
|
+
}
|
|
166
172
|
resetEmptyPollCount(existingAgent.id);
|
|
167
173
|
return { agent: getAgentById(agentId), created: false };
|
|
168
174
|
}
|
|
@@ -176,6 +182,7 @@ export async function handleAgentRegister(
|
|
|
176
182
|
role: parsed.body.role,
|
|
177
183
|
capabilities: parsed.body.capabilities ?? [],
|
|
178
184
|
maxTasks: parsed.body.maxTasks ?? 1,
|
|
185
|
+
provider: parsed.body.provider,
|
|
179
186
|
});
|
|
180
187
|
|
|
181
188
|
return { agent, created: true };
|
package/src/http/session-data.ts
CHANGED
|
@@ -72,10 +72,10 @@ const createSessionCostRoute = route({
|
|
|
72
72
|
isError: z.boolean().optional(),
|
|
73
73
|
/**
|
|
74
74
|
* Phase 6: when present, drives the codex pricing-table recompute path.
|
|
75
|
-
* Other providers ('claude' / 'pi') always trust harness-reported USD.
|
|
75
|
+
* Other providers ('claude' / 'pi' / 'opencode') always trust harness-reported USD.
|
|
76
76
|
* Optional / undefined keeps back-compat for existing callers.
|
|
77
77
|
*/
|
|
78
|
-
provider: z.enum(["claude", "codex", "pi"]).optional(),
|
|
78
|
+
provider: z.enum(["claude", "codex", "pi", "opencode"]).optional(),
|
|
79
79
|
/**
|
|
80
80
|
* Phase 6: epoch-ms timestamp used as the "active price at time T" lookup
|
|
81
81
|
* basis. Defaults to `Date.now()` when omitted. Including it lets
|
|
@@ -193,7 +193,7 @@ export async function handleSessionData(
|
|
|
193
193
|
// time, recompute `totalCostUsd` from tokens × DB prices and tag the
|
|
194
194
|
// row as 'pricing-table'. If any class has no row, fall back to the
|
|
195
195
|
// worker-reported value with `costSource='harness'` (back-compat for
|
|
196
|
-
// unseeded models). Claude / pi paths always use 'harness'.
|
|
196
|
+
// unseeded models). Claude / pi / opencode paths always use 'harness'.
|
|
197
197
|
let totalCostUsd = parsed.body.totalCostUsd;
|
|
198
198
|
let costSource: SessionCostSource = "harness";
|
|
199
199
|
|
package/src/http/tasks.ts
CHANGED
|
@@ -83,6 +83,7 @@ const updateClaudeSession = route({
|
|
|
83
83
|
z.object({
|
|
84
84
|
claudeSessionId: z.string().min(1),
|
|
85
85
|
provider: z.literal("devin"),
|
|
86
|
+
model: z.string().optional(),
|
|
86
87
|
providerMeta: z.object({
|
|
87
88
|
sessionUrl: z.string(),
|
|
88
89
|
maxAcuLimit: z.number().optional(),
|
|
@@ -92,6 +93,7 @@ const updateClaudeSession = route({
|
|
|
92
93
|
z.object({
|
|
93
94
|
claudeSessionId: z.string().min(1),
|
|
94
95
|
provider: ProviderNameSchema.exclude(["devin"]).optional(),
|
|
96
|
+
model: z.string().optional(),
|
|
95
97
|
providerMeta: z.object({}).optional(),
|
|
96
98
|
}),
|
|
97
99
|
]),
|
|
@@ -312,6 +314,7 @@ export async function handleTasks(
|
|
|
312
314
|
parsed.body.claudeSessionId,
|
|
313
315
|
parsed.body.provider,
|
|
314
316
|
parsed.body.providerMeta,
|
|
317
|
+
parsed.body.model,
|
|
315
318
|
);
|
|
316
319
|
if (!task) {
|
|
317
320
|
jsonError(res, "Task not found", 404);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SessionErrorTracker,
|
|
8
8
|
trackErrorFromJson,
|
|
9
9
|
} from "../utils/error-tracker";
|
|
10
|
+
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
10
11
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
11
12
|
import type {
|
|
12
13
|
CostData,
|
|
@@ -42,70 +43,6 @@ async function cleanupTaskFile(pid: number): Promise<void> {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/** Fetch installed MCP servers from the API and return them as .mcp.json-compatible entries */
|
|
46
|
-
async function fetchInstalledMcpServers(
|
|
47
|
-
apiUrl: string,
|
|
48
|
-
apiKey: string,
|
|
49
|
-
agentId: string,
|
|
50
|
-
): Promise<Record<string, Record<string, unknown>> | null> {
|
|
51
|
-
try {
|
|
52
|
-
const res = await fetch(`${apiUrl}/api/agents/${agentId}/mcp-servers?resolveSecrets=true`, {
|
|
53
|
-
headers: {
|
|
54
|
-
Authorization: `Bearer ${apiKey}`,
|
|
55
|
-
"X-Agent-ID": agentId,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
if (!res.ok) return null;
|
|
59
|
-
|
|
60
|
-
const data = (await res.json()) as {
|
|
61
|
-
servers: Array<{
|
|
62
|
-
name: string;
|
|
63
|
-
transport: string;
|
|
64
|
-
isActive: boolean;
|
|
65
|
-
isEnabled: boolean;
|
|
66
|
-
command?: string;
|
|
67
|
-
args?: string;
|
|
68
|
-
url?: string;
|
|
69
|
-
headers?: string;
|
|
70
|
-
resolvedEnv?: Record<string, string>;
|
|
71
|
-
resolvedHeaders?: Record<string, string>;
|
|
72
|
-
}>;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const entries: Record<string, Record<string, unknown>> = {};
|
|
76
|
-
for (const srv of data.servers.filter((s) => s.isActive && s.isEnabled)) {
|
|
77
|
-
if (srv.transport === "stdio" && srv.command) {
|
|
78
|
-
let args: string[] = [];
|
|
79
|
-
try {
|
|
80
|
-
args = srv.args ? JSON.parse(srv.args) : [];
|
|
81
|
-
} catch {
|
|
82
|
-
// invalid JSON — use empty args
|
|
83
|
-
}
|
|
84
|
-
entries[srv.name] = {
|
|
85
|
-
command: srv.command,
|
|
86
|
-
args,
|
|
87
|
-
env: srv.resolvedEnv || {},
|
|
88
|
-
};
|
|
89
|
-
} else if ((srv.transport === "http" || srv.transport === "sse") && srv.url) {
|
|
90
|
-
let parsedHeaders: Record<string, string> = {};
|
|
91
|
-
try {
|
|
92
|
-
parsedHeaders = srv.headers ? JSON.parse(srv.headers) : {};
|
|
93
|
-
} catch {
|
|
94
|
-
// invalid JSON — use empty headers
|
|
95
|
-
}
|
|
96
|
-
entries[srv.name] = {
|
|
97
|
-
type: srv.transport,
|
|
98
|
-
url: srv.url,
|
|
99
|
-
headers: { ...parsedHeaders, ...(srv.resolvedHeaders || {}) },
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return Object.keys(entries).length > 0 ? entries : null;
|
|
104
|
-
} catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
46
|
/**
|
|
110
47
|
* Merge a base MCP config (typically read from `.mcp.json`) with freshly-resolved
|
|
111
48
|
* installed servers from the API, and inject the per-task `X-Source-Task-Id` header
|
|
@@ -592,7 +529,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
592
529
|
// Fetch installed MCP servers from API for this agent
|
|
593
530
|
const installedServers =
|
|
594
531
|
config.apiUrl && config.apiKey && config.agentId
|
|
595
|
-
? await fetchInstalledMcpServers(config.apiUrl, config.apiKey, config.agentId)
|
|
532
|
+
? await fetchInstalledMcpServers(config.apiUrl, config.apiKey, config.agentId, "claude")
|
|
596
533
|
: null;
|
|
597
534
|
if (installedServers) {
|
|
598
535
|
console.log(
|
package/src/providers/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { ClaudeAdapter } from "./claude-adapter";
|
|
|
12
12
|
import { ClaudeManagedAdapter } from "./claude-managed-adapter";
|
|
13
13
|
import { CodexAdapter } from "./codex-adapter";
|
|
14
14
|
import { DevinAdapter } from "./devin-adapter";
|
|
15
|
+
import { OpencodeAdapter } from "./opencode-adapter";
|
|
15
16
|
import { PiMonoAdapter } from "./pi-mono-adapter";
|
|
16
17
|
import type { ProviderAdapter } from "./types";
|
|
17
18
|
|
|
@@ -28,9 +29,11 @@ export function createProviderAdapter(provider: string): ProviderAdapter {
|
|
|
28
29
|
return new ClaudeManagedAdapter();
|
|
29
30
|
case "devin":
|
|
30
31
|
return new DevinAdapter();
|
|
32
|
+
case "opencode":
|
|
33
|
+
return new OpencodeAdapter();
|
|
31
34
|
default:
|
|
32
35
|
throw new Error(
|
|
33
|
-
`Unknown HARNESS_PROVIDER: "${provider}". Supported: claude, pi, codex, devin, claude-managed`,
|
|
36
|
+
`Unknown HARNESS_PROVIDER: "${provider}". Supported: claude, pi, codex, devin, claude-managed, opencode`,
|
|
34
37
|
);
|
|
35
38
|
}
|
|
36
39
|
}
|