Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `COMPATIBILITY_MANIFEST` records each built-in adapter's install extra,
import module, accepted SDK range, tested lockfile version, and runtime binary
versions such as `openai-codex-cli-bin`.
- `RuntimeReadiness`, `ReadinessStatus`, the optional
`RuntimeReadinessProvider`, and bounded async `check_readiness()` probes
distinguish `READY_TO_ATTEMPT`, `NOT_READY`, and `INDETERMINATE` without
running an agent task. Registry, `AgentKit`, and provider-diagnostics helpers
expose the same async checks.

### Changed

Expand All @@ -37,6 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
tool-filter, and per-MCP-server environment support. Built-in `run()` methods
consume the same support report used by preflight, eliminating divergent
rejection paths.
- `availability()` is now strictly package-only and side-effect-free. Credential
and setup checks moved to async readiness probes: Codex uses the supported
account API with guaranteed cleanup, and Antigravity runs Google ADC discovery
off the event loop. Probe failures/timeouts are secret-safe and indeterminate.

### Fixed

Expand Down
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ capabilities visible.
`agent-runtime-kit` is for Python developers who want to run coding-agent tasks
through vendor runtimes without rewriting their application around each SDK. It
normalizes the runtime boundary: task inputs, capability checks, event streams,
tool audits, availability diagnostics, and typed results.
tool audits, package/readiness diagnostics, and typed results.

The library keeps vendor differences visible. Claude, Codex, and Antigravity
still expose different capabilities, permission models, setup requirements, and
Expand All @@ -23,7 +23,7 @@ shape instead of hiding them behind a lowest-common-denominator wrapper.

The package is intentionally not a router, benchmark harness, queue, hosted
service, or full agent framework. It is the reusable layer underneath those
systems: task models, runtime capabilities, event sinks, availability
systems: task models, runtime capabilities, event sinks, package/readiness
diagnostics, and adapters.

## Install
Expand Down Expand Up @@ -83,7 +83,7 @@ caches them per kind, and turns Python types into structured output.
import asyncio
from dataclasses import dataclass

from agent_runtime_kit import AgentKit
from agent_runtime_kit import AgentKit, ReadinessStatus


@dataclass
Expand All @@ -94,9 +94,9 @@ class RepoSummary:

async def main() -> None:
async with AgentKit() as kit:
diagnostic = kit.availability_for("claude")
if not diagnostic.available:
raise RuntimeError(diagnostic.message)
readiness = await kit.readiness_for("claude")
if readiness.status is ReadinessStatus.NOT_READY:
raise RuntimeError(readiness.message)
result = await kit.run(
"claude",
goal="Summarize this repository",
Expand All @@ -114,15 +114,15 @@ Adapters also work standalone when you need vendor-specific configuration:
```python
import asyncio

from agent_runtime_kit import AgentTask
from agent_runtime_kit import AgentTask, ReadinessStatus, check_readiness
from agent_runtime_kit.adapters import ClaudeAgentRuntime


async def main() -> None:
runtime = ClaudeAgentRuntime(default_model="claude-sonnet-4-6")
diagnostic = runtime.availability()
if not diagnostic.available:
raise RuntimeError(diagnostic.message)
readiness = await check_readiness(runtime)
if readiness.status is ReadinessStatus.NOT_READY:
raise RuntimeError(readiness.message)
result = await runtime.run(AgentTask(goal="Summarize this repository"))
print(result.output)

Expand All @@ -149,6 +149,13 @@ issue. Third-party runtimes can opt into provider-specific checks with the
`TaskSupportProvider` protocol, while older `AgentRuntime` implementations
continue to work through capability-based fallback checks.

`availability()` is deliberately synchronous, side-effect-free, and package-only.
Use `await check_readiness(runtime)` (or `kit.readiness_for(...)`) for an explicit,
bounded credential/setup probe. `READY_TO_ATTEMPT` means setup was positively
detected, not that a future provider call is guaranteed to succeed;
`INDETERMINATE` lets callers decide whether to attempt provider-chain or local
login authentication. Probe failures and timeouts never include credential values.

`AgentResult` returns output, finish reason (see `FinishReason`), locally
validated structured output, usage, cost, session id, tool-call audits, and
provider metadata. `is_success` is true only for a natural, error-free
Expand Down
7 changes: 7 additions & 0 deletions docs/api-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ import the names from the top-level package instead.
`TaskSupportProvider` is an optional extension, not a new requirement on the
`AgentRuntime` protocol, so existing third-party runtimes retain structural
compatibility and use capability-based fallback checks.
- **Package availability and execution readiness are separate.** Synchronous
`availability()` is package-only and side-effect-free. The bounded async
`check_readiness()` helper uses the optional `RuntimeReadinessProvider`
extension without changing the `AgentRuntime` protocol. A third-party runtime
without the extension maps a missing package to `NOT_READY` and present
package to `INDETERMINATE`. `READY_TO_ATTEMPT` establishes only that known
setup signals are present; it is not a guarantee of future execution.
- **`AgentKit` is sugar, not a second API.** The hub assembles the same frozen
`AgentTask` and returns the same `AgentResult` the runtimes produce
(`ParsedResult` is a runtime-identical subclass adding only the typed
Expand Down
5 changes: 3 additions & 2 deletions docs/capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
| Streaming output events | Yes — incremental `output.delta` while the SDK runs | No — a single `output.delta` at the end (non-streaming SDK) | Yes — from response chunks |
| Tool audit events | Yes — streamed from message blocks | Yes — emitted from parsed `TurnResult` items after the turn | Yes — from tool chunks |
| `vendor.turn` events | No | No | Yes — from thought/unknown chunks |
| Missing package diagnostics | Yes (`AgentRuntimeUnavailableError`) | Yes (`AgentRuntimeUnavailableError`) | Yes (`AgentRuntimeUnavailableError`) |
| Missing credential diagnostics | No — `availability()` reports available with an `auth_source` label; auth failures surface at `run()` | No — same as Claude (`auth_source` label; deferred) | Yes — `availability()` returns `MISSING_CREDENTIALS` when no API key / ADC-Vertex project is configured |
| Package availability | Sync, side-effect-free, package-only | Sync, side-effect-free, package-only | Sync, side-effect-free, package-only; never calls ADC |
| Async readiness | Direct API key/OAuth/Bedrock bearer signal → `READY_TO_ATTEMPT`; provider/local chains → `INDETERMINATE` | Supported `AsyncCodex.account(refresh_token=False)` probe; absent account → `NOT_READY` | API key or off-loop ADC/project probe; missing → `NOT_READY` |
| Probe failure/timeout | `INDETERMINATE` | `INDETERMINATE`, app-server always closed | `INDETERMINATE` |
| Live smoke test | Opt-in | Opt-in | Opt-in |

The matrix is intentionally not a lowest-common-denominator contract. Adapters
Expand Down
63 changes: 61 additions & 2 deletions docs/providers.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Provider Diagnostics

`agent-runtime-kit` keeps provider setup checks explicit. Each runtime exposes
`availability()` and returns a `RuntimeAvailability` value with:
`agent-runtime-kit` separates cheap package discovery from provider setup
probes. Each runtime's synchronous `availability()` is side-effect-free and
package-only. It returns a `RuntimeAvailability` value with:

- runtime kind
- availability flag
Expand All @@ -10,6 +11,64 @@
- package name
- installed version when discoverable

Availability never imports a vendor SDK, starts a subprocess, reads a
credential store, calls Google ADC, or contacts a provider. Package presence is
not a claim that execution will work.

Use the public `await check_readiness(runtime, timeout=5.0)` for an explicit
credential/setup probe. Registry and hub forms are also available:

```python
readiness = await registry.readiness_for("codex")
readiness = await kit.readiness_for("codex")
all_readiness = await kit.readiness()
```

`RuntimeReadiness.status` has three outcomes:

- `READY_TO_ATTEMPT`: a supported setup or credential signal was positively
established. It does not promise that a later model/network request succeeds.
- `NOT_READY`: a concrete problem such as a missing package, account, API key,
or ADC project was established.
- `INDETERMINATE`: the runtime uses a provider/local credential chain that
cannot be verified safely, lacks the optional readiness extension, or the
bounded probe failed/timed out. The caller chooses whether to attempt work.

The built-in probes never return credential values or raw exception messages.
They report only safe categories such as auth source and exception type. The
optional `RuntimeReadinessProvider` protocol does not change the required
`AgentRuntime` protocol. For older third-party runtimes,
`check_readiness()` maps negative availability to `NOT_READY` and positive
package presence conservatively to `INDETERMINATE`.

Provider behavior is deliberately specific:

- Claude treats direct API key, OAuth-token, and Bedrock bearer-token signals
as `READY_TO_ATTEMPT`. Provider chains and provider-owned local login remain
`INDETERMINATE`; probing them would require starting a real task or scraping
unsupported credential stores.
- Codex starts the supported `AsyncCodex` client, calls
`account(refresh_token=False)`, and always closes the app-server context. An
account is `READY_TO_ATTEMPT`, no account is `NOT_READY`, and startup/API/
cleanup failures are `INDETERMINATE`.
- Antigravity accepts an explicit or ambient Gemini API key, otherwise probes
Google Application Default Credentials and project setup in a worker thread
so synchronous Google discovery cannot block the event loop. Missing setup
is `NOT_READY`; errors and timeouts are `INDETERMINATE`.

For all built-ins at once, the existing sync collector stays package-only while
the async collector performs bounded readiness checks:

```python
from agent_runtime_kit.adapters.diagnostics import (
collect_provider_diagnostics,
collect_provider_readiness,
)

packages = collect_provider_diagnostics()
readiness = await collect_provider_readiness(timeout=5.0)
```

## Install Model

The plain `agent-runtime-kit` package is the vendor-SDK-free core and includes
Expand Down
9 changes: 7 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,10 @@ runtime = ClaudeAgentRuntime(default_model="claude-sonnet-4-6", reuse_process=Tr
result = await runtime.run(AgentTask(goal="Summarize this repository"))
```

Use `kit.availability()` (or `runtime.availability()`) before dispatching work
in applications that need clear setup diagnostics.
Use synchronous `kit.availability()` (or `runtime.availability()`) for a
side-effect-free package check. Use `await kit.readiness()` or
`await check_readiness(runtime)` when an application also needs a bounded
credential/setup probe. Treat `ReadinessStatus.READY_TO_ATTEMPT` as permission
to try — not a guarantee that the provider, model, or network will accept the
next request. `NOT_READY` is a confirmed setup problem; `INDETERMINATE` leaves
the attempt policy to the caller.
11 changes: 10 additions & 1 deletion src/agent_runtime_kit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
ParsedResult,
PermissionMode,
PermissionProfile,
ReadinessStatus,
RuntimeAvailability,
RuntimeReadiness,
RuntimeReadinessProvider,
SessionResumeState,
TaskSupportIssue,
TaskSupportProvider,
Expand All @@ -50,6 +53,7 @@
tool_requested_event,
vendor_turn_event,
)
from agent_runtime_kit.readiness import DEFAULT_READINESS_TIMEOUT, check_readiness
from agent_runtime_kit.registry import RuntimeRegistry, create_default_registry
from agent_runtime_kit.support import validate_task

Expand All @@ -64,22 +68,26 @@
"AgentTask",
"ArtifactRef",
"AvailabilityReason",
"COMPATIBILITY_MANIFEST",
"DEFAULT_READINESS_TIMEOUT",
"EventSink",
"FakeAgentRuntime",
"FilesystemAccess",
"FinishReason",
"KIND_ALIASES",
"COMPATIBILITY_MANIFEST",
"McpServerConfig",
"OutputSchemaError",
"OutputTypeError",
"PackageVersion",
"ParsedResult",
"PermissionMode",
"PermissionProfile",
"ReadinessStatus",
"RuntimeAvailability",
"RuntimeCompatibility",
"RuntimeNotRegisteredError",
"RuntimeReadiness",
"RuntimeReadinessProvider",
"RuntimeRegistry",
"SessionResumeState",
"TaskSupportIssue",
Expand All @@ -90,6 +98,7 @@
"Usage",
"create_default_registry",
"compatibility_for",
"check_readiness",
"runtime_kind_value",
"output_delta_event",
"safe_emit",
Expand Down
32 changes: 29 additions & 3 deletions src/agent_runtime_kit/_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
PermissionMode,
PermissionProfile,
RuntimeAvailability,
RuntimeReadiness,
SessionResumeState,
TaskSupportReport,
)
from agent_runtime_kit.readiness import DEFAULT_READINESS_TIMEOUT, check_readiness
from agent_runtime_kit.registry import RuntimeFactory, RuntimeRegistry, create_default_registry
from agent_runtime_kit.support import validate_task as validate_runtime_task

Expand Down Expand Up @@ -64,8 +66,8 @@ class AgentKit:

``AgentKit()`` builds a registry with the fake runtime and the vendor
adapters registered (adapters resolve their SDKs lazily, so this works
without any extra installed — ``availability_for`` reports what is
missing). Pass ``registry=`` to bring your own; the other flags then do
without any extra installed — ``availability_for`` reports missing
packages). Pass ``registry=`` to bring your own; the other flags then do
not apply.

Runtimes resolved through the hub are constructed zero-arg, cached per
Expand Down Expand Up @@ -101,13 +103,37 @@ def kinds(self) -> tuple[AgentRuntimeKind | str, ...]:
return self._registry.kinds()

def availability(self) -> tuple[RuntimeAvailability, ...]:
"""Availability diagnostics for every registered kind."""
"""Side-effect-free package diagnostics for every registered kind."""

return tuple(self._registry.availability_for(kind) for kind in self._registry.kinds())

def availability_for(self, kind: AgentRuntimeKind | str) -> RuntimeAvailability:
return self._registry.availability_for(self._normalize_kind(kind))

async def readiness(
self,
*,
timeout: float = DEFAULT_READINESS_TIMEOUT,
) -> tuple[RuntimeReadiness, ...]:
"""Probe every registered runtime concurrently."""

return tuple(
await asyncio.gather(
*(self.readiness_for(kind, timeout=timeout) for kind in self.kinds())
)
)

async def readiness_for(
self,
kind: AgentRuntimeKind | str,
*,
timeout: float = DEFAULT_READINESS_TIMEOUT,
) -> RuntimeReadiness:
"""Probe one cached runtime without executing an agent task."""

runtime = await self._runtime_for(self._normalize_kind(kind))
return await check_readiness(runtime, timeout=timeout)

def capabilities_for(self, kind: AgentRuntimeKind | str) -> AgentCapabilities:
return self._registry.capabilities_for(self._normalize_kind(kind))

Expand Down
12 changes: 12 additions & 0 deletions src/agent_runtime_kit/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
AgentRuntimeKind,
AgentTask,
RuntimeAvailability,
RuntimeReadiness,
TaskSupportReport,
ToolCallAudit,
)
Expand Down Expand Up @@ -64,6 +65,17 @@ def validate_task(self, task: AgentTask) -> TaskSupportReport:

return _validate_declared_task_support(self.kind, self.capabilities, task)

async def check_readiness(self) -> RuntimeReadiness:
"""Fake runtime is deterministic and always ready to attempt."""

availability = self.availability()
return RuntimeReadiness.ready_to_attempt(
self.kind,
package=availability.package,
version=availability.version,
metadata=availability.metadata,
)

async def run(self, task: AgentTask) -> AgentResult:
"""Return a deterministic result after validating capabilities."""

Expand Down
Loading