handoffkit 1.3.0__tar.gz → 1.4.0__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.
Files changed (158) hide show
  1. {handoffkit-1.3.0/handoffkit.egg-info → handoffkit-1.4.0}/PKG-INFO +51 -1
  2. {handoffkit-1.3.0 → handoffkit-1.4.0}/README.md +50 -0
  3. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/mai_clinical_mimo_benchmark.py +8 -0
  4. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/opencode_go_agent.py +21 -5
  5. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/opencode_zen_agent.py +21 -5
  6. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/__init__.py +1 -1
  7. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/cli.py +149 -1
  8. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/providers/__init__.py +20 -0
  9. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/providers/opencode_provider.py +83 -16
  10. handoffkit-1.4.0/handoffkit/providers/registry.py +316 -0
  11. {handoffkit-1.3.0 → handoffkit-1.4.0/handoffkit.egg-info}/PKG-INFO +51 -1
  12. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit.egg-info/SOURCES.txt +2 -0
  13. {handoffkit-1.3.0 → handoffkit-1.4.0}/pyproject.toml +1 -1
  14. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_api_stability.py +2 -2
  15. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_cli.py +40 -1
  16. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_clinical_benchmark_audit.py +23 -1
  17. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_opencode_provider_mock.py +71 -0
  18. handoffkit-1.4.0/tests/test_provider_registry.py +135 -0
  19. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_stable_metadata.py +2 -2
  20. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_wheel_smoke.py +1 -1
  21. {handoffkit-1.3.0 → handoffkit-1.4.0}/LICENSE +0 -0
  22. {handoffkit-1.3.0 → handoffkit-1.4.0}/MANIFEST.in +0 -0
  23. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/async_demo.py +0 -0
  24. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/coding_review.py +0 -0
  25. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/coding_review_recipe.py +0 -0
  26. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/coding_team.py +0 -0
  27. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/context_handoff_demo.py +0 -0
  28. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/contract_validation_demo.py +0 -0
  29. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/doctor_benchmark.py +0 -0
  30. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/doctor_orchestrator.py +0 -0
  31. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/evaluation_demo.py +0 -0
  32. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/extension_demo.py +0 -0
  33. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/fake_provider_tool_call_demo.py +0 -0
  34. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/freemodel_best_model_demo.py +0 -0
  35. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/freemodel_coding_team.py +0 -0
  36. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/freemodel_openai_compatible.py +0 -0
  37. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/handoff_demo.py +0 -0
  38. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/handoff_quality_demo.py +0 -0
  39. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/langgraph_integration.py +0 -0
  40. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/mai_clinical_ensemble_benchmark.py +0 -0
  41. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/mai_clinical_mimo_rescue.py +0 -0
  42. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/mai_clinical_targeted_rescue.py +0 -0
  43. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/mai_style_doctor_benchmark.py +0 -0
  44. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/medical_tools_demo.py +0 -0
  45. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/memory_demo.py +0 -0
  46. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/ollama_agent.py +0 -0
  47. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/openai_agents_sdk_integration.py +0 -0
  48. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/project_context_demo.py +0 -0
  49. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/provider_matrix_demo.py +0 -0
  50. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/provider_tool_adapter_demo.py +0 -0
  51. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/provider_tool_formats_demo.py +0 -0
  52. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/pydantic_ai_integration.py +0 -0
  53. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/real_task_calculator.py +0 -0
  54. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/recipe_demo.py +0 -0
  55. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/replay_demo.py +0 -0
  56. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/repo_inspired_protocols.py +0 -0
  57. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/research_workflow.py +0 -0
  58. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/simple_agent.py +0 -0
  59. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/structured_output_demo.py +0 -0
  60. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/structured_recipe_demo.py +0 -0
  61. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/support_escalation.py +0 -0
  62. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/template_demo.py +0 -0
  63. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/tool_agent.py +0 -0
  64. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/tool_execution_demo.py +0 -0
  65. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/tool_schema_demo.py +0 -0
  66. {handoffkit-1.3.0 → handoffkit-1.4.0}/examples/trace_demo.py +0 -0
  67. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/agent.py +0 -0
  68. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/builtins.py +0 -0
  69. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/context.py +0 -0
  70. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/doctor_benchmark.py +0 -0
  71. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/doctor_cases_100.py +0 -0
  72. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/doctor_cases_30.py +0 -0
  73. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/doctor_cases_400.py +0 -0
  74. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/errors.py +0 -0
  75. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/evaluation.py +0 -0
  76. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/extensions.py +0 -0
  77. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/handoff.py +0 -0
  78. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/mai_benchmark.py +0 -0
  79. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/memory.py +0 -0
  80. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/protocol.py +0 -0
  81. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/protocols/__init__.py +0 -0
  82. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/protocols/compressed.py +0 -0
  83. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/protocols/hybrid_min.py +0 -0
  84. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/protocols/hybrid_state.py +0 -0
  85. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/protocols/natural.py +0 -0
  86. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/provider_adapters.py +0 -0
  87. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/providers/base.py +0 -0
  88. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/providers/echo_provider.py +0 -0
  89. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/providers/ollama_provider.py +0 -0
  90. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/providers/openai_compatible.py +0 -0
  91. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/providers/openai_provider.py +0 -0
  92. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/py.typed +0 -0
  93. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/quality.py +0 -0
  94. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/recipes.py +0 -0
  95. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/replay.py +0 -0
  96. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/reports.py +0 -0
  97. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/runner.py +0 -0
  98. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/safety.py +0 -0
  99. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/schemas.py +0 -0
  100. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/showcases.py +0 -0
  101. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/structured.py +0 -0
  102. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/templates.py +0 -0
  103. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tool.py +0 -0
  104. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tool_execution.py +0 -0
  105. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tool_factory.py +0 -0
  106. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tools/__init__.py +0 -0
  107. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tools/filesystem.py +0 -0
  108. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tools/medical.py +0 -0
  109. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tools/shell.py +0 -0
  110. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tools/text.py +0 -0
  111. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/tracing.py +0 -0
  112. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit/validation.py +0 -0
  113. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit.egg-info/dependency_links.txt +0 -0
  114. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit.egg-info/entry_points.txt +0 -0
  115. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit.egg-info/requires.txt +0 -0
  116. {handoffkit-1.3.0 → handoffkit-1.4.0}/handoffkit.egg-info/top_level.txt +0 -0
  117. {handoffkit-1.3.0 → handoffkit-1.4.0}/setup.cfg +0 -0
  118. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_070_examples.py +0 -0
  119. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_adoption_docs.py +0 -0
  120. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_agent.py +0 -0
  121. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_async_runtime.py +0 -0
  122. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_cli_doctor.py +0 -0
  123. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_clinical_ensemble_benchmark.py +0 -0
  124. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_clinical_targeted_rescue.py +0 -0
  125. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_coding_team.py +0 -0
  126. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_contract_validation.py +0 -0
  127. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_docs_090.py +0 -0
  128. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_doctor_benchmark.py +0 -0
  129. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_evaluation.py +0 -0
  130. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_handoff.py +0 -0
  131. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_imports.py +0 -0
  132. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_json_schemas.py +0 -0
  133. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_mai_benchmark.py +0 -0
  134. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_medical_tools.py +0 -0
  135. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_memory_context.py +0 -0
  136. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_ollama_provider_mock.py +0 -0
  137. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_openai_model_selection.py +0 -0
  138. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_openai_provider_mock.py +0 -0
  139. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_protocols.py +0 -0
  140. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_provider_adapters.py +0 -0
  141. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_provider_matrix.py +0 -0
  142. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_public_api_compatibility.py +0 -0
  143. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_quality.py +0 -0
  144. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_real_task_demo.py +0 -0
  145. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_recipes_extensions.py +0 -0
  146. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_release_process_docs.py +0 -0
  147. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_replay.py +0 -0
  148. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_reports.py +0 -0
  149. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_shell_safety.py +0 -0
  150. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_showcases.py +0 -0
  151. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_structured_outputs.py +0 -0
  152. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_team.py +0 -0
  153. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_templates.py +0 -0
  154. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_tool.py +0 -0
  155. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_tool_execution.py +0 -0
  156. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_tool_factory.py +0 -0
  157. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_tracing.py +0 -0
  158. {handoffkit-1.3.0 → handoffkit-1.4.0}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: handoffkit
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: A lightweight Python framework for building multi-agent workflows with structured state transfer protocols.
5
5
  Author: DaosPath
6
6
  License-Expression: MIT
@@ -231,6 +231,51 @@ clinical_trials_search.run(condition="systemic sclerosis", max_results=3)
231
231
  dailymed_drug_search.run(drug_name="ibuprofen", max_results=3)
232
232
  ```
233
233
 
234
+ ## What 1.4.0 Adds
235
+
236
+ HandoffKit 1.4.0 adds experimental provider routing:
237
+
238
+ - provider registry entries for OpenCode Go, OpenCode Zen, OpenAI-compatible
239
+ endpoints, and Ollama,
240
+ - `ProviderSelector` for offline model lists and real opt-in probes,
241
+ - `ProviderRouter` for ordered fallback between model candidates,
242
+ - retry/backoff for transient OpenCode `429`, `5xx`, and transport failures,
243
+ - `handoffkit providers ...` CLI commands that never call real APIs unless
244
+ `--real` is passed.
245
+
246
+ ```bash
247
+ handoffkit providers list
248
+ handoffkit providers models --provider opencode-go
249
+ handoffkit providers select --provider opencode-go --models mimo-v2.5,deepseek-v4-flash --json
250
+ ```
251
+
252
+ Real probes require an explicit flag and a scoped provider key:
253
+
254
+ ```powershell
255
+ $env:OPENCODE_API_KEY="..."
256
+ handoffkit providers probe --provider opencode-go --models mimo-v2.5,deepseek-v4-flash --real
257
+ ```
258
+
259
+ ## Provider Registry and Model Routing
260
+
261
+ The provider registry is experimental in 1.4.0. It is intended for diagnostics,
262
+ model selection, and fallback without adding heavy provider SDKs.
263
+
264
+ ```python
265
+ from handoffkit.providers import ModelCandidate, ProviderRouter
266
+
267
+ router = ProviderRouter(
268
+ [
269
+ ModelCandidate("opencode-go", "mimo-v2.5"),
270
+ ModelCandidate("opencode-go", "deepseek-v4-flash"),
271
+ ]
272
+ )
273
+
274
+ print(router.generate("Create a concise release checklist."))
275
+ ```
276
+
277
+ Docs: [`docs/PROVIDERS.md`](docs/PROVIDERS.md).
278
+
234
279
  ## What 1.1.0 Adds
235
280
 
236
281
  HandoffKit 1.1.0 focuses on adoption:
@@ -256,6 +301,8 @@ Use it as the contract and reporting layer beside orchestration frameworks:
256
301
  - [`Pydantic AI`](docs/integrations/PYDANTIC_AI.md): typed outputs become workflow handoff state,
257
302
  - [`OpenCode Go / Zen`](docs/integrations/OPENCODE.md): provider adapters route
258
303
  OpenCode model families to their matching API endpoint styles.
304
+ - [`Provider Registry`](docs/PROVIDERS.md): experimental provider listing,
305
+ probing, model selection, and fallback routing.
259
306
 
260
307
  Runnable offline examples:
261
308
  [`examples/langgraph_integration.py`](examples/langgraph_integration.py),
@@ -1085,6 +1132,9 @@ handoffkit demo-trace
1085
1132
  handoffkit demo-replay
1086
1133
  handoffkit validate-report reports/trace_demo.json
1087
1134
  handoffkit evaluate reports/trace_demo.json
1135
+ handoffkit providers list
1136
+ handoffkit providers models --provider opencode-go
1137
+ handoffkit providers select --provider opencode-go --models mimo-v2.5,deepseek-v4-flash --json
1088
1138
  handoffkit init my-agent --template basic-agent --output .
1089
1139
  ```
1090
1140
 
@@ -199,6 +199,51 @@ clinical_trials_search.run(condition="systemic sclerosis", max_results=3)
199
199
  dailymed_drug_search.run(drug_name="ibuprofen", max_results=3)
200
200
  ```
201
201
 
202
+ ## What 1.4.0 Adds
203
+
204
+ HandoffKit 1.4.0 adds experimental provider routing:
205
+
206
+ - provider registry entries for OpenCode Go, OpenCode Zen, OpenAI-compatible
207
+ endpoints, and Ollama,
208
+ - `ProviderSelector` for offline model lists and real opt-in probes,
209
+ - `ProviderRouter` for ordered fallback between model candidates,
210
+ - retry/backoff for transient OpenCode `429`, `5xx`, and transport failures,
211
+ - `handoffkit providers ...` CLI commands that never call real APIs unless
212
+ `--real` is passed.
213
+
214
+ ```bash
215
+ handoffkit providers list
216
+ handoffkit providers models --provider opencode-go
217
+ handoffkit providers select --provider opencode-go --models mimo-v2.5,deepseek-v4-flash --json
218
+ ```
219
+
220
+ Real probes require an explicit flag and a scoped provider key:
221
+
222
+ ```powershell
223
+ $env:OPENCODE_API_KEY="..."
224
+ handoffkit providers probe --provider opencode-go --models mimo-v2.5,deepseek-v4-flash --real
225
+ ```
226
+
227
+ ## Provider Registry and Model Routing
228
+
229
+ The provider registry is experimental in 1.4.0. It is intended for diagnostics,
230
+ model selection, and fallback without adding heavy provider SDKs.
231
+
232
+ ```python
233
+ from handoffkit.providers import ModelCandidate, ProviderRouter
234
+
235
+ router = ProviderRouter(
236
+ [
237
+ ModelCandidate("opencode-go", "mimo-v2.5"),
238
+ ModelCandidate("opencode-go", "deepseek-v4-flash"),
239
+ ]
240
+ )
241
+
242
+ print(router.generate("Create a concise release checklist."))
243
+ ```
244
+
245
+ Docs: [`docs/PROVIDERS.md`](docs/PROVIDERS.md).
246
+
202
247
  ## What 1.1.0 Adds
203
248
 
204
249
  HandoffKit 1.1.0 focuses on adoption:
@@ -224,6 +269,8 @@ Use it as the contract and reporting layer beside orchestration frameworks:
224
269
  - [`Pydantic AI`](docs/integrations/PYDANTIC_AI.md): typed outputs become workflow handoff state,
225
270
  - [`OpenCode Go / Zen`](docs/integrations/OPENCODE.md): provider adapters route
226
271
  OpenCode model families to their matching API endpoint styles.
272
+ - [`Provider Registry`](docs/PROVIDERS.md): experimental provider listing,
273
+ probing, model selection, and fallback routing.
227
274
 
228
275
  Runnable offline examples:
229
276
  [`examples/langgraph_integration.py`](examples/langgraph_integration.py),
@@ -1053,6 +1100,9 @@ handoffkit demo-trace
1053
1100
  handoffkit demo-replay
1054
1101
  handoffkit validate-report reports/trace_demo.json
1055
1102
  handoffkit evaluate reports/trace_demo.json
1103
+ handoffkit providers list
1104
+ handoffkit providers models --provider opencode-go
1105
+ handoffkit providers select --provider opencode-go --models mimo-v2.5,deepseek-v4-flash --json
1056
1106
  handoffkit init my-agent --template basic-agent --output .
1057
1107
  ```
1058
1108
 
@@ -11,6 +11,7 @@ import hashlib
11
11
  import json
12
12
  import os
13
13
  import re
14
+ import sys
14
15
  import time
15
16
  import uuid
16
17
  from pathlib import Path
@@ -69,6 +70,12 @@ CLINICAL_EQUIVALENCE_ALIASES = {
69
70
  }
70
71
 
71
72
 
73
+ def _configure_output_encoding() -> None:
74
+ for stream in (sys.stdout, sys.stderr):
75
+ if hasattr(stream, "reconfigure"):
76
+ stream.reconfigure(encoding="utf-8", errors="replace")
77
+
78
+
72
79
  def _compact(value: str) -> str:
73
80
  return re.sub(r"[^a-z0-9]+", "", value.lower())
74
81
 
@@ -824,6 +831,7 @@ Judge:
824
831
 
825
832
 
826
833
  def main() -> int:
834
+ _configure_output_encoding()
827
835
  parser = argparse.ArgumentParser()
828
836
  parser.add_argument("--cases", type=int, default=3)
829
837
  parser.add_argument("--model", default=os.getenv("OPENCODE_GO_MODEL", DEFAULT_MODEL))
@@ -5,6 +5,7 @@ Requires OPENCODE_API_KEY. No secrets are printed.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import argparse
8
9
  import os
9
10
  import sys
10
11
 
@@ -18,10 +19,25 @@ def _configure_stdout() -> None:
18
19
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
19
20
 
20
21
 
21
- def main() -> None:
22
+ def _parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(description="Run HandoffKit against OpenCode Go.")
24
+ parser.add_argument(
25
+ "--model",
26
+ default=os.getenv("OPENCODE_GO_MODEL", DEFAULT_OPENCODE_GO_MODEL),
27
+ help="OpenCode Go model id.",
28
+ )
29
+ parser.add_argument(
30
+ "--task",
31
+ default="Create a concise architecture plan for a Python CLI calculator.",
32
+ help="Task prompt.",
33
+ )
34
+ return parser
35
+
36
+
37
+ def main(argv: list[str] | None = None) -> None:
22
38
  _configure_stdout()
39
+ args = _parser().parse_args(argv)
23
40
  api_key = os.getenv("OPENCODE_API_KEY")
24
- model = os.getenv("OPENCODE_GO_MODEL", DEFAULT_OPENCODE_GO_MODEL)
25
41
  if not api_key:
26
42
  print("OPENCODE_API_KEY is not set.")
27
43
  print('Set it with: $env:OPENCODE_API_KEY="..."')
@@ -29,19 +45,19 @@ def main() -> None:
29
45
  return
30
46
 
31
47
  try:
32
- provider = OpenCodeGoProvider(model=model, api_key=api_key)
48
+ provider = OpenCodeGoProvider(model=args.model, api_key=api_key)
33
49
  agent = Agent(
34
50
  name="OpenCodeGoArchitect",
35
51
  role="Create concise coding architecture plans.",
36
52
  provider=provider,
37
53
  )
38
- response = agent.run("Create a concise architecture plan for a Python CLI calculator.")
54
+ response = agent.run(args.task)
39
55
  except (ProviderConfigurationError, ProviderExecutionError) as exc:
40
56
  print(f"OpenCode Go unavailable: {exc}")
41
57
  return
42
58
 
43
59
  print("Provider: OpenCode Go")
44
- print(f"Model: {model}")
60
+ print(f"Model: {args.model}")
45
61
  print(response)
46
62
 
47
63
 
@@ -5,6 +5,7 @@ Requires OPENCODE_API_KEY. No secrets are printed.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import argparse
8
9
  import os
9
10
  import sys
10
11
 
@@ -18,10 +19,25 @@ def _configure_stdout() -> None:
18
19
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
19
20
 
20
21
 
21
- def main() -> None:
22
+ def _parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(description="Run HandoffKit against OpenCode Zen.")
24
+ parser.add_argument(
25
+ "--model",
26
+ default=os.getenv("OPENCODE_ZEN_MODEL", DEFAULT_OPENCODE_ZEN_MODEL),
27
+ help="OpenCode Zen model id.",
28
+ )
29
+ parser.add_argument(
30
+ "--task",
31
+ default="Create a concise architecture plan for a Python CLI calculator.",
32
+ help="Task prompt.",
33
+ )
34
+ return parser
35
+
36
+
37
+ def main(argv: list[str] | None = None) -> None:
22
38
  _configure_stdout()
39
+ args = _parser().parse_args(argv)
23
40
  api_key = os.getenv("OPENCODE_API_KEY")
24
- model = os.getenv("OPENCODE_ZEN_MODEL", DEFAULT_OPENCODE_ZEN_MODEL)
25
41
  if not api_key:
26
42
  print("OPENCODE_API_KEY is not set.")
27
43
  print('Set it with: $env:OPENCODE_API_KEY="..."')
@@ -30,19 +46,19 @@ def main() -> None:
30
46
  return
31
47
 
32
48
  try:
33
- provider = OpenCodeZenProvider(model=model, api_key=api_key)
49
+ provider = OpenCodeZenProvider(model=args.model, api_key=api_key)
34
50
  agent = Agent(
35
51
  name="OpenCodeZenArchitect",
36
52
  role="Create concise coding architecture plans.",
37
53
  provider=provider,
38
54
  )
39
- response = agent.run("Create a concise architecture plan for a Python CLI calculator.")
55
+ response = agent.run(args.task)
40
56
  except (ProviderConfigurationError, ProviderExecutionError) as exc:
41
57
  print(f"OpenCode Zen unavailable: {exc}")
42
58
  return
43
59
 
44
60
  print("Provider: OpenCode Zen")
45
- print(f"Model: {model}")
61
+ print(f"Model: {args.model}")
46
62
  print(response)
47
63
 
48
64
 
@@ -66,7 +66,7 @@ from handoffkit.validation import (
66
66
  ValidationReport,
67
67
  )
68
68
 
69
- __version__ = "1.3.0"
69
+ __version__ = "1.4.0"
70
70
 
71
71
  __all__ = [
72
72
  "Agent",
@@ -20,7 +20,7 @@ from handoffkit.handoff import HandoffState
20
20
  from handoffkit.mai_benchmark import run_mai_style_benchmark
21
21
  from handoffkit.protocol import HandoffProtocol
22
22
  from handoffkit.provider_adapters import ProviderToolAdapter
23
- from handoffkit.providers import BaseProvider
23
+ from handoffkit.providers import BaseProvider, ProviderSelector
24
24
  from handoffkit.quality import HandoffQualityEvaluator
25
25
  from handoffkit.recipes import RecipeRunner
26
26
  from handoffkit.replay import ReplayRunner
@@ -382,6 +382,90 @@ def list_showcases() -> str:
382
382
  return "\n".join(lines)
383
383
 
384
384
 
385
+ def _split_models(value: str | None) -> list[str] | None:
386
+ if not value:
387
+ return None
388
+ return [item.strip() for item in value.split(",") if item.strip()]
389
+
390
+
391
+ def list_providers(*, json_output: bool = False) -> str:
392
+ """List experimental provider registry entries."""
393
+ selector = ProviderSelector()
394
+ specs = [spec.to_dict() for spec in selector.list_providers()]
395
+ if json_output:
396
+ return json.dumps({"providers": specs}, indent=2, sort_keys=True)
397
+ lines = ["HandoffKit providers", ""]
398
+ for spec in selector.list_providers():
399
+ lines.append(f"- {spec.name}: {spec.description}")
400
+ lines.append(f" default: {spec.default_model}")
401
+ lines.append(f" models: {', '.join(spec.suggested_models)}")
402
+ lines.append(f" env: {', '.join(spec.env_vars)}")
403
+ return "\n".join(lines)
404
+
405
+
406
+ def list_provider_models(provider: str, *, real: bool = False, json_output: bool = False) -> str:
407
+ """List known provider models without network unless real=True."""
408
+ selector = ProviderSelector()
409
+ models = selector.list_models(provider, real=real)
410
+ payload = {"provider": provider, "real": real, "models": models}
411
+ if json_output:
412
+ return json.dumps(payload, indent=2, sort_keys=True)
413
+ mode = "real" if real else "offline"
414
+ return (
415
+ f"HandoffKit provider models ({mode})\n"
416
+ f"Provider: {provider}\n"
417
+ + "\n".join(f"- {model}" for model in models)
418
+ )
419
+
420
+
421
+ def probe_provider_models(
422
+ provider: str,
423
+ *,
424
+ models: str | None = None,
425
+ real: bool = False,
426
+ json_output: bool = False,
427
+ ) -> str:
428
+ """Probe provider model candidates."""
429
+ selector = ProviderSelector()
430
+ results = [
431
+ selector.probe(candidate, real=real)
432
+ for candidate in selector.candidates(provider, _split_models(models))
433
+ ]
434
+ payload = {"provider": provider, "real": real, "results": [item.to_dict() for item in results]}
435
+ if json_output:
436
+ return json.dumps(payload, indent=2, sort_keys=True)
437
+ mode = "real" if real else "offline"
438
+ lines = [f"HandoffKit provider probe ({mode})", f"Provider: {provider}"]
439
+ for result in results:
440
+ status = "ok" if result.success else "failed"
441
+ suffix = f" error={result.error}" if result.error else ""
442
+ lines.append(f"- {result.model}: {status} {result.latency_seconds:.3f}s{suffix}")
443
+ return "\n".join(lines)
444
+
445
+
446
+ def select_provider_model(
447
+ provider: str,
448
+ *,
449
+ models: str | None = None,
450
+ real: bool = False,
451
+ json_output: bool = False,
452
+ ) -> str:
453
+ """Select the first working provider model."""
454
+ selector = ProviderSelector()
455
+ result = selector.select(provider, _split_models(models), real=real)
456
+ payload = result.to_dict()
457
+ if json_output:
458
+ return json.dumps(payload, indent=2, sort_keys=True)
459
+ mode = "real" if real else "offline"
460
+ return (
461
+ f"HandoffKit provider select ({mode})\n"
462
+ f"Provider: {result.provider}\n"
463
+ f"Model: {result.model}\n"
464
+ f"Success: {result.success}\n"
465
+ f"Latency: {result.latency_seconds:.3f}s"
466
+ )
467
+
468
+
385
469
  def run_named_showcase(slug: str) -> str:
386
470
  """Run one named showcase and write runs/latest artifacts."""
387
471
  return run_showcase(slug).to_markdown()
@@ -471,6 +555,42 @@ def main(argv: list[str] | None = None) -> int:
471
555
  subparsers.add_parser("demo-support", help="Run the support escalation showcase.")
472
556
  subparsers.add_parser("demo-research", help="Run the research workflow showcase.")
473
557
  subparsers.add_parser("demo-doctor", help="Run the educational doctor-orchestrator showcase.")
558
+ providers_parser = subparsers.add_parser("providers", help="Inspect provider registry.")
559
+ providers_subparsers = providers_parser.add_subparsers(dest="providers_command")
560
+ providers_list_parser = providers_subparsers.add_parser("list", help="List providers.")
561
+ providers_list_parser.add_argument("--json", action="store_true", help="Print JSON.")
562
+ providers_models_parser = providers_subparsers.add_parser(
563
+ "models",
564
+ help="List provider models.",
565
+ )
566
+ providers_models_parser.add_argument("--provider", required=True, help="Provider name.")
567
+ providers_models_parser.add_argument(
568
+ "--real",
569
+ action="store_true",
570
+ help="Call provider endpoint.",
571
+ )
572
+ providers_models_parser.add_argument("--json", action="store_true", help="Print JSON.")
573
+ providers_probe_parser = providers_subparsers.add_parser("probe", help="Probe provider models.")
574
+ providers_probe_parser.add_argument("--provider", required=True, help="Provider name.")
575
+ providers_probe_parser.add_argument("--models", default=None, help="Comma-separated models.")
576
+ providers_probe_parser.add_argument(
577
+ "--real",
578
+ action="store_true",
579
+ help="Call provider endpoint.",
580
+ )
581
+ providers_probe_parser.add_argument("--json", action="store_true", help="Print JSON.")
582
+ providers_select_parser = providers_subparsers.add_parser(
583
+ "select",
584
+ help="Select a provider model.",
585
+ )
586
+ providers_select_parser.add_argument("--provider", required=True, help="Provider name.")
587
+ providers_select_parser.add_argument("--models", default=None, help="Comma-separated models.")
588
+ providers_select_parser.add_argument(
589
+ "--real",
590
+ action="store_true",
591
+ help="Call provider endpoint.",
592
+ )
593
+ providers_select_parser.add_argument("--json", action="store_true", help="Print JSON.")
474
594
  benchmark_doctor_parser = subparsers.add_parser(
475
595
  "benchmark-doctor",
476
596
  help="Run the real-case doctor benchmark harness.",
@@ -556,6 +676,34 @@ def main(argv: list[str] | None = None) -> int:
556
676
  if args.command == "demo-doctor":
557
677
  print(run_doctor_orchestrator_demo())
558
678
  return 0
679
+ if args.command == "providers":
680
+ if args.providers_command == "list":
681
+ print(list_providers(json_output=args.json))
682
+ return 0
683
+ if args.providers_command == "models":
684
+ print(list_provider_models(args.provider, real=args.real, json_output=args.json))
685
+ return 0
686
+ if args.providers_command == "probe":
687
+ print(
688
+ probe_provider_models(
689
+ args.provider,
690
+ models=args.models,
691
+ real=args.real,
692
+ json_output=args.json,
693
+ )
694
+ )
695
+ return 0
696
+ if args.providers_command == "select":
697
+ print(
698
+ select_provider_model(
699
+ args.provider,
700
+ models=args.models,
701
+ real=args.real,
702
+ json_output=args.json,
703
+ )
704
+ )
705
+ return 0
706
+ parser.error("providers requires a subcommand")
559
707
  if args.command == "benchmark-doctor":
560
708
  print(run_doctor_benchmark_demo(args.cases))
561
709
  return 0
@@ -14,12 +14,23 @@ from handoffkit.providers.opencode_provider import (
14
14
  DEFAULT_OPENCODE_GO_MODEL,
15
15
  DEFAULT_OPENCODE_ZEN_BASE_URL,
16
16
  DEFAULT_OPENCODE_ZEN_MODEL,
17
+ OPENCODE_GO_MODELS,
18
+ OPENCODE_ZEN_MODELS,
17
19
  OpenCodeGoProvider,
18
20
  OpenCodeProvider,
19
21
  OpenCodeZenProvider,
22
+ RetryPolicy,
20
23
  infer_opencode_api_style,
21
24
  list_opencode_models,
22
25
  )
26
+ from handoffkit.providers.registry import (
27
+ ModelCandidate,
28
+ ProviderProbeResult,
29
+ ProviderRouter,
30
+ ProviderSelector,
31
+ ProviderSpec,
32
+ default_provider_specs,
33
+ )
23
34
 
24
35
  __all__ = [
25
36
  "BaseProvider",
@@ -29,12 +40,21 @@ __all__ = [
29
40
  "DEFAULT_OPENCODE_ZEN_BASE_URL",
30
41
  "DEFAULT_OPENCODE_ZEN_MODEL",
31
42
  "ModelSelectionResult",
43
+ "ModelCandidate",
44
+ "OPENCODE_GO_MODELS",
45
+ "OPENCODE_ZEN_MODELS",
32
46
  "OllamaProvider",
33
47
  "OpenCodeGoProvider",
34
48
  "OpenCodeProvider",
35
49
  "OpenCodeZenProvider",
36
50
  "OpenAIProvider",
51
+ "ProviderProbeResult",
52
+ "ProviderRouter",
53
+ "ProviderSelector",
54
+ "ProviderSpec",
55
+ "RetryPolicy",
37
56
  "choose_openai_compatible_model",
57
+ "default_provider_specs",
38
58
  "infer_opencode_api_style",
39
59
  "list_opencode_models",
40
60
  "list_openai_compatible_models",
@@ -4,8 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import os
7
+ import time
7
8
  import urllib.error
8
9
  import urllib.request
10
+ from dataclasses import dataclass
9
11
  from typing import Any, Literal
10
12
 
11
13
  from handoffkit.errors import ProviderConfigurationError, ProviderExecutionError
@@ -18,6 +20,53 @@ DEFAULT_OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1"
18
20
  DEFAULT_OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1"
19
21
  DEFAULT_OPENCODE_ZEN_MODEL = "gpt-5.4"
20
22
  DEFAULT_OPENCODE_GO_MODEL = "deepseek-v4-flash"
23
+ OPENCODE_USER_AGENT = "handoffkit/1.4 OpenCodeProvider"
24
+
25
+ OPENCODE_GO_MODELS = (
26
+ "mimo-v2.5",
27
+ "deepseek-v4-flash",
28
+ "deepseek-v4-pro",
29
+ "glm-5.2",
30
+ "qwen3.7-max",
31
+ "qwen3.7-plus",
32
+ "kimi-k2.7-code",
33
+ "minimax-m3",
34
+ )
35
+
36
+ OPENCODE_ZEN_MODELS = (
37
+ "gpt-5.4",
38
+ "claude-sonnet-5",
39
+ "qwen3.7-max",
40
+ "qwen3.7-plus",
41
+ "deepseek-v4-flash",
42
+ "deepseek-v4-pro",
43
+ "glm-5.2",
44
+ "kimi-k2.7-code",
45
+ "minimax-m3",
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class RetryPolicy:
51
+ """Retry policy for transient provider failures."""
52
+
53
+ max_attempts: int = 3
54
+ backoff_seconds: float = 0.5
55
+ retry_status_codes: tuple[int, ...] = (429, 500, 502, 503, 504)
56
+
57
+ def should_retry_status(self, status_code: int, attempt: int) -> bool:
58
+ """Return whether this HTTP status should be retried."""
59
+ return attempt < self.max_attempts and status_code in self.retry_status_codes
60
+
61
+ def should_retry_url_error(self, attempt: int) -> bool:
62
+ """Return whether this URL-level error should be retried."""
63
+ return attempt < self.max_attempts
64
+
65
+ def sleep(self, attempt: int) -> None:
66
+ """Sleep before the next retry attempt."""
67
+ delay = self.backoff_seconds * max(0, attempt - 1)
68
+ if delay > 0:
69
+ time.sleep(delay)
21
70
 
22
71
  _GO_ANTHROPIC_MODELS = {
23
72
  "minimax-m3",
@@ -92,7 +141,7 @@ def list_opencode_models(
92
141
  "Authorization": f"Bearer {resolved_key}",
93
142
  "Accept": "application/json",
94
143
  "Content-Type": "application/json",
95
- "User-Agent": "handoffkit/1.2 OpenCodeProvider",
144
+ "User-Agent": OPENCODE_USER_AGENT,
96
145
  },
97
146
  method="GET",
98
147
  )
@@ -110,7 +159,13 @@ def list_opencode_models(
110
159
  f"OpenCode {resolved_catalog} models request failed for "
111
160
  f"{resolved_base_url}: {exc.reason}"
112
161
  ) from exc
113
- data = body.get("data", body if isinstance(body, list) else [])
162
+ data = (
163
+ body.get("data", [])
164
+ if isinstance(body, dict)
165
+ else body
166
+ if isinstance(body, list)
167
+ else []
168
+ )
114
169
  models: list[str] = []
115
170
  if isinstance(data, list):
116
171
  for item in data:
@@ -133,6 +188,7 @@ class OpenCodeProvider(BaseProvider):
133
188
  base_url: str | None = None,
134
189
  api_style: OpenCodeAPIStyle = "auto",
135
190
  timeout: float = 60.0,
191
+ retry_policy: RetryPolicy | None = None,
136
192
  ) -> None:
137
193
  self.catalog = _normalize_catalog(catalog)
138
194
  self.model = _strip_opencode_prefix(
@@ -143,6 +199,7 @@ class OpenCodeProvider(BaseProvider):
143
199
  self.base_url = _resolve_base_url(self.catalog, base_url)
144
200
  self.api_style = infer_opencode_api_style(self.model, self.catalog, api_style)
145
201
  self.timeout = timeout
202
+ self.retry_policy = retry_policy or RetryPolicy()
146
203
  if not self.api_key:
147
204
  raise ProviderConfigurationError(
148
205
  "OPENCODE_API_KEY is required for OpenCodeProvider. "
@@ -227,7 +284,7 @@ class OpenCodeProvider(BaseProvider):
227
284
  "Authorization": f"Bearer {self.api_key}",
228
285
  "Accept": "application/json",
229
286
  "Content-Type": "application/json",
230
- "User-Agent": "handoffkit/1.2 OpenCodeProvider",
287
+ "User-Agent": OPENCODE_USER_AGENT,
231
288
  }
232
289
  if extra_headers:
233
290
  headers.update(extra_headers)
@@ -237,19 +294,29 @@ class OpenCodeProvider(BaseProvider):
237
294
  headers=headers,
238
295
  method="POST",
239
296
  )
240
- try:
241
- with urllib.request.urlopen(request, timeout=self.timeout) as response:
242
- body = json.loads(response.read().decode("utf-8"))
243
- except urllib.error.HTTPError as exc:
244
- detail = exc.read().decode("utf-8", errors="replace")
245
- raise ProviderExecutionError(
246
- f"OpenCode {self.catalog} request failed with HTTP {exc.code}: "
247
- f"{_sanitize_error_detail(detail, self.api_key)}"
248
- ) from exc
249
- except urllib.error.URLError as exc:
250
- raise ProviderExecutionError(
251
- f"OpenCode {self.catalog} request failed for {self.base_url}: {exc.reason}"
252
- ) from exc
297
+ for attempt in range(1, self.retry_policy.max_attempts + 1):
298
+ try:
299
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
300
+ body = json.loads(response.read().decode("utf-8"))
301
+ break
302
+ except urllib.error.HTTPError as exc:
303
+ detail = exc.read().decode("utf-8", errors="replace")
304
+ if self.retry_policy.should_retry_status(exc.code, attempt):
305
+ self.retry_policy.sleep(attempt)
306
+ continue
307
+ raise ProviderExecutionError(
308
+ f"OpenCode {self.catalog} request failed with HTTP {exc.code}: "
309
+ f"{_sanitize_error_detail(detail, self.api_key)}"
310
+ ) from exc
311
+ except urllib.error.URLError as exc:
312
+ if self.retry_policy.should_retry_url_error(attempt):
313
+ self.retry_policy.sleep(attempt)
314
+ continue
315
+ raise ProviderExecutionError(
316
+ f"OpenCode {self.catalog} request failed for {self.base_url}: {exc.reason}"
317
+ ) from exc
318
+ else: # pragma: no cover - defensive fallback.
319
+ raise ProviderExecutionError(f"OpenCode {self.catalog} request failed.")
253
320
  if not isinstance(body, dict):
254
321
  raise ProviderExecutionError(
255
322
  f"OpenCode {self.catalog} response was not a JSON object."