@desplega.ai/agent-swarm 1.83.1 → 1.84.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/openapi.json +158 -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/http/webhooks.ts +101 -0
- package/src/integrations/kapso/client.ts +198 -0
- package/src/integrations/kapso/config.ts +104 -0
- package/src/integrations/kapso/inbound.ts +111 -0
- 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/server.ts +14 -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/kapso-client.test.ts +94 -0
- package/src/tests/kapso-inbound.test.ts +198 -0
- 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/tool-annotations.test.ts +3 -2
- 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/register-kapso-number.ts +210 -0
- 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/tools/templates.ts +35 -0
- package/src/tools/tool-config.ts +6 -0
- package/src/tools/whatsapp-message.ts +135 -0
- package/src/types.ts +6 -2
- package/src/utils/error-tracker.ts +122 -9
- package/templates/skills/agentmail-sending/SKILL.md +49 -0
- package/templates/skills/kapso-whatsapp/SKILL.md +383 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
createServer as createHttpServer,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type Server,
|
|
7
|
+
type ServerResponse,
|
|
8
|
+
} from "node:http";
|
|
9
|
+
import { closeDb, createUser, getDb, initDb } from "../be/db";
|
|
10
|
+
import { fingerprintApiKey } from "../be/users";
|
|
11
|
+
import { handleCore } from "../http/core";
|
|
12
|
+
import { handleUsers } from "../http/users";
|
|
13
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
14
|
+
|
|
15
|
+
const TEST_DB_PATH = "./test-user-token-routes.sqlite";
|
|
16
|
+
const API_KEY = "test-user-token-key";
|
|
17
|
+
const ORIGINAL_API_KEY = process.env.AGENT_SWARM_API_KEY;
|
|
18
|
+
|
|
19
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
20
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
21
|
+
try {
|
|
22
|
+
await unlink(path + suffix);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function listen(server: Server): Promise<number> {
|
|
30
|
+
const port = 15174;
|
|
31
|
+
await new Promise<void>((resolve, reject) => {
|
|
32
|
+
server.once("error", reject);
|
|
33
|
+
server.listen(port, "127.0.0.1", () => {
|
|
34
|
+
server.off("error", reject);
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const addr = server.address();
|
|
39
|
+
if (!addr || typeof addr === "string") throw new Error("no port");
|
|
40
|
+
return addr.port;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createTestServer(apiKey: string): Server {
|
|
44
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
45
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
46
|
+
const handled = await handleCore(req, res, myAgentId, apiKey);
|
|
47
|
+
if (handled) return;
|
|
48
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
49
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
50
|
+
const ok = await handleUsers(req, res, pathSegments, queryParams);
|
|
51
|
+
if (!ok) {
|
|
52
|
+
res.writeHead(404);
|
|
53
|
+
res.end("Not Found");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let server: Server;
|
|
59
|
+
let port: number;
|
|
60
|
+
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
63
|
+
initDb(TEST_DB_PATH);
|
|
64
|
+
process.env.AGENT_SWARM_API_KEY = API_KEY;
|
|
65
|
+
server = createTestServer(API_KEY);
|
|
66
|
+
port = await listen(server);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterAll(async () => {
|
|
70
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
71
|
+
closeDb();
|
|
72
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
73
|
+
if (ORIGINAL_API_KEY === undefined) {
|
|
74
|
+
delete process.env.AGENT_SWARM_API_KEY;
|
|
75
|
+
} else {
|
|
76
|
+
process.env.AGENT_SWARM_API_KEY = ORIGINAL_API_KEY;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
const db = getDb();
|
|
82
|
+
db.run("DELETE FROM user_identity_events");
|
|
83
|
+
db.run("DELETE FROM user_tokens");
|
|
84
|
+
db.run("DELETE FROM users");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function url(path: string): string {
|
|
88
|
+
return `http://127.0.0.1:${port}${path}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function authedFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
92
|
+
const headers: Record<string, string> = {
|
|
93
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
...((init.headers as Record<string, string>) ?? {}),
|
|
96
|
+
};
|
|
97
|
+
return fetch(url(path), { ...init, headers });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type TokenSummary = {
|
|
101
|
+
id: string;
|
|
102
|
+
userId: string;
|
|
103
|
+
label: string | null;
|
|
104
|
+
tokenPreview: string;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
lastUsedAt: string | null;
|
|
107
|
+
revokedAt: string | null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
describe("operator MCP token routes", () => {
|
|
111
|
+
test("POST mints an aswt_ plaintext once and persists only hash + preview", async () => {
|
|
112
|
+
const user = createUser({ name: "Token User", email: "token@example.com" });
|
|
113
|
+
|
|
114
|
+
const response = await authedFetch(`/api/users/${user.id}/mcp-tokens`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: JSON.stringify({ label: "laptop" }),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(response.status).toBe(200);
|
|
120
|
+
const body = (await response.json()) as {
|
|
121
|
+
plaintext: string;
|
|
122
|
+
token: TokenSummary;
|
|
123
|
+
user: { id: string; tokens: TokenSummary[]; recentEvents: Array<{ eventType: string }> };
|
|
124
|
+
};
|
|
125
|
+
expect(body.plaintext.startsWith("aswt_")).toBe(true);
|
|
126
|
+
expect(body.token.label).toBe("laptop");
|
|
127
|
+
expect(body.token.tokenPreview).toBe(body.plaintext.slice(-4));
|
|
128
|
+
expect(body.token.userId).toBe(user.id);
|
|
129
|
+
expect(body.user.tokens).toContainEqual(body.token);
|
|
130
|
+
expect(body.user.recentEvents.map((event) => event.eventType)).toContain("token_minted");
|
|
131
|
+
|
|
132
|
+
const stored = getDb()
|
|
133
|
+
.prepare<{ tokenHash: string; tokenPreview: string }, string>(
|
|
134
|
+
"SELECT tokenHash, tokenPreview FROM user_tokens WHERE id = ?",
|
|
135
|
+
)
|
|
136
|
+
.get(body.token.id);
|
|
137
|
+
expect(stored).toBeTruthy();
|
|
138
|
+
expect(stored!.tokenHash).not.toBe(body.plaintext);
|
|
139
|
+
expect(stored!.tokenHash).toHaveLength(64);
|
|
140
|
+
expect(stored!.tokenPreview).toBe(body.plaintext.slice(-4));
|
|
141
|
+
|
|
142
|
+
const reread = await authedFetch(`/api/users/${user.id}`);
|
|
143
|
+
const rereadBody = (await reread.json()) as {
|
|
144
|
+
user: { tokens: TokenSummary[]; recentEvents: Array<{ eventType: string }> };
|
|
145
|
+
};
|
|
146
|
+
expect(JSON.stringify(rereadBody)).not.toContain(body.plaintext);
|
|
147
|
+
expect(rereadBody.user.tokens[0]!.tokenPreview).toBe(body.plaintext.slice(-4));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("DELETE revokes a token and records token_revoked", async () => {
|
|
151
|
+
const user = createUser({ name: "Revoked User" });
|
|
152
|
+
const mintResponse = await authedFetch(`/api/users/${user.id}/mcp-tokens`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: JSON.stringify({ label: null }),
|
|
155
|
+
});
|
|
156
|
+
const minted = (await mintResponse.json()) as { token: TokenSummary };
|
|
157
|
+
|
|
158
|
+
const revokeResponse = await authedFetch(
|
|
159
|
+
`/api/users/${user.id}/mcp-tokens/${minted.token.id}`,
|
|
160
|
+
{ method: "DELETE" },
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(revokeResponse.status).toBe(200);
|
|
164
|
+
const body = (await revokeResponse.json()) as {
|
|
165
|
+
user: { tokens: TokenSummary[]; recentEvents: Array<{ eventType: string }> };
|
|
166
|
+
};
|
|
167
|
+
expect(body.user.tokens[0]!.id).toBe(minted.token.id);
|
|
168
|
+
expect(body.user.tokens[0]!.revokedAt).toBeTruthy();
|
|
169
|
+
expect(body.user.recentEvents.map((event) => event.eventType)).toEqual(
|
|
170
|
+
expect.arrayContaining(["token_minted", "token_revoked"]),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("POST and DELETE reject without the swarm key", async () => {
|
|
175
|
+
const user = createUser({ name: "Auth User" });
|
|
176
|
+
|
|
177
|
+
const post = await fetch(url(`/api/users/${user.id}/mcp-tokens`), {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "Content-Type": "application/json" },
|
|
180
|
+
body: JSON.stringify({ label: "missing-auth" }),
|
|
181
|
+
});
|
|
182
|
+
expect(post.status).toBe(401);
|
|
183
|
+
|
|
184
|
+
const deleteResponse = await fetch(url(`/api/users/${user.id}/mcp-tokens/unknown`), {
|
|
185
|
+
method: "DELETE",
|
|
186
|
+
});
|
|
187
|
+
expect(deleteResponse.status).toBe(401);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("DELETE unknown token returns 404", async () => {
|
|
191
|
+
const user = createUser({ name: "Unknown Token User" });
|
|
192
|
+
const response = await authedFetch(`/api/users/${user.id}/mcp-tokens/not-a-token`, {
|
|
193
|
+
method: "DELETE",
|
|
194
|
+
});
|
|
195
|
+
expect(response.status).toBe(404);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("POST unknown user returns 404", async () => {
|
|
199
|
+
const response = await authedFetch("/api/users/not-a-user/mcp-tokens", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
body: JSON.stringify({ label: "nope" }),
|
|
202
|
+
});
|
|
203
|
+
expect(response.status).toBe(404);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("operator events are tagged with the API-key fingerprint", async () => {
|
|
207
|
+
const user = createUser({ name: "Actor User" });
|
|
208
|
+
const response = await authedFetch(`/api/users/${user.id}/mcp-tokens`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: JSON.stringify({ label: "actor" }),
|
|
211
|
+
});
|
|
212
|
+
expect(response.status).toBe(200);
|
|
213
|
+
|
|
214
|
+
const row = getDb()
|
|
215
|
+
.prepare<{ actor: string }, string>(
|
|
216
|
+
"SELECT actor FROM user_identity_events WHERE userId = ? AND eventType = 'token_minted'",
|
|
217
|
+
)
|
|
218
|
+
.get(user.id);
|
|
219
|
+
expect(row?.actor).toBe(`operator:${fingerprintApiKey(API_KEY)}`);
|
|
220
|
+
});
|
|
221
|
+
});
|
package/src/tools/cancel-task.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import * as z from "zod";
|
|
3
4
|
import {
|
|
4
5
|
cancelTask,
|
|
@@ -7,107 +8,160 @@ import {
|
|
|
7
8
|
getTaskById,
|
|
8
9
|
updateAgentStatusFromCapacity,
|
|
9
10
|
} from "@/be/db";
|
|
11
|
+
import { assertOwnsTask, ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
|
|
10
12
|
import { createToolRegistrar } from "@/tools/utils";
|
|
11
13
|
import type { AgentTask } from "@/types";
|
|
12
14
|
import { AgentTaskSchema } from "@/types";
|
|
13
15
|
|
|
14
|
-
export const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
title: "Cancel Task",
|
|
19
|
-
description:
|
|
20
|
-
"Cancel a task that is pending or in progress. Only the lead or task creator can cancel tasks. The worker will be notified via hooks.",
|
|
21
|
-
annotations: { destructiveHint: true },
|
|
16
|
+
export const cancelTaskInputSchema = z.object({
|
|
17
|
+
taskId: z.uuid().describe("The ID of the task to cancel."),
|
|
18
|
+
reason: z.string().optional().describe("Reason for cancellation."),
|
|
19
|
+
});
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
export const cancelTaskOutputSchema = z.object({
|
|
22
|
+
yourAgentId: z.string().uuid().optional(),
|
|
23
|
+
success: z.boolean(),
|
|
24
|
+
message: z.string(),
|
|
25
|
+
task: AgentTaskSchema.optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
type CancelTaskArgs = z.infer<typeof cancelTaskInputSchema>;
|
|
29
|
+
|
|
30
|
+
export async function cancelTaskHandler(
|
|
31
|
+
ctx: ToolCtx,
|
|
32
|
+
{ taskId, reason }: CancelTaskArgs,
|
|
33
|
+
): Promise<CallToolResult> {
|
|
34
|
+
if (ctx.kind === "owner" && !ctx.agentId) {
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
|
|
37
|
+
structuredContent: {
|
|
38
|
+
success: false,
|
|
39
|
+
message: 'Agent ID not found. Set the "X-Agent-ID" header.',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const agentId = ctx.kind === "owner" ? ctx.agentId : undefined;
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
const txn = getDb().transaction(() => {
|
|
47
|
+
if (ctx.kind === "owner") {
|
|
48
|
+
const ownerAgentId = ctx.agentId;
|
|
49
|
+
if (!ownerAgentId) {
|
|
38
50
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
success: false,
|
|
42
|
-
message: 'Agent ID not found. Set the "X-Agent-ID" header.',
|
|
43
|
-
},
|
|
51
|
+
success: false,
|
|
52
|
+
message: 'Agent ID not found. Set the "X-Agent-ID" header.',
|
|
44
53
|
};
|
|
45
54
|
}
|
|
55
|
+
const callerAgent = getAgentById(ownerAgentId);
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
success: false,
|
|
55
|
-
message: "Caller agent not found.",
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const existingTask = getTaskById(taskId);
|
|
60
|
-
|
|
61
|
-
if (!existingTask) {
|
|
62
|
-
return {
|
|
63
|
-
success: false,
|
|
64
|
-
message: `Task "${taskId}" not found.`,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Verify the requester has permission (lead or task creator)
|
|
69
|
-
const canCancel = callerAgent.isLead || existingTask.creatorAgentId === agentId;
|
|
70
|
-
if (!canCancel) {
|
|
71
|
-
return {
|
|
72
|
-
success: false,
|
|
73
|
-
message: "Only the lead or task creator can cancel tasks.",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const cancelled = cancelTask(taskId, reason);
|
|
78
|
-
|
|
79
|
-
if (!cancelled) {
|
|
80
|
-
return {
|
|
81
|
-
success: false,
|
|
82
|
-
message: `Cannot cancel task in status "${existingTask.status}". Only pending/in_progress tasks can be cancelled.`,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Update agent status based on capacity
|
|
87
|
-
if (cancelled.agentId) {
|
|
88
|
-
updateAgentStatusFromCapacity(cancelled.agentId);
|
|
89
|
-
}
|
|
57
|
+
if (!callerAgent) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
message: "Caller agent not found.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
90
63
|
|
|
64
|
+
const existingTask = getTaskById(taskId);
|
|
65
|
+
|
|
66
|
+
if (!existingTask) {
|
|
91
67
|
return {
|
|
92
|
-
success:
|
|
93
|
-
message: `Task "${taskId}"
|
|
94
|
-
task: cancelled,
|
|
68
|
+
success: false,
|
|
69
|
+
message: `Task "${taskId}" not found.`,
|
|
95
70
|
};
|
|
96
|
-
}
|
|
71
|
+
}
|
|
97
72
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
73
|
+
// Verify the requester has permission (lead or task creator)
|
|
74
|
+
const canCancel = callerAgent.isLead || existingTask.creatorAgentId === ownerAgentId;
|
|
75
|
+
if (!canCancel) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
message: "Only the lead or task creator can cancel tasks.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cancelled = cancelTask(taskId, reason);
|
|
83
|
+
|
|
84
|
+
if (!cancelled) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
message: `Cannot cancel task in status "${existingTask.status}". Only pending/in_progress tasks can be cancelled.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Update agent status based on capacity
|
|
92
|
+
if (cancelled.agentId) {
|
|
93
|
+
updateAgentStatusFromCapacity(cancelled.agentId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
message: `Task "${taskId}" has been cancelled.`,
|
|
99
|
+
task: cancelled,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const existingTask = getTaskById(taskId);
|
|
104
|
+
|
|
105
|
+
if (!existingTask) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
message: `Task "${taskId}" not found.`,
|
|
102
109
|
};
|
|
110
|
+
}
|
|
103
111
|
|
|
112
|
+
const ownershipError = assertOwnsTask(ctx, existingTask);
|
|
113
|
+
if (ownershipError) return ownershipError;
|
|
114
|
+
|
|
115
|
+
const cancelled = cancelTask(taskId, reason);
|
|
116
|
+
|
|
117
|
+
if (!cancelled) {
|
|
104
118
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
yourAgentId: agentId,
|
|
108
|
-
...result,
|
|
109
|
-
},
|
|
119
|
+
success: false,
|
|
120
|
+
message: `Cannot cancel task in status "${existingTask.status}". Only pending/in_progress tasks can be cancelled.`,
|
|
110
121
|
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (cancelled.agentId) {
|
|
125
|
+
updateAgentStatusFromCapacity(cancelled.agentId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
message: `Task "${taskId}" has been cancelled.`,
|
|
131
|
+
task: cancelled,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = txn() as
|
|
136
|
+
| {
|
|
137
|
+
success: boolean;
|
|
138
|
+
message: string;
|
|
139
|
+
task?: AgentTask;
|
|
140
|
+
}
|
|
141
|
+
| CallToolResult;
|
|
142
|
+
|
|
143
|
+
if ("content" in result) return result;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: "text", text: result.message }],
|
|
147
|
+
structuredContent: {
|
|
148
|
+
yourAgentId: agentId,
|
|
149
|
+
...result,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const registerCancelTaskTool = (server: McpServer) => {
|
|
155
|
+
createToolRegistrar(server)(
|
|
156
|
+
"cancel-task",
|
|
157
|
+
{
|
|
158
|
+
title: "Cancel Task",
|
|
159
|
+
description:
|
|
160
|
+
"Cancel a task that is pending or in progress. Only the lead or task creator can cancel tasks. The worker will be notified via hooks.",
|
|
161
|
+
annotations: { destructiveHint: true },
|
|
162
|
+
inputSchema: cancelTaskInputSchema,
|
|
163
|
+
outputSchema: cancelTaskOutputSchema,
|
|
111
164
|
},
|
|
165
|
+
async (args, info, _meta) => cancelTaskHandler(ownerCtx(info), args),
|
|
112
166
|
);
|
|
113
167
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import * as z from "zod";
|
|
3
4
|
import {
|
|
4
5
|
getLogsByTaskIdChronological,
|
|
@@ -6,9 +7,78 @@ import {
|
|
|
6
7
|
getTaskById,
|
|
7
8
|
getUserById,
|
|
8
9
|
} from "@/be/db";
|
|
10
|
+
import { assertOwnsTask, ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
|
|
9
11
|
import { createToolRegistrar } from "@/tools/utils";
|
|
10
12
|
import { AgentLogSchema, AgentTaskSchema, TaskAttachmentSchema } from "@/types";
|
|
11
13
|
|
|
14
|
+
export const getTaskDetailsInputSchema = z.object({
|
|
15
|
+
taskId: z.uuid().describe("The ID of the task to get details for."),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const getTaskDetailsOutputSchema = z.object({
|
|
19
|
+
yourAgentId: z.string().uuid().optional(),
|
|
20
|
+
success: z.boolean(),
|
|
21
|
+
message: z.string(),
|
|
22
|
+
task: AgentTaskSchema.optional(),
|
|
23
|
+
requestedBy: z
|
|
24
|
+
.object({ name: z.string(), email: z.string().optional() })
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Resolved user who requested this task"),
|
|
27
|
+
logs: z.array(AgentLogSchema).optional(),
|
|
28
|
+
attachments: z
|
|
29
|
+
.array(TaskAttachmentSchema)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe(
|
|
32
|
+
"Pointer-based artifacts attached to this task via store-progress, ordered by created_at.",
|
|
33
|
+
),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
type GetTaskDetailsArgs = z.infer<typeof getTaskDetailsInputSchema>;
|
|
37
|
+
|
|
38
|
+
export async function getTaskDetailsHandler(
|
|
39
|
+
ctx: ToolCtx,
|
|
40
|
+
{ taskId }: GetTaskDetailsArgs,
|
|
41
|
+
): Promise<CallToolResult> {
|
|
42
|
+
const task = getTaskById(taskId);
|
|
43
|
+
const agentId = ctx.kind === "owner" ? ctx.agentId : undefined;
|
|
44
|
+
|
|
45
|
+
if (!task) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: `Task with ID "${taskId}" not found.` }],
|
|
48
|
+
structuredContent: {
|
|
49
|
+
yourAgentId: agentId,
|
|
50
|
+
success: false,
|
|
51
|
+
message: `Task with ID "${taskId}" not found.`,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ownershipError = assertOwnsTask(ctx, task);
|
|
57
|
+
if (ownershipError) return ownershipError;
|
|
58
|
+
|
|
59
|
+
const logs = getLogsByTaskIdChronological(taskId);
|
|
60
|
+
const attachments = getTaskAttachments(taskId);
|
|
61
|
+
|
|
62
|
+
// Resolve requesting user details if available
|
|
63
|
+
const requestedByUser = task.requestedByUserId ? getUserById(task.requestedByUserId) : undefined;
|
|
64
|
+
const requestedBy = requestedByUser
|
|
65
|
+
? { name: requestedByUser.name, email: requestedByUser.email }
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: `Task "${taskId}" details retrieved.` }],
|
|
70
|
+
structuredContent: {
|
|
71
|
+
yourAgentId: agentId,
|
|
72
|
+
success: true,
|
|
73
|
+
message: `Task "${taskId}" details retrieved.`,
|
|
74
|
+
task,
|
|
75
|
+
requestedBy,
|
|
76
|
+
logs,
|
|
77
|
+
attachments,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
12
82
|
export const registerGetTaskDetailsTool = (server: McpServer) => {
|
|
13
83
|
createToolRegistrar(server)(
|
|
14
84
|
"get-task-details",
|
|
@@ -17,65 +87,9 @@ export const registerGetTaskDetailsTool = (server: McpServer) => {
|
|
|
17
87
|
description:
|
|
18
88
|
"Returns detailed information about a specific task, including output, failure reason, and log history.",
|
|
19
89
|
annotations: { readOnlyHint: true },
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
taskId: z.uuid().describe("The ID of the task to get details for."),
|
|
23
|
-
}),
|
|
24
|
-
outputSchema: z.object({
|
|
25
|
-
yourAgentId: z.string().uuid().optional(),
|
|
26
|
-
success: z.boolean(),
|
|
27
|
-
message: z.string(),
|
|
28
|
-
task: AgentTaskSchema.optional(),
|
|
29
|
-
requestedBy: z
|
|
30
|
-
.object({ name: z.string(), email: z.string().optional() })
|
|
31
|
-
.optional()
|
|
32
|
-
.describe("Resolved user who requested this task"),
|
|
33
|
-
logs: z.array(AgentLogSchema).optional(),
|
|
34
|
-
attachments: z
|
|
35
|
-
.array(TaskAttachmentSchema)
|
|
36
|
-
.optional()
|
|
37
|
-
.describe(
|
|
38
|
-
"Pointer-based artifacts attached to this task via store-progress, ordered by created_at.",
|
|
39
|
-
),
|
|
40
|
-
}),
|
|
41
|
-
},
|
|
42
|
-
async ({ taskId }, requestInfo, _meta) => {
|
|
43
|
-
const task = getTaskById(taskId);
|
|
44
|
-
|
|
45
|
-
if (!task) {
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: "text", text: `Task with ID "${taskId}" not found.` }],
|
|
48
|
-
structuredContent: {
|
|
49
|
-
yourAgentId: requestInfo.agentId,
|
|
50
|
-
success: false,
|
|
51
|
-
message: `Task with ID "${taskId}" not found.`,
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const logs = getLogsByTaskIdChronological(taskId);
|
|
57
|
-
const attachments = getTaskAttachments(taskId);
|
|
58
|
-
|
|
59
|
-
// Resolve requesting user details if available
|
|
60
|
-
const requestedByUser = task.requestedByUserId
|
|
61
|
-
? getUserById(task.requestedByUserId)
|
|
62
|
-
: undefined;
|
|
63
|
-
const requestedBy = requestedByUser
|
|
64
|
-
? { name: requestedByUser.name, email: requestedByUser.email }
|
|
65
|
-
: undefined;
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
content: [{ type: "text", text: `Task "${taskId}" details retrieved.` }],
|
|
69
|
-
structuredContent: {
|
|
70
|
-
yourAgentId: requestInfo.agentId,
|
|
71
|
-
success: true,
|
|
72
|
-
message: `Task "${taskId}" details retrieved.`,
|
|
73
|
-
task,
|
|
74
|
-
requestedBy,
|
|
75
|
-
logs,
|
|
76
|
-
attachments,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
90
|
+
inputSchema: getTaskDetailsInputSchema,
|
|
91
|
+
outputSchema: getTaskDetailsOutputSchema,
|
|
79
92
|
},
|
|
93
|
+
async (args, info, _meta) => getTaskDetailsHandler(ownerCtx(info), args),
|
|
80
94
|
);
|
|
81
95
|
};
|