tether-sdk 0.2.5__tar.gz → 0.2.7__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 (201) hide show
  1. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/AGENTS.md +3 -3
  2. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/PKG-INFO +1 -1
  3. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/README.md +10 -2
  4. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/grade.py +88 -2
  5. tether_sdk-0.2.7/bench/harness-bench/README.md +25 -0
  6. tether_sdk-0.2.7/bench/harness-bench/tether-adapter.patch +134 -0
  7. tether_sdk-0.2.7/bench/report.py +48 -0
  8. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/run.py +30 -4
  9. tether_sdk-0.2.7/bench/tasks/codereview/goal.txt +1 -0
  10. tether_sdk-0.2.7/bench/tasks/codereview/ws/inventory.py +40 -0
  11. tether_sdk-0.2.7/bench/tasks/plan/goal.txt +1 -0
  12. tether_sdk-0.2.7/bench/tasks/plan/ws/README.md +3 -0
  13. tether_sdk-0.2.7/bench/tasks/plan/ws/svc/client.py +18 -0
  14. tether_sdk-0.2.7/bench/tasks/plan/ws/svc/config.py +3 -0
  15. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/architecture.md +2 -2
  16. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/context-and-store.md +1 -1
  17. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/providers.md +1 -1
  18. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/release-notes.md +26 -0
  19. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/testing.md +1 -1
  20. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/security-and-sandbox.md +1 -1
  21. tether_sdk-0.2.7/e2e/__init__.py +0 -0
  22. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_faults.py +12 -0
  23. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_limits.py +17 -0
  24. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_sandbox.py +15 -0
  25. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_cli.py +16 -0
  26. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_context.py +24 -1
  27. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_harness.py +70 -1
  28. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_params.py +30 -0
  29. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_security_hardening.py +52 -1
  30. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_store.py +23 -0
  31. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_tether.py +1 -1
  32. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/__init__.py +1 -1
  33. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/cli.py +15 -1
  34. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/context.py +6 -2
  35. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/harness.py +25 -4
  36. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/anthropic.py +3 -0
  37. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/base.py +17 -1
  38. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/ollama.py +3 -0
  39. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/openai.py +3 -0
  40. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/sandbox.py +30 -9
  41. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/store.py +18 -1
  42. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.claude/feature-context/active/.gitkeep +0 -0
  43. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.claude/feature-context/active/v0-2-bugs.md +0 -0
  44. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.claude/feature-context/archive/.gitkeep +0 -0
  45. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.github/workflows/ci.yml +0 -0
  46. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.github/workflows/delivery.yml +0 -0
  47. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.github/workflows/docs.yml +0 -0
  48. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.gitignore +0 -0
  49. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/LICENSE +0 -0
  50. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/README.md +0 -0
  51. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/naive_agent.py +0 -0
  52. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/goal.txt +0 -0
  53. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/hidden/test_hidden.py +0 -0
  54. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/__init__.py +0 -0
  55. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/errors.py +0 -0
  56. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/evaluator.py +0 -0
  57. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/parser.py +0 -0
  58. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/tokenizer.py +0 -0
  59. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/__init__.py +0 -0
  60. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/errors.py +0 -0
  61. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/evaluator.py +0 -0
  62. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/parser.py +0 -0
  63. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/tokenizer.py +0 -0
  64. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/tests/__init__.py +0 -0
  65. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/tests/test_calc.py +0 -0
  66. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/goal.txt +0 -0
  67. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/hidden/test_hidden.py +0 -0
  68. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/cli.py +0 -0
  69. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/ledger/__init__.py +0 -0
  70. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/ledger/parser.py +0 -0
  71. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/ledger/report.py +0 -0
  72. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/sample.csv +0 -0
  73. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/tests/__init__.py +0 -0
  74. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/tests/test_report.py +0 -0
  75. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/goal.txt +0 -0
  76. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/hidden/test_hidden.py +0 -0
  77. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/ws/tests/__init__.py +0 -0
  78. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/ws/tests/test_textutils.py +0 -0
  79. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/ws/textutils.py +0 -0
  80. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/content/goal.txt +0 -0
  81. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/content/ws/.keep +0 -0
  82. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/goal.txt +0 -0
  83. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/hidden/test_hidden.py +0 -0
  84. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/AGENTS.md +0 -0
  85. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/README.md +0 -0
  86. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/docs/architecture.md +0 -0
  87. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/docs/style.md +0 -0
  88. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/__init__.py +0 -0
  89. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/cli.py +0 -0
  90. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/fmt.py +0 -0
  91. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/stats.py +0 -0
  92. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/textio.py +0 -0
  93. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/tests/__init__.py +0 -0
  94. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/tests/test_stats.py +0 -0
  95. {tether_sdk-0.2.5/e2e → tether_sdk-0.2.7/bench/tasks/plan/ws/svc}/__init__.py +0 -0
  96. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/qa/goal.txt +0 -0
  97. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/qa/ws/.keep +0 -0
  98. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/review/goal.txt +0 -0
  99. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/review/ws/paper.md +0 -0
  100. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/benchmarks.md +0 -0
  101. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/extending.md +0 -0
  102. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/index.md +0 -0
  103. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/javascripts/mathjax.js +0 -0
  104. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/agent-interface.md +0 -0
  105. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/harness.md +0 -0
  106. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/index.md +0 -0
  107. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/multiplexer-and-decorator.md +0 -0
  108. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/sandbox-and-skills.md +0 -0
  109. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/tools.md +0 -0
  110. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/releasing.md +0 -0
  111. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/troubleshooting.md +0 -0
  112. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/agent-interface.md +0 -0
  113. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/built-in-tools.md +0 -0
  114. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/cli-and-headless.md +0 -0
  115. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/index.md +0 -0
  116. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/multi-agent.md +0 -0
  117. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/sdk-and-custom-tools.md +0 -0
  118. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/skills.md +0 -0
  119. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/workflows-and-telemetry.md +0 -0
  120. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/conftest.py +0 -0
  121. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/proxy.py +0 -0
  122. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_agents.py +0 -0
  123. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_defaults.py +0 -0
  124. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_lifecycle.py +0 -0
  125. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_quirks.py +0 -0
  126. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_surface.py +0 -0
  127. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/README.md +0 -0
  128. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/checks.py +0 -0
  129. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/course.yaml +0 -0
  130. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/ruff.toml +0 -0
  131. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/planner/SKILL.md +0 -0
  132. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/searcher/SKILL.md +0 -0
  133. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/searcher/tools.py +0 -0
  134. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/writer/SKILL.md +0 -0
  135. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/writer/tools.py +0 -0
  136. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/mkdocs.yml +0 -0
  137. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/pyproject.toml +0 -0
  138. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/scripts/record_golden.py +0 -0
  139. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/skills/researcher/SKILL.md +0 -0
  140. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/__init__.py +0 -0
  141. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/conftest.py +0 -0
  142. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/fixtures/dialects.jsonl +0 -0
  143. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/coding_edit_failure_modes.json +0 -0
  144. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/coding_edit_failure_modes.snap.json +0 -0
  145. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/done_nudge.json +0 -0
  146. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/done_nudge.snap.json +0 -0
  147. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/read_range_pingpong.json +0 -0
  148. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/read_range_pingpong.snap.json +0 -0
  149. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/research_web.json +0 -0
  150. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/research_web.snap.json +0 -0
  151. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/shell_wrappers.json +0 -0
  152. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/shell_wrappers.snap.json +0 -0
  153. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/waiting_input_resume.json +0 -0
  154. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/waiting_input_resume.snap.json +0 -0
  155. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/__init__.py +0 -0
  156. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_anthropic.py +0 -0
  157. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_base.py +0 -0
  158. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_declarative.py +0 -0
  159. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_dialects.py +0 -0
  160. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_ollama.py +0 -0
  161. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_openai.py +0 -0
  162. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_retry.py +0 -0
  163. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_agent_interface.py +0 -0
  164. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_context_config.py +0 -0
  165. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_decorator.py +0 -0
  166. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_edit_tool.py +0 -0
  167. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_golden.py +0 -0
  168. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_integration.py +0 -0
  169. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_misses.py +0 -0
  170. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_multiplexer.py +0 -0
  171. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_pipeline.py +0 -0
  172. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_release_hardening.py +0 -0
  173. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_retro.py +0 -0
  174. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_sandbox.py +0 -0
  175. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_skills.py +0 -0
  176. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_structured_history.py +0 -0
  177. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/__init__.py +0 -0
  178. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_agent.py +0 -0
  179. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_bash.py +0 -0
  180. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_done.py +0 -0
  181. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_edit.py +0 -0
  182. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_mem.py +0 -0
  183. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_web.py +0 -0
  184. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/api.py +0 -0
  185. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/decorator.py +0 -0
  186. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/mcp.py +0 -0
  187. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/multiplexer.py +0 -0
  188. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/pipeline.py +0 -0
  189. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/__init__.py +0 -0
  190. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/declarative.py +0 -0
  191. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/retro.py +0 -0
  192. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/schema.py +0 -0
  193. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/skills.py +0 -0
  194. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/__init__.py +0 -0
  195. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/agent.py +0 -0
  196. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/bash.py +0 -0
  197. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/done.py +0 -0
  198. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/edit.py +0 -0
  199. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/mem.py +0 -0
  200. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/web.py +0 -0
  201. {tether_sdk-0.2.5 → tether_sdk-0.2.7}/uv.lock +0 -0
@@ -45,7 +45,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
45
45
  - Sessions do **not** terminate simply because the model emitted a text response without a tool call (small 9B models frequently "think out loud" before acting).
46
46
  - A task only completes when the agent explicitly calls `act("done", stdin="<final deliverable>")` (or `act("done '<short answer>'")`), pauses for human/parent input via `act("done --ask", stdin="<question>")` (transitioning the session to `WAITING_INPUT` and releasing the worker process until resumed via `send`), or hits `max_steps`.
47
47
  - If the model emits plain text without calling `act`, the harness appends a brief deterministic nudge reminding it to either execute a command or call `done`.
48
- - After 6 consecutive read-only/verification steps following a change, the harness appends a one-shot `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete (0 static prompt tokens, #74). Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change end the session `FAILED` as `Stalled` (#92); `tether stop <id>` terminates a detached worker (#89).
48
+ - After 6 consecutive read-only/verification steps following a change, the harness appends a one-shot `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete (0 static prompt tokens, #74). Before any change, 12 read-only steps in a row (then every 8 more) append a one-line `[NUDGE]` to make the edit or call `done` (#127). Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change end the session `FAILED` as `Stalled` (#92); `tether stop <id>` terminates a detached worker (#89).
49
49
  - When `verify` gate commands are configured (via `Harness(verify=[...])`, CLI `--verify`, or Skill YAML `verify: [...]`), `Harness.execute_session` deterministically executes each command inside the session's `SandboxPolicy` before allowing `done` to transition to `COMPLETED` (consuming 0 static prompt tokens until a gate fails). A gate that errors instead of failing (sandbox violation, exit 124/126/127) ends the session `FAILED` at once as `gate_error` ("gate misconfigured"), without counting toward the consecutive-failure abort.
50
50
 
51
51
  ### Rule 4: Progressive `--help` (JIT Schema Disclosure)
@@ -62,7 +62,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
62
62
  ```text
63
63
  [obs_3 folded: `web fetch https://...` (3,420 chars) | use `mem recall obs_3` to expand]
64
64
  ```
65
- - Exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The 2,400-char head+tail elision still applies, and under `max_context_tokens` pressure it is folded like any other (oldest first). The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
65
+ - Exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The head+tail elision still applies (an observation may use up to a third of `max_context_tokens`, at least 2,400 chars), and under `max_context_tokens` pressure it is folded like any other (oldest first). The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
66
66
  - History is replayed as structured `tool_calls` + `tool` results (providers with `structured_history = True`; textual `act('...')` form only as fallback), with `stdin` replayed as a real 400-char prefix, never a `<N chars>` placeholder. A call and its result are trimmed together.
67
67
 
68
68
  ### Rule 6: Deterministic Security Envelope (Never Trust the LLM for Safety)
@@ -71,7 +71,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
71
71
  - All `edit` and `bash` paths are canonicalized (`Path.resolve()`) and confined to `workspace_root`.
72
72
  - Sensitive files/directories (`.env*`, `.git/config`, `.git/hooks`, `~/.ssh`, `.tether/`) are deterministically protected from mutation or read exfiltration.
73
73
  - `bash` subprocesses get an allowlisted environment only (secrets with any name are dropped; opt-in via `TETHER_ENV_PASSTHROUGH`). `web fetch` blocks loopback/private/metadata addresses (SSRF guard). `SandboxPolicy(network=False)` / `--no-network` adds `--unshare-net` (fails closed without `bwrap`; children can only narrow it).
74
- - On Linux, `bash` automatically uses **`bwrap` (Bubblewrap) / `unshare`** when available (read-only `/`, hidden `/home` and `/var/tmp` tmpfs (existing `PATH` dirs under them, e.g. `~/.local/bin`, are re-bound read-only so `uv`/`node` still run, #76), `/run` unix sockets such as `docker.sock` masked, read-write bind on `workspace_root` with secret-looking files at any depth (`.env*`, `*.pem`, `*.key`, ...), `.tether/`, `.git/config` and `.git/hooks` masked at the mount level, `PR_SET_NO_NEW_PRIVS`), falling back to a strict deterministic command allowlist/denylist (`rm -rf /`, `sudo`, `dd`, `mkfs`, `chmod 777`, etc. blocked).
74
+ - On Linux, `bash` automatically uses **`bwrap` (Bubblewrap) / `unshare`** when available (read-only `/`, hidden `/home` and `/var/tmp` tmpfs (existing `PATH` dirs under them, e.g. `~/.local/bin`, are re-bound read-only so `uv`/`node` still run, #76), `/run` unix sockets such as `docker.sock` masked, read-write bind on `workspace_root` with secret-looking files at any depth (`.env*`, `*.pem`, `*.key`, ...), `.tether/`, `.git/config` and `.git/hooks` masked at the mount level, `PR_SET_NO_NEW_PRIVS`), falling back to a strict deterministic command allowlist/denylist (path, `..` and `.env` text rules run only in this fallback, the mounts enforce them under `bwrap`, #125; `.env.example`-style templates are not secrets) (`rm -rf /`, `sudo`, `dd`, `mkfs`, `chmod 777`, etc. blocked).
75
75
  - Sub-agents spawned via `agent spawn` strictly inherit or narrow their parent session's `SandboxPolicy` (no privilege escalation).
76
76
 
77
77
  ### Rule 7: Zero Framework Bloat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tether-sdk
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: General-purpose, headless-first LLM-Native micro-harness for small language models
5
5
  Project-URL: Homepage, https://github.com/igormcsouza/tether
6
6
  Project-URL: Repository, https://github.com/igormcsouza/tether
@@ -4,11 +4,19 @@ Not part of the `tether` package, not run in CI. Use it to onboard a new model o
4
4
  release for regressions that deterministic tests cannot see (unit, golden and e2e tests stay the
5
5
  gate; see `docs/testing.md`).
6
6
 
7
- Seven tasks in `tasks/<name>/` (workspace in `ws/`, goal in `goal.txt`, hidden tests in `hidden/`):
8
- `focus` (one exact edit in a distractor-heavy repo, scope-checked), `code-simple`, `code-medium`, `code-hard` (graded by hidden unit tests; editing the visible tests
7
+ Nine tasks in `tasks/<name>/` (workspace in `ws/`, goal in `goal.txt`, hidden tests in `hidden/`):
8
+ `focus` (one exact edit in a distractor-heavy repo, scope-checked), `plan` (an implementation plan, checked for named files, failure handling and tests), `codereview` (eight seeded bugs to find, no edits), `code-simple`, `code-medium`, `code-hard` (graded by hidden unit tests; editing the visible tests
9
9
  scores 0), `qa`, `content`, `review` (graded by constraint counts and keyword checks, so they are
10
10
  a smoke test for quality, not a judge).
11
11
 
12
+ **Polyglot-Python** is the score we answer to. `--tasks polyglot` runs the 34 Python exercises of Aider's
13
+ [polyglot-benchmark](https://github.com/Aider-AI/polyglot-benchmark) (cloned once into
14
+ `~/.cache/tether-bench/polyglot-benchmark`, or set `POLYGLOT_DIR`); `--tasks polyglot-fast` runs a 12-exercise
15
+ subset that mixes passes, partials and failures. Each task is graded by the exercise's own tests (the test file is
16
+ restored before grading, so editing it cannot pass), `--poly-verify` adds a `--verify` gate on those tests. Then
17
+ `python3 bench/report.py <out>` prints pass rate and mean score with a failure breakdown that keeps provider outages
18
+ (`outage`, `timeout`) apart from harness outcomes (`max_steps`, `stalled`, `wrong_answer`, `worker_error`).
19
+
12
20
  Three runners, so a gap can be attributed:
13
21
 
14
22
  | runner | what it is |
@@ -135,10 +135,96 @@ def _focus(task, ws, result):
135
135
  return r
136
136
 
137
137
 
138
+ def _steps(t):
139
+ return len(re.findall(r"^\s*\d+[.)]", t, re.M))
140
+
141
+
142
+ def _plan(_task, _ws, result):
143
+ t = result or ""
144
+ c = {
145
+ "6-10 numbered steps": 6 <= _steps(t) <= 10,
146
+ "names client.py": "client.py" in t,
147
+ "names config.py": "config.py" in t,
148
+ "names get/post": bool(re.search(r"\bget\b", t)) and bool(re.search(r"\bpost\b", t)),
149
+ "backoff+jitter": bool(re.search(r"exponential", t, re.I)) and bool(re.search(r"jitter", t, re.I)),
150
+ "retry settings": bool(re.search(r"max[_ -]?(retries|attempts)|retry[_ -]?(limit|count)", t, re.I)) and bool(re.search(r"base[_ -]?(delay|backoff)|backoff[_ -]?(base|factor)", t, re.I)),
151
+ "429 and 5xx": "429" in t and bool(re.search(r"5xx|50\d", t)),
152
+ "not retried (4xx)": bool(re.search(r"4xx|not retr|never retr|do not retry|don't retry|non-retry", t, re.I)),
153
+ "Retry-After": bool(re.search(r"retry-after", t, re.I)),
154
+ "tests without network": bool(re.search(r"mock|fake|stub|patch|monkeypatch|inject", t, re.I)) and bool(re.search(r"test", t, re.I)),
155
+ "idempotency of post": bool(re.search(r"idempot|post.{0,60}(retry|retri)|(retry|retri).{0,60}post", t, re.I)),
156
+ }
157
+ return {"score": round(sum(c.values()) / len(c), 3), "pass": sum(c.values()) >= 9, "detail": {"missed": [k for k, v in c.items() if not v]}}
158
+
159
+
160
+ BUGS = {
161
+ "page off-by-one": r"page.{0,200}(off.by.one|number\s*-\s*1|\(number - 1\)|1-based|zero|skips? the first)|(off.by.one|1-based).{0,200}page",
162
+ "mutable default": r"mutable default|tags=\[\]|default (argument|arg|list)|shared (between|across)",
163
+ "float money": r"float.{0,120}(money|price|currenc|rounding|precision|decimal)|decimal",
164
+ "float equality": r"(==|equal).{0,100}(float|0\.0)|is_free|float.{0,60}(compar|equal)|isclose",
165
+ "sql injection": r"sql.?inject|parameteri[sz]|placeholder|string concat|bind (var|param)",
166
+ "file not closed": r"(not|never) clos|resource leak|context manager|with open|leak",
167
+ "keyerror/missing": r"keyerror|missing (key|item|name)|\.get\(|unknown name|not in (the )?(stock|dict)",
168
+ "split malformed": r"split.{0,100}(more than|exactly|two|malformed|blank|empty|valueerror)|blank line|malformed|empty line|valueerror",
169
+ }
170
+
171
+
172
+ def _codereview(_task, _ws, result):
173
+ t = result or ""
174
+ hit = {k: bool(re.search(v, t, re.I | re.S)) for k, v in BUGS.items()}
175
+ must = ["page off-by-one", "mutable default", "sql injection", "file not closed", "keyerror/missing"]
176
+ return {"score": round(sum(hit.values()) / len(hit), 3), "pass": all(hit[k] for k in must), "detail": {"missed": [k for k, v in hit.items() if not v], "items": _steps(t)}}
177
+
178
+
179
+ # Fast set (12): easy passes, partials and hard failures from the 0.2.6 baseline; tether's weak spots.
180
+ POLY_FAST = ("affine-cipher bottle-song list-ops paasio dot-dsl food-chain forth go-counting poker pov react "
181
+ "rest-api").split()
182
+
183
+
184
+ def poly_dir(ex=""):
185
+ """Aider polyglot-benchmark Python exercises (cloned once, never vendored)."""
186
+ root = os.environ.get("POLYGLOT_DIR") or os.path.expanduser("~/.cache/tether-bench/polyglot-benchmark")
187
+ if not os.path.isdir(root):
188
+ subprocess.run(["git", "clone", "--depth", "1", "https://github.com/Aider-AI/polyglot-benchmark", root], check=True)
189
+ return os.path.join(root, "python", "exercises", "practice", ex)
190
+
191
+
192
+ def poly_goal(ex):
193
+ d = poly_dir(ex)
194
+ docs = "\n\n".join(open(os.path.join(d, ".docs", f)).read() for f in ("instructions.md", "instructions.append.md")
195
+ if os.path.exists(os.path.join(d, ".docs", f)))
196
+ sol = ex.replace("-", "_") + ".py"
197
+ return (f"Implement `{sol}` in the current directory so that `python3 -m unittest discover -p '*_test.py'` passes. "
198
+ f"Do not edit the test file.\n\n{docs}")
199
+
200
+
201
+ def _poly(task, ws, _result):
202
+ ex = task.removeprefix("poly-")
203
+ tmp = tempfile.mkdtemp(prefix="g_")
204
+ try:
205
+ shutil.copytree(ws, tmp, dirs_exist_ok=True)
206
+ test = ex.replace("-", "_") + "_test.py"
207
+ shutil.copy(os.path.join(poly_dir(ex), test), os.path.join(tmp, test)) # restore: editing tests cannot pass
208
+ try:
209
+ p = subprocess.run([sys.executable, "-m", "unittest", "-v", test[:-3]], cwd=tmp, capture_output=True, text=True, timeout=90)
210
+ except subprocess.TimeoutExpired:
211
+ return {"score": 0.0, "pass": False, "detail": {"error": "tests timed out"}}
212
+ out = p.stderr + p.stdout
213
+ ok = len(re.findall(r"\.\.\. ok$", out, re.M))
214
+ bad = len(re.findall(r"\.\.\. (FAIL|ERROR)$", out, re.M))
215
+ total = ok + bad
216
+ return {"score": round(ok / total, 3) if total else 0.0, "pass": p.returncode == 0 and total > 0, "detail": {"ok": ok, "total": total}}
217
+ finally:
218
+ shutil.rmtree(tmp, ignore_errors=True)
219
+
220
+
138
221
  def grade(task, ws, result):
139
- fn = {"qa": _qa, "content": _content, "review": _review, "focus": _focus}.get(task, _code)
222
+ orig = task
223
+ if task.startswith("poly-"):
224
+ task = "poly"
225
+ fn = {"poly": _poly, "qa": _qa, "content": _content, "review": _review, "focus": _focus, "plan": _plan, "codereview": _codereview}.get(task, _code)
140
226
  try:
141
- return fn(task, ws, result)
227
+ return fn(orig, ws, result)
142
228
  except Exception as e: # grader bug must never look like a model result
143
229
  return {"score": 0.0, "pass": False, "detail": {"grader_error": repr(e)}}
144
230
 
@@ -0,0 +1,25 @@
1
+ # Harness-Bench adapter (pilot, not yet run end to end)
2
+
3
+ [Harness-Bench](https://github.com/Qihoo360/harness-bench) ([paper](https://arxiv.org/abs/2605.27922)) scores
4
+ whole harnesses on 274 offline workflow tasks (plus 26 multimodal) with hidden oracles. This directory holds
5
+ the patch that registers Tether as an eighth harness. It is a way to benchmark, not a Tether feature.
6
+
7
+ `tether-adapter.patch` applies to harness-bench commit `b8d89634c6f445fcc68ceb6744a59bef6cdf4863`:
8
+
9
+ ```bash
10
+ git clone https://github.com/Qihoo360/harness-bench && cd harness-bench
11
+ git apply /path/to/tether/bench/harness-bench/tether-adapter.patch
12
+ uv sync --extra dev
13
+ export PYTHONPATH=$PWD/src:$PWD/tools:$PWD/tools/task_maintenance
14
+ python tools/run_benchmark.py --base-url "$URL" --model "$MODEL" --harness tether \
15
+ --tasks 001-meeting-summary --workers 1 --run-root /tmp/hb-run
16
+ ```
17
+
18
+ What the patch adds: `docker/harnesses/tether/` (their toolchain image plus `tether-sdk`, an entrypoint that runs
19
+ `tether run --tools bash,edit` with state kept outside the graded workspace), a `tether` `HarnessSpec`, and a
20
+ `providers.yaml` renderer.
21
+
22
+ Status (2026-10-23): image builds, `tether run` starts inside the trial container and reaches `ProviderError ...
23
+ ConnectError`, because on the machine used containers cannot reach host ports (`host.docker.internal`, where the
24
+ per-trial model proxy listens). The host firewall must allow the docker bridge to the proxy's ports. No task has
25
+ been scored yet.
@@ -0,0 +1,134 @@
1
+ diff --git a/docker/harnesses/tether/Dockerfile b/docker/harnesses/tether/Dockerfile
2
+ new file mode 100644
3
+ index 0000000..d9f3e28
4
+ --- /dev/null
5
+ +++ b/docker/harnesses/tether/Dockerfile
6
+ @@ -0,0 +1,45 @@
7
+ +FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b AS task-toolchain
8
+ +
9
+ +ARG PIP_INDEX_URL
10
+ +ARG PIP_EXTRA_INDEX_URL
11
+ +ARG PIP_TRUSTED_HOST
12
+ +COPY docker/openclaw/requirements.lock /tmp/harnessbench-requirements.lock
13
+ +RUN python -m pip install --disable-pip-version-check --no-cache-dir \
14
+ + --requirement /tmp/harnessbench-requirements.lock \
15
+ + && rm /tmp/harnessbench-requirements.lock
16
+ +
17
+ +FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
18
+ +
19
+ +ARG TETHER_VERSION=0.2.5
20
+ +ARG PYPI_INDEX_URL=https://pypi.org/simple
21
+ +RUN apt-get update \
22
+ + && apt-get install -y --no-install-recommends bash git ripgrep sqlite3 \
23
+ + && rm -rf /var/lib/apt/lists/*
24
+ +
25
+ +
26
+ +
27
+ +RUN useradd --create-home --uid 1000 harness
28
+ +COPY --from=task-toolchain /usr/local/ /usr/local/
29
+ +COPY docker/openclaw/verify_toolchain.py /opt/harnessbench/verify_toolchain.py
30
+ +COPY docker/harnesses/tether/entrypoint.sh /opt/harness/bin/run
31
+ +RUN ln -sf /usr/local/bin/python3 /usr/bin/python3 \
32
+ + && python /opt/harnessbench/verify_toolchain.py --python-only \
33
+ + && python -m venv /opt/tether \
34
+ + && /opt/tether/bin/python -m pip install --no-cache-dir \
35
+ + --index-url "${PYPI_INDEX_URL}" "tether-sdk==${TETHER_VERSION}" \
36
+ + && chmod 0755 /opt/harness/bin/run
37
+ +
38
+ +ENV PATH="/opt/harnessbench/frontend-bin:/usr/local/bin:/opt/tether/bin:${PATH}" \
39
+ + NANOBOT_STREAM_IDLE_TIMEOUT_S=7200 \
40
+ + NANOBOT_OPENAI_COMPAT_TIMEOUT_S=7200 \
41
+ + NANOBOT_LLM_TIMEOUT_S=0
42
+ +
43
+ +# BEGIN NANOBOT LOGIN-SHELL PATH
44
+ +RUN printf '%s\n' \
45
+ + 'export PATH="/usr/local/bin:/opt/tether/bin:$PATH"' \
46
+ + > /etc/profile.d/harnessbench-path.sh \
47
+ + && chmod 0644 /etc/profile.d/harnessbench-path.sh
48
+ +# END NANOBOT LOGIN-SHELL PATH
49
+ +
50
+ +USER 1000:1000
51
+ +ENTRYPOINT ["/opt/harness/bin/run"]
52
+ diff --git a/docker/harnesses/tether/entrypoint.sh b/docker/harnesses/tether/entrypoint.sh
53
+ new file mode 100644
54
+ index 0000000..105cee6
55
+ --- /dev/null
56
+ +++ b/docker/harnesses/tether/entrypoint.sh
57
+ @@ -0,0 +1,38 @@
58
+ +#!/usr/bin/env bash
59
+ +set -euo pipefail
60
+ +
61
+ +: "${HARNESSBENCH_PROMPT:?}"
62
+ +: "${HARNESSBENCH_CONFIG:?}"
63
+ +: "${HARNESSBENCH_WORKSPACE:?}"
64
+ +: "${HARNESSBENCH_STATE:?}"
65
+ +: "${HARNESSBENCH_TRACE:?}"
66
+ +
67
+ +export HOME="${HARNESSBENCH_STATE}/home"
68
+ +mkdir -p "${HOME}" "${HARNESSBENCH_TRACE}"
69
+ +runtime_config="${HARNESSBENCH_STATE}/providers.yaml"
70
+ +cp "${HARNESSBENCH_CONFIG}" "${runtime_config}"
71
+ +# The render step writes `# model: <id>` and `# turns: <n>` on the first lines.
72
+ +model="$(sed -n 's/^# model: //p' "${runtime_config}" | head -1)"
73
+ +turns="$(sed -n 's/^# turns: //p' "${runtime_config}" | head -1)"
74
+ +prompt="$(<"${HARNESSBENCH_PROMPT}")"
75
+ +
76
+ +export TETHER_PROVIDERS="${runtime_config}"
77
+ +export VLLM_API_KEY="${VLLM_API_KEY:-local-vllm-no-auth}" # placeholder; the trial proxy injects the real key
78
+ +export TETHER_DB="${HARNESSBENCH_STATE}/tether.db" # state stays outside the graded workspace
79
+ +export TETHER_ENV_PASSTHROUGH="PATH,NODE_PATH,GIT_CONFIG_GLOBAL"
80
+ +
81
+ +set +e
82
+ +cd "${HARNESSBENCH_WORKSPACE}"
83
+ +tether run \
84
+ + --model "harnessbench/${model}" \
85
+ + --tools bash,edit \
86
+ + --max-steps "${turns:-150}" \
87
+ + "${prompt}" \
88
+ + > >(tee "${HARNESSBENCH_TRACE}/events.log") \
89
+ + 2> >(tee "${HARNESSBENCH_TRACE}/stderr.log" >&2)
90
+ +status=$?
91
+ +set -e
92
+ +
93
+ +[[ -f "${TETHER_DB}" ]] && cp "${TETHER_DB}" "${HARNESSBENCH_TRACE}/tether.db"
94
+ +chmod -R a+rX "${HARNESSBENCH_WORKSPACE}" "${HARNESSBENCH_TRACE}" 2>/dev/null || true
95
+ +exit "${status}"
96
+ diff --git a/tools/harness_runtime.py b/tools/harness_runtime.py
97
+ index c57ba22..e8763dc 100644
98
+ --- a/tools/harness_runtime.py
99
+ +++ b/tools/harness_runtime.py
100
+ @@ -67,6 +67,14 @@ SPECS = {
101
+ config_name="config.json",
102
+ docker_context=HARNESS_DOCKER_DIR / "nanobot",
103
+ ),
104
+ + "tether": HarnessSpec(
105
+ + harness_id="tether",
106
+ + version="0.2.5",
107
+ + image="harnessbench/tether:0.2.5",
108
+ + protocol="chat_completions",
109
+ + config_name="providers.yaml",
110
+ + docker_context=HARNESS_DOCKER_DIR / "tether",
111
+ + ),
112
+ "goose": HarnessSpec(
113
+ harness_id="goose",
114
+ version="1.44.0",
115
+ @@ -291,6 +299,19 @@ def render_harness_config(
116
+ }
117
+ return json.dumps(config, ensure_ascii=False, indent=2) + "\n"
118
+
119
+ + if spec.harness_id == "tether":
120
+ + entry = {
121
+ + "kind": "openai",
122
+ + "base_url": base_url,
123
+ + "api_key_env": "VLLM_API_KEY",
124
+ + "context_tokens": context_window,
125
+ + "fold_after": 8,
126
+ + "timeout": 1800,
127
+ + "params": {"temperature": temperature, "max_tokens": max_tokens},
128
+ + }
129
+ + header = f"# model: {model}\n# turns: {max_agent_turns}\n"
130
+ + return header + yaml.safe_dump({"harnessbench": entry}, sort_keys=False)
131
+ +
132
+ if spec.harness_id == "goose":
133
+ config = {
134
+ "provider": "openai",
@@ -0,0 +1,48 @@
1
+ """Failure breakdown for a results dir: `python3 bench/report.py <out> [<out> ...]`.
2
+
3
+ Splits every run into pass, model-side outcomes (wrong answer, ran out of steps, stalled) and
4
+ failures that say nothing about the harness's logic (provider outage, killed by timeout), so a
5
+ score never hides infrastructure noise and infrastructure never hides a harness bug.
6
+ """
7
+ import collections
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+
13
+ OUTAGE = re.compile(r"unavailable after|429|503|Not Found|ConnectError|TimeoutError")
14
+
15
+
16
+ def outcome(r):
17
+ if r["pass"]:
18
+ return "pass"
19
+ try:
20
+ j = json.loads(open(r["dir"] + "/stdout.txt").read())
21
+ except (OSError, ValueError):
22
+ return "timeout" if r.get("run_status") == "timeout" else "no_output"
23
+ f = j.get("failure") or {}
24
+ kind, msg = f.get("kind"), f.get("message", "")
25
+ if kind == "worker_error":
26
+ return "outage" if OUTAGE.search(msg) else "worker_error"
27
+ return kind or ("wrong_answer" if j.get("status") == "COMPLETED" else "timeout")
28
+
29
+
30
+ def main(dirs):
31
+ rows = []
32
+ for d in dirs:
33
+ rows += [json.loads(line) for line in open(os.path.join(d, "results.jsonl"))]
34
+ groups = collections.defaultdict(list)
35
+ for r in rows:
36
+ groups[(r["runner"], r["model"])].append(r)
37
+ for (runner, model), rs in sorted(groups.items()):
38
+ c = collections.Counter(outcome(r) for r in rs)
39
+ valid = [r for r in rs if outcome(r) not in ("outage", "timeout", "no_output")]
40
+ mean = lambda l: round(sum(x["score"] for x in l) / len(l), 3) if l else 0.0 # noqa: E731
41
+ wall = round(sum(r["wall_s"] for r in rs) / len(rs))
42
+ print(f"{runner} {model}: {len(rs)} runs, pass {c['pass']} ({c['pass'] / len(rs):.0%}), mean {mean(rs)}, "
43
+ f"mean over valid runs {mean(valid)}, {wall}s/run")
44
+ print(" " + ", ".join(f"{k} {v}" for k, v in c.most_common()))
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main(sys.argv[1:])
@@ -20,10 +20,22 @@ HERE = os.path.dirname(os.path.abspath(__file__))
20
20
  TASK_CFG = { # tether tools / step cap / wall timeout
21
21
  "code-simple": ("edit,bash", 30, 600), "code-medium": ("edit,bash", 40, 900), "code-hard": ("edit,bash", 50, 1200),
22
22
  "focus": ("edit,bash,mem", 30, 600),
23
+ "plan": ("edit,bash", 20, 420), "codereview": ("edit,bash", 20, 420),
23
24
  "qa": ("", 10, 300), "content": ("", 10, 300), "review": ("edit", 15, 420),
24
25
  }
25
26
 
26
27
 
28
+ POLY_VERIFY = False
29
+
30
+
31
+ def cfg(task):
32
+ return ("edit,bash", 30, 600) if task.startswith("poly-") else TASK_CFG[task]
33
+
34
+
35
+ def poly_tasks():
36
+ return ",".join("poly-" + e for e in sorted(os.listdir(grade.poly_dir())))
37
+
38
+
27
39
  def group(model):
28
40
  return "nvidia" if model.startswith("openai/") else "ollama" if model.startswith("ollama/") else "claude" if model.startswith(("claude", "cc/")) else "nvidia"
29
41
 
@@ -39,7 +51,7 @@ def flags(tether_bin):
39
51
 
40
52
 
41
53
  def build_cmd(runner, model, task, goal, ws, rd, tether_bin, params=()):
42
- tools, steps, _ = TASK_CFG[task]
54
+ tools, steps, _ = cfg(task)
43
55
  if runner == "tether":
44
56
  h = flags(tether_bin)
45
57
  cmd = [tether_bin, "run", "--model", model, "--max-steps", str(steps), "--db", f"{rd}/t.db"]
@@ -49,6 +61,8 @@ def build_cmd(runner, model, task, goal, ws, rd, tether_bin, params=()):
49
61
  cmd += ["--tools", tools]
50
62
  for item in params:
51
63
  cmd += ["--param", item]
64
+ if task.startswith("poly-") and POLY_VERIFY:
65
+ cmd += ["--verify", "python3 -m unittest discover -p '*_test.py'"]
52
66
  return cmd + [goal]
53
67
  if runner == "naive":
54
68
  base, key = ("http://localhost:11435/v1", "") if model.startswith("ollama/") else ("https://integrate.api.nvidia.com/v1", "NVIDIA_API_KEY")
@@ -98,8 +112,13 @@ async def one(sem, rec, args, results):
98
112
  async with sem[group(model)]:
99
113
  shutil.rmtree(rd, ignore_errors=True)
100
114
  os.makedirs(rd)
101
- shutil.copytree(f"{HERE}/tasks/{task}/ws", ws)
102
- goal = open(f"{HERE}/tasks/{task}/goal.txt").read().strip()
115
+ if task.startswith("poly-"):
116
+ ex = task[5:]
117
+ shutil.copytree(grade.poly_dir(ex), ws, ignore=shutil.ignore_patterns(".meta", ".docs"))
118
+ goal = grade.poly_goal(ex)
119
+ else:
120
+ shutil.copytree(f"{HERE}/tasks/{task}/ws", ws)
121
+ goal = open(f"{HERE}/tasks/{task}/goal.txt").read().strip()
103
122
  cmd = build_cmd(runner, model, task, goal, ws, rd, args.tether_bin, args.param)
104
123
  env = dict(os.environ, OLLAMA_HOST="http://localhost:11435", PYTHONDONTWRITEBYTECODE="1")
105
124
  if model.startswith("openai/"): # old tether has no providers.yaml: route openai/ to NVIDIA's OpenAI-compatible API
@@ -108,7 +127,7 @@ async def one(sem, rec, args, results):
108
127
  p = await asyncio.create_subprocess_exec(*cmd, cwd=ws, env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, start_new_session=True)
109
128
  status = "ok"
110
129
  try:
111
- out, err = await asyncio.wait_for(p.communicate(), TASK_CFG[task][2])
130
+ out, err = await asyncio.wait_for(p.communicate(), cfg(task)[2])
112
131
  except asyncio.TimeoutError:
113
132
  os.killpg(p.pid, signal.SIGKILL)
114
133
  out, err = await p.communicate()
@@ -160,7 +179,14 @@ async def main():
160
179
  ap.add_argument("--par", default="nvidia=4,ollama=1,claude=3")
161
180
  ap.add_argument("--force", action="store_true")
162
181
  ap.add_argument("--redo-infra", action="store_true", help="rerun cells that failed on provider errors")
182
+ ap.add_argument("--poly-verify", action="store_true", help="verify gate on the exercise tests: gate runs at `done`")
163
183
  args = ap.parse_args()
184
+ global POLY_VERIFY
185
+ POLY_VERIFY = args.poly_verify
186
+ if args.tasks == "polyglot":
187
+ args.tasks = poly_tasks()
188
+ elif args.tasks == "polyglot-fast":
189
+ args.tasks = ",".join("poly-" + e for e in grade.POLY_FAST)
164
190
  os.makedirs(args.out, exist_ok=True)
165
191
  sem = {k: asyncio.Semaphore(int(v)) for k, v in (x.split("=") for x in args.par.split(","))}
166
192
  recs = [(r, m, t, k) for r in args.runners.split(",") for m in args.models.split(",") for t in args.tasks.split(",") for k in range(args.trials)
@@ -0,0 +1 @@
1
+ Do not edit any file. Review inventory.py. Call done with a numbered list of the real defects you find: for each, name the function, say what is wrong and give the one-line fix. Report only real bugs, no style remarks.
@@ -0,0 +1,40 @@
1
+ """Inventory helpers for a small shop."""
2
+ import sqlite3
3
+
4
+
5
+ def page(items, number, size=10):
6
+ """Return page `number` (1-based) of `items`."""
7
+ start = number * size
8
+ return items[start:start + size]
9
+
10
+
11
+ def add_tag(item, tag, tags=[]):
12
+ tags.append(tag)
13
+ item["tags"] = tags
14
+ return item
15
+
16
+
17
+ def total_price(lines):
18
+ total = 0.0
19
+ for line in lines:
20
+ total += line["price"] * line["qty"]
21
+ return total
22
+
23
+
24
+ def is_free(price):
25
+ return price == 0.0
26
+
27
+
28
+ def find_by_name(conn, name):
29
+ cur = conn.execute("SELECT id, name FROM items WHERE name = '" + name + "'")
30
+ return cur.fetchall()
31
+
32
+
33
+ def load_stock(path):
34
+ f = open(path)
35
+ data = f.read()
36
+ return dict(line.split(",") for line in data.splitlines())
37
+
38
+
39
+ def stock_of(stock, name):
40
+ return int(stock[name])
@@ -0,0 +1 @@
1
+ Do not edit any file. Read svc/client.py and svc/config.py, then call done with an implementation plan (a numbered list of 6 to 10 steps) for adding client-side retries with exponential backoff and jitter to the HTTP client. The plan must name the exact files and functions to change, say how the retry limit and base delay become settings, say which failures are retried (for example HTTP 429 and 5xx, timeouts) and which are not, say how Retry-After is honoured, and say how it will be tested without a real network. Plan only, no code.
@@ -0,0 +1,3 @@
1
+ # svc
2
+
3
+ Tiny service layer. All network calls go through `svc/client.py` (`get`, `post`). Settings live in `svc/config.py`. Tests live in `tests/` (none for the client yet).
@@ -0,0 +1,18 @@
1
+ """HTTP client used by every service call."""
2
+ import json
3
+ import urllib.request
4
+
5
+ from svc.config import BASE_URL, TIMEOUT
6
+
7
+
8
+ def get(path):
9
+ with urllib.request.urlopen(BASE_URL + path, timeout=TIMEOUT) as resp:
10
+ return json.loads(resp.read())
11
+
12
+
13
+ def post(path, payload):
14
+ req = urllib.request.Request(
15
+ BASE_URL + path, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"}
16
+ )
17
+ with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
18
+ return json.loads(resp.read())
@@ -0,0 +1,3 @@
1
+ """Settings."""
2
+ BASE_URL = "https://api.example.internal"
3
+ TIMEOUT = 5
@@ -53,12 +53,12 @@ In *"MemGPT: Towards LLMs as Operating Systems"*, Packer et al. (2023) introduce
53
53
 
54
54
  Tether implements a deterministic three-tier memory hierarchy backed by `.tether/tether.db` (SQLite WAL + FTS5):
55
55
 
56
- 1. **Head + Tail L1 Active Elision**: Even within the active $K=2$ turn window, if a command emits an oversized output (`> 2,400 chars`), `ContextAssembler` keeps the first 900 chars (headers/command summary) + last 900 chars (stack trace/exit status) and elides the middle with a pointer to `mem recall <obs_id>`.
56
+ 1. **Head + Tail L1 Active Elision**: Even within the active $K=2$ turn window, if a command emits an oversized output (more than a third of `max_context_tokens` in chars, at least 2,400), `ContextAssembler` keeps the first 60% of the cap (headers/command summary) + the last 40% (stack trace/exit status) and elides the middle with a pointer to `mem recall <obs_id>`.
57
57
  2. **Ephemeral $K$-Turn Folding**: Once an observation ages past $K=2$ steps, it is folded into a single-line tombstone:
58
58
  ```text
59
59
  [obs_1 folded: `web fetch https://...` (3,420 chars) | use `mem recall obs_1` or `mem search <query>`]
60
60
  ```
61
- Exception (#69): the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The 2,400-char head+tail elision still applies, and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
61
+ Exception (#69): the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The head+tail elision still applies (an observation may use up to a third of `max_context_tokens`, at least 2,400 chars), and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
62
62
  3. **Sub-Millisecond SQLite FTS5 BM25 RAG**: Every raw observation and final deliverable is automatically chunked (~150 tokens per paragraph/section) and indexed into an FTS5 virtual table (`chunks_fts`) so the agent (or a parent agent) can retrieve exact passages via `mem search` or `agent query`.
63
63
 
64
64
  ---
@@ -22,7 +22,7 @@ When the provider sets `structured_history = True` (Ollama, OpenAI-compatible, A
22
22
 
23
23
  Raw tool outputs stay in the prompt for $K=2$ turns, then fold into a one-line tombstone (see [Built-in Tools](../tutorial/built-in-tools.md#mem-pinned-scratchpad-bm25-observation-recall)). For small models this has consequences ([#27](https://github.com/igormcsouza/tether/issues/27)), seen in the [course-builder example](https://github.com/igormcsouza/tether/tree/main/examples/course):
24
24
 
25
- - **Fix (#69).** Folding exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The 2,400-char head+tail elision still applies, and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command"). (`ContextAssembler(mem_enabled=..., workspace_root=...)`, `MAX_LIVE_READS`).
25
+ - **Fix (#69).** Folding exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The head+tail elision still applies (an observation may use up to a third of `max_context_tokens`, at least 2,400 chars), and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command"). (`ContextAssembler(mem_enabled=..., workspace_root=...)`, `MAX_LIVE_READS`).
26
26
  - **Re-viewing files loops, then invents data.** The model runs `edit view`, the output folds two turns later, it views again, and eventually writes from memory.
27
27
  - **What worked: one compact joined view, used right before the write, that ends with the exact next call.** The `course-source <module>` tool returns a module's plan and chosen videos in one output and finishes with `NOW WRITE THIS MODULE: act("edit write part-N.md", ...)`, so the data is still in the prompt when the write happens.
28
28
  - **Write long outputs in small pieces.** One big write dropped a whole section. The writer skill writes one `part-N.md` per module with `edit write`, then a `course-build` tool concatenates them into `course.md`.
@@ -48,7 +48,7 @@ Every entry, of any kind, may set optional positive integers: `context_tokens` (
48
48
 
49
49
  ### Timeout and retry
50
50
 
51
- Every entry may set `timeout` (positive seconds). It applies to the HTTP kinds (`openai`, `ollama`, `anthropic`; defaults 120 / 120 / 180 s) and to `command` (default 300 s). The HTTP providers retry automatically, at most 2 times with 1 s then 2 s backoff, on timeouts, network errors and HTTP 429/500/502/503/504. A 401/403 is never retried: it raises `ProviderError` naming the provider and the credential variable (`openai: HTTP 401: `NVIDIA_API_KEY` is missing or rejected; set it to a valid key`; the value is never printed), and a declared `api_key_env` that is unset or empty fails the session before any request with the same message. Other 4xx responses are never retried. When retries run out the call raises `ProviderError`, so the session fails with a readable `Worker error: ProviderError: openai model `gpt-4o` unavailable after 3 attempts (timeout 120s, last error: ReadTimeout)`. The retry is invisible to the prompt (0 tokens, no prefix change).
51
+ Every entry may set `timeout` (positive seconds). It applies to the HTTP kinds (`openai`, `ollama`, `anthropic`; defaults 120 / 120 / 180 s) and to `command` (default 300 s). The HTTP providers retry automatically, at most 2 times with 1 s then 2 s backoff, on timeouts, network errors and HTTP 429/500/502/503/504. A 401/403 is never retried: it raises `ProviderError` naming the provider and the credential variable (`openai: HTTP 401: `NVIDIA_API_KEY` is missing or rejected; set it to a valid key`; the value is never printed), and a declared `api_key_env` that is unset or empty fails the session before any request with the same message. Other 4xx responses are never retried. An HTTP 500 whose body names a rejected tool call (Ollama: `invalid tool call arguments`, a cut-off tool-call JSON) is not retried either, since the same request fails the same way; `ollama`, `openai` and `anthropic` turn it into a one-line hint to the model (a no-action reply), so the session continues instead of failing. The match is the text `tool call` in a 500 body, for any server. When retries run out the call raises `ProviderError`, so the session fails with a readable `Worker error: ProviderError: openai model `gpt-4o` unavailable after 3 attempts (timeout 120s, last error: ReadTimeout)`. The retry is invisible to the prompt (0 tokens, no prefix change).
52
52
 
53
53
  ### Kinds
54
54
 
@@ -1,5 +1,29 @@
1
1
  # Release Notes
2
2
 
3
+ ## v0.2.7 (2026-10-24)
4
+
5
+ Found by running Tether on the Polyglot-Python benchmark (34 Aider polyglot Python exercises) and reading why runs failed instead of guessing: sessions that ended mid-task on a server error, and models that re-read one file until the step cap. Closes #138, #141.
6
+
7
+ ### Benchmark
8
+
9
+ - `bench/run.py --tasks polyglot|polyglot-fast` runs the Polyglot-Python exercises, graded by each exercise's own tests; `bench/report.py` prints pass rate, mean score and a failure breakdown that keeps provider outages apart from harness outcomes. On the 12-exercise fast set (2 trials), the observation-cap fix below moved `qwen3.5:9b` from mean 0.64 to 0.80 and cut re-reads of one file from 32% to 19% of steps; `nemotron-3.5-lightning` went from 10 to 12 passes of 24.
10
+
11
+ ### Fixes
12
+
13
+ - Context: one active observation may now use up to a third of the context budget (`max_context_tokens * 4 / 3` chars, at least 2,400) before head+tail elision, instead of a fixed 2,400 chars. Before, a 6-12 KB test file or a 100-line slice of one was always cut to head+tail, so the model asked for another slice, got that cut too, and re-read the same file 10+ times: on the Polyglot-Python fast set 24 of 48 runs ended at the step cap, 45% of their steps were reads and only 19% were edits. The hard budget still folds older observations first, so the prompt cannot overflow; `ContextAssembler(active_char_cap=...)` still sets a fixed cap.
14
+ - Providers: when a server (Ollama, vLLM, llama.cpp behind `openai`, `anthropic`) answers HTTP 500 because it rejected the model's own tool call (`invalid tool call arguments for "act": unexpected end of JSON input`), the call is no longer retried and no longer ends the session as `worker_error` (#138). The model gets one line (`Your last call was rejected: the tool-call JSON was invalid or cut off. Send a shorter call, e.g. write long files in smaller pieces.`) through the existing no-action reminder and carries on, bounded by `max_steps`. Before, 13 of 34 Polyglot-Python runs on `qwen3.5:9b` died this way, mid-task and often with most tests passing. `ModelResponse` gains an optional `hint`. 0 static prompt tokens.
15
+
16
+ ## v0.2.6 (2026-10-23)
17
+
18
+ Found by using tether as its own SWE agent and by the v0.2.5 post-release check: a sandbox that refused harmless commands, sessions that only read, and a `query` that could not say why it found nothing. Closes #77, #93, #125, #127.
19
+
20
+ ### Fixes
21
+
22
+ - CLI: `tether query` now says why nothing matched (#93): `Error: unknown session` with exit code 1 for a wrong id, `No chunks are indexed for <id> yet` for an empty session, and the number of indexed chunks otherwise. `search_chunks` also tries a prefix match (`loop*`) after the AND and OR matches, so `loop` finds `looping`.
23
+ - Store: `SQLiteStore(":memory:")` no longer creates a file literally named `:memory:` in the working directory (#77). The path now maps to a temporary file outside the workspace, so every store method still shares one database.
24
+ - Sandbox: with `bwrap` active, `bash` no longer refuses commands by text for `..`, absolute paths outside the workspace, or `.env`/`.tether`/`.git/config` mentions (#125). The kernel mounts already enforce them (read-only root, hidden `$HOME`, masked secrets), so the text rules only produced false refusals such as `ls ~/..` or `cat /etc/hosts`. They remain the fallback when `bwrap` is missing, and `sudo`, `su`, disk tools, `rm -rf /`, `chmod 777`, fork bombs and `~/.ssh` stay refused in both modes.
25
+ - Sandbox: `.env.example`, `.env.sample`, `.env.template` and `.env.dist` are readable and writable; real `.env`, `.env.local`, `.env.production` stay masked and protected. Templates hold no secrets.
26
+
3
27
  ## v0.2.5 (2026-10-23)
4
28
 
5
29
  Found by benchmarking the same models inside Tether and inside a plain tool loop (7 tasks, 10+ models): model idioms made the single-`act` grammar fail invisibly, and loops or oversized prompts burned steps and tokens. Closes #76, #81, #86, #89, #92 and the shell-idiom bugs #109, #110.
@@ -46,6 +70,8 @@ Dogfooding fixes: the four milestone bugs (#74 #75 #85 #88) plus the friction fo
46
70
 
47
71
  ### Fixes
48
72
 
73
+ - Harness: a session that only reads gets a deterministic hint (#127). After 12 consecutive read-only steps with no change at all, then every 8 more, the observation ends with `[NUDGE] No file changes in N steps. Make your edit now, or call `done` with what you found.` (0 static prompt tokens). Before, the quiet nudge started only after a first change, so a model that never edited (nemotron-3-ultra and glm-5.3 on scoped briefs: 40 steps, 150k to 215k prompt tokens, no change) was never told.
74
+ - Harness: `awk`/`gawk`/`mawk` count as read-only (unless they print to a file, pipe to a shell, call `system()` or edit in place) and as range-shift reads of one file, so the #92 stall guard sees `awk 'NR>=a && NR<=b' f` loops. `NR>=5` used to look like a redirect, also inside compound commands (`grep ...; awk 'NR>=5' f`), which silently turned the nudges off.
49
75
  - Store: a `RUNNING` session whose worker pid no longer exists is flipped to `FAILED` (`Worker died (pid N) before finishing`) when read via `get_session`/`list_sessions`, so `status`, `list`, `wait` and `read` no longer report a killed worker as running forever (#85). A pid owned by another user counts as alive.
50
76
  - Providers: `429` retries wait `Retry-After` (else 10s doubling, capped 30s) and get up to 5 attempts instead of 3 quick ones, so rate-limited hosted models (NVIDIA free tier) no longer fail the session after 3 quick attempts.
51
77
  - Providers: each `post_with_retry` attempt is now bounded by a total `asyncio.wait_for(timeout)` deadline, so a trickling/stalled response cannot hang past the configured timeout; the deadline is retried like other transient errors and shows as `TimeoutError` (vs `ReadTimeout`) in the final `ProviderError` (#88).
@@ -85,7 +85,7 @@ trigger it, script the reply; if infrastructure must fail, add a fault to the pr
85
85
 
86
86
  ## Model conformance bench (developers only)
87
87
 
88
- `bench/` holds seven tasks (the `focus` task checks that an exact single-file goal is not lost in a distractor-heavy repo) with hidden-test graders and three runners (Tether, a plain tool loop,
88
+ `bench/` holds nine tasks (the `focus` task checks that an exact single-file goal is not lost in a distractor-heavy repo) with hidden-test graders and three runners (Tether, a plain tool loop,
89
89
  Claude Code) so the same model can be compared across harnesses and releases. It needs real
90
90
  models, so it is not in CI and not in the package: use it when onboarding a model or before a
91
91
  release to see friction that scripted tests cannot (the shell-wrapper, bare-command and