tsugite-cli 0.17.2__tar.gz → 0.17.3__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 (170) hide show
  1. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/PKG-INFO +1 -1
  2. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/pyproject.toml +1 -1
  3. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/runner.py +1 -1
  4. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/agent.py +21 -9
  5. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/jobs.py +7 -0
  6. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/schedule.py +19 -6
  7. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/.gitignore +0 -0
  8. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/AGENTS.md +0 -0
  9. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/CONTRIBUTING.md +0 -0
  10. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/LICENSE +0 -0
  11. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/README.md +0 -0
  12. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/__init__.py +0 -0
  13. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_inheritance.py +0 -0
  14. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_preparation.py +0 -0
  15. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/__init__.py +0 -0
  16. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/exec_directives.py +0 -0
  17. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/exec_runner.py +0 -0
  18. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/helpers.py +0 -0
  19. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/history_integration.py +0 -0
  20. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/metrics.py +0 -0
  21. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/models.py +0 -0
  22. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_runner/validation.py +0 -0
  23. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/agent_utils.py +0 -0
  24. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/attachments/__init__.py +0 -0
  25. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/attachments/auto_context.py +0 -0
  26. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/attachments/base.py +0 -0
  27. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/attachments/file.py +0 -0
  28. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/attachments/inline.py +0 -0
  29. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/attachments/storage.py +0 -0
  30. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/attachments/url.py +0 -0
  31. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_agents/.gitkeep +0 -0
  32. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_agents/code_searcher.md +0 -0
  33. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_agents/default.md +0 -0
  34. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_agents/file_searcher.md +0 -0
  35. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_agents/job_verifier.md +0 -0
  36. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_agents/job_worker.md +0 -0
  37. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_agents/onboard.md +0 -0
  38. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/.gitkeep +0 -0
  39. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  40. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  41. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  42. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  43. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
  44. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  45. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  46. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  47. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  48. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cache.py +0 -0
  49. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cancellation.py +0 -0
  50. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/__init__.py +0 -0
  51. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/agents.py +0 -0
  52. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/attachments.py +0 -0
  53. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/cache.py +0 -0
  54. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/chat.py +0 -0
  55. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/config.py +0 -0
  56. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/daemon.py +0 -0
  57. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/exec.py +0 -0
  58. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/helpers.py +0 -0
  59. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/history.py +0 -0
  60. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/init.py +0 -0
  61. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/models.py +0 -0
  62. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/plugins.py +0 -0
  63. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/render.py +0 -0
  64. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/run.py +0 -0
  65. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/secrets.py +0 -0
  66. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/skills.py +0 -0
  67. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/tools.py +0 -0
  68. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/usage.py +0 -0
  69. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/validate.py +0 -0
  70. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/cli/workspace.py +0 -0
  71. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/config.py +0 -0
  72. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/console.py +0 -0
  73. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/constants.py +0 -0
  74. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/__init__.py +0 -0
  75. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/content_blocks.py +0 -0
  76. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/executor.py +0 -0
  77. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/executor_registry.py +0 -0
  78. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/memory.py +0 -0
  79. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/proxy.py +0 -0
  80. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/record_store.py +0 -0
  81. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/sandbox.py +0 -0
  82. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/state.py +0 -0
  83. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/subprocess_executor.py +0 -0
  84. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/core/tools.py +0 -0
  85. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/events/__init__.py +0 -0
  86. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/events/base.py +0 -0
  87. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/events/bus.py +0 -0
  88. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/events/events.py +0 -0
  89. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/events/helpers.py +0 -0
  90. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/exceptions.py +0 -0
  91. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/__init__.py +0 -0
  92. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/base.py +0 -0
  93. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/models.py +0 -0
  94. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/reconstruction.py +0 -0
  95. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/registry.py +0 -0
  96. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/sqlite_backend.py +0 -0
  97. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/sqlite_conn.py +0 -0
  98. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/sqlite_schema.py +0 -0
  99. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/storage.py +0 -0
  100. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/history/ui_events.py +0 -0
  101. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/hooks.py +0 -0
  102. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/interaction.py +0 -0
  103. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/md_agents.py +0 -0
  104. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/models.py +0 -0
  105. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/options.py +0 -0
  106. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/plugins.py +0 -0
  107. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/__init__.py +0 -0
  108. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/anthropic.py +0 -0
  109. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/base.py +0 -0
  110. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/model_cache.py +0 -0
  111. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/model_registry.py +0 -0
  112. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/ollama.py +0 -0
  113. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/openai_compat.py +0 -0
  114. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/providers/openrouter.py +0 -0
  115. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/py.typed +0 -0
  116. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/renderer.py +0 -0
  117. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/schemas/__init__.py +0 -0
  118. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/schemas/agent.schema.json +0 -0
  119. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/__init__.py +0 -0
  120. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/backend.py +0 -0
  121. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/env.py +0 -0
  122. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/exec.py +0 -0
  123. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/file.py +0 -0
  124. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/masking.py +0 -0
  125. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/registry.py +0 -0
  126. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/secrets/sqlite.py +0 -0
  127. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/shell_tool_config.py +0 -0
  128. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/skill_discovery.py +0 -0
  129. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/templates/AGENTS.md +0 -0
  130. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/templates/IDENTITY.md +0 -0
  131. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/templates/MEMORY.md +0 -0
  132. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/templates/USER.md +0 -0
  133. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/templates/personas/casual-technical.md +0 -0
  134. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/templates/personas/marvin.md +0 -0
  135. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/templates/personas/minimal.md +0 -0
  136. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/__init__.py +0 -0
  137. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/agents.py +0 -0
  138. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/fs.py +0 -0
  139. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/history.py +0 -0
  140. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/http.py +0 -0
  141. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/interactive.py +0 -0
  142. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/notify.py +0 -0
  143. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/scratchpad.py +0 -0
  144. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/secrets.py +0 -0
  145. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/sessions.py +0 -0
  146. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/shell.py +0 -0
  147. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/shell_tools.py +0 -0
  148. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/skills.py +0 -0
  149. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tools/time.py +0 -0
  150. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/tsugite.py +0 -0
  151. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/__init__.py +0 -0
  152. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/base.py +0 -0
  153. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/chat.py +0 -0
  154. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/helpers.py +0 -0
  155. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/jsonl.py +0 -0
  156. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/plain.py +0 -0
  157. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/repl_chat.py +0 -0
  158. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/repl_commands.py +0 -0
  159. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/repl_completer.py +0 -0
  160. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui/repl_handler.py +0 -0
  161. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/ui_context.py +0 -0
  162. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/usage/__init__.py +0 -0
  163. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/usage/store.py +0 -0
  164. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/user_agent.py +0 -0
  165. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/utils.py +0 -0
  166. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/workspace/__init__.py +0 -0
  167. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/workspace/context.py +0 -0
  168. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/workspace/models.py +0 -0
  169. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/workspace/session.py +0 -0
  170. {tsugite_cli-0.17.2 → tsugite_cli-0.17.3}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.17.2
3
+ Version: 0.17.3
4
4
  Summary: Micro-agent runner for task automation using markdown definitions
5
5
  Author: Justyn Shull
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.17.2"
3
+ version = "0.17.3"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -681,7 +681,7 @@ async def _execute_agent_with_prompt(
681
681
  model=model_string,
682
682
  source="cli",
683
683
  total_tokens=agent.total_tokens,
684
- cost_usd=agent.total_cost if agent.total_cost > 0 else None,
684
+ cost_usd=agent.reported_cost,
685
685
  cache_creation_tokens=agent.cache_creation_tokens,
686
686
  cache_read_tokens=agent.cache_read_tokens,
687
687
  )
@@ -339,6 +339,9 @@ class TsugiteAgent:
339
339
  self._resume_after_compaction = resume_after_compaction
340
340
 
341
341
  self.total_cost = 0.0
342
+ # Distinguishes "provider reported $0" (subscription models) from "no
343
+ # cost data at all" (interrupted turn) - only the latter records NULL.
344
+ self.cost_reported = False
342
345
  self.total_tokens = 0
343
346
  self.last_input_tokens = 0
344
347
  self.cache_creation_tokens = 0
@@ -571,7 +574,7 @@ class TsugiteAgent:
571
574
  "answer": str(final_value)[:500] if final_value is not None else "",
572
575
  "turns": turn_num + 1,
573
576
  "tokens": total_tokens,
574
- "cost": self.total_cost if self.total_cost > 0 else None,
577
+ "cost": self.reported_cost,
575
578
  }
576
579
 
577
580
  from tsugite.hooks import fire_hooks_background
@@ -585,13 +588,13 @@ class TsugiteAgent:
585
588
  answer_data=_safe_json(final_value),
586
589
  turns=turn_num + 1,
587
590
  tokens=total_tokens,
588
- cost=self.total_cost if self.total_cost > 0 else None,
591
+ cost=self.reported_cost,
589
592
  )
590
593
  )
591
594
  self.event_bus.emit(
592
595
  CostSummaryEvent(
593
596
  tokens=total_tokens,
594
- cost=self.total_cost if self.total_cost > 0 else None,
597
+ cost=self.reported_cost,
595
598
  model=self.model_name,
596
599
  duration_seconds=time.time() - start_time,
597
600
  cache_creation_input_tokens=self.cache_creation_tokens or None,
@@ -610,7 +613,7 @@ class TsugiteAgent:
610
613
  return AgentResult(
611
614
  output=final_value,
612
615
  token_usage=total_tokens,
613
- cost=self.total_cost if self.total_cost > 0 else None,
616
+ cost=self.reported_cost,
614
617
  steps=self.memory.steps,
615
618
  error=error_message,
616
619
  provider_state=self._provider.get_state(),
@@ -731,7 +734,7 @@ class TsugiteAgent:
731
734
  self.event_bus.emit(StreamCompleteEvent())
732
735
 
733
736
  if final_chunk and final_chunk.usage:
734
- step_cost = self._accumulate_usage(final_chunk.usage, final_chunk.cost or 0.0)
737
+ step_cost = self._accumulate_usage(final_chunk.usage, final_chunk.cost)
735
738
 
736
739
  accumulated_content, spoofed = escape_runtime_injection_tags(accumulated_content)
737
740
  parsed = self._parse_response_from_text(accumulated_content)
@@ -765,8 +768,10 @@ class TsugiteAgent:
765
768
 
766
769
  step_cost = response.cost or 0.0
767
770
  if response.usage:
768
- self._accumulate_usage(response.usage, step_cost)
771
+ self._accumulate_usage(response.usage, response.cost)
769
772
  else:
773
+ if response.cost is not None:
774
+ self.cost_reported = True
770
775
  self.total_cost += step_cost
771
776
 
772
777
  if response.reasoning_content:
@@ -1135,7 +1140,12 @@ class TsugiteAgent:
1135
1140
  parts.append('warning="approaching turn limit, wrap up soon"')
1136
1141
  return f"\n<tsugite_budget {' '.join(parts)} />"
1137
1142
 
1138
- def _accumulate_usage(self, usage, cost: float = 0.0) -> float:
1143
+ @property
1144
+ def reported_cost(self) -> float | None:
1145
+ """Cumulative cost when any provider response carried one, else None."""
1146
+ return self.total_cost if (self.cost_reported or self.total_cost > 0) else None
1147
+
1148
+ def _accumulate_usage(self, usage, cost: float | None = None) -> float:
1139
1149
  """Update cumulative token/cost counters from a usage object.
1140
1150
 
1141
1151
  Returns the step cost for caller convenience.
@@ -1146,8 +1156,10 @@ class TsugiteAgent:
1146
1156
  )
1147
1157
  self.cache_creation_tokens += usage.cache_creation_input_tokens or 0
1148
1158
  self.cache_read_tokens += usage.cache_read_input_tokens or 0
1149
- self.total_cost += cost
1150
- return cost
1159
+ if cost is not None:
1160
+ self.cost_reported = True
1161
+ self.total_cost += cost or 0.0
1162
+ return cost or 0.0
1151
1163
 
1152
1164
  def _parse_response_from_text(self, content: str) -> ParsedResponse:
1153
1165
  """Parse text content into thought, code, and content blocks."""
@@ -37,6 +37,7 @@ def spawn_job(
37
37
  repo: Optional[str] = None,
38
38
  model: Optional[str] = None,
39
39
  verifier_model: Optional[str] = None,
40
+ model_ladder: Optional[list] = None,
40
41
  timeout_minutes: int = 30,
41
42
  agent: Optional[str] = None,
42
43
  max_attempts: Optional[int] = None,
@@ -67,6 +68,11 @@ def spawn_job(
67
68
  verifier_model: Optional separate model for the verifier round. Defaults
68
69
  to `model` (then the workspace default) when unset - handy to pin
69
70
  verification to a cheaper/more-available model than the worker.
71
+ model_ladder: Ordered "cheap first" model list, e.g.
72
+ ["claude_code:haiku", "claude_code:opus"]. The Job starts on the
73
+ first entry; when a rung exhausts its verifier attempts or the
74
+ worker dies on a usage/rate limit, the next attempt escalates to
75
+ the next model with a fresh budget. Overrides `model`.
70
76
  timeout_minutes: Per-phase budget for each worker run and verifier round
71
77
  (re-armed each phase, not a whole-job wall clock); on expiry the Job
72
78
  transitions to `stuck`.
@@ -104,6 +110,7 @@ def spawn_job(
104
110
  repo=repo,
105
111
  model=model,
106
112
  verifier_model=verifier_model,
113
+ model_ladder=model_ladder,
107
114
  agent=agent,
108
115
  timeout_minutes=timeout_minutes,
109
116
  spawned_by="agent-tool",
@@ -36,14 +36,27 @@ def _call(fn, *args, **kwargs):
36
36
  return call_on_loop(_loop, fn, *args, timeout=10, **kwargs)
37
37
 
38
38
 
39
- def _validate_notify(notify: Optional[list[str]], notify_tool: bool) -> None:
40
- """Validate notification channels and notify_tool requirement."""
39
+ def _validate_notify(notify, notify_tool: bool) -> Optional[list[str]]:
40
+ """Normalize and validate the notify argument; returns the coerced list.
41
+
42
+ Agents pass booleans and bare strings here in practice - those must coerce
43
+ or produce a message that names the argument and the expected type, never
44
+ leak "'bool' object is not iterable"."""
45
+ if notify is True or (notify is not None and notify is not False and not isinstance(notify, (list, str))):
46
+ channels = f"Available channels: {', '.join(sorted(_channel_names))}" if _channel_names else ""
47
+ got = "True" if notify is True else type(notify).__name__
48
+ raise ValueError(f"notify must be a list of channel names, got {got}. {channels}".strip())
49
+ if notify is False:
50
+ notify = None
51
+ elif isinstance(notify, str):
52
+ notify = [notify]
41
53
  if notify_tool and not notify:
42
54
  raise ValueError("notify_tool=True requires a non-empty 'notify' list")
43
55
  if notify:
44
56
  unknown = set(notify) - _channel_names
45
57
  if unknown:
46
58
  raise ValueError(f"Unknown notification channel(s): {', '.join(sorted(unknown))}")
59
+ return notify
47
60
 
48
61
 
49
62
  def _validate_agent(agent: str) -> None:
@@ -128,7 +141,7 @@ def schedule_create(
128
141
  if not prompt and not agent_file:
129
142
  raise ValueError("Provide at least one of 'prompt' or 'agent_file'")
130
143
 
131
- _validate_notify(notify, notify_tool)
144
+ notify = _validate_notify(notify, notify_tool)
132
145
  agent = _resolve_agent(agent)
133
146
  _validate_agent(agent)
134
147
 
@@ -274,8 +287,8 @@ def schedule_update(
274
287
  fields = {k: v for k, v in simple.items() if v is not None}
275
288
 
276
289
  if notify is not None:
277
- _validate_notify(notify, False)
278
- fields["notify"] = notify
290
+ notify = _validate_notify(notify, False)
291
+ fields["notify"] = notify or []
279
292
  if notify_tool is not None:
280
293
  if notify_tool:
281
294
  effective_notify = notify if notify is not None else _call(_scheduler.get, id).notify
@@ -436,7 +449,7 @@ def background_task(
436
449
  if on_complete and (not isinstance(on_complete, dict) or on_complete.get("action") != "reply"):
437
450
  raise ValueError("on_complete must be {'action': 'reply'}")
438
451
 
439
- _validate_notify(notify, notify_tool)
452
+ notify = _validate_notify(notify, notify_tool)
440
453
  agent = _resolve_agent(agent)
441
454
  _validate_agent(agent)
442
455
 
File without changes
File without changes
File without changes
File without changes