pi-python-core 0.8.1__tar.gz → 0.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pi_python_core-0.8.2/PKG-INFO +124 -0
- pi_python_core-0.8.2/README.md +93 -0
- pi_python_core-0.8.1/README.md → pi_python_core-0.8.2/README.zh-CN.md +10 -7
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/COVERAGE.md +2 -2
- pi_python_core-0.8.2/docs/API.md +200 -0
- pi_python_core-0.8.2/docs/CONCEPTS.md +75 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/docs/IMPLEMENTATION.md +3 -1
- pi_python_core-0.8.2/docs/PROVIDERS.md +215 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/docs/history/IMPLEMENTATION-0.3.0.md +1 -1
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/docs/history/IMPLEMENTATION-0.4.0.md +2 -2
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/docs/history/IMPLEMENTATION-0.5.0.md +4 -4
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/docs/history/IMPLEMENTATION-0.6.0.md +2 -2
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/docs/history/IMPLEMENTATION-0.7.0.md +1 -1
- {pi_python_core-0.8.1/docs → pi_python_core-0.8.2/docs/zh}/API.md +3 -1
- {pi_python_core-0.8.1/docs → pi_python_core-0.8.2/docs/zh}/CONCEPTS.md +3 -1
- {pi_python_core-0.8.1/docs → pi_python_core-0.8.2/docs/zh}/PROVIDERS.md +15 -11
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/local_model.py +1 -1
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/quickstart.py +1 -1
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/pyproject.toml +7 -4
- pi_python_core-0.8.2/src/pi_python/_version.py +1 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/providers/__init__.py +1 -1
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/providers/oauth.py +6 -1
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_examples.py +0 -2
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_oauth.py +6 -0
- pi_python_core-0.8.1/PKG-INFO +0 -119
- pi_python_core-0.8.1/src/pi_python/_version.py +0 -1
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/.gitignore +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/LICENSE +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/NOTICE +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/__init__.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/baseline.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/contracts/release-candidate.example.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/contracts/release-candidate.schema.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/contracts/upstream-baseline.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C01-text.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C02-tool.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C03-sequential.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C04-parallel.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C05-one-sequential.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C06-one-failure.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C07-unknown.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C08-prepare.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C09-before-error.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C09-block.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C09-replace.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C10-length.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C11-transform.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C12-system.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C13-steering.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C14-followup-all.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C14-followup-one.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C15-prepare.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C16-continue.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C16-end.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C16-mixed.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C16-terminate.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C19-abort-stream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C19-abort-tools.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/fixtures/C24-provider-error.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-oauth.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-request-cache-long.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-request-cache-none.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-request-replay.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-request-tools-image.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-text.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-thinking.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/anthropic-tool.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/codex-request-tools-image.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/codex-text.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-compat-strict.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-deepseek.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-format-ant-ling.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-format-baseten.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-format-string-thinking.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-format-together.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-format-zai.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-midconvo-tools.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-moonshot-usage.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-openai-off.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-openrouter.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-qwen-length.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-reasoning-content.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-request-replay.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-text.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/completions-tool-stream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-incomplete.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-request-cache-long.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-request-cache-none.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-request-reasoning.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-request-replay.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-request-tools-image.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-text.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-thinking-backfill.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-thinking.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/openai-tool.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/proxy-text.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/proxy-thinking-tool.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-anthropic-collapse-budget.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-anthropic-native-tools.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-anthropic-oauth-effort.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-anthropic-redefinition.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-anthropic-server-fallback.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-anthropic-thinking-off.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-codex-tool-search.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-openai-additional-tools.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-openai-collapse.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-openai-explicit-cache-long.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-openai-explicit-cache-none.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/provider-fixtures/session-openai-removal.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/recovery-cases.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/release.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C01-text.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C01-text.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C02-tool.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C02-tool.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C03-sequential.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C03-sequential.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C04-parallel.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C04-parallel.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C05-one-sequential.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C05-one-sequential.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C06-one-failure.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C06-one-failure.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C07-unknown.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C07-unknown.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C08-prepare.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C08-prepare.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C09-before-error.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C09-before-error.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C09-block.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C09-block.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C09-replace.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C09-replace.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C10-length.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C10-length.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C11-transform.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C11-transform.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C12-system.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C12-system.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C13-steering.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C13-steering.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C14-followup-all.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C14-followup-all.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C14-followup-one.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C14-followup-one.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C15-prepare.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C15-prepare.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-continue.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-continue.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-end.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-end.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-mixed.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-mixed.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-terminate.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C16-terminate.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C19-abort-stream.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C19-abort-stream.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C19-abort-tools.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C19-abort-tools.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C24-provider-error.python.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/C24-provider-error.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/benchmark.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/bootstrap.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/candidate-live-noop.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/conformance.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/linux-3.14.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/linux-install-hash-repair.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-0.5-regression.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-0.5-session-claude.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-0.5-session-codex.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-claude-reasoning-replay.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-final-matrix.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-initial.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-loop-initial.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-providers.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-reasoning-replay.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-refresh.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-regression-0.4-deepseek.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-regression-0.4-first-run.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-regression-0.4.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-session-claude.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-session-codex.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-websocket-cached-recheck.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-websocket-cached.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/live-websocket-final.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/provider-conformance.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/recovery-conformance.upstream.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/runtime-dependencies.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/source-verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.1.0/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.1.0/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.1.0/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.1.0/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.1.0/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.2.0/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.2.0/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.2.0/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.2.0/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.2.0/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.3.0/against-0.4-fixtures.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.3.0/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.3.0/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.3.0/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.3.0/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.3.0/provider-conformance.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.3.0/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.4.0/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.4.0/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.4.0/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.4.0/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.4.0/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.5.0/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.5.0/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.5.0/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.5.0/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.5.0/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.6.0/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.6.0/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.6.0/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.6.0/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.6.0/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.7.0/artifacts.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.7.0/linux-3.11.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.7.0/linux-3.12.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.7.0/linux-3.13.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.7.0/linux-3.14.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/v0.7.0/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/verification.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/results/websocket-conformance.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/runner.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/websocket-fixtures/codex-continuation-lost-state.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/compat/websocket-fixtures/codex-continuation-tool-loop.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/in_memory.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/mcp_tools.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/provider_chat.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/recovery.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/save_restore.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/examples/subagent.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/__init__.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/agent.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/cancellation.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/data/models.json +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/errors.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/estimate.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/events.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/function_tools.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/hooks.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/limits.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/loop.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/lowlevel.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/mcp.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/messages.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/models.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/provider.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/providers/anthropic.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/providers/common.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/providers/completions.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/providers/openai.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/providers/transport.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/proxy.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/py.typed +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/queues.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/recovery.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/run.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/stream.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/sync.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/testing.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/tools.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/src/pi_python/transcript.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/servers/mcp_server.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_completions.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_conformance.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_contract_v3.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_control.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_extended_core.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_function_tools.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_loop.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_mcp.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_messages.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_models.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_providers.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_recovery.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_recovery_conformance.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_recovery_helpers.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_release.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_session_changes.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_sync.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_tools.py +0 -0
- {pi_python_core-0.8.1 → pi_python_core-0.8.2}/tests/test_websocket_continuation.py +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pi-python-core
|
|
3
|
+
Version: 0.8.2
|
|
4
|
+
Summary: An embeddable asyncio agent core with opt-in Claude and OpenAI providers
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: NOTICE
|
|
8
|
+
Classifier: Framework :: AsyncIO
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx<1,>=0.27
|
|
22
|
+
Requires-Dist: jsonschema<5,>=4.18
|
|
23
|
+
Requires-Dist: websockets>=14.2
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: mcp>=1.10; extra == 'mcp'
|
|
26
|
+
Provides-Extra: oauth
|
|
27
|
+
Requires-Dist: pyjwt[crypto]<3,>=2.8; extra == 'oauth'
|
|
28
|
+
Provides-Extra: providers
|
|
29
|
+
Requires-Dist: pyjwt[crypto]<3,>=2.8; extra == 'providers'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# pi-python-core
|
|
33
|
+
|
|
34
|
+
**English** | [中文](README.zh-CN.md)
|
|
35
|
+
|
|
36
|
+
An embeddable Python agent core, ported from [Pi](https://github.com/earendil-works/pi) (pinned to `v1.0.0`). It does one job: send the conversation to a model, run the tools the model asks for, hand the results back, and repeat until the model answers. Tools are plain Python functions; the model can be Claude, GPT, DeepSeek, or an open model running on your own machine or cluster.
|
|
37
|
+
|
|
38
|
+
`pi-agent-core` on PyPI is a separate project that ports an older version from pi-mono (early 2026). This library follows the behavior of Pi v1.0.0, compared case by case with the upstream code's actual output, and ships its own connectors for Claude, OpenAI, DeepSeek and local models, with no model SDK required.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Python 3.11–3.14 (including free-threaded 3.14t) and PyPy 3.11. No Node.js or model SDK needed.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install pi-python-core # or: uv add pi-python-core
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You install `pi-python-core` and import `pi_python`.
|
|
49
|
+
|
|
50
|
+
| Option | What it adds |
|
|
51
|
+
|---|---|
|
|
52
|
+
| (none) | The agent core and the built-in model connectors: Claude, OpenAI, Codex, DeepSeek, and any OpenAI-compatible server (Ollama, vLLM, llama.cpp, …) |
|
|
53
|
+
| `[oauth]` | Verifies the identity token when you sign in with a ChatGPT account (`openai-chatgpt`); it brings in the compiled `cryptography` package. Claude subscription and Codex sign-in do not need it |
|
|
54
|
+
| `[providers]` | Same as `[oauth]`; keeps install commands from 0.8.1 and earlier working |
|
|
55
|
+
| `[mcp]` | Gives the agent the tools of MCP servers |
|
|
56
|
+
|
|
57
|
+
Dependencies are version ranges rather than pins, so the package fits into most existing environments.
|
|
58
|
+
|
|
59
|
+
## Five-minute start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pi_python import Agent, tool
|
|
63
|
+
from pi_python.providers import AnthropicProvider
|
|
64
|
+
|
|
65
|
+
@tool
|
|
66
|
+
def word_count(text: str) -> int:
|
|
67
|
+
"""Count the words in a text."""
|
|
68
|
+
return len(text.split())
|
|
69
|
+
|
|
70
|
+
agent = Agent(provider=AnthropicProvider(api_key="..."), model="claude-sonnet-4-5", tools=[word_count])
|
|
71
|
+
result = agent.prompt_sync("How many words are in 'to be or not to be'?")
|
|
72
|
+
print(result.messages[-1].content[0].text)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`@tool` builds a tool from the function's signature and docstring; both plain and `async` functions work. In async code, use `await agent.prompt(...)`. Switching to a local model changes two lines:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from pi_python.providers import OpenAICompletionsProvider
|
|
79
|
+
|
|
80
|
+
llm = OpenAICompletionsProvider(base_url="http://localhost:11434/v1", name="ollama")
|
|
81
|
+
agent = Agent(provider=llm, model=llm.model("qwen3:8b", context_window=40960), tools=[word_count])
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To try it offline first: `python examples/quickstart.py`.
|
|
85
|
+
|
|
86
|
+
## What it does
|
|
87
|
+
|
|
88
|
+
| Need | How | Example |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| Write tools | Decorate a plain function with `@tool`; arguments such as pydantic models, dataclasses, enums and dates are converted automatically; hand-written or MCP-generated JSON Schema works too | [quickstart](examples/quickstart.py) |
|
|
91
|
+
| Connect a model | Claude and GPT through an API key or subscription sign-in; DeepSeek; any OpenAI-compatible server | [local_model](examples/local_model.py), [provider_chat](examples/provider_chat.py) |
|
|
92
|
+
| Use MCP tools | `async with connect_stdio(...) as tools` | [mcp_tools](examples/mcp_tools.py) |
|
|
93
|
+
| Let one agent call another | Wrap the sub-agent as a tool; cancellation propagates down | [subagent](examples/subagent.py) |
|
|
94
|
+
| Intervene mid-run | `steer` injects guidance, `follow_up` queues the next task, `abort` cancels at any time (callable from any thread) | |
|
|
95
|
+
| Context full, service errors | `is_context_overflow` and `is_retryable_error` tell you why, `continue_run()` retries, `transform_context` compacts | [recovery](examples/recovery.py) |
|
|
96
|
+
| Save and restore conversations | `encode_messages` / `decode_messages`; your application decides where to store them | [save_restore](examples/save_restore.py) |
|
|
97
|
+
| Observe and audit | Subscribe to events; hooks before and after tool execution can block or rewrite tool calls | |
|
|
98
|
+
|
|
99
|
+
Apart from `provider_chat`, which needs real credentials, every example runs offline without an API key, and the tests run each one.
|
|
100
|
+
|
|
101
|
+
## Relationship to Pi
|
|
102
|
+
|
|
103
|
+
The run loop, event order, hooks, queues, and the handling of errors and cancellation all match Pi, and this is checked differentially: the same inputs go to the pinned upstream code and to this library, and the model requests, tool calls, events and final transcript are compared item by item. All cases currently agree: 25 for the core loop, 50 for model connectors, 2 for multi-turn WebSocket, and 44 error-classification samples.
|
|
104
|
+
|
|
105
|
+
A few differences are deliberate, such as strict tool-argument validation without type coercion, and returning copies of state to callers. Others are additions for Python users, such as `@tool`, blocking calls, and calling `continue_run()` directly after a failure. Each one is recorded in the [coverage map](compat/COVERAGE.md) (Chinese). Features Pi keeps in its application layer (terminal UI, session file format, context compaction) are not in this core; compaction can be built with hooks, and an example shows the full approach. This project uses its own version numbers and is not an official Pi release.
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
- [Concepts on one page: five ideas and one turn](docs/CONCEPTS.md)
|
|
110
|
+
- [Public API](docs/API.md)
|
|
111
|
+
- [Model connectors, subscription sign-in and local models](docs/PROVIDERS.md)
|
|
112
|
+
- [Implementation and verification results](docs/IMPLEMENTATION.md) (Chinese)
|
|
113
|
+
- [Item-by-item comparison with Pi, and deliberate differences](compat/COVERAGE.md) (Chinese)
|
|
114
|
+
- [Rebuilding the reference and checking release candidates](reference/README.md) (Chinese)
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv sync --locked --extra oauth --extra mcp
|
|
120
|
+
uv run pytest -q
|
|
121
|
+
uv run python scripts/verify.py # every check, including the comparison with upstream (needs Node)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
CI is defined in `.github/workflows/ci.yml` and runs on GitHub Actions for every push: each Python version on Linux (including 3.14t and PyPy), plus macOS and Windows.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# pi-python-core
|
|
2
|
+
|
|
3
|
+
**English** | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
An embeddable Python agent core, ported from [Pi](https://github.com/earendil-works/pi) (pinned to `v1.0.0`). It does one job: send the conversation to a model, run the tools the model asks for, hand the results back, and repeat until the model answers. Tools are plain Python functions; the model can be Claude, GPT, DeepSeek, or an open model running on your own machine or cluster.
|
|
6
|
+
|
|
7
|
+
`pi-agent-core` on PyPI is a separate project that ports an older version from pi-mono (early 2026). This library follows the behavior of Pi v1.0.0, compared case by case with the upstream code's actual output, and ships its own connectors for Claude, OpenAI, DeepSeek and local models, with no model SDK required.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Python 3.11–3.14 (including free-threaded 3.14t) and PyPy 3.11. No Node.js or model SDK needed.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install pi-python-core # or: uv add pi-python-core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
You install `pi-python-core` and import `pi_python`.
|
|
18
|
+
|
|
19
|
+
| Option | What it adds |
|
|
20
|
+
|---|---|
|
|
21
|
+
| (none) | The agent core and the built-in model connectors: Claude, OpenAI, Codex, DeepSeek, and any OpenAI-compatible server (Ollama, vLLM, llama.cpp, …) |
|
|
22
|
+
| `[oauth]` | Verifies the identity token when you sign in with a ChatGPT account (`openai-chatgpt`); it brings in the compiled `cryptography` package. Claude subscription and Codex sign-in do not need it |
|
|
23
|
+
| `[providers]` | Same as `[oauth]`; keeps install commands from 0.8.1 and earlier working |
|
|
24
|
+
| `[mcp]` | Gives the agent the tools of MCP servers |
|
|
25
|
+
|
|
26
|
+
Dependencies are version ranges rather than pins, so the package fits into most existing environments.
|
|
27
|
+
|
|
28
|
+
## Five-minute start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from pi_python import Agent, tool
|
|
32
|
+
from pi_python.providers import AnthropicProvider
|
|
33
|
+
|
|
34
|
+
@tool
|
|
35
|
+
def word_count(text: str) -> int:
|
|
36
|
+
"""Count the words in a text."""
|
|
37
|
+
return len(text.split())
|
|
38
|
+
|
|
39
|
+
agent = Agent(provider=AnthropicProvider(api_key="..."), model="claude-sonnet-4-5", tools=[word_count])
|
|
40
|
+
result = agent.prompt_sync("How many words are in 'to be or not to be'?")
|
|
41
|
+
print(result.messages[-1].content[0].text)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`@tool` builds a tool from the function's signature and docstring; both plain and `async` functions work. In async code, use `await agent.prompt(...)`. Switching to a local model changes two lines:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from pi_python.providers import OpenAICompletionsProvider
|
|
48
|
+
|
|
49
|
+
llm = OpenAICompletionsProvider(base_url="http://localhost:11434/v1", name="ollama")
|
|
50
|
+
agent = Agent(provider=llm, model=llm.model("qwen3:8b", context_window=40960), tools=[word_count])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To try it offline first: `python examples/quickstart.py`.
|
|
54
|
+
|
|
55
|
+
## What it does
|
|
56
|
+
|
|
57
|
+
| Need | How | Example |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Write tools | Decorate a plain function with `@tool`; arguments such as pydantic models, dataclasses, enums and dates are converted automatically; hand-written or MCP-generated JSON Schema works too | [quickstart](examples/quickstart.py) |
|
|
60
|
+
| Connect a model | Claude and GPT through an API key or subscription sign-in; DeepSeek; any OpenAI-compatible server | [local_model](examples/local_model.py), [provider_chat](examples/provider_chat.py) |
|
|
61
|
+
| Use MCP tools | `async with connect_stdio(...) as tools` | [mcp_tools](examples/mcp_tools.py) |
|
|
62
|
+
| Let one agent call another | Wrap the sub-agent as a tool; cancellation propagates down | [subagent](examples/subagent.py) |
|
|
63
|
+
| Intervene mid-run | `steer` injects guidance, `follow_up` queues the next task, `abort` cancels at any time (callable from any thread) | |
|
|
64
|
+
| Context full, service errors | `is_context_overflow` and `is_retryable_error` tell you why, `continue_run()` retries, `transform_context` compacts | [recovery](examples/recovery.py) |
|
|
65
|
+
| Save and restore conversations | `encode_messages` / `decode_messages`; your application decides where to store them | [save_restore](examples/save_restore.py) |
|
|
66
|
+
| Observe and audit | Subscribe to events; hooks before and after tool execution can block or rewrite tool calls | |
|
|
67
|
+
|
|
68
|
+
Apart from `provider_chat`, which needs real credentials, every example runs offline without an API key, and the tests run each one.
|
|
69
|
+
|
|
70
|
+
## Relationship to Pi
|
|
71
|
+
|
|
72
|
+
The run loop, event order, hooks, queues, and the handling of errors and cancellation all match Pi, and this is checked differentially: the same inputs go to the pinned upstream code and to this library, and the model requests, tool calls, events and final transcript are compared item by item. All cases currently agree: 25 for the core loop, 50 for model connectors, 2 for multi-turn WebSocket, and 44 error-classification samples.
|
|
73
|
+
|
|
74
|
+
A few differences are deliberate, such as strict tool-argument validation without type coercion, and returning copies of state to callers. Others are additions for Python users, such as `@tool`, blocking calls, and calling `continue_run()` directly after a failure. Each one is recorded in the [coverage map](compat/COVERAGE.md) (Chinese). Features Pi keeps in its application layer (terminal UI, session file format, context compaction) are not in this core; compaction can be built with hooks, and an example shows the full approach. This project uses its own version numbers and is not an official Pi release.
|
|
75
|
+
|
|
76
|
+
## Documentation
|
|
77
|
+
|
|
78
|
+
- [Concepts on one page: five ideas and one turn](docs/CONCEPTS.md)
|
|
79
|
+
- [Public API](docs/API.md)
|
|
80
|
+
- [Model connectors, subscription sign-in and local models](docs/PROVIDERS.md)
|
|
81
|
+
- [Implementation and verification results](docs/IMPLEMENTATION.md) (Chinese)
|
|
82
|
+
- [Item-by-item comparison with Pi, and deliberate differences](compat/COVERAGE.md) (Chinese)
|
|
83
|
+
- [Rebuilding the reference and checking release candidates](reference/README.md) (Chinese)
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv sync --locked --extra oauth --extra mcp
|
|
89
|
+
uv run pytest -q
|
|
90
|
+
uv run python scripts/verify.py # every check, including the comparison with upstream (needs Node)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
CI is defined in `.github/workflows/ci.yml` and runs on GitHub Actions for every push: each Python version on Linux (including 3.14t and PyPy), plus macOS and Windows.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pi-python-core
|
|
2
2
|
|
|
3
|
+
[English](README.md) | **中文**
|
|
4
|
+
|
|
3
5
|
一个可嵌入的 Python agent 核心,移植自 [Pi](https://github.com/earendil-works/pi)(固定参照 `v1.0.0`)。它负责一件事:把对话发给模型,执行模型要求的工具,把结果交回模型,直到得到回答。工具就是普通的 Python 函数;模型可以是 Claude、GPT、DeepSeek,也可以是本机或集群上的开源模型。
|
|
4
6
|
|
|
5
7
|
PyPI 上的 `pi-agent-core` 是另一个独立项目,移植的是 2026 年初 pi-mono 中的旧版本。本库对照 Pi v1.0.0 的行为,与上游的实际运行结果逐组比较;并自带 Claude、OpenAI、DeepSeek 和本地模型的接入,不需要安装任何模型 SDK。
|
|
@@ -9,15 +11,16 @@ PyPI 上的 `pi-agent-core` 是另一个独立项目,移植的是 2026 年初
|
|
|
9
11
|
支持 Python 3.11–3.14(包括无 GIL 的 3.14t)和 PyPy 3.11,不需要 Node 或任何模型 SDK。
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
pip install
|
|
14
|
+
pip install pi-python-core # 或 uv add pi-python-core
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
安装名是 `pi-python-core`,导入名是 `pi_python`。
|
|
16
18
|
|
|
17
19
|
| 安装选项 | 带来什么 |
|
|
18
20
|
|---|---|
|
|
19
|
-
| 不加选项 |
|
|
20
|
-
| `[
|
|
21
|
+
| 不加选项 | 执行核心和内置模型接入:Claude、OpenAI、Codex、DeepSeek,以及任何 OpenAI 兼容服务(Ollama、vLLM、llama.cpp 等) |
|
|
22
|
+
| `[oauth]` | 用 ChatGPT 账号登录(`openai-chatgpt`)时校验身份令牌,会带进需要编译的 `cryptography`。Claude 订阅和 Codex 登录不需要它 |
|
|
23
|
+
| `[providers]` | 与 `[oauth]` 相同,让 0.8.1 及以前的安装命令仍然可用 |
|
|
21
24
|
| `[mcp]` | 把 MCP 服务器的工具交给 agent |
|
|
22
25
|
|
|
23
26
|
依赖写的是版本范围而不是固定版本,能和大多数已有环境共存。
|
|
@@ -72,9 +75,9 @@ agent = Agent(provider=llm, model=llm.model("qwen3:8b", context_window=40960), t
|
|
|
72
75
|
|
|
73
76
|
## 文档
|
|
74
77
|
|
|
75
|
-
- [一页看懂:五个概念和一轮的流程](docs/CONCEPTS.md)
|
|
76
|
-
- [公开 API](docs/API.md)
|
|
77
|
-
- [模型接入、订阅登录与本地模型](docs/PROVIDERS.md)
|
|
78
|
+
- [一页看懂:五个概念和一轮的流程](docs/zh/CONCEPTS.md)
|
|
79
|
+
- [公开 API](docs/zh/API.md)
|
|
80
|
+
- [模型接入、订阅登录与本地模型](docs/zh/PROVIDERS.md)
|
|
78
81
|
- [实施与验证结果](docs/IMPLEMENTATION.md)
|
|
79
82
|
- [与 Pi 的逐项对照和有意差异](compat/COVERAGE.md)
|
|
80
83
|
- [参照重建与候选版本验证](reference/README.md)
|
|
@@ -82,7 +85,7 @@ agent = Agent(provider=llm, model=llm.model("qwen3:8b", context_window=40960), t
|
|
|
82
85
|
## 开发
|
|
83
86
|
|
|
84
87
|
```bash
|
|
85
|
-
uv sync --locked --extra
|
|
88
|
+
uv sync --locked --extra oauth --extra mcp
|
|
86
89
|
uv run pytest -q
|
|
87
90
|
uv run python scripts/verify.py # 全部检查,含与上游的差分(需要 Node)
|
|
88
91
|
```
|
|
@@ -48,7 +48,7 @@ D1–D8 保持原设计的含义。D1 严格输入;D2 执行前钩子不能通
|
|
|
48
48
|
|
|
49
49
|
新增 Python 合同见 `test_extended_core.py`、`test_providers.py`、`test_oauth.py`。8 个共享网络流输入在 `provider-fixtures/`,实际上游解析器输出和 Python 输出保存在 [provider-conformance.json](results/provider-conformance.json)。它们对比最终内容、签名和停止原因,不等价于所有供应商请求选项或真实账号端到端兼容。
|
|
50
50
|
|
|
51
|
-
当前边界与明确差异见 [PROVIDERS.md](../docs/PROVIDERS.md)。0.1 的 accepted baseline 保留为历史核心验收记录,新增能力单独记录,不修改固定上游提交。
|
|
51
|
+
当前边界与明确差异见 [PROVIDERS.md](../docs/zh/PROVIDERS.md)。0.1 的 accepted baseline 保留为历史核心验收记录,新增能力单独记录,不修改固定上游提交。
|
|
52
52
|
|
|
53
53
|
## 0.3 范围修订
|
|
54
54
|
|
|
@@ -93,7 +93,7 @@ D1–D8 不变。本轮明确记录的差异:WebSocket 帧不带 `stream` 字
|
|
|
93
93
|
这一轮的目标是好装、多版本可用、容易在上面搭完整的 agent。按与上游的关系分三类记录。
|
|
94
94
|
|
|
95
95
|
与上游一致,并有对照证据:
|
|
96
|
-
- **Chat Completions 接入**:逐项移植 `openai-completions.ts`,新增 16 组 Provider 差分(Provider 差分共 50 组),覆盖 Ollama、vLLM、llama.cpp、OpenRouter、DeepSeek 等兼容配置和全部 11 种推理参数格式。与上游的差异见 [PROVIDERS](../docs/PROVIDERS.md#本地模型与-openai-兼容服务)。
|
|
96
|
+
- **Chat Completions 接入**:逐项移植 `openai-completions.ts`,新增 16 组 Provider 差分(Provider 差分共 50 组),覆盖 Ollama、vLLM、llama.cpp、OpenRouter、DeepSeek 等兼容配置和全部 11 种推理参数格式。与上游的差异见 [PROVIDERS](../docs/zh/PROVIDERS.md#本地模型与-openai-兼容服务)。
|
|
97
97
|
- **出错判断**:`is_context_overflow`、`is_retryable_error`、`is_recoverable_length` 移植自 `overflow.ts` 和 `retry.ts`,44 条共享样例与上游函数的实际输出一致(`scripts/recovery_conformance.py`)。另外认识本库自己的错误格式:Python 网络错误名,以及按 HTTP 状态码判断本库的 HTTP 错误;上游格式的文字仍按上游规则判断。
|
|
98
98
|
- **工具 schema**:不再只接受一个子集。与上游一样接受 draft-07 等标准 schema,并按 `format`、`pattern` 校验参数(已用上游校验器对同一 schema 实测);仍只允许指向 schema 内部的引用。D1 的"不转换类型"不变。
|
|
99
99
|
- **HTTP 错误正文**:与上游一样保留,最多 4000 字符;本库另外把请求所带的凭据替换为 `[redacted]`。
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Python API
|
|
2
|
+
|
|
3
|
+
**English** | [中文](zh/API.md)
|
|
4
|
+
|
|
5
|
+
Import the public types from `pi_python`. The minimum Python version is 3.11, and only the `asyncio` backend is used. Read [the one-page concepts](CONCEPTS.md) first. `Agent` makes model requests through a Provider; it never reads keys implicitly and never closes a shared Provider that the caller passed in. Real model connections are covered in [model connectors](PROVIDERS.md).
|
|
6
|
+
|
|
7
|
+
## Agent and run results
|
|
8
|
+
|
|
9
|
+
`Agent(provider=..., model="mock" or ModelInfo(...), options={}, system_prompt="", tools=[], messages=[], hooks=Hooks(), limits=RunLimits(), execution_mode="parallel", steering_mode="one_at_a_time", follow_up_mode="one_at_a_time")`. If there is neither a `provider` nor a default stream set with `set_default_stream_fn`, `prompt` raises `ConfigurationError` immediately.
|
|
10
|
+
|
|
11
|
+
| Method | Behavior |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `await prompt(str_or_message_or_list)` | Appends the input and runs; returns a `RunResult` |
|
|
14
|
+
| `await continue_run()` | Continues from a valid user/tool-result history, or consumes input queued after the final answer; if the last answer failed or was cancelled, retries that turn (see "Recovering from failures"); never replays past tools |
|
|
15
|
+
| `prompt_sync(...)` / `continue_run_sync()` | Blocking versions for plain scripts; Ctrl+C cancels the current run; see "Using it from plain scripts" |
|
|
16
|
+
| `steer(message)` | Accepts guidance at the next safe boundary; also takes a string |
|
|
17
|
+
| `follow_up(message)` | Accepts follow-up input once tools and guidance no longer ask to continue |
|
|
18
|
+
| `abort(reason="requested")` | Requests cancellation and returns immediately; the run then wraps up the way Pi does, see "Cancellation, timeouts and events" below |
|
|
19
|
+
| `await wait_for_idle()` | Waits for local tasks and the critical end-of-run subscribers; raises if cleanup times out |
|
|
20
|
+
| `subscribe(listener)` | Registers a sync or async critical subscriber; returns an unsubscribe function |
|
|
21
|
+
| `update_config(AgentConfigUpdate(...))` | Replaces tools, model or options field by field; during a run, the change waits for the next turn boundary |
|
|
22
|
+
| `clear_queues(steering=True, follow_up=True)` | Explicitly clears the given queues |
|
|
23
|
+
| `await aclose()` | Requests cancellation and waits for this instance's cleanup; does not close a shared Provider |
|
|
24
|
+
|
|
25
|
+
One instance does not accept overlapping `prompt` / `continue_run` calls; the second call raises `AgentBusyError` immediately. `async with Agent(...)` is supported. `state` returns a defensive copy; changing it does not change the history.
|
|
26
|
+
|
|
27
|
+
`RunResult` contains `status` (`completed` / `failed` / `cancelled` / `limit_reached`), the `messages` added by this run, the accumulated numeric `usage`, `stop_reason`, `errors`, `tool_outcomes`, `reconciliation_required`, `cleanup_complete`, and the number of items left in each queue. Usage only sums the top-level numeric fields the Provider reports; it does not estimate cost.
|
|
28
|
+
|
|
29
|
+
`tool_outcomes` records, in call order, the raw arguments, the prepared arguments, `raw_result` and the final `result`. `execution_status` is `not_started`, `running`, `succeeded`, `failed`, `cancelled` or `unknown`. `cancelled` means the tool was cancelled or timed out; `unknown` only comes from a `ToolOutcomeUnknownError` that the tool raised itself. If execution succeeded but the output check or the after-hook failed, the status is still `succeeded` and the final result is an error. No error triggers an automatic retry.
|
|
30
|
+
|
|
31
|
+
## Provider
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
class MyProvider:
|
|
35
|
+
async def stream(self, request, cancel):
|
|
36
|
+
cancel.raise_if_cancelled()
|
|
37
|
+
yield ModelEvent.boundary("start", 0, TextContent(""))
|
|
38
|
+
yield ModelEvent.text("hello")
|
|
39
|
+
yield ModelEvent.boundary("end", 0, TextContent("hello"))
|
|
40
|
+
yield ModelEvent.done(AssistantMessage.text("hello", provider="my-provider", model=request.model))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`stream` returns an async iterator; the caller does not await it first. `ModelRequest` contains `messages`, the replayed `tools`, `model` (the name), the caller-supplied `model_info` (may be None) and `options`. These are copies, isolated from the run configuration: a Provider may change its own copy but cannot change the history through it.
|
|
44
|
+
|
|
45
|
+
There is a single event protocol, the same as upstream: an optional `start`; for each content block, in order, `*_start`, any number of `*_delta`, then `*_end` (block types are text, thinking and toolcall); and finally exactly one `done`, carrying the complete answer. The constructors are `ModelEvent.boundary("start"|"end", index, block)`, `ModelEvent.text(delta, index)`, `ModelEvent.thinking(delta, index)`, `ModelEvent.toolcall(json_fragment, index)` and `ModelEvent.done(message)`; `index` is the block's position in the final answer. A non-streaming Provider may emit only `done`. To fail, a Provider can raise an exception or emit an `error` event (all remote Providers do the latter). Either way, the Agent does what Pi does: it records the answer as a message with `stop_reason="error"`, keeping the partial content already streamed, the provider and model names and the error text, and then calls `finish_turn` and emits `turn_end` as usual.
|
|
46
|
+
|
|
47
|
+
Every event passes the same check: blocks must pair up, deltas must land in an open block of the same type, `done` must be the last event and the iterator must end normally, and the final answer must match the finished blocks (the only allowed addition is upstream's completion of encrypted reasoning content). If the check fails, the turn fails and no tool runs. The usual stop reasons are `stop`, `tool_use`, `length`, `error` and `aborted`.
|
|
48
|
+
|
|
49
|
+
Message data also allows `pending` and `deferred`. `pending` is only for in-stream snapshots and cannot be committed to history; `deferred` can hold a background handle, but the library does not yet poll background tasks. For `length`, every tool call gets a not-executed result, and the model may then handle the error. `error` / `aborted` messages cannot declare tool calls: before the message is committed, its tool calls are removed, the rest of the partial content is kept, and a `removed_tool_calls` entry is added to `diagnostics`.
|
|
50
|
+
|
|
51
|
+
## Tool
|
|
52
|
+
|
|
53
|
+
`Tool(name, description, input_schema, execute, output_schema=None, execution_mode="parallel", prepare_arguments=None)`.
|
|
54
|
+
|
|
55
|
+
`execute(args, context)` can be an async function or a plain function. Plain functions run in a worker thread so they do not block the event loop. Threads cannot be interrupted: after cancellation, the library waits for the function until the cleanup deadline and uses its result if it finishes; past the deadline, it is treated as a tool that could not be stopped (see "Cancellation, timeouts and events"). Long-running tools are better written as async functions. `ToolContext` provides `run_id`, `call_id`, `cancel` and `await emit_update(json_value)`.
|
|
56
|
+
|
|
57
|
+
A tool can return a `ToolResult` or a plain value: a string becomes a text result; `None` becomes empty content; dicts, lists and numbers become their JSON text and are also kept as `structured_content`.
|
|
58
|
+
|
|
59
|
+
`ToolResult.text(text, details=None, structured_content=None, is_error=False, terminate=False)` creates a text result. `details` and `structured_content` are not sent to the model automatically. Content accepts `TextContent` and `ImageContent`. Input is strictly validated by default: types are not coerced and nulls are not dropped. `prepare_arguments(args)` can return new arguments, synchronously or asynchronously, which are then validated the same way; both the original and the converted arguments are kept.
|
|
60
|
+
|
|
61
|
+
Standard JSON Schema is accepted: draft-04, 06, 07, 2019-09 or 2020-12 is chosen by `$schema`, and 2020-12 is used when `$schema` is absent. Schemas generated by pydantic and the schemas of common MCP tools work as they are. As in Pi, argument validation checks `pattern` and `format`. A `format` is checked only when jsonschema has a checker for it: `email`, `date`, `ipv4` and others work out of the box, while `uri`, `date-time` and others need `jsonschema[format-nongpl]` installed. Only a `$ref` pointing inside the schema is allowed; references to the network or to files are rejected at registration. Objects and arrays in a schema may nest at most 100 levels deep (each object or array counts as one level, so one level of `properties` nesting takes two); deeper schemas raise `ConfigurationError` at registration.
|
|
62
|
+
|
|
63
|
+
### Tools from functions
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from pi_python import tool
|
|
67
|
+
|
|
68
|
+
@tool
|
|
69
|
+
def search_papers(query: str, year: int | None = None, limit: int = 10) -> list[dict]:
|
|
70
|
+
"""Search the paper index.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
query: Keywords.
|
|
74
|
+
year: Only papers from this year.
|
|
75
|
+
limit: Maximum number of results.
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`@tool` turns a function into a `Tool`: the function name is the tool name, the first paragraph of the docstring is the description, the parameter types generate the input schema, and argument descriptions in Google, NumPy or Sphinx style go into the schema. You can also write `@tool(name=..., description=..., execution_mode=..., output_schema=...)`, or call `tool(fn)` on an existing function.
|
|
81
|
+
|
|
82
|
+
Supported parameter types: `str`, `int`, `float`, `bool`, `None`, `list`, `set`, `tuple`, `dict[str, T]`, `Literal`, `Enum`, `Optional` and other unions, `Annotated[T, "description"]`, `TypedDict`, dataclasses, `datetime`, `date`, `UUID`, `Path`, and pydantic models. Before the call, the JSON arguments are converted to the types the function asks for, such as enum members, dates, dataclasses or pydantic models. A parameter annotated as `ToolContext` receives the call context and does not appear in the schema. `*args`, `**kwargs` and types that cannot be represented as JSON raise `ConfigurationError` at registration.
|
|
83
|
+
|
|
84
|
+
Concurrency is unlimited by default: a batch of tools all run at once, as in Pi; set `RunLimits(max_concurrency=...)` when you need a cap. If any tool requires `sequential`, the whole batch runs one at a time. When running concurrently, preparation completes in call order before execution starts; end events follow the order in which post-processing finishes, and results in history follow call order.
|
|
85
|
+
|
|
86
|
+
A standalone program can call `await run_tool_call(tool, call, context, before_tool_call=..., after_tool_call=...)` to reuse the same validation and hook path. For standalone calls, the program manages lifetime, timeouts and cancellation itself; `Agent` adds batch management and `RunLimits`.
|
|
87
|
+
|
|
88
|
+
## Hooks
|
|
89
|
+
|
|
90
|
+
Every hook can be a sync or an async function. Contexts and arguments are passed as copies; the cancel token and the tool-update channel are shared control interfaces.
|
|
91
|
+
|
|
92
|
+
| Hook | Arguments and return value |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `prepare_request(context, cancel)` | Returns a `TurnUpdate` or None; called before every model request |
|
|
95
|
+
| `prepare_next_turn(context, cancel)` | Returns a `TurnUpdate` or None; called from the second turn on |
|
|
96
|
+
| `transform_context(messages, cancel)` | Returns the message list used for the model request; runs before convert |
|
|
97
|
+
| `convert_to_llm(messages)` | Returns the message list that can be sent to the model; must handle or explicitly filter out `CustomMessage` |
|
|
98
|
+
| `before_tool_call(call, args, context)` | True / None allows, False blocks, or return a `ToolResult` to use without executing; if the hook raises, that call gets an error result (`hook_error`) and the run continues, as in Pi |
|
|
99
|
+
| `after_tool_call(call, result, context)` | Returns a full `ToolResult`, a partial `ToolResultUpdate`, or None |
|
|
100
|
+
| `finish_turn(context, cancel)` | Returns `"continue"`, `"end"` or None |
|
|
101
|
+
|
|
102
|
+
For the preparation and finish hooks, `context` is a `RunContext` containing `messages`, `model`, `options`, `tools`, the latest `message` and `tool_results`. `TurnUpdate(model=..., options=..., tools=..., context=..., messages=...)` affects the rest of this run without changing the Agent's defaults. `context` replaces this run's model context without rewriting committed history; `messages` are new inputs to append. Only an explicit `update_config` changes the defaults for the next run. Options are replaced field by field as whole values, not deep-merged.
|
|
103
|
+
|
|
104
|
+
When an after-hook replaces the text with `ToolResultUpdate(content=[...])`, the old `structured_content` is cleared; to keep it, supply new structured content at the same time. Successful results are checked against the output schema both before and after the after-hook. `terminate=True` suppresses the next request triggered by tools only if it holds for every final result in the batch; queued input can still trigger continuation. `finish_turn="end"` does not consume pending queues.
|
|
105
|
+
|
|
106
|
+
## Cancellation, timeouts and events
|
|
107
|
+
|
|
108
|
+
By default, `RunLimits` does not limit model requests, tool calls or tool concurrency, as in Pi; applications set `max_model_requests`, `max_tool_calls` and `max_concurrency` when they need them. The cleanup deadline defaults to 1 second. `tool_timeout` and `run_timeout` default to None and are measured on a monotonic clock. When a tool-call cap is set, it is checked before a batch starts; if the remaining allowance is not enough, none of the batch runs.
|
|
109
|
+
|
|
110
|
+
An explicit `abort` ends the run the way Pi does. `abort` can be called from any thread, for example from a GUI stop button or a watchdog thread:
|
|
111
|
+
|
|
112
|
+
- Cancellation goes through asyncio task cancellation and reaches the running model stream, tools and hooks, much like Pi's AbortSignal. Each operation ends in its own way within the cleanup deadline.
|
|
113
|
+
- When the model stream is cancelled, the partial content already streamed is recorded as an answer with `stop_reason="aborted"`, and `finish_turn` and `turn_end` happen as usual.
|
|
114
|
+
- When a tool is cancelled, it gets an ordinary error result, `Operation aborted` (`execution_status="cancelled"`). A result the tool returns itself after catching the cancellation is used as usual, and completed results are kept. After the whole batch is committed, as in Pi, one more turn begins: it sends no model request, records an aborted answer and ends.
|
|
115
|
+
- `prompt` returns `status="cancelled"`, and the Agent remains usable.
|
|
116
|
+
|
|
117
|
+
When the caller cancels the Python Task running `prompt`, the flow above does not apply: the library performs bounded cleanup and re-raises `asyncio.CancelledError`.
|
|
118
|
+
|
|
119
|
+
When a run fails or is cancelled outside the model answer (for example, a preparation hook raises), an `error` / `aborted` answer is appended and `turn_end` is emitted, like Pi's `handleRunFailure`. A subscriber failure is the exception: publishing stops and nothing more is appended.
|
|
120
|
+
|
|
121
|
+
A tool timeout likewise cancels that tool; the result is the error `tool_timeout`, which goes to the model, and the run continues.
|
|
122
|
+
|
|
123
|
+
When a tool cannot confirm whether an external commit succeeded, it can raise `ToolOutcomeUnknownError`. This is a guard this library adds beyond Pi: it stops the current run and refuses later prompts, continues and configuration updates, so that the application can reconcile externally and create a new instance with corrected history. Cancellation and timeouts do not trigger this guard.
|
|
124
|
+
|
|
125
|
+
Python cannot forcibly stop an uncooperative coroutine or the thread running a plain function. If a tool still has not finished by the cleanup deadline after cancellation or a timeout, its result is the error `not_stopped`, the run stops at once, and no more model requests are sent. Until the tool actually finishes, the instance stays busy: `cleanup_complete=False`, and `prompt`, `wait_for_idle` and `aclose` raise `CleanupTimeoutError`. As soon as the tool finishes, the instance becomes usable again. Applications that need to force termination should manage subprocesses themselves. A timeout or cancellation result does not mean the external operation was rolled back.
|
|
126
|
+
|
|
127
|
+
Events are `agent_start/end`, `turn_start/end`, `message_start/update/end`, `tool_execution_start/update/end`, and `config_update` for explicit configuration changes. The envelope carries `schema_version=1`, the run ID, the turn ID, a monotonic sequence number and an optional call ID. Each subscriber receives its own copy. Subscribers are awaited in order; if one fails, execution stops, and `state.diagnostics` records the failing subscriber and those not yet called. The failing subscriber is not called again recursively.
|
|
128
|
+
|
|
129
|
+
A subscriber callback must not wait for this instance to become idle, or it will end up waiting on itself. A display layer can use a bounded `EventQueue(maxsize=128, drop_text_updates=True)`: only message deltas may be dropped, the count is kept in `dropped_updates`, and final tool events always apply backpressure. On the normal path, `prompt` returning means the critical subscribers have handled the end events.
|
|
130
|
+
|
|
131
|
+
## Recovering from failures
|
|
132
|
+
|
|
133
|
+
When the model errors or is cancelled, the answer stays in history with `stop_reason="error"` or `"aborted"`, and its `error` field holds the original error text. HTTP failures also carry the response body returned by the server (at most 4000 characters, with the key used for this request replaced by `[redacted]`). The functions below are ported from upstream pi-ai. They read that answer to tell why it failed, and on 44 shared samples their verdicts match upstream:
|
|
134
|
+
|
|
135
|
+
| Function | Purpose |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `is_context_overflow(message, context_window=None)` | Whether it failed because the context was too long. Given the window size, it also recognizes silent overflow (reported input larger than the window) and a length stop with zero output after truncation |
|
|
138
|
+
| `is_retryable_error(message)` | Whether it looks transient: overload, rate limit, 5xx, network interruption, stream ended early. Quota and billing problems do not count |
|
|
139
|
+
| `is_recoverable_length(message, desired_max_output)` | A length stop with less output than the desired limit, which may be caused by context pressure |
|
|
140
|
+
| `retry_delay(attempt, base=2.0, max_delay=60.0)` | Seconds to wait before retry number `attempt`, with exponential backoff |
|
|
141
|
+
|
|
142
|
+
`continue_run()` retries the turn when the last answer failed or was cancelled and there is no queued input. The failed answer stays in history and is skipped when the history is replayed to the model. This differs from Pi, whose application layer deletes the failed answer before continuing. [examples/recovery.py](../examples/recovery.py) shows the full approach: on overflow, use `transform_context` to replace the earliest turns with a summary and continue; on a transient error, back off with `retry_delay` and continue.
|
|
143
|
+
|
|
144
|
+
## Using it from plain scripts
|
|
145
|
+
|
|
146
|
+
`agent.prompt_sync(...)`, `agent.continue_run_sync()` and the general `run_sync(awaitable)` block and run in plain scripts, so you do not have to write `asyncio.run` yourself. They share one background event loop, so connections cached by a Provider stay usable across calls. On Ctrl+C, the current run wraps up the way `abort` does, and then `KeyboardInterrupt` is raised.
|
|
147
|
+
|
|
148
|
+
Where an event loop is already running (async programs, top-level await in Jupyter), these functions raise an error suggesting `await agent.prompt(...)` instead. Use one style per Agent: either only the blocking versions, or only await in your own event loop.
|
|
149
|
+
|
|
150
|
+
## MCP tools
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from pi_python.mcp import connect_stdio
|
|
154
|
+
|
|
155
|
+
async with connect_stdio("uvx", ["mcp-server-fetch"], prefix="web") as tools:
|
|
156
|
+
agent = Agent(provider=..., tools=tools)
|
|
157
|
+
await agent.prompt("Summarize https://example.org")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Requires `pip install 'pi-python-core[mcp]'` and works with the official MCP SDK 1.10 and later, including 2.x. `connect_stdio` starts a stdio MCP server and closes it when the `async with` block exits. If you already have a `ClientSession`, wrap its tools with `await mcp_tools(session, prefix=None, names=None)`. Conversion follows Pi's MCP adapter: text and images convert directly; embedded text or image resources are unpacked; audio, resource links and binary resources become short text descriptions; a result with only structured content becomes JSON text and is also kept as `structured_content`; MCP's `isError` becomes a tool error; progress notifications become `tool_execution_update` events. After the prefix is added, tool names keep only letters, digits, `_` and `-`, up to 64 characters. A tool whose schema cannot be used is skipped with a warning, without affecting the server's other tools. A stdio server written with Python MCP SDK 2.3 cannot start on PyPy (`fcntl.F_DUPFD_CLOEXEC` is missing); this is a limitation of the SDK itself, and the client side is unaffected.
|
|
161
|
+
|
|
162
|
+
## Messages and encoding
|
|
163
|
+
|
|
164
|
+
The public message types are `SystemMessage`, `UserMessage`, `AssistantMessage`, `ToolResultMessage` and `CustomMessage`; the content blocks are `TextContent`, `ImageContent`, `ThinkingContent` and `ToolCall`. User messages and tool results can carry images; assistant messages can carry signed text, reasoning and tool calls. A `SystemMessage`'s text appends instructions, and `sections` replaces sections by name (None deletes one); tool declarations are replayed through `tools_added` / `tools_removed`.
|
|
165
|
+
|
|
166
|
+
`encode_messages` / `decode_messages` and `encode_event` / `decode_event` are pure string conversions; they do not read or write files. The message schema version is 3 and the event envelope version is 1; any other version is rejected. NaN, Infinity, functions and file handles are not accepted, and dangling calls, duplicate results and unknown versions are rejected. Importing history only validates data; it never runs the model or tools. External adapters can use `current_tools`, `current_system_message` and `current_system_prompt` to replay state.
|
|
167
|
+
|
|
168
|
+
## Standalone loop and convenience APIs
|
|
169
|
+
|
|
170
|
+
`agent_loop(prompts, AgentContext(...), AgentLoopConfig(...), cancel=None)` and `agent_loop_continue(context, config, cancel=None)` return an `AgentEventStream`, which supports `async for` and `await stream.result()`. `run_agent_loop(..., emit=None, cancel=None)` / `run_agent_loop_continue(...)` return the new messages directly and accept a sync or async event callback. They reuse the Agent's execution engine. The continue versions write history back into the context passed in; the prompt versions leave the original context unchanged, matching upstream.
|
|
171
|
+
|
|
172
|
+
The configuration includes `provider` or `stream_fn`, `model`, `options`, `hooks`, `limits`, `tool_execution`, and `get_steering_messages` / `get_follow_up_messages`. The queue callbacks are called at the same boundaries as upstream and can return message lists synchronously or asynchronously.
|
|
173
|
+
|
|
174
|
+
The event stream uses a bounded queue. If you only wait for the result, `result()` consumes the events; if you use `async for`, consume to the end before reading the result. To give up midway, call `await stream.aclose()`. Do not just wait for the producer to finish while you have paused consuming, or backpressure will block it.
|
|
175
|
+
|
|
176
|
+
`set_default_stream_fn(provider_or_function)` sets an explicit process-wide default stream; pass None to clear it. `Agent` can then omit the provider, or use `stream_fn=`. `reset()` clears the conversation and queues but keeps the replayed system prompt and tool declarations; it is refused during a run. An instance whose tool reported an unknown outcome still has to be rebuilt after reconciliation; reset does not clear that guard.
|
|
177
|
+
|
|
178
|
+
`has_queued_messages()`, `peek_queued_messages()`, `clear_steering_queue()`, `clear_follow_up_queue()` and `clear_all_queues()` correspond to upstream's convenience methods. Peeking prefers steering and previews follow-up only when steering is empty; it returns copies. `signal` returns the active `CancelToken` during a run and None when idle. `prompt(text, images=[...])` is a shortcut for sending images.
|
|
179
|
+
|
|
180
|
+
An Agent can also be constructed with `thinking_level`, `thinking_budgets`, `transport`, `session_id`, `get_api_key`, `on_payload`, `on_response` and `on_provider_stream_event`. The last four can also go in Hooks; explicit constructor arguments take precedence. Their protocols, and the Provider parameters, are described in [the connector guide](PROVIDERS.md).
|
|
181
|
+
|
|
182
|
+
## Fields of messages, tool results and events
|
|
183
|
+
|
|
184
|
+
`SystemMessage.content` accepts a string or a list of text blocks. `AssistantMessage` also contains `response_model`, `response_id`, `thinking_level`, `diagnostics`, `raw_stop_reason`, `end_turn` and `deferred`. `provider_thinking_level` is the provider's native effort; `thinking_level` is the requested level. Fields whose value is null in arbitrary user JSON are preserved intact.
|
|
185
|
+
|
|
186
|
+
`ToolResult`, `ToolResultUpdate` and the history's `ToolResultMessage` support `usage` and `nested_calls`; history also stores `details`. Nested-call records have the form `{complete: bool, calls: [{id, name, status, ...}]}`, where status is ok/error/unfinished. details, usage and nested_calls are stripped before anything is sent to the main model. Tool execution and after-hooks get assistant_message, agent_context, tool_call, args, result and is_error through `ToolContext`; `RunContext.new_messages` collects the messages added by this run. All of these contexts are copies.
|
|
187
|
+
|
|
188
|
+
Among checked events, non-terminal events carry `partial` (an independent snapshot that does not change with the next event); block boundaries carry `block`, text/reasoning ends carry `content`, tool deltas carry that block's `call_id` and `name`, and terminal events carry `message` and `reason`. Tool-argument previews may be incomplete, but execution uses the strictly parsed final arguments. The data of the Agent's `message_update` event is `delta_type`, `block_index`, `delta`, `content`, `tool_call_id` and `partial`.
|
|
189
|
+
|
|
190
|
+
OpenAI may add encrypted_content back in the final response. If the visible reasoning and the other signature fields are identical and only the missing encrypted content is added, the terminal message may carry that one extra field compared with the block-end snapshot. Text, arguments and tool identity must still match.
|
|
191
|
+
|
|
192
|
+
## Mid-session changes and context estimation
|
|
193
|
+
|
|
194
|
+
`render_system_update(message)` returns the text sent when a later system message is delivered in place: after the body, each section is written as `Updated system prompt section "name":` followed by the new content, or `Removed system prompt section "name".`. When a system message consists of several text blocks, the blocks are joined with a single newline, the same as upstream's `contentText`. How each Provider sends mid-session changes to tools, instructions and reasoning effort is described in [the connector guide](PROVIDERS.md#mid-session-changes).
|
|
195
|
+
|
|
196
|
+
`estimate_context_tokens(messages)` ports upstream's character-based estimate: the latest valid usage record plus an estimate of the messages after it, at about 1 token per 4 UTF-16 characters, with each image counted as 4800 characters. It is not a tokenizer. `clamp_max_tokens_to_context(context_window, messages, max_tokens)` uses it to reserve 4096 safety tokens, with a result of at least 1; Providers use it to shorten the output limit of known models.
|
|
197
|
+
|
|
198
|
+
`AssistantMessage.provider_thinking_level` matches upstream: it records this answer's effort only for Claude models that use effort markers, so later requests can rebuild the markers in history. Read `thinking_level` when you need the requested level.
|
|
199
|
+
|
|
200
|
+
When replaying history, Providers skip `error` / `aborted` answers following upstream's rules; reasoning from a different model is converted to plain text and its signature is dropped; a system message that sits between a tool call and its result is moved after the result. The session history itself is unchanged.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# pi-python on one page
|
|
2
|
+
|
|
3
|
+
**English** | [中文](zh/CONCEPTS.md)
|
|
4
|
+
|
|
5
|
+
This library does one thing: it repeatedly sends the conversation to a model, runs the tools the model asks for, and hands the results back to the model, until the model gives an answer. Understand the five concepts below and you can read all of the code.
|
|
6
|
+
|
|
7
|
+
## Five concepts
|
|
8
|
+
|
|
9
|
+
| Concept | What it is | Where |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Transcript** | An append-only list of messages: user input, model answers, tool results, and system messages. System instructions and tool declarations are written as system messages too, so every change made mid-conversation stays in the record | `messages.py`, `transcript.py` |
|
|
12
|
+
| **Agent** | One conversation. It holds the transcript, the default configuration (model, options, tools), two input queues and the event subscribers. It runs one prompt at a time | `agent.py`, `queues.py` |
|
|
13
|
+
| **Run** | One execution of `prompt` or `continue_run`. It holds that run's cancel token, background tasks and tool-execution records, and packages them as a `RunResult` at the end | `run.py`, `loop.py` |
|
|
14
|
+
| **Provider** | A function that takes a request and produces a stream of model events, the last of which is the complete answer. The Agent does not know whether HTTP, WebSocket or a script is behind it | `provider.py`, `providers/` |
|
|
15
|
+
| **Tool** | A name, a description, an argument schema and an execute function (async or plain). Usually generated with `@tool` from a type-annotated function. Arguments are strictly validated before execution, and the result is written back to the transcript | `tools.py`, `function_tools.py` |
|
|
16
|
+
|
|
17
|
+
Model capabilities are not a sixth concept but an input to the Provider: `ModelInfo` records the context length, output limit, reasoning levels and server-side features. When you give a real Provider a model name, it looks the name up in the built-in model table; if the name is missing, it raises an error, and you pass a `ModelInfo` instead. The scripted `ScriptedProvider` needs no model table.
|
|
18
|
+
|
|
19
|
+
## What happens in one turn
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
prompt("…")
|
|
23
|
+
│
|
|
24
|
+
▼
|
|
25
|
+
Accept input (queued guidance; tool changes are written as system messages)
|
|
26
|
+
│
|
|
27
|
+
▼
|
|
28
|
+
pre-request hooks ─► transform_context ─► convert_to_llm
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
Provider stream: start → for each content block start/delta/end → done
|
|
32
|
+
│ (events are checked first: blocks must pair up, deltas must land in an open block,
|
|
33
|
+
│ and the final answer must match the finished blocks; otherwise the turn fails and no tool runs)
|
|
34
|
+
▼
|
|
35
|
+
Answer written to the transcript
|
|
36
|
+
│
|
|
37
|
+
├─ no tool calls ──► check the follow-up queue ──► end if it is empty
|
|
38
|
+
│
|
|
39
|
+
└─ tool calls ──► validate arguments → before_tool_call → execute → after_tool_call
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
results written in call order ──► finish_turn ──► next turn
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
All hooks are optional. When the model errors or is cancelled, the answer (with whatever was already generated) is recorded in history as usual, just as in Pi; `finish_turn` and `turn_end` run, and then the run ends. The run also ends when it reaches a limit the application has set. `RunResult.status` says why it ended.
|
|
46
|
+
|
|
47
|
+
## Minimal example
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from pi_python import Agent, AssistantMessage, ScriptedProvider, Tool, ToolCall, ToolResult
|
|
51
|
+
|
|
52
|
+
async def add(args, context):
|
|
53
|
+
return ToolResult.text(str(args["a"] + args["b"]))
|
|
54
|
+
|
|
55
|
+
provider = ScriptedProvider([
|
|
56
|
+
AssistantMessage([ToolCall("c1", "add", {"a": 2, "b": 3})], "tool_use"),
|
|
57
|
+
AssistantMessage.text("5"),
|
|
58
|
+
])
|
|
59
|
+
schema = {"type": "object", "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
|
|
60
|
+
"required": ["a", "b"], "additionalProperties": False}
|
|
61
|
+
result = await Agent(provider=provider, tools=[Tool("add", "Add", schema, add)]).prompt("2+3?")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To use a real model, change only the Provider and the model name: `Agent(provider=AnthropicProvider(api_key=...), model="claude-sonnet-5-5", ...)`.
|
|
65
|
+
|
|
66
|
+
## What goes beyond Pi
|
|
67
|
+
|
|
68
|
+
Pi leaves the following to applications; this library puts them in the core for research and HPC use. Read about them when you need them:
|
|
69
|
+
|
|
70
|
+
- Strict argument validation, with no automatic conversion of strings to numbers (write `prepare_arguments` if you need it);
|
|
71
|
+
- `RunLimits`: the cleanup deadline after cancellation, plus optional caps on model requests, tool calls and tool concurrency, a tool timeout and a total run time (none of these caps is set by default; as in Pi, the application decides);
|
|
72
|
+
- When a tool reports that its outcome is "unknown" (for example, the connection dropped after a job was submitted), the run stops and waits for the application to reconcile, with no automatic retry. Cancelling a tool, or a tool timing out, does not trigger this guard; it is recorded as an ordinary error result;
|
|
73
|
+
- State returned to callers is always a copy, and configuration updates made during a run take effect at the next turn.
|
|
74
|
+
|
|
75
|
+
Details are in the [API](API.md); model connectors (including local models) are in [PROVIDERS](PROVIDERS.md); the item-by-item comparison with Pi is in the [coverage map](../compat/COVERAGE.md) (Chinese). Common patterns for building fuller agents all have runnable examples: [sub-agents](../examples/subagent.py), [saving and restoring conversations](../examples/save_restore.py), [compaction on overflow and retry on errors](../examples/recovery.py), [MCP tools](../examples/mcp_tools.py), [local models](../examples/local_model.py).
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
0.8.0 发布时 macOS 和 Windows 还没有实际运行过。之后 CI 在 GitHub 上首次运行,发现了 Windows 和 PyPy 上的问题;0.8.1 只包含这些修复,见[验收映射](../compat/COVERAGE.md#081-跨平台修复)。
|
|
30
30
|
|
|
31
|
+
0.8.2 调整了安装选项,执行行为没有变化:`httpx` 和 `websockets` 成为默认依赖,`pip install pi-python-core` 装完即可连接模型;只有 ChatGPT 账号登录需要的 PyJWT 放进 `[oauth]`,`[providers]` 保留为它的别名,缺少时报错会写明要装哪个选项。用户文档改为中英双语:英文是默认版本,中文在 `README.zh-CN.md` 和 `docs/zh/`;验证记录仍只有中文。
|
|
32
|
+
|
|
31
33
|
## 复现与验证附录
|
|
32
34
|
|
|
33
|
-
- 说明文档:[一页概念](CONCEPTS.md)、[API](API.md)、[模型接入与本地模型](PROVIDERS.md)、[验收映射](../compat/COVERAGE.md)
|
|
35
|
+
- 说明文档:[一页概念](zh/CONCEPTS.md)、[API](zh/API.md)、[模型接入与本地模型](zh/PROVIDERS.md)、[验收映射](../compat/COVERAGE.md)
|
|
34
36
|
- 全部检查命令与输出:[verification.json](../compat/results/verification.json),命令 `uv run python scripts/verify.py`
|
|
35
37
|
- 差分结果:[核心](../compat/results/conformance.json)、[Provider](../compat/results/provider-conformance.json)、[WebSocket 多轮](../compat/results/websocket-conformance.json)、[出错判断](../compat/results/recovery-conformance.upstream.json)
|
|
36
38
|
- Linux 安装:[3.11](../compat/results/linux-3.11.json)、[3.12](../compat/results/linux-3.12.json)、[3.13](../compat/results/linux-3.13.json)、[3.14](../compat/results/linux-3.14.json),命令 `uv run python scripts/linux_verify.py`
|