Metadata-Version: 2.4
Name: qoder-agent-sdk
Version: 1.0.12
Summary: Python SDK for Qoder Agent
Author: Qoder
License: Copyright (c) 2026 Qoder
        
        Use of this software is governed by the Qoder Product Service Terms:
        
        https://qoder.com/product-service
        
        By installing or using this package, you agree to those terms.
License-File: LICENSE
Keywords: agent,ai,qoder,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: anyio>=4.0.0
Requires-Dist: mcp<2.0.0,>=1.0.0
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.11'
Provides-Extra: dev
Requires-Dist: anyio[trio]>=4.0.0; extra == 'dev'
Requires-Dist: mypy>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.20.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Requires-Dist: tomli>=2.0.0; (python_version < '3.11') and extra == 'dev'
Provides-Extra: examples
Requires-Dist: asyncpg<1,>=0.27.0; extra == 'examples'
Requires-Dist: redis<9,>=5.0.0; extra == 'examples'
Description-Content-Type: text/markdown

# Qoder Agent SDK for Python

Python SDK for building applications on top of Qoder Agent.

The SDK starts `qodercli` for you, streams agent messages back to Python, and
lets your application configure tools, permissions, working directories, MCP
servers, hooks, and interactive sessions.

## Installation

```bash
pip install qoder-agent-sdk
```

Prerequisites:

- Python 3.10+
- A Qoder account or another authentication method supported by your host
  application

## CLI Behavior

Published platform wheels include a bundled `qodercli`, so a separate CLI
installation is not required for normal SDK use. If you prefer to use a
system-wide CLI or a pinned local build, pass `QoderAgentOptions(cli_path=...)`.

## Authentication

Every SDK query needs an explicit authentication option.

| Authentication method | Identity | Use case |
| --- | --- | --- |
| Personal Access Token (PAT) | A Qoder user | Automation that needs the user's permissions and data |
| Service Account | An organization workload | Services and jobs that should not depend on a personal account |
| Local `qodercli` session | The signed-in user | Interactive development on a workstation |

For a PAT, generate a token at
[qoder.com/account/integrations](https://qoder.com/account/integrations), store
it in a secret manager, and expose it through the default environment variable:

```bash
export QODER_PERSONAL_ACCESS_TOKEN=your-token
```

```python
from qoder_agent_sdk import QoderAgentOptions, access_token_from_env

options = QoderAgentOptions(auth=access_token_from_env())
```

For a Service Account, read the key from your secret manager and pass it
directly to the SDK:

```python
from qoder_agent_sdk import QoderAgentOptions, service_account

# Get the Service Account key from the host's secret manager adapter.
service_account_key = read_secret("qoder-service-account-key")
options = QoderAgentOptions(
    auth=service_account(service_account_key=service_account_key)
)
```

The SDK and CLI obtain and refresh short-lived Service Account tokens for this
authentication method. A host can retain the Service Account key and use
`service_account(fetch_service_account_token=...)` to obtain and refresh
short-lived SATs for qodercli. See the
[host callback example](examples/service_account_token.py) for a complete Token
exchange and query. To reuse a signed-in developer
workstation, use `qodercli_auth()`. See the
[SDK authentication guide](docs/public/en/cli/sdk/python/authentication.mdx) for
complete setup instructions and security guidance.

## Quick Start

```python
import anyio
from qoder_agent_sdk import QoderAgentOptions, qodercli_auth, query


async def main() -> None:
    options = QoderAgentOptions(auth=qodercli_auth())

    async for message in query(
        prompt="What is 2 + 2?",
        options=options,
    ):
        print(message)


anyio.run(main)
```

## Basic Usage

`query()` runs a single SDK query and returns an async iterator of response
messages.

```python
from qoder_agent_sdk import (
    AssistantMessage,
    QoderAgentOptions,
    TextBlock,
    qodercli_auth,
    query,
)

options = QoderAgentOptions(
    auth=qodercli_auth(),
    system_prompt="You are a helpful assistant.",
    max_turns=1,
)

async for message in query(prompt="Explain this repository", options=options):
    if isinstance(message, AssistantMessage):
        for block in message.content:
            if isinstance(block, TextBlock):
                print(block.text)
```

## Tools and Permissions

Qoder Agent can use tools such as file reads, file edits, shell commands, and
MCP tools. `allowed_tools` is an approval allowlist: listed tools are
auto-approved, while unlisted tools continue through `permission_mode` and
`can_use_tool` for a decision. It does not remove tools from the agent's
available toolset. To block tools, use `disallowed_tools`.

```python
from qoder_agent_sdk import QoderAgentOptions, qodercli_auth, query

options = QoderAgentOptions(
    auth=qodercli_auth(),
    allowed_tools=["Read", "Edit"],
    disallowed_tools=["Bash"],
    permission_mode="acceptEdits",
)

async for message in query(
    prompt="Update the README introduction.",
    options=options,
):
    print(message)
```

For application-specific approval flows, provide `can_use_tool`:

```python
from qoder_agent_sdk import (
    PermissionResultAllow,
    PermissionResultDeny,
    QoderAgentOptions,
    ToolPermissionContext,
    qodercli_auth,
)


async def can_use_tool(
    tool_name: str,
    tool_input: dict,
    context: ToolPermissionContext,
):
    if tool_name == "Bash":
        return PermissionResultDeny(message="Shell commands are disabled here.")
    return PermissionResultAllow()


options = QoderAgentOptions(
    auth=qodercli_auth(),
    can_use_tool=can_use_tool,
)
```

## Working Directory

Use `cwd` to run the agent in a specific project directory:

```python
from pathlib import Path

from qoder_agent_sdk import QoderAgentOptions, qodercli_auth

options = QoderAgentOptions(
    auth=qodercli_auth(),
    cwd=Path("/path/to/project"),
)
```

## Interactive Sessions

Use `QoderSDKClient` when you need a long-lived, bidirectional session instead
of a single `query()` call.

```python
from qoder_agent_sdk import QoderAgentOptions, QoderSDKClient, qodercli_auth

options = QoderAgentOptions(auth=qodercli_auth())

async with QoderSDKClient(options=options) as client:
    await client.query("Inspect this project and summarize the main modules.")

    async for message in client.receive_response():
        print(message)
```

`QoderSDKClient` is useful for chat interfaces, follow-up prompts, interrupts,
runtime permission changes, MCP server management, and other workflows that need
state across multiple turns.

Use message priority to steer a turn that is already running:

```python
await client.query(
    "Stop the current direction and inspect the failing tests first.",
    priority="now",
)
```

`priority="now"` stops the current response and handles the message
immediately. `priority="next"` is the default and uses the next suitable
point. `priority="later"` waits until the current response finishes.
`should_query=False` adds the message to the conversation without starting a
response by itself; its processing time still follows `priority`.

Assign a session-unique `message_uuid` to messages that need tracking or
cancellation, and do not reuse UUIDs within a session.
`await client.interrupt()` stops the current response and returns `None`.
`await client.cancel_async_message(message_uuid)` returns `True` when the
queued message is cancelled and `False` when it can no longer be cancelled.

## External Session Storage

Use `session_store` when a host needs durable transcripts outside the local
machine. The SDK mirrors entries after qodercli commits them locally. A later
process can restore the same session before qodercli starts:

```text
qodercli commit -> SDK append(key, entries) -> external store
external store -> SDK load(key) -> temporary QODER_CONFIG_DIR -> qodercli resume
```

```python
from qoder_agent_sdk import (
    InMemorySessionStore,
    QoderAgentOptions,
    qodercli_auth,
    query,
)

session_store = InMemorySessionStore()

options = QoderAgentOptions(
    auth=qodercli_auth(),
    cwd="/path/to/project",
    session_store=session_store,
)

async for message in query(prompt="Inspect this project.", options=options):
    print(message)

resume_options = QoderAgentOptions(
    auth=qodercli_auth(),
    cwd="/path/to/project",
    resume="11111111-1111-4111-8111-111111111111",
    session_store=session_store,
)
```

Every store implements async `append(key, entries)` and `load(key)`. Implement
`list_sessions(project_key)` for `continue_conversation=True` and session
listing, `list_subkeys(key)` to restore child-agent transcripts, and
`delete(key)` for deletion. Entries are opaque JSON dictionaries and must remain
in append order. A child transcript uses an opaque `subpath` such as
`subagents/agent-<id>`; the key does not include the on-disk `.jsonl`
extension.

When `load()` returns `None` or an empty list for an explicit `resume`, the SDK
falls back to the same local session ID. Missing or empty child transcripts do
not prevent restoration of the main session, and unsafe subpaths are ignored.

`session_store_flush="batched"` is the default. `"eager"` starts each append
without waiting for the result boundary. Final append failures are emitted as
non-fatal `SDKMirrorErrorMessage` values. `load_timeout_ms` defaults to 60,000
ms. Session storage cannot be combined with file checkpointing, a custom
transport, or the Cloud Agent runtime. It requires the built-in subprocess
transport.

The existing local session helpers remain synchronous. External stores use the
async helpers `list_sessions_from_store`, `get_session_info_from_store`,
`get_session_messages_from_store`, `rename_session_via_store`,
`tag_session_via_store`, `fork_session_via_store`, and
`delete_session_via_store`. Local and external child-agent transcripts are
available through `list_subagents` / `get_subagent_messages` and
`list_subagents_from_store` / `get_subagent_messages_from_store`. Use
`import_session_to_store` to copy an existing local main transcript,
child-agent transcripts, and metadata into a store.

### Production stores

The SDK exports the `SessionStore` protocol but does not ship a
production-ready external storage implementation. Implement the protocol
against shared storage operated by your application, then validate its
append/load ordering, project isolation, subkey handling, and deletion behavior
with `run_session_store_conformance`.

## Custom Tools

You can expose Python functions to Qoder Agent as in-process SDK MCP servers.
This avoids managing a separate MCP subprocess for simple application-local
tools.

```python
from qoder_agent_sdk import (
    QoderAgentOptions,
    QoderSDKClient,
    create_sdk_mcp_server,
    qodercli_auth,
    tool,
)


@tool("greet", "Greet a user", {"name": str})
async def greet_user(args):
    return {
        "content": [
            {"type": "text", "text": f"Hello, {args['name']}!"}
        ]
    }


server = create_sdk_mcp_server(
    name="my-tools",
    version="1.0.0",
    tools=[greet_user],
)

options = QoderAgentOptions(
    auth=qodercli_auth(),
    mcp_servers={"tools": server},
    allowed_tools=["mcp__tools__greet"],
)

async with QoderSDKClient(options=options) as client:
    await client.query("Greet Alice.")
    async for message in client.receive_response():
        print(message)
```

## Hooks

Hooks are deterministic Python callbacks invoked at specific points in the
agent loop. They are useful for validation, policy checks, logging, and
application-specific feedback.

```python
from qoder_agent_sdk import HookMatcher, QoderAgentOptions, qodercli_auth


async def block_script(input_data, tool_use_id, context):
    if input_data["tool_name"] != "Bash":
        return {}

    command = input_data["tool_input"].get("command", "")
    if "./deploy.sh" in command:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": "Deployment scripts require review.",
            }
        }
    return {}


options = QoderAgentOptions(
    auth=qodercli_auth(),
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[block_script]),
        ],
    },
)
```

## Error Handling

```python
from qoder_agent_sdk import (
    CLIConnectionError,
    CLIJSONDecodeError,
    CLINotFoundError,
    ProcessError,
    QoderAgentOptions,
    QoderSDKError,
    qodercli_auth,
    query,
)

try:
    async for message in query(
        prompt="Hello Qoder",
        options=QoderAgentOptions(auth=qodercli_auth()),
    ):
        print(message)
except CLINotFoundError:
    print("qodercli was not found. Install a platform wheel or set cli_path.")
except CLIConnectionError as exc:
    print(f"Connection failed: {exc}")
except ProcessError as exc:
    print(f"qodercli exited with code {exc.exit_code}")
except CLIJSONDecodeError as exc:
    print(f"Could not parse qodercli output: {exc}")
except QoderSDKError as exc:
    print(f"SDK error: {exc}")
```

## License and Terms

Copyright (c) 2026 Qoder

Use of this software is governed by the Qoder Product Service Terms:

https://qoder.com/product-service

By installing or using this package, you agree to those terms.
