tether-sdk 0.2.9__tar.gz → 0.2.10__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 (221) hide show
  1. tether_sdk-0.2.10/.claude/gru/active/north-star.md +40 -0
  2. {tether_sdk-0.2.9/.claude/gru/active → tether_sdk-0.2.10/.claude/gru/archive}/v0-2-bugs.md +28 -2
  3. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/AGENTS.md +12 -3
  4. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/PKG-INFO +1 -1
  5. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/README.md +12 -0
  6. tether_sdk-0.2.10/bench/canary.py +39 -0
  7. tether_sdk-0.2.10/bench/canary_baseline.json +1 -0
  8. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/grade.py +4 -0
  9. tether_sdk-0.2.10/bench/history.jsonl +3 -0
  10. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/run.py +9 -2
  11. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/providers.md +2 -2
  12. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/release-notes.md +17 -0
  13. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/troubleshooting.md +3 -1
  14. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/conftest.py +1 -0
  15. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_faults.py +11 -3
  16. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_quirks.py +20 -0
  17. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/test_ollama.py +33 -0
  18. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_context.py +16 -0
  19. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_harness.py +96 -0
  20. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_multiplexer.py +29 -0
  21. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_sandbox.py +29 -0
  22. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_tether.py +1 -1
  23. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/tools/test_done.py +2 -1
  24. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/__init__.py +1 -1
  25. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/context.py +19 -1
  26. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/harness.py +51 -4
  27. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/multiplexer.py +10 -0
  28. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/providers/ollama.py +9 -1
  29. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/sandbox.py +37 -0
  30. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/tools/done.py +3 -0
  31. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.claude/gru/active/.gitkeep +0 -0
  32. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.claude/gru/archive/.gitkeep +0 -0
  33. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.claude/gru/archive/work-on-v0-2-4.md +0 -0
  34. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.claude/gru.json +0 -0
  35. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.github/workflows/ci.yml +0 -0
  36. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.github/workflows/delivery.yml +0 -0
  37. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.github/workflows/docs.yml +0 -0
  38. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/.gitignore +0 -0
  39. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/CONTRIBUTING.md +0 -0
  40. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/LICENSE +0 -0
  41. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/README.md +0 -0
  42. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/harness-bench/README.md +0 -0
  43. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/harness-bench/tether-adapter.patch +0 -0
  44. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/naive_agent.py +0 -0
  45. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/report.py +0 -0
  46. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/goal.txt +0 -0
  47. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/hidden/test_hidden.py +0 -0
  48. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ref/calc/__init__.py +0 -0
  49. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ref/calc/errors.py +0 -0
  50. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ref/calc/evaluator.py +0 -0
  51. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ref/calc/parser.py +0 -0
  52. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ref/calc/tokenizer.py +0 -0
  53. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ws/calc/__init__.py +0 -0
  54. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ws/calc/errors.py +0 -0
  55. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ws/calc/evaluator.py +0 -0
  56. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ws/calc/parser.py +0 -0
  57. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ws/calc/tokenizer.py +0 -0
  58. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ws/tests/__init__.py +0 -0
  59. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-hard/ws/tests/test_calc.py +0 -0
  60. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/goal.txt +0 -0
  61. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/hidden/test_hidden.py +0 -0
  62. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/ws/cli.py +0 -0
  63. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/ws/ledger/__init__.py +0 -0
  64. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/ws/ledger/parser.py +0 -0
  65. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/ws/ledger/report.py +0 -0
  66. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/ws/sample.csv +0 -0
  67. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/ws/tests/__init__.py +0 -0
  68. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-medium/ws/tests/test_report.py +0 -0
  69. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-simple/goal.txt +0 -0
  70. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-simple/hidden/test_hidden.py +0 -0
  71. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-simple/ws/tests/__init__.py +0 -0
  72. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-simple/ws/tests/test_textutils.py +0 -0
  73. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/code-simple/ws/textutils.py +0 -0
  74. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/codereview/goal.txt +0 -0
  75. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/codereview/ws/inventory.py +0 -0
  76. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/content/goal.txt +0 -0
  77. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/content/ws/.keep +0 -0
  78. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/goal.txt +0 -0
  79. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/hidden/test_hidden.py +0 -0
  80. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/AGENTS.md +0 -0
  81. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/README.md +0 -0
  82. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/docs/architecture.md +0 -0
  83. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/docs/style.md +0 -0
  84. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/mini/__init__.py +0 -0
  85. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/mini/cli.py +0 -0
  86. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/mini/fmt.py +0 -0
  87. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/mini/stats.py +0 -0
  88. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/mini/textio.py +0 -0
  89. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/tests/__init__.py +0 -0
  90. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/focus/ws/tests/test_stats.py +0 -0
  91. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/plan/goal.txt +0 -0
  92. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/plan/ws/README.md +0 -0
  93. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/plan/ws/svc/__init__.py +0 -0
  94. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/plan/ws/svc/client.py +0 -0
  95. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/plan/ws/svc/config.py +0 -0
  96. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/qa/goal.txt +0 -0
  97. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/qa/ws/.keep +0 -0
  98. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/review/goal.txt +0 -0
  99. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/bench/tasks/review/ws/paper.md +0 -0
  100. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/architecture.md +0 -0
  101. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/benchmarks.md +0 -0
  102. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/extending.md +0 -0
  103. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/glossary.md +0 -0
  104. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/guides/a-pipeline-with-checks.md +0 -0
  105. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/guides/add-your-own-tool.md +0 -0
  106. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/guides/fix-a-failing-test.md +0 -0
  107. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/guides/use-tether-from-your-agent.md +0 -0
  108. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/index.md +0 -0
  109. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/javascripts/mathjax.js +0 -0
  110. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/llms.txt +0 -0
  111. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/agent-interface.md +0 -0
  112. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/cli.md +0 -0
  113. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/context-and-store.md +0 -0
  114. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/harness.md +0 -0
  115. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/index.md +0 -0
  116. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/multiplexer-and-decorator.md +0 -0
  117. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/sandbox-and-skills.md +0 -0
  118. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/reference/tools.md +0 -0
  119. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/agent-interface.md +0 -0
  120. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/built-in-tools.md +0 -0
  121. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/cli-and-headless.md +0 -0
  122. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/index.md +0 -0
  123. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/multi-agent.md +0 -0
  124. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/sdk-and-custom-tools.md +0 -0
  125. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/security-and-sandbox.md +0 -0
  126. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/skills.md +0 -0
  127. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/docs/tutorial/workflows-and-telemetry.md +0 -0
  128. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/__init__.py +0 -0
  129. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/proxy.py +0 -0
  130. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_agents.py +0 -0
  131. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_defaults.py +0 -0
  132. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_lifecycle.py +0 -0
  133. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_limits.py +0 -0
  134. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_sandbox.py +0 -0
  135. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/e2e/test_surface.py +0 -0
  136. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/README.md +0 -0
  137. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/checks.py +0 -0
  138. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/course.yaml +0 -0
  139. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/ruff.toml +0 -0
  140. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/skills/planner/SKILL.md +0 -0
  141. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/skills/searcher/SKILL.md +0 -0
  142. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/skills/searcher/tools.py +0 -0
  143. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/skills/writer/SKILL.md +0 -0
  144. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/examples/course/skills/writer/tools.py +0 -0
  145. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/mkdocs.yml +0 -0
  146. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/pyproject.toml +0 -0
  147. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/scripts/record_golden.py +0 -0
  148. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/skills/researcher/SKILL.md +0 -0
  149. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/__init__.py +0 -0
  150. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/conftest.py +0 -0
  151. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/fixtures/dialects.jsonl +0 -0
  152. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/coding_edit_failure_modes.json +0 -0
  153. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/coding_edit_failure_modes.snap.json +0 -0
  154. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/done_nudge.json +0 -0
  155. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/done_nudge.snap.json +0 -0
  156. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/read_range_pingpong.json +0 -0
  157. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/read_range_pingpong.snap.json +0 -0
  158. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/research_web.json +0 -0
  159. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/research_web.snap.json +0 -0
  160. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/shell_wrappers.json +0 -0
  161. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/shell_wrappers.snap.json +0 -0
  162. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/waiting_input_resume.json +0 -0
  163. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/golden/waiting_input_resume.snap.json +0 -0
  164. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/__init__.py +0 -0
  165. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/test_anthropic.py +0 -0
  166. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/test_base.py +0 -0
  167. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/test_declarative.py +0 -0
  168. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/test_dialects.py +0 -0
  169. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/test_openai.py +0 -0
  170. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/providers/test_retry.py +0 -0
  171. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_agent_interface.py +0 -0
  172. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_benchmarks_docs.py +0 -0
  173. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_cli.py +0 -0
  174. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_cli_docs.py +0 -0
  175. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_context_config.py +0 -0
  176. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_decorator.py +0 -0
  177. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_docs_structure.py +0 -0
  178. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_edit_tool.py +0 -0
  179. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_golden.py +0 -0
  180. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_guides_docs.py +0 -0
  181. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_integration.py +0 -0
  182. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_llms_txt.py +0 -0
  183. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_misses.py +0 -0
  184. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_params.py +0 -0
  185. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_pipeline.py +0 -0
  186. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_providers_docs.py +0 -0
  187. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_readme.py +0 -0
  188. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_release_hardening.py +0 -0
  189. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_retro.py +0 -0
  190. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_security_hardening.py +0 -0
  191. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_skills.py +0 -0
  192. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_store.py +0 -0
  193. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_structured_history.py +0 -0
  194. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/test_troubleshooting_docs.py +0 -0
  195. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/tools/__init__.py +0 -0
  196. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/tools/test_agent.py +0 -0
  197. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/tools/test_bash.py +0 -0
  198. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/tools/test_edit.py +0 -0
  199. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/tools/test_mem.py +0 -0
  200. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tests/tools/test_web.py +0 -0
  201. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/api.py +0 -0
  202. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/cli.py +0 -0
  203. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/decorator.py +0 -0
  204. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/mcp.py +0 -0
  205. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/pipeline.py +0 -0
  206. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/providers/__init__.py +0 -0
  207. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/providers/anthropic.py +0 -0
  208. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/providers/base.py +0 -0
  209. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/providers/declarative.py +0 -0
  210. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/providers/openai.py +0 -0
  211. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/retro.py +0 -0
  212. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/schema.py +0 -0
  213. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/skills.py +0 -0
  214. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/store.py +0 -0
  215. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/tools/__init__.py +0 -0
  216. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/tools/agent.py +0 -0
  217. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/tools/bash.py +0 -0
  218. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/tools/edit.py +0 -0
  219. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/tools/mem.py +0 -0
  220. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/tether/tools/web.py +0 -0
  221. {tether_sdk-0.2.9 → tether_sdk-0.2.10}/uv.lock +0 -0
@@ -0,0 +1,40 @@
1
+ # North Star: Polyglot-Python 80% on qwen3.5:9b
2
+ - Slug: north-star
3
+ - Status: active
4
+ - Started: 2026-10-27 Updated: 2026-10-27 Closed: —
5
+ - Branches / PRs: branch fix/north-star-cheap-wins (worktree .claude/worktrees/north-star-c852b8), tracker #173
6
+ - Budget: warn 300k / stop 400k paid tokens, max escalations 3, Tier 3 never without ask Used: ~small (gru only)
7
+
8
+ ## Goal
9
+ Polyglot-Python (34 ex, 2 trials, `--poly-verify`, default 8192 ctx) pass rate on ollama/qwen3.5:9b: 51.5% -> 80% by v0.3.0. Lanes: default (official) and `swe` role (#172, must not score below default).
10
+
11
+ ## Requests
12
+ - 2026-10-27 PO: "Get to the North Star"; dev too slow, benches unreported. Asked to review the last cross-session and close it (done: v0-2-bugs archived).
13
+
14
+ ## Decisions
15
+ - 2026-10-27 PO answers: lane 1 = deterministic fixes first (no GPU); merge proof = tests + offline replay, one batched full 34x2 bench after 2-3 PRs, reported immediately; budget default 300k/400k.
16
+ - Ranked levers (Opus assessment): (1) runaway thinking hits num_ctx, +8-12pp, two attempts inconclusive (think=false 20 rows +3; length-retry rejected); (2) compact test output (relativize 195-char workspace path = 14% of obs chars, tracebacks = 51%), +3-6pp; (3) 4/68 runs died on 120s ProviderError timeout ~6pp (floor #171); (4) test-file access #170.
17
+ - STOP list: nudge variants, whole-set 24-run A/Bs, wall cap as part of the north star.
18
+
19
+ ## Problems
20
+ - 14 exercises never pass: book-store bowling connect forth hangman paasio poker pov react rest-api scale-generator sgf-parsing transpose zipper. Bench GPU: 8GB RTX 5050, one Ollama.
21
+
22
+ ## Plan (task cards)
23
+ - A: ollama default timeout 120 -> 300 (tests/providers/test_ollama.py test written; docs/reference/providers.md line 95 says 120). Gru patches (1 line).
24
+ - B: bash output: strip workspace prefix + ==== / ---- rule lines in sandbox.execute_bash_with_code (tests/test_sandbox.py test written). Gru patches (small).
25
+ - C (next): collapse unittest tracebacks; build bench/replay.py offline measure first.
26
+ - Then: output headroom / bounded thinking (#135/#147), #170, #171.
27
+
28
+ ## Ledger
29
+ | Task | Tier | Model | Harness | Session | Attempt | Tokens | Time | Result |
30
+ |---|---|---|---|---|---|---|---|---|
31
+ | A,B | plan | gru | - | - | 1 | small | - | tests written failing, patch pending |
32
+
33
+ ## Progress log
34
+ - 2026-10-27 branch fix/north-star-cheap-wins; failing tests added (sandbox prefix, ollama timeout). Closed v0-2-bugs log.
35
+
36
+ ## Next steps
37
+ - [ ] Patch A and B, update docs (providers.md, release notes), unit+e2e, PR with `north star:` line
38
+ - [ ] bench/replay.py offline proof for B
39
+ - [ ] One batched full 34x2 after A+B+C, report at once
40
+ - 2026-10-27 merged #180 (timeout 300s, compact bash, canary). PR #181 traceback collapse (canary +0.12, offline -12.6% obs chars, e2e 69). PR #182 outage retry (#171, e2e new scenario). PO: 80% is the impossible focus goal; wants v0.2.10 plan ranked by north star. Canary tool: bench/canary.py (12 min, 7 rows). Note: bench `pass` is graded by exercise tests, so #177 (done at max steps) does NOT move the bench number, only the user-facing floor.
@@ -1,7 +1,7 @@
1
1
  # v0.2 bugs
2
2
  - Slug: v0-2-bugs
3
- - Status: active
4
- - Started: 2026-10-20 Updated: 2026-10-24 Closed: —
3
+ - Status: done
4
+ - Started: 2026-10-20 Updated: 2026-10-24 Closed: 2026-10-27
5
5
  - Branches / PRs: branch `claude/keep-feature-context-resume-0ec918` (worktree); base `main` at 125b386 (release v0.2.2)
6
6
 
7
7
  ## Goal
@@ -108,3 +108,29 @@ Track and fix bugs found in Tether v0.2.x. Exact bug list not yet given by PO.
108
108
  - 2026-10-25 PO: milestones reshuffled: v0.2.8 = release now (#144 #145 + sandbox venv + CLI/providers docs), v0.2.9 = docs overhaul (#150-#154, #63-#66), v0.2.10 = #27 #79 #132-#137. Merged: #146 #148 #149 #155 #156 #157 #158. Release PR v0.2.8 (version bump, notes) prepared; GitHub Release only on PO yes.
109
109
  - 2026-10-25 RELEASED v0.2.8 'Room to Answer' (release PR #159 -> f4370b1, GitHub Release, delivery.yml success, PyPI tether-sdk 0.2.8 live, milestone closed). Docs v0.2.9 started: #151 site structure PR (nav = Start here/Guides/Concepts/Reference/Help, 'For:' line per page, CONTRIBUTING.md replaces docs/testing.md + releasing.md, tests/test_docs_structure.py). Lesson: ruff format --check also formats Python blocks in docs; renamed anchors break the strict mkdocs build via old release-notes links -> run both before pushing docs.
110
110
  - 2026-10-25 PO: 'merge all docs PRs and cut the next version'. Docs plan (#150) done: #155 #156 #157 #160 #161 #162 #165 #166 #167 merged, epic closed. Found #163 while writing guide 1 (qwen3.5:9b fixes the test, passes, never calls done -> FAILED Stalled); #164 merged on PO yes (Rule 3 reading: passing verify gates at stall time -> COMPLETED). Release PR v0.2.9 prepared. Next: v0.2.10 = #27 #79 #132-#137 (8 issues; pick by evidence, e.g. bench failure breakdown after v0.2.9). Tether friction this stretch: docs rewrites by Nemotron invent API facts and end without done; harness.py cards need 40+ steps.
111
+ - 2026-10-26 v0.2.9 released (#168). Polyglot fast set + poly-verify on v0.2.9: 12/24, mean 0.765; 8192 vs 16384 context on 12 exercises: 16384 not a regression (easy 9/12 both, mean 0.872->0.937; hard 3->5/12) but ~+50% s/step (decode-bound), more 600s bench-cap timeouts; default stays 8192 (token saver), --context-tokens bench option on branch bench/137-context-tokens (no PR). Root-cause read of the breakdown found #169: bare `done` / typed `act("done", ...)` rejected, 8 of 36 runs wasted 1-13 steps, 2 false FAILED at score 1.0.
112
+ - 2026-10-26 #169 fix (PR on branch fix/169-bare-done): tests first (e4f0feb), fix 143d79e. Ledger: #169 card A (bare done) | Tier 0 | z-ai/glm-5.3 | tether | ses_152e5df2 | attempt 2 | 7 steps, 6.6k tokens | ok (3 lines). Card B (act() text unwrap) | nemotron-3-ultra ses_d9caa19b (combined card A+B): 25 steps, 230k prompt tokens, zero edits; glm-5.3 ses (800s timeout, rate-limited, zero edits) -> escalated to plan level, 5-line patch written by gru.
113
+ - Tether friction (#169): (1) nemotron-3-ultra on a 2-file card read 3 large files and ran pytest 12 times in 25 steps without ever calling `edit write` (read-only loop, no pre-change nudge fired before step 12, prompt 230k tokens); (2) nvidia free tier + glm-5.3 ~70s per step (rate limit) so an 800s timeout ended with no edit. Cards under ~15 lines and one file each worked (glm-5.3 7 steps).
114
+ - 2026-10-26 #169 bench (done-fix vs v0.2.9, qwen3.5:9b, poly-verify, 2 trials, 8192): done-rejected steps 18 (4 runs) -> 0; pass 12/24 -> 10/24, mean 0.765 -> 0.694 (one infra outage cell rerun). Within noise: every failing run in the new set made 0 done calls (30 steps, never called done), so none touched the changed code. Affine-cipher t0 28 -> 7 steps, bottle-song t0 21 -> 14. Remaining dominant failure: max_steps without calling done (rewrite loops, e.g. food-chain t1 scores 1.0 yet FAILED). e2e 69 passed, unit 391 passed, 100% cov.
115
+ - 2026-10-27 North star set (#173, AGENTS.md "North Star"): Polyglot-Python on qwen3.5:9b default 8192, full 34: 80% by v0.3.0. Baseline pinned (#178, bench/history.jsonl): main@b25cbbe 35/68 = 51.5%, mean 0.718; 17 fails at 30-step cap, 11 at the 600s bench wall cap, 14 exercises never pass (book-store bowling connect forth hangman paasio poker pov react rest-api scale-generator sgf-parsing transpose zipper). Naive no-harness loop on fast-12: 3/24 (12.5%) vs tether 12/24.
116
+ - 2026-10-27 Experiments, all unmerged (data on issues): ctx 16384 fast-12 11/24 vs 12/24 (mean 0.807 vs 0.711, 9/13 fails hit 600s cap) -> rejected (#170); paging nudge flat; bounded cmd replay worse (10->8/24); think=false on 20 targeted rows 9/20 vs 6/20 (net +3, pre-reg +4) ; adaptive length-retry (branch fix/135-length-retry) 1/12 on failing rows -> rejected (#135); blanket think=false on fast-12 stopped at 11/24 runs (7 pass), inconclusive.
117
+ - 2026-10-27 OPUS ASSESSMENT (architect, read-only, 13 tool calls over base34 t.db):
118
+ * First-order failure nobody had measured = runaway thinking hitting num_ctx: 53 assistant turns with no action, 45 have prompt+completion in [8192,9216) (re-verified by gru: 45/53, 15 runs, 3672 s). Model thinks until the window is full, Ollama stops, tether records "(no output)" and nudges; ~85-110 s per turn. In the 11 wall-cap runs 41% of model time was empty turns: wall-cap failures are this, not slow progress. context.py:333 budgets prompt to 3/4 of ctx and leaves ~2048 for output; thinking does not fit.
119
+ * 4 of 68 runs died of ProviderError timeout 120s x3 (book-store t1, transpose t0, hangman t1, pig-latin t1): unretried infra loss ~6pp, breaks the floor (#171).
120
+ * Second mechanism: read-loop to the step cap (paasio poker react zipper sgf pov-t1 grep-t0 phone-t0 bowling-t1; fast 6-11 s/step). Test-run output (unittest tracebacks) = 51% of all observation chars; the 195-char bench workspace path = 14% of observation chars (~60 tokens per traceback line); react prints 6.5KB per test run, paasio 12KB.
121
+ * Why no progress: picked by proxy (re-read %) not mechanism; thinking not recorded (#32); 24-run bench sd ~2.4 passes = a +10pp change is ~1 sigma; whole-set A/B dilutes effects; the 600s wall cap measures GPU speed and thinking length (confound; ctx16 raised mean score but lost on wall); infra deaths baked into baseline.
122
+ * Fast assessment tiers: T0 offline replay `bench/replay.py` (~150 lines; rebuild each step's prompt from a recorded t.db through the candidate assembler; prompt tokens, output headroom, turns exceeding num_ctx, test-output chars; seconds, no GPU; proves prompt shape only). T1 determinism: same exercise twice with --param seed=1 (temp already 0.2), compare command sequences; first divergence = did the change fire; identical = zero effect (10 min). T2 counterfactual fork: copy a failing t.db, cut where the mechanism first fires, resume with the change (5-15 min/run). T3 targeted subset: only rows where the mechanism fired + 4 always-pass heavy-thinking canaries (zebra t1, vlq t0, two-bucket t0, go-counting t1), paired seeds, sequential stop (ship +4 net flips, kill at -2), <=1h. T4 full 34x2 overnight. Rule: a change gets GPU time only if T0/T1 shows its target metric moves.
123
+ * Parallelism: GPU is an 8GB RTX 5050; two Ollama instances will not fit; try OLLAMA_NUM_PARALLEL=2 (~1.5x?) for 10 min, only after dropping the wall cap (contention distorts wall).
124
+ * Ranked levers: (1) output headroom + bounded thinking, expected +8-12pp (15 affected runs, partial 0.6-0.75): test think=false T3, real fix = on done_reason=="length" with no tool call retry once with think=false, reserve output headroom in context.py, scale provider timeout to the generation budget, overlaps #135/#147; (2) compact test-run output: relativize workspace_root in bash observations, collapse unittest tracebacks to `test_x:105 AssertionError: 2 != 4`, drop ====/---- lines, +3-6pp, verify via T0 then T3; (3) test-file access #170, +2-4pp low confidence (def test_ index when file exceeds cap).
125
+ * 2-week workflow: D1 build replay.py + seed determinism check + queue think=false T3; D2 headroom/think-retry PR; D3-4 output-compaction PR proven with T0 then T3; bench-rules PR (official cap = 30 steps only with 1800s safety wall, p50/p90 wall as indicator, short workspace path /tmp/tb/<ex>, provider timeout no longer kills a run silently, re-pin baseline overnight once); end wk1 full T4; wk2 #170 then #171. Opus does diagnosis/tooling/acceptance tests/verdicts, minions mechanical PRs only; one GPU queue. STOP: nudge variants, whole-set 24-run A/Bs, wall cap as part of the north star.
126
+ - 2026-10-27 Result of following it (gru): lever 1 tested twice, neither clean: think=false 20 rows +3 net, adaptive length-retry rejected (empty turns 3672s -> 332s but 20 turns still >2500 completion tokens: thinking is slow even when it fits; blanket think=false ran 405 turns in the time retry ran 243). T0/T1/T2 never built.
127
+ - 2026-10-27 PO feedback (verbatim gist): dev is far too slow ("1 line of code and an entire day benching it"), benches finish unreported, results get thrown away because of the impossible goal; gru is losing itself on the work; wants the Opus findings logged. LESSON: stop bench-per-line-of-code loops; decide with a fast tier first, ship deterministic/obviously-correct improvements with unit+e2e evidence, report every bench end immediately (watcher fires on the results file, not on the launcher shell).
128
+ ## Next steps (as of 2026-10-27, awaiting PO decision)
129
+ - [ ] PO to choose: (a) finish blanket think=false fast-12 (~13 runs, 45 min), (b) offline output compaction (no GPU), (c) plan only. Nothing is running; no open PRs.
130
+
131
+
132
+ ## Wrap-up (2026-10-27, closed by PO request)
133
+ - Outcome: v0.2.3 -> v0.2.9 released (PyPI tether-sdk 0.2.9). Bugs and docs overhaul done; goal shifted to the North Star (#173): Polyglot-Python 80% on qwen3.5:9b by v0.3.0. Baseline 35/68 = 51.5% (main@b25cbbe).
134
+ - Durable lessons: (1) never bench per line of code: decide with a fast tier first (offline replay of recorded t.db, determinism check), GPU only for targeted rows; (2) report every bench end immediately; (3) cards under ~15 lines, one file, worked on tether minions (glm-5.3 7 steps); 2-file cards and docs rewrites on Nemotron Ultra loop or invent API facts; (4) run `ruff format --check` and strict mkdocs before docs pushes; (5) nudge/wording variants and whole-set 24-run A/Bs do not move the number (sd ~2.4 passes).
135
+ - Open debt: #27 #79 #132-#137 #170 #171; branches unmerged: fix/170-bounded-cmd-replay (rejected), fix/135-length-retry (rejected), bench/137-context-tokens; Harness-Bench #123 blocked (docker firewall + key).
136
+ - Tier cost: Tier 0 (Nemotron Ultra, GLM-5.3 on tether) solved only tiny single-file cards; everything else was finished at plan level by gru.
@@ -28,6 +28,15 @@ Tether stays **lightweight, reliable, extensible, model-agnostic, easy to use, r
28
28
 
29
29
  Reviewers and the Team Leader cite the failing line when they reject a change.
30
30
 
31
+ ### North Star (the one number; see tracker #173)
32
+ **Polyglot-Python pass rate on `ollama/qwen3.5:9b` (our weakest model), default config, full 34 exercises: 80% by v0.3.0.** Nothing important is set aside for it, but everything is ranked by it:
33
+ 1. Every issue, PR and benchmark must say how it moves this number (or tokens per solved exercise at the same pass rate). A benchmark that cannot show a change in it is not run; an issue that cannot name the link waits behind one that can.
34
+ 1b. Two lanes, both tracked: the **general-purpose default** (no role) is the official number and must reach 80%; the shipped **`swe` role** (#172) is reported next to it, must never score below the default, and gets its own target once its baseline is pinned.
35
+ 2. Weakest-model rule: every stronger model must score at least what `qwen3.5:9b` scores (and at least 80% once reached). If we plateau, stronger models may not fall below the plateau.
36
+ 3. Other indicators (tokens per pass, steps, wall-clock, prompt size, coverage) may slow or pause but never degrade between releases.
37
+ 4. Floor, a promise rather than a goal: every task delivers a real, honest, usable result in a fair time; no unretried outage, no empty result, no unrelated hallucinated deliverable (#171).
38
+ 5. Every PR body and issue-close comment ends with `north star: <before> -> <after> (runs, noise, run dir)`; the tracker's History table and `bench/history.jsonl` are updated at every release.
39
+
31
40
  ---
32
41
 
33
42
  ## 2. The Golden Rules (Non-Negotiable Invariants)
@@ -43,7 +52,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
43
52
 
44
53
  ### Rule 3: Explicit Termination (`act("done", stdin="...")`), Interactive Inquiry (`done --ask`), & Pre-`done` Verification Gates
45
54
  - 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
- - 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`.
55
+ - 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`. A bare `done` (no answer, no stdin) completes with the result `Done.` and still runs the `verify` gates; `done --ask` with no question is an error (#169).
47
56
  - 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
57
  - 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
58
  - 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. When the stall guard is about to end a session as `Stalled` and gates are configured, it runs them first: a change was made and every gate passes, so the session ends `COMPLETED` ("Finished by the harness"), since the deterministic gate is the completion criterion the caller asked for (#163).
@@ -205,7 +214,7 @@ uv run mypy tether/
205
214
  2. **Async-First Design:**
206
215
  - All model calls and I/O-bound tool executions must be `async`/`await` native, while seamlessly wrapping synchronous user `@tool` functions.
207
216
  3. **Graceful Degradation on 9B Hallucinations:**
208
- - Small models occasionally emit `cmd` with minor quirks (e.g., leading `$`, wrapping backticks, or calling a tool name directly instead of `act`). The multiplexer must normalize trivial syntax quirks deterministically before failing. This includes unescaping literal `\n`/`\t` in one-line `stdin` for `edit write`/`edit replace` and `done` (never `bash`), turning a trailing shell heredoc in `cmd` (`edit write f <<'EOF' ... EOF`) into `stdin` (never for `bash`), unwrapping `bash -c/-lc '<cmd>'` and `sh -c` wrappers, running an unknown first word through the enabled `bash` tool (never for disabled built-ins), registering extra quirk fixers with `register_normalizer`, mapping a native tool call named other than `act` to `<name> --k 'v'` so the JIT usage answers it, inferring the `edit` verb (`edit <path>` -> view/replace/write) and accepting `apply_patch` (`*** Begin Patch` or unified-diff patches, also when a diff arrives as `edit <path>` stdin) as an `edit` verb.
217
+ - Small models occasionally emit `cmd` with minor quirks (e.g., leading `$`, wrapping backticks, or calling a tool name directly instead of `act`). The multiplexer must normalize trivial syntax quirks deterministically before failing. This includes unescaping literal `\n`/`\t` in one-line `stdin` for `edit write`/`edit replace` and `done` (never `bash`), turning a trailing shell heredoc in `cmd` (`edit write f <<'EOF' ... EOF`) into `stdin` (never for `bash`), unwrapping `bash -c/-lc '<cmd>'` and `sh -c` wrappers, unwrapping an `act("<cmd>", stdin="...")` call typed as text in `cmd` (also after `bash`) into the command and its `stdin` (#169), running an unknown first word through the enabled `bash` tool (never for disabled built-ins), registering extra quirk fixers with `register_normalizer`, mapping a native tool call named other than `act` to `<name> --k 'v'` so the JIT usage answers it, inferring the `edit` verb (`edit <path>` -> view/replace/write) and accepting `apply_patch` (`*** Begin Patch` or unified-diff patches, also when a diff arrives as `edit <path>` stdin) as an `edit` verb.
209
218
  4. **Golden traces:** behaviour the model sees (framing, folding, nudges, tool output) is locked by `tests/golden/*.snap.json`. A PR that changes it runs `UPDATE_GOLDEN=1 pytest tests/test_golden.py` and the snapshot diff is part of the review; unrelated traces must not change. See `CONTRIBUTING.md`.
210
219
  5. **E2E scenarios:** every feature or bug fix ships with a scenario in `e2e/` (real CLI, real ollama behind a fault-injecting proxy, local only, `TETHER_E2E=1 uv run pytest e2e`). Assert on what tether sends and records, never on the model's skill; script the reply or inject the fault so the test does not depend on model potency. **Before merging any PR, run `TETHER_E2E=1 uv run pytest e2e` locally and put the result in the PR description (CI cannot run it). Every failure must be mitigated before merge: fix the code, or fix a wrong scenario, or record in the PR why it is acceptable and what follows up. A PR is not merged with an unexplained red scenario.** See `CONTRIBUTING.md`.
211
220
  6. **Documentation Integrity:**
@@ -225,5 +234,5 @@ uv run mypy tether/
225
234
  - Hard stops (ask first): force push / history rewrite; merging a PR and cutting a release need the PO's explicit yes each time
226
235
  - Design docs: `AGENTS.md` sections 1-3, `docs/architecture.md`, `docs/reference/*`
227
236
  - Orchestration: **`/gru` runs this repo** (Team Leader + router). Strong model plans, writes the failing acceptance tests first, reviews and decides; tether does the typing as the minion at Tier 0 on the strongest free model (`nvidia/nvidia/nemotron-3-ultra-550b-a55b`, fallback `nvidia/z-ai/glm-5.3`), escalating one tier only on evidence. Tiers, context sizes and sensitive paths: `.claude/gru.json`. tether's own failures while doing a card are logged as friction and filed as issues (with the PO's OK).
228
- - Improvement loop: **every PR leaves tether a little better**, measured, not asserted: a behaviour change ships with a bench or e2e delta (Polyglot-Python fast set on `ollama/qwen3.5:9b`, `bench/`) in the PR body, reported honestly even when it is flat. After a merge, gru re-reads the next planned milestone and the other open bug issues and picks the one that clearly improves tether most (evidence from bench failure breakdowns and `tether stats` friction beats guesses); features and docs wait behind bugs that cost steps or pass rate.
237
+ - Improvement loop: **every PR leaves tether a little better**, measured, not asserted: a behaviour change ships with a bench or e2e delta (Polyglot-Python on `ollama/qwen3.5:9b`, `bench/`) in the PR body as a `north star:` line (see North Star), reported honestly even when it is flat. After a merge, gru re-reads the next planned milestone and the other open bug issues and picks the one that clearly improves tether most (evidence from bench failure breakdowns and `tether stats` friction beats guesses); features and docs wait behind bugs that cost steps or pass rate.
229
238
  - gru store: .claude/gru/ (committed; legacy `feature-context` logs were moved here)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tether-sdk
3
- Version: 0.2.9
3
+ Version: 0.2.10
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
@@ -17,6 +17,10 @@ restored before grading, so editing it cannot pass), `--poly-verify` adds a `--v
17
17
  `python3 bench/report.py <out>` prints pass rate and mean score with a failure breakdown that keeps provider outages
18
18
  (`outage`, `timeout`) apart from harness outcomes (`max_steps`, `stalled`, `wrong_answer`, `worker_error`).
19
19
 
20
+ **Did my change help? (15 min)** `python3 bench/canary.py /tmp/canary-<change> [--tether-bin <venv>/bin/tether]` runs 7 exercises
21
+ once (4 flaky ones that can flip, 3 fast guards) and prints each score next to the pinned baseline with a verdict
22
+ (`REJECT` / `PROMISING` / `FLAT`). Only `PROMISING` earns the full 34x2 run. `--tasks canary` is the same set for `run.py`.
23
+
20
24
  Three runners, so a gap can be attributed:
21
25
 
22
26
  | runner | what it is |
@@ -43,3 +47,11 @@ Rate limits (NVIDIA: 40 requests/min) invalidate results silently; set `rpm:` in
43
47
  and keep parallelism low (`--par nvidia=2`).
44
48
 
45
49
  Caveats: one run per cell is noisy (about 0.1); the scores are regex and hidden-test based.
50
+
51
+ ## History (the north-star ledger)
52
+
53
+ `bench/history.jsonl` is the single source for the tracker's History table (#173). One JSON object per line: `date`, `version`, `model`, `runner`, `tasks`, `trials`, `context`, `runs`, `passed`, `pass_rate`, `mean_score`, failure counts, mean steps/prompt tokens/wall, `note`.
54
+
55
+ A PR that changes behaviour the model sees appends one line for its own run (same model, default 8192 context, `--poly-verify`) and ends its body with `north star: <before> -> <after> (runs, noise, run dir)`. Noise is about 0.1 per run, so a claim needs enough runs to beat it; a flat result is appended and reported as flat.
56
+
57
+ The official number is the full 34 exercises × 2 trials (68 runs) with the 600 s per-run wall cap. Use `--tasks polyglot-fast` for quick checks and label it `polyglot-fast-12`.
@@ -0,0 +1,39 @@
1
+ """15-minute check: did this change help? `python3 bench/canary.py <out-dir> [--tether-bin ...]`.
2
+
3
+ Runs 7 Polyglot exercises once (4 flaky ones that can flip, 3 fast guards), then compares each
4
+ score with the pinned baseline (`canary_baseline.json`, mean of 2 runs on main@b25cbbe).
5
+ Verdict: REJECT if a guard drops >0.2, PROMISING if the flaky mean gains >=0.1, else FLAT.
6
+ Only PROMISING changes earn a full 34x2 run. One run per exercise is noisy: a FLAT verdict
7
+ means "no evidence", not "proven useless".
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+
15
+ HERE = os.path.dirname(os.path.abspath(__file__))
16
+ sys.path.insert(0, HERE)
17
+ import grade # noqa: E402
18
+
19
+
20
+ def main(out, extra):
21
+ cmd = [sys.executable, f"{HERE}/run.py", "--models", "ollama/qwen3.5:9b", "--tasks", "canary",
22
+ "--poly-verify", "--par", "ollama=1", "--out", out, *extra]
23
+ subprocess.run(cmd, check=True)
24
+ base = json.load(open(f"{HERE}/canary_baseline.json"))
25
+ now = {}
26
+ for line in open(f"{out}/results.jsonl"):
27
+ r = json.loads(line)
28
+ now[r["task"][5:]] = r["score"]
29
+ flaky = [e for e in base if e not in grade.GUARDS]
30
+ for e in base:
31
+ print(f"{e:20} {base[e]:.2f} -> {now.get(e, float('nan')):.2f}{' (guard)' if e in grade.GUARDS else ''}")
32
+ gain = sum(now.get(e, 0) - base[e] for e in flaky) / len(flaky)
33
+ drops = [e for e in grade.GUARDS if now.get(e, 0) < base[e] - 0.2]
34
+ verdict = "REJECT" if drops else "PROMISING" if gain >= 0.1 else "FLAT"
35
+ print(f"flaky mean delta {gain:+.2f}; guard drops {drops or 'none'} -> {verdict}")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main(sys.argv[1], sys.argv[2:])
@@ -0,0 +1 @@
1
+ {"phone-number": 0.69, "tree-building": 0.92, "grep": 0.66, "pig-latin": 0.91, "proverb": 1.0, "robot-name": 1.0, "list-ops": 1.0}
@@ -180,6 +180,10 @@ def _codereview(_task, _ws, result):
180
180
  POLY_FAST = ("affine-cipher bottle-song list-ops paasio dot-dsl food-chain forth go-counting poker pov react "
181
181
  "rest-api").split()
182
182
 
183
+ # ~15 min: 4 flaky exercises that can flip + 3 fast guards that must not regress (baseline in canary_baseline.json)
184
+ POLY_CANARY = "phone-number tree-building grep pig-latin proverb robot-name list-ops".split()
185
+ GUARDS = ("proverb", "robot-name", "list-ops")
186
+
183
187
 
184
188
  def poly_dir(ex=""):
185
189
  """Aider polyglot-benchmark Python exercises (cloned once, never vendored)."""
@@ -0,0 +1,3 @@
1
+ {"date": "2026-10-27", "version": "main@b25cbbe (post v0.2.9, #176)", "model": "ollama/qwen3.5:9b", "runner": "tether", "tasks": "polyglot-34", "trials": 2, "context": 8192, "note": "baseline; wall cap 600s; 14 exercises never pass", "runs": 68, "passed": 35, "pass_rate": 0.515, "mean_score": 0.718, "fail_max_steps": 17, "fail_wall_cap": 11, "mean_steps": 18.2, "mean_prompt_tokens": 82120, "mean_wall_s": 274}
2
+ {"date": "2026-10-27", "version": "main@b25cbbe", "model": "ollama/qwen3.5:9b", "runner": "tether", "tasks": "polyglot-fast-12", "trials": 2, "context": 8192, "note": "same runs as baseline, fast subset", "runs": 24, "passed": 12, "pass_rate": 0.5, "mean_score": 0.711, "fail_max_steps": 8, "fail_wall_cap": 3, "mean_steps": 20.7, "mean_prompt_tokens": 94828, "mean_wall_s": 265}
3
+ {"date": "2026-10-27", "version": "naive_agent", "model": "ollama/qwen3.5:9b", "runner": "naive", "tasks": "polyglot-fast-12", "trials": 2, "context": 8192, "note": "no-harness ceiling reference", "runs": 24, "passed": 3, "pass_rate": 0.125, "mean_score": 0.243, "fail_max_steps": 0, "fail_wall_cap": 0, "mean_steps": 4.7, "mean_prompt_tokens": 8054, "mean_wall_s": 55}
@@ -50,7 +50,7 @@ def flags(tether_bin):
50
50
  return _HELP[tether_bin]
51
51
 
52
52
 
53
- def build_cmd(runner, model, task, goal, ws, rd, tether_bin, params=()):
53
+ def build_cmd(runner, model, task, goal, ws, rd, tether_bin, params=(), context_tokens=None):
54
54
  tools, steps, _ = cfg(task)
55
55
  if runner == "tether":
56
56
  h = flags(tether_bin)
@@ -61,6 +61,8 @@ def build_cmd(runner, model, task, goal, ws, rd, tether_bin, params=()):
61
61
  cmd += ["--tools", tools]
62
62
  for item in params:
63
63
  cmd += ["--param", item]
64
+ if context_tokens:
65
+ cmd += ["--context-tokens", str(context_tokens)]
64
66
  if task.startswith("poly-") and POLY_VERIFY:
65
67
  cmd += ["--verify", "python3 -m unittest discover -p '*_test.py'"]
66
68
  return cmd + [goal]
@@ -119,7 +121,9 @@ async def one(sem, rec, args, results):
119
121
  else:
120
122
  shutil.copytree(f"{HERE}/tasks/{task}/ws", ws)
121
123
  goal = open(f"{HERE}/tasks/{task}/goal.txt").read().strip()
122
- cmd = build_cmd(runner, model, task, goal, ws, rd, args.tether_bin, args.param)
124
+ cmd = build_cmd(
125
+ runner, model, task, goal, ws, rd, args.tether_bin, args.param, args.context_tokens
126
+ )
123
127
  env = dict(os.environ, OLLAMA_HOST="http://localhost:11435", PYTHONDONTWRITEBYTECODE="1")
124
128
  if model.startswith("openai/"): # old tether has no providers.yaml: route openai/ to NVIDIA's OpenAI-compatible API
125
129
  env.update(OPENAI_BASE_URL="https://integrate.api.nvidia.com/v1", OPENAI_API_KEY=os.environ.get("NVIDIA_API_KEY", ""))
@@ -177,6 +181,7 @@ async def main():
177
181
  ap.add_argument("--tether-bin", default="tether")
178
182
  ap.add_argument("--param", action="append", default=[], help="forwarded to `tether run --param`")
179
183
  ap.add_argument("--par", default="nvidia=4,ollama=1,claude=3")
184
+ ap.add_argument("--context-tokens", type=int, default=None, help="forwarded to `tether run`")
180
185
  ap.add_argument("--force", action="store_true")
181
186
  ap.add_argument("--redo-infra", action="store_true", help="rerun cells that failed on provider errors")
182
187
  ap.add_argument("--poly-verify", action="store_true", help="verify gate on the exercise tests: gate runs at `done`")
@@ -185,6 +190,8 @@ async def main():
185
190
  POLY_VERIFY = args.poly_verify
186
191
  if args.tasks == "polyglot":
187
192
  args.tasks = poly_tasks()
193
+ elif args.tasks == "canary":
194
+ args.tasks = ",".join("poly-" + e for e in grade.POLY_CANARY)
188
195
  elif args.tasks == "polyglot-fast":
189
196
  args.tasks = ",".join("poly-" + e for e in grade.POLY_FAST)
190
197
  os.makedirs(args.out, exist_ok=True)
@@ -92,12 +92,12 @@ Every entry, of any kind, may set optional positive integers: `context_tokens` (
92
92
 
93
93
  ### Timeout and retry
94
94
 
95
- 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: `MY_GATEWAY_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).
95
+ Every entry may set `timeout` (positive seconds). It applies to the HTTP kinds (`openai`, `ollama`, `anthropic`; defaults 120 / 300 / 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: `MY_GATEWAY_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)`. When they run out the harness waits 10 s (set `TETHER_OUTAGE_BACKOFF`) and tries the whole call again, up to 3 more times with doubling waits, before the session fails. The retry is invisible to the prompt (0 tokens, no prefix change).
96
96
 
97
97
 
98
98
  ### Request parameters (`params`)
99
99
 
100
- HTTP kinds (`openai`, `ollama`, `anthropic`) accept `params`, merged into the request body over the adapter's defaults (temperature 0.2). For `ollama`, `think`, `keep_alive`, `format` and `stream` are request-level keys and everything else (`temperature`, `top_p`, ...) goes into `options`.
100
+ HTTP kinds (`openai`, `ollama`, `anthropic`) accept `params`, merged into the request body over the adapter's defaults (temperature 0.2). `ollama` also sends `think=false` on a turn whose prompt leaves under 3072 tokens of `num_ctx` free, unless you set `think` yourself. For `ollama`, `think`, `keep_alive`, `format` and `stream` are request-level keys and everything else (`temperature`, `top_p`, ...) goes into `options`.
101
101
 
102
102
  ```yaml
103
103
  my-gateway:
@@ -1,5 +1,22 @@
1
1
  # Release Notes
2
2
 
3
+ ## Unreleased
4
+
5
+ ## v0.2.10 (2026-10-28)
6
+
7
+ The run-killers found in the v0.2.9 Polyglot breakdown on `qwen3.5:9b`: provider outages, a window filled by thinking, noisy test output and big test files re-read slice by slice. The full 34x2 benchmark for this release is published separately once it finishes.
8
+
9
+ ### Fixes
10
+
11
+ - When a long observation is cut (head + tail), the tests defined in the cut middle are listed as `name:line` (up to 30): a test file too big for the window is no longer re-read slice after slice, which cost 54% of the steps in max-step runs (#170).
12
+ - A session that ends without `done` (max steps or stall) now says what it did: the last change commands and, with `--verify`, whether the gates pass or which one fails. Status and exit code are unchanged (#171).
13
+ - Ollama: when the prompt leaves less than 3072 tokens of `num_ctx` free, the request sends `think=false`. A thinking reply no longer fills the window and comes back empty (45 of 53 empty baseline turns sat at the window limit). An explicit `--param think=...` still wins (#183).
14
+ - A provider outage that outlasts the provider's own retries no longer ends the session: the harness tries the call again up to 3 more times (waits of 10, 20, 40 s; `TETHER_OUTAGE_BACKOFF` changes the first wait) and only then fails with the same `ProviderError`. 4 of 68 baseline Polyglot runs died of one timeout (#171).
15
+ - `bash` observations keep only the last two frames of a Python traceback (the call site and the cause) with `... N frames ...` for the rest, and drop the `^^^^` marker rows: test failures cost fewer tokens.
16
+ - Ollama default timeout is now 300 s (was 120 s): a model that thinks until the context window is full needs 85-110 s per turn, and 4 of 68 baseline Polyglot runs died on the old limit.
17
+ - `bash` observations no longer carry the absolute workspace path (`<root>/`) or `====`/`----` rule lines: less noise in test output, fewer tokens per step.
18
+ - `done`: a bare `done` with no answer now completes the session with the result `Done.` (the `--verify` gates still run first) instead of returning an error the model repeated until `max_steps`. `done --ask` with no question is still an error. A model that types the call as text, `act("done", stdin="...")` or `bash act("done", ...)`, in `cmd` is understood the same way as a real call (#169). Found in the v0.2.9 Polyglot breakdown: 8 of 36 `qwen3.5:9b` runs wasted 1-13 steps on a rejected `done`, and 2 runs that passed every test ended `FAILED` at max steps.
19
+
3
20
  ## v0.2.9 (2026-10-25)
4
21
 
5
22
  The docs are rebuilt around the people who use tether, and writing the first guide with a real run found a real bug: a small model that fixed a failing test, saw it pass, then repeated the check until the stall guard ended the session as a failure. Closes #163, #61, #60, #62, #63, #64, #151, #152, #153, #154.
@@ -18,7 +18,9 @@ With `--verify` gates configured, a stall after a change whose gates all pass en
18
18
 
19
19
  **Cause.** The task needs more steps than allowed, or the model never found the way to finish.
20
20
 
21
- **Fix.** Check `tether read <id> --steps`: if the work is done and the model keeps verifying, tell it to call `done` in the goal. If it needs more room, raise `--max-steps` (for a pipeline, per stage). `tether send <id> "keep going"` resumes a finished session with a fresh budget.
21
+ The result is never bare: it lists the last change commands and, with `--verify`, whether the gates pass now (`Stalled` ends the same way). A run that stopped with passing gates already holds a usable result in the workspace.
22
+
23
+ **Fix.** Check `tether read <id> --steps`: if the work is done and the model keeps verifying, tell it to call `done` in the goal. A bare `done` is enough: it ends the session with the result `Done.` once the gates pass. If it needs more room, raise `--max-steps` (for a pipeline, per stage). `tether send <id> "keep going"` resumes a finished session with a fresh budget.
22
24
 
23
25
  ### `FAILED` with `Verification gate ... misconfigured`
24
26
 
@@ -108,6 +108,7 @@ class Tether:
108
108
  "HOME": str(self.home),
109
109
  "TETHER_PROVIDERS": str(providers),
110
110
  "E2E_KEY": "sekret-value-123",
111
+ "TETHER_OUTAGE_BACKOFF": "0",
111
112
  }
112
113
 
113
114
  def cli(
@@ -20,12 +20,20 @@ def test_429_is_retried_honoring_retry_after(tmp_path: Path, chaos, model: str)
20
20
 
21
21
 
22
22
  def test_persistent_5xx_fails_loudly_with_exit_5(tether: Tether, chaos) -> None:
23
- chaos.fault("status", times=10, code=503)
23
+ chaos.fault("status", times=20, code=503)
24
24
  r = tether.run("anything")
25
25
  assert (r["rc"], r["status"]) == (5, "FAILED")
26
26
  assert "unavailable after 3 attempts" in r["failure"]["message"]
27
27
 
28
28
 
29
+ def test_outage_that_outlasts_provider_retries_is_survived(tether: Tether, chaos) -> None:
30
+ """#171: 3 failed attempts end one provider call, not the session; the next try succeeds."""
31
+ chaos.fault("status", times=3, code=503)
32
+ chaos.script(("done", "ok"))
33
+ r = tether.run("go", "--max-steps", "6")
34
+ assert (r["rc"], r["status"]) == (0, "COMPLETED"), r
35
+
36
+
29
37
  def test_rejected_tool_call_reaches_the_model_instead_of_ending_the_session(
30
38
  tether: Tether, chaos
31
39
  ) -> None:
@@ -40,13 +48,13 @@ def test_rejected_tool_call_reaches_the_model_instead_of_ending_the_session(
40
48
 
41
49
  def test_trickling_response_hits_the_per_attempt_deadline(tmp_path: Path, chaos) -> None:
42
50
  """#88: bytes keep arriving so httpx's read timeout never fires; the deadline must."""
43
- chaos.fault("trickle", times=3, seconds=60)
51
+ chaos.fault("trickle", times=12, seconds=60) # 3 attempts x 4 turn-level tries
44
52
  t = Tether(tmp_path, chaos, timeout=3)
45
53
  start = time.time()
46
54
  r = t.run("anything")
47
55
  assert (r["rc"], r["status"]) == (5, "FAILED")
48
56
  assert "TimeoutError" in r["failure"]["message"], r["failure"]
49
- assert time.time() - start < 40, "must not hang past attempts x timeout"
57
+ assert time.time() - start < 80, "must not hang past attempts x timeout"
50
58
 
51
59
 
52
60
  def test_auth_error_names_the_variable_never_the_value(tether: Tether, chaos) -> None:
@@ -86,6 +86,13 @@ def test_stall_with_a_failing_gate_stays_failed(tether: Tether, chaos) -> None:
86
86
  assert r["status"] == "FAILED" and r["failure"]["message"].startswith("Stalled")
87
87
 
88
88
 
89
+ def test_unfinished_session_reports_changes_and_gate(tether: Tether, chaos) -> None:
90
+ chaos.script(*(["bash touch fixed.txt"] * 6), ("done", "ok")) # #171: never a bare error
91
+ r = tether.run("loop", "--tools", "bash", "--verify", "test -f missing.txt")
92
+ msg = r["failure"]["message"]
93
+ assert "Changed: `bash touch fixed.txt`" in msg and "Gate `test -f missing.txt` fails" in msg
94
+
95
+
89
96
  def test_two_command_cycle_is_a_cycle_stall(tether: Tether, chaos) -> None:
90
97
  chaos.script(*(["bash echo a", "bash echo b"] * 3), ("done", "ok"))
91
98
  r = tether.run("cycle", "--tools", "bash")
@@ -162,3 +169,16 @@ def test_skill_enables_its_tools_without_touching_the_schema(tether: Tether, cha
162
169
  sysmsg = req["messages"][0]["content"]
163
170
  assert [t["function"]["name"] for t in req["tools"]] == ["act"]
164
171
  assert "- bash:" in sysmsg and "[SKILL]" in sysmsg and "Always run `bash true` first." in sysmsg
172
+
173
+
174
+ def test_bare_done_completes_instead_of_looping(tether: Tether, chaos) -> None:
175
+ chaos.script("done") # #169: was "Error: provide final deliverable", repeated to max_steps
176
+ r = tether.run("finish", "--tools", "bash")
177
+ assert r["status"] == "COMPLETED"
178
+ assert tether.events(r["id"]) == ["completed"]
179
+
180
+
181
+ def test_act_call_typed_as_text_is_unwrapped(tether: Tether, chaos) -> None:
182
+ chaos.script('act("done", stdin="all green")')
183
+ r = tether.run("finish", "--tools", "bash")
184
+ assert r["status"] == "COMPLETED" and r["result"] == "all green"
@@ -109,3 +109,36 @@ async def test_ollama_unknown_model_fails_loudly_by_default() -> None:
109
109
  op = OllamaProvider("doesnotexist", client=client, fallback=False)
110
110
  with pytest.raises(RuntimeError, match="doesnotexist.*installed: qwen3.5:9b"):
111
111
  await op.generate([{"role": "user", "content": "hi"}], [])
112
+
113
+
114
+ def test_default_timeout_fits_a_full_window_of_thinking() -> None:
115
+ """North star: 4/68 baseline runs died on a 120s ReadTimeout while the model was thinking."""
116
+ from tether.providers.ollama import OllamaProvider
117
+
118
+ assert OllamaProvider().timeout >= 300
119
+
120
+
121
+ async def _sent_payload(provider_kwargs: dict, text: str) -> dict:
122
+ seen: dict = {}
123
+
124
+ def handler(request: httpx.Request) -> httpx.Response:
125
+ seen.update(__import__("json").loads(request.content))
126
+ return httpx.Response(200, json={"message": {"content": "hi"}})
127
+
128
+ client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
129
+ provider = OllamaProvider("m", client=client, **provider_kwargs)
130
+ await provider.generate([{"role": "user", "content": text}], [])
131
+ return seen
132
+
133
+
134
+ async def test_thinking_is_off_when_little_window_is_left_for_the_reply() -> None:
135
+ """#183: a prompt that leaves < 3072 tokens of num_ctx cannot hold a thinking reply."""
136
+ small = await _sent_payload({"num_ctx": 8192}, "x" * 400)
137
+ assert "think" not in small
138
+ full = await _sent_payload({"num_ctx": 8192}, "x" * 4 * 5200)
139
+ assert full["think"] is False
140
+ # an explicit `think` param always wins
141
+ forced = await _sent_payload({"num_ctx": 8192, "params": {"think": True}}, "x" * 4 * 5200)
142
+ assert forced["think"] is True
143
+ # headroom scales with the window: the same prompt is fine in a 32k window
144
+ assert "think" not in await _sent_payload({"num_ctx": 32768}, "x" * 4 * 5200)
@@ -349,3 +349,19 @@ def test_budget_reserves_a_quarter_of_the_window_for_the_reply() -> None:
349
349
  # #144: a prompt that fills the window cuts off the model's file writes
350
350
  ca, msgs = _budget_run(8, 2400, 2000, fold_after_turns=8)
351
351
  assert estimate_messages_tokens(msgs, [ACT_TOOL_SCHEMA]) <= 2000 * 3 // 4
352
+
353
+
354
+ def test_elided_test_file_keeps_an_index_of_its_tests() -> None:
355
+ """#170: the model stops re-reading a test file when the cut part still names its tests."""
356
+ body = "".join(
357
+ f" def test_case_{i}(self):\n assert f({i}) == {i}\n" for i in range(60)
358
+ )
359
+ out = elide_active_observation("obs_1", body, max_chars=600)
360
+ assert "elided in L1" in out
361
+ assert "tests in the elided part: " in out and "test_case_30:61" in out
362
+ index = out.split("tests in the elided part: ")[1].splitlines()[0]
363
+ assert len(index) < 700 # bounded
364
+ # nothing to index in an ordinary elided output
365
+ assert "tests in the elided" not in elide_active_observation(
366
+ "obs_2", "x\n" * 900, max_chars=600
367
+ )
@@ -12,6 +12,7 @@ from tests.conftest import FakeScriptedProvider
12
12
  from tether import ActionCall, Harness, ModelResponse, register_provider, tool
13
13
  from tether.decorator import ToolSpec
14
14
  from tether.harness import detect_cycle, is_quiet, read_target, spawn_detached_worker
15
+ from tether.providers.base import ProviderError
15
16
  from tether.store import SQLiteStore
16
17
 
17
18
 
@@ -237,6 +238,37 @@ async def test_pre_done_verification_gates_and_self_healing(tmp_path: Path) -> N
237
238
  assert "Fix the reported issues before calling `done` again." in step2_prompt
238
239
 
239
240
 
241
+ async def test_bare_done_still_runs_verification_gates(tmp_path: Path) -> None:
242
+ """A bare `done` completes the session only once the gates pass (#169)."""
243
+ from tether.sandbox import SandboxPolicy
244
+
245
+ ws = tmp_path / "ws_bare"
246
+ ws.mkdir()
247
+ (ws / "module.py").write_text("VALUE = 1\n", encoding="utf-8")
248
+ provider = FakeScriptedProvider(
249
+ [
250
+ ModelResponse(content="", action=ActionCall(cmd="done")),
251
+ ModelResponse(
252
+ content="", action=ActionCall(cmd="edit write module.py", stdin="VALUE = 42\n")
253
+ ),
254
+ ModelResponse(content="", action=ActionCall(cmd="done")),
255
+ ]
256
+ )
257
+ harness = Harness(
258
+ db_path=tmp_path / "tether.db",
259
+ tools=["edit"],
260
+ verify=["python3 -c 'import module, sys; sys.exit(0 if module.VALUE == 42 else 1)'"],
261
+ sandbox=SandboxPolicy(workspace_root=ws, use_kernel_sandbox=False),
262
+ provider=provider,
263
+ )
264
+ session = await harness.run("Set VALUE = 42 in module.py")
265
+ assert session.status == "COMPLETED" and session.step_count == 3
266
+ assert session.result == "Done."
267
+ assert "[VERIFICATION GATE FAILED:" in "\n".join(
268
+ m["content"] for m in provider.recorded_calls[1]
269
+ )
270
+
271
+
240
272
  async def test_verification_gate_circuit_breaker_and_skill_verify(tmp_path: Path) -> None:
241
273
  """Verify circuit breaker trips on consecutive gate failures and skills declare verify gates."""
242
274
  from tether.sandbox import SandboxPolicy
@@ -757,3 +789,67 @@ async def test_stall_after_a_change_with_passing_gate_is_completed(tmp_path: Pat
757
789
  async def test_stall_with_failing_gate_stays_failed(tmp_path: Path) -> None:
758
790
  session = await _stalled_after_a_change(tmp_path, "false")
759
791
  assert session.status == "FAILED" and (session.result or "").startswith("Stalled")
792
+
793
+
794
+ class _FlakyProvider:
795
+ """Raises ProviderError `fails` times, then calls done."""
796
+
797
+ model_name = "qwen3.5:9b"
798
+
799
+ def __init__(self, fails: int) -> None:
800
+ self.fails = fails
801
+ self.calls = 0
802
+
803
+ async def generate(self, messages: Any, tools: Any) -> ModelResponse:
804
+ self.calls += 1
805
+ if self.calls <= self.fails:
806
+ raise ProviderError("ollama model `qwen3.5:9b` unavailable after 3 attempts")
807
+ return ModelResponse(content="", action=ActionCall(cmd="done ok"))
808
+
809
+
810
+ async def test_outage_is_retried_at_turn_level_then_fails_loudly(
811
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
812
+ ) -> None:
813
+ """North star floor (#171): 4/68 baseline runs died on one provider outage; retry 3 times."""
814
+ sleeps: list[float] = []
815
+
816
+ async def fake_sleep(delay: float) -> None:
817
+ sleeps.append(delay)
818
+
819
+ monkeypatch.setattr("tether.harness.asyncio.sleep", fake_sleep)
820
+
821
+ ok = _FlakyProvider(fails=2)
822
+ done = await Harness(db_path=tmp_path / "a.db", provider=ok, max_steps=3).run("go")
823
+ assert done.status == "COMPLETED" and ok.calls == 3 and len(sleeps) == 2
824
+
825
+ dead = _FlakyProvider(fails=99)
826
+ failed = await Harness(db_path=tmp_path / "b.db", provider=dead, max_steps=3).run("go")
827
+ assert failed.status == "FAILED" and "unavailable after 3 attempts" in (failed.result or "")
828
+ assert dead.calls == 4 # first try + 3 turn-level retries
829
+
830
+
831
+ async def _unfinished(tmp_path: Path, cmds: list[str], verify: list[str]) -> str:
832
+ from tether.sandbox import SandboxPolicy
833
+
834
+ script = [ModelResponse(content="", action=ActionCall(cmd=c)) for c in cmds]
835
+ h = Harness(
836
+ db_path=tmp_path / "t.db",
837
+ tools=["edit"],
838
+ provider=FakeScriptedProvider(script),
839
+ max_steps=len(cmds),
840
+ verify=verify,
841
+ sandbox=SandboxPolicy(workspace_root=tmp_path, use_kernel_sandbox=False),
842
+ )
843
+ return (await h.run("goal")).result or ""
844
+
845
+
846
+ async def test_max_steps_result_reports_changes_and_gate(tmp_path: Path) -> None:
847
+ """#171 floor: a session that never calls done still ends with something usable."""
848
+ write = "edit write a.py\nprint(1)"
849
+ res = await _unfinished(tmp_path, [write], ["false"])
850
+ assert res.startswith("Max steps (1) reached without calling `done`.")
851
+ assert "Changed: `edit write a.py" in res and "Gate `false` fails (exit 1)" in res
852
+ (tmp_path / "ok").mkdir()
853
+ assert "Gate passes" in await _unfinished(tmp_path / "ok", [write], ["true"])
854
+ res = await _unfinished(tmp_path, ["ls"], [])
855
+ assert res.endswith("No change was made.")