@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 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, or Claude Managed Agents. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
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.4",
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.73.4",
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": "latest",
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.67.2",
107
- "@mariozechner/pi-ai": "^0.67.2",
108
- "@mariozechner/pi-coding-agent": "^0.67.2",
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.118.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;
@@ -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;
@@ -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 };
@@ -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(
@@ -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
  }