Metadata-Version: 2.4
Name: qoder-agent-sdk
Version: 1.0.10
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.

To reuse the local `qodercli` login state:

```python
from qoder_agent_sdk import QoderAgentOptions, qodercli_auth

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

To authenticate with a personal access token:

Generate a Personal Access Token at
[qoder.com/account/integrations](https://qoder.com/account/integrations):

1. Sign in to your Qoder account.
2. Open the integrations page.
3. Create a new PAT, choosing the expiry and scopes you need.
4. Copy the token immediately. The value cannot be retrieved again after the
   page is closed.

Use separate tokens for local scripts, CI, and production services when
possible, so each environment can be revoked independently. Do not hard-code
tokens in source code.

```python
from qoder_agent_sdk import QoderAgentOptions, access_token_from_env

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

`access_token_from_env()` reads `QODER_PERSONAL_ACCESS_TOKEN` by default.

## 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(
    "先停止当前方向，优先检查失败的测试。",
    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.
