Metadata-Version: 2.5
Name: buildathena-sdk
Version: 0.6.6
Summary: Athena Labs Python SDK for agentic ML workflow orchestration
Project-URL: Homepage, https://buildathena.dev
Project-URL: Documentation, https://buildathena.dev/docs/sdk
Author-email: Athena <richard@buildathena.dev>
License: Proprietary
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Requires-Dist: aiofiles>=23.0.0
Requires-Dist: click<9.0.0,>=8.0.0
Requires-Dist: googleapis-common-protos<2.0.0,>=1.70.0
Requires-Dist: grpcio<2.0.0,>=1.82.1
Requires-Dist: httpx<1.0.0,>=0.26.0
Requires-Dist: hydra-core<2.0.0,>=1.3.2
Requires-Dist: protobuf<8.0.0,>=7.35.0
Requires-Dist: pydantic<3.0.0,>=2.12.0
Requires-Dist: pyyaml<7.0.0,>=6.0.0
Provides-Extra: dev
Requires-Dist: grpcio-tools==1.82.1; extra == 'dev'
Requires-Dist: mypy>=1.7.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.2.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.0.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Description-Content-Type: text/markdown

<!-- Verified against codebase: 2026-08-24 -->

# buildathena-sdk

[![PyPI version](https://img.shields.io/pypi/v/buildathena-sdk)](https://pypi.org/project/buildathena-sdk/)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
[![Status: Alpha](https://img.shields.io/badge/status-alpha-orange)](https://buildathena.dev)

Python SDK for building blocks and workflows on the [Athena Labs](https://buildathena.dev) ML orchestration platform. Athena Labs makes ML workflows reproducible, observable, interruptible, and composable through a DAG execution engine with a built-in session that can build, run, monitor, and repair pipelines.

> **Alpha software** — APIs may change between releases. Pin your version in production.

## Installation

```bash
ATHENA_VERSION="$(athena --version | awk '{print $2}')"
pip install "buildathena-sdk==${ATHENA_VERSION}"
```

Requires Python 3.11+.

## Quick Start

Define a block, read resolved config from `ctx.config`, emit metrics and progress, and register an explicit artifact when you want a durable named asset:

```python
from athena import BlockContext, ConfigRef, block

@block(
    name="TrainModel",
    outputs=["checkpoint"],
    config=ConfigRef(search_path="conf", config_name="train"),
)
async def train_model(ctx: BlockContext) -> dict:
    epochs = int(ctx.config.get("epochs", 100))
    learning_rate = float(ctx.config.get("learning_rate", 1e-3))

    for epoch in range(epochs):
        loss = train_epoch(lr=learning_rate)
        await ctx.emit_metric("loss", loss, step=epoch)
        await ctx.emit_progress(epoch + 1, epochs)
        await ctx.check_pause()  # cooperative pause point

    checkpoint = await ctx.artifacts.register(
        "model.pt",
        name="checkpoint",
        format="pickle",
        mime_type="application/octet-stream",
        tags=["training", "final"],
    )
    return {"checkpoint": checkpoint.as_ref()}
```

Portable resource requests may include CPU, system memory, `/dev/shm` shared
memory, ephemeral local storage, and accelerator requirements. `memory` is the
total container RAM reservation/limit. `shared_memory` is only the `/dev/shm`
mount-size ceiling: it may be used without `memory` or be larger than `memory`,
and it does not create an additional physical RAM pool. When `memory` is
omitted, RAM remains unreserved.

```python
from athena import BlockEnvironment, ResourceSpec

environment = BlockEnvironment(
    backends=["docker", "kubernetes"],
    image="us-docker.pkg.dev/acme/athena/train:2026-08-24",
    resources=ResourceSpec(
        cpu="2",
        memory="4Gi",
        shared_memory="8Gi",
        ephemeral_storage="16Gi",
    ),
)
```

Docker supports `shared_memory` and intentionally does not match requests that
contain `ephemeral_storage`. Kubernetes supports both.
Process accepts resource-bearing work but does not enforce or reserve those
fields. Omit `process` from `BlockEnvironment.backends` when enforcement is
required.

Docker and Kubernetes mount the exact run code bundle at `/athena/workspace`,
set `ATHENA_WORKSPACE=/athena/workspace`, and execute with that directory as the
working directory. The Dockerfile owns dependency installation and environment
layout; the runtime mount hides anything the image placed under the reserved path.
Ensure the declared executable resolves through `PATH` or use an absolute path.
`BlockEnvironment.executable` overrides the image's `ENTRYPOINT` and `CMD` on both
backends, and Athena appends `-m athena.harness`. Include any required runtime
wrapper explicitly in that prefix.
Process workers set `ATHENA_WORKSPACE` to a fresh detached, attempt-owned worktree
at the exact `code_ref` commit. An ignored authoring `.venv` is not copied there;
reuse requires a prepared interpreter available on the execution endpoint.

## Consuming Inputs

Block inputs come from the Python signature after `ctx`. Athena hydrates those
arguments from upstream outputs before invoking the block:

```python
@block(name="Evaluate", outputs=["report"])
async def evaluate(ctx: BlockContext, checkpoint) -> dict:
    checkpoint_ref = checkpoint
    checkpoint_path = await ctx.artifacts.resolve(checkpoint_ref)
    model = load_model(checkpoint_path)
    score = run_eval(model)
    await ctx.emit_metric("accuracy", score)
    return {"report": {"accuracy": score}}
```

### Structured values and forwarding

Lists, tuples (delivered as lists), and dictionaries with string keys preserve their
structure as typed metadata. Bulk Python values are stored as blob leaves. The
consumer recursively hydrates those leaves before invocation; `ArtifactRef` leaves
stay references until explicitly loaded. `artifact.as_data()` requests data delivery.

For example, given blocks `produce` (output `bundle`) and `consume` (output `report`):

```python
@block(outputs=["report"])
async def driver(ctx: BlockContext):
    produced = produce()
    consumed = consume(batch={"items": [produced.bundle], "label": "trial"})
    result = await consumed
    return {"report": result["report"]}
```

Materializing `consume` waits for `produce` to finish and forwards its durable output
identity. It does not load `produced.bundle` in the driver. Awaiting `consumed`
explicitly loads the consumer's result in the driver. Indexing an unresolved output
reference does not load it; await the reference when Python needs that value.

A returned output may likewise contain nested references:

```python
return {"bundle": {"images": [image_a.as_ref(), image_b.as_ref()], "scores": scores}}
```

Cache and seed reuse carry every artifact reference in these typed containers into
the destination run, rewriting repeated references consistently within each output.
Artifacts registered but not returned are not carried forward. Athena never searches
inside pickle payloads, artifact files, or historical opaque containers for references.
A Python result-loading failure does not change a successfully completed child call.

Execution values share a **1 MiB inline-data budget per operation**, across all
named inputs or outputs. Larger reference-free JSON subtrees use the existing
managed blob upload path and are decoded normally by the consumer. This applies
to block outputs, nested-call inputs, public Run inputs, and adaptive expansion
literals. Artifact and execution-output references remain visible and retain
their identity; size planning never hydrates referenced output payloads.

Explicit `BlobRef`, `ArtifactRef`, `InlineBytes`, and `EncodedValue` leaves use the
canonical value codec, preserving identity, media type, and declared codec/schema
metadata. They never become pickled wrapper objects. Explicit inline byte bodies
consume the same inline budget and raise a size error if they do not fit; they are
not automatically changed into another representation.

Run and dynamic-call inputs accept `InlineBytes` and inline `EncodedValue` bodies
with codec `bytes`, `json`, or `pickle`, subject to the SDK inline budget. New bulk
inputs require an owned, finalized upload; an `EncodedValue` with a blob body is
not a substitute for that admission. Pass ordinary Python data or an explicit
`OutputHandle` to stage bulk data, or forward `call.output(name)` / `run.output(name)`
to reuse an existing output. A manually constructed `BlobRef` is not a read capability.
Adaptive expansions require byte-backed values to be published and referenced;
they do not accept direct `InlineBytes` or `EncodedValue` literals.

The server applies the same inline planning rules when collecting map outputs,
composing workflow outputs, and preparing worker inputs. It resolves existing output
metadata without downloading referenced blobs. Large reference-free JSON subtrees
are uploaded through the existing managed-storage lifecycle; typed reference leaves
retain their identities. Each published output has its own inline allowance. Worker
input preparation shares an allowance across the attempt's named inputs.

Run output selection uses `GetRunOutput`; it does not fetch sibling outputs.
Awaiting the whole Run follows byte-bounded `ListRunOutputs` pages, then hydrates
its values. Runtime calls use the same output descriptors through
`GetRuntimeCallOutput` and `ListRuntimeCallOutputs`. Their status snapshots contain
no output values: `await call.wait()` consumes the terminal result without fetching
or decoding descriptors. Selecting or forwarding one output reads only its descriptor;
forwarding does not download its data. These reads never create blobs.

The complete execution RPC, including reference metadata and its envelope, must
fit **4 MiB**. Oversized reference-bearing metadata is rejected with a size error;
split such operations into smaller calls. The shared limits are defined in
`proto/athena/value/v1/value.proto` and generated for each language.

Server-side control evaluation has a separate cumulative budget, configured by
`ATHENA_EXPANSION_MAX_CONTROL_VALUE_BYTES` (default **10 MiB**). It counts inline
JSON, keys and container framing across the whole resolved value. Each blob
costs the larger of its downloaded size and its parsed, serialized JSON size,
using the remaining allowance. This limit does not cap ordinary bulk payloads
hydrated in a worker, and existing nesting
limits still apply.

## Credentials

Declare required secrets in the `@block` decorator and access them at runtime via `ctx.secrets`. Credentials are encrypted at rest and injected only during execution:

```python
@block(name="FetchData", outputs=["dataset"], secrets=["API_KEY"])
async def fetch_data(ctx: BlockContext) -> dict:
    key = ctx.secrets["API_KEY"]
    data = await download(api_key=key)
    return {"dataset": data}
```

Declare outputs on the decorator and return a mapping with exactly those keys.

## Cooperative Pause

Call `check_pause()` inside long-running loops to let Athena Labs pause the block between iterations without losing progress:

```python
for epoch in range(epochs):
    train_step()
    await ctx.check_pause()  # yields control if a pause was requested
```

## BlockContext API

| Method / Accessor | Description |
|---|---|
| `ctx.secrets["KEY"]` | Access declared secrets |
| `await ctx.emit_metric(name, value, step=, labels=)` | Emit one scalar metric |
| `await ctx.emit_metrics({"loss": loss, "accuracy": acc}, step=, labels=)` | Emit multiple scalar metrics |
| `await ctx.emit_progress(current, total, message=)` | Emit progress (current/total) |
| `await ctx.emit_log(message, level=, source=)` | Emit a structured log event |
| `await ctx.check_pause()` | Cooperative pause checkpoint |
| `await ctx.artifacts.register(source, format=, mime_type=, name=, tags=, metadata=)` | Create a durable artifact |
| `artifact.as_ref()` / `artifact.as_data()` | Choose pointer or hydrated downstream delivery |
| `await ctx.artifacts.resolve(ref)` | Resolve a managed artifact to a local path |
| `await ctx.artifacts.load(ref)` | Load and deserialize a formatted managed artifact |
| `ctx.athena` | Attempt-scoped Repo, Session, Chat, Workflow, and Run resources |

`as_data()`, `resolve()`, and `load()` apply to managed artifacts. URI-backed external artifacts
use `as_ref()` and expose their location through `ref.uri` for code that can access the bound store
or filesystem.

## Block-Scoped Athena Client

Athena injects the canonical Repo, Session, and private Chat client into an
executing block. Chat collections paginate transparently, sending returns after
durable acceptance, and receipt waits target only the submitted turn:

```python
repo = await ctx.athena.repos.get("https://github.com/acme/training.git")
session = await ctx.athena.sessions.create(
    title="Candidate search",
    repos=[repo.at("candidate", new_branch=True)],
)

proposal_branch = session.repos[0].branch_name
chat = await session.chats.create(model_key="claude-opus-5")
receipt = await chat.send(
    f"""Athena prepared `{proposal_branch}` as a fresh proposal branch seeded from the
exact current head of `candidate`. Work directly in the prepared branch. After the
edit, use athena_attachment_git first with action `commit` and a concise message,
then with action `publish` and no message.""",
)
turn = await receipt.wait(timeout=None)

async for existing_chat in session.chats.list():
    print(existing_chat.id)

async for item in chat.items():
    consume(item)

async for item in turn.items():
    consume_turn_item(item)

# Stop interrupts current work but leaves the Chat open.
stop = await chat.stop(reason="operator requested")

# Close only after the Chat is quiescent, then read current state explicitly.
await chat.close()
closed = await session.chats.get(chat.id)
assert closed.closed_at is not None

# Exact lookup is the durable pointer; lookup alone does not reopen the Chat.
same_chat = await session.chats.get(closed.id)
await same_chat.resume()
resumed = await session.chats.get(same_chat.id)
assert resumed.closed_at is None
```

`model_key` is the stable model identity shown in Athena. With no `connection`,
Athena uses its platform connection. To use a team connection, pass its permanent
name, for example `connection="Acme inference"`. Athena resolves that pair to an
exact route when the Chat is created and keeps the route pinned on the Chat.

`title` is optional. If it is omitted, the server assigns a stable friendly title such as
`quiet-snail-cafe`; the generated title is returned on the `Session` object and in browser
projections. Titles are trimmed and limited to 255 characters.

Block code does not author runtime call keys or application idempotency keys.
Invoke a `Block` or nested `Workflow` directly with `child(...)`; there is no
separate `.call(...)` authoring path. Session, Chat, and independent
`Workflow.run()` operations likewise derive their internal replay identity. Ordinary
mutations use a stable logical call slot; a response to a pending Chat request uses
that exact request's identity. Keep construction order deterministic so the same
source slot continues to represent the same logical operation after a controller retry.
Browser-generated user intents and transport-level request keys are separate
internal boundaries, not block SDK arguments.

`repo.at(parent, new_branch=True)` seeds a new branch from the parent's published
HEAD, resolving origin when no published head exists. Session creation attempts
checkout preparation automatically but may return before it is ready.
`authoring.head_sha` is the checkout's reported HEAD and may be absent; it is not
the published HEAD. `client.sessions.get(session.id)` observes state without
retrying preparation. To prepare and await usable checkouts:

```python
session = await session.prepare(timeout=60)      # All current caller-owned repos
attached = await session.repos[0].prepare(timeout=60)  # Or one exact selection
```

Both return fresh snapshots and raise on preparation failure or timeout. Concurrent
callers wait on existing checkout mutations; a changed branch selection fails the
wait. Preparation uses the current authoring placement policy; it does not reconcile
Git or wait for discovery. A timeout stops waiting, while already dispatched checkout
work may finish afterward. Use the returned handles, since placement can advance
the attachment selection version.

Reconcile an existing clean checkout with its canonical remote branch:

```python
attached = await attached.reconcile(mode="ff_only")
print(attached.authoring.head_sha)
# If the controller already knows the intended remote SHA:
attached = await attached.reconcile(
    mode="ff_only", expected_published_head_sha=published_sha,
)
```

`ff_only` rejects a checkout with local commits ahead of or diverged from the
remote. Use `merge` to merge remote changes or `rebase` to replay local commits
onto them; both require the normal Git identity setup when creating commits.
Conflicts fail the operation, with an abort attempted to restore the checkout.
The optional expected SHA is checked against the fetched remote head before
mutation. Reconciliation requires materialization, returns a fresh `SessionRepo`
snapshot, and schedules discovery refresh; it does not publish local commits.
It is a direct Git mutation with no automatic replay after transport failure.
Inspect the checkout before retrying an uncertain result.

Agent-authored changes are committed with a message and then published without a
message.

`chat.items()` and `turn.items()` expose only the typed, durable, user-visible
transcript projection. They are not live watches or raw event/trace access.
Canonical values decode to ordinary Python primitives where lossless; inline
bytes, opaque blobs, encoded values, and artifacts remain explicit
`InlineBytes`, `BlobRef`, `EncodedValue`, and `ArtifactRef` wrappers rather than
generated Protobuf messages or implicitly fetched bulk data.

`chat.stop()` durably requests interruption of work active at admission time;
it does not close the Chat or prevent later messages and wakes. `chat.close()`
is a synchronous lifecycle mutation that requires the Chat to have no active or
queued agent work. While closed, that exact Chat rejects new ingress and cannot
receive gate wakes, but it remains readable through `session.chats.get(chat_id)`
and `chat.items()`. Only explicit `chat.resume()` reopens it. Closing one Chat
does not disable a Session policy that is defined to create a different fresh
Chat.

### Independent Runs

`Workflow.run()` returns a durable `Run` handle. Lifecycle reads return an
immutable `RunSnapshot`; outputs and metrics have focused accessors:

```python
run = await candidate.workflow("workflows/evaluate.py:evaluate").run(profile=None)

snapshot = await run.wait(timeout=3600)
outputs = await run
fitness = await run.metric("fitness")

print(snapshot.status, snapshot.is_terminal)
print(snapshot.source.repo_url)
print(snapshot.source.branch_name, snapshot.source.commit_sha)
```

This evaluation passes `profile=None` because the controller polls the Run directly and does not
need profile gates or agent wakes.

`RunSnapshot` carries `id`, `lab_id`, `workflow_key`, `status`, `is_terminal`,
`failure`, `held_node_ids`, `created_at`, `started_at`, `completed_at`, and
`source`. Its `source` contains `repo_url`, `branch_name`, and `commit_sha` for
that observation. `run.source` is the latest source observed by the handle.

Use `status()` for a current snapshot, `wait()` for a terminal snapshot,
`await run` for hydrated named outputs, `metric(name)` for the latest metric,
and `kill()` or `release_held_nodes(...)` for lifecycle controls.

Block call and Run handles share the same completion and value-loading contract:

| Operation | Block call | Independent Run |
| --- | --- | --- |
| Wait for terminal status without loading outputs | `await call.wait()` | `await run.wait()` |
| Wait for success and load all outputs | `await call` | `await run` |
| Load one named output | `await call.output("x")` | `await run.output("x")` |
| Forward an output without loading it | `call.output("x")` | `run.output("x")` |

Status waits report `succeeded`, `failed`, or `killed`; value-loading awaits raise
on failure or termination. `CallSnapshot` contains the call ID, status, and optional
`CallFailure` details. A cancelled waiter leaves the durable execution running.
Use `asyncio.timeout(...)` to bound a value-loading await. The Run launch itself
remains separate: `run = await workflow.run(...)` returns its handle.

Run waits reconnect after recoverable watch interruptions, using capped backoff
and fresh versioned snapshots. Non-retryable service failures and protocol errors still raise.

To pass a Run's public output into another independent Run, select
the output reference without downloading its value:

```python
producer = await candidate.workflow("workflows/produce.py:produce").run(profile=None)
consumer = await candidate.workflow("workflows/consume.py:consume").run(
    dataset=producer.output("dataset"),
    profile=None,
)
values = await consumer  # Hydrates the consumer's named outputs.
```

`Run.output(name)` returns a symbolic `RunOutputRef` immediately. The consuming
`.run(...)` waits for the producer to succeed, resolves the durable output ID
without downloading its value, then launches the consumer. `await run.output(name)`
loads that named output; `await run` loads all named outputs.

Block call outputs and Run outputs use the same reference-forwarding boundary,
including references nested inside lists and dictionaries:

```python
# Inside an executing async block:
training = train(dataset=dataset)  # CallHandle, within the current Run
evaluation = await candidate.workflow("workflows/evaluate.py:evaluate").run(
    checkpoint=training.output("checkpoint"),
    profile=None,
)
report = summarize(metrics=evaluation.output("metrics"))
metrics = await evaluation.output("metrics")  # Explicitly load only metrics.
return {"report": (await report).report}
```

The training checkpoint need not be a published workflow output to become a Run
input. An action can forward outputs of its consumed block calls and of independent
Runs launched by that same parent Run/node; sibling actions do not inherit access.
Returned graph expansions use the same authorization rule.

Inputs and returns share automatic encoding: supported scalar values and containers
remain typed, references retain their identities, and opaque Python leaves become
managed blobs. The Run API stages opaque inputs before admitting the Run. For
example, a set can be an input without the caller serializing it manually:

```python
evaluation = await candidate.workflow("workflows/evaluate.py:evaluate").run(
    labels={"sand", "shale"},
    profile=None,
)
```

For explicit serialization, use `OutputHandle.from_data(value, format="pickle")`
from `athena.output_handle` (also `"json"` or `"bytes"`). This works for block
returns, dynamic block inputs, and Run inputs. Explicit serialization makes the
wrapped value opaque: references hidden inside it are not rewritten or forwarded
as dependencies. Custom Python objects require compatible classes and dependencies
in the consuming environment.

## Config

Blocks use Hydra-backed `ConfigRef` values. Discovery passes Hydra's exact composed mapping to
`ctx.config`; it does not unwrap directory-named keys or pass config values as block arguments.
Parameters after `ctx` remain workflow-wired dataflow inputs. Hydra owns composition and
interpolation, while per-run block overrides live under exact root node IDs in
`RunProfile.config.nodes` and are deep-merged over the discovered mapping.

```python
from athena import ConfigRef, block

@block(
    config=ConfigRef(
        search_path="configs",
        config_name="train",
    )
)
async def train(ctx):
    epochs = ctx.config["epochs"]
```

`config_name` is relative to `search_path`. For a flat mapping from
`configs/blocks/train.yaml`, use `search_path="configs/blocks"` and
`config_name="train"`. Using `search_path="configs"` with
`config_name="blocks/train"` intentionally follows Hydra package semantics and can produce a
top-level `blocks` key.

See the [config docs](https://buildathena.dev/docs/features/config) for discovery and launch layers.

## Documentation

- [Getting Started](https://buildathena.dev/docs/setup) — installation and first workflow
- [SDK Reference](https://buildathena.dev/docs/sdk) — decorators, config, and discovery
- [Generated Python API Reference](./API_REFERENCE.md) — exact public signatures, types, and defaults
- [BlockContext API](https://buildathena.dev/docs/sdk/block-context) — full method reference
- [Evolutionary Controllers](https://buildathena.dev/docs/sdk/evolutionary) — candidate branches, private Chats, evaluation Runs, and lineage
- [Features](https://buildathena.dev/docs/features) — gates, inspectors, caching, and more
- [knowledge-base/current/architecture/sdk.md](../../../knowledge-base/current/architecture/sdk.md) — internal architecture and implementation reference

## License

Proprietary - Copyright (c) 2026 Athena Labs Research Inc. All rights reserved.
