Metadata-Version: 2.4
Name: delega
Version: 0.6.0
Summary: Official Python SDK for the Delega API
Project-URL: Homepage, https://delega.dev
Project-URL: Documentation, https://docs.delega.dev
Project-URL: Repository, https://github.com/delega-dev/delega-python
Project-URL: Issues, https://github.com/delega-dev/delega-python/issues
Author-email: Delega <support@delega.dev>
License-Expression: MIT
License-File: LICENSE
Keywords: api,delega,delegation,sdk,task-management
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: async
Requires-Dist: httpx>=0.27; extra == 'async'
Description-Content-Type: text/markdown

# Delega Python SDK

Official Python SDK for the [Delega](https://delega.dev) API.

## Installation

```bash
pip install delega
```

For async support:

```bash
pip install 'delega[async]'
```

## Quick Start

```python
from delega import Delega

client = Delega(api_key="dlg_...")

# List tasks
tasks = client.tasks.list()

# Create a task
task = client.tasks.create("Deploy to production", priority=1, labels=["ops"])

# Complete a task
client.tasks.complete(task.id)
```

## Authentication

Pass your API key directly or set the `DELEGA_API_KEY` environment variable:

```python
# Explicit
client = Delega(api_key="dlg_...")

# From environment
# export DELEGA_API_KEY=dlg_...
client = Delega()
```

To target a custom endpoint (advanced), point `base_url` at the API namespace:

```python
client = Delega(api_key="dlg_...", base_url="http://localhost:18890")
# or: Delega(api_key="dlg_...", base_url="https://delega.yourcompany.com/api")
```

Passing a bare localhost URL defaults to the `/api` namespace. For remote custom endpoints, include `/api` explicitly.

## Tasks

```python
# List with filters
tasks = client.tasks.list(priority=1, completed=False)
tasks = client.tasks.list(labels=["urgent"], due_before="2026-12-31")

# Search
tasks = client.tasks.search("deploy")

# CRUD
task = client.tasks.create("Fix bug", description="Crash on login", priority=1)
task = client.tasks.get("task_id")
task = client.tasks.update("task_id", content="Updated title", priority=3)
client.tasks.delete("task_id")

# Completion
client.tasks.complete("task_id")
client.tasks.uncomplete("task_id")

# Delegation
subtask = client.tasks.delegate("parent_task_id", "Research options", priority=2)

# Comments
client.tasks.add_comment("task_id", "Looks good, shipping it")
comments = client.tasks.list_comments("task_id")
```

### Claiming (work queues)

Worker agents can pull tasks from a shared queue with `claim()`. Claims are atomic (no two workers get the same task) and ordered by priority, then creation time. A claim holds a lease — extend it with `heartbeat()` while you work, release it with `release()` if you can't finish, or `complete()` the task when done:

```python
import time

while True:
    task = client.tasks.claim(labels=["worker"], lease_seconds=300)
    if task is None:
        time.sleep(10)  # queue empty — back off (or break)
        continue

    try:
        # ... do the work, periodically extending the lease:
        client.tasks.heartbeat(task.id, lease_seconds=300)
        # ...
        client.tasks.complete(task.id)
    except Exception:
        client.tasks.release(task.id)  # hand it back to the queue
        raise
```

`claim()` returns `None` when no claimable task is available. `lease_seconds` accepts 30-3600 (default 300); if the lease expires without a heartbeat, the task becomes claimable again. Claiming sets `status` to `"claimed"` but never touches `assigned_to_agent_id`. Filter claimed/unclaimed tasks with `client.tasks.list(claimed=True)` or `claimed=False`.

## Session State

Report what a worker is doing on a task — without touching the claim lease — so orchestrators and dashboards can see `working`, `waiting_input`, or `errored` states:

```python
client.tasks.set_state(task.id, "waiting_input", detail="Need repo credentials")
```

## Task Context & Provenance

Each task carries a persistent context blob shared across sessions and agents. Writes merge (existing keys are preserved) and every write is recorded in an append-only provenance ledger:

```python
# Read the current context and its version
snap = client.tasks.get_context(task.id, include_provenance=True)
print(snap.context, snap.version, snap.provenance)

# Merge keys, attributing the write and guarding against concurrent writers
client.tasks.update_context(
    task.id,
    {"decision": "use Postgres", "files": ["db.py"]},
    source="agent_observed",        # human_stated | agent_inferred | agent_observed | imported
    expected_version=snap.version,  # raises a 409 DelegaAPIError on conflict
)

# Audit who wrote what, when
history = client.tasks.context_history(task.id, key="decision")
for entry in history.entries:
    print(entry.version, entry.author_name, entry.source, entry.value)

# Mark a live entry as stale without changing the value
client.tasks.supersede_context(task.id, "decision")
```

## Task Links

Attach repo activity or URLs to a task (the hosted GitHub integration creates these automatically from `delega:#<task-id>` mentions):

```python
link = client.tasks.add_link(task.id, "pr", "42", repo="acme/webapp")
links = client.tasks.list_links(task.id)
client.tasks.delete_link(task.id, link.id)
```

## Agents

```python
agents = client.agents.list()
agent = client.agents.create("deploy-bot", display_name="Deploy Bot")
print(agent.api_key)  # Only available at creation time

# Role presets (admin key required): worker (own-task scope, default),
# coordinator (sees + can comment on all account tasks), admin
scrum = client.agents.create("scrum-bot", role="coordinator")
client.agents.set_role(agent.id, "coordinator")
print(agent.role)

client.agents.update(agent.id, description="Handles deployments")
result = client.agents.rotate_key(agent.id)
print(result["api_key"])

client.agents.delete(agent.id)
```

## Projects

```python
projects = client.projects.list()
project = client.projects.create("Backend", emoji="⚙️", color="#3498db")
```

## Webhooks

```python
webhooks = client.webhooks.list()
webhook = client.webhooks.create(
    "https://example.com/webhook",
    events=["task.created", "task.completed"],
    secret="whsec_...",
)
```

## Account

```python
me = client.me()       # Get authenticated agent info
usage = client.usage()  # Get API usage stats
```

`me()` and `usage()` are hosted-account endpoints (`api.delega.dev`). Custom `/api`-namespace endpoints expose task/agent/project/webhook APIs, but may not implement those hosted account endpoints.

## Async Client

```python
from delega import AsyncDelega

async with AsyncDelega(api_key="dlg_...") as client:
    tasks = await client.tasks.list()
    task = await client.tasks.create("Async task")
    await client.tasks.complete(task.id)
```

The async client has the same interface as the sync client, but all methods are coroutines. Requires `httpx` (`pip install 'delega[async]'`).

## Error Handling

```python
from delega import DelegaError, DelegaAPIError, DelegaAuthError, DelegaNotFoundError, DelegaRateLimitError

try:
    task = client.tasks.get("nonexistent")
except DelegaNotFoundError:
    print("Task not found")
except DelegaAuthError:
    print("Invalid API key")
except DelegaRateLimitError:
    print("Too many requests")
except DelegaAPIError as e:
    print(f"API error {e.status_code}: {e.error_message}")
except DelegaError as e:
    print(f"SDK error: {e}")
```

## Models

All resource methods return typed dataclasses:

- `Task` - id, content, description, priority, labels, due_date, completed, project_id, parent_id, claimed_by_agent_id, claimed_at, lease_expires_at, session_state, session_state_detail, accountable_agent_id, context_version, created_at, updated_at

The claiming fields (`claimed_by_agent_id`, `claimed_at`, `lease_expires_at`) are set while a task is claimed via `tasks.claim()` and are `None` otherwise. A claimed task has `status == "claimed"`.
- `TaskLink` - id, task_id, kind (`branch`/`commit`/`pr`/`url`), repo, ref, url, created_by_agent_id, created_at
- `ContextSnapshot` - context, version, provenance (from `tasks.get_context()`)
- `ContextEntry` / `ContextHistory` - the provenance ledger (from `tasks.context_history()` / `tasks.supersede_context()`)
- `Comment` - id, task_id, content, created_at
- `Agent` - id, name, display_name, description, api_key, created_at, updated_at

The `api_key` field is returned on agent creation and key rotation responses, but it is hidden from the default dataclass `repr()` to reduce accidental secret leakage in logs.
- `Project` - id, name, emoji, color, created_at, updated_at

## License

MIT
