@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.
Files changed (55) hide show
  1. package/openapi.json +139 -8
  2. package/package.json +1 -1
  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 +35 -2
  8. package/src/be/migrations/074_user_budget_scope.sql +85 -0
  9. package/src/commands/resume-session.ts +118 -0
  10. package/src/commands/runner.ts +137 -67
  11. package/src/http/core.ts +4 -1
  12. package/src/http/index.ts +16 -0
  13. package/src/http/integrations.ts +26 -0
  14. package/src/http/mcp-user.ts +111 -0
  15. package/src/http/poll.ts +19 -5
  16. package/src/http/schedules.ts +1 -1
  17. package/src/http/users.ts +107 -2
  18. package/src/jira/client.ts +3 -5
  19. package/src/jira/oauth.ts +1 -0
  20. package/src/jira/sync.ts +2 -2
  21. package/src/oauth/ensure-token.ts +1 -0
  22. package/src/oauth/wrapper.ts +38 -7
  23. package/src/providers/claude-adapter.ts +7 -2
  24. package/src/providers/claude-managed-adapter.ts +1 -1
  25. package/src/providers/codex-adapter.ts +30 -0
  26. package/src/providers/opencode-adapter.ts +149 -14
  27. package/src/providers/pi-mono-adapter.ts +41 -1
  28. package/src/providers/types.ts +1 -1
  29. package/src/server-user.ts +117 -0
  30. package/src/tests/artifact-sdk.test.ts +23 -19
  31. package/src/tests/budget-user-scope.test.ts +376 -0
  32. package/src/tests/claude-managed-adapter.test.ts +6 -0
  33. package/src/tests/codex-adapter.test.ts +192 -0
  34. package/src/tests/codex-rate-limit-parse.test.ts +256 -0
  35. package/src/tests/db-queries-oauth.test.ts +43 -0
  36. package/src/tests/ensure-token.test.ts +93 -0
  37. package/src/tests/error-tracker.test.ts +52 -0
  38. package/src/tests/fetch-resolved-env.test.ts +33 -20
  39. package/src/tests/http-users.test.ts +29 -1
  40. package/src/tests/mcp-user-route.test.ts +325 -0
  41. package/src/tests/opencode-adapter.test.ts +75 -0
  42. package/src/tests/pi-mono-adapter.test.ts +21 -1
  43. package/src/tests/rate-limit-event.test.ts +69 -6
  44. package/src/tests/resume-session.test.ts +93 -0
  45. package/src/tests/task-tools-ctx.test.ts +100 -0
  46. package/src/tests/task-tools-ownership.test.ts +167 -0
  47. package/src/tests/user-token-routes.test.ts +221 -0
  48. package/src/tools/cancel-task.ts +137 -83
  49. package/src/tools/get-task-details.ts +73 -59
  50. package/src/tools/get-tasks.ts +134 -126
  51. package/src/tools/send-task.ts +312 -312
  52. package/src/tools/task-action.ts +464 -367
  53. package/src/tools/task-tool-ctx.ts +43 -0
  54. package/src/types.ts +6 -2
  55. package/src/utils/error-tracker.ts +122 -9
@@ -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 registerCancelTaskTool = (server: McpServer) => {
15
- createToolRegistrar(server)(
16
- "cancel-task",
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
- inputSchema: z.object({
24
- taskId: z.uuid().describe("The ID of the task to cancel."),
25
- reason: z.string().optional().describe("Reason for cancellation."),
26
- }),
27
- outputSchema: z.object({
28
- yourAgentId: z.string().uuid().optional(),
29
- success: z.boolean(),
30
- message: z.string(),
31
- task: AgentTaskSchema.optional(),
32
- }),
33
- },
34
- async (input, requestInfo, _meta) => {
35
- const { taskId, reason } = input;
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
- if (!requestInfo.agentId) {
46
+ const txn = getDb().transaction(() => {
47
+ if (ctx.kind === "owner") {
48
+ const ownerAgentId = ctx.agentId;
49
+ if (!ownerAgentId) {
38
50
  return {
39
- content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
40
- structuredContent: {
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
- const agentId = requestInfo.agentId;
48
-
49
- const txn = getDb().transaction(() => {
50
- const callerAgent = getAgentById(agentId);
51
-
52
- if (!callerAgent) {
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: true,
93
- message: `Task "${taskId}" has been cancelled.`,
94
- task: cancelled,
68
+ success: false,
69
+ message: `Task "${taskId}" not found.`,
95
70
  };
96
- });
71
+ }
97
72
 
98
- const result = txn() as {
99
- success: boolean;
100
- message: string;
101
- task?: AgentTask;
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
- content: [{ type: "text", text: result.message }],
106
- structuredContent: {
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
- inputSchema: z.object({
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
  };
@@ -1,6 +1,8 @@
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 { getAllTasks } from "@/be/db";
5
+ import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
4
6
  import { createToolRegistrar } from "@/tools/utils";
5
7
  import type { AgentTask, AgentTaskSummary } from "@/types";
6
8
  import { AgentTaskStatusSchema } from "@/types";
@@ -24,6 +26,135 @@ const TaskSummarySchema = z.object({
24
26
  progress: z.string().optional(),
25
27
  });
26
28
 
29
+ export const getTasksInputSchema = z.object({
30
+ status: AgentTaskStatusSchema.optional().describe(
31
+ "Filter by task status (unassigned, offered, pending, in_progress, completed, failed).",
32
+ ),
33
+ mineOnly: z.boolean().optional().describe("Only return tasks assigned to you."),
34
+ unassigned: z.boolean().optional().describe("Only return unassigned tasks in the pool."),
35
+ offeredToMe: z
36
+ .boolean()
37
+ .optional()
38
+ .describe("Only return tasks offered to you (awaiting accept/reject)."),
39
+ readyOnly: z.boolean().optional().describe("Only return tasks whose dependencies are met."),
40
+ taskType: z.string().optional().describe("Filter by task type (e.g., 'bug', 'feature')."),
41
+ tags: z.array(z.string()).optional().describe("Filter by any matching tag."),
42
+ search: z.string().optional().describe("Search in task description."),
43
+ scheduleId: z
44
+ .string()
45
+ .uuid()
46
+ .optional()
47
+ .describe("Filter by schedule ID to find tasks created by a specific schedule."),
48
+ includeHeartbeat: z
49
+ .boolean()
50
+ .optional()
51
+ .describe("Include heartbeat/system tasks in results (excluded by default)."),
52
+ limit: z
53
+ .number()
54
+ .int()
55
+ .min(1)
56
+ .max(100)
57
+ .optional()
58
+ .describe("Max tasks to return (default: 25, max: 100)."),
59
+ includeFull: z
60
+ .boolean()
61
+ .optional()
62
+ .describe("Return the full `task` text instead of a ~300-char `taskPreview`. Default false."),
63
+ });
64
+
65
+ export const getTasksOutputSchema = z.object({
66
+ yourAgentId: z.string().uuid().optional(),
67
+ tasks: z.array(TaskSummarySchema),
68
+ });
69
+
70
+ type GetTasksArgs = z.infer<typeof getTasksInputSchema>;
71
+
72
+ export async function getTasksHandler(
73
+ ctx: ToolCtx,
74
+ {
75
+ status,
76
+ mineOnly,
77
+ unassigned,
78
+ offeredToMe,
79
+ readyOnly,
80
+ taskType,
81
+ tags,
82
+ search,
83
+ scheduleId,
84
+ includeHeartbeat,
85
+ limit,
86
+ includeFull,
87
+ }: GetTasksArgs,
88
+ ): Promise<CallToolResult> {
89
+ const agentId = ctx.kind === "owner" ? ctx.agentId : undefined;
90
+
91
+ // Build filters. User context is hard-scoped by requestedByUserId and ignores
92
+ // agent-specific shortcuts like mineOnly/offeredToMe.
93
+ const taskFilters = {
94
+ status,
95
+ agentId: ctx.kind === "owner" && mineOnly ? (agentId ?? undefined) : undefined,
96
+ unassigned: ctx.kind === "owner" ? unassigned : undefined,
97
+ offeredTo: ctx.kind === "owner" && offeredToMe ? (agentId ?? undefined) : undefined,
98
+ readyOnly,
99
+ taskType,
100
+ tags,
101
+ search,
102
+ scheduleId,
103
+ includeHeartbeat,
104
+ limit,
105
+ requestedByUserId: ctx.kind === "user" ? ctx.userId : undefined,
106
+ };
107
+ // Default to slim rows (full `task` text → ~300-char `taskPreview`).
108
+ const tasks: Array<AgentTask | AgentTaskSummary> = includeFull
109
+ ? getAllTasks(taskFilters)
110
+ : getAllTasks(taskFilters, { slim: true });
111
+
112
+ // Slim rows carry a truncated `task`; surface it as `taskPreview` so the
113
+ // agent knows it is truncated. `includeFull` returns the full `task`.
114
+ const taskSummaries = tasks.map((t) => ({
115
+ id: t.id,
116
+ agentId: t.agentId,
117
+ ...(includeFull ? { task: t.task } : { taskPreview: t.task }),
118
+ status: t.status,
119
+ taskType: t.taskType,
120
+ tags: t.tags,
121
+ priority: t.priority,
122
+ dependsOn: t.dependsOn,
123
+ offeredTo: t.offeredTo,
124
+ createdAt: t.createdAt,
125
+ lastUpdatedAt: t.lastUpdatedAt,
126
+ finishedAt: t.finishedAt,
127
+ progress: t.progress,
128
+ }));
129
+
130
+ // Build filter description for message
131
+ const filters: string[] = [];
132
+ if (status) filters.push(`status='${status}'`);
133
+ if (ctx.kind === "owner" && mineOnly) filters.push("mine only");
134
+ if (ctx.kind === "owner" && unassigned) filters.push("unassigned");
135
+ if (ctx.kind === "owner" && offeredToMe) filters.push("offered to me");
136
+ if (readyOnly) filters.push("ready only");
137
+ if (taskType) filters.push(`type='${taskType}'`);
138
+ if (tags?.length) filters.push(`tags=[${tags.join(", ")}]`);
139
+ if (search) filters.push(`search='${search}'`);
140
+ if (scheduleId) filters.push(`scheduleId='${scheduleId}'`);
141
+
142
+ const filterMsg = filters.length > 0 ? ` (${filters.join(", ")})` : "";
143
+
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: `Found ${taskSummaries.length} task(s)${filterMsg}.`,
149
+ },
150
+ ],
151
+ structuredContent: {
152
+ yourAgentId: agentId,
153
+ tasks: taskSummaries,
154
+ },
155
+ };
156
+ }
157
+
27
158
  export const registerGetTasksTool = (server: McpServer) => {
28
159
  createToolRegistrar(server)(
29
160
  "get-tasks",
@@ -32,132 +163,9 @@ export const registerGetTasksTool = (server: McpServer) => {
32
163
  description:
33
164
  "Returns a list of tasks in the swarm with various filters. Sorted by priority (desc) then lastUpdatedAt (desc). Each row carries a `taskPreview` (~300 chars) — enough to pool-triage; pass includeFull:true (or call `get-task-details` by id) for the full `task` text.",
34
165
  annotations: { readOnlyHint: true },
35
-
36
- inputSchema: z.object({
37
- status: AgentTaskStatusSchema.optional().describe(
38
- "Filter by task status (unassigned, offered, pending, in_progress, completed, failed).",
39
- ),
40
- mineOnly: z.boolean().optional().describe("Only return tasks assigned to you."),
41
- unassigned: z.boolean().optional().describe("Only return unassigned tasks in the pool."),
42
- offeredToMe: z
43
- .boolean()
44
- .optional()
45
- .describe("Only return tasks offered to you (awaiting accept/reject)."),
46
- readyOnly: z.boolean().optional().describe("Only return tasks whose dependencies are met."),
47
- taskType: z.string().optional().describe("Filter by task type (e.g., 'bug', 'feature')."),
48
- tags: z.array(z.string()).optional().describe("Filter by any matching tag."),
49
- search: z.string().optional().describe("Search in task description."),
50
- scheduleId: z
51
- .string()
52
- .uuid()
53
- .optional()
54
- .describe("Filter by schedule ID to find tasks created by a specific schedule."),
55
- includeHeartbeat: z
56
- .boolean()
57
- .optional()
58
- .describe("Include heartbeat/system tasks in results (excluded by default)."),
59
- limit: z
60
- .number()
61
- .int()
62
- .min(1)
63
- .max(100)
64
- .optional()
65
- .describe("Max tasks to return (default: 25, max: 100)."),
66
- includeFull: z
67
- .boolean()
68
- .optional()
69
- .describe(
70
- "Return the full `task` text instead of a ~300-char `taskPreview`. Default false.",
71
- ),
72
- }),
73
- outputSchema: z.object({
74
- yourAgentId: z.string().uuid().optional(),
75
- tasks: z.array(TaskSummarySchema),
76
- }),
77
- },
78
- async (
79
- {
80
- status,
81
- mineOnly,
82
- unassigned,
83
- offeredToMe,
84
- readyOnly,
85
- taskType,
86
- tags,
87
- search,
88
- scheduleId,
89
- includeHeartbeat,
90
- limit,
91
- includeFull,
92
- },
93
- requestInfo,
94
- _meta,
95
- ) => {
96
- const agentId = requestInfo.agentId;
97
-
98
- // Build filters
99
- const taskFilters = {
100
- status,
101
- agentId: mineOnly ? (agentId ?? undefined) : undefined,
102
- unassigned,
103
- offeredTo: offeredToMe ? (agentId ?? undefined) : undefined,
104
- readyOnly,
105
- taskType,
106
- tags,
107
- search,
108
- scheduleId,
109
- includeHeartbeat,
110
- limit,
111
- };
112
- // Default to slim rows (full `task` text → ~300-char `taskPreview`).
113
- const tasks: Array<AgentTask | AgentTaskSummary> = includeFull
114
- ? getAllTasks(taskFilters)
115
- : getAllTasks(taskFilters, { slim: true });
116
-
117
- // Slim rows carry a truncated `task`; surface it as `taskPreview` so the
118
- // agent knows it is truncated. `includeFull` returns the full `task`.
119
- const taskSummaries = tasks.map((t) => ({
120
- id: t.id,
121
- agentId: t.agentId,
122
- ...(includeFull ? { task: t.task } : { taskPreview: t.task }),
123
- status: t.status,
124
- taskType: t.taskType,
125
- tags: t.tags,
126
- priority: t.priority,
127
- dependsOn: t.dependsOn,
128
- offeredTo: t.offeredTo,
129
- createdAt: t.createdAt,
130
- lastUpdatedAt: t.lastUpdatedAt,
131
- finishedAt: t.finishedAt,
132
- progress: t.progress,
133
- }));
134
-
135
- // Build filter description for message
136
- const filters: string[] = [];
137
- if (status) filters.push(`status='${status}'`);
138
- if (mineOnly) filters.push("mine only");
139
- if (unassigned) filters.push("unassigned");
140
- if (offeredToMe) filters.push("offered to me");
141
- if (readyOnly) filters.push("ready only");
142
- if (taskType) filters.push(`type='${taskType}'`);
143
- if (tags?.length) filters.push(`tags=[${tags.join(", ")}]`);
144
- if (search) filters.push(`search='${search}'`);
145
- if (scheduleId) filters.push(`scheduleId='${scheduleId}'`);
146
-
147
- const filterMsg = filters.length > 0 ? ` (${filters.join(", ")})` : "";
148
-
149
- return {
150
- content: [
151
- {
152
- type: "text",
153
- text: `Found ${taskSummaries.length} task(s)${filterMsg}.`,
154
- },
155
- ],
156
- structuredContent: {
157
- yourAgentId: agentId,
158
- tasks: taskSummaries,
159
- },
160
- };
166
+ inputSchema: getTasksInputSchema,
167
+ outputSchema: getTasksOutputSchema,
161
168
  },
169
+ async (args, info, _meta) => getTasksHandler(ownerCtx(info), args),
162
170
  );
163
171
  };