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
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,13 @@ jobs:
atomr-agents-coding-cli-vendor-antigravity
atomr-agents-coding-cli-harness
atomr-agents-coding-cli-harness-web
# --- Claude Agent SDK harness ----------------------------
# core → harness (needs agent) → harness-web (needs harness).
# All publish before host/py-bindings/umbrella, which depend
# on agent-sdk-harness.
atomr-agents-agent-sdk-core
atomr-agents-agent-sdk-harness
atomr-agents-agent-sdk-harness-web
# --- deep-research capability ----------------------------
atomr-agents-deep-research-core
atomr-agents-deep-research-harness
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,45 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added — Claude Agent SDK harness

Anthropic's **Claude Agent SDK** (`claude-agent-sdk` /
`@anthropic-ai/claude-agent-sdk`) — the programmable form of Claude Code —
wrapped as a first-class atomr-agents harness. It exposes Claude Code's full
feature set (slash commands, subagents, hooks, MCP, permission modes,
sessions, custom system prompts, built-in Read/Write/Edit/Bash/Grep/WebSearch
tools) and bills against your Anthropic API credits by driving the bundled
`claude` CLI. Distinct from the server-side "Managed Agents" API.

- **`atomr-agents-agent-sdk-core`** — the contract: `AgentSdkConfig` mirroring
the SDK's `ClaudeAgentOptions` (round-trips to a JSON dict), a normalized
message/result/event schema, and the pluggable `AgentSdkBackend` /
`AgentSdkSession` trait seam with a deterministic in-memory `MockBackend` so
the whole surface is testable without the SDK, the `claude` CLI, or
network/credits.
- **`atomr-agents-agent-sdk-harness`** — orchestrator with a pluggable backend,
an event broadcast, a session registry under an atomically-reserved
concurrency quota, Anthropic-credit spend tracking via the shared
`SpendLedger`, a `.claude/` projection (skills / slash commands / MCP /
settings), and a `Callable` impl. Behind the `actor` feature it exposes the
bidirectional interactive session as an `atomr_core::actor::Actor`. The
default `permission_mode` is `bypassPermissions` (max autonomy — overridable
per request/spec).
- **`atomr-agents-agent-sdk-harness-web`** — axum REST + SSE companion (start
runs/sessions, post messages, interrupt, change mode/model, stream events).
- **PyO3 bridge** — `from atomr_agents.agent_sdk import harness`. The Rust
`PythonAgentSdkBackend` drives the real `claude-agent-sdk` over a reverse
async-iterator bridge (Rust drives the Python `__anext__`); the interactive
session is exposed as `AgentSdkSession` (the actor surface). atomr `@tool`
guests and Rust tools are bridged into the agent as in-process SDK MCP tools
via `create_sdk_mcp_server`.
- **Host loader** — `<root>/agent-sdk/<id>/` (`harness.yaml` + `commands/` +
`skills/` + `mcp/`) → `AgentSdkHarnessSpec` + `.claude` projection, reusing
the host's on-disk conventions.
- **Umbrella** — new `agent-sdk` feature.
- **Packaging** — `pip install atomr-agents[agent-sdk]` pulls the SDK (which
auto-bundles the `claude` CLI); the native extension imports it lazily.

### Added — MicroVM sandbox capability (local + dev tiers)

Secure, instant-boot compute environments for executing untrusted agent
Expand Down
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ members = [
"crates/coding-cli-vendor-antigravity",
"crates/coding-cli-harness",
"crates/coding-cli-harness-web",
"crates/agent-sdk-core",
"crates/agent-sdk-harness",
"crates/agent-sdk-harness-web",
"crates/sandbox-core",
"crates/sandbox-harness",
"crates/sandbox-tool",
Expand Down Expand Up @@ -125,6 +128,9 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
bytes = "1"

# Streams (broadcast→SSE, mpsc→Stream bridges)
tokio-stream = "0.1"

# Error handling
thiserror = "1"
anyhow = "1"
Expand Down Expand Up @@ -237,6 +243,9 @@ atomr-agents-coding-cli-vendor-codex = { path = "crates/coding-cli-vendor-cod
atomr-agents-coding-cli-vendor-antigravity = { path = "crates/coding-cli-vendor-antigravity", version = "0.19.0" }
atomr-agents-coding-cli-harness = { path = "crates/coding-cli-harness", version = "0.19.0" }
atomr-agents-coding-cli-harness-web = { path = "crates/coding-cli-harness-web", version = "0.19.0" }
atomr-agents-agent-sdk-core = { path = "crates/agent-sdk-core", version = "0.19.0" }
atomr-agents-agent-sdk-harness = { path = "crates/agent-sdk-harness", version = "0.19.0" }
atomr-agents-agent-sdk-harness-web = { path = "crates/agent-sdk-harness-web", version = "0.19.0" }
atomr-agents-sandbox-core = { path = "crates/sandbox-core", version = "0.19.0" }
atomr-agents-sandbox-harness = { path = "crates/sandbox-harness", version = "0.19.0" }
atomr-agents-sandbox-tool = { path = "crates/sandbox-tool", version = "0.19.0" }
Expand Down
28 changes: 28 additions & 0 deletions crates/agent-sdk-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "atomr-agents-agent-sdk-core"
version = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
homepage = { workspace = true }
authors = { workspace = true }
description = "Uniform contract for the Claude Agent SDK harness: config mirroring ClaudeAgentOptions, normalized message/result/event schema, and the pluggable AgentSdkBackend / AgentSdkSession trait seam."

keywords = ["agents", "claude", "harness", "atomr"]
categories = ["asynchronous"]
readme = "README.md"

[dependencies]
async-trait = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
tokio = { workspace = true }
futures = { workspace = true }
parking_lot = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["full", "test-util"] }
12 changes: 12 additions & 0 deletions crates/agent-sdk-core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# atomr-agents-agent-sdk-core

Contract layer for the **Claude Agent SDK** harness — config that mirrors the
SDK's `ClaudeAgentOptions`, a normalized message/result/event schema, and the
pluggable `AgentSdkBackend` / `AgentSdkSession` trait seam (with an in-memory
`MockBackend` for network-free tests).

Wraps Anthropic's programmable Claude Code agent
(`claude-agent-sdk` / `@anthropic-ai/claude-agent-sdk`). Distinct from the
server-side "Managed Agents" API.

See `docs/agent-sdk-harness.md` for the full design.
69 changes: 69 additions & 0 deletions crates/agent-sdk-core/src/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! The pluggable backend abstraction.
//!
//! [`AgentSdkBackend`] is the seam between the harness and whatever actually
//! drives the Claude Agent SDK. [`MockBackend`](crate::MockBackend) keeps
//! Rust tests network-free; `PythonAgentSdkBackend` (in `py-bindings`)
//! drives the real `claude-agent-sdk`. A future pure-Rust backend that
//! spawns the `claude` CLI directly can implement the same traits.

use std::pin::Pin;

use async_trait::async_trait;
use futures::Stream;

use crate::error::AgentSdkError;
use crate::message::AgentSdkMessage;
use crate::request::{AgentSessionId, QueryRequest, SessionSpec};

/// A stream of normalized messages. `Result<_, _>` items let a backend
/// surface a mid-stream error (e.g. a Python exception) as a stream error
/// rather than a silent truncation.
pub type MessageStream =
Pin<Box<dyn Stream<Item = Result<AgentSdkMessage, AgentSdkError>> + Send>>;

/// Drives the Claude Agent SDK — one-shot queries and stateful sessions.
#[async_trait]
pub trait AgentSdkBackend: Send + Sync {
/// Stable identifier used in logs (`mock`, `python`, `claude-cli`).
fn name(&self) -> &str;

/// Whether this backend is usable on the current host (SDK installed,
/// `claude` CLI present, credentials configured…).
async fn available(&self) -> bool;

/// Run a one-shot query (the SDK `query()` path), streaming normalized
/// messages to a terminal [`Result`](AgentSdkMessage::Result).
async fn query(&self, req: QueryRequest) -> Result<MessageStream, AgentSdkError>;

/// Open a stateful interactive session (the SDK `ClaudeSDKClient` path).
async fn create_session(
&self,
spec: SessionSpec,
) -> Result<Box<dyn AgentSdkSession>, AgentSdkError>;
}

/// A live interactive session. Maps 1:1 onto `ClaudeSDKClient`.
#[async_trait]
pub trait AgentSdkSession: Send + Sync {
/// The opaque session handle (registry / actor key).
fn session_id(&self) -> &AgentSessionId;

/// Send a user prompt (`ClaudeSDKClient.query()`).
async fn send(&self, prompt: String) -> Result<(), AgentSdkError>;

/// Stream the agent's response for the current turn
/// (`receive_response()`), ending at a terminal result.
async fn receive(&self) -> Result<MessageStream, AgentSdkError>;

/// Interrupt the in-flight turn.
async fn interrupt(&self) -> Result<(), AgentSdkError>;

/// Change permission mode mid-conversation.
async fn set_permission_mode(&self, mode: String) -> Result<(), AgentSdkError>;

/// Change model mid-conversation.
async fn set_model(&self, model: String) -> Result<(), AgentSdkError>;

/// Tear down the session and its `claude` subprocess.
async fn close(&self) -> Result<(), AgentSdkError>;
}
Loading
Loading