Skip to main content

buildathena-sdk

PyPI version Python 3.11+ Status: Alpha

Python SDK for building blocks and workflows on the Athena Labs 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

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:

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.

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:

@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):

@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:

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:

@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:

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:

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)],
)

session = await session.prepare(timeout=60)
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.

Within the server-owned execution scope, the SDK identifies each operation by its authored call site, operation/target, normalized arguments, and occurrence number among identical requests. Different requests have separate counters, so changing their concurrent arrival order does not change their identities. Counters start empty when the controller restarts; replay reconstructs the same keys and reconnects to persisted operations. Repeated identical calls remain separate occurrences; a retry of an existing failed child returns that failure.

Keep authored requests stable across retries. Changed arguments create a new identity. Identical concurrent requests are distinguished only by occurrence: which coroutine receives which result can change on replay. Include the logical candidate or evaluation identity in the actual request when that distinction matters. There are no caller-supplied replay scopes or keys. Bulk inputs are serialized once before identity allocation, and those exact bytes are uploaded; custom encoders must produce deterministic bytes across retries. Deferred output references use producer/output identity without executing or loading the producer.

A response to a pending Chat request uses that exact request's identity. Browser-generated user intents and transport-level request keys remain separate internal boundaries.

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 and session.add_repos() persist selections without preparing checkouts. Retain the returned Session, then explicitly prepare it before checkout operations. 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:

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:

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:

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:

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:

# 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:

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.

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 for discovery and launch layers.

Documentation

License

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

Metadata

Release files for buildathena-sdk 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for buildathena-sdk 0.7.0
File Interpreter ABI Platform
buildathena_sdk-0.7.0-py3-none-any.whl Python 3 none any Details

Release files / buildathena_sdk-0.7.0-py3-none-any.whl

Download URL buildathena_sdk-0.7.0-py3-none-any.whl
Size 264.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d985daac9a30a34c852cd10d2b687482848b239f23cf345af3ade25f668930e2
BLAKE2b-256 checksum
How to use checksums
75e320cd2a088c4c88e3ab476c0b96b20597ea28d7a39218ec3efcf67d60a74f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.0

Release history Release notifications | RSS feed

0.7.3

1 release file

0.7.2

1 release file

0.7.1

1 release file

This release

0.7.0 This release

1 release file

0.6.8

1 release file

0.6.7

1 release file

0.6.6

1 release file

0.6.5

1 release file

0.6.4

1 release file

0.6.3

1 release file

0.6.2

1 release file

0.6.1

1 release file

0.6.0

1 release file

0.5.1

1 release file

0.5.0

1 release file

0.4.10

1 release file

0.4.9

1 release file

0.4.8

1 release file

0.4.6

1 release file

0.4.5

1 release file

0.4.3

1 release file

0.4.2

1 release file

0.3.20

2 release files

0.3.17

2 release files

0.3.16

2 release files

0.3.15

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.2.20

2 release files

0.2.18

2 release files

0.2.16

2 release files

0.2.15

2 release files

0.2.13

2 release files

0.2.11

2 release files

0.2.6

2 release files

0.2.2

2 release files

0.1.65

2 release files

0.1.31

2 release files

0.1.30

2 release files

0.1.29

2 release files

0.1.28

2 release files

0.1.27

2 release files

0.1.25

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page