From 0cfe9721a7df320f2ab5690f760e33bc61e302fa Mon Sep 17 00:00:00 2001 From: Ben Wesser Date: Tue, 18 Aug 2026 05:18:42 +0200 Subject: [PATCH] feat: add Taskmarket action provider --- .../.changeset/taskmarket-action-provider.md | 5 + typescript/agentkit/README.md | 17 + .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/taskmarket/README.md | 20 + .../action-providers/taskmarket/constants.ts | 12 + .../src/action-providers/taskmarket/index.ts | 3 + .../action-providers/taskmarket/schemas.ts | 105 +++++ .../taskmarketActionProvider.test.ts | 134 ++++++ .../taskmarket/taskmarketActionProvider.ts | 400 ++++++++++++++++++ 9 files changed, 697 insertions(+) create mode 100644 typescript/.changeset/taskmarket-action-provider.md create mode 100644 typescript/agentkit/src/action-providers/taskmarket/README.md create mode 100644 typescript/agentkit/src/action-providers/taskmarket/constants.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/index.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/schemas.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts diff --git a/typescript/.changeset/taskmarket-action-provider.md b/typescript/.changeset/taskmarket-action-provider.md new file mode 100644 index 000000000..cc0dd0a19 --- /dev/null +++ b/typescript/.changeset/taskmarket-action-provider.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Added a Taskmarket action provider for public task discovery and explicitly confirmed USDC-escrowed task creation with configurable spending limits. diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index 37b14207f..9e631dc78 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -769,6 +769,23 @@ const agent = createAgent({
+Taskmarket + + + + + + + + + + + + + +
list_tasksDiscovers public Taskmarket work with reward, tag, mode, deadline, and pagination filters.
get_taskFetches a Taskmarket task specification, escrow state, submission window, and available next actions.
create_taskPreviews a task and, only after explicit confirmation, creates it with USDC escrow on Base mainnet under a configurable reward limit.
+
+
x402 diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..48dc009b7 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -37,6 +37,7 @@ export * from "./onramp"; export * from "./vaultsfyi"; export * from "./x402"; export * from "./yelay"; +export * from "./taskmarket"; export * from "./zerion"; export * from "./zerodev"; export * from "./zeroX"; diff --git a/typescript/agentkit/src/action-providers/taskmarket/README.md b/typescript/agentkit/src/action-providers/taskmarket/README.md new file mode 100644 index 000000000..f91aa4e95 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/README.md @@ -0,0 +1,20 @@ +# Taskmarket Action Provider + +The `taskmarketActionProvider` adds Taskmarket discovery and explicit task-creation actions to AgentKit. + +```ts +import { AgentKit, taskmarketActionProvider } from "@coinbase/agentkit"; + +const agentKit = new AgentKit({ + walletProvider, + actionProviders: [taskmarketActionProvider({ maxRewardUsdc: 25 })], +}); +``` + +The provider exposes three actions: + +- `list_tasks` reads the public Taskmarket feed and supports reward, tag, mode, deadline, and pagination filters. +- `get_task` reads one task's specification, escrow state, submission window, and available next actions. +- `create_task` previews a task without making a request when `confirm` is `false`. It only submits the x402-paid creation request when `confirm` is `true`, the wallet is on Base mainnet, and the reward is within the configured `maxRewardUsdc` limit. + +Taskmarket task creation pays the requested reward into escrow at creation time. Applications should present the exact description and reward to their user and set `confirm: true` only after explicit approval. Discovery and task detail reads never move funds. diff --git a/typescript/agentkit/src/action-providers/taskmarket/constants.ts b/typescript/agentkit/src/action-providers/taskmarket/constants.ts new file mode 100644 index 000000000..cc6331150 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/constants.ts @@ -0,0 +1,12 @@ +/** + * Default Taskmarket API endpoint. + */ +export const TASKMARKET_BASE_URL = "https://api.taskmarket.dev"; + +/** + * Maximum default escrow amount allowed by the action provider. + * + * This is a guardrail for agents. Applications can provide a lower limit in + * the provider configuration, but cannot raise the default without opting in. + */ +export const DEFAULT_MAX_REWARD_USDC = 100; diff --git a/typescript/agentkit/src/action-providers/taskmarket/index.ts b/typescript/agentkit/src/action-providers/taskmarket/index.ts new file mode 100644 index 000000000..53b660d04 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/index.ts @@ -0,0 +1,3 @@ +export * from "./constants"; +export * from "./schemas"; +export * from "./taskmarketActionProvider"; diff --git a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts new file mode 100644 index 000000000..0753797b1 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts @@ -0,0 +1,105 @@ +import { z } from "zod"; + +/** + * Supported task lifecycle phases exposed by the Taskmarket API. + */ +export const TaskmarketPhaseSchema = z.enum([ + "active", + "in_review", + "awaiting_settlement", + "resolved", +]); + +/** + * Input schema for discovering open or historical Taskmarket work. + */ +export const ListTaskmarketTasksSchema = z + .object({ + status: z + .string() + .optional() + .describe("Task status filter, for example 'open' or 'completed'. Defaults to 'open'."), + phase: TaskmarketPhaseSchema.optional().describe("Optional lifecycle phase filter."), + mode: z + .enum(["bounty", "claim", "pitch", "benchmark", "auction", "ALL"]) + .optional() + .describe("Optional task competition mode filter."), + tags: z.array(z.string()).max(20).optional().describe("Optional task tags to match."), + minRewardUsdc: z + .number() + .nonnegative() + .finite() + .optional() + .describe("Optional minimum reward in whole USDC."), + maxRewardUsdc: z + .number() + .nonnegative() + .finite() + .optional() + .describe("Optional maximum reward in whole USDC."), + deadlineHours: z + .number() + .positive() + .finite() + .optional() + .describe("Only return tasks whose deadline is within this many hours."), + sort: z + .enum(["newest", "reward_desc", "reward_asc", "deadline_asc"]) + .optional() + .describe("Result ordering. Defaults to newest."), + limit: z + .number() + .int() + .positive() + .max(100) + .optional() + .describe("Maximum number of tasks to return, up to 100."), + cursor: z.string().optional().describe("Pagination cursor returned by a previous call."), + }) + .strict(); + +/** + * Input schema for retrieving one Taskmarket task by id. + */ +export const GetTaskmarketTaskSchema = z + .object({ + taskId: z + .string() + .regex(/^0x[0-9a-fA-F]{64}$/, "Task id must be a 32-byte hex value.") + .describe("Taskmarket task id."), + }) + .strict(); + +/** + * Input schema for creating a Taskmarket task. + */ +export const CreateTaskmarketTaskSchema = z + .object({ + description: z.string().min(1).max(50_000).describe("The work specification for the task."), + rewardUsdc: z + .number() + .positive() + .finite() + .describe("Escrow reward in whole USDC; the same amount is paid through x402."), + durationHours: z + .number() + .positive() + .finite() + .max(24 * 365) + .describe("How many hours the task remains open."), + mode: z + .enum(["bounty", "claim", "pitch", "benchmark", "auction"]) + .optional() + .describe("Task competition mode; defaults to bounty."), + tags: z.array(z.string()).max(20).optional().describe("Optional discovery tags."), + taskVisibility: z + .enum(["public", "unlisted", "private"]) + .optional() + .describe("Task visibility; defaults to public."), + confirm: z + .boolean() + .describe( + "Must be true only after the user explicitly approves creating the task and escrow payment.", + ), + }) + .strict(); diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts new file mode 100644 index 000000000..4c353ba23 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts @@ -0,0 +1,134 @@ +import { + taskmarketActionProvider, + fromTaskmarketBaseUnits, + toTaskmarketBaseUnits, +} from "./taskmarketActionProvider"; + +describe("TaskmarketActionProvider", () => { + const fetchMock = jest.fn(); + const provider = taskmarketActionProvider({ + apiUrl: "https://taskmarket.example", + maxRewardUsdc: 5, + }); + + beforeEach(() => { + jest.resetAllMocks(); + global.fetch = fetchMock; + }); + + it("converts USDC amounts to and from Taskmarket base units", () => { + expect(toTaskmarketBaseUnits(5)).toBe("5000000"); + expect(toTaskmarketBaseUnits(0.125)).toBe("125000"); + expect(fromTaskmarketBaseUnits("5500000")).toBe(5.5); + expect(fromTaskmarketBaseUnits("not-a-number")).toBeNull(); + }); + + it("lists compact task summaries and applies discovery filters", async () => { + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + tasks: [ + { + id: "0x" + "1".repeat(64), + description: "A public API task", + reward: "6000000", + netReward: "5550000", + status: "open", + phase: "active", + mode: "bounty", + tags: ["api"], + submissionCount: 2, + awardCount: 0, + }, + ], + hasMore: true, + nextCursor: "2026-08-18T00:00:00.000Z", + }), + }); + + const result = await provider.listTasks({ + minRewardUsdc: 5, + tags: ["api"], + sort: "reward_desc", + limit: 10, + }); + const parsed = JSON.parse(result); + const requestUrl = new URL(fetchMock.mock.calls[0][0]); + + expect(parsed.success).toBe(true); + expect(parsed.tasks[0]).toMatchObject({ + rewardUsdc: 6, + netRewardUsdc: 5.55, + descriptionTruncated: false, + }); + expect(requestUrl.pathname).toBe("/api/tasks"); + expect(requestUrl.searchParams.get("status")).toBe("open"); + expect(requestUrl.searchParams.get("minReward")).toBe("5000000"); + expect(requestUrl.searchParams.get("tags")).toBe("api"); + expect(requestUrl.searchParams.get("sort")).toBe("reward_desc"); + }); + + it("returns the full task detail", async () => { + const task = { + id: "0x" + "2".repeat(64), + description: "Inspect this task", + reward: "1000000", + pendingActions: [{ role: "worker", action: "submit" }], + }; + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(task), + }); + + const result = await provider.getTask({ taskId: task.id }); + + expect(JSON.parse(result)).toEqual({ success: true, task }); + expect(fetchMock).toHaveBeenCalledWith(`https://taskmarket.example/api/tasks/${task.id}`); + }); + + it("does not contact the paid endpoint before explicit confirmation", async () => { + const wallet = {} as never; + const result = await provider.createTask(wallet, { + description: "Delegate a bounded public API check", + rewardUsdc: 2, + durationHours: 4, + tags: ["api"], + confirm: false, + }); + const parsed = JSON.parse(result); + + expect(parsed.status).toBe("confirmation_required"); + expect(parsed.task.reward).toBe("2000000"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("rejects a confirmed escrow above the configured limit", async () => { + const result = await provider.createTask({} as never, { + description: "This should not be sent", + rewardUsdc: 5.01, + durationHours: 1, + confirm: true, + }); + + const parsed = JSON.parse(result); + expect(parsed.success).toBe(false); + expect(parsed.error).toContain("exceeds the configured maximum"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("reports HTTP errors without throwing", async () => { + fetchMock.mockResolvedValue({ + ok: false, + status: 503, + json: jest.fn().mockResolvedValue({ message: "temporarily unavailable" }), + }); + + const result = await provider.listTasks({}); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(false); + expect(parsed.error).toContain("HTTP 503"); + }); +}); diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts new file mode 100644 index 000000000..5309b1dba --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts @@ -0,0 +1,400 @@ +import { wrapFetchWithPayment, x402Client } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { z } from "zod"; +import { Network } from "../../network"; +import { EvmWalletProvider, WalletProvider } from "../../wallet-providers"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { + CreateTaskmarketTaskSchema, + GetTaskmarketTaskSchema, + ListTaskmarketTasksSchema, +} from "./schemas"; +import { DEFAULT_MAX_REWARD_USDC, TASKMARKET_BASE_URL } from "./constants"; + +/** + * Configuration for the Taskmarket action provider. + */ +export interface TaskmarketActionProviderConfig { + /** Base URL for the Taskmarket REST API. */ + apiUrl?: string; + /** Maximum amount of USDC the create action may escrow in one call. */ + maxRewardUsdc?: number; +} + +type TaskmarketTask = { + id?: string; + description?: string; + reward?: string; + netReward?: string; + status?: string; + phase?: string; + mode?: string; + tags?: string[]; + expiryTime?: string; + submissionCount?: number; + awardCount?: number; +}; + +type TaskmarketListResponse = { + tasks?: TaskmarketTask[]; + hasMore?: boolean; + nextCursor?: string | null; +}; + +/** + * Convert a whole-USDC amount to the six-decimal base-unit representation + * required by the Taskmarket API. + * + * @param amountUsdc - Amount in whole USDC. + * @returns Amount in USDC base units. + */ +export function toTaskmarketBaseUnits(amountUsdc: number): string { + if (!Number.isFinite(amountUsdc) || amountUsdc <= 0) { + throw new Error("USDC amount must be a positive finite number"); + } + + return String(Math.round(amountUsdc * 1_000_000)); +} + +/** + * Convert a Taskmarket base-unit amount to a display amount in USDC. + * + * @param baseUnits - Amount in six-decimal USDC base units. + * @returns Amount in whole USDC, or null when the value is not numeric. + */ +export function fromTaskmarketBaseUnits(baseUnits: unknown): number | null { + const value = + typeof baseUnits === "string" || typeof baseUnits === "number" ? Number(baseUnits) : NaN; + return Number.isFinite(value) ? value / 1_000_000 : null; +} + +/** + * Action provider for discovering and explicitly authorizing Taskmarket work. + * + * Read actions are available on every wallet network. Creating a task is + * restricted to Base mainnet and requires both the explicit `confirm: true` + * input and the configured per-call escrow limit. + */ +export class TaskmarketActionProvider extends ActionProvider { + private readonly apiUrl: string; + private readonly maxRewardUsdc: number; + + /** + * Creates a Taskmarket action provider. + * + * @param config - Optional API and escrow guardrail configuration. + */ + constructor(config: TaskmarketActionProviderConfig = {}) { + super("taskmarket", []); + + const apiUrl = config.apiUrl ?? TASKMARKET_BASE_URL; + try { + new URL(apiUrl); + } catch { + throw new Error(`Invalid Taskmarket API URL: ${apiUrl}`); + } + + const maxRewardUsdc = config.maxRewardUsdc ?? DEFAULT_MAX_REWARD_USDC; + if (!Number.isFinite(maxRewardUsdc) || maxRewardUsdc <= 0) { + throw new Error("maxRewardUsdc must be a positive finite number"); + } + + this.apiUrl = apiUrl.replace(/\/$/, ""); + this.maxRewardUsdc = maxRewardUsdc; + } + + /** + * Lists Taskmarket tasks using the public, read-only task feed. + * + * @param args - Discovery filters and pagination options. + * @returns A JSON result containing compact task summaries. + */ + @CreateAction({ + name: "list_tasks", + description: `Discover work on Taskmarket, the USDC-escrowed task marketplace. +Use this read-only action to find external work before deciding whether to solve it locally or delegate a request. Results include the task id, description, gross and net rewards, deadline, competition mode, and submission counts. The default status is 'open'.`, + schema: ListTaskmarketTasksSchema, + }) + async listTasks(args: z.infer): Promise { + try { + const url = new URL(`${this.apiUrl}/api/tasks`); + url.searchParams.set("status", args.status ?? "open"); + if (args.phase) url.searchParams.set("phase", args.phase); + if (args.mode) url.searchParams.set("mode", args.mode); + for (const tag of args.tags ?? []) url.searchParams.append("tags", tag); + if (args.minRewardUsdc !== undefined) { + url.searchParams.set("minReward", toTaskmarketBaseUnits(args.minRewardUsdc)); + } + if (args.maxRewardUsdc !== undefined) { + url.searchParams.set("maxReward", toTaskmarketBaseUnits(args.maxRewardUsdc)); + } + if (args.deadlineHours !== undefined) { + url.searchParams.set("deadlineHours", String(args.deadlineHours)); + } + if (args.sort) url.searchParams.set("sort", args.sort); + if (args.limit !== undefined) url.searchParams.set("limit", String(args.limit)); + if (args.cursor) url.searchParams.set("cursor", args.cursor); + + const response = await fetch(url); + const data = (await response.json()) as TaskmarketListResponse; + if (!response.ok) + return this.errorResult(`Taskmarket returned HTTP ${response.status}`, data); + + return JSON.stringify( + { + success: true, + tasks: (data.tasks ?? []).map(task => this.summarizeTask(task)), + hasMore: data.hasMore ?? false, + nextCursor: data.nextCursor ?? null, + }, + null, + 2, + ); + } catch (error) { + return this.errorResult("Failed to list Taskmarket tasks", error); + } + } + + /** + * Retrieves one Taskmarket task and its currently available next actions. + * + * @param args - The public task id. + * @returns The task detail as JSON. + */ + @CreateAction({ + name: "get_task", + description: + "Fetch one Taskmarket task by id. Use this after list_tasks to inspect the full specification, escrow state, submission window, and available next actions before doing any work or payment-related operation.", + schema: GetTaskmarketTaskSchema, + }) + async getTask(args: z.infer): Promise { + try { + const response = await fetch(`${this.apiUrl}/api/tasks/${encodeURIComponent(args.taskId)}`); + const data = (await response.json()) as TaskmarketTask | null; + if (!response.ok) + return this.errorResult(`Taskmarket returned HTTP ${response.status}`, data); + if (data === null) return this.errorResult("Taskmarket task was not found", null); + + return JSON.stringify({ success: true, task: data }, null, 2); + } catch (error) { + return this.errorResult("Failed to fetch the Taskmarket task", error); + } + } + + /** + * Creates a Taskmarket task after an explicit confirmation guard. + * + * The first call with `confirm: false` is a dry-run and does not contact the + * paid endpoint. A second call with `confirm: true` pays the requested + * reward into Taskmarket escrow through x402. The configured maximum reward + * is enforced before any payment attempt. + * + * @param walletProvider - EVM wallet used for the Base mainnet x402 payment. + * @param args - Task specification and explicit payment confirmation. + * @returns A JSON result containing either the confirmation preview or task id. + */ + @CreateAction({ + name: "create_task", + description: `Create a Taskmarket task with USDC escrow on Base mainnet. +This action has a hard safety gate: call it first with confirm=false to preview the task and payment, then call it again with confirm=true only after the user explicitly approves the exact reward and specification. The provider's configured maximum escrow is enforced before any payment attempt. Never treat confirm=true as permission to spend more than the supplied rewardUsdc.`, + schema: CreateTaskmarketTaskSchema, + }) + async createTask( + walletProvider: WalletProvider, + args: z.infer, + ): Promise { + if (!args.confirm) { + return JSON.stringify( + { + status: "confirmation_required", + message: "No request was sent and no funds were moved.", + task: this.buildCreatePayload(args), + rewardUsdc: args.rewardUsdc, + maxRewardUsdc: this.maxRewardUsdc, + nextStep: "Repeat with confirm=true only after explicit user approval.", + }, + null, + 2, + ); + } + + if (args.rewardUsdc > this.maxRewardUsdc) { + return this.errorResult( + `Requested reward ${args.rewardUsdc} USDC exceeds the configured maximum of ${this.maxRewardUsdc} USDC`, + null, + ); + } + + if (walletProvider.getNetwork().networkId !== "base-mainnet") { + return this.errorResult("Taskmarket escrow creation requires an EVM wallet on Base mainnet", { + networkId: walletProvider.getNetwork().networkId ?? null, + requiredNetwork: "base-mainnet", + }); + } + + if (walletProvider.getNetwork().protocolFamily !== "evm") { + return this.errorResult("Taskmarket escrow creation requires an EVM wallet", null); + } + + try { + const evmWalletProvider = walletProvider as EvmWalletProvider; + const response = await this.createWithX402(evmWalletProvider, this.buildCreatePayload(args)); + const data = await this.parseResponse(response); + + if (!response.ok) { + return this.errorResult(`Taskmarket create returned HTTP ${response.status}`, data); + } + + return JSON.stringify( + { + success: true, + taskmarket: data, + escrow: { + network: "base-mainnet", + rewardUsdc: args.rewardUsdc, + settled: Boolean(response.headers.get("payment-response")), + }, + }, + null, + 2, + ); + } catch (error) { + return this.errorResult("Taskmarket task creation failed", error); + } + } + + /** + * Taskmarket's public discovery and detail endpoints work with every + * AgentKit wallet network; the paid create action performs its own Base + * mainnet check. + * + * @param _ - Current wallet network. + * @returns Always true because read-only discovery is network-agnostic. + */ + supportsNetwork(_: Network): boolean { + return true; + } + + /** + * Build the API payload for a task creation request. + * + * @param args - User-provided task details. + * @returns Taskmarket create request body. + */ + private buildCreatePayload(args: z.infer) { + return { + description: args.description, + reward: toTaskmarketBaseUnits(args.rewardUsdc), + duration: args.durationHours, + mode: args.mode ?? "bounty", + tags: args.tags ?? [], + taskVisibility: args.taskVisibility ?? "public", + }; + } + + /** + * Create a Taskmarket x402 client bound to an EVM wallet. + * + * @param walletProvider - Wallet used to sign the payment authorization. + * @returns Configured x402 client. + */ + private createX402Client(walletProvider: EvmWalletProvider): x402Client { + const client = new x402Client(); + const account = walletProvider.toSigner(); + const signer = { + ...account, + readContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }) => + walletProvider.readContract({ + address: args.address, + abi: args.abi as never, + functionName: args.functionName as never, + args: args.args as never, + }), + }; + registerExactEvmScheme(client, { signer }); + return client; + } + + /** + * Send a task creation request through x402. + * + * @param walletProvider - Wallet used for payment authorization. + * @param payload - Taskmarket create request body. + * @returns The HTTP response from Taskmarket. + */ + private async createWithX402( + walletProvider: EvmWalletProvider, + payload: ReturnType, + ): Promise { + const fetchWithPayment = wrapFetchWithPayment(fetch, this.createX402Client(walletProvider)); + return fetchWithPayment(`${this.apiUrl}/api/tasks`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + } + + /** + * Parse JSON where possible and fall back to text for diagnostics. + * + * @param response - HTTP response to parse. + * @returns Parsed response body. + */ + private async parseResponse(response: Response): Promise { + const text = await response.text(); + try { + return JSON.parse(text); + } catch { + return text; + } + } + + /** + * Return a compact task summary for an LLM context. + * + * @param task - Raw Taskmarket task. + * @returns Compact, display-oriented task data. + */ + private summarizeTask(task: TaskmarketTask) { + return { + id: task.id ?? null, + description: task.description?.slice(0, 2_000) ?? null, + descriptionTruncated: Boolean(task.description && task.description.length > 2_000), + rewardUsdc: fromTaskmarketBaseUnits(task.reward), + netRewardUsdc: fromTaskmarketBaseUnits(task.netReward), + status: task.status ?? null, + phase: task.phase ?? null, + mode: task.mode ?? null, + tags: task.tags ?? [], + expiryTime: task.expiryTime ?? null, + submissionCount: task.submissionCount ?? null, + awardCount: task.awardCount ?? null, + }; + } + + /** + * Format a consistent action error without throwing into the agent loop. + * + * @param message - Short error message. + * @param details - Optional diagnostic value. + * @returns JSON error result. + */ + private errorResult(message: string, details: unknown): string { + return JSON.stringify({ success: false, error: message, details }, null, 2); + } +} + +/** + * Factory for the Taskmarket action provider. + * + * @param config - Optional API and escrow guardrail configuration. + * @returns A configured Taskmarket action provider. + */ +export const taskmarketActionProvider = (config: TaskmarketActionProviderConfig = {}) => + new TaskmarketActionProvider(config);