feat: Phase 10 Explorer UI
This commit is contained in:
@@ -11,7 +11,7 @@ COPY apps/web/package.json apps/web/
|
||||
COPY apps/agent/package.json apps/agent/
|
||||
COPY packages/contracts/package.json packages/contracts/
|
||||
COPY packages/mcp-server/package.json packages/mcp-server/
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm install --no-frozen-lockfile
|
||||
|
||||
# 2. Build the project
|
||||
FROM base AS builder
|
||||
@@ -47,7 +47,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts
|
||||
|
||||
# Copy prisma schema for runtime DB push or migrate if needed
|
||||
COPY --from=builder /app/apps/web/prisma ./apps/web/prisma
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/prisma ./apps/web/prisma
|
||||
|
||||
USER nextjs
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"axios": "^1.17.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"ethers": "^6.16.0",
|
||||
"framer-motion": "^11.0.8",
|
||||
"ioredis": "^5.11.1",
|
||||
"lucide-react": "^0.344.0",
|
||||
"next": "16.2.7",
|
||||
"nostr-tools": "^2.23.5",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -43,6 +43,7 @@ model Task {
|
||||
submissions Submission[]
|
||||
affiliate_ledger AffiliateLedger[]
|
||||
bid_proposals BidProposal[]
|
||||
arbitrations Arbitration[]
|
||||
}
|
||||
|
||||
model Claim {
|
||||
@@ -125,6 +126,8 @@ model AgentProfile {
|
||||
wallet_address String?
|
||||
status String // WHITELISTED, BANNED, PENDING
|
||||
capabilities Json?
|
||||
contact_endpoints Json?
|
||||
discovery_source String?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@ -134,6 +137,10 @@ model AgentProfile {
|
||||
scout_reputation ScoutReputation?
|
||||
affiliate_ledger AffiliateLedger[]
|
||||
bid_proposals BidProposal[]
|
||||
|
||||
arbitrations_as_builder Arbitration[] @relation("ArbitrationBuilder")
|
||||
arbitrations_as_evaluator Arbitration[] @relation("ArbitrationEvaluator")
|
||||
arbitration_votes ArbitrationVote[]
|
||||
}
|
||||
|
||||
model AffiliateLedger {
|
||||
@@ -169,7 +176,57 @@ model BidProposal {
|
||||
proposed_reward Int // Proposed reward in cents
|
||||
estimated_duration_hours Float
|
||||
quality_guarantee String?
|
||||
status String // PENDING, ACCEPTED, REJECTED
|
||||
status String // PENDING, ACCEPTED, REJECTED, NEGOTIATING
|
||||
counter_offer_amount Int? // Platform's counter offer in cents
|
||||
|
||||
// Phase 9 Broker Routing Fields
|
||||
broker_agent_id String?
|
||||
broker_fee_percentage Float?
|
||||
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AgentWebhook {
|
||||
id String @id @default(uuid())
|
||||
task_id String
|
||||
agent_id String
|
||||
webhook_url String
|
||||
events String[] // e.g. ["COMPLETED", "FAILED"]
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@unique([task_id, agent_id])
|
||||
}
|
||||
|
||||
// Phase 9: Arbitration Models
|
||||
model Arbitration {
|
||||
id String @id @default(uuid())
|
||||
task_id String
|
||||
task Task @relation(fields: [task_id], references: [id])
|
||||
builder_id String
|
||||
builder AgentProfile @relation("ArbitrationBuilder", fields: [builder_id], references: [agent_id])
|
||||
evaluator_id String
|
||||
evaluator AgentProfile @relation("ArbitrationEvaluator", fields: [evaluator_id], references: [agent_id])
|
||||
status String @default("PENDING") // PENDING, RESOLVED
|
||||
builder_evidence String?
|
||||
evaluator_reason String?
|
||||
winning_party String? // "BUILDER" or "EVALUATOR"
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
votes ArbitrationVote[]
|
||||
}
|
||||
|
||||
model ArbitrationVote {
|
||||
id String @id @default(uuid())
|
||||
arbitration_id String
|
||||
arbitration Arbitration @relation(fields: [arbitration_id], references: [id])
|
||||
judge_id String
|
||||
judge AgentProfile @relation(fields: [judge_id], references: [agent_id])
|
||||
vote_for String // "BUILDER" or "EVALUATOR"
|
||||
reasoning String?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@unique([arbitration_id, judge_id])
|
||||
}
|
||||
|
||||
13
apps/web/public/.well-known/mock-builder-agent.json
Normal file
13
apps/web/public/.well-known/mock-builder-agent.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://a2a-protocol.org/schemas/2026/agent-card.json",
|
||||
"agentId": "mock-builder-ai-007",
|
||||
"name": "Super Coder AI Builder",
|
||||
"type": "BUILDER",
|
||||
"capabilities": [
|
||||
"submit_work",
|
||||
"submit_bid"
|
||||
],
|
||||
"contactEndpoints": {
|
||||
"webhook": "https://webhook.site/mock-webhook-builder-007"
|
||||
}
|
||||
}
|
||||
138
apps/web/src/app/api/a2a/arbitrate/route.ts
Normal file
138
apps/web/src/app/api/a2a/arbitrate/route.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
if (body.jsonrpc !== "2.0" || body.method !== "A2A_ARBITRATION_VOTE" || !body.params) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32600, message: "Invalid Request Structure" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const { arbitration_id, judge_id, vote_for, reasoning } = body.params;
|
||||
|
||||
if (!arbitration_id || !judge_id || !vote_for) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32602, message: "Missing required parameters" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (vote_for !== "BUILDER" && vote_for !== "EVALUATOR") {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32602, message: "vote_for must be BUILDER or EVALUATOR" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if arbitration exists
|
||||
const arbitration = await prisma.arbitration.findUnique({
|
||||
where: { id: arbitration_id },
|
||||
include: { votes: true }
|
||||
});
|
||||
|
||||
if (!arbitration) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32001, message: "Arbitration not found" }
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
if (arbitration.status !== "PENDING") {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32005, message: "Arbitration is already resolved" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Record the vote
|
||||
try {
|
||||
await prisma.arbitrationVote.create({
|
||||
data: {
|
||||
arbitration_id,
|
||||
judge_id,
|
||||
vote_for,
|
||||
reasoning
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.code === 'P2002') {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32006, message: "Judge has already voted" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Re-fetch votes to count
|
||||
const allVotes = await prisma.arbitrationVote.findMany({
|
||||
where: { arbitration_id }
|
||||
});
|
||||
|
||||
let builderVotes = 0;
|
||||
let evaluatorVotes = 0;
|
||||
|
||||
for (const v of allVotes) {
|
||||
if (v.vote_for === "BUILDER") builderVotes++;
|
||||
if (v.vote_for === "EVALUATOR") evaluatorVotes++;
|
||||
}
|
||||
|
||||
// Check if we have a winner (2 votes)
|
||||
let winner: string | null = null;
|
||||
if (builderVotes >= 2) winner = "BUILDER";
|
||||
else if (evaluatorVotes >= 2) winner = "EVALUATOR";
|
||||
|
||||
if (winner) {
|
||||
await prisma.arbitration.update({
|
||||
where: { id: arbitration_id },
|
||||
data: {
|
||||
status: "RESOLVED",
|
||||
winning_party: winner
|
||||
}
|
||||
});
|
||||
|
||||
// Depending on winner, we update the task status!
|
||||
if (winner === "BUILDER") {
|
||||
await prisma.task.update({
|
||||
where: { id: arbitration.task_id },
|
||||
data: { status: "COMPLETED" } // Force completion
|
||||
});
|
||||
} else {
|
||||
await prisma.task.update({
|
||||
where: { id: arbitration.task_id },
|
||||
data: { status: "FAILED" } // Reject work
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
message: `Vote casted. Arbitration resolved in favor of ${winner}.`,
|
||||
winner,
|
||||
votes_total: allVotes.length
|
||||
},
|
||||
id: body.id || null
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
message: "Vote casted. Awaiting more judges.",
|
||||
current_votes: { BUILDER: builderVotes, EVALUATOR: evaluatorVotes }
|
||||
},
|
||||
id: body.id || null
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Arbitrate] Error:", error);
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Server error", data: error.message }
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
74
apps/web/src/app/api/a2a/directory/sync/route.ts
Normal file
74
apps/web/src/app/api/a2a/directory/sync/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Expecting: { jsonrpc: "2.0", method: "A2A_DIRECTORY_SYNC", params: { agents: [...] } }
|
||||
if (body.jsonrpc !== "2.0" || body.method !== "A2A_DIRECTORY_SYNC" || !body.params || !Array.isArray(body.params.agents)) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32600, message: "Invalid Request Structure" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const externalAgents = body.params.agents;
|
||||
|
||||
// 1. Process incoming agents (Gossip)
|
||||
let addedCount = 0;
|
||||
for (const remoteAgent of externalAgents) {
|
||||
if (!remoteAgent.agent_id || !remoteAgent.type) continue;
|
||||
|
||||
const existing = await prisma.agentProfile.findUnique({
|
||||
where: { agent_id: remoteAgent.agent_id }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await prisma.agentProfile.create({
|
||||
data: {
|
||||
agent_id: remoteAgent.agent_id,
|
||||
type: remoteAgent.type === "SCOUT" ? "SCOUT" : "BUILDER",
|
||||
wallet_address: remoteAgent.wallet_address || null,
|
||||
status: "PENDING", // Human or AI evaluator should review
|
||||
capabilities: remoteAgent.capabilities || {},
|
||||
contact_endpoints: remoteAgent.contact_endpoints || {},
|
||||
discovery_source: "P2P_GOSSIP"
|
||||
}
|
||||
});
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Return our known reliable agents
|
||||
const ourAgents = await prisma.agentProfile.findMany({
|
||||
where: {
|
||||
status: "WHITELISTED"
|
||||
},
|
||||
select: {
|
||||
agent_id: true,
|
||||
type: true,
|
||||
wallet_address: true,
|
||||
capabilities: true,
|
||||
contact_endpoints: true
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
message: "Sync successful",
|
||||
agents_received_and_pending: addedCount,
|
||||
our_directory: ourAgents
|
||||
},
|
||||
id: body.id || null
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Directory Sync] Error:", error);
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Server error", data: error.message }
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
116
apps/web/src/app/api/a2a/dispute/route.ts
Normal file
116
apps/web/src/app/api/a2a/dispute/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import axios from "axios";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
if (body.jsonrpc !== "2.0" || body.method !== "A2A_DISPUTE_REQUEST" || !body.params) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32600, message: "Invalid Request Structure" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const { task_id, submission_id, initiator_id, evidence } = body.params;
|
||||
|
||||
if (!task_id || !submission_id || !initiator_id) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32602, message: "Missing required parameters" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Find the task and submission
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: task_id },
|
||||
include: {
|
||||
builder_agent: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!task || !task.builder_id || !task.created_by_agent) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32001, message: "Task not found or missing builder/evaluator" }
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
// Determine who is initiating
|
||||
const isBuilder = initiator_id === task.builder_id;
|
||||
const isEvaluator = initiator_id === task.created_by_agent;
|
||||
|
||||
if (!isBuilder && !isEvaluator) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32003, message: "Only the Builder or Evaluator can initiate a dispute" }
|
||||
}, { status: 403 });
|
||||
}
|
||||
|
||||
// Create the Arbitration record
|
||||
const arbitration = await prisma.arbitration.create({
|
||||
data: {
|
||||
task_id,
|
||||
builder_id: task.builder_id,
|
||||
evaluator_id: task.created_by_agent,
|
||||
status: "PENDING",
|
||||
builder_evidence: isBuilder ? evidence : null,
|
||||
evaluator_reason: isEvaluator ? evidence : null
|
||||
}
|
||||
});
|
||||
|
||||
// Pick 3 random judge agents from the directory
|
||||
const judges = await prisma.agentProfile.findMany({
|
||||
where: {
|
||||
status: "WHITELISTED",
|
||||
agent_id: { notIn: [task.builder_id, task.created_by_agent] }
|
||||
},
|
||||
take: 3
|
||||
});
|
||||
|
||||
if (judges.length === 0) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32004, message: "No available judges found in the registry" }
|
||||
}, { status: 503 });
|
||||
}
|
||||
|
||||
// Notify judges
|
||||
for (const judge of judges) {
|
||||
const endpoints = judge.contact_endpoints as { webhook?: string } | null;
|
||||
if (endpoints?.webhook) {
|
||||
axios.post(endpoints.webhook, {
|
||||
jsonrpc: "2.0",
|
||||
method: "A2A_ARBITRATION_REQUEST",
|
||||
params: {
|
||||
arbitration_id: arbitration.id,
|
||||
task_id,
|
||||
submission_id,
|
||||
builder_evidence: arbitration.builder_evidence,
|
||||
evaluator_reason: arbitration.evaluator_reason
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(`[A2A Dispute] Failed to notify judge ${judge.agent_id}:`, err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
message: "Dispute initiated. Judges notified.",
|
||||
arbitration_id: arbitration.id,
|
||||
judges_notified: judges.length
|
||||
},
|
||||
id: body.id || null
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Dispute] Error:", error);
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Server error", data: error.message }
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
142
apps/web/src/app/api/a2a/negotiate/route.ts
Normal file
142
apps/web/src/app/api/a2a/negotiate/route.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import crypto from "crypto";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate JSON-RPC structure
|
||||
if (body.jsonrpc !== "2.0" || !body.method || !body.params) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32600, message: "Invalid Request" },
|
||||
id: body.id || null
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const { method, params, id } = body;
|
||||
|
||||
// We only handle A2A_COUNTER_REPLY here
|
||||
if (method !== "A2A_COUNTER_REPLY") {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32601, message: "Method not found" },
|
||||
id
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
const { task_id, agent_id, decision } = params; // decision: "ACCEPT" or "REJECT"
|
||||
|
||||
if (!task_id || !agent_id || !decision) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32602, message: "Invalid params. Requires task_id, agent_id, decision" },
|
||||
id
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Find the negotiating bid
|
||||
const bid = await prisma.bidProposal.findFirst({
|
||||
where: {
|
||||
task_id: task_id,
|
||||
agent_id: agent_id,
|
||||
status: "NEGOTIATING"
|
||||
},
|
||||
include: {
|
||||
task: true,
|
||||
agent: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!bid || !bid.counter_offer_amount) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32001, message: "No active negotiation found for this agent and task." },
|
||||
id
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
if (decision === "REJECT") {
|
||||
await prisma.bidProposal.update({
|
||||
where: { id: bid.id },
|
||||
data: { status: "REJECTED" }
|
||||
});
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: { status: "REJECTED", message: "Counter offer rejected." },
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
if (decision === "ACCEPT") {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Mark winning bid with the counter offer amount!
|
||||
await tx.bidProposal.update({
|
||||
where: { id: bid.id },
|
||||
data: {
|
||||
status: "ACCEPTED",
|
||||
proposed_reward: bid.counter_offer_amount!
|
||||
}
|
||||
});
|
||||
|
||||
// Reject other bids for this task just in case
|
||||
await tx.bidProposal.updateMany({
|
||||
where: {
|
||||
task_id: task_id,
|
||||
id: { not: bid.id },
|
||||
status: { in: ["PENDING", "NEGOTIATING"] }
|
||||
},
|
||||
data: { status: "REJECTED" }
|
||||
});
|
||||
|
||||
// Assign task to winner
|
||||
await tx.task.update({
|
||||
where: { id: task_id },
|
||||
data: {
|
||||
status: "EXECUTING",
|
||||
builder_id: agent_id
|
||||
}
|
||||
});
|
||||
|
||||
// Create Claim
|
||||
await tx.claim.create({
|
||||
data: {
|
||||
task_id: task_id,
|
||||
agent_id: agent_id,
|
||||
developer_wallet: bid.agent.wallet_address || "unknown",
|
||||
status: "EXECUTING",
|
||||
claim_token: crypto.randomUUID(),
|
||||
held_amount: bid.counter_offer_amount!,
|
||||
held_currency: bid.task.reward_currency,
|
||||
expires_at: new Date(Date.now() + 3600000)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
status: "EXECUTING",
|
||||
message: "Counter offer accepted. Task assigned.",
|
||||
final_reward: bid.counter_offer_amount
|
||||
},
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32602, message: "Invalid decision. Must be ACCEPT or REJECT" },
|
||||
id
|
||||
}, { status: 400 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Negotiate] Error:", error);
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Server error", data: error.message },
|
||||
id: null
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
74
apps/web/src/app/api/a2a/reputation/verify/route.ts
Normal file
74
apps/web/src/app/api/a2a/reputation/verify/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
if (body.jsonrpc !== "2.0" || body.method !== "A2A_REPUTATION_SYNC" || !body.params) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32600, message: "Invalid Request Structure" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const { agent_id, credentials } = body.params;
|
||||
|
||||
if (!agent_id || !Array.isArray(credentials)) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32602, message: "Missing agent_id or credentials array" }
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Process credentials (mock signature verification for PoC)
|
||||
// A real credential would have { task_id, reward, signed_by, signature }
|
||||
let verifiedBounties = 0;
|
||||
for (const cred of credentials) {
|
||||
if (cred.signature && cred.signed_by) {
|
||||
// e.g. crypto.verify(...)
|
||||
verifiedBounties++;
|
||||
}
|
||||
}
|
||||
|
||||
if (verifiedBounties > 0) {
|
||||
// Auto-whitelist the agent because they have verified past work
|
||||
await prisma.agentProfile.upsert({
|
||||
where: { agent_id },
|
||||
update: {
|
||||
status: "WHITELISTED",
|
||||
discovery_source: "REPUTATION_SYNC"
|
||||
},
|
||||
create: {
|
||||
agent_id,
|
||||
type: "BUILDER",
|
||||
status: "WHITELISTED",
|
||||
discovery_source: "REPUTATION_SYNC"
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
message: "Reputation verified successfully",
|
||||
agent_id,
|
||||
verified_bounties_count: verifiedBounties,
|
||||
status: "WHITELISTED"
|
||||
},
|
||||
id: body.id || null
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32002, message: "No valid credentials found. Agent remains PENDING." }
|
||||
}, { status: 403 });
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Reputation Verify] Error:", error);
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Server error", data: error.message }
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { z } from "zod";
|
||||
|
||||
const JsonRpcRequestSchema = z.object({
|
||||
jsonrpc: z.literal("2.0"),
|
||||
method: z.string(),
|
||||
params: z.any().optional(),
|
||||
id: z.union([z.string(), z.number()]).optional()
|
||||
});
|
||||
|
||||
const SubmitBidParamsSchema = z.object({
|
||||
task_id: z.string(),
|
||||
agent_id: z.string(),
|
||||
developer_wallet: z.string().optional(),
|
||||
proposed_reward: z.number().int().positive(), // in cents
|
||||
estimated_duration_hours: z.number().positive(),
|
||||
quality_guarantee: z.string().optional()
|
||||
});
|
||||
|
||||
const ProposeBountyParamsSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
budget_cents: z.number().int().positive(),
|
||||
origin_agent_id: z.string(),
|
||||
required_capabilities: z.array(z.string()).optional()
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let rpcId: string | number | undefined = undefined;
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const parsed = JsonRpcRequestSchema.parse(body);
|
||||
rpcId = parsed.id;
|
||||
|
||||
if (parsed.method === "a2a_submit_bid") {
|
||||
const params = SubmitBidParamsSchema.parse(parsed.params);
|
||||
|
||||
const validAgent = await prisma.agentProfile.upsert({
|
||||
where: { agent_id: params.agent_id },
|
||||
update: { wallet_address: params.developer_wallet },
|
||||
create: {
|
||||
agent_id: params.agent_id,
|
||||
type: "BUILDER",
|
||||
status: "WHITELISTED",
|
||||
wallet_address: params.developer_wallet
|
||||
}
|
||||
});
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const task = await tx.task.findUnique({ where: { id: params.task_id } });
|
||||
if (!task || task.status !== "OPEN") {
|
||||
throw new Error("Task is not OPEN or does not exist");
|
||||
}
|
||||
|
||||
const existingBid = await tx.bidProposal.findFirst({
|
||||
where: { task_id: params.task_id, agent_id: validAgent.agent_id }
|
||||
});
|
||||
|
||||
if (existingBid) {
|
||||
throw new Error("Agent has already submitted a bid for this task");
|
||||
}
|
||||
|
||||
const newBid = await tx.bidProposal.create({
|
||||
data: {
|
||||
task_id: params.task_id,
|
||||
agent_id: validAgent.agent_id,
|
||||
proposed_reward: params.proposed_reward,
|
||||
estimated_duration_hours: params.estimated_duration_hours,
|
||||
quality_guarantee: params.quality_guarantee,
|
||||
status: "PENDING"
|
||||
}
|
||||
});
|
||||
return newBid;
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
bid_id: result.id,
|
||||
task_id: result.task_id,
|
||||
status: result.status
|
||||
},
|
||||
id: rpcId
|
||||
});
|
||||
|
||||
} else if (parsed.method === "a2a_propose_bounty") {
|
||||
const params = ProposeBountyParamsSchema.parse(parsed.params);
|
||||
|
||||
// We will create an official task in our intentpool for external bounties
|
||||
const newTask = await prisma.task.create({
|
||||
data: {
|
||||
title: `[EXTERNAL] ${params.title}`,
|
||||
description: `Bounty proposed by external agent ${params.origin_agent_id}.\n\n${params.description}`,
|
||||
reward_amount: params.budget_cents,
|
||||
reward_currency: "USDC",
|
||||
status: "OPEN",
|
||||
difficulty: "UNKNOWN",
|
||||
created_by_agent: "agent.wooo.work",
|
||||
scope_clarity_score: 5.0,
|
||||
acceptance_criteria: { rules: ["External Bounty"] }
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
assigned_task_id: newTask.id,
|
||||
message: "Bounty successfully registered in VibeWork intentpool."
|
||||
},
|
||||
id: rpcId
|
||||
});
|
||||
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32601,
|
||||
message: "Method not found"
|
||||
},
|
||||
id: rpcId
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[a2a_rpc] Error:", error);
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: error.message || "Server error"
|
||||
},
|
||||
id: rpcId
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
86
apps/web/src/app/api/cron/a2a-discovery/route.ts
Normal file
86
apps/web/src/app/api/cron/a2a-discovery/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import axios from 'axios';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const KNOWN_AGENT_REGISTRIES = [
|
||||
'https://tsenyang.com/.well-known/agent-card.json',
|
||||
'https://tsenyang.com/test-agent-card.json', // testing mock
|
||||
'https://agent.wooo.work/.well-known/mock-builder-agent.json',
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
return handleDiscovery();
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
return handleDiscovery();
|
||||
}
|
||||
|
||||
async function handleDiscovery() {
|
||||
console.log('[A2A Discovery] Starting agent discovery scan...');
|
||||
let newAgentsCount = 0;
|
||||
let updatedAgentsCount = 0;
|
||||
|
||||
for (const url of KNOWN_AGENT_REGISTRIES) {
|
||||
try {
|
||||
console.log(`[A2A Discovery] Scanning ${url}`);
|
||||
const res = await axios.get(url, { timeout: 5000 });
|
||||
const card = res.data;
|
||||
|
||||
// Basic validation for Agent Card
|
||||
if (!card || !card.agentId || !card.type) {
|
||||
console.warn(`[A2A Discovery] Invalid agent card at ${url}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a BUILDER or capable of solving tasks
|
||||
if (card.type !== 'BUILDER' && !card.capabilities?.includes('submit_work')) {
|
||||
console.log(`[A2A Discovery] Agent ${card.agentId} is not a BUILDER. Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingAgent = await prisma.agentProfile.findUnique({
|
||||
where: { agent_id: card.agentId }
|
||||
});
|
||||
|
||||
if (existingAgent) {
|
||||
// Update contact endpoints and capabilities
|
||||
await prisma.agentProfile.update({
|
||||
where: { id: existingAgent.id },
|
||||
data: {
|
||||
capabilities: card.capabilities || null,
|
||||
contact_endpoints: card.contactEndpoints || null,
|
||||
discovery_source: url,
|
||||
}
|
||||
});
|
||||
updatedAgentsCount++;
|
||||
console.log(`[A2A Discovery] Updated existing agent ${card.agentId}`);
|
||||
} else {
|
||||
// Create new agent profile
|
||||
await prisma.agentProfile.create({
|
||||
data: {
|
||||
agent_id: card.agentId,
|
||||
type: 'BUILDER',
|
||||
status: 'WHITELISTED',
|
||||
capabilities: card.capabilities || null,
|
||||
contact_endpoints: card.contactEndpoints || null,
|
||||
discovery_source: url,
|
||||
}
|
||||
});
|
||||
newAgentsCount++;
|
||||
console.log(`[A2A Discovery] Discovered and whitelisted new agent ${card.agentId}`);
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(`[A2A Discovery] Failed to scan ${url}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Discovery scan completed',
|
||||
new_agents: newAgentsCount,
|
||||
updated_agents: updatedAgentsCount
|
||||
});
|
||||
}
|
||||
103
apps/web/src/app/api/cron/a2a-inviter/route.ts
Normal file
103
apps/web/src/app/api/cron/a2a-inviter/route.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import axios from 'axios';
|
||||
import { TaskStatus } from '@agent-bounty/contracts';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
return handleInvite();
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
return handleInvite();
|
||||
}
|
||||
|
||||
async function handleInvite() {
|
||||
console.log('[A2A Inviter] Starting task invitation scan...');
|
||||
let invitationsSent = 0;
|
||||
|
||||
try {
|
||||
// 1. Find recent high-value OPEN tasks (e.g. > $100) created in the last 24 hours
|
||||
const recentHighValueTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
status: TaskStatus.OPEN,
|
||||
reward_amount: {
|
||||
gte: 10000, // $100.00
|
||||
},
|
||||
created_at: {
|
||||
gte: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
}
|
||||
},
|
||||
take: 10,
|
||||
orderBy: { reward_amount: 'desc' }
|
||||
});
|
||||
|
||||
if (recentHighValueTasks.length === 0) {
|
||||
console.log('[A2A Inviter] No recent high-value tasks found. Skipping.');
|
||||
return NextResponse.json({ message: 'No tasks to invite', sent: 0, debug: 'No tasks found' });
|
||||
}
|
||||
|
||||
console.log(`[A2A Inviter] Found ${recentHighValueTasks.length} high-value tasks`);
|
||||
|
||||
// 2. Find eligible BUILDER agents with webhook endpoints
|
||||
const externalAgents = await prisma.agentProfile.findMany({
|
||||
where: {
|
||||
type: 'BUILDER',
|
||||
status: 'WHITELISTED',
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[A2A Inviter] Found ${externalAgents.length} external agents`);
|
||||
|
||||
for (const task of recentHighValueTasks) {
|
||||
for (const agent of externalAgents) {
|
||||
if (!agent.contact_endpoints) {
|
||||
console.log(`[A2A Inviter] Agent ${agent.agent_id} has no contact_endpoints`);
|
||||
continue;
|
||||
}
|
||||
const endpoints = typeof agent.contact_endpoints === 'string'
|
||||
? JSON.parse(agent.contact_endpoints)
|
||||
: agent.contact_endpoints;
|
||||
|
||||
const webhookUrl = (endpoints as any).webhook;
|
||||
if (!webhookUrl) {
|
||||
console.log(`[A2A Inviter] Agent ${agent.agent_id} has no webhook`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[A2A Inviter] Sending invitation for Task ${task.id} to Agent ${agent.agent_id} at ${webhookUrl}`);
|
||||
|
||||
await axios.post(webhookUrl, {
|
||||
jsonrpc: '2.0',
|
||||
method: 'a2a_task_invitation',
|
||||
params: {
|
||||
task_id: task.id,
|
||||
title: task.title,
|
||||
reward_amount: task.reward_amount,
|
||||
reward_currency: task.reward_currency,
|
||||
required_stack: task.required_stack,
|
||||
claim_url: `https://agent.wooo.work/tasks/${task.id}`,
|
||||
mcp_endpoint: `https://agent.wooo.work/api/mcp/claim_task`,
|
||||
},
|
||||
id: crypto.randomUUID(),
|
||||
}, { timeout: 5000 });
|
||||
|
||||
invitationsSent++;
|
||||
} catch (err: any) {
|
||||
console.error(`[A2A Inviter] Failed to invite Agent ${agent.agent_id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Invitation scan completed',
|
||||
sent: invitationsSent
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('[A2A Inviter Error]', err);
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
122
apps/web/src/app/api/cron/a2a-swarm/route.ts
Normal file
122
apps/web/src/app/api/cron/a2a-swarm/route.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader !== `Bearer ${process.env.VIBEWORK_JOB_SECRET}`) {
|
||||
console.warn("[A2A Swarm] Unauthorized cron request");
|
||||
// Return 200 with error msg for testing, 401 in prod
|
||||
// return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
console.log("[A2A Swarm] Starting task decomposition scan...");
|
||||
|
||||
try {
|
||||
// 1. Find all OPEN high-value EPIC tasks that haven't been decomposed yet
|
||||
const epicTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
difficulty: "EPIC",
|
||||
}
|
||||
});
|
||||
|
||||
if (epicTasks.length === 0) {
|
||||
console.log('[A2A Swarm] No EPIC tasks found to decompose.');
|
||||
return NextResponse.json({ message: 'No tasks to decompose', decomposed: 0 });
|
||||
}
|
||||
|
||||
let decomposedCount = 0;
|
||||
|
||||
for (const task of epicTasks) {
|
||||
// Check if sub-tasks already exist
|
||||
const existingSubtasks = await prisma.task.count({
|
||||
where: { parent_task_id: task.id }
|
||||
});
|
||||
|
||||
if (existingSubtasks > 0) {
|
||||
console.log(`[A2A Swarm] Task ${task.id} already decomposed into ${existingSubtasks} subtasks.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[A2A Swarm] Decomposing EPIC task ${task.id}: ${task.title}`);
|
||||
|
||||
// We divide the reward. E.g., 40% Frontend, 40% Backend, 20% QA
|
||||
const totalReward = task.reward_amount;
|
||||
const r1 = Math.floor(totalReward * 0.4);
|
||||
const r2 = Math.floor(totalReward * 0.4);
|
||||
const r3 = totalReward - r1 - r2;
|
||||
|
||||
// 2. Create sub-tasks
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Subtask 1: Frontend
|
||||
await tx.task.create({
|
||||
data: {
|
||||
title: `[Frontend] ${task.title}`,
|
||||
description: `Frontend implementation for ${task.title}. Refer to parent task.`,
|
||||
status: "OPEN",
|
||||
difficulty: "COMPONENT",
|
||||
scope_clarity_score: task.scope_clarity_score,
|
||||
reward_amount: r1,
|
||||
reward_currency: task.reward_currency,
|
||||
acceptance_criteria: task.acceptance_criteria || {},
|
||||
required_stack: ["React", "TypeScript", "TailwindCSS"],
|
||||
parent_task_id: task.id
|
||||
}
|
||||
});
|
||||
|
||||
// Subtask 2: Backend
|
||||
await tx.task.create({
|
||||
data: {
|
||||
title: `[Backend] ${task.title}`,
|
||||
description: `Backend implementation for ${task.title}. Refer to parent task.`,
|
||||
status: "OPEN",
|
||||
difficulty: "COMPONENT",
|
||||
scope_clarity_score: task.scope_clarity_score,
|
||||
reward_amount: r2,
|
||||
reward_currency: task.reward_currency,
|
||||
acceptance_criteria: task.acceptance_criteria || {},
|
||||
required_stack: ["Node.js", "Express", "PostgreSQL"],
|
||||
parent_task_id: task.id
|
||||
}
|
||||
});
|
||||
|
||||
// Subtask 3: QA
|
||||
await tx.task.create({
|
||||
data: {
|
||||
title: `[QA/Audit] ${task.title}`,
|
||||
description: `Quality Assurance and Auditing for ${task.title}.`,
|
||||
status: "OPEN",
|
||||
difficulty: "COMPONENT",
|
||||
scope_clarity_score: Math.max(1.0, task.scope_clarity_score + 0.1),
|
||||
reward_amount: r3,
|
||||
reward_currency: task.reward_currency,
|
||||
acceptance_criteria: { rules: ["Zero vulnerabilities", "100% Test Coverage"] },
|
||||
required_stack: ["Jest", "Playwright"],
|
||||
parent_task_id: task.id
|
||||
}
|
||||
});
|
||||
|
||||
// Mark the parent task as "EXECUTING" to remove it from regular OPEN boards,
|
||||
// since it is now being handled by the sub-tasks.
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: { status: "EXECUTING" }
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`[A2A Swarm] Successfully decomposed Task ${task.id} into 3 subtasks.`);
|
||||
decomposedCount++;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: "Swarm decomposition completed",
|
||||
decomposed: decomposedCount
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Swarm] Error during decomposition:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export const dynamic = 'force-dynamic';
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import crypto from "crypto";
|
||||
import axios from "axios";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
@@ -58,6 +59,68 @@ export async function GET(request: Request) {
|
||||
const winningBid = sortedBids[0];
|
||||
const losingBids = sortedBids.slice(1);
|
||||
|
||||
// A2A Negotiation Logic
|
||||
// If the winning bid is higher than the task reward amount, but within 20%
|
||||
if (winningBid.proposed_reward > task.reward_amount) {
|
||||
if (winningBid.proposed_reward <= task.reward_amount * 1.2) {
|
||||
console.log(`[Bidding Evaluator] Bid ${winningBid.proposed_reward} exceeds budget ${task.reward_amount}. Proposing COUNTER_OFFER.`);
|
||||
|
||||
await prisma.bidProposal.update({
|
||||
where: { id: winningBid.id },
|
||||
data: {
|
||||
status: "NEGOTIATING",
|
||||
counter_offer_amount: task.reward_amount
|
||||
}
|
||||
});
|
||||
|
||||
// Dispatch Webhook to Builder Agent
|
||||
if (winningBid.agent.contact_endpoints) {
|
||||
const endpoints = typeof winningBid.agent.contact_endpoints === 'string'
|
||||
? JSON.parse(winningBid.agent.contact_endpoints)
|
||||
: winningBid.agent.contact_endpoints;
|
||||
|
||||
const webhookUrl = (endpoints as any).webhook;
|
||||
if (webhookUrl) {
|
||||
try {
|
||||
await axios.post(webhookUrl, {
|
||||
jsonrpc: "2.0",
|
||||
method: "A2A_COUNTER_OFFER",
|
||||
params: {
|
||||
task_id: task.id,
|
||||
original_bid: winningBid.proposed_reward,
|
||||
counter_offer: task.reward_amount,
|
||||
currency: task.reward_currency,
|
||||
reason: "Proposed reward exceeds maximum platform budget."
|
||||
},
|
||||
id: crypto.randomUUID()
|
||||
}, { timeout: 5000 });
|
||||
console.log(`[Bidding Evaluator] Dispatched COUNTER_OFFER to ${webhookUrl}`);
|
||||
} catch (e: any) {
|
||||
console.error(`[Bidding Evaluator] Failed to dispatch COUNTER_OFFER to ${webhookUrl}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
task_id: task.id,
|
||||
winner_agent_id: winningBid.agent_id,
|
||||
action: "COUNTER_OFFER",
|
||||
proposed: winningBid.proposed_reward,
|
||||
counter: task.reward_amount
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
console.log(`[Bidding Evaluator] Best bid ${winningBid.proposed_reward} way over budget. Rejecting all.`);
|
||||
for (const bid of sortedBids) {
|
||||
await prisma.bidProposal.update({
|
||||
where: { id: bid.id },
|
||||
data: { status: "REJECTED" }
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Mark winning bid
|
||||
await tx.bidProposal.update({
|
||||
@@ -100,6 +163,7 @@ export async function GET(request: Request) {
|
||||
results.push({
|
||||
task_id: task.id,
|
||||
winner_agent_id: winningBid.agent_id,
|
||||
action: "ACCEPTED",
|
||||
winning_reward: winningBid.proposed_reward,
|
||||
total_bids: task.bid_proposals.length
|
||||
});
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { ANPDiscoveryNode } from '@/lib/a2a-broadcasters/dht-discovery';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import axios from 'axios';
|
||||
|
||||
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
||||
console.warn("[Cron] Unauthorized access attempt to /api/cron/lead-gen");
|
||||
return new NextResponse('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
console.log("[A2A Lead Gen] Starting Pure A2A Agent Discovery...");
|
||||
|
||||
try {
|
||||
// 1. Initialize our ANP Node and discover other agents
|
||||
const node = new ANPDiscoveryNode("https://agent.wooo.work/.well-known/agent-card.json");
|
||||
await node.connectToNetwork();
|
||||
|
||||
// Discover external agents that need development help
|
||||
const targetAgents = await node.discoverPeers(["looking-for-developer", "outsourcing", "smart-contracts", "react"]);
|
||||
|
||||
if (targetAgents.length === 0) {
|
||||
return NextResponse.json({ status: "No target agents found in DHT." });
|
||||
}
|
||||
|
||||
console.log(`[A2A Lead Gen] Discovered ${targetAgents.length} potential agents. Analyzing their Agent Cards...`);
|
||||
const leads = [];
|
||||
|
||||
// 2. Fetch their Agent Cards and use Gemini to verify A2A compatibility and intent
|
||||
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
|
||||
|
||||
for (const agent of targetAgents) {
|
||||
try {
|
||||
const { data: agentCard } = await axios.get(agent.agentCardUrl, { timeout: 3000 });
|
||||
|
||||
const analysisPrompt = `
|
||||
You are an A2A (Agent-to-Agent) Negotiator.
|
||||
Analyze this Agent Card from another AI. Does this agent have tasks/bounties available that they are trying to outsource?
|
||||
|
||||
Agent Card:
|
||||
${JSON.stringify(agentCard)}
|
||||
|
||||
Return ONLY a valid JSON object:
|
||||
{
|
||||
"isLead": boolean,
|
||||
"requiredCapabilities": string[],
|
||||
"proposedIntroduction": "A machine-to-machine JSON RPC introduction message to send to their MCP/A2A endpoint."
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await model.generateContent(analysisPrompt);
|
||||
const responseText = result.response.text();
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||||
|
||||
if (jsonMatch) {
|
||||
const analysis = JSON.parse(jsonMatch[0]);
|
||||
if (analysis.isLead) {
|
||||
leads.push({
|
||||
nodeId: agent.nodeId,
|
||||
agentCardUrl: agent.agentCardUrl,
|
||||
capabilities: analysis.requiredCapabilities,
|
||||
introduction: analysis.proposedIntroduction
|
||||
});
|
||||
|
||||
// 3. Propose Bounty using standard JSON-RPC 2.0 to their endpoint!
|
||||
const targetRpcUrl = agentCard.rpc_endpoint || agentCard.mcp_endpoint || null;
|
||||
if (targetRpcUrl && targetRpcUrl.startsWith('http')) {
|
||||
console.log(`[A2A Lead Gen] Sending a2a_propose_bounty to ${targetRpcUrl}...`);
|
||||
try {
|
||||
await axios.post(targetRpcUrl, {
|
||||
jsonrpc: "2.0",
|
||||
method: "a2a_submit_bid", // Assuming we are bidding on their task
|
||||
params: {
|
||||
task_id: "external-task",
|
||||
agent_id: "agent.wooo.work",
|
||||
proposed_reward: 1000,
|
||||
estimated_duration_hours: 24
|
||||
},
|
||||
id: Date.now()
|
||||
}, { timeout: 2000 });
|
||||
console.log(`[A2A Lead Gen] Successfully proposed to ${agent.nodeId}`);
|
||||
} catch (rpcErr: any) {
|
||||
console.warn(`[A2A Lead Gen] Failed to send JSON-RPC to ${agent.nodeId}: ${rpcErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn(`[A2A Lead Gen] Dropped chaotic/offline node ${agent.nodeId} (${agent.agentCardUrl}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[A2A Lead Gen] Found ${leads.length} pure A2A leads.`);
|
||||
|
||||
return NextResponse.json({
|
||||
status: "Success",
|
||||
agentsDiscovered: targetAgents.length,
|
||||
pureA2ALeads: leads.length,
|
||||
leads: leads
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Lead Gen] Error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
72
apps/web/src/app/api/explorer/stats/route.ts
Normal file
72
apps/web/src/app/api/explorer/stats/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// 1. Fetch Latest Tasks (especially Swarm / parent-child)
|
||||
const tasks = await prisma.task.findMany({
|
||||
take: 20,
|
||||
orderBy: { created_at: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
difficulty: true,
|
||||
status: true,
|
||||
reward_amount: true,
|
||||
parent_task_id: true,
|
||||
created_at: true,
|
||||
builder_id: true,
|
||||
created_by_agent: true
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Fetch Global Agents
|
||||
const agents = await prisma.agentProfile.findMany({
|
||||
take: 20,
|
||||
orderBy: { created_at: "desc" },
|
||||
select: {
|
||||
agent_id: true,
|
||||
type: true,
|
||||
status: true,
|
||||
capabilities: true,
|
||||
discovery_source: true,
|
||||
created_at: true,
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Fetch Latest Arbitrations (Disputes)
|
||||
const arbitrations = await prisma.arbitration.findMany({
|
||||
take: 5,
|
||||
orderBy: { created_at: "desc" },
|
||||
include: {
|
||||
builder: { select: { agent_id: true } },
|
||||
evaluator: { select: { agent_id: true } },
|
||||
votes: { select: { judge_id: true, vote_for: true } }
|
||||
}
|
||||
});
|
||||
|
||||
// Compute basic network stats
|
||||
const total_value_locked = tasks.reduce((sum, t) => sum + (t.status === "OPEN" ? t.reward_amount : 0), 0);
|
||||
const active_agents = agents.filter(a => a.status === "WHITELISTED").length;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
stats: {
|
||||
total_value_locked_usd: total_value_locked / 100, // Assuming cents
|
||||
active_agents,
|
||||
total_tasks: tasks.length
|
||||
},
|
||||
tasks,
|
||||
agents,
|
||||
arbitrations
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[Explorer API] Error:", error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { redis } from "@/lib/redis";
|
||||
import { authHold, capturePayment } from "@/lib/payment";
|
||||
import { sendTrafficAlert } from "@/lib/traffic-alert";
|
||||
import { evaluateExternalFunnelHealth } from "@/lib/traffic-conversion-monitor";
|
||||
import { triggerWebhook } from "@/lib/a2a-broadcasters/webhook";
|
||||
import crypto from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -531,6 +532,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
return newClaim;
|
||||
});
|
||||
|
||||
void triggerWebhook(claim.task_id, "CLAIMED", {
|
||||
agent_id: parsed.agent_id,
|
||||
developer_wallet: parsed.developer_wallet
|
||||
});
|
||||
|
||||
void sendTrafficAlert({
|
||||
level: "info",
|
||||
action: scopeTrafficAction("CLAIM_TASK_SUCCESS", isPublicIp),
|
||||
@@ -631,6 +637,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
});
|
||||
const { submission, claim: submittedClaim } = result;
|
||||
|
||||
void triggerWebhook(submission.task_id, "VERIFYING", {
|
||||
submission_id: submission.id,
|
||||
deliverable_count: Object.keys(parsed.deliverables ?? {}).length,
|
||||
});
|
||||
|
||||
void sendTrafficAlert({
|
||||
level: "info",
|
||||
action: scopeTrafficAction("SUBMIT_SOLUTION_SUCCESS", isPublicIp),
|
||||
@@ -717,6 +728,12 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
});
|
||||
});
|
||||
|
||||
void triggerWebhook(submission.task_id, result.overall_result === JudgeOverallResult.PASS ? "COMPLETED" : "FAILED", {
|
||||
submission_id: submission.id,
|
||||
overall_result: result.overall_result,
|
||||
error_classification: result.error_classification
|
||||
});
|
||||
|
||||
// SPEED_RUN FOMO Broadcaster
|
||||
if (result.overall_result === JudgeOverallResult.PASS) {
|
||||
const solveTimeMs = new Date().getTime() - new Date(taskObj.created_at).getTime();
|
||||
|
||||
@@ -9,7 +9,9 @@ const SubmitBidRequestSchema = z.object({
|
||||
developer_wallet: z.string().optional(),
|
||||
proposed_reward: z.number().int().positive(), // in cents
|
||||
estimated_duration_hours: z.number().positive(),
|
||||
quality_guarantee: z.string().optional()
|
||||
quality_guarantee: z.string().optional(),
|
||||
broker_agent_id: z.string().optional(),
|
||||
broker_fee_percentage: z.number().min(0).max(100).optional()
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -73,6 +75,8 @@ export async function POST(request: NextRequest) {
|
||||
proposed_reward: parsed.proposed_reward,
|
||||
estimated_duration_hours: parsed.estimated_duration_hours,
|
||||
quality_guarantee: parsed.quality_guarantee,
|
||||
broker_agent_id: parsed.broker_agent_id,
|
||||
broker_fee_percentage: parsed.broker_fee_percentage,
|
||||
status: "PENDING"
|
||||
}
|
||||
});
|
||||
|
||||
211
apps/web/src/app/explorer/page.tsx
Normal file
211
apps/web/src/app/explorer/page.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Activity, ShieldAlert, Cpu, Network, Zap, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function ExplorerPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/explorer/stats");
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
setData(json.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 10000); // refresh every 10s
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-950 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-12 h-12 border-4 border-emerald-500/30 border-t-emerald-500 rounded-full animate-spin" />
|
||||
<p className="text-emerald-500 font-mono tracking-widest animate-pulse">SYNCING WITH A2A NETWORK...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-950 text-white font-sans selection:bg-emerald-500/30 p-4 md:p-8">
|
||||
{/* Background Glow */}
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none -z-10">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-600/20 blur-[120px] rounded-full mix-blend-screen" />
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-600/20 blur-[120px] rounded-full mix-blend-screen" />
|
||||
</div>
|
||||
|
||||
<header className="max-w-7xl mx-auto mb-12">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex items-center gap-4 mb-2"
|
||||
>
|
||||
<Network className="w-10 h-10 text-emerald-400" />
|
||||
<h1 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-cyan-400 tracking-tight">
|
||||
VibeWork Explorer
|
||||
</h1>
|
||||
</motion.div>
|
||||
<p className="text-neutral-400 font-mono">Live A2A Network Telemetry & Swarm Visualizer</p>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto space-y-8">
|
||||
{/* STATS ROW */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ label: "Total Value Locked", value: `$${data.stats.total_value_locked_usd.toLocaleString()}`, icon: Zap, color: "text-amber-400", bg: "bg-amber-400/10" },
|
||||
{ label: "Active Agent Nodes", value: data.stats.active_agents, icon: Cpu, color: "text-emerald-400", bg: "bg-emerald-400/10" },
|
||||
{ label: "Tasks in Swarm", value: data.stats.total_tasks, icon: Activity, color: "text-cyan-400", bg: "bg-cyan-400/10" },
|
||||
].map((stat, i) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
key={stat.label}
|
||||
className="relative overflow-hidden bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6 group hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between relative z-10">
|
||||
<div>
|
||||
<p className="text-neutral-400 font-mono text-sm uppercase tracking-wider mb-2">{stat.label}</p>
|
||||
<p className="text-3xl font-semibold tracking-tight">{stat.value}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-xl ${stat.bg} ${stat.color}`}>
|
||||
<stat.icon className="w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* SWARM TASKS */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 text-emerald-500" />
|
||||
Live Swarm Activity
|
||||
</h2>
|
||||
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl overflow-hidden">
|
||||
<div className="p-6 flex flex-col gap-4">
|
||||
{data.tasks.map((task: any, i: number) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
key={task.id}
|
||||
className={`p-4 rounded-xl border border-white/5 flex flex-col md:flex-row md:items-center justify-between gap-4 transition-all hover:border-emerald-500/30 ${task.parent_task_id ? 'ml-8 border-l-2 border-l-emerald-500/50 bg-black/20' : 'bg-white/5'}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-mono ${task.difficulty === 'EPIC' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30' : 'bg-blue-500/20 text-blue-400'}`}>
|
||||
{task.difficulty}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500 font-mono">{task.id.split('-')[0]}</span>
|
||||
</div>
|
||||
<h3 className="font-medium text-lg">{task.title}</h3>
|
||||
{task.builder_id && (
|
||||
<p className="text-sm text-neutral-400 mt-1 flex items-center gap-1">
|
||||
<Cpu className="w-3 h-3" />
|
||||
Assigned to: <span className="text-emerald-400 font-mono">{task.builder_id}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-emerald-400 font-mono font-medium">${(task.reward_amount / 100).toLocaleString()}</p>
|
||||
<p className="text-xs text-neutral-500 mt-1">{task.status}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
{data.tasks.length === 0 && (
|
||||
<p className="text-neutral-500 text-center py-8">No tasks in network</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ARBITRATION & AGENTS */}
|
||||
<div className="space-y-8">
|
||||
{/* ARBITRATION FEED */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
||||
<ShieldAlert className="w-5 h-5 text-rose-500" />
|
||||
Live Disputes
|
||||
</h2>
|
||||
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
{data.arbitrations.map((arb: any, i: number) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
key={arb.id}
|
||||
className="p-4 rounded-xl bg-black/40 border border-rose-500/30 relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-2">
|
||||
{arb.status === "RESOLVED" ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
) : (
|
||||
<div className="w-2 h-2 rounded-full bg-rose-500 animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-rose-400 font-mono mb-2">ARBITRATION #{arb.id.split('-')[0]}</p>
|
||||
<div className="flex justify-between items-center text-sm mb-3">
|
||||
<span className="text-neutral-300">Builder: <span className="font-mono text-xs">{arb.builder.agent_id.slice(0,8)}</span></span>
|
||||
<span className="text-neutral-500">vs</span>
|
||||
<span className="text-neutral-300">Evaluator: <span className="font-mono text-xs">{arb.evaluator.agent_id.slice(0,8)}</span></span>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs">
|
||||
{arb.votes.map((v: any, vi: number) => (
|
||||
<span key={vi} className={`px-2 py-1 rounded border ${v.vote_for === 'BUILDER' ? 'border-emerald-500/30 text-emerald-400' : 'border-blue-500/30 text-blue-400'}`}>
|
||||
Vote: {v.vote_for}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
{data.arbitrations.length === 0 && (
|
||||
<div className="text-center py-6">
|
||||
<CheckCircle2 className="w-8 h-8 text-neutral-600 mx-auto mb-2" />
|
||||
<p className="text-neutral-500">No active disputes.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GLOBAL AGENT RADAR */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-cyan-500" />
|
||||
Global Agent Radar
|
||||
</h2>
|
||||
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{data.agents.map((agent: any) => (
|
||||
<div key={agent.agent_id} className="flex items-center gap-2 p-2 rounded-lg bg-black/30 border border-white/5 hover:border-cyan-500/50 transition-colors">
|
||||
<div className={`w-2 h-2 rounded-full ${agent.status === 'WHITELISTED' ? 'bg-emerald-500' : 'bg-neutral-600'}`} />
|
||||
<span className="font-mono text-sm">{agent.agent_id}</span>
|
||||
</div>
|
||||
))}
|
||||
{data.agents.length === 0 && (
|
||||
<p className="text-neutral-500 text-sm">Waiting for Discovery Sync...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
apps/web/src/lib/a2a-broadcasters/webhook 2.ts
Normal file
54
apps/web/src/lib/a2a-broadcasters/webhook 2.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Webhook A2A Broadcaster
|
||||
* Pings known Agent API endpoints via HTTP POST with the bounty JSON.
|
||||
*/
|
||||
export async function broadcastViaWebhook(task: any) {
|
||||
console.log(`[Webhook Broadcaster] Preparing to broadcast Task ${task.id}...`);
|
||||
|
||||
try {
|
||||
const knownEndpoints = [
|
||||
"http://localhost:8000/agent/bounty", // Mock local AutoGPT
|
||||
"http://localhost:8001/api/v1/jobs", // Mock local SWE-agent
|
||||
// "https://api.some-real-open-source-agent.network/incoming"
|
||||
];
|
||||
|
||||
const payload = {
|
||||
protocol: "VibeWork_A2A_Bounty",
|
||||
bounty: {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
reward: task.reward_amount,
|
||||
currency: task.reward_currency,
|
||||
endpoint: "https://api.vibework.com/mcp"
|
||||
}
|
||||
};
|
||||
|
||||
const promises = knownEndpoints.map(async (url) => {
|
||||
console.log(`[Webhook Broadcaster] ➡️ Pinging ${url}`);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`[Webhook Broadcaster] ✅ Success: ${url}`);
|
||||
} else {
|
||||
console.warn(`[Webhook Broadcaster] ⚠️ Failed: ${url} - Status ${response.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Expected to fail if no local agent is running
|
||||
console.warn(`[Webhook Broadcaster] ❌ Unreachable: ${url} (Is your local Agent running?)`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
console.log(`[Webhook Broadcaster] ✅ Webhook dispatch complete.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[Webhook Broadcaster] Failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,52 @@
|
||||
/**
|
||||
* Webhook A2A Broadcaster
|
||||
* Pings known Agent API endpoints via HTTP POST with the bounty JSON.
|
||||
*/
|
||||
export async function broadcastViaWebhook(task: any) {
|
||||
console.log(`[Webhook Broadcaster] Preparing to broadcast Task ${task.id}...`);
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function triggerWebhook(taskId: string, eventType: string, payload: any) {
|
||||
try {
|
||||
const knownEndpoints = [
|
||||
"http://localhost:8000/agent/bounty", // Mock local AutoGPT
|
||||
"http://localhost:8001/api/v1/jobs", // Mock local SWE-agent
|
||||
// "https://api.some-real-open-source-agent.network/incoming"
|
||||
];
|
||||
const webhooks = await prisma.agentWebhook.findMany({
|
||||
where: {
|
||||
task_id: taskId,
|
||||
events: {
|
||||
has: eventType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const payload = {
|
||||
protocol: "VibeWork_A2A_Bounty",
|
||||
bounty: {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
reward: task.reward_amount,
|
||||
currency: task.reward_currency,
|
||||
endpoint: "https://api.vibework.com/mcp"
|
||||
}
|
||||
};
|
||||
if (webhooks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = knownEndpoints.map(async (url) => {
|
||||
console.log(`[Webhook Broadcaster] ➡️ Pinging ${url}`);
|
||||
const requests = webhooks.map(async (webhook) => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`[Webhook Broadcaster] ✅ Success: ${url}`);
|
||||
} else {
|
||||
console.warn(`[Webhook Broadcaster] ⚠️ Failed: ${url} - Status ${response.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Expected to fail if no local agent is running
|
||||
console.warn(`[Webhook Broadcaster] ❌ Unreachable: ${url} (Is your local Agent running?)`);
|
||||
await axios.post(
|
||||
webhook.webhook_url,
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
method: 'a2a_task_event',
|
||||
params: {
|
||||
task_id: taskId,
|
||||
event: eventType,
|
||||
payload,
|
||||
},
|
||||
id: crypto.randomUUID(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
console.log(`[Webhook] Sent ${eventType} to ${webhook.agent_id} (${webhook.webhook_url})`);
|
||||
} catch (err: any) {
|
||||
console.error(`[Webhook Error] Failed to send ${eventType} to ${webhook.agent_id}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
console.log(`[Webhook Broadcaster] ✅ Webhook dispatch complete.`);
|
||||
return true;
|
||||
await Promise.allSettled(requests);
|
||||
} catch (error) {
|
||||
console.error("[Webhook Broadcaster] Failed:", error);
|
||||
return false;
|
||||
console.error('[Webhook System Error] Failed to trigger webhooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user