Metadata-Version: 2.4
Name: aga-runtime
Version: 0.4.0
Summary: Durable execution for Python — typed functions that survive crashes, restarts, and month-long waits.
Project-URL: Homepage, https://coding2fun.in/aga
Project-URL: Documentation, https://coding2fun.in/aga/python
Project-URL: Source, https://github.com/vedhlabs/sdk-python
Project-URL: Issues, https://github.com/vedhlabs/sdk-python/issues
Author: Kishore Karunakaran
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: background-jobs,distributed-systems,durable-execution,orchestration,resilience,saga,workflow,workflows
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: System :: Distributed Computing
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cbor2>=5.6
Requires-Dist: msgspec>=0.19
Description-Content-Type: text/markdown

# Aga — durable execution for Python

Write typed functions that survive crashes, restarts, deploys, and long waits.
One `App` owns declarations, connection, Run creation, and worker lifecycle.
Aga records each durable boundary and reuses its committed result on recovery.

> **Breaking 0.4 release.** This version removes the equivalent spellings from
> 0.3 and intentionally provides one public form for each operation.

## Install

The predecessor was published as `ogha`. Its package name and API are incompatible
with Aga 0.4, so upgrade as a hard cutover rather than installing both generations:

```bash
pip install "aga-runtime==0.4.0"
```

The distribution is `aga-runtime`; import it with the short, documented alias:

```python
import aga_runtime as aga
```

`App` reads `AGA_URL`, `AGA_TENANT`, `AGA_NAMESPACE`, and `AGA_API_KEY` by
default; constructor values override the connection location and scope.

## Declare durable functions

Declarations belong to one App:

| Annotation | Meaning |
| :--- | :--- |
| `@app.step(...)` | typed durable work plus retry, timeout, and compensation policy |
| `@app.remote(service, ...)` | typed cross-service or cross-language boundary |
| `@app.workflow(...)` | durable control flow and worker placement |
| `@app.schedule(...)` | engine-owned cron creation of root Runs |

```python
from dataclasses import dataclass

import aga_runtime as aga

app = aga.App("checkout")


@dataclass
class Order:
    id: str
    amount: int


@dataclass
class Receipt:
    id: str
    approved: bool


@app.step(retry=aga.RetryPolicy(max_attempts=5), pivot=True)
def charge(order: Order) -> Receipt:
    return payments.charge(order)


@app.workflow()
async def checkout(order: Order) -> Receipt:
    receipt = await charge(order)
    if order.amount > 10_000:
        await aga.signal(
            aga.Approval("large-charge", evidence=receipt),
            timeout=24 * 60 * 60,
        )
    aga.event("order.charged", {"order": order.id})
    return receipt


if __name__ == "__main__":
    app.serve()
```

Aga restores supported declared Python types at durable JSON boundaries,
including dataclasses, containers, enums, dates, UUIDs, and Pydantic models.

## Create and coordinate work

Calling a registered Step or Remote eagerly creates a typed `Handle`. Workflows
have one creation form: pass the registered Workflow to `app.start(...)`.

| Method or function | Meaning |
| :--- | :--- |
| `step(args...)` | create Step work inside the active Workflow |
| `remote(request)` | create cross-service work inside the active Workflow |
| `app.start(workflow, args...)` | create a root Run outside a Workflow or an owned child Run inside it |
| `await aga.join(*handles, count=None)` | await all, the first, or a threshold |
| `await aga.sleep(seconds)` | park until durable engine time reaches a deadline |
| `await aga.signal(name_or_approval, timeout=...)` | wait for external input or governed approval |
| `aga.event(name, value)` | record an operator-visible milestone |
| `aga.cancel(handle, reason)` | cooperatively cancel durable work |
| `aga.info()` | read immutable current-Run metadata |

Every Step, Remote, root Run, and child Run uses the same `Handle[T]`. Inside a
Workflow, await a Handle. Outside, a root Handle also supports blocking
`.result(timeout=...)`.

```python
run = app.start(checkout.options(run_id=order.id), order)
receipt = run.result()


@app.workflow()
async def checkout_with_audit(order: Order) -> Receipt:
    audit = app.start(audit_order, order)  # owned child; auto-joined
    receipt = await charge(order)
    await audit
    return receipt
```

Only independent child work opts out of ownership:

```python
app.start(audit_order.options(detached=True), order)
```

`join` is the single multi-Handle combinator:

```python
all_values = await aga.join(*handles)
first_value = await aga.join(*handles, count=1)
two_values = await aga.join(*handles, count=2)
```

## Sync, Async, and Async Distributed

These describe two separate choices:

- **Sync versus Async is caller behavior.** Sync waits for a root Handle's
  result; Async keeps the same Handle and lets the caller continue.
- **Async versus Async Distributed is workflow placement.** Default Async keeps
  ordinary Steps with the Workflow worker. Async Distributed independently
  dispatches and fences each Step.

Use Sync when a request or script needs the result now. Use Async for long work
or responsive callers. Use Async Distributed when Steps need separate scaling,
isolation, placement, or leases. There is no `execution="sync"`; waiting never
changes placement.

```python
# Sync caller
receipt = app.start(checkout, order).result()

# Async caller
run = app.start(checkout, order)
receipt = await run


# Distributed placement
@app.workflow(execution="async_distributed")
async def distributed_checkout(order: Order) -> Receipt:
    return await charge(order)
```

A caller timeout does not cancel the durable Run. Cancellation is explicit with
`aga.cancel(run, reason)`.

## Waiting, services, and schedules

`sleep` completes through engine time. `signal` completes through external
input. `Approval` is an immutable request passed to `signal`, not a third wait
verb; it selects governed, fail-closed behavior and replay-bound evidence.

A Remote accepts one required request object so its envelope maps to Python,
Go, Java, and TypeScript without language-specific argument rules. The serving
App registers a Step with the same method name and portable request schema.

Schedules are engine-owned root-Run creation and continue while workers are
offline:

```python
@app.schedule("0 6 * * *", input={"report": "kpi"}, revision=2)
@app.workflow(name="reports.daily")
async def reports_daily(request: dict[str, str]) -> Report:
    return await build_report(request["report"])
```

## Migrating from 0.3

| Python 0.3 | Python 0.4 |
| :--- | :--- |
| `workflow.run(args...)` | `app.start(workflow, args...).result()` |
| root `workflow.start(args...)` | `app.start(workflow, args...)` |
| child `workflow(args...)` / `.spawn(...)` | `app.start(workflow, args...)` |
| `workflow.detach(args...)` | `app.start(workflow.options(detached=True), args...)` |
| `aga.gather/race/quorum` | `aga.join(..., count=...)` |
| `aga.approval(...)` | `aga.signal(aga.Approval(...), timeout=...)` |
| `run.cancel(reason)` | `aga.cancel(run, reason)` |
| `aga.scheduled_time()` | `aga.info().scheduled_time` |
| `aga.Client`, `aga.RunState` | `aga.client.Client`, `aga.protocol.wire.RunState` |

This generation requires the lineage-aware server and wire contract. Freeze
submission ingress and schedules for the complete database, settle or explicitly
cancel every resumable 0.3 Run, archive its resettable hot origins, revoke the old
worker credentials, and stop every old server and worker before starting the 0.4
fleet. A target or Workflow name is not a generation fence, and old and current
workers must not coexist even on different targets. Once 0.4 accepts work, do not
roll back to an older generation.

Documentation: [overview](https://coding2fun.in/aga) ·
[Python guide](https://coding2fun.in/aga/python) ·
[source](https://github.com/vedhlabs/sdk-python)

Apache License 2.0. See [`LICENSE`](LICENSE).
