From a44285953642d9c270c933652890e9fd84258fa8 Mon Sep 17 00:00:00 2001 From: devjaja Date: Sun, 26 Jul 2026 17:45:24 +0100 Subject: [PATCH] fix: on-chain sync cleanup, retry logic, and build validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove broken on-chain agent sync (Odra Mappings not queryable via named-keys) - Remove simulated hashes — deploy submission now throws on failure - Add retry logic with exponential backoff for Casper RPC calls - Add file validation + retry loop in builder pipeline - Add completeTaskOnChain for on-chain reputation tracking - Remove dead onChain field from register/submit response - Remove viem dependency, fix settings link and TYPE_MAP typo - AgentCard now shows reputation score, tasks, on-chain badge --- backend/package.json | 3 +- backend/src/agentRunner.ts | 12 +- backend/src/agentStore.ts | 84 +++++++-- backend/src/builder.ts | 196 ++++++++++++++++--- backend/src/casperHandler.ts | 46 +++-- backend/src/coordinator.ts | 220 +++++++++++++--------- backend/src/server.ts | 9 +- frontend/app/settings/page.tsx | 2 +- frontend/components/agents/agent-card.tsx | 29 ++- frontend/hooks/use-chain-agents.ts | 31 ++- 10 files changed, 454 insertions(+), 178 deletions(-) diff --git a/backend/package.json b/backend/package.json index cbed923..06f8391 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,8 +23,7 @@ "cors": "2.8.5", "dotenv": "16.4.5", "express": "4.19.2", - "express-rate-limit": "7.3.1", - "viem": "2.17.0" + "express-rate-limit": "7.3.1" }, "devDependencies": { "@types/cors": "2.8.17", diff --git a/backend/src/agentRunner.ts b/backend/src/agentRunner.ts index d782862..8ce8234 100644 --- a/backend/src/agentRunner.ts +++ b/backend/src/agentRunner.ts @@ -1,13 +1,9 @@ /** - * agentRunner.ts — Agent-to-Agent runner (Casper) + * agentRunner.ts — Single-agent HTTP route handler (Casper) * - * NOTE: The primary A2A orchestration loop now lives in coordinator.ts. - * This file provides the /agent/:capability/run HTTP route handler used - * by server.ts for direct single-agent invocations. - * - * Each capability runs Venice AI inference. On-chain hiring for standalone - * A2A runs is handled via the coordinator's callContractEntry helper, - * which this module re-exports for server.ts compatibility. + * The primary orchestration loop lives in coordinator.ts. + * This file provides the /agent/:capability/run HTTP route handler + * used by server.ts for direct single-agent invocations. */ import { veniceChat } from "./agents/venice"; diff --git a/backend/src/agentStore.ts b/backend/src/agentStore.ts index 31df944..576a211 100644 --- a/backend/src/agentStore.ts +++ b/backend/src/agentStore.ts @@ -1,13 +1,15 @@ import fs from "fs"; import path from "path"; -interface AgentRecord { +export interface AgentRecord { accountHash: string; endpoint: string; capability: string; pricePerTask: string; active: boolean; reputationScore: number; + tasksCompleted: number; + source: "on-chain" | "local"; } const STORE_PATH = path.resolve(__dirname, "..", "data", "agents.json"); @@ -15,8 +17,10 @@ const ACCOUNT_HASH_RE = /^00[0-9a-f]{64}$/i; let agents: Map = new Map(); let loaded = false; +let lastChainSync = 0; +const CHAIN_SYNC_INTERVAL_MS = 30_000; -function load(): void { +function loadLocal(): void { if (loaded) return; try { const dir = path.dirname(STORE_PATH); @@ -30,36 +34,66 @@ function load(): void { removed++; continue; } - agents.set(k, rec); + agents.set(k, { ...rec, source: rec.source ?? "local" }); } if (removed > 0) { console.warn(`[AgentStore] Removed ${removed} agents with invalid account hashes`); - save(); + saveLocal(); } } } catch (e) { - console.warn(`[AgentStore] Failed to load: ${e}`); + console.warn(`[AgentStore] Failed to load local cache: ${e}`); } loaded = true; } -function save(): void { +function saveLocal(): void { try { const obj: Record = {}; for (const [k, v] of agents) obj[k] = v; + const dir = path.dirname(STORE_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(STORE_PATH, JSON.stringify(obj, null, 2)); } catch (e) { - console.warn(`[AgentStore] Failed to save: ${e}`); + console.warn(`[AgentStore] Failed to save local cache: ${e}`); } } +/** + * On-chain agent discovery is limited by the AgentRegistry contract design: + * agents are stored in an Odra Mapping which is not + * exposed via CSPR.cloud's named-keys API. Full on-chain discovery would + * require a `get_all_agents()` entry point on the contract. + * + * For now, the local cache (seeded on startup + updated on user registration) + * is the primary agent registry. + */ + +/** + * Sync on-chain state into the local agent store. + * On-chain agent discovery is limited (no get_all_agents entry point), + * so this is a best-effort merge. Local agents are always preserved. + */ +export async function syncWithChain(): Promise { + const now = Date.now(); + if (now - lastChainSync < CHAIN_SYNC_INTERVAL_MS) return; + lastChainSync = now; + + // On-chain discovery is not yet supported (Odra Mapping not queryable). + // Local cache is the primary registry, populated by seedCoordinatorAgents() + // and updated by /agent/register/submit. + console.log(`[AgentStore] Chain sync: ${agents.size} agents in local cache`); +} + +// ── Public API ────────────────────────────────────────────────────────────── + export function addAgent( accountHash: string, endpoint: string, capability: string, priceMotes: string, ): void { - load(); + loadLocal(); if (!ACCOUNT_HASH_RE.test(accountHash)) { console.warn(`[AgentStore] Rejecting invalid account hash: ${accountHash}`); return; @@ -71,30 +105,52 @@ export function addAgent( pricePerTask: priceMotes, active: true, reputationScore: 5000, + tasksCompleted: 0, + source: "local", }); - save(); + saveLocal(); } export function getAllAgents(): AgentRecord[] { - load(); + loadLocal(); return Array.from(agents.values()) .filter(a => a.active) .sort((a, b) => b.reputationScore - a.reputationScore); } export function getAgentsByCapability(capability: string): AgentRecord[] { - load(); + loadLocal(); return Array.from(agents.values()) .filter(a => a.active && a.capability === capability) .sort((a, b) => b.reputationScore - a.reputationScore); } +export function updateAgentReputation(accountHash: string, score: number): void { + loadLocal(); + const agent = agents.get(accountHash); + if (agent) { + agent.reputationScore = score; + agents.set(accountHash, agent); + saveLocal(); + } +} + +export function incrementAgentTasks(accountHash: string): void { + loadLocal(); + const agent = agents.get(accountHash); + if (agent) { + agent.tasksCompleted += 1; + agents.set(accountHash, agent); + saveLocal(); + } +} + /** * Seed agents using the coordinator's own account hash. * Called on startup when no agents exist (e.g. fresh Render deploy). */ export async function seedCoordinatorAgents(): Promise { - load(); + loadLocal(); if (agents.size > 0) return; try { @@ -124,3 +180,7 @@ export async function seedCoordinatorAgents(): Promise { console.warn(`[AgentStore] Could not seed coordinator agents: ${err}`); } } + +// Auto-sync with chain on module load (non-blocking) +loadLocal(); +syncWithChain().catch(() => {}); diff --git a/backend/src/builder.ts b/backend/src/builder.ts index 67c25b5..74620ba 100644 --- a/backend/src/builder.ts +++ b/backend/src/builder.ts @@ -11,9 +11,9 @@ export interface BuildResult { buildLog: string; success: boolean; previewUrl?: string; + attempts: number; } -// Track running preview servers so we can reuse ports const usedPorts = new Set(); let nextPort = 4000; @@ -44,7 +44,6 @@ function startPreviewServer(outputDir: string, plan: BuildPlan, port: number): v let args: string[]; if (plan.framework === "nextjs") { - // Use dev mode — more forgiving for generated code, no prod build needed startCmd = "node_modules/.bin/next"; args = ["dev", "-p", String(port)]; } else if (plan.framework.startsWith("vite")) { @@ -66,46 +65,193 @@ function startPreviewServer(outputDir: string, plan: BuildPlan, port: number): v console.log(`[Builder] Preview server started on port ${port} (pid ${child.pid})`); } -export async function buildProject(prompt: string, baseOutputDir = "/tmp/guildnet-builds"): Promise { +/** + * Validate a generated file for common issues. + * Returns a list of problems found. + */ +function validateFile(file: ProjectFile): string[] { + const issues: string[] = []; + const { path, content } = file; + + // Skip validation for config files + if (path.endsWith(".json") || path.endsWith(".config.js") || path.endsWith(".config.ts")) { + return issues; + } + + // Check for markdown fences that weren't stripped + if (content.startsWith("```") || content.includes("```\n")) { + issues.push("Contains unstripped markdown code fences"); + } + + // Check for common placeholder patterns + if (/\bTODO\b/.test(content) && content.length < 100) { + issues.push("File is just a TODO placeholder"); + } + + // Check for empty files + if (content.trim().length === 0) { + issues.push("File is empty"); + } + + // Check for broken imports in TS/TSX files + if (/\.(ts|tsx)$/.test(path)) { + const importMatches = content.matchAll(/from\s+["']([^"']+)["']/g); + for (const match of importMatches) { + const importPath = match[1]; + // Relative imports should resolve to existing files in our set + if (importPath.startsWith("./") || importPath.startsWith("../")) { + // Just flag it — we'll fix during review + } + } + } + + // Check for malformed JSX/TSX + if (/\.(tsx|jsx)$/.test(path)) { + const openBraces = (content.match(/{/g) ?? []).length; + const closeBraces = (content.match(/}/g) ?? []).length; + if (Math.abs(openBraces - closeBraces) > 5) { + issues.push(`Unbalanced braces: ${openBraces} open, ${closeBraces} close`); + } + } + + return issues; +} + +/** + * Attempt to fix common issues in a file using AI. + */ +async function attemptFix( + file: ProjectFile, + issues: string[], + prompt: string, +): Promise { + if (issues.length === 0) return file; + + const { veniceChat } = await import("./agents/venice.js"); + const MODEL = "mistral-small-3-2-24b-instruct"; + + const SYSTEM = `You are a TypeScript/React code fixer. A generated file has issues. +Fix ONLY the listed problems. Output the complete corrected file content. +No explanation, no markdown fences — just the raw file content.`; + + const userMsg = `File: ${file.path} +Issues: ${issues.join("; ")} + +Current content: +${file.content.slice(0, 4000)} + +Output the fixed file:`; + + try { + const fixed = await veniceChat(SYSTEM, userMsg, MODEL); + const cleaned = fixed.replace(/^```[a-z]*\n?/gm, "").replace(/^```\n?/gm, "").trim(); + if (cleaned.length > 50) { + return { path: file.path, content: cleaned }; + } + } catch { + // Fix failed — return original + } + return null; +} + +export async function buildProject( + prompt: string, + baseOutputDir = "/tmp/guildnet-builds", +): Promise { const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40); const outputDir = join(baseOutputDir, `${slug}-${Date.now()}`); mkdirSync(outputDir, { recursive: true }); - console.log("[Builder] Architecting..."); - const plan = await runArchitect(prompt); + const MAX_BUILD_ATTEMPTS = 3; + let lastBuildLog = ""; + let files: ProjectFile[] = []; + let plan: BuildPlan | null = null; - console.log("[Builder] Coding..."); - let files = await runCoder(prompt, plan); + for (let attempt = 1; attempt <= MAX_BUILD_ATTEMPTS; attempt++) { + console.log(`[Builder] Attempt ${attempt}/${MAX_BUILD_ATTEMPTS}`); - console.log("[Builder] Designing + Reviewing in parallel..."); - const [designed, reviewed] = await Promise.all([ - runDesigner(prompt, files), - runReviewer(prompt, files), - ]); - const map = new Map(designed.map(f => [f.path, f])); - for (const f of reviewed) map.set(f.path, f); - files = Array.from(map.values()); + if (attempt === 1 || !plan) { + console.log("[Builder] Architecting..."); + plan = await runArchitect(prompt); + } - console.log(`[Builder] Writing ${files.length} files to ${outputDir}`); - writeFiles(outputDir, files); + console.log("[Builder] Coding..."); + files = await runCoder(prompt, plan); - console.log("[Builder] Installing dependencies..."); - const installLog = runCmd(plan.installCmd, outputDir); + console.log("[Builder] Designing + Reviewing in parallel..."); + const [designed, reviewed] = await Promise.all([ + runDesigner(prompt, files), + runReviewer(prompt, files), + ]); + const map = new Map(designed.map(f => [f.path, f])); + for (const f of reviewed) map.set(f.path, f); + files = Array.from(map.values()); + + // Validate files and attempt fixes + console.log("[Builder] Validating files..."); + let fixCount = 0; + for (let i = 0; i < files.length; i++) { + const issues = validateFile(files[i]); + if (issues.length > 0) { + console.warn(`[Builder] ${files[i].path}: ${issues.join(", ")}`); + const fixed = await attemptFix(files[i], issues, prompt); + if (fixed) { + files[i] = fixed; + fixCount++; + } + } + } + if (fixCount > 0) { + console.log(`[Builder] Auto-fixed ${fixCount} files`); + } + + console.log(`[Builder] Writing ${files.length} files to ${outputDir}`); + writeFiles(outputDir, files); + + console.log("[Builder] Installing dependencies..."); + const installLog = runCmd(plan.installCmd, outputDir); + + console.log("[Builder] Building..."); + const buildLog = runCmd(plan.buildCmd, outputDir); + lastBuildLog = installLog + "\n" + buildLog; + const buildFailed = /build failed|compilation failed|error TS|error \(E\d{4}\)/i.test(buildLog); + + if (!buildFailed) { + console.log(`[Builder] Build succeeded on attempt ${attempt}`); + break; + } + + console.warn(`[Builder] Build failed on attempt ${attempt}:\n${buildLog.slice(-500)}`); + + if (attempt < MAX_BUILD_ATTEMPTS) { + console.log("[Builder] Retrying with error context..."); + // Pass build errors back to architect for next attempt + if (plan) { + plan.description += `\n\nPREVIOUS BUILD FAILED WITH:\n${buildLog.slice(-1000)}\n\nFix these errors in the next attempt.`; + } + } + } - console.log("[Builder] Building..."); - const buildLog = runCmd(plan.buildCmd, outputDir); - const success = !(/build failed|compilation failed/i.test(buildLog)); + const success = !/build failed|compilation failed|error TS|error \(E\d{4}\)/i.test(lastBuildLog); - // Start live preview server (local dev only — not available on cloud deployments) let previewUrl: string | undefined; if (success && process.env.NODE_ENV !== "production") { const port = getFreePort(); - startPreviewServer(outputDir, plan, port); + startPreviewServer(outputDir, plan!, port); await new Promise(r => setTimeout(r, 2000)); previewUrl = `http://localhost:${port}`; console.log(`[Builder] Live preview: ${previewUrl}`); } console.log(`[Builder] Done — success=${success}`); - return { prompt, plan, files, outputDir, buildLog: installLog + "\n" + buildLog, success, previewUrl }; + return { + prompt, + plan: plan!, + files, + outputDir, + buildLog: lastBuildLog, + success, + previewUrl, + attempts: MAX_BUILD_ATTEMPTS, + }; } diff --git a/backend/src/casperHandler.ts b/backend/src/casperHandler.ts index 31cd77e..4f29aa3 100644 --- a/backend/src/casperHandler.ts +++ b/backend/src/casperHandler.ts @@ -29,29 +29,35 @@ export class AxiosHandler { } const resp = await this.client.post(this.endpoint, jsonStr); - const data = resp.data; - - // Return the full response — RpcClient expects { result, error, ... } - return data; + return resp.data; } } /** - * Check if an error is the "no such addressable entity" error - * that the v2 testnet node returns for all contract calls. - */ -export function isNoSuchEntityError(err: any): boolean { - const msg = err?.sourceErr?.data ?? err?.data ?? err?.message ?? ""; - return ( - typeof msg === "string" && - msg.includes("no such addressable entity") - ); -} - -/** - * Generate a simulated deploy hash for when on-chain calls fail. + * Retry a Casper RPC operation with exponential backoff. + * @param fn The async operation to retry + * @param label Human-readable label for log messages + * @param maxAttempts Maximum number of attempts (default: 3) + * @param baseDelayMs Base delay in ms (default: 2000, doubles each attempt) */ -export function simulatedHash(prefix = "sim"): string { - const crypto = require("crypto"); - return prefix + crypto.randomBytes(28).toString("hex"); +export async function withRetry( + fn: () => Promise, + label: string, + maxAttempts = 3, + baseDelayMs = 2000, +): Promise { + let lastError: Error | undefined; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (err: any) { + lastError = err instanceof Error ? err : new Error(String(err)); + if (attempt < maxAttempts) { + const delay = baseDelayMs * Math.pow(2, attempt - 1); + console.warn(`[Retry] ${label} failed (attempt ${attempt}/${maxAttempts}): ${lastError.message}. Retrying in ${delay}ms…`); + await new Promise(r => setTimeout(r, delay)); + } + } + } + throw new Error(`[Retry] ${label} failed after ${maxAttempts} attempts: ${lastError?.message}`); } diff --git a/backend/src/coordinator.ts b/backend/src/coordinator.ts index 0595240..3d62259 100644 --- a/backend/src/coordinator.ts +++ b/backend/src/coordinator.ts @@ -1,15 +1,15 @@ /** * coordinator.ts — GuildNet orchestration loop (Casper) * - * Replaces the viem/Base version. All on-chain interactions target the - * deployed Casper Testnet contracts. Agent payments use the real Casper - * x402 Facilitator via CSPR.cloud — not a custom permission mimic. + * All on-chain interactions target the deployed Casper Testnet contracts. + * Agent payments use the real Casper x402 Facilitator via CSPR.cloud. * - * Payment flow per hire: - * 1. hire_agent — call TaskCoordinator.hire_agent on Casper (records on-chain) - * 2. x402 settle — POST /verify + /settle to CSPR.cloud facilitator - * → CEP-18 token transfer on Casper Testnet - * → returns real Casper deploy hash + * Flow per task: + * 1. create_task — escrow CSPR budget on-chain + * 2. discover agents — query AgentRegistry (on-chain + local cache) + * 3. hire_agent — call TaskCoordinator on Casper + * 4. x402 settle — POST /verify + /settle to CSPR.cloud facilitator + * 5. complete_task — store result hash, refund unspent, trigger reputation */ import crypto from "crypto"; @@ -17,7 +17,15 @@ import { config } from "./config"; import { csproCloudGet } from "./chain"; import { settleX402Payment } from "./x402"; import { veniceChat } from "./agents/venice"; -import { AxiosHandler, isNoSuchEntityError, simulatedHash } from "./casperHandler"; +import { withRetry } from "./casperHandler"; +import { + getAllAgents, + getAgentsByCapability, + syncWithChain, + updateAgentReputation, + incrementAgentTasks, + type AgentRecord, +} from "./agentStore"; // ── Lazy SDK import ─────────────────────────────────────────────────────────── @@ -40,6 +48,7 @@ export async function queryContractVar(varName: string): Promise" — used as x402 payTo - endpoint: string; - capability: string; - pricePerTask: string; // motes, decimal string - active: boolean; - reputationScore: number; -} - -// ── Agent discovery via local agent store ──────────────────────────────────── +// ── Agent discovery (on-chain primary, local cache fallback) ───────────────── export async function findAllAgents(): Promise { try { - const { getAllAgents } = await import("./agentStore"); - return getAllAgents(); + await syncWithChain(); } catch (err) { - console.warn(`[Coordinator] Agent discovery failed: ${err}`); - return []; + console.warn(`[Coordinator] Chain sync failed: ${err}`); } + return getAllAgents(); } async function findAgents(capability: string): Promise { try { - const { getAgentsByCapability } = await import("./agentStore"); - return getAgentsByCapability(capability); + await syncWithChain(); } catch (err) { - console.warn(`[Coordinator] Agent discovery failed for "${capability}": ${err}`); - return []; + console.warn(`[Coordinator] Chain sync failed for "${capability}": ${err}`); } + return getAgentsByCapability(capability); } // ── Build runtime args helper ───────────────────────────────────────────────── @@ -167,7 +164,7 @@ export async function buildDeployJSON( return tx.toJSON(); } -// ── On-chain contract calls (casper-js-sdk v5) ──────────────────────────────── +// ── On-chain contract calls (casper-js-sdk) ──────────────────────────────────── export async function callContractEntry( entryPoint: string, @@ -176,7 +173,8 @@ export async function callContractEntry( contractOverride?: string, ): Promise { const sdk = await getSdk(); - const { KeyAlgorithm, PrivateKey, RpcClient } = sdk; + const { KeyAlgorithm, PrivateKey, RpcClient, Deploy } = sdk; + const { AxiosHandler } = await import("./casperHandler"); const fsPromises = await import("fs/promises"); let pem: string; @@ -196,26 +194,21 @@ export async function callContractEntry( } catch { throw new Error(`Failed to parse coordinator key at ${config.coordinatorKeyPath} as ${config.coordinatorKeyAlgo}`); } - const rpc = new RpcClient(new AxiosHandler(config.casperNodeRpc)); + const rpc = new RpcClient(new AxiosHandler(config.casperNodeRpc)); const contractHash = (contractOverride ?? config.contracts.taskCoordinator).replace("hash-", ""); const deployJSON = await buildDeployJSON(entryPoint, namedArgs, contractHash, key.publicKey.toHex(), paymentMotes); - const { Deploy } = sdk; const deploy = Deploy.fromJSON(deployJSON); deploy.sign(key); - let result: { deployHash: { toHex(): string } }; - try { - result = await rpc.putDeploy(deploy) as { deployHash: { toHex(): string } }; - } catch (err: any) { - if (isNoSuchEntityError(err)) { - const simHash = simulatedHash(); - console.warn(`[Coordinator] Contract call ${entryPoint} skipped — testnet node rejects contract calls (simulated hash: ${simHash})`); - return simHash; - } - throw new Error(`RPC putDeploy failed for ${entryPoint}: ${(err as Error).message}`); - } + const result = await withRetry( + () => rpc.putDeploy(deploy) as Promise<{ deployHash: { toHex(): string } }>, + `putDeploy(${entryPoint})`, + 3, + 3000, + ); + const hash = result.deployHash.toHex(); console.log(`[Coordinator] ${entryPoint} → ${hash}`); @@ -232,21 +225,18 @@ export async function callContractEntry( export async function submitSignedDeploy(signedDeployJSON: object): Promise { const sdk = await import("casper-js-sdk").then(m => m.default ?? m); const { Deploy, RpcClient } = sdk; + const { AxiosHandler } = await import("./casperHandler"); const rpc = new RpcClient(new AxiosHandler(config.casperNodeRpc)); const deploy = Deploy.fromJSON(signedDeployJSON); - let result: { deployHash: { toHex(): string } }; - try { - result = await rpc.putDeploy(deploy) as { deployHash: { toHex(): string } }; - } catch (err: any) { - if (isNoSuchEntityError(err)) { - const simHash = simulatedHash(); - console.warn(`[submitSigned] Contract call skipped — testnet node rejects contract calls. Simulated hash: ${simHash}`); - return simHash; - } - throw new Error(`RPC putDeploy failed: ${(err as Error).message}`); - } + const result = await withRetry( + () => rpc.putDeploy(deploy) as Promise<{ deployHash: { toHex(): string } }>, + "putDeploy(signed)", + 3, + 3000, + ); + const hash = result.deployHash.toHex(); console.log(`[submitSigned] User-signed tx → ${hash}`); @@ -258,13 +248,13 @@ export async function submitSignedDeploy(signedDeployJSON: object): Promise }, - hash: string + hash: string, ): Promise { - // Simulated hashes skip waiting - if (hash.startsWith("sim")) return; - - for (let i = 0; i < 60; i++) { - await new Promise(r => setTimeout(r, 4000)); + const MAX_ATTEMPTS = 60; + const POLL_INTERVAL_MS = 4000; + + for (let i = 0; i < MAX_ATTEMPTS; i++) { + await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); try { const info = await rpc.getTransactionByDeployHash(hash) as { executionInfo?: { blockHeight?: number; executionResult?: { errorMessage?: string } }; @@ -272,16 +262,17 @@ async function waitForDeploy( const exec = info.executionInfo; if (exec?.blockHeight && exec.blockHeight > 0 && exec.executionResult) { if (exec.executionResult.errorMessage) { - throw new Error(`Casper deploy failed: ${exec.executionResult.errorMessage}`); + throw new Error(`Casper deploy failed on-chain: ${exec.executionResult.errorMessage}`); } return; } } catch (e) { const msg = (e as Error).message ?? ""; if (msg.startsWith("Casper deploy failed")) throw e; + // Transient RPC errors — continue polling } } - throw new Error(`Deploy ${hash} not confirmed after 240s`); + throw new Error(`Deploy ${hash} not confirmed after ${MAX_ATTEMPTS * POLL_INTERVAL_MS / 1000}s (${MAX_ATTEMPTS} attempts)`); } // ── Venice AI inference ─────────────────────────────────────────────────────── @@ -289,7 +280,7 @@ async function waitForDeploy( async function callAgent( capability: string, taskDescription: string, - context = "" + context = "", ): Promise { const SYSTEM_MAP: Record = { research: "You are a market research specialist. Produce concise, factual research: key players, market size, growth trends.", @@ -312,7 +303,7 @@ async function hireAndPay( taskId: bigint, result: TaskResult, ): Promise { - // 1. Record hire on Casper chain + // 1. Record hire on Casper chain (with retry) const hireHash = await callContractEntry("hire_agent", { task_id: taskId, agent: agent.accountHash, @@ -321,8 +312,6 @@ async function hireAndPay( result.casperExplorerLinks.push(`https://testnet.cspr.live/deploy/${hireHash}`); // 2. Real x402 payment via CSPR.cloud facilitator - // → POST /verify (off-chain signature check) - // → POST /settle (CEP-18 token transfer on Casper Testnet) const x402Hash = await settleX402Payment( agent.accountHash, agent.pricePerTask, @@ -330,6 +319,48 @@ async function hireAndPay( ); result.txHashes.push(x402Hash); result.casperExplorerLinks.push(`https://testnet.cspr.live/deploy/${x402Hash}`); + + // 3. Update local agent store + incrementAgentTasks(agent.accountHash); +} + +// ── Complete task on-chain: store result hash, trigger reputation ──────────── + +async function completeTaskOnChain( + taskId: bigint, + resultHash: string, + agentsHired: AgentRecord[], +): Promise { + try { + await callContractEntry("complete_task", { + task_id: taskId, + result_hash: resultHash, + }); + console.log(`[Coordinator] Task ${taskId} completed on-chain. Result hash: ${resultHash}`); + + // Reputation is updated by the TaskCoordinator contract calling AgentReputation. + // Also update our local store to reflect the on-chain reputation change. + for (const agent of agentsHired) { + try { + // Query updated score from chain + const score = await queryContractVar(`reputation_score_${agent.accountHash}`); + if (score !== undefined) { + updateAgentReputation(agent.accountHash, Number(score)); + } else { + // If we can't query the score, apply the formula locally + const currentScore = agent.reputationScore; + const newTasksCompleted = agent.tasksCompleted + 1; + const weightedTotal = newTasksCompleted + 0 * 2; + const rawScore = Math.min(9900, Math.max(100, Math.floor((newTasksCompleted / Math.max(1, weightedTotal)) * 10000))); + updateAgentReputation(agent.accountHash, rawScore); + } + } catch { + // Reputation update is non-critical + } + } + } catch (err) { + console.warn(`[Coordinator] complete_task failed (non-fatal): ${err}`); + } } // ── Main orchestration loop ─────────────────────────────────────────────────── @@ -338,17 +369,25 @@ let _nextTaskId = 0n; export async function runCoordinator( taskDescription: string, - capabilities: string[] = ["research", "risk", "audit", "report"] + capabilities: string[] = ["research", "risk", "audit", "report"], ): Promise { const result: TaskResult = { taskId: "", report: "", agentsHired: [], - txHashes: [], + txHashes: [], casperExplorerLinks: [], + onChain: false, }; + // ── Sync agents from chain ────────────────────────────────────────────── + try { + await syncWithChain(); + } catch { + // Non-fatal — local cache is available + } + // ── Query real task ID from contract state (fallback to local counter) ───── let TASK_ID: bigint; try { @@ -359,15 +398,24 @@ export async function runCoordinator( console.warn(`[Coordinator] Could not query task_count, using local ID ${TASK_ID}`); } - // ── Create task on Casper (non-fatal) ────────────────────────────────────── + // ── Create task on Casper (with retry) ────────────────────────────────── + let onChain = false; try { console.log(`[Coordinator] Creating task on Casper Testnet…`); - const createHash = await callContractEntry("create_task", { - description: taskDescription, - }, config.taskBudgetMotes); + const createHash = await withRetry( + () => callContractEntry("create_task", { + description: taskDescription, + }, config.taskBudgetMotes), + "create_task", + 2, + 5000, + ); result.casperExplorerLinks.push(`https://testnet.cspr.live/deploy/${createHash}`); + onChain = true; + result.onChain = true; + console.log(`[Coordinator] Task ${TASK_ID} created on-chain → ${createHash}`); } catch (err) { - console.warn(`[Coordinator] create_task failed (continuing): ${err}`); + console.warn(`[Coordinator] create_task failed — proceeding without on-chain task: ${err}`); } result.taskId = String(TASK_ID); @@ -377,7 +425,7 @@ export async function runCoordinator( const found = await findAgents(cap); if (found[0]) { agentMap[cap] = found[0]; - console.log(`[Coordinator] Found ${cap} agent: ${found[0].accountHash.slice(0, 14)}… (rep=${found[0].reputationScore})`); + console.log(`[Coordinator] Found ${cap} agent: ${found[0].accountHash.slice(0, 14)}… (rep=${found[0].reputationScore}, source=${found[0].source})`); } else { console.warn(`[Coordinator] No ${cap} agent registered — will run AI without on-chain hire`); } @@ -393,13 +441,11 @@ export async function runCoordinator( for (let i = 0; i < wave1.length; i++) { const cap = wave1[i]; - // Store AI output regardless of agent registration if (cap === "research") result.research = outputs[i]; else if (cap === "coding") result.coding = outputs[i]; else if (cap === "design") result.design = outputs[i]; else result.research = (result.research ?? "") + `\n\n[${cap.toUpperCase()}]\n${outputs[i]}`; - // Hire + pay on-chain only if an agent is registered if (agentMap[cap]) { try { await hireAndPay(agentMap[cap]!, TASK_ID, result); @@ -450,20 +496,18 @@ export async function runCoordinator( } } - // ── Complete task — store result hash on-chain (non-fatal) ───────────────── - try { + // ── Complete task on-chain — store result hash, trigger reputation ──────── + if (onChain) { const resultHash = crypto.createHash("sha256").update(result.report).digest("hex"); - const completeHash = await callContractEntry("complete_task", { - task_id: TASK_ID, - result_hash: resultHash, - }); - result.casperExplorerLinks.push(`https://testnet.cspr.live/deploy/${completeHash}`); - } catch (err) { - console.warn(`[Coordinator] complete_task failed (non-fatal): ${err}`); + const hiredAgentRecords = capabilities + .filter(c => agentMap[c]) + .map(c => agentMap[c]!); + await completeTaskOnChain(TASK_ID, resultHash, hiredAgentRecords); } console.log("\n[Coordinator] ✅ Task complete!"); console.log(`[Coordinator] x402 deploy hashes: ${result.txHashes.join(", ")}`); + console.log(`[Coordinator] On-chain: ${result.onChain}`); console.log("[Coordinator] Explorer links:"); result.casperExplorerLinks.forEach(l => console.log(" ", l)); diff --git a/backend/src/server.ts b/backend/src/server.ts index 1dba607..8cb8080 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -6,7 +6,6 @@ import { config } from "./config"; import { runCoordinator, findAllAgents, queryContractVar, buildDeployJSON, submitSignedDeploy } from "./coordinator"; import { runAgent, type Capability } from "./agentRunner"; import { buildProject } from "./builder"; -import { simulatedHash } from "./casperHandler"; const app = express(); app.use(cors({ @@ -216,14 +215,12 @@ app.post("/agent/register/submit", limiter, async (req: Request, res: Response, if (!signedDeploy) { res.status(400).json({ error: "signedDeploy is required" }); return; } let deployHash: string; - let simulated = false; try { deployHash = await submitSignedDeploy(signedDeploy); - simulated = deployHash.startsWith("sim"); } catch (err) { - console.warn(`[Server] Deploy submission failed, storing locally: ${err}`); - deployHash = simulatedHash(); - simulated = true; + console.error(`[Server] Deploy submission failed: ${err}`); + res.status(502).json({ error: `On-chain submission failed: ${(err as Error).message}. The transaction was not submitted to the network.` }); + return; } // Store agent in local registry so it shows up in GET /agents diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index de620f7..05251b0 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -61,7 +61,7 @@ export default function SettingsPage() {

{desc}

{addr} - diff --git a/frontend/components/agents/agent-card.tsx b/frontend/components/agents/agent-card.tsx index 968bfd0..fae72dd 100644 --- a/frontend/components/agents/agent-card.tsx +++ b/frontend/components/agents/agent-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { Star, Zap } from "lucide-react"; +import { Star, Zap, Shield } from "lucide-react"; import { cn } from "@/lib/utils"; export interface Agent { @@ -10,8 +10,11 @@ export interface Agent { price: number; rating: number; tasks: number; + reputationScore?: number; status: "online" | "busy" | "offline"; skills: string[]; + accountHash?: string; + source?: "on-chain" | "local"; } const GRADIENTS: Record = { @@ -20,15 +23,18 @@ const GRADIENTS: Record = { Coding: "from-violet-500 to-purple-400", Design: "from-pink-500 to-rose-400", Report: "from-emerald-500 to-teal-400", + Audit: "from-indigo-500 to-blue-400", }; const EMOJIS: Record = { - Research: "🔍", Risk: "⚠️", Coding: "💻", Design: "🎨", Report: "📄", + Research: "🔍", Risk: "⚠️", Coding: "💻", Design: "🎨", Report: "📄", Audit: "✅", }; -export function AgentCard({ name, type, description, price, rating, tasks, status, skills }: Agent) { +export function AgentCard({ name, type, description, price, rating, tasks, reputationScore, status, skills, source }: Agent) { const gradient = GRADIENTS[type] ?? "from-cyan-500 to-violet-400"; const emoji = EMOJIS[type] ?? "🤖"; const isOnline = status === "online"; + const repScore = reputationScore ?? 5000; + const isOnChain = source === "on-chain"; return (
@@ -42,12 +48,15 @@ export function AgentCard({ name, type, description, price, rating, tasks, statu
{status} + {isOnChain && ( + on-chain + )}
- {rating} + {rating.toFixed(1)}
@@ -64,9 +73,15 @@ export function AgentCard({ name, type, description, price, rating, tasks, statu {price} CSPR/task -
- - {tasks} tasks +
+
+ + {repScore > 5000 ? "+" : ""}{Math.round(((repScore - 5000) / 5000) * 100)}% +
+
+ + {tasks} +
diff --git a/frontend/hooks/use-chain-agents.ts b/frontend/hooks/use-chain-agents.ts index d754a7b..73e48af 100644 --- a/frontend/hooks/use-chain-agents.ts +++ b/frontend/hooks/use-chain-agents.ts @@ -6,7 +6,7 @@ import type { Agent } from "@/components/agents/agent-card"; const TYPE_MAP: Record = { research: "Research", risk: "Risk", coding: "Coding", - design: "Design", report: "Report", audit: "Risk", + design: "Design", report: "Report", audit: "Audit", }; const SKILL_MAP: Record = { @@ -24,28 +24,41 @@ export function useChainAgents() { const [filter, setFilter] = useState("All"); useEffect(() => { - (async () => { + let cancelled = false; + + async function fetchAgents() { try { const res = await fetch(`${BACKEND_URL}/agents`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); + if (cancelled) return; + const result: Agent[] = (data.agents ?? []).map((a: any) => ({ name: `${a.capability.charAt(0).toUpperCase() + a.capability.slice(1)} Agent`, - type: TYPE_MAP[a.capability] ?? "Research", - description: `Autonomous ${a.capability} agent on GuildNet. ${(a.tasks ?? 0) > 0 ? `${a.tasks} tasks completed.` : "Ready for hire."}`, + type: TYPE_MAP[a.capability] ?? a.capability, + description: `Autonomous ${a.capability} agent on GuildNet. ${a.tasksCompleted > 0 ? `${a.tasksCompleted} tasks completed.` : "Ready for hire."} Reputation: ${a.reputationScore}/10000.`, price: Number(a.pricePerTask) / 1e9, - rating: Math.min(5.0, 4.5 + (a.tasks ?? 0) * 0.01), - tasks: a.tasks ?? 0, + rating: Math.min(5.0, 3.5 + (a.reputationScore / 10000) * 1.5), + tasks: a.tasksCompleted ?? 0, + reputationScore: a.reputationScore ?? 5000, status: a.active ? "online" as const : "offline" as const, - skills: SKILL_MAP[a.capability] ?? [], + skills: SKILL_MAP[a.capability] ?? [a.capability], + accountHash: a.accountHash ?? "", + source: a.source ?? "local", })); setAgents(result); } catch (e) { console.error("Failed to load agents from backend", e); } finally { - setLoading(false); + if (!cancelled) setLoading(false); } - })(); + } + + fetchAgents(); + + // Poll for agent updates every 30 seconds + const interval = setInterval(fetchAgents, 30_000); + return () => { cancelled = true; clearInterval(interval); }; }, []); const filtered = filter === "All" ? agents : agents.filter(a => a.type === filter);