Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a safe TaskMarket action provider for browsing public tasks and preparing explicit-approval delegation drafts from AgentKit agents.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
SuperfluidActionProvider,
superfluid_action_provider,
)
from .taskmarket.taskmarket_action_provider import (
TaskMarketActionProvider,
taskmarket_action_provider,
)
from .twitter.twitter_action_provider import TwitterActionProvider, twitter_action_provider
from .wallet.wallet_action_provider import WalletActionProvider, wallet_action_provider
from .weth.weth_action_provider import WethActionProvider, weth_action_provider
Expand All @@ -57,6 +61,7 @@
"PythActionProvider",
"SshActionProvider",
"SuperfluidActionProvider",
"TaskMarketActionProvider",
"TwitterActionProvider",
"WalletActionProvider",
"WethActionProvider",
Expand All @@ -78,6 +83,7 @@
"pyth_action_provider",
"ssh_action_provider",
"superfluid_action_provider",
"taskmarket_action_provider",
"twitter_action_provider",
"wallet_action_provider",
"weth_action_provider",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# TaskMarket Action Provider

This directory contains the **TaskMarketActionProvider**, a safe delegation helper for discovering public TaskMarket work and preparing auditable TaskMarket task drafts from AgentKit-powered agents.

The provider is intentionally **no-spend by default**. It can browse public tasks and prepare a structured draft, but it does not create, fund, or accept TaskMarket tasks. Host applications must present the draft to the user and obtain explicit approval before connecting any wallet, payment, or task-creation flow.

## Directory Structure

```text
taskmarket/
├── taskmarket_action_provider.py # Main provider implementation
├── schemas.py # Pydantic schemas for provider actions
├── __init__.py # Provider exports
└── README.md # This file
```

## Configuration

```python
from coinbase_agentkit import AgentKit
from coinbase_agentkit.action_providers.taskmarket import taskmarket_action_provider

agentkit = AgentKit(
wallet_provider=wallet_provider,
action_providers=[taskmarket_action_provider()],
)
```

For tests, self-hosted deployments, or API gateways, pass a custom API base URL:

```python
provider = taskmarket_action_provider(api_url="https://api.taskmarket.dev")
```

## Actions

### `browse_taskmarket_tasks`

Browse public TaskMarket tasks without spending funds.

Example input:

```json
{
"limit": 10,
"tag": "ai",
"max_submissions": 20
}
```

Example response shape:

```json
{
"success": true,
"tasks": [
{
"id": "0x...",
"url": "https://taskmarket.dev/task/0x...",
"title": "Create a reproducible benchmark...",
"mode": "bounty",
"status": "open",
"netReward": "925000",
"submissionCount": 12,
"awardCount": 0,
"tags": ["agents", "benchmark"],
"submissionWindowOpen": true
}
],
"returned": 1,
"safety": "read_only_no_spend"
}
```

### `prepare_taskmarket_task_draft`

Prepare a structured delegation draft for user review. This action does **not** create or fund a task.

Example input:

```json
{
"title": "Audit a Python CLI release candidate",
"deliverable": "Review the repository diff, run the documented test suite, and return a concise bug report with reproduction commands.",
"acceptance_criteria": [
"Includes exact commit tested",
"Includes commands run and outputs",
"Separates confirmed bugs from suggestions"
],
"max_budget_usdc": 5,
"deadline_iso": "2026-08-15T18:00:00Z",
"requires_human_approval": true
}
```

Example response shape:

```json
{
"success": true,
"draft": {
"title": "Audit a Python CLI release candidate",
"deliverable": "Review the repository diff...",
"acceptance_criteria": ["Includes exact commit tested"],
"max_budget_usdc": 5.0,
"deadline_iso": "2026-08-15T18:00:00Z",
"requires_human_approval": true
},
"nextStep": "Present this draft to the user for explicit approval before creating or funding a TaskMarket task.",
"safety": {
"spendsFunds": false,
"createsTask": false,
"requiresExplicitApproval": true
}
}
```

## Safety Model

- Read-only browsing does not require a wallet signature.
- Draft preparation does not create a TaskMarket task.
- `requires_human_approval` must remain `true` for generated drafts.
- The provider never spends funds, exposes private keys, bypasses wallet permissions, or auto-accepts worker submissions.
- Any future create/fund/accept action should live behind explicit host-application policy, user confirmation, and spending limits.

## Tests

Run the targeted Python tests from the `python/coinbase-agentkit` directory:

```bash
.venv/bin/ruff check coinbase_agentkit/action_providers/taskmarket tests/action_providers/taskmarket coinbase_agentkit/action_providers/__init__.py
.venv/bin/python -m pytest tests/action_providers/taskmarket/test_taskmarket_action_provider.py -q
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""TaskMarket action provider exports."""

from .schemas import BrowseTaskMarketTasksSchema, TaskMarketTaskDraftSchema
from .taskmarket_action_provider import TaskMarketActionProvider, taskmarket_action_provider

__all__ = [
"BrowseTaskMarketTasksSchema",
"TaskMarketActionProvider",
"TaskMarketTaskDraftSchema",
"taskmarket_action_provider",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Schemas for the TaskMarket action provider."""

from pydantic import BaseModel, Field, field_validator


class BrowseTaskMarketTasksSchema(BaseModel):
"""Input schema for browsing public TaskMarket tasks."""

limit: int = Field(default=10, ge=1, le=50, description="Maximum number of tasks to return")
tag: str | None = Field(default=None, description="Optional tag keyword to filter tasks")
max_submissions: int | None = Field(
default=None,
ge=0,
description="Optional maximum submission count to prefer lower-competition tasks",
)


class TaskMarketTaskDraftSchema(BaseModel):
"""Input schema for preparing a TaskMarket delegation draft."""

title: str = Field(..., min_length=5, max_length=140)
deliverable: str = Field(..., min_length=20, max_length=2000)
acceptance_criteria: list[str] = Field(..., min_length=1, max_length=10)
max_budget_usdc: float = Field(..., gt=0, le=10_000)
deadline_iso: str = Field(..., min_length=10, max_length=40)
requires_human_approval: bool = Field(
default=True,
description="Must remain true unless the hosting app has an explicit spending policy",
)

@field_validator("requires_human_approval")
@classmethod
def require_human_approval(cls, value: bool) -> bool:
"""Require explicit approval for TaskMarket spending by default."""
if value is not True:
raise ValueError("TaskMarket delegation drafts require explicit human approval")
return value
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""TaskMarket action provider for delegated work discovery and drafting.

The provider is intentionally read-only/safe by default: it can browse public TaskMarket work
and prepare an auditable task draft, but it does not create, fund, or accept tasks without an
explicit host-application approval flow.
"""

from __future__ import annotations

import json
from typing import Any

import requests

from ...network import Network
from ...wallet_providers import WalletProvider
from ..action_decorator import create_action
from ..action_provider import ActionProvider
from .schemas import BrowseTaskMarketTasksSchema, TaskMarketTaskDraftSchema

TASKMARKET_API_URL = "https://api.taskmarket.dev"


class TaskMarketActionProvider(ActionProvider[WalletProvider]):
"""Provides safe TaskMarket discovery and delegation-draft actions."""

def __init__(self, api_url: str = TASKMARKET_API_URL):
"""Initialize the TaskMarket provider.

Args:
api_url: Base TaskMarket API URL. Override in tests or self-hosted deployments.

"""
super().__init__("taskmarket", [])
self.api_url = api_url.rstrip("/")

@create_action(
name="browse_taskmarket_tasks",
description="""Browse public TaskMarket tasks as a delegation option for work that is better handled by external workers. This action is read-only and never spends funds. Use filters such as tag and max_submissions to find relevant, lower-competition tasks.""",
schema=BrowseTaskMarketTasksSchema,
)
def browse_taskmarket_tasks(self, args: dict[str, Any]) -> str:
"""Browse public TaskMarket tasks.

Args:
args: limit, optional tag, and optional max_submissions filters.

Returns:
JSON string with simplified open-task data or an error payload.

"""
validated_args = BrowseTaskMarketTasksSchema(**args)
try:
response = requests.get(
f"{self.api_url}/api/tasks",
params={"status": "open", "limit": validated_args.limit},
timeout=20,
)
if not response.ok:
return json.dumps(
{"success": False, "error": f"HTTP error! status: {response.status_code}"}
)

payload = response.json()
raw_tasks = payload.get("tasks") or payload.get("data", {}).get("tasks") or []
tasks = []
for task in raw_tasks:
tags = task.get("tags") or []
submission_count = task.get("submissionCount") or task.get("submission_count") or 0
if validated_args.tag and validated_args.tag.lower() not in [
str(tag).lower() for tag in tags
]:
continue
if (
validated_args.max_submissions is not None
and submission_count > validated_args.max_submissions
):
continue
task_id = task.get("id")
tasks.append(
{
"id": task_id,
"url": f"https://taskmarket.dev/task/{task_id}" if task_id else None,
"title": task.get("title") or (task.get("description") or "")[:120],
"mode": task.get("mode"),
"status": task.get("status"),
"netReward": task.get("netReward")
or task.get("net_reward")
or task.get("netRewardBaseUnits")
or task.get("rewardBaseUnits"),
"submissionCount": submission_count,
"awardCount": task.get("awardCount") or task.get("award_count") or 0,
"tags": tags,
"submissionWindowOpen": task.get("submissionWindowOpen"),
}
)

return json.dumps(
{
"success": True,
"tasks": tasks,
"returned": len(tasks),
"safety": "read_only_no_spend",
}
)
except Exception as exc:
return json.dumps({"success": False, "error": str(exc)})

@create_action(
name="prepare_taskmarket_task_draft",
description="""Prepare a TaskMarket delegation draft with budget, deadline, deliverable, and acceptance criteria. This action does not create or fund a task; the host app must display the draft and get explicit user approval before any wallet/payment action.""",
schema=TaskMarketTaskDraftSchema,
)
def prepare_taskmarket_task_draft(self, args: dict[str, Any]) -> str:
"""Prepare an auditable TaskMarket task draft without spending funds."""
draft = TaskMarketTaskDraftSchema(**args)
return json.dumps(
{
"success": True,
"draft": draft.model_dump(),
"nextStep": "Present this draft to the user for explicit approval before creating or funding a TaskMarket task.",
"safety": {
"spendsFunds": False,
"createsTask": False,
"requiresExplicitApproval": True,
},
}
)

def supports_network(self, network: Network) -> bool:
"""TaskMarket discovery/drafting is network agnostic and does not sign transactions."""
return True


def taskmarket_action_provider(api_url: str = TASKMARKET_API_URL) -> TaskMarketActionProvider:
"""Create a TaskMarket action provider."""
return TaskMarketActionProvider(api_url=api_url)
Loading
Loading