@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.
- package/openapi.json +177 -10
- package/package.json +6 -6
- 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 +37 -4
- package/src/be/migrations/074_user_budget_scope.sql +85 -0
- package/src/be/schedules/validate.ts +21 -0
- package/src/be/skill-sync.ts +65 -15
- package/src/commands/resume-session.ts +118 -0
- package/src/commands/runner.ts +178 -121
- 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 +35 -10
- package/src/http/skills.ts +27 -2
- 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-api-integration.test.ts +36 -0
- 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/runner-skills-refresh.test.ts +200 -0
- package/src/tests/schedule-validation-helper.test.ts +51 -0
- package/src/tests/skill-sync.test.ts +73 -9
- package/src/tests/skills-signature.test.ts +141 -0
- package/src/tests/task-tools-ctx.test.ts +100 -0
- package/src/tests/task-tools-ownership.test.ts +167 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +161 -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/schedules/update-schedule.ts +48 -8
- package/src/tools/send-task.ts +312 -312
- package/src/tools/slack-upload-file.ts +17 -5
- 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
- package/src/utils/skills-refresh.ts +123 -0
package/src/tools/send-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
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
94
|
+
type SendTaskArgs = z.infer<typeof sendTaskInputSchema>;
|
|
166
95
|
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
221
|
+
const txn = getDb().transaction(() => {
|
|
222
|
+
const finalTags = tags;
|
|
264
223
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
};
|