From 1c6ed51f05ad79827021452fe843ce1adfc1828d Mon Sep 17 00:00:00 2001 From: xieningjun <779016161@qq.com> Date: Wed, 15 Jul 2026 01:33:36 +0800 Subject: [PATCH 1/2] Expand package READMEs in English and Chinese --- CHANGELOG.md | 2 + python/kernel/README.md | 312 ++++++++++++++++++++++++++++++-- python/providers/README.md | 332 +++++++++++++++++++++++++++++++++- python/toolkit/README.md | 276 +++++++++++++++++++++++++++- tests/test_release_process.py | 23 +++ 5 files changed, 918 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f827fff..865a71a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed +- Expanded each distribution README into a standalone PyPI guide while keeping + every page within its declared dependency boundary. - Added explicit code ownership and weekly dependency update pull requests for the uv workspace and GitHub Actions. - Made package-index publication fully unattended and added checksum-verified diff --git a/python/kernel/README.md b/python/kernel/README.md index 4640c9c..de5591d 100644 --- a/python/kernel/README.md +++ b/python/kernel/README.md @@ -1,24 +1,314 @@ # jharness-kernel -`jharness-kernel` is the dependency-free, model-neutral runtime distribution. It owns -immutable run state, `Runtime` and `Invocation`, model and tool protocols, -control, events, limits, atomic checkpoints, repository ports, and explicit -wire codecs. +**English** | [简体中文](#简体中文) -Stable runtime concepts are imported from `jharness.kernel`. Portable codecs and -diagnostics are opt-in namespaces: +`jharness-kernel` is the dependency-free, model-neutral execution core for +JHarness. It provides immutable run state, explicit lifecycle transitions, +atomic checkpoints, bounded execution, live events, durable wire codecs, and +opt-in trace verification. + +The package targets Python 3.11 and newer and is fully typed. + +## Install ```bash uv add jharness-kernel ``` +The distribution installs the implicit namespace `jharness` and is imported +through `jharness.kernel`. It has no required third-party runtime dependencies. + +## What It Provides + +- `Runtime`, an immutable configuration object that creates single-use + `Invocation` executions; +- flat run states: `Planning`, `ToolsPending`, `Suspended`, `Completed`, + `Failed`, and `Limited`; +- model-neutral messages, content parts, tool calls, model requests, model + responses, and streaming deltas; +- atomic `Checkpoint` values containing both the next snapshot and its + semantic fact; +- bounded concurrency, monotonic deadlines, run limits, approvals, history + reduction, and repository ports; +- ordered live events separated from durable committed state; +- explicit JSON-compatible codecs under `jharness.kernel.wire`; +- trace construction and verification under `jharness.kernel.diagnostics`. + +## Quick Start + +Supply an object that implements the async `Model` protocol, create a runtime, +and await the invocation result: + +```python +import asyncio + +from jharness.kernel import ( + Completed, + ContentPart, + DeltaSink, + Message, + ModelCapabilities, + ModelRequest, + ModelResponse, + RunContext, + Runtime, +) + + +class HelloModel: + @property + def capabilities(self) -> ModelCapabilities: + return ModelCapabilities() + + async def invoke( + self, + request: ModelRequest, + context: RunContext, + *, + stream: bool, + emit_delta: DeltaSink | None, + ) -> ModelResponse: + del request, context, stream, emit_delta + return ModelResponse( + parts=(ContentPart.text_part("Hello from JHarness."),), + finish_reason="end_turn", + ) + + +async def main() -> None: + invocation = Runtime(model=HelloModel()).start( + (Message.user("Say hello."),) + ) + checkpoint = await invocation.result() + state = checkpoint.snapshot.state + if not isinstance(state, Completed): + raise RuntimeError(f"run stopped with {checkpoint.snapshot.status}") + print("".join(part.text or "" for part in state.parts)) + + +asyncio.run(main()) +``` + +An invocation may also be observed while it runs: + +```python +invocation = runtime.start(messages, stream=True) +events = tuple([event async for event in invocation.events()]) +checkpoint = await invocation.result() +``` + +Each invocation is single-use. Create a new invocation with `start`, +`continue_from`, or `resume` for each execution boundary. + +## Durable State + +Checkpoints are immutable and can be persisted atomically. Encoding is +explicit rather than reflection-driven: + +```python +from jharness.kernel.wire import decode_checkpoint, encode_checkpoint + +payload = encode_checkpoint(checkpoint) +restored = decode_checkpoint(payload) +``` + +Suspended work can be resumed with an optional selector and appended external +messages. Terminal checkpoints cannot be continued or resumed. + +## Diagnostics + +Trace helpers are opt-in and do not add a second execution state machine: + ```python -from jharness.kernel import Message, Runtime from jharness.kernel.diagnostics import build_trace, verify_trace + +trace = build_trace(events, "start") +verification = verify_trace(trace) +print(verification.checkpoint_count) +``` + +## Public Namespaces + +- `jharness.kernel` — runtime, state, messages, model and tool contracts, + controls, limits, events, checkpoints, and repository ports; +- `jharness.kernel.wire` — explicit portable encoders and decoders; +- `jharness.kernel.diagnostics` — trace construction and verification. + +## Design Guarantees + +- public values are immutable; +- checkpoint persistence is atomic; +- model-order tool results commit as one batch; +- deadlines and concurrency are bounded; +- live deltas never become durable unless represented in a completed result; +- cancellation settles owned work before escaping; +- no hidden threads are created. + +## Documentation + +- [Architecture](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/architecture.md) +- [State machine](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/state-machine.md) +- [Model protocol](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/model-protocol.md) +- [Tool protocol](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/tool-protocol.md) +- [Event stream](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/event-stream.md) +- [Wire protocol](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/wire-protocol.md) +- [Diagnostics](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/diagnostics.md) +- [Issue tracker](https://github.com/Ezio2000/jharness-python/issues) + +## License + +MIT + +## 简体中文 + +[English](#jharness-kernel) | **简体中文** + +`jharness-kernel` 是 JHarness 无第三方运行时依赖、模型中立的执行核心。它提供 +不可变运行状态、明确的生命周期转换、原子检查点、有界执行、实时事件、持久化 +Wire 编解码以及可选的 Trace 验证。 + +本包支持 Python 3.11 及以上版本,并提供完整类型标注。 + +### 安装 + +```bash +uv add jharness-kernel +``` + +发行包安装隐式命名空间 `jharness`,通过 `jharness.kernel` 导入。它没有必需的 +第三方运行时依赖。 + +### 核心能力 + +- `Runtime`:不可变配置对象,用于创建单次使用的 `Invocation`; +- 扁平运行状态:`Planning`、`ToolsPending`、`Suspended`、`Completed`、 + `Failed` 和 `Limited`; +- 模型中立的消息、内容、工具调用、模型请求、模型响应和流式增量; +- 原子 `Checkpoint`,同时包含下一状态快照和对应的语义事实; +- 有界并发、单调截止时间、运行限制、审批、历史归约和存储端口; +- 与持久化提交状态分离的有序实时事件; +- `jharness.kernel.wire` 下明确的 JSON 兼容编解码器; +- `jharness.kernel.diagnostics` 下的 Trace 构建和验证。 + +### 快速开始 + +提供一个实现异步 `Model` 协议的对象,创建 Runtime,然后等待执行结果: + +```python +import asyncio + +from jharness.kernel import ( + Completed, + ContentPart, + DeltaSink, + Message, + ModelCapabilities, + ModelRequest, + ModelResponse, + RunContext, + Runtime, +) + + +class HelloModel: + @property + def capabilities(self) -> ModelCapabilities: + return ModelCapabilities() + + async def invoke( + self, + request: ModelRequest, + context: RunContext, + *, + stream: bool, + emit_delta: DeltaSink | None, + ) -> ModelResponse: + del request, context, stream, emit_delta + return ModelResponse( + parts=(ContentPart.text_part("Hello from JHarness."),), + finish_reason="end_turn", + ) + + +async def main() -> None: + invocation = Runtime(model=HelloModel()).start( + (Message.user("Say hello."),) + ) + checkpoint = await invocation.result() + state = checkpoint.snapshot.state + if not isinstance(state, Completed): + raise RuntimeError(f"run stopped with {checkpoint.snapshot.status}") + print("".join(part.text or "" for part in state.parts)) + + +asyncio.run(main()) +``` + +也可以在执行期间观察事件: + +```python +invocation = runtime.start(messages, stream=True) +events = tuple([event async for event in invocation.events()]) +checkpoint = await invocation.result() +``` + +每个 Invocation 只能使用一次。每个执行边界都应通过 `start`、 +`continue_from` 或 `resume` 创建新的 Invocation。 + +### 持久化状态 + +Checkpoint 不可变,并且可以原子持久化。编码过程是明确的,不依赖反射: + +```python from jharness.kernel.wire import decode_checkpoint, encode_checkpoint + +payload = encode_checkpoint(checkpoint) +restored = decode_checkpoint(payload) ``` -See the repository -[architecture](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/architecture.md), -[state machine](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/state-machine.md), and -[Python package boundaries](https://github.com/Ezio2000/jharness-python/blob/main/docs/python-package-boundaries.md). +挂起的工作可以通过可选选择器和追加的外部消息恢复。终止状态的 Checkpoint +不能继续或恢复。 + +### 诊断 + +Trace 辅助功能按需启用,不会引入第二套执行状态机: + +```python +from jharness.kernel.diagnostics import build_trace, verify_trace + +trace = build_trace(events, "start") +verification = verify_trace(trace) +print(verification.checkpoint_count) +``` + +### 公共命名空间 + +- `jharness.kernel`:Runtime、状态、消息、模型和工具契约、控制、限制、事件、 + Checkpoint 和存储端口; +- `jharness.kernel.wire`:明确的可移植编码器和解码器; +- `jharness.kernel.diagnostics`:Trace 构建和验证。 + +### 设计保证 + +- 公共值不可变; +- Checkpoint 持久化是原子的; +- 工具结果按模型顺序作为一个批次提交; +- 截止时间和并发都有明确上限; +- 实时增量只有在完整结果中得到表示时才会成为持久化状态; +- 取消向外传播前会先结束自身拥有的工作; +- 不会创建隐藏线程。 + +### 文档 + +- [架构](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/architecture.md) +- [状态机](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/state-machine.md) +- [模型协议](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/model-protocol.md) +- [工具协议](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/tool-protocol.md) +- [事件流](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/event-stream.md) +- [Wire 协议](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/wire-protocol.md) +- [诊断](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/diagnostics.md) +- [问题跟踪](https://github.com/Ezio2000/jharness-python/issues) + +### 许可证 + +MIT diff --git a/python/providers/README.md b/python/providers/README.md index 4bc64d1..bc75ba0 100644 --- a/python/providers/README.md +++ b/python/providers/README.md @@ -1,21 +1,335 @@ # jharness-providers -`jharness-providers` contains optional concrete implementations of the -provider-neutral `jharness.kernel.Model` protocol. Public adapters are imported from -provider namespaces: +**English** | [简体中文](#简体中文) + +`jharness-providers` contains concrete async model adapters, profiles, +transports, errors, and wire codecs for JHarness applications. + +The package targets Python 3.11 and newer and is fully typed. + +## Install + +```bash +uv add jharness-providers +``` + +Public adapters live in provider-specific namespaces. The top-level +`jharness.providers` namespace intentionally has no convenience exports. + +## Supported APIs + +| Namespace | API surface | +| --- | --- | +| `jharness.providers.openai` | OpenAI-compatible Chat Completions | +| `jharness.providers.anthropic` | Anthropic-compatible Messages | +| `jharness.providers.deepseek` | DeepSeek profiles for both wire formats | + +The adapters support non-streaming and streaming responses, text, tool calls, +usage, normalized errors, and profile-gated structured or multimodal features. +Actual capabilities are exposed by each configured model instance. + +## OpenAI Chat Completions + +```python +import os + +from jharness.providers.openai import ( + OpenAIChatCompletionsModel, + OpenAIChatCompletionsProfile, +) + +model = OpenAIChatCompletionsModel( + base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), + api_key=os.environ["OPENAI_API_KEY"], + model=os.environ["OPENAI_MODEL"], + profile=OpenAIChatCompletionsProfile(), +) + +print(model.capabilities) +``` + +The adapter calls `POST /chat/completions`. A profile records endpoint-specific +capabilities such as streaming, tools, parallel tool calls, image input, JSON +mode, JSON Schema output, token field selection, and request extensions. + +The Responses API and legacy text-completions API are intentionally outside +this adapter. + +## Anthropic Messages + +```python +import os + +from jharness.providers.anthropic import AnthropicModel, AnthropicProfile + +model = AnthropicModel( + base_url=os.environ.get("ANTHROPIC_BASE_URL", "https://api.anthropic.com"), + api_key=os.environ["ANTHROPIC_API_KEY"], + model=os.environ["ANTHROPIC_MODEL"], + profile=AnthropicProfile(), +) + +print(model.capabilities) +``` + +The adapter calls `POST /v1/messages`. Its profile controls authentication, +API version, streaming, tool choice, image and document input, structured +output, maximum output tokens, beta headers, and endpoint extensions. + +Message Batches, file uploads, token counting, hosted tools, and managed +conversation sessions are intentionally outside this adapter. + +## DeepSeek Profiles + +DeepSeek factories configure one of the concrete wire-format adapters without +adding another client abstraction: + +```python +import os + +from jharness.providers.anthropic import AnthropicModel +from jharness.providers.deepseek import ( + deepseek_anthropic_profile, + deepseek_openai_chat_profile, +) +from jharness.providers.openai import OpenAIChatCompletionsModel + +api_key = os.environ["DEEPSEEK_API_KEY"] +model_name = os.environ["DEEPSEEK_MODEL"] + +chat_model = OpenAIChatCompletionsModel( + base_url="https://api.deepseek.com", + api_key=api_key, + model=model_name, + profile=deepseek_openai_chat_profile(thinking=False), +) + +thinking_model = AnthropicModel( + base_url="https://api.deepseek.com/anthropic", + api_key=api_key, + model=model_name, + profile=deepseek_anthropic_profile(thinking=True, effort="max"), +) +``` + +Thinking mode and effort are explicit profile options. Advertised capabilities +follow the selected DeepSeek protocol and mode. + +## HTTP Client Ownership + +Each model accepts an optional host-owned `httpx.AsyncClient`: + +```python +import asyncio + +import httpx + +from jharness.providers.openai import OpenAIChatCompletionsModel + + +async def main() -> None: + async with httpx.AsyncClient() as client: + model = OpenAIChatCompletionsModel( + base_url="https://api.example.com/v1", + api_key="secret", + model="example-model", + client=client, + timeout=30.0, + ) + print(model.capabilities) + + +asyncio.run(main()) +``` + +Inject a client to reuse connection pools in high-throughput applications. If +no client is supplied, each request owns a short-lived client. Response bodies +are closed and in-flight stream work is settled before cancellation escapes. + +## Errors and Codecs + +- `OpenAIChatCompletionsError` and `AnthropicError` describe malformed local + wire data; +- remote and transport failures are normalized at the model boundary; +- `OpenAIChatCompletionsCodec` and `AnthropicCodec` are public for explicit + request/response translation; +- provider-local details remain in profiles and metadata rather than changing + portable execution semantics. + +Adapters do not implement retry loops. Retry policy remains owned by the host +application so attempts, budgets, and observability stay explicit. + +## Related Links + +- [jharness-kernel](https://pypi.org/project/jharness-kernel/) +- [Provider documentation](https://github.com/Ezio2000/jharness-python/blob/main/docs/model-providers.md) +- [Model protocol](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/model-protocol.md) +- [Issue tracker](https://github.com/Ezio2000/jharness-python/issues) + +## License + +MIT + +## 简体中文 + +[English](#jharness-providers) | **简体中文** + +`jharness-providers` 为 JHarness 应用提供具体的异步模型适配器、Profile、 +Transport、错误类型和 Wire 编解码器。 + +本包支持 Python 3.11 及以上版本,并提供完整类型标注。 + +### 安装 ```bash uv add jharness-providers ``` +公共适配器位于各自的专属命名空间。顶层 `jharness.providers` 命名空间有意不提供 +便捷导出。 + +### 支持的 API + +| 命名空间 | API 范围 | +| --- | --- | +| `jharness.providers.openai` | OpenAI 兼容的 Chat Completions | +| `jharness.providers.anthropic` | Anthropic 兼容的 Messages | +| `jharness.providers.deepseek` | 两种 Wire 格式的 DeepSeek Profile | + +适配器支持非流式和流式响应、文本、工具调用、Usage、统一错误,以及由 Profile +控制的结构化或多模态能力。实际能力由每个完成配置的模型实例公开。 + +### OpenAI Chat Completions + ```python +import os + +from jharness.providers.openai import ( + OpenAIChatCompletionsModel, + OpenAIChatCompletionsProfile, +) + +model = OpenAIChatCompletionsModel( + base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), + api_key=os.environ["OPENAI_API_KEY"], + model=os.environ["OPENAI_MODEL"], + profile=OpenAIChatCompletionsProfile(), +) + +print(model.capabilities) +``` + +适配器调用 `POST /chat/completions`。Profile 记录端点的流式输出、工具、并行工具 +调用、图像输入、JSON 模式、JSON Schema 输出、Token 字段和请求扩展能力。 + +Responses API 和旧版文本 Completions API 不属于此适配器范围。 + +### Anthropic Messages + +```python +import os + +from jharness.providers.anthropic import AnthropicModel, AnthropicProfile + +model = AnthropicModel( + base_url=os.environ.get("ANTHROPIC_BASE_URL", "https://api.anthropic.com"), + api_key=os.environ["ANTHROPIC_API_KEY"], + model=os.environ["ANTHROPIC_MODEL"], + profile=AnthropicProfile(), +) + +print(model.capabilities) +``` + +适配器调用 `POST /v1/messages`。Profile 控制认证方式、API 版本、流式输出、工具 +选择、图像和文档输入、结构化输出、最大输出 Token、Beta Header 和端点扩展。 + +Message Batches、文件上传、Token Counting、托管工具以及托管对话 Session +不属于此适配器范围。 + +### DeepSeek Profile + +DeepSeek 工厂使用其中一种具体 Wire 格式配置适配器,不会增加另一层 Client 抽象: + +```python +import os + from jharness.providers.anthropic import AnthropicModel -from jharness.providers.deepseek import deepseek_anthropic_profile +from jharness.providers.deepseek import ( + deepseek_anthropic_profile, + deepseek_openai_chat_profile, +) +from jharness.providers.openai import OpenAIChatCompletionsModel + +api_key = os.environ["DEEPSEEK_API_KEY"] +model_name = os.environ["DEEPSEEK_MODEL"] + +chat_model = OpenAIChatCompletionsModel( + base_url="https://api.deepseek.com", + api_key=api_key, + model=model_name, + profile=deepseek_openai_chat_profile(thinking=False), +) + +thinking_model = AnthropicModel( + base_url="https://api.deepseek.com/anthropic", + api_key=api_key, + model=model_name, + profile=deepseek_anthropic_profile(thinking=True, effort="max"), +) +``` + +Thinking 模式和 Effort 是明确的 Profile 选项。公开能力取决于所选 DeepSeek +协议和模式。 + +### HTTP Client 所有权 + +每个模型都可以接收由宿主拥有的 `httpx.AsyncClient`: + +```python +import asyncio + +import httpx + from jharness.providers.openai import OpenAIChatCompletionsModel + + +async def main() -> None: + async with httpx.AsyncClient() as client: + model = OpenAIChatCompletionsModel( + base_url="https://api.example.com/v1", + api_key="secret", + model="example-model", + client=client, + timeout=30.0, + ) + print(model.capabilities) + + +asyncio.run(main()) ``` -Provider transports, profiles, and codecs do not define runtime semantics. -See the -[provider documentation](https://github.com/Ezio2000/jharness-python/blob/main/docs/model-providers.md) -and -[Python package boundaries](https://github.com/Ezio2000/jharness-python/blob/main/docs/python-package-boundaries.md). +高吞吐量应用可以注入 Client 以复用连接池。没有提供 Client 时,每次请求会拥有 +一个短生命周期 Client。取消操作向外传播前,Response Body 会被关闭,进行中的 +流式工作也会结束。 + +### 错误与 Codec + +- `OpenAIChatCompletionsError` 和 `AnthropicError` 描述本地 Wire 数据错误; +- 远程错误和传输错误会在模型边界统一处理; +- `OpenAIChatCompletionsCodec` 和 `AnthropicCodec` 可用于明确的请求和响应转换; +- 端点特有细节保留在 Profile 和 Metadata 中,不会改变可移植执行语义。 + +适配器不会实现重试循环。重试策略由宿主应用负责,从而保持尝试次数、预算和 +可观测性都是明确的。 + +### 相关链接 + +- [jharness-kernel](https://pypi.org/project/jharness-kernel/) +- [适配器文档](https://github.com/Ezio2000/jharness-python/blob/main/docs/model-providers.md) +- [模型协议](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/model-protocol.md) +- [问题跟踪](https://github.com/Ezio2000/jharness-python/issues) + +### 许可证 + +MIT diff --git a/python/toolkit/README.md b/python/toolkit/README.md index 3ef432f..4d27dcc 100644 --- a/python/toolkit/README.md +++ b/python/toolkit/README.md @@ -1,18 +1,280 @@ # jharness-toolkit -`jharness-toolkit` is the concrete Python tool-support distribution for -`jharness.kernel`. It provides an immutable tool registry, JSON Schema -validation, async function adapters, and retry and circuit-breaking decorators. +**English** | [简体中文](#简体中文) + +`jharness-toolkit` provides concrete Python tool registration, JSON Schema +validation, async function adaptation, retries, and circuit breaking for +JHarness applications. + +The package targets Python 3.11 and newer and is fully typed. + +## Install ```bash uv add jharness-toolkit ``` +Import its public API from `jharness.toolkit`. + +## What It Provides + +- `ToolRegistry` for thread-safe registration and immutable invocation-local + catalogs; +- `function_tool` and `FunctionTool` for adapting async Python functions; +- JSON Schema Draft 2020-12 validation for tool inputs and structured outputs; +- eager schema compilation and rejection of unresolved references; +- duplicate-name and unknown-tool protection; +- `RetryingTool` for explicitly idempotent operations; +- `CircuitBreakingTool` for consecutive implementation failures. + +## Quick Start + +Define an async function with explicit schemas, then register it: + ```python +import asyncio + +from jharness.kernel import ( + ContentPart, + SettledResult, + ToolCall, + ToolContext, + ToolExecution, + ToolResult, + ToolSuccess, +) from jharness.toolkit import ToolRegistry, function_tool + + +@function_tool( + name="uppercase", + description="Convert text to uppercase.", + input_schema={ + "type": "object", + "required": ["text"], + "properties": {"text": {"type": "string"}}, + "additionalProperties": False, + }, + output_schema={ + "type": "object", + "required": ["text"], + "properties": {"text": {"type": "string"}}, + "additionalProperties": False, + }, + execution=ToolExecution(idempotent=True), +) +async def uppercase(call: ToolCall, context: ToolContext) -> ToolResult: + del context + text = str(call.arguments["text"]).upper() + return SettledResult( + ToolSuccess( + (ContentPart.text_part(text),), + structured_content={"text": text}, + ) + ) + + +async def main() -> None: + catalog = await ToolRegistry((uppercase,)).open_catalog() + binding = catalog.bind( + ToolCall("call-1", "uppercase", {"text": "hello"}) + ) + print(binding.spec.name) + + +asyncio.run(main()) ``` -Runtime semantics and portable tool values remain in `jharness.kernel`; -`jharness.toolkit` only implements concrete adaptation and execution policies. See the -[tool protocol](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/tool-protocol.md) and -[Python package boundaries](https://github.com/Ezio2000/jharness-python/blob/main/docs/python-package-boundaries.md). +`open_catalog()` returns an immutable snapshot. Later registry changes do not +alter catalogs already opened by active invocations. + +## Validation + +Schemas are checked when a tool is registered. Inputs are validated when a +call is bound, before implementation code runs. If an output schema is +declared, `structured_content` is validated after invocation. + +Invalid schemas raise `ValueError`; invalid calls and outputs raise +`ToolError`. Schemas remain explicit—function signatures are not inspected to +invent a schema. + +## Retry and Circuit Breaking + +Decorators compose around any object implementing the `Tool` protocol: + +```python +from jharness.toolkit import CircuitBreakingTool, RetryingTool + +resilient_tool = CircuitBreakingTool( + RetryingTool( + uppercase, + max_attempts=3, + attempt_timeout_seconds=2.0, + ), + failure_threshold=3, +) +``` + +Retries require `spec.execution.idempotent=True`. They retry implementation +exceptions, while model-visible failure results remain normal results. Circuit +state is owned by the decorator and is not persisted as execution state. + +## Public API + +| Name | Purpose | +| --- | --- | +| `ToolRegistry` | Register tools and open immutable catalogs. | +| `Tool` | Runtime-checkable async tool protocol. | +| `function_tool` | Build a `FunctionTool` with explicit schemas and policy. | +| `FunctionTool` | Immutable async-function adapter. | +| `RetryingTool` | Retry idempotent implementation failures. | +| `CircuitBreakingTool` | Open a circuit after consecutive exceptions. | + +## Related Links + +- [jharness-kernel](https://pypi.org/project/jharness-kernel/) +- [Tool protocol](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/tool-protocol.md) +- [Python package boundaries](https://github.com/Ezio2000/jharness-python/blob/main/docs/python-package-boundaries.md) +- [Issue tracker](https://github.com/Ezio2000/jharness-python/issues) + +## License + +MIT + +## 简体中文 + +[English](#jharness-toolkit) | **简体中文** + +`jharness-toolkit` 为 JHarness 应用提供具体的 Python 工具注册、JSON Schema +验证、异步函数适配、重试和熔断能力。 + +本包支持 Python 3.11 及以上版本,并提供完整类型标注。 + +### 安装 + +```bash +uv add jharness-toolkit +``` + +公共 API 从 `jharness.toolkit` 导入。 + +### 核心能力 + +- `ToolRegistry`:线程安全注册,并为每次调用创建不可变工具目录; +- `function_tool` 和 `FunctionTool`:适配异步 Python 函数; +- 使用 JSON Schema Draft 2020-12 验证工具输入和结构化输出; +- 提前编译 Schema,并拒绝无法解析的引用; +- 防止重复工具名和未知工具调用; +- `RetryingTool`:用于明确声明为幂等的操作; +- `CircuitBreakingTool`:针对连续实现异常进行熔断。 + +### 快速开始 + +使用明确的 Schema 定义异步函数,然后注册工具: + +```python +import asyncio + +from jharness.kernel import ( + ContentPart, + SettledResult, + ToolCall, + ToolContext, + ToolExecution, + ToolResult, + ToolSuccess, +) +from jharness.toolkit import ToolRegistry, function_tool + + +@function_tool( + name="uppercase", + description="Convert text to uppercase.", + input_schema={ + "type": "object", + "required": ["text"], + "properties": {"text": {"type": "string"}}, + "additionalProperties": False, + }, + output_schema={ + "type": "object", + "required": ["text"], + "properties": {"text": {"type": "string"}}, + "additionalProperties": False, + }, + execution=ToolExecution(idempotent=True), +) +async def uppercase(call: ToolCall, context: ToolContext) -> ToolResult: + del context + text = str(call.arguments["text"]).upper() + return SettledResult( + ToolSuccess( + (ContentPart.text_part(text),), + structured_content={"text": text}, + ) + ) + + +async def main() -> None: + catalog = await ToolRegistry((uppercase,)).open_catalog() + binding = catalog.bind( + ToolCall("call-1", "uppercase", {"text": "hello"}) + ) + print(binding.spec.name) + + +asyncio.run(main()) +``` + +`open_catalog()` 返回不可变快照。之后对 Registry 的修改不会影响已经为活动调用 +打开的目录。 + +### 验证 + +工具注册时会检查 Schema。调用绑定时会验证输入,验证发生在实现代码运行之前。 +如果声明了输出 Schema,则会在调用结束后验证 `structured_content`。 + +无效 Schema 会引发 `ValueError`;无效调用和输出会引发 `ToolError`。Schema +始终需要明确声明,不会通过检查函数签名自动生成。 + +### 重试与熔断 + +装饰器可以组合到任何实现 `Tool` 协议的对象上: + +```python +from jharness.toolkit import CircuitBreakingTool, RetryingTool + +resilient_tool = CircuitBreakingTool( + RetryingTool( + uppercase, + max_attempts=3, + attempt_timeout_seconds=2.0, + ), + failure_threshold=3, +) +``` + +重试要求 `spec.execution.idempotent=True`。它只重试实现异常;模型可见的失败结果 +仍然属于正常结果。熔断状态由装饰器持有,不会被保存为执行状态。 + +### 公共 API + +| 名称 | 用途 | +| --- | --- | +| `ToolRegistry` | 注册工具并打开不可变目录。 | +| `Tool` | 可在运行时检查的异步工具协议。 | +| `function_tool` | 使用明确的 Schema 和策略构建 `FunctionTool`。 | +| `FunctionTool` | 不可变异步函数适配器。 | +| `RetryingTool` | 重试幂等操作的实现异常。 | +| `CircuitBreakingTool` | 连续异常达到阈值后打开熔断器。 | + +### 相关链接 + +- [jharness-kernel](https://pypi.org/project/jharness-kernel/) +- [工具协议](https://github.com/Ezio2000/jharness/blob/v0.1.0/docs/tool-protocol.md) +- [Python 包边界](https://github.com/Ezio2000/jharness-python/blob/main/docs/python-package-boundaries.md) +- [问题跟踪](https://github.com/Ezio2000/jharness-python/issues) + +### 许可证 + +MIT diff --git a/tests/test_release_process.py b/tests/test_release_process.py index d0ae7c3..a062539 100644 --- a/tests/test_release_process.py +++ b/tests/test_release_process.py @@ -64,6 +64,29 @@ def test_repository_ownership_and_dependency_updates_are_explicit() -> None: assert dependabot.count("interval: weekly") == 2 +def test_package_readmes_follow_distribution_dependencies() -> None: + kernel = (ROOT / "python" / "kernel" / "README.md").read_text().lower() + toolkit = (ROOT / "python" / "toolkit" / "README.md").read_text().lower() + providers = (ROOT / "python" / "providers" / "README.md").read_text().lower() + + assert "jharness-toolkit" not in kernel + assert "jharness.toolkit" not in kernel + assert "jharness-providers" not in kernel + assert "jharness.providers" not in kernel + + assert "jharness-providers" not in toolkit + assert "jharness.providers" not in toolkit + assert "https://pypi.org/project/jharness-kernel/" in toolkit + + assert "jharness-toolkit" not in providers + assert "jharness.toolkit" not in providers + assert "https://pypi.org/project/jharness-kernel/" in providers + + for readme in (kernel, toolkit, providers): + assert "**english** | [简体中文](#简体中文)" in readme + assert "[english](#jharness-" in readme + + def test_testpypi_smoke_project_uses_an_explicit_index() -> None: script = (ROOT / "scripts" / "verify_testpypi.py").read_text() assert script.count('{{ index = "testpypi" }}') == 3 From 0e168e853946425979f76ce3f3186b2e0d0747c5 Mon Sep 17 00:00:00 2001 From: xieningjun <779016161@qq.com> Date: Wed, 15 Jul 2026 01:35:43 +0800 Subject: [PATCH 2/2] Read package documentation as UTF-8 --- tests/test_examples_and_docs.py | 2 +- tests/test_release_process.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_examples_and_docs.py b/tests/test_examples_and_docs.py index 69434a9..80a1b92 100644 --- a/tests/test_examples_and_docs.py +++ b/tests/test_examples_and_docs.py @@ -39,7 +39,7 @@ def test_every_local_markdown_link_resolves() -> None: markdown_files.extend((ROOT / "tests").glob("*.md")) pattern = re.compile(r"\[[^\]]+\]\(([^)]+)\)") for path in markdown_files: - for target in pattern.findall(path.read_text()): + for target in pattern.findall(path.read_text(encoding="utf-8")): if target.startswith(("http://", "https://", "#")): continue relative = target.split("#", 1)[0] diff --git a/tests/test_release_process.py b/tests/test_release_process.py index a062539..c7968bd 100644 --- a/tests/test_release_process.py +++ b/tests/test_release_process.py @@ -65,9 +65,9 @@ def test_repository_ownership_and_dependency_updates_are_explicit() -> None: def test_package_readmes_follow_distribution_dependencies() -> None: - kernel = (ROOT / "python" / "kernel" / "README.md").read_text().lower() - toolkit = (ROOT / "python" / "toolkit" / "README.md").read_text().lower() - providers = (ROOT / "python" / "providers" / "README.md").read_text().lower() + kernel = (ROOT / "python" / "kernel" / "README.md").read_text(encoding="utf-8").lower() + toolkit = (ROOT / "python" / "toolkit" / "README.md").read_text(encoding="utf-8").lower() + providers = (ROOT / "python" / "providers" / "README.md").read_text(encoding="utf-8").lower() assert "jharness-toolkit" not in kernel assert "jharness.toolkit" not in kernel