@desplega.ai/agent-swarm 1.83.0 → 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.
Files changed (67) hide show
  1. package/openapi.json +177 -10
  2. package/package.json +6 -6
  3. package/src/artifact-sdk/server.ts +23 -1
  4. package/src/be/budget-admission.ts +28 -4
  5. package/src/be/budget-refusal-notify.ts +19 -3
  6. package/src/be/db-queries/oauth.ts +43 -0
  7. package/src/be/db.ts +37 -4
  8. package/src/be/migrations/074_user_budget_scope.sql +85 -0
  9. package/src/be/schedules/validate.ts +21 -0
  10. package/src/be/skill-sync.ts +65 -15
  11. package/src/commands/resume-session.ts +118 -0
  12. package/src/commands/runner.ts +178 -121
  13. package/src/http/core.ts +4 -1
  14. package/src/http/index.ts +16 -0
  15. package/src/http/integrations.ts +26 -0
  16. package/src/http/mcp-user.ts +111 -0
  17. package/src/http/poll.ts +19 -5
  18. package/src/http/schedules.ts +35 -10
  19. package/src/http/skills.ts +27 -2
  20. package/src/http/users.ts +107 -2
  21. package/src/jira/client.ts +3 -5
  22. package/src/jira/oauth.ts +1 -0
  23. package/src/jira/sync.ts +2 -2
  24. package/src/oauth/ensure-token.ts +1 -0
  25. package/src/oauth/wrapper.ts +38 -7
  26. package/src/providers/claude-adapter.ts +7 -2
  27. package/src/providers/claude-managed-adapter.ts +1 -1
  28. package/src/providers/codex-adapter.ts +30 -0
  29. package/src/providers/opencode-adapter.ts +149 -14
  30. package/src/providers/pi-mono-adapter.ts +41 -1
  31. package/src/providers/types.ts +1 -1
  32. package/src/server-user.ts +117 -0
  33. package/src/tests/artifact-sdk.test.ts +23 -19
  34. package/src/tests/budget-user-scope.test.ts +376 -0
  35. package/src/tests/claude-managed-adapter.test.ts +6 -0
  36. package/src/tests/codex-adapter.test.ts +192 -0
  37. package/src/tests/codex-rate-limit-parse.test.ts +256 -0
  38. package/src/tests/db-queries-oauth.test.ts +43 -0
  39. package/src/tests/ensure-token.test.ts +93 -0
  40. package/src/tests/error-tracker.test.ts +52 -0
  41. package/src/tests/fetch-resolved-env.test.ts +33 -20
  42. package/src/tests/http-api-integration.test.ts +36 -0
  43. package/src/tests/http-users.test.ts +29 -1
  44. package/src/tests/mcp-user-route.test.ts +325 -0
  45. package/src/tests/opencode-adapter.test.ts +75 -0
  46. package/src/tests/pi-mono-adapter.test.ts +21 -1
  47. package/src/tests/rate-limit-event.test.ts +69 -6
  48. package/src/tests/resume-session.test.ts +93 -0
  49. package/src/tests/runner-skills-refresh.test.ts +200 -0
  50. package/src/tests/schedule-validation-helper.test.ts +51 -0
  51. package/src/tests/skill-sync.test.ts +73 -9
  52. package/src/tests/skills-signature.test.ts +141 -0
  53. package/src/tests/task-tools-ctx.test.ts +100 -0
  54. package/src/tests/task-tools-ownership.test.ts +167 -0
  55. package/src/tests/update-schedule-mcp-tool.test.ts +161 -0
  56. package/src/tests/user-token-routes.test.ts +221 -0
  57. package/src/tools/cancel-task.ts +137 -83
  58. package/src/tools/get-task-details.ts +73 -59
  59. package/src/tools/get-tasks.ts +134 -126
  60. package/src/tools/schedules/update-schedule.ts +48 -8
  61. package/src/tools/send-task.ts +312 -312
  62. package/src/tools/slack-upload-file.ts +17 -5
  63. package/src/tools/task-action.ts +464 -367
  64. package/src/tools/task-tool-ctx.ts +43 -0
  65. package/src/types.ts +6 -2
  66. package/src/utils/error-tracker.ts +122 -9
  67. package/src/utils/skills-refresh.ts +123 -0
@@ -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
  createTaskExtended,
@@ -10,343 +11,342 @@ import {
10
11
  hasCapacity,
11
12
  } from "@/be/db";
12
13
  import { findDuplicateTask } from "@/tools/task-dedup";
14
+ import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
13
15
  import { createToolRegistrar } from "@/tools/utils";
14
16
  import { AgentTaskSchema } from "@/types";
15
17
 
16
- export const registerSendTaskTool = (server: McpServer) => {
17
- createToolRegistrar(server)(
18
- "send-task",
19
- {
20
- title: "Send a task",
21
- annotations: { destructiveHint: false },
22
- description:
23
- "Sends a task to a specific agent, creates an unassigned task for the pool, or offers a task for acceptance.",
24
- inputSchema: z.object({
25
- agentId: z
26
- .uuid()
27
- .optional()
28
- .describe("The agent to assign/offer task to. Omit to create unassigned task for pool."),
29
- task: z.string().min(1).describe("The task description to send."),
30
- offerMode: z
31
- .boolean()
32
- .default(false)
33
- .describe("If true, offer the task instead of direct assign (agent must accept/reject)."),
34
- taskType: z
35
- .string()
36
- .max(50)
37
- .optional()
38
- .describe("Task type (e.g., 'bug', 'feature', 'review')."),
39
- tags: z
40
- .array(z.string())
41
- .optional()
42
- .describe("Tags for filtering (e.g., ['urgent', 'frontend'])."),
43
- priority: z
44
- .number()
45
- .int()
46
- .min(0)
47
- .max(100)
48
- .optional()
49
- .describe("Priority 0-100 (default: 50)."),
50
- dependsOn: z.array(z.uuid()).optional().describe("Task IDs this task depends on."),
51
- parentTaskId: z
52
- .uuid()
53
- .optional()
54
- .describe(
55
- "Parent task ID for session continuity. Child task will resume the parent's Claude session. Auto-routes to the same worker unless agentId is explicitly provided.",
56
- ),
57
- dir: z
58
- .string()
59
- .min(1)
60
- .startsWith("/")
61
- .optional()
62
- .describe(
63
- "Working directory (absolute path) for the agent to start in. If the directory doesn't exist, falls back to the default working directory.",
64
- ),
65
- vcsRepo: z
66
- .string()
67
- .optional()
68
- .describe(
69
- "VCS repo identifier (e.g., 'desplega-ai/agent-swarm' for GitHub or 'group/project' for GitLab). Links the task to a registered repo for workspace context.",
70
- ),
71
- model: z
72
- .enum(["haiku", "sonnet", "opus"])
73
- .optional()
74
- .describe(
75
- "Model to use for this task ('haiku', 'sonnet', or 'opus'). If not set, uses agent/global config MODEL_OVERRIDE or defaults to 'opus'.",
76
- ),
77
- allowDuplicate: z
78
- .boolean()
79
- .default(false)
80
- .describe(
81
- "If true, skip duplicate detection and create the task even if a similar one exists.",
82
- ),
83
- slackChannelId: z
84
- .string()
85
- .optional()
86
- .describe(
87
- "Slack channel ID to post progress updates to. Use this to propagate Slack context when delegating from a Slack thread.",
88
- ),
89
- slackThreadTs: z
90
- .string()
91
- .optional()
92
- .describe(
93
- "Slack thread timestamp. Required with slackChannelId for thread-level updates.",
94
- ),
95
- slackUserId: z.string().optional().describe("Slack user ID of the original requester."),
96
- requestedByUserId: z
97
- .string()
98
- .uuid()
99
- .optional()
100
- .describe(
101
- "ID of the human user who originally requested this task chain. When omitted, inherited from the caller's current task so the attribution flows through multi-hop delegation automatically.",
102
- ),
103
- }),
104
- outputSchema: z.object({
105
- yourAgentId: z.string().uuid().optional(),
106
- success: z.boolean(),
107
- message: z.string(),
108
- task: AgentTaskSchema.optional(),
109
- }),
110
- },
111
- async (
112
- {
113
- agentId,
114
- task,
115
- offerMode,
116
- taskType,
117
- tags,
118
- priority,
119
- dependsOn,
120
- dir,
121
- parentTaskId,
122
- vcsRepo,
123
- model,
124
- allowDuplicate,
125
- slackChannelId,
126
- slackThreadTs,
127
- slackUserId,
128
- requestedByUserId,
129
- },
130
- requestInfo,
131
- _meta,
132
- ) => {
133
- if (!requestInfo.agentId) {
134
- return {
135
- content: [
136
- {
137
- type: "text",
138
- text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
139
- },
140
- ],
141
- structuredContent: {
142
- yourAgentId: requestInfo.agentId,
143
- success: false,
144
- message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
145
- },
146
- };
147
- }
18
+ export const sendTaskInputSchema = z.object({
19
+ agentId: z
20
+ .uuid()
21
+ .optional()
22
+ .describe("The agent to assign/offer task to. Omit to create unassigned task for pool."),
23
+ task: z.string().min(1).describe("The task description to send."),
24
+ offerMode: z
25
+ .boolean()
26
+ .default(false)
27
+ .describe("If true, offer the task instead of direct assign (agent must accept/reject)."),
28
+ taskType: z.string().max(50).optional().describe("Task type (e.g., 'bug', 'feature', 'review')."),
29
+ tags: z
30
+ .array(z.string())
31
+ .optional()
32
+ .describe("Tags for filtering (e.g., ['urgent', 'frontend'])."),
33
+ priority: z.number().int().min(0).max(100).optional().describe("Priority 0-100 (default: 50)."),
34
+ dependsOn: z.array(z.uuid()).optional().describe("Task IDs this task depends on."),
35
+ parentTaskId: z
36
+ .uuid()
37
+ .optional()
38
+ .describe(
39
+ "Parent task ID for session continuity. Child task will resume the parent's Claude session. Auto-routes to the same worker unless agentId is explicitly provided.",
40
+ ),
41
+ dir: z
42
+ .string()
43
+ .min(1)
44
+ .startsWith("/")
45
+ .optional()
46
+ .describe(
47
+ "Working directory (absolute path) for the agent to start in. If the directory doesn't exist, falls back to the default working directory.",
48
+ ),
49
+ vcsRepo: z
50
+ .string()
51
+ .optional()
52
+ .describe(
53
+ "VCS repo identifier (e.g., 'desplega-ai/agent-swarm' for GitHub or 'group/project' for GitLab). Links the task to a registered repo for workspace context.",
54
+ ),
55
+ model: z
56
+ .enum(["haiku", "sonnet", "opus"])
57
+ .optional()
58
+ .describe(
59
+ "Model to use for this task ('haiku', 'sonnet', or 'opus'). If not set, uses agent/global config MODEL_OVERRIDE or defaults to 'opus'.",
60
+ ),
61
+ allowDuplicate: z
62
+ .boolean()
63
+ .default(false)
64
+ .describe(
65
+ "If true, skip duplicate detection and create the task even if a similar one exists.",
66
+ ),
67
+ slackChannelId: z
68
+ .string()
69
+ .optional()
70
+ .describe(
71
+ "Slack channel ID to post progress updates to. Use this to propagate Slack context when delegating from a Slack thread.",
72
+ ),
73
+ slackThreadTs: z
74
+ .string()
75
+ .optional()
76
+ .describe("Slack thread timestamp. Required with slackChannelId for thread-level updates."),
77
+ slackUserId: z.string().optional().describe("Slack user ID of the original requester."),
78
+ requestedByUserId: z
79
+ .string()
80
+ .uuid()
81
+ .optional()
82
+ .describe(
83
+ "ID of the human user who originally requested this task chain. When omitted, inherited from the caller's current task so the attribution flows through multi-hop delegation automatically.",
84
+ ),
85
+ });
148
86
 
149
- if (agentId === requestInfo.agentId) {
150
- return {
151
- content: [
152
- {
153
- type: "text",
154
- text: "Cannot send a task to yourself, are you drunk?",
155
- },
156
- ],
157
- structuredContent: {
158
- yourAgentId: requestInfo.agentId,
159
- success: false,
160
- message: "Cannot send a task to yourself, are you drunk?",
161
- },
162
- };
163
- }
87
+ export const sendTaskOutputSchema = z.object({
88
+ yourAgentId: z.string().uuid().optional(),
89
+ success: z.boolean(),
90
+ message: z.string(),
91
+ task: AgentTaskSchema.optional(),
92
+ });
164
93
 
165
- const effectiveVcsRepo = vcsRepo;
94
+ type SendTaskArgs = z.infer<typeof sendTaskInputSchema>;
166
95
 
167
- // Auto-default parentTaskId to caller's current task for tree tracking
168
- const effectiveParentTaskId = parentTaskId ?? requestInfo.sourceTaskId;
96
+ export async function sendTaskHandler(
97
+ ctx: ToolCtx,
98
+ {
99
+ agentId,
100
+ task,
101
+ offerMode,
102
+ taskType,
103
+ tags,
104
+ priority,
105
+ dependsOn,
106
+ dir,
107
+ parentTaskId,
108
+ vcsRepo,
109
+ model,
110
+ allowDuplicate,
111
+ slackChannelId,
112
+ slackThreadTs,
113
+ slackUserId,
114
+ requestedByUserId: inputRequestedByUserId,
115
+ }: SendTaskArgs,
116
+ ): Promise<CallToolResult> {
117
+ if (ctx.kind === "owner" && !ctx.agentId) {
118
+ return {
119
+ content: [
120
+ {
121
+ type: "text",
122
+ text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
123
+ },
124
+ ],
125
+ structuredContent: {
126
+ yourAgentId: ctx.agentId,
127
+ success: false,
128
+ message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
129
+ },
130
+ };
131
+ }
169
132
 
170
- // Inherit requestedByUserId from the caller's current task when not explicitly provided
171
- const callerTask = requestInfo.sourceTaskId ? getTaskById(requestInfo.sourceTaskId) : null;
172
- const effectiveRequestedByUserId =
173
- requestedByUserId ?? callerTask?.requestedByUserId ?? undefined;
133
+ const creatorAgentId = ctx.kind === "owner" ? ctx.agentId : undefined;
134
+ const sourceTaskId = ctx.kind === "owner" ? ctx.sourceTaskId : undefined;
135
+ const callerTask = sourceTaskId ? getTaskById(sourceTaskId) : null;
136
+ const requestedByUserId =
137
+ ctx.kind === "user"
138
+ ? ctx.userId
139
+ : (inputRequestedByUserId ?? callerTask?.requestedByUserId ?? undefined);
174
140
 
175
- // Auto-route to parent's worker if parentTaskId is set and no explicit agentId
176
- let effectiveAgentId = agentId;
177
- if (effectiveParentTaskId && !agentId) {
178
- const parentTask = getTaskById(effectiveParentTaskId);
179
- if (parentTask?.agentId) {
180
- effectiveAgentId = parentTask.agentId;
181
- }
182
- }
141
+ if (ctx.kind === "owner" && agentId === ctx.agentId) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: "Cannot send a task to yourself, are you drunk?",
147
+ },
148
+ ],
149
+ structuredContent: {
150
+ yourAgentId: ctx.agentId,
151
+ success: false,
152
+ message: "Cannot send a task to yourself, are you drunk?",
153
+ },
154
+ };
155
+ }
183
156
 
184
- // Dedup guard: check for similar recent tasks
185
- if (!allowDuplicate && requestInfo.agentId) {
186
- const duplicate = findDuplicateTask({
187
- taskDescription: task,
188
- creatorAgentId: requestInfo.agentId,
189
- targetAgentId: effectiveAgentId ?? undefined,
190
- });
191
- if (duplicate) {
192
- const msg = `Duplicate task detected (matches task ${duplicate.task.id.slice(0, 8)}, ${duplicate.reason}). Skipping. Use allowDuplicate: true to override.`;
193
- return {
194
- content: [{ type: "text", text: msg }],
195
- structuredContent: {
196
- yourAgentId: requestInfo.agentId,
197
- success: false,
198
- message: msg,
199
- },
200
- };
201
- }
202
- }
157
+ const effectiveVcsRepo = vcsRepo;
203
158
 
204
- // Guard: prevent re-delegation from follow-up tasks
205
- // When the source task is a "follow-up" (worker completed/failed notification),
206
- // check if there are completed tasks in the same Slack thread recently.
207
- // This prevents the cycle: worker completes → follow-up → Lead re-delegates → repeat.
208
- if (requestInfo.sourceTaskId) {
209
- const sourceTask = getTaskById(requestInfo.sourceTaskId);
210
- if (
211
- sourceTask?.taskType === "follow-up" &&
212
- sourceTask.slackThreadTs &&
213
- sourceTask.slackChannelId
214
- ) {
215
- const recentCompleted = findCompletedTaskInThread(
216
- sourceTask.slackChannelId,
217
- sourceTask.slackThreadTs,
218
- 2880, // 48 hours in minutes
219
- );
220
- if (recentCompleted) {
221
- const msg = `Blocked: re-delegation from follow-up task in a thread that already has completed work (task ${recentCompleted.id.slice(0, 8)}). The original request was already handled.`;
222
- return {
223
- content: [{ type: "text", text: msg }],
224
- structuredContent: {
225
- yourAgentId: requestInfo.agentId,
226
- success: false,
227
- message: msg,
228
- },
229
- };
230
- }
231
- }
232
- }
159
+ // Auto-default parentTaskId to caller's current task for tree tracking
160
+ const effectiveParentTaskId = parentTaskId ?? sourceTaskId;
233
161
 
234
- const txn = getDb().transaction(() => {
235
- const finalTags = tags;
162
+ // Auto-route to parent's worker if parentTaskId is set and no explicit agentId
163
+ let effectiveAgentId = agentId;
164
+ if (effectiveParentTaskId && !agentId) {
165
+ const parentTask = getTaskById(effectiveParentTaskId);
166
+ if (parentTask?.agentId) {
167
+ effectiveAgentId = parentTask.agentId;
168
+ }
169
+ }
236
170
 
237
- // If no agentId (and no auto-routed agentId), create an unassigned task for the pool
238
- if (!effectiveAgentId) {
239
- const newTask = createTaskExtended(task, {
240
- creatorAgentId: requestInfo.agentId,
241
- sourceTaskId: requestInfo.sourceTaskId,
242
- taskType,
243
- tags: finalTags,
244
- priority,
245
- dependsOn,
246
- dir,
247
- parentTaskId: effectiveParentTaskId,
248
- vcsRepo: effectiveVcsRepo,
249
- model,
250
- slackChannelId,
251
- slackThreadTs,
252
- slackUserId,
253
- requestedByUserId: effectiveRequestedByUserId,
254
- });
171
+ // Dedup guard: check for similar recent tasks
172
+ if (!allowDuplicate && creatorAgentId) {
173
+ const duplicate = findDuplicateTask({
174
+ taskDescription: task,
175
+ creatorAgentId,
176
+ targetAgentId: effectiveAgentId ?? undefined,
177
+ });
178
+ if (duplicate) {
179
+ const msg = `Duplicate task detected (matches task ${duplicate.task.id.slice(0, 8)}, ${duplicate.reason}). Skipping. Use allowDuplicate: true to override.`;
180
+ return {
181
+ content: [{ type: "text", text: msg }],
182
+ structuredContent: {
183
+ yourAgentId: creatorAgentId,
184
+ success: false,
185
+ message: msg,
186
+ },
187
+ };
188
+ }
189
+ }
255
190
 
256
- return {
257
- success: true,
258
- message: `Created unassigned task "${newTask.id}" in the pool.`,
259
- task: newTask,
260
- };
261
- }
191
+ // Guard: prevent re-delegation from follow-up tasks
192
+ // When the source task is a "follow-up" (worker completed/failed notification),
193
+ // check if there are completed tasks in the same Slack thread recently.
194
+ // This prevents the cycle: worker completes → follow-up → Lead re-delegates → repeat.
195
+ if (sourceTaskId) {
196
+ const sourceTask = getTaskById(sourceTaskId);
197
+ if (
198
+ sourceTask?.taskType === "follow-up" &&
199
+ sourceTask.slackThreadTs &&
200
+ sourceTask.slackChannelId
201
+ ) {
202
+ const recentCompleted = findCompletedTaskInThread(
203
+ sourceTask.slackChannelId,
204
+ sourceTask.slackThreadTs,
205
+ 2880, // 48 hours in minutes
206
+ );
207
+ if (recentCompleted) {
208
+ const msg = `Blocked: re-delegation from follow-up task in a thread that already has completed work (task ${recentCompleted.id.slice(0, 8)}). The original request was already handled.`;
209
+ return {
210
+ content: [{ type: "text", text: msg }],
211
+ structuredContent: {
212
+ yourAgentId: creatorAgentId,
213
+ success: false,
214
+ message: msg,
215
+ },
216
+ };
217
+ }
218
+ }
219
+ }
262
220
 
263
- const agent = getAgentById(effectiveAgentId);
221
+ const txn = getDb().transaction(() => {
222
+ const finalTags = tags;
264
223
 
265
- if (!agent) {
266
- return {
267
- success: false,
268
- message: `Agent with ID "${effectiveAgentId}" not found.`,
269
- };
270
- }
224
+ // If no agentId (and no auto-routed agentId), create an unassigned task for the pool
225
+ if (!effectiveAgentId) {
226
+ const newTask = createTaskExtended(task, {
227
+ creatorAgentId,
228
+ requestedByUserId,
229
+ sourceTaskId,
230
+ taskType,
231
+ tags: finalTags,
232
+ priority,
233
+ dependsOn,
234
+ dir,
235
+ parentTaskId: effectiveParentTaskId,
236
+ vcsRepo: effectiveVcsRepo,
237
+ model,
238
+ slackChannelId,
239
+ slackThreadTs,
240
+ slackUserId,
241
+ });
271
242
 
272
- if (agent.isLead) {
273
- return {
274
- success: false,
275
- message: `Cannot assign tasks to the lead agent "${agent.name}", wtf?`,
276
- };
277
- }
243
+ return {
244
+ success: true,
245
+ message: `Created unassigned task "${newTask.id}" in the pool.`,
246
+ task: newTask,
247
+ };
248
+ }
278
249
 
279
- // For direct assignment (not offer), check if agent has capacity
280
- if (!offerMode && !hasCapacity(effectiveAgentId)) {
281
- const activeCount = getActiveTaskCount(effectiveAgentId);
282
- return {
283
- success: false,
284
- message: `Agent "${agent.name}" is at capacity (${activeCount}/${agent.maxTasks ?? 1} tasks). Use offerMode: true to offer the task instead, or wait for a task to complete.`,
285
- };
286
- }
250
+ const agent = getAgentById(effectiveAgentId);
287
251
 
288
- if (offerMode) {
289
- // Offer the task to the agent (they must accept/reject)
290
- const newTask = createTaskExtended(task, {
291
- offeredTo: effectiveAgentId,
292
- creatorAgentId: requestInfo.agentId,
293
- sourceTaskId: requestInfo.sourceTaskId,
294
- taskType,
295
- tags: finalTags,
296
- priority,
297
- dependsOn,
298
- dir,
299
- parentTaskId: effectiveParentTaskId,
300
- vcsRepo: effectiveVcsRepo,
301
- model,
302
- slackChannelId,
303
- slackThreadTs,
304
- slackUserId,
305
- requestedByUserId: effectiveRequestedByUserId,
306
- });
252
+ if (!agent) {
253
+ return {
254
+ success: false,
255
+ message: `Agent with ID "${effectiveAgentId}" not found.`,
256
+ };
257
+ }
307
258
 
308
- return {
309
- success: true,
310
- message: `Task "${newTask.id}" offered to agent "${agent.name}". They must accept or reject it.`,
311
- task: newTask,
312
- };
313
- }
259
+ if (agent.isLead) {
260
+ return {
261
+ success: false,
262
+ message: `Cannot assign tasks to the lead agent "${agent.name}", wtf?`,
263
+ };
264
+ }
314
265
 
315
- // Direct assignment
316
- const newTask = createTaskExtended(task, {
317
- agentId: effectiveAgentId,
318
- creatorAgentId: requestInfo.agentId,
319
- sourceTaskId: requestInfo.sourceTaskId,
320
- taskType,
321
- tags: finalTags,
322
- priority,
323
- dependsOn,
324
- dir,
325
- parentTaskId: effectiveParentTaskId,
326
- vcsRepo: effectiveVcsRepo,
327
- model,
328
- slackChannelId,
329
- slackThreadTs,
330
- slackUserId,
331
- requestedByUserId: effectiveRequestedByUserId,
332
- });
266
+ // For direct assignment (not offer), check if agent has capacity
267
+ if (!offerMode && !hasCapacity(effectiveAgentId)) {
268
+ const activeCount = getActiveTaskCount(effectiveAgentId);
269
+ return {
270
+ success: false,
271
+ message: `Agent "${agent.name}" is at capacity (${activeCount}/${agent.maxTasks ?? 1} tasks). Use offerMode: true to offer the task instead, or wait for a task to complete.`,
272
+ };
273
+ }
333
274
 
334
- return {
335
- success: true,
336
- message: `Task "${newTask.id}" sent to agent "${agent.name}".`,
337
- task: newTask,
338
- };
275
+ if (offerMode) {
276
+ // Offer the task to the agent (they must accept/reject)
277
+ const newTask = createTaskExtended(task, {
278
+ offeredTo: effectiveAgentId,
279
+ creatorAgentId,
280
+ requestedByUserId,
281
+ sourceTaskId,
282
+ taskType,
283
+ tags: finalTags,
284
+ priority,
285
+ dependsOn,
286
+ dir,
287
+ parentTaskId: effectiveParentTaskId,
288
+ vcsRepo: effectiveVcsRepo,
289
+ model,
290
+ slackChannelId,
291
+ slackThreadTs,
292
+ slackUserId,
339
293
  });
340
294
 
341
- const result = txn();
342
-
343
295
  return {
344
- content: [{ type: "text", text: result.message }],
345
- structuredContent: {
346
- yourAgentId: requestInfo.agentId,
347
- ...result,
348
- },
296
+ success: true,
297
+ message: `Task "${newTask.id}" offered to agent "${agent.name}". They must accept or reject it.`,
298
+ task: newTask,
349
299
  };
300
+ }
301
+
302
+ // Direct assignment
303
+ const newTask = createTaskExtended(task, {
304
+ agentId: effectiveAgentId,
305
+ creatorAgentId,
306
+ requestedByUserId,
307
+ sourceTaskId,
308
+ taskType,
309
+ tags: finalTags,
310
+ priority,
311
+ dependsOn,
312
+ dir,
313
+ parentTaskId: effectiveParentTaskId,
314
+ vcsRepo: effectiveVcsRepo,
315
+ model,
316
+ slackChannelId,
317
+ slackThreadTs,
318
+ slackUserId,
319
+ });
320
+
321
+ return {
322
+ success: true,
323
+ message: `Task "${newTask.id}" sent to agent "${agent.name}".`,
324
+ task: newTask,
325
+ };
326
+ });
327
+
328
+ const result = txn();
329
+
330
+ return {
331
+ content: [{ type: "text", text: result.message }],
332
+ structuredContent: {
333
+ yourAgentId: creatorAgentId,
334
+ ...result,
335
+ },
336
+ };
337
+ }
338
+
339
+ export const registerSendTaskTool = (server: McpServer) => {
340
+ createToolRegistrar(server)(
341
+ "send-task",
342
+ {
343
+ title: "Send a task",
344
+ annotations: { destructiveHint: false },
345
+ description:
346
+ "Sends a task to a specific agent, creates an unassigned task for the pool, or offers a task for acceptance.",
347
+ inputSchema: sendTaskInputSchema,
348
+ outputSchema: sendTaskOutputSchema,
350
349
  },
350
+ async (args, info, _meta) => sendTaskHandler(ownerCtx(info), args),
351
351
  );
352
352
  };