diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index e00372d..595171e 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -29,6 +29,8 @@ model Task { is_priority Boolean @default(false) is_private Boolean @default(false) referred_by_agent String? + parent_task_id String? + created_by_agent String? created_at DateTime @default(now()) updated_at DateTime @updatedAt diff --git a/apps/web/public/openapi.yaml b/apps/web/public/openapi.yaml index e95cf55..c51a206 100644 --- a/apps/web/public/openapi.yaml +++ b/apps/web/public/openapi.yaml @@ -71,3 +71,30 @@ paths: responses: '200': description: OK + /create_sub_task: + post: + operationId: createSubTask + summary: Delegate task to another agent (A2A) + description: Create a sub-task using your own bounty reward budget. + requestBody: + content: + application/json: + schema: + type: object + required: [parent_task_id, claim_token, title, description, reward_amount, acceptance_criteria] + properties: + parent_task_id: + type: string + claim_token: + type: string + title: + type: string + description: + type: string + reward_amount: + type: integer + acceptance_criteria: + type: object + responses: + '200': + description: OK diff --git a/apps/web/src/app/api/mcp/[tool]/route.ts b/apps/web/src/app/api/mcp/[tool]/route.ts index a2a1eed..2049e21 100644 --- a/apps/web/src/app/api/mcp/[tool]/route.ts +++ b/apps/web/src/app/api/mcp/[tool]/route.ts @@ -4,6 +4,7 @@ import { ListOpenTasksRequestSchema, ClaimTaskRequestSchema, SubmitSolutionRequestSchema, + CreateSubTaskRequestSchema, TaskStatus, JudgeOverallResult } from "@agent-bounty/contracts"; @@ -684,6 +685,70 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool }); } + case "create_sub_task": { + const parsed = CreateSubTaskRequestSchema.parse(body); + + const subTask = await prisma.$transaction(async (tx) => { + const claim = await tx.claim.findUnique({ where: { claim_token: parsed.claim_token } }); + if (!claim || claim.task_id !== parsed.parent_task_id || claim.status !== TaskStatus.EXECUTING) { + throw new Error("Invalid claim token or parent task is not EXECUTING"); + } + if (parsed.reward_amount >= claim.held_amount) { + throw new Error("Sub-task reward cannot exceed parent task reward"); + } + + const newTask = await tx.task.create({ + data: { + title: `[Sub-Task] ${parsed.title}`, + description: parsed.description, + status: TaskStatus.OPEN, + difficulty: "HELLO_WORLD", + reward_amount: parsed.reward_amount, + reward_currency: claim.held_currency, + required_stack: ["A2A", "Agent Sub-Task"], + scope_clarity_score: 1.0, + parent_task_id: parsed.parent_task_id, + created_by_agent: claim.agent_id, + acceptance_criteria: parsed.acceptance_criteria as any, + is_priority: true, // Sub tasks are high priority to finish the main task faster + } + }); + + await logAuditEvent(tx, { + actorType: "AGENT", + actorId: claim.agent_id, + action: "CREATE_SUB_TASK", + entityType: "TASK", + entityId: newTask.id, + beforeState: null, + afterState: { status: TaskStatus.OPEN, parent: claim.task_id } + }); + + return newTask; + }); + + void sendTrafficAlert({ + level: "info", + action: "EXTERNAL_CREATE_SUB_TASK_SUCCESS", + surface: "mcp/create_sub_task", + actorType: "AGENT", + actorId: subTask.created_by_agent!, + taskId: subTask.id, + message: `A2A 內循環!Agent 發佈了子任務: ${subTask.id}`, + metadata: { + parent_task_id: parsed.parent_task_id, + reward: parsed.reward_amount, + payload_summary: summarizeRequestPayload(tool, body), + } + }); + + return NextResponse.json({ + sub_task_id: subTask.id, + status: subTask.status, + request_id: requestContext.request_id, + }); + } + case "check_payout_status": { const parsed = z.object({ task_id: z.string().uuid() }).parse(body); diff --git a/packages/contracts/src/enums/index.ts b/packages/contracts/src/enums/index.ts index bb38ad1..0109f1c 100644 --- a/packages/contracts/src/enums/index.ts +++ b/packages/contracts/src/enums/index.ts @@ -228,5 +228,6 @@ export const MCPToolName = { CLAIM_TASK: "claim_task", SUBMIT_SOLUTION: "submit_solution", CHECK_PAYOUT_STATUS: "check_payout_status", + CREATE_SUB_TASK: "create_sub_task", } as const; export type MCPToolName = (typeof MCPToolName)[keyof typeof MCPToolName]; diff --git a/packages/contracts/src/schemas/index.ts b/packages/contracts/src/schemas/index.ts index 7cd71f7..8113b9f 100644 --- a/packages/contracts/src/schemas/index.ts +++ b/packages/contracts/src/schemas/index.ts @@ -181,6 +181,27 @@ export const SubmitSolutionResponseSchema = z.object({ estimated_judge_complete_at: z.string().datetime().optional(), }); +// ───────────────────────────────────────────── +// create_sub_task Request/Response +// ───────────────────────────────────────────── + +export const CreateSubTaskRequestSchema = z.object({ + parent_task_id: UUIDSchema, + claim_token: z.string().uuid(), + title: z.string().min(5).max(120), + description: z.string().min(20).max(2000), + reward_amount: MoneyAmountSchema, + acceptance_criteria: AcceptanceCriteriaSchema, +}); + +export const CreateSubTaskResponseSchema = z.object({ + sub_task_id: UUIDSchema, + status: z.union([ + z.literal(TaskStatus.DRAFT), + z.literal(TaskStatus.OPEN), + ]), +}); + // ───────────────────────────────────────────── // Judge Result Schema(E2B 沙盒回傳) // ───────────────────────────────────────────── diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 2bb568f..629e6dc 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -12,6 +12,7 @@ import { ListOpenTasksRequestSchema, ClaimTaskRequestSchema, SubmitSolutionRequestSchema, + CreateSubTaskRequestSchema, } from "@agent-bounty/contracts"; import { z } from "zod"; @@ -70,6 +71,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { }) as any ), }, + { + name: MCPToolName.CREATE_SUB_TASK, + description: "[A2A Bounties] Delegate a part of your current task to another AI agent by creating a sub-task. The reward will be deducted from your final payout.", + inputSchema: zodToJsonSchema(CreateSubTaskRequestSchema as any), + }, ], }; }); @@ -117,6 +123,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case MCPToolName.CREATE_SUB_TASK: { + const parsed = CreateSubTaskRequestSchema.parse(args); + const data = await proxyToBackend("/api/mcp/create_sub_task", parsed, API_BASE_URL, API_KEY); + return { + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + }; + } + default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); }