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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
working-directory: python
- name: Type-check samples
run: |
for sample in quickstart multi-turn-conversation streaming-chat code-review tool-permissions ask-user-question model-selection hooks custom-tools subagents; do
uv run mypy "$sample/main.py"
for sample in quickstart multi-turn-conversation streaming-chat code-review tool-permissions ask-user-question model-selection hooks custom-tools subagents external-session-storage; do
uv run mypy "$sample"
done
working-directory: python
18 changes: 10 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ Use an existing sample (for example, `quickstart`) as a template, then register
the new sample everywhere it needs to appear. Replace `<sample>` with your
kebab-case sample name.

1. **Create the TypeScript sample** at `typescript/<sample>/` with `index.ts`,
`README.md`, and `package.json`. Copy `package.json` from an existing sample
and update the `name` field to `@qoder-samples/typescript-<sample>`.
2. **Create the Python sample** at `python/<sample>/` with `main.py`,
`README.md`, and `requirements.txt`.
1. **Create the TypeScript sample** at `typescript/<sample>/` with one or more
runnable `.ts` entry files, `README.md`, and `package.json`. Copy
`package.json` from an existing sample and update the `name` field to
`@qoder-samples/typescript-<sample>`.
2. **Create the Python sample** at `python/<sample>/` with one or more runnable
`.py` entry files, `README.md`, and `requirements.txt`.
3. **Register the TypeScript workspace:** add `<sample>` to the `workspaces`
array in `typescript/package.json`.
4. **Register the Python type check:** add `<sample>` to the `for sample in ...`
Expand All @@ -59,7 +60,8 @@ kebab-case sample name.
in `README.zh-CN.md`.

Keep the TypeScript and Python versions behaviorally equivalent so readers can
switch between languages.
switch between languages. When one concept has multiple independently copyable
variants, keep them in one sample directory and list every entry in its README.

## Running the checks

Expand All @@ -69,8 +71,8 @@ request:
```bash
cd typescript && npm ci && npm run check --workspaces
cd ../python && uv sync && uv run ruff check . && uv run ruff format --check .
for sample in quickstart multi-turn-conversation streaming-chat code-review tool-permissions ask-user-question model-selection hooks custom-tools subagents; do
uv run mypy "$sample/main.py"
for sample in quickstart multi-turn-conversation streaming-chat code-review tool-permissions ask-user-question model-selection hooks custom-tools subagents external-session-storage; do
uv run mypy "$sample"
done
```

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ Every sample is available in TypeScript and Python.
| Hooks | Add lifecycle observation, context injection, and tool policy | [Open](typescript/hooks) | [Open](python/hooks) |
| Custom tools | Expose application functions as in-process MCP tools | [Open](typescript/custom-tools) | [Open](python/custom-tools) |
| Subagents | Delegate a task to specialized SDK-defined agents | [Open](typescript/subagents) | [Open](python/subagents) |
| External session storage | Resume a session on another runtime through Redis or PostgreSQL | [Open](typescript/external-session-storage) | [Open](python/external-session-storage) |

## Setup

These samples read a Personal Access Token from the environment. See
[SDK Authentication](https://docs.qoder.com/en/cli/sdk/authentication) for the
Unless a sample README says otherwise, samples read a Personal Access Token
from the environment. See
[SDK Authentication](https://docs.qoder.com/en/cli/sdk/authentication) for
setup and other supported authentication methods.

```bash
Expand All @@ -45,10 +47,10 @@ commands.
The sample manifests declare compatible SDK version ranges, while the
repository lockfiles record the exact versions used by CI.

Last verified on July 20, 2026:
Last verified on July 29, 2026:

- TypeScript SDK 1.0.15
- Python SDK 1.0.9
- TypeScript SDK 1.0.16
- Python SDK 1.0.10

## License and terms

Expand Down
10 changes: 6 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ TypeScript 和 Python 实现。
| Hooks | 添加生命周期观测、上下文注入和工具策略 | [查看](typescript/hooks) | [查看](python/hooks) |
| Custom tools | 将应用函数注册为进程内 MCP 工具 | [查看](typescript/custom-tools) | [查看](python/custom-tools) |
| Subagents | 将任务委派给 SDK 定义的专业子 Agent | [查看](typescript/subagents) | [查看](python/subagents) |
| External session storage | 通过 Redis 或 PostgreSQL 在另一个运行实例上恢复会话 | [查看](typescript/external-session-storage) | [查看](python/external-session-storage) |

## 准备工作

这些示例从环境变量读取 Personal Access Token。认证配置和其他可用认证方式见
除非示例 README 另有说明,否则示例从环境变量读取 Personal Access Token。认证
配置和其他可用认证方式见
[SDK 认证文档](https://docs.qoder.com/en/cli/sdk/authentication)。

```bash
Expand All @@ -36,10 +38,10 @@ TypeScript 需要 Node.js 18 或更高版本,Python 需要 Python 3.10 或更

各示例的依赖清单声明了兼容的 SDK 版本范围,仓库锁文件则记录了 CI 使用的确切版本。

最近一次验证时间:2026 年 7 月 20 日:
最近一次验证时间:2026 年 7 月 29 日:

- TypeScript SDK 1.0.15
- Python SDK 1.0.9
- TypeScript SDK 1.0.16
- Python SDK 1.0.10

## 许可与条款

Expand Down
5 changes: 3 additions & 2 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Python samples

Each directory is an independent Python project. Create a virtual environment
inside the sample directory, install its requirements, and run `main.py`.
Complete the repository-level setup before running a sample.
inside the sample directory, install its requirements, and run the entry file
documented in its README. Complete the repository-level setup before running a
sample.

The repository-level development environment uses `uv` only for maintainer
checks; users do not need `uv` to run a sample.
2 changes: 1 addition & 1 deletion python/ask-user-question/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
qoder-agent-sdk>=1.0.9,<2
qoder-agent-sdk>=1.0.10,<2
2 changes: 1 addition & 1 deletion python/code-review/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
qoder-agent-sdk>=1.0.9,<2
qoder-agent-sdk>=1.0.10,<2
2 changes: 1 addition & 1 deletion python/custom-tools/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
qoder-agent-sdk>=1.0.9,<2
qoder-agent-sdk>=1.0.10,<2
143 changes: 143 additions & 0 deletions python/external-session-storage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# External session storage with Redis or PostgreSQL

Copy and run one self-contained sample for the storage backend used by the
application:

| Sample | Backend | Run |
| --- | --- | --- |
| [`redis_sample.py`](redis_sample.py) | Redis lists and indexes | `python redis_sample.py /path/to/repository` |
| [`postgres_sample.py`](postgres_sample.py) | PostgreSQL JSONB rows | `python postgres_sample.py /path/to/repository` |

Each file contains both a complete `SessionStore` adapter and the same
end-to-end handoff:

```text
host A shared store host B
local transcript -- append --> Redis/PostgreSQL <-- load -- no transcript
| |
creates session resumes session
|
local transcript is deleted before host B starts
```

The application asks Host A to remember a unique marker, deletes its local
transcript, resumes the session through the external store, and verifies that
Host B returns the marker.

Complete the [repository setup](../../README.md#setup), then:

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

Both samples use the same absolute workspace path for Host A and Host B.
Session storage uses the canonical workspace path to identify the project.

## Redis sample

Start Redis:

```bash
docker run --rm --name qoder-session-redis \
-p 6379:6379 \
redis:7-alpine
```

In another terminal:

```bash
python redis_sample.py /path/to/repository
```

The default connection is `redis://127.0.0.1:6379`. Override it when needed:

```bash
export QODER_SAMPLE_REDIS_URL="redis://127.0.0.1:6379"
```

`redis_sample.py` is independently copyable. It needs `qoder-agent-sdk` and
`redis`, and stores:

```text
entries:<project>:<session>:main Redis list, append order
entries:<project>:<session>:child:* Redis list, append order
sessions:<project> sorted set, modification time
subkeys:<project>:<session> set of child transcript paths
```

Project keys, session IDs, and child paths are base64url-encoded before they
become Redis key segments.

## PostgreSQL sample

Start PostgreSQL:

```bash
docker run --rm --name qoder-session-postgres \
-e POSTGRES_USER=qoder \
-e POSTGRES_PASSWORD=qoder \
-e POSTGRES_DB=qoder_session_store \
-p 5432:5432 \
postgres:17-alpine
```

In another terminal:

```bash
python postgres_sample.py /path/to/repository
```

The default connection uses the development credentials from the Docker
command. Override it when needed:

```bash
export QODER_SAMPLE_POSTGRES_URL="postgresql://qoder:qoder@127.0.0.1:5432/qoder_session_store"
```

`postgres_sample.py` is independently copyable. It needs `qoder-agent-sdk` and
`asyncpg`. The adapter creates `qoder_session_entries`: each transcript entry
is one JSONB row, the increasing `seq` column preserves replay order, and the
`(project_key, session_id, subpath, seq)` index supports loading main and child
transcripts.

## SessionStore behavior

The SDK-facing application code needs only a store instance:

```python
options = QoderAgentOptions(
auth=access_token_from_env(),
cwd=workspace,
session_store=store,
resume=session_id,
)

async for message in query(prompt=prompt, options=options):
...
```

`append()` receives newly committed entries. `load()` returns the complete
history for one key in append order. `list_sessions()`, `list_subkeys()`, and
`delete()` enable latest-session resume, child-agent restoration, and deletion.

Each sample removes its local and external transcript data during cleanup.

## Production considerations

These adapters are readable reference implementations, not production-ready
database packages. Before deployment:

- isolate tenants in the Redis prefix or PostgreSQL schema
- define retention, backup, encryption, and access control
- serialize concurrent writers for the same session
- account for a failed `append()` being retried with the same entries
- monitor `SDKMirrorErrorMessage`; the conversation can succeed even when an
external write fails
- configure Redis eviction and TTL behavior for transcript retention
- size the PostgreSQL pool and schedule deletion of expired rows
- add backend-specific load, concurrency, failover, and cleanup tests

SessionStore contains transcript history only. It does not store credentials,
application settings, file checkpoints, or external tool side effects.
Loading
Loading