@desplega.ai/agent-swarm 1.83.1 → 1.83.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 +139 -8
- package/package.json +1 -1
- package/src/artifact-sdk/server.ts +23 -1
- package/src/be/budget-admission.ts +28 -4
- package/src/be/budget-refusal-notify.ts +19 -3
- package/src/be/db-queries/oauth.ts +43 -0
- package/src/be/db.ts +35 -2
- package/src/be/migrations/074_user_budget_scope.sql +85 -0
- package/src/commands/resume-session.ts +118 -0
- package/src/commands/runner.ts +137 -67
- package/src/http/core.ts +4 -1
- package/src/http/index.ts +16 -0
- package/src/http/integrations.ts +26 -0
- package/src/http/mcp-user.ts +111 -0
- package/src/http/poll.ts +19 -5
- package/src/http/schedules.ts +1 -1
- package/src/http/users.ts +107 -2
- package/src/jira/client.ts +3 -5
- package/src/jira/oauth.ts +1 -0
- package/src/jira/sync.ts +2 -2
- package/src/oauth/ensure-token.ts +1 -0
- package/src/oauth/wrapper.ts +38 -7
- package/src/providers/claude-adapter.ts +7 -2
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +30 -0
- package/src/providers/opencode-adapter.ts +149 -14
- package/src/providers/pi-mono-adapter.ts +41 -1
- package/src/providers/types.ts +1 -1
- package/src/server-user.ts +117 -0
- package/src/tests/artifact-sdk.test.ts +23 -19
- package/src/tests/budget-user-scope.test.ts +376 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-adapter.test.ts +192 -0
- package/src/tests/codex-rate-limit-parse.test.ts +256 -0
- package/src/tests/db-queries-oauth.test.ts +43 -0
- package/src/tests/ensure-token.test.ts +93 -0
- package/src/tests/error-tracker.test.ts +52 -0
- package/src/tests/fetch-resolved-env.test.ts +33 -20
- package/src/tests/http-users.test.ts +29 -1
- package/src/tests/mcp-user-route.test.ts +325 -0
- package/src/tests/opencode-adapter.test.ts +75 -0
- package/src/tests/pi-mono-adapter.test.ts +21 -1
- package/src/tests/rate-limit-event.test.ts +69 -6
- package/src/tests/resume-session.test.ts +93 -0
- package/src/tests/task-tools-ctx.test.ts +100 -0
- package/src/tests/task-tools-ownership.test.ts +167 -0
- package/src/tests/user-token-routes.test.ts +221 -0
- package/src/tools/cancel-task.ts +137 -83
- package/src/tools/get-task-details.ts +73 -59
- package/src/tools/get-tasks.ts +134 -126
- package/src/tools/send-task.ts +312 -312
- package/src/tools/task-action.ts +464 -367
- package/src/tools/task-tool-ctx.ts +43 -0
- package/src/types.ts +6 -2
- package/src/utils/error-tracker.ts +122 -9
|
@@ -100,6 +100,74 @@ function isAssistantMessage(msg: unknown): msg is AssistantMessage {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const DOCKER_PLUGIN_PATH = "/home/worker/.config/opencode/plugins/agent-swarm.ts";
|
|
103
|
+
const MODEL_CACHE_REFRESH_TIMEOUT_MS = 15_000;
|
|
104
|
+
|
|
105
|
+
function isOpenRouterModel(model: string | undefined): boolean {
|
|
106
|
+
return Boolean(model?.toLowerCase().startsWith("openrouter/"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isModelNotFoundError(message: string): boolean {
|
|
110
|
+
return /model not found:/i.test(message);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function readSpawnOutput(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
114
|
+
if (!stream) return "";
|
|
115
|
+
return await new Response(stream).text();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatUnknownError(err: unknown): string {
|
|
119
|
+
return err instanceof Error ? err.message : String(err);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function refreshOpenRouterModelCache(
|
|
123
|
+
opencodeConfig: Config & { plugin?: string[] },
|
|
124
|
+
configFilePath: string,
|
|
125
|
+
dataHomePath: string,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const binary = process.env.OPENCODE_BINARY || "opencode";
|
|
128
|
+
const proc = Bun.spawn([binary, "models", "--refresh", "openrouter"], {
|
|
129
|
+
stdout: "pipe",
|
|
130
|
+
stderr: "pipe",
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
OPENCODE_CONFIG: configFilePath,
|
|
134
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeConfig),
|
|
135
|
+
OPENCODE_DATA_HOME: dataHomePath,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let timedOut = false;
|
|
140
|
+
const timeout = setTimeout(() => {
|
|
141
|
+
timedOut = true;
|
|
142
|
+
proc.kill();
|
|
143
|
+
}, MODEL_CACHE_REFRESH_TIMEOUT_MS);
|
|
144
|
+
|
|
145
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
146
|
+
readSpawnOutput(proc.stdout),
|
|
147
|
+
readSpawnOutput(proc.stderr),
|
|
148
|
+
proc.exited,
|
|
149
|
+
]).finally(() => clearTimeout(timeout));
|
|
150
|
+
|
|
151
|
+
if (timedOut) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`opencode models --refresh openrouter timed out after ${MODEL_CACHE_REFRESH_TIMEOUT_MS}ms`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (exitCode !== 0) {
|
|
157
|
+
const detail = scrubSecrets([stderr.trim(), stdout.trim()].filter(Boolean).join("\n"));
|
|
158
|
+
throw new Error(
|
|
159
|
+
`opencode models --refresh openrouter exited with code ${exitCode}${detail ? `: ${detail}` : ""}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let refreshOpenRouterModelCacheImpl = refreshOpenRouterModelCache;
|
|
165
|
+
|
|
166
|
+
export function _setOpenRouterModelCacheRefreshForTests(
|
|
167
|
+
fn: typeof refreshOpenRouterModelCache | null,
|
|
168
|
+
): void {
|
|
169
|
+
refreshOpenRouterModelCacheImpl = fn ?? refreshOpenRouterModelCache;
|
|
170
|
+
}
|
|
103
171
|
|
|
104
172
|
function resolvePluginPath(): string {
|
|
105
173
|
const override = process.env.OPENCODE_SWARM_PLUGIN_PATH;
|
|
@@ -141,6 +209,8 @@ export class OpencodeSession implements ProviderSession {
|
|
|
141
209
|
private agentFilePath: string;
|
|
142
210
|
private configFilePath: string;
|
|
143
211
|
private dataHomePath: string;
|
|
212
|
+
private retryAfterModelRefresh?: () => Promise<boolean>;
|
|
213
|
+
private modelRefreshRecoveryInFlight = false;
|
|
144
214
|
|
|
145
215
|
// Track which tool callIDs have already emitted tool_start, so transitions
|
|
146
216
|
// through pending → running → completed don't fire duplicate events.
|
|
@@ -155,6 +225,7 @@ export class OpencodeSession implements ProviderSession {
|
|
|
155
225
|
agentFilePath: string,
|
|
156
226
|
configFilePath: string,
|
|
157
227
|
dataHomePath: string,
|
|
228
|
+
retryAfterModelRefresh?: () => Promise<boolean>,
|
|
158
229
|
) {
|
|
159
230
|
this._sessionId = sessionId;
|
|
160
231
|
this.server = server;
|
|
@@ -164,6 +235,7 @@ export class OpencodeSession implements ProviderSession {
|
|
|
164
235
|
this.agentFilePath = agentFilePath;
|
|
165
236
|
this.configFilePath = configFilePath;
|
|
166
237
|
this.dataHomePath = dataHomePath;
|
|
238
|
+
this.retryAfterModelRefresh = retryAfterModelRefresh;
|
|
167
239
|
this.completionPromise = new Promise<ProviderResult>((resolve, reject) => {
|
|
168
240
|
this.completionResolve = resolve;
|
|
169
241
|
this.completionReject = reject;
|
|
@@ -211,6 +283,33 @@ export class OpencodeSession implements ProviderSession {
|
|
|
211
283
|
for (const l of this.listeners) l(event);
|
|
212
284
|
}
|
|
213
285
|
|
|
286
|
+
emitModelCacheRefreshProgress(): void {
|
|
287
|
+
this.emit({
|
|
288
|
+
type: "progress",
|
|
289
|
+
message: "opencode model cache is stale; refreshing OpenRouter models and retrying once",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
emitModelCacheRefreshFailure(message: string, err: unknown): void {
|
|
294
|
+
this.emitError(`${message}; OpenRouter model cache refresh failed: ${formatUnknownError(err)}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private recoverFromModelNotFound(message: string): void {
|
|
298
|
+
if (!this.retryAfterModelRefresh || this.modelRefreshRecoveryInFlight) {
|
|
299
|
+
this.emitError(message);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.modelRefreshRecoveryInFlight = true;
|
|
303
|
+
this.emitModelCacheRefreshProgress();
|
|
304
|
+
this.retryAfterModelRefresh()
|
|
305
|
+
.then((retried) => {
|
|
306
|
+
if (!retried) this.emitError(message);
|
|
307
|
+
})
|
|
308
|
+
.catch((err: unknown) => {
|
|
309
|
+
this.emitModelCacheRefreshFailure(message, err);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
214
313
|
/** Best-effort cleanup of per-task isolation files and directories. */
|
|
215
314
|
private async cleanupFiles(): Promise<void> {
|
|
216
315
|
try {
|
|
@@ -361,6 +460,10 @@ export class OpencodeSession implements ProviderSession {
|
|
|
361
460
|
ev.properties.error && "message" in ev.properties.error
|
|
362
461
|
? String((ev.properties.error as { message?: string }).message ?? "unknown error")
|
|
363
462
|
: "opencode session error";
|
|
463
|
+
if (isModelNotFoundError(errMsg)) {
|
|
464
|
+
this.recoverFromModelNotFound(errMsg);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
364
467
|
this.emitError(errMsg);
|
|
365
468
|
break;
|
|
366
469
|
}
|
|
@@ -562,6 +665,30 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
562
665
|
const opencodeSession = createResult.data;
|
|
563
666
|
const sessionId = opencodeSession.id;
|
|
564
667
|
|
|
668
|
+
let promptRefreshAttempted = false;
|
|
669
|
+
let promptRefreshPromise: Promise<boolean> | undefined;
|
|
670
|
+
const sendPrompt = async () => {
|
|
671
|
+
await client.session.prompt({
|
|
672
|
+
path: { id: sessionId },
|
|
673
|
+
query: { directory: config.cwd },
|
|
674
|
+
body: {
|
|
675
|
+
agent: agentName,
|
|
676
|
+
parts: [{ type: "text", text: config.prompt }],
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
};
|
|
680
|
+
const refreshOpenRouterAndRetryPrompt = async (): Promise<boolean> => {
|
|
681
|
+
if (promptRefreshPromise) return await promptRefreshPromise;
|
|
682
|
+
if (promptRefreshAttempted || !isOpenRouterModel(config.model)) return false;
|
|
683
|
+
promptRefreshAttempted = true;
|
|
684
|
+
promptRefreshPromise = (async () => {
|
|
685
|
+
await refreshOpenRouterModelCacheImpl(opencodeConfig, configFilePath, dataHomePath);
|
|
686
|
+
await sendPrompt();
|
|
687
|
+
return true;
|
|
688
|
+
})();
|
|
689
|
+
return await promptRefreshPromise;
|
|
690
|
+
};
|
|
691
|
+
|
|
565
692
|
const session = new OpencodeSession(
|
|
566
693
|
sessionId,
|
|
567
694
|
server,
|
|
@@ -571,6 +698,7 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
571
698
|
agentFilePath,
|
|
572
699
|
configFilePath,
|
|
573
700
|
dataHomePath,
|
|
701
|
+
isOpenRouterModel(config.model) ? refreshOpenRouterAndRetryPrompt : undefined,
|
|
574
702
|
);
|
|
575
703
|
|
|
576
704
|
// Emit session_init synchronously; the session buffers events until the
|
|
@@ -594,21 +722,28 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
594
722
|
});
|
|
595
723
|
|
|
596
724
|
// Fire-and-forget: send the prompt using the per-task agent
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
725
|
+
sendPrompt().catch((err: unknown) => {
|
|
726
|
+
const message = formatUnknownError(err);
|
|
727
|
+
if (isModelNotFoundError(message) && isOpenRouterModel(config.model)) {
|
|
728
|
+
session.emitModelCacheRefreshProgress();
|
|
729
|
+
refreshOpenRouterAndRetryPrompt()
|
|
730
|
+
.then((retried) => {
|
|
731
|
+
if (retried) return;
|
|
732
|
+
session.handleOpencodeEvent({
|
|
733
|
+
type: "session.error",
|
|
734
|
+
properties: { sessionID: sessionId, error: { message } as never },
|
|
735
|
+
});
|
|
736
|
+
})
|
|
737
|
+
.catch((retryErr: unknown) => {
|
|
738
|
+
session.emitModelCacheRefreshFailure(message, retryErr);
|
|
739
|
+
});
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
session.handleOpencodeEvent({
|
|
743
|
+
type: "session.error",
|
|
744
|
+
properties: { sessionID: sessionId, error: { message } as never },
|
|
611
745
|
});
|
|
746
|
+
});
|
|
612
747
|
|
|
613
748
|
return session;
|
|
614
749
|
}
|
|
@@ -17,9 +17,11 @@ import type {
|
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import {
|
|
19
19
|
type AgentSession,
|
|
20
|
+
AuthStorage,
|
|
20
21
|
createAgentSession,
|
|
21
22
|
DefaultResourceLoader,
|
|
22
23
|
getAgentDir,
|
|
24
|
+
ModelRegistry,
|
|
23
25
|
SessionManager,
|
|
24
26
|
} from "@earendil-works/pi-coding-agent";
|
|
25
27
|
import { type TSchema, Type } from "typebox";
|
|
@@ -180,6 +182,39 @@ function envHasAnthropicCred(env: Record<string, string | undefined>): boolean {
|
|
|
180
182
|
return !!(env.ANTHROPIC_API_KEY || env.ANTHROPIC_OAUTH_TOKEN);
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
const PI_RUNTIME_API_KEYS = [
|
|
186
|
+
["OPENROUTER_API_KEY", "openrouter"],
|
|
187
|
+
["ANTHROPIC_API_KEY", "anthropic"],
|
|
188
|
+
["OPENAI_API_KEY", "openai"],
|
|
189
|
+
["GOOGLE_API_KEY", "google"],
|
|
190
|
+
] as const;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build pi-coding-agent auth services from the runner's per-task resolved env.
|
|
194
|
+
*
|
|
195
|
+
* The runner intentionally does not copy rotated credential-pool selections
|
|
196
|
+
* into `process.env` because that would freeze rotation globally. pi-mono runs
|
|
197
|
+
* in-process, so pass selected keys through pi's runtime auth override instead
|
|
198
|
+
* of relying on environment lookup.
|
|
199
|
+
*/
|
|
200
|
+
export function createPiRuntimeAuth(env: Record<string, string | undefined> = process.env): {
|
|
201
|
+
authStorage: AuthStorage;
|
|
202
|
+
modelRegistry: ModelRegistry;
|
|
203
|
+
} {
|
|
204
|
+
const authStorage = AuthStorage.create();
|
|
205
|
+
for (const [envKey, provider] of PI_RUNTIME_API_KEYS) {
|
|
206
|
+
const apiKey = env[envKey];
|
|
207
|
+
if (apiKey) {
|
|
208
|
+
authStorage.setRuntimeApiKey(provider, apiKey);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
authStorage,
|
|
214
|
+
modelRegistry: ModelRegistry.create(authStorage),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
183
218
|
/**
|
|
184
219
|
* Resolve a model string to a pi-ai Model object.
|
|
185
220
|
*
|
|
@@ -672,8 +707,11 @@ export class PiMonoAdapter implements ProviderAdapter {
|
|
|
672
707
|
}
|
|
673
708
|
}
|
|
674
709
|
|
|
710
|
+
const sessionEnv = config.env ?? process.env;
|
|
711
|
+
|
|
675
712
|
// 3. Resolve model
|
|
676
|
-
const model = resolveModel(config.model);
|
|
713
|
+
const model = resolveModel(config.model, sessionEnv);
|
|
714
|
+
const { authStorage, modelRegistry } = createPiRuntimeAuth(sessionEnv);
|
|
677
715
|
|
|
678
716
|
// 4. Create swarm hooks extension
|
|
679
717
|
const swarmExtension = createSwarmHooksExtension({
|
|
@@ -698,6 +736,8 @@ export class PiMonoAdapter implements ProviderAdapter {
|
|
|
698
736
|
model,
|
|
699
737
|
customTools,
|
|
700
738
|
resourceLoader,
|
|
739
|
+
authStorage,
|
|
740
|
+
modelRegistry,
|
|
701
741
|
};
|
|
702
742
|
|
|
703
743
|
// 7. Create the session
|
package/src/providers/types.ts
CHANGED
|
@@ -125,7 +125,7 @@ export interface ProviderResult {
|
|
|
125
125
|
* ISO timestamp of the rate limit reset time, parsed from a structured
|
|
126
126
|
* `rate_limit_event` line in the Claude CLI stream. Only set by the Claude
|
|
127
127
|
* adapter when a `status: "rejected"` event is present. Already clamped to
|
|
128
|
-
* [now+60s, now+
|
|
128
|
+
* [now+60s, now+7d] at the source. The runner uses this as tier-1 of the
|
|
129
129
|
* three-tier cooldown resolver.
|
|
130
130
|
*/
|
|
131
131
|
rateLimitResetAt?: string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import pkg from "../package.json";
|
|
4
|
+
import {
|
|
5
|
+
cancelTaskHandler,
|
|
6
|
+
cancelTaskInputSchema,
|
|
7
|
+
cancelTaskOutputSchema,
|
|
8
|
+
} from "./tools/cancel-task";
|
|
9
|
+
import {
|
|
10
|
+
getTaskDetailsHandler,
|
|
11
|
+
getTaskDetailsInputSchema,
|
|
12
|
+
getTaskDetailsOutputSchema,
|
|
13
|
+
} from "./tools/get-task-details";
|
|
14
|
+
import { getTasksHandler, getTasksInputSchema, getTasksOutputSchema } from "./tools/get-tasks";
|
|
15
|
+
import { sendTaskHandler, sendTaskOutputSchema } from "./tools/send-task";
|
|
16
|
+
import {
|
|
17
|
+
taskActionHandler,
|
|
18
|
+
taskActionInputSchema,
|
|
19
|
+
taskActionOutputSchema,
|
|
20
|
+
} from "./tools/task-action";
|
|
21
|
+
import { userCtx } from "./tools/task-tool-ctx";
|
|
22
|
+
import { createToolRegistrar } from "./tools/utils";
|
|
23
|
+
import type { User } from "./types";
|
|
24
|
+
|
|
25
|
+
const userSendTaskInputSchema = z.object({
|
|
26
|
+
task: z.string().min(1).describe("The task description to send."),
|
|
27
|
+
taskType: z.string().max(50).optional().describe("Task type (e.g., 'bug', 'feature', 'review')."),
|
|
28
|
+
tags: z.array(z.string()).optional().describe("Tags for filtering (e.g., ['urgent'])."),
|
|
29
|
+
priority: z.number().int().min(0).max(100).optional().describe("Priority 0-100 (default: 50)."),
|
|
30
|
+
model: z
|
|
31
|
+
.enum(["haiku", "sonnet", "opus"])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Model to use for this task ('haiku', 'sonnet', or 'opus')."),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function createUserServer(user: User): McpServer {
|
|
37
|
+
const server = new McpServer(
|
|
38
|
+
{
|
|
39
|
+
name: `${pkg.name}-user`,
|
|
40
|
+
version: pkg.version,
|
|
41
|
+
description: "End-user task MCP surface for Agent Swarm.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
capabilities: {
|
|
45
|
+
logging: {},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const registerTool = createToolRegistrar(server);
|
|
51
|
+
|
|
52
|
+
registerTool(
|
|
53
|
+
"send-task",
|
|
54
|
+
{
|
|
55
|
+
title: "Send a task",
|
|
56
|
+
annotations: { destructiveHint: false },
|
|
57
|
+
description: "Creates an unassigned task requested by the authenticated user.",
|
|
58
|
+
inputSchema: userSendTaskInputSchema,
|
|
59
|
+
outputSchema: sendTaskOutputSchema,
|
|
60
|
+
},
|
|
61
|
+
async (args, info, _meta) =>
|
|
62
|
+
sendTaskHandler(userCtx(user, info.sessionId), {
|
|
63
|
+
offerMode: false,
|
|
64
|
+
allowDuplicate: false,
|
|
65
|
+
...args,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
registerTool(
|
|
70
|
+
"get-tasks",
|
|
71
|
+
{
|
|
72
|
+
title: "Get tasks",
|
|
73
|
+
description: "Returns tasks requested by the authenticated user.",
|
|
74
|
+
annotations: { readOnlyHint: true },
|
|
75
|
+
inputSchema: getTasksInputSchema,
|
|
76
|
+
outputSchema: getTasksOutputSchema,
|
|
77
|
+
},
|
|
78
|
+
async (args, info, _meta) => getTasksHandler(userCtx(user, info.sessionId), args),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
registerTool(
|
|
82
|
+
"get-task-details",
|
|
83
|
+
{
|
|
84
|
+
title: "Get task details",
|
|
85
|
+
description: "Returns detailed information about one of your tasks.",
|
|
86
|
+
annotations: { readOnlyHint: true },
|
|
87
|
+
inputSchema: getTaskDetailsInputSchema,
|
|
88
|
+
outputSchema: getTaskDetailsOutputSchema,
|
|
89
|
+
},
|
|
90
|
+
async (args, info, _meta) => getTaskDetailsHandler(userCtx(user, info.sessionId), args),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
registerTool(
|
|
94
|
+
"cancel-task",
|
|
95
|
+
{
|
|
96
|
+
title: "Cancel Task",
|
|
97
|
+
description: "Cancel one of your pending or in-progress tasks.",
|
|
98
|
+
annotations: { destructiveHint: true },
|
|
99
|
+
inputSchema: cancelTaskInputSchema,
|
|
100
|
+
outputSchema: cancelTaskOutputSchema,
|
|
101
|
+
},
|
|
102
|
+
async (args, info, _meta) => cancelTaskHandler(userCtx(user, info.sessionId), args),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
registerTool(
|
|
106
|
+
"task-action",
|
|
107
|
+
{
|
|
108
|
+
title: "Task Pool Action",
|
|
109
|
+
description: "Move one of your tasks to or from backlog.",
|
|
110
|
+
inputSchema: taskActionInputSchema,
|
|
111
|
+
outputSchema: taskActionOutputSchema,
|
|
112
|
+
},
|
|
113
|
+
async (args, info, _meta) => taskActionHandler(userCtx(user, info.sessionId), args),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return server;
|
|
117
|
+
}
|
|
@@ -2,7 +2,11 @@ import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
|
|
2
2
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { BROWSER_SDK_JS } from "../artifact-sdk/browser-sdk";
|
|
4
4
|
import { getAvailablePort } from "../artifact-sdk/port";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createArtifactServer,
|
|
7
|
+
createBunHonoFetchHandler,
|
|
8
|
+
createBunResponse,
|
|
9
|
+
} from "../artifact-sdk/server";
|
|
6
10
|
import { getBasePrompt } from "../prompts/base-prompt";
|
|
7
11
|
|
|
8
12
|
// ─── Port allocation tests ──────────────────────────────────────────────
|
|
@@ -25,7 +29,7 @@ describe("getAvailablePort", () => {
|
|
|
25
29
|
// Try to start a Bun server on the port — should succeed
|
|
26
30
|
const server = Bun.serve({
|
|
27
31
|
port,
|
|
28
|
-
fetch: () =>
|
|
32
|
+
fetch: () => createBunResponse("ok"),
|
|
29
33
|
});
|
|
30
34
|
expect(server.port).toBe(port);
|
|
31
35
|
server.stop();
|
|
@@ -172,7 +176,7 @@ describe("createArtifactServer", () => {
|
|
|
172
176
|
});
|
|
173
177
|
honoApp.route("/", app);
|
|
174
178
|
|
|
175
|
-
const server = Bun.serve({ port, fetch: honoApp
|
|
179
|
+
const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(honoApp) });
|
|
176
180
|
|
|
177
181
|
try {
|
|
178
182
|
// Test content serving
|
|
@@ -211,7 +215,7 @@ describe("createArtifactServer", () => {
|
|
|
211
215
|
const app = new Hono();
|
|
212
216
|
app.use("/*", serveStatic({ root: testDir }));
|
|
213
217
|
|
|
214
|
-
const server = Bun.serve({ port, fetch: app
|
|
218
|
+
const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(app) });
|
|
215
219
|
|
|
216
220
|
try {
|
|
217
221
|
const res = await fetch(`http://localhost:${port}/index.html`);
|
|
@@ -244,7 +248,7 @@ describe("createArtifactServer", () => {
|
|
|
244
248
|
}
|
|
245
249
|
});
|
|
246
250
|
|
|
247
|
-
const server = Bun.serve({ port, fetch: app
|
|
251
|
+
const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(app) });
|
|
248
252
|
|
|
249
253
|
try {
|
|
250
254
|
const res = await fetch(`http://localhost:${port}/@swarm/api/agents`);
|
|
@@ -272,7 +276,7 @@ describe("createArtifactServer", () => {
|
|
|
272
276
|
});
|
|
273
277
|
});
|
|
274
278
|
|
|
275
|
-
const server = Bun.serve({ port, fetch: app
|
|
279
|
+
const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(app) });
|
|
276
280
|
|
|
277
281
|
try {
|
|
278
282
|
const res = await fetch(`http://localhost:${port}/@swarm/api/tasks`, {
|
|
@@ -303,7 +307,7 @@ describe("createArtifactServer", () => {
|
|
|
303
307
|
for (const [key, value] of req.headers.entries()) {
|
|
304
308
|
capturedHeaders[key.toLowerCase()] = value;
|
|
305
309
|
}
|
|
306
|
-
return
|
|
310
|
+
return createBunResponse(JSON.stringify({ success: true }), {
|
|
307
311
|
headers: { "Content-Type": "application/json" },
|
|
308
312
|
});
|
|
309
313
|
},
|
|
@@ -336,7 +340,7 @@ describe("createArtifactServer", () => {
|
|
|
336
340
|
}
|
|
337
341
|
});
|
|
338
342
|
|
|
339
|
-
const proxy = Bun.serve({ port: proxyPort, fetch: app
|
|
343
|
+
const proxy = Bun.serve({ port: proxyPort, fetch: createBunHonoFetchHandler(app) });
|
|
340
344
|
|
|
341
345
|
try {
|
|
342
346
|
// Test GET request headers
|
|
@@ -369,7 +373,7 @@ describe("createArtifactServer", () => {
|
|
|
369
373
|
const mockMcp = Bun.serve({
|
|
370
374
|
port: mockMcpPort,
|
|
371
375
|
fetch: () =>
|
|
372
|
-
|
|
376
|
+
createBunResponse(JSON.stringify({ ok: true }), {
|
|
373
377
|
headers: { "Content-Type": "application/json" },
|
|
374
378
|
}),
|
|
375
379
|
});
|
|
@@ -393,7 +397,7 @@ describe("createArtifactServer", () => {
|
|
|
393
397
|
}
|
|
394
398
|
});
|
|
395
399
|
|
|
396
|
-
const proxy = Bun.serve({ port: proxyPort, fetch: app
|
|
400
|
+
const proxy = Bun.serve({ port: proxyPort, fetch: createBunHonoFetchHandler(app) });
|
|
397
401
|
|
|
398
402
|
try {
|
|
399
403
|
const res = await fetch(`http://localhost:${proxyPort}/@swarm/api/agents`);
|
|
@@ -415,7 +419,7 @@ describe("createArtifactServer", () => {
|
|
|
415
419
|
port: mockMcpPort,
|
|
416
420
|
fetch: (req) => {
|
|
417
421
|
capturedPath = new URL(req.url).pathname;
|
|
418
|
-
return
|
|
422
|
+
return createBunResponse(JSON.stringify({ ok: true }));
|
|
419
423
|
},
|
|
420
424
|
});
|
|
421
425
|
|
|
@@ -431,7 +435,7 @@ describe("createArtifactServer", () => {
|
|
|
431
435
|
}
|
|
432
436
|
});
|
|
433
437
|
|
|
434
|
-
const proxy = Bun.serve({ port: proxyPort, fetch: app
|
|
438
|
+
const proxy = Bun.serve({ port: proxyPort, fetch: createBunHonoFetchHandler(app) });
|
|
435
439
|
|
|
436
440
|
try {
|
|
437
441
|
await fetch(`http://localhost:${proxyPort}/@swarm/api/tasks/123/progress`);
|
|
@@ -535,7 +539,7 @@ describe("artifact CLI command", () => {
|
|
|
535
539
|
fetch: (req) => {
|
|
536
540
|
const url = new URL(req.url);
|
|
537
541
|
if (url.pathname === "/api/services") {
|
|
538
|
-
return
|
|
542
|
+
return createBunResponse(
|
|
539
543
|
JSON.stringify({
|
|
540
544
|
services: [
|
|
541
545
|
{
|
|
@@ -561,7 +565,7 @@ describe("artifact CLI command", () => {
|
|
|
561
565
|
}),
|
|
562
566
|
);
|
|
563
567
|
}
|
|
564
|
-
return
|
|
568
|
+
return createBunResponse("Not found", { status: 404 });
|
|
565
569
|
},
|
|
566
570
|
});
|
|
567
571
|
|
|
@@ -595,7 +599,7 @@ describe("artifact CLI command", () => {
|
|
|
595
599
|
const mockPort = await getAvailablePort();
|
|
596
600
|
const mockServer = Bun.serve({
|
|
597
601
|
port: mockPort,
|
|
598
|
-
fetch: () =>
|
|
602
|
+
fetch: () => createBunResponse(JSON.stringify({ services: [] })),
|
|
599
603
|
});
|
|
600
604
|
|
|
601
605
|
const origEnv = { ...process.env };
|
|
@@ -627,7 +631,7 @@ describe("artifact CLI command", () => {
|
|
|
627
631
|
const mockServer = Bun.serve({
|
|
628
632
|
port: mockPort,
|
|
629
633
|
fetch: () =>
|
|
630
|
-
|
|
634
|
+
createBunResponse(
|
|
631
635
|
JSON.stringify({
|
|
632
636
|
services: [
|
|
633
637
|
{
|
|
@@ -685,7 +689,7 @@ describe("artifact CLI command", () => {
|
|
|
685
689
|
fetch: (req) => {
|
|
686
690
|
const url = new URL(req.url);
|
|
687
691
|
if (req.method === "GET" && url.pathname === "/api/services") {
|
|
688
|
-
return
|
|
692
|
+
return createBunResponse(
|
|
689
693
|
JSON.stringify({
|
|
690
694
|
services: [
|
|
691
695
|
{
|
|
@@ -699,9 +703,9 @@ describe("artifact CLI command", () => {
|
|
|
699
703
|
}
|
|
700
704
|
if (req.method === "DELETE" && url.pathname.startsWith("/api/services/")) {
|
|
701
705
|
deletedServiceId = url.pathname.split("/").pop() || "";
|
|
702
|
-
return
|
|
706
|
+
return createBunResponse(JSON.stringify({ success: true }));
|
|
703
707
|
}
|
|
704
|
-
return
|
|
708
|
+
return createBunResponse("Not found", { status: 404 });
|
|
705
709
|
},
|
|
706
710
|
});
|
|
707
711
|
|