Metadata-Version: 2.4
Name: apto-code-sdk
Version: 0.1.0
Summary: A dependency-free Python SDK that drives the APTO Code CLI over stream-json stdio, mirroring the Claude Agent SDK interface.
Project-URL: Homepage, https://github.com/Northwestern-CSSI/proj-apto-code
Project-URL: Repository, https://github.com/Northwestern-CSSI/proj-apto-code
Project-URL: Issues, https://github.com/Northwestern-CSSI/proj-apto-code/issues
Author: Northwestern-CSSI
License: MIT
License-File: LICENSE
Keywords: agent,apto,apto-code,claude-code,cli,llm,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Description-Content-Type: text/markdown

# apto-code-sdk

A dependency-free Python SDK for the **APTO Code CLI** (a Claude-Code-style
coding agent). It spawns the `apto` CLI over `stream-json` stdio and yields
**typed messages**, mirroring the interface of the official
[Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python)
(`query()` async iterator, an options dataclass, typed message objects).

It exists because the official Claude Agent SDK only locates the `claude` binary
on `PATH` and offers no way to point at a forked, locally-served CLI. This SDK
drives **any** apto-compatible CLI you name, wrapping the same subprocess +
process-group hygiene the APTO research driver uses by hand.

## Install

```bash
pip install apto-code-sdk
```

Zero runtime dependencies (Python ≥ 3.10, stdlib only). You must have the `apto`
CLI installed separately (this SDK drives it; it does not bundle it).

## Usage

```python
import asyncio
from apto_code_sdk import query, AptoCodeOptions, AssistantMessage, ResultMessage, TextBlock

async def main():
    opts = AptoCodeOptions(
        binary="apto",                       # name on PATH, or an absolute path
        permission_mode="bypassPermissions", # non-interactive one-shot runs need this
        model="deepseek-v4-flash",
    )
    async for message in query(prompt="List three prime numbers.", options=opts):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, TextBlock):
                    print(block.text)
        elif isinstance(message, ResultMessage):
            print("turns:", message.num_turns, "cost:", message.total_cost_usd)

asyncio.run(main())
```

For non-async callers (e.g. a thread-pool driver) there is a blocking collector:

```python
from apto_code_sdk import query_sync
messages = query_sync(prompt="Say hi.", options=opts)
```

## Message types

`query()` yields, in stream order: `SystemMessage` (e.g. the `init` event),
`AssistantMessage` (with `TextBlock` / `ThinkingBlock` / `ToolUseBlock` content),
`UserMessage` (tool results fed back), and finally one `ResultMessage` — whose
arrival ends the iteration. Every message also carries `.raw` (the original
decoded dict) so a field this version does not model yet is never lost.

The SDK reports **what the CLI did**; it does not judge whether the run
"succeeded" for you — that contract belongs to your caller.

## Reasoning models & thinking blocks

Reasoning models (e.g. DeepSeek-V4) emit chain-of-thought that is meant to be
**ephemeral** — separated from the answer and dropped between turns. When the
deployment separates it (a server reasoning parser plus a router that maps the
model's `reasoning_content` to a thinking block), the SDK surfaces it as a
`ThinkingBlock` and the `TextBlock` / `ResultMessage.result` stay clean.

If instead you see the CoT **inline** in a `TextBlock` (e.g.
`…reasoning</think>answer`), the chain-of-thought is not being separated
upstream — fix it in the model server / router config, not here. The SDK is a
faithful transport: it reports the content the CLI produced and never strips
text. Leaving leaked CoT inline is harmful to multi-turn agents, since it
pollutes the context fed back into later turns.

## `AptoCodeOptions`

| field | maps to | notes |
|---|---|---|
| `binary` | argv[0] | `"apto"` (PATH) or an absolute path |
| `cwd`, `env` | subprocess | `env` is merged over `os.environ` |
| `permission_mode` | `--permission-mode` | `default` / `acceptEdits` / `plan` / `bypassPermissions` |
| `allowed_tools` / `disallowed_tools` | `--allowedTools` / `--disallowedTools` | comma-joined |
| `system_prompt` / `append_system_prompt` | `--system-prompt` / `--append-system-prompt` | |
| `model` | `--model` | |
| `max_turns` | `--max-turns` | |
| `add_dirs` | `--add-dir` (repeated) | |
| `settings` / `setting_sources` | `--settings` / `--setting-sources` | |
| `session_persistence` | `--no-session-persistence` when `False` | default `False` (one-shot) |
| `include_partial_messages` | `--include-partial-messages` | |
| `extra_args` | any flag | escape hatch: `{"flag": "value" or None}` |

Runtime knobs (SDK-side, not CLI flags): `step_timeout` (wall-clock seconds →
kills the process group on expiry), `post_result_grace` (kill a process that
lingers past its terminal result — e.g. a sealed container), `ready_check`
(a callable; return `True` to terminate early once a required output appears),
`log_path` (tee the raw stream-json transcript for provenance).

## License

MIT.
