Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cacd09e
Add ai.harnesses.experimental and ai.workspaces.experimental
kajogo777 Sep 29, 2026
745f1d0
Track vercel-sandbox main now that interactive PTY is merged
kajogo777 Sep 29, 2026
4f3cd97
Keep named local ptys alive after their creator exits on Linux
kajogo777 Sep 29, 2026
6ff467c
CI: no-extras job, and claude-agent-sdk in the SDK compatibility job
kajogo777 Sep 29, 2026
31789e9
Fix explicit VercelSandbox credentials on vercel-sandbox 0.7
kajogo777 Sep 30, 2026
0de6144
afk: set up Vercel on first use, installable with uv tool install
kajogo777 Sep 30, 2026
59983cf
tty.bridge: detach on Ctrl-] in enhanced keyboard modes, one pty send…
kajogo777 Oct 1, 2026
b8536de
afk: bare afk on a terminal is an inline picker
kajogo777 Oct 1, 2026
122884c
VercelSandbox: write a process's stdin one writer at a time
kajogo777 Oct 1, 2026
48b35c1
afk: peek from the picker; an unreachable sandbox is listed, not a crash
kajogo777 Oct 1, 2026
484a49e
VercelSandbox: boot a new sandbox from a chosen image
kajogo777 Oct 2, 2026
8510f8b
Add .env to gitignore
kajogo777 Oct 2, 2026
78b9020
harnesses: a CLI started in a terminal shows as running, with its pid
kajogo777 Oct 2, 2026
def9414
afk: Enter enters a conversation here too; peek and stop one open els…
kajogo777 Oct 2, 2026
a8dafb5
afk: take over a session open here; peek says it follows, and stops w…
kajogo777 Oct 2, 2026
ba09247
pty holder: a program that ends at once still shows its output and st…
kajogo777 Oct 9, 2026
f184ecc
afk: tests patch HARNESSES in rows, where it lives
kajogo777 Oct 9, 2026
3bd4494
afk: attach steps into one pushed unattended
kajogo777 Oct 9, 2026
6e8b734
workspaces: resolve() for real paths; a sandbox spawn no longer races…
kajogo777 Oct 19, 2026
a14bc04
harnesses: turns settle right from any task; codex approvals stop blo…
kajogo777 Oct 19, 2026
71300f7
plans: the harness SDK fix plan, with what was measured along the way
kajogo777 Oct 19, 2026
0dce70c
harnesses, workspaces: codex results and listings, long protocol line…
kajogo777 Oct 19, 2026
5488641
examples: deepysec sandboxes outlive a run; afk pull keeps a script r…
kajogo777 Oct 19, 2026
f460bcf
plans: session 5, afk and deepysec run for real
kajogo777 Oct 19, 2026
cd955f2
VercelSandbox: close destroys; a stopped sandbox resumes; stop() and …
kajogo777 Oct 19, 2026
e454a52
harnesses: a sandbox's CLI check is remembered; a lost install answer…
kajogo777 Oct 19, 2026
0509bed
afk: snapshots, cost, a branch home, --prompt, open network, wait
kajogo777 Oct 19, 2026
8a73c2e
plans: the afk plan, what was measured, and how each item was verified
kajogo777 Oct 19, 2026
35d8ae1
pty holder: a closed program really ends, and leaves no directory behind
kajogo777 Oct 19, 2026
55d64dd
plans: the cleanup, and the pty holder leak it found
kajogo777 Oct 19, 2026
e2fe56d
afk: the first push's billing note, in two short lines
kajogo777 Oct 19, 2026
7f0d566
afk: a pushed conversation is listed by its title, not its id twice
kajogo777 Oct 19, 2026
542c915
afk: a running sandbox shows what it costs per hour, not a +
kajogo777 Oct 19, 2026
0d6a8f6
afk: the hourly rate in parentheses: $0.031 ($0.085/h)
kajogo777 Oct 19, 2026
f04a6de
harnesses: approvals run in their turn and show on its stream; a turn…
kajogo777 Oct 20, 2026
1b1cb01
docs, examples: a harness is an agent; combine it, nest it, loop arou…
kajogo777 Oct 20, 2026
42b082a
plans: fit the harness SDK into ai — framing, levers, contract, what …
kajogo777 Oct 20, 2026
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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ jobs:
- sdk-versions: minimum
openai: openai==2.34.0
anthropic: anthropic==0.83.0
claude-agent-sdk: claude-agent-sdk==0.2.144
- sdk-versions: latest
openai: openai>=2.34.0,<4.0.0
anthropic: anthropic>=0.83.0,<2.0.0
claude-agent-sdk: claude-agent-sdk>=0.2.144,<0.3
name: Provider SDKs (${{ matrix.sdk-versions }})
steps:
- uses: actions/checkout@v4
Expand All @@ -64,10 +66,35 @@ jobs:
uv pip install --upgrade
'${{ matrix.openai }}'
'${{ matrix.anthropic }}'
'${{ matrix.claude-agent-sdk }}'

- run: uv run --no-sync mypy src tests
- run: uv run --no-sync ty check
- run: >-
uv run --no-sync pytest
tests/providers/openai
tests/providers/anthropic
tests/harnesses

no-extras:
# `pip install ai` with no extras: the harness and sandbox pieces must
# import, and fail with InstallationError when built, not on first use.
runs-on: ubuntu-latest
name: Without optional extras
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- uses: astral-sh/setup-uv@v5

- run: uv sync --no-default-groups

- run: uv pip install pytest

- run: >-
uv run --no-sync pytest
tests/harnesses/experimental/test_extras.py
tests/workspaces/experimental/test_extras.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ __marimo__/

.claude/
.codex
.env*
112 changes: 112 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,115 @@
## Harnesses and workspaces

`ai.harnesses.experimental` drives coding-agent CLIs. Adapters live in
`src/ai/harnesses/experimental/_adapters/` (`claude.py` on
`claude-agent-sdk`, `codex.py` on `codex app-server`). Workspaces live in
`src/ai/workspaces/experimental/` (`_local.py`, `_sandbox.py`).

Rules that apply to both:

- **Layering.** Caller, then `Harness`, then adapter, then workspace. Each
layer knows only the one below. A workspace never imports harness names
and never learns a CLI's dialect. A new CLI touches one adapter; a new
place to run touches one workspace.
- **Capability honesty.** Every `Capabilities` field is a claim you watched
be true. When a capability is absent, raise `UnsupportedError`. Never a
silent no-op.
- **Translate errors at the boundary.** Callers never catch a CLI library's
or provider's exceptions. Map them to `ai.harnesses.experimental.errors`
or `ai.workspaces.experimental.errors`.

### Adding a harness

1. Pick the CLI protocol first. It must offer, in one mode: a long-lived
process or resume by id, input while a turn runs (steer), an interrupt,
and a way to ask the client about tool calls. Codex had to move from
`codex exec --json` to `app-server` for this.
2. Implement the `Adapter` protocol in `_adapters/base.py`. `new_session()`
returns the CLI's own id, never one you mint.
3. Never spawn a process yourself. Call `workspace.spawn(argv)`. If the CLI
library insists on spawning, give it a transport backed by the workspace
(see `_adapters/remote_cli.py`).
4. Yield `ai.types.events` only, ending with `StreamEnd`. Text, reasoning,
and tool calls need Start / Delta / End triples. Emit tool arguments as a
`ToolDelta`, even when the call arrives complete. Tool results are
`ToolCallResult` events.
5. `approve is None` means approve everything. Answer the CLI's approval
requests yourself; never fall back to asking a terminal that is not there.
Exceptions inside a permission callback do not reach the caller: park them
on the adapter and re-raise from `turn()`.
6. Hold the workspace session lock (`_session_lock.SessionLock`) for every
conversation you open, so a second writer gets `SessionBusyError` on every
CLI.
7. Store `history=` natively with the rules in `_handoff.py`: text verbatim,
tool calls with their original names, reasoning as a note or dropped with
a warning. Never drop context silently.
8. Spell the gateway in the adapter (`gateway_env`, `gateway_config`). When
`workspace.injects_credentials_for(gateway.host)`, hand the CLI
`gateway.brokered()`.
9. Add a factory in `_factories.py` (and to `BUILDERS`), then add the kind to
`SPECS` and `HARNESS_CLI` in `tests/harnesses/experimental/conftest.py`.
That runs the whole e2e suite against the new adapter.

### Adding a workspace

1. Subclass `Workspace` from `_base.py`. Set `kind`, `owner` (`"provider"`
only for a box the SDK may install software into), and `duplex_spawn`.
`env` belongs to the work, not the agent.
2. Do not override `contains()`. It is pure-path on purpose: a remote path
does not exist on this machine.
3. Move files through `read_text`, `write_text`, `_copy_in`, and `_copy_out`.
Filter both directions with `walk_uploadable` and `copy_tree`. Batch
uploads; round trips are the cost on a remote box.
4. Map failures: a program that cannot run is `exit_code=127` from `exec`
and `WorkspaceError` from `spawn`; a missing file is `FileNotFoundError`;
a workspace that no longer exists is `WorkspaceGoneError`.
5. `Process.terminate()` returns only when the program is gone, and raises
when it could not make that true.
6. If the workspace can outlive the client, support reconnect by `name=`,
carry that identity in `coords`, and add a `keep` flag. Live state is
found by asking a harness inside the workspace, never by the workspace.
7. For ptys, reuse `_pty_holder.py` and `_pty.FramedPty` over your byte
channel. Never name a module `pty.py`.
8. Export it from `src/ai/workspaces/experimental/__init__.py`, and add it to
the `any_workspace` fixtures in `tests/workspaces/experimental/conftest.py`
and `tests/harnesses/experimental/conftest.py` as
`pytest.param("yours", marks=pytest.mark.yours)`. It must present the same
`project` the local branch does.

### Tests

Live tests drive real CLIs and real sandboxes. No fakes, no recorded doubles.
A missing CLI skips; it never passes silently. Markers:

- `live`: drives a real local `claude` or `codex`.
- `sandbox`: needs Vercel Sandbox credentials.
- `slow`: minutes, not seconds.

```bash
uv run pytest # offline (default)
uv run pytest tests/harnesses tests/workspaces -m "not sandbox" # plus local CLIs
uv run pytest tests/harnesses tests/workspaces -m "" # everything
```

Plain `uv run pytest` deselects `live` and `sandbox` (see `addopts` in
`pyproject.toml`); passing `-m` replaces that filter.

The live suite needs `claude` and `codex` CLIs that can reach a model: a
login, or the gateway variables in `.env.local`. The sandbox suite
also needs Vercel credentials and `AI_GATEWAY_API_KEY` in a `.env.local` at
the repo root (`vercel link`, then `vercel env pull`). Every test is capped at
300 seconds.

When writing tests, never branch on native tool names, wait on events instead
of sleeping, assert exact answers, and put anything on the control surface
on `any_harness` so it runs on both harnesses and both workspaces.

Run the harness examples with:

```bash
uv run examples/.test_scripts/run-examples.py --harnesses
```

## Releasing

Every PR needs to be labeled as `breaking`, `feature`, `fix`, or
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ official SDK lazily. Vercel OIDC for AI Gateway also uses an optional extra:
uv add "ai[openai]" # OpenAI-compatible providers
uv add "ai[anthropic]" # Anthropic-compatible providers
uv add "ai[vercel]" # Vercel OIDC for AI Gateway
uv add "ai[claude-code]" # Claude Code harness (experimental)
uv add "ai[sandbox]" # Vercel Sandbox workspaces (experimental)
```

```python
Expand Down Expand Up @@ -156,6 +158,24 @@ approval = await ai.hook(
ai.resolve_hook("approve_send_email", {"granted": True, "reason": "approved"})
```

## Harnesses (experimental)

Harnesses drive coding-agent CLIs you already have, Claude Code and Codex,
on this machine or in a Vercel Sandbox. A turn streams the same `ai.events`
as an agent:

```python
from ai.harnesses import experimental as harnesses
from ai.workspaces import experimental as workspaces

async with harnesses.claude_code(workspace=workspaces.Local(".")) as harness:
result = await harness.run("What does this repo do?")
```

This API is experimental and may change or be removed. See
[Harnesses](https://ai-python.dev/docs/basics/harnesses) and
[Workspaces](https://ai-python.dev/docs/basics/workspaces).

## Examples

Focused samples live in category directories under `examples/`.
Expand All @@ -164,6 +184,8 @@ Focused samples live in category directories under `examples/`.
- `examples/media/` - image, video, speech, transcription, embeddings,
reranking, and multimodal input/output
- `examples/models/` - streaming, structured output, and provider examples
- `examples/harnesses/` - driving Claude Code and Codex, locally and in a
sandbox (experimental)
- `examples/apps/` - end-to-end demos

End-to-end demos:
Expand All @@ -173,3 +195,7 @@ End-to-end demos:
- `examples/apps/durable_agent_temporal/` - durable agent with Temporal
- `examples/apps/durable_agent_workflows/` - durable agent with Workflows
- `examples/apps/slack_agent/` - Slack agent
- `examples/apps/afk/` - move a coding-agent conversation into a sandbox and
back (experimental harnesses)
- `examples/apps/deepysec/` - security-scanning pipeline on harnesses
(experimental)
Loading