tsugite-cli 0.19.1__tar.gz → 0.19.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/AGENTS.md +4 -4
  2. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/PKG-INFO +1 -1
  3. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/pyproject.toml +1 -1
  4. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/runner.py +11 -8
  5. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/file.py +39 -1
  6. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/daemon.py +46 -40
  7. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/config.py +6 -0
  8. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/agent.py +43 -60
  9. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/executor.py +13 -56
  10. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/proxy.py +34 -7
  11. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/subprocess_executor.py +28 -22
  12. tsugite_cli-0.19.2/tsugite/core/tools.py +184 -0
  13. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/events.py +5 -1
  14. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/hooks.py +7 -1
  15. tsugite_cli-0.19.2/tsugite/secrets/redaction.py +86 -0
  16. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/__init__.py +18 -1
  17. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/http.py +9 -2
  18. tsugite_cli-0.19.1/tsugite/core/tools.py +0 -270
  19. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/.gitignore +0 -0
  20. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/CONTRIBUTING.md +0 -0
  21. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/LICENSE +0 -0
  22. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/README.md +0 -0
  23. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/__init__.py +0 -0
  24. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_inheritance.py +0 -0
  25. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_preparation.py +0 -0
  26. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/__init__.py +0 -0
  27. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_directives.py +0 -0
  28. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_runner.py +0 -0
  29. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/helpers.py +0 -0
  30. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/history_integration.py +0 -0
  31. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/metrics.py +0 -0
  32. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/models.py +0 -0
  33. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/steps.py +0 -0
  34. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/validation.py +0 -0
  35. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_utils.py +0 -0
  36. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/approval.py +0 -0
  37. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/__init__.py +0 -0
  38. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/agent_config.py +0 -0
  39. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/auto_context.py +0 -0
  40. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/base.py +0 -0
  41. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/delegation.py +0 -0
  42. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/inline.py +0 -0
  43. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/storage.py +0 -0
  44. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/url.py +0 -0
  45. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/.gitkeep +0 -0
  46. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/code_searcher.md +0 -0
  47. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/default.md +0 -0
  48. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/file_searcher.md +0 -0
  49. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_verifier.md +0 -0
  50. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_worker.md +0 -0
  51. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/onboard.md +0 -0
  52. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_context.py +0 -0
  53. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/.gitkeep +0 -0
  54. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  55. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  56. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  57. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  58. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
  59. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  60. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  61. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  62. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  63. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cache.py +0 -0
  64. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cancellation.py +0 -0
  65. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/__init__.py +0 -0
  66. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/agents.py +0 -0
  67. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/attachments.py +0 -0
  68. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/cache.py +0 -0
  69. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/chat.py +0 -0
  70. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/config.py +0 -0
  71. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/exec.py +0 -0
  72. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/helpers.py +0 -0
  73. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/history.py +0 -0
  74. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/init.py +0 -0
  75. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/models.py +0 -0
  76. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/plugins.py +0 -0
  77. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/render.py +0 -0
  78. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/run.py +0 -0
  79. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/secrets.py +0 -0
  80. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/skills.py +0 -0
  81. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/tools.py +0 -0
  82. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/usage.py +0 -0
  83. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/validate.py +0 -0
  84. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/workspace.py +0 -0
  85. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/console.py +0 -0
  86. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/constants.py +0 -0
  87. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/context.py +0 -0
  88. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/__init__.py +0 -0
  89. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/content_blocks.py +0 -0
  90. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/executor_registry.py +0 -0
  91. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/memory.py +0 -0
  92. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/record_store.py +0 -0
  93. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/sandbox.py +0 -0
  94. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/state.py +0 -0
  95. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/__init__.py +0 -0
  96. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/base.py +0 -0
  97. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/bus.py +0 -0
  98. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/helpers.py +0 -0
  99. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/exceptions.py +0 -0
  100. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/__init__.py +0 -0
  101. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/base.py +0 -0
  102. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/models.py +0 -0
  103. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/reconstruction.py +0 -0
  104. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/registry.py +0 -0
  105. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/sqlite_backend.py +0 -0
  106. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/sqlite_conn.py +0 -0
  107. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/sqlite_schema.py +0 -0
  108. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/ui_events.py +0 -0
  109. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/interaction.py +0 -0
  110. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/md_agents.py +0 -0
  111. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/models.py +0 -0
  112. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/options.py +0 -0
  113. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/permissions.py +0 -0
  114. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/plugins.py +0 -0
  115. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/__init__.py +0 -0
  116. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/anthropic.py +0 -0
  117. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/base.py +0 -0
  118. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/model_cache.py +0 -0
  119. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/model_registry.py +0 -0
  120. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/ollama.py +0 -0
  121. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/openai_compat.py +0 -0
  122. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/openrouter.py +0 -0
  123. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/py.typed +0 -0
  124. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/renderer.py +0 -0
  125. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/schemas/__init__.py +0 -0
  126. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/schemas/agent.schema.json +0 -0
  127. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/__init__.py +0 -0
  128. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/backend.py +0 -0
  129. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/env.py +0 -0
  130. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/exec.py +0 -0
  131. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/file.py +0 -0
  132. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/masking.py +0 -0
  133. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/registry.py +0 -0
  134. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/sqlite.py +0 -0
  135. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/shell_tool_config.py +0 -0
  136. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/skill_discovery.py +0 -0
  137. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/AGENTS.md +0 -0
  138. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/IDENTITY.md +0 -0
  139. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/MEMORY.md +0 -0
  140. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/USER.md +0 -0
  141. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/personas/casual-technical.md +0 -0
  142. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/personas/marvin.md +0 -0
  143. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/personas/minimal.md +0 -0
  144. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/agents.py +0 -0
  145. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/fs.py +0 -0
  146. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/history.py +0 -0
  147. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/interactive.py +0 -0
  148. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/jobs.py +0 -0
  149. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/notify.py +0 -0
  150. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/schedule.py +0 -0
  151. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/scratchpad.py +0 -0
  152. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/secrets.py +0 -0
  153. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/sessions.py +0 -0
  154. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/shell.py +0 -0
  155. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/shell_tools.py +0 -0
  156. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/skills.py +0 -0
  157. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/time.py +0 -0
  158. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tsugite.py +0 -0
  159. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/__init__.py +0 -0
  160. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/base.py +0 -0
  161. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/chat.py +0 -0
  162. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/dispatch.py +0 -0
  163. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/helpers.py +0 -0
  164. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/jsonl.py +0 -0
  165. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/plain.py +0 -0
  166. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_chat.py +0 -0
  167. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_commands.py +0 -0
  168. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_completer.py +0 -0
  169. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_handler.py +0 -0
  170. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui_context.py +0 -0
  171. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/usage/__init__.py +0 -0
  172. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/usage/store.py +0 -0
  173. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/user_agent.py +0 -0
  174. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/utils.py +0 -0
  175. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/workspace/__init__.py +0 -0
  176. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/workspace/models.py +0 -0
  177. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/workspace/session.py +0 -0
  178. {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/workspace/templates.py +0 -0
@@ -20,8 +20,8 @@ uv run pytest tests/test_file.py::test_function
20
20
  # Run tests with coverage
21
21
  uv run pytest --cov=tsugite --cov-report=html
22
22
 
23
- # Lint and format
24
- uv run black .
23
+ # Lint and format (CI gates on these, repo-wide)
24
+ uv run ruff format .
25
25
  uv run ruff check .
26
26
  uv run pylint tsugite
27
27
 
@@ -504,7 +504,7 @@ Source lives at `plugins/tsugite-daemon/frontend/` (**Svelte 5 runes + TypeScrip
504
504
 
505
505
  - `mise run web-dev` - Vite dev server on :5173 with HMR, proxying `/api` to a local daemon on 127.0.0.1:8374 (`uv run tsu daemon`).
506
506
  - `mise run web-build` - production build into `tsugite_daemon/web/` (gitignored; never committed).
507
- - `mise run web-check` - svelte-check + vitest + prettier check.
507
+ - `mise run web-check` - svelte-check + vitest unit project + prettier check. It does not run component tests (`*.svelte.test.ts`); those need `npm --prefix plugins/tsugite-daemon/frontend run test:browser`.
508
508
 
509
509
  ### Theme tokens
510
510
 
@@ -513,7 +513,7 @@ Source lives at `plugins/tsugite-daemon/frontend/` (**Svelte 5 runes + TypeScrip
513
513
 
514
514
  ### Testing
515
515
 
516
- - e2e selectors use `data-testid` values from `frontend/src/lib/testids.ts` (a frozen contract - extend, don't rename). App readiness marker: `[data-testid="app-ready"]`; test hooks hang off `window.__tsugite`.
516
+ - e2e selectors use `data-testid` values from `frontend/src/lib/testids.ts` (a frozen contract - extend, don't rename). App readiness marker: `[data-testid="app-ready"]`. There are no window-global test hooks; drive the real DOM and seed over HTTP.
517
517
  - Frontend behavioral logic (stores, SSE replay, parsers/reducers) is vitest-tested (`frontend/src/**/*.test.ts`), failing-test-first.
518
518
  - Browser verification on UI changes is mandatory. Daemon spinup + Playwright wiring recipe: project memory `project_daemon_ui_smoketest.md` (its DOM selector list predates the Svelte rebuild - trust the testids contract instead).
519
519
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.19.1
3
+ Version: 0.19.2
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.19.1"
3
+ version = "0.19.2"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -180,7 +180,7 @@ def get_default_instructions() -> str:
180
180
  return base + output + rules
181
181
 
182
182
 
183
- def _render_args(value: Any, renderer: AgentRenderer) -> Any:
183
+ def _render_args(value: Any, renderer: AgentRenderer, context: Dict[str, Any]) -> Any:
184
184
  """Recursively render Jinja in string leaves of a tool args structure.
185
185
 
186
186
  Non-string values pass through unchanged. Strings without Jinja markers
@@ -189,13 +189,13 @@ def _render_args(value: Any, renderer: AgentRenderer) -> Any:
189
189
  if isinstance(value, str):
190
190
  if "{{" not in value and "{%" not in value:
191
191
  return value
192
- return renderer.render_string(value, {})
192
+ return renderer.render_string(value, context)
193
193
  if isinstance(value, dict):
194
- return {k: _render_args(v, renderer) for k, v in value.items()}
194
+ return {k: _render_args(v, renderer, context) for k, v in value.items()}
195
195
  if isinstance(value, list):
196
- return [_render_args(v, renderer) for v in value]
196
+ return [_render_args(v, renderer, context) for v in value]
197
197
  if isinstance(value, tuple):
198
- return tuple(_render_args(v, renderer) for v in value)
198
+ return tuple(_render_args(v, renderer, context) for v in value)
199
199
  return value
200
200
 
201
201
 
@@ -219,7 +219,7 @@ def execute_prefetch(prefetch_config: List[Dict[str, Any]], event_bus: Optional[
219
219
  continue
220
220
 
221
221
  try:
222
- rendered_args = _render_args(args, renderer)
222
+ rendered_args = _render_args(args, renderer, context)
223
223
  except Exception as e:
224
224
  raise RuntimeError(f"Prefetch render failed for tool '{tool_name}': {e}") from e
225
225
 
@@ -245,7 +245,7 @@ def execute_tool_directives(
245
245
 
246
246
  Args:
247
247
  content: Markdown content with tool directives
248
- existing_context: Current template context (for error messages, not used for execution)
248
+ existing_context: Current template context; directive args render against it
249
249
  event_bus: Optional event bus for emitting warnings
250
250
 
251
251
  Returns:
@@ -289,8 +289,11 @@ def execute_tool_directives(
289
289
  renderer = AgentRenderer()
290
290
 
291
291
  for directive in directives:
292
+ # Directives run in order, so each one renders against the caller's
293
+ # context (step assignments, prefetch results) plus whatever earlier
294
+ # directives in this same content have assigned.
292
295
  try:
293
- rendered_args = _render_args(directive.args, renderer)
296
+ rendered_args = _render_args(directive.args, renderer, {**existing_context, **new_context})
294
297
  except Exception as e:
295
298
  raise RuntimeError(f"Tool directive render failed for '{directive.name}': {e}") from e
296
299
 
@@ -47,6 +47,39 @@ class FileHandler(AttachmentHandler):
47
47
  ),
48
48
  }
49
49
 
50
+ # `mimetypes` maps a number of plain-text formats into `application/*`
51
+ # (`.sh` -> `application/x-sh`, `.json` -> `application/json`), and which
52
+ # ones depends on the platform's mime.types. Reading those as documents
53
+ # base64-encodes source code the model was meant to read, so match them
54
+ # back to text. `+zip` and friends are deliberately absent from the
55
+ # structured suffixes: `application/epub+zip` is not text.
56
+ TEXT_MIME_TYPES = frozenset(
57
+ {
58
+ "application/ecmascript",
59
+ "application/javascript",
60
+ "application/json",
61
+ "application/sql",
62
+ "application/toml",
63
+ "application/x-csh",
64
+ "application/x-javascript",
65
+ "application/x-latex",
66
+ "application/x-perl",
67
+ "application/x-php",
68
+ "application/x-powershell",
69
+ "application/x-python",
70
+ "application/x-python-code",
71
+ "application/x-ruby",
72
+ "application/x-sh",
73
+ "application/x-shellscript",
74
+ "application/x-tcl",
75
+ "application/x-yaml",
76
+ "application/xml",
77
+ "application/yaml",
78
+ }
79
+ )
80
+
81
+ TEXT_MIME_SUFFIXES = ("+json", "+xml", "+yaml")
82
+
50
83
  def can_handle(self, source: str) -> bool:
51
84
  """Check if source is a file path.
52
85
 
@@ -92,12 +125,17 @@ class FileHandler(AttachmentHandler):
92
125
  return (mime_type, AttachmentContentType.IMAGE)
93
126
  elif mime_type.startswith("audio/"):
94
127
  return (mime_type, AttachmentContentType.AUDIO)
95
- elif mime_type.startswith("application/"):
128
+ elif mime_type.startswith("application/") and not self._is_text_mime(mime_type):
96
129
  return (mime_type, AttachmentContentType.DOCUMENT)
97
130
 
98
131
  # Default to text
99
132
  return ("text/plain", AttachmentContentType.TEXT)
100
133
 
134
+ @classmethod
135
+ def _is_text_mime(cls, mime_type: str) -> bool:
136
+ """Whether an `application/*` MIME type actually carries text."""
137
+ return mime_type in cls.TEXT_MIME_TYPES or mime_type.endswith(cls.TEXT_MIME_SUFFIXES)
138
+
101
139
  def fetch(self, source: str) -> Attachment:
102
140
  """Read file content.
103
141
 
@@ -8,11 +8,17 @@ import typer
8
8
  from rich.console import Console
9
9
  from rich.panel import Panel
10
10
 
11
- from tsugite.config import get_xdg_data_path, get_xdg_write_path
11
+ from tsugite.config import DEFAULT_DAEMON_HOST, DEFAULT_DAEMON_PORT, get_xdg_data_path, get_xdg_write_path
12
12
 
13
13
  daemon_app = typer.Typer(help="Daemon management commands")
14
14
  console = Console()
15
15
 
16
+ # Connection flags shared by every daemon client command. Typer resolves
17
+ # flag > env var > default natively, so there is nothing to hand-roll here.
18
+ HOST_OPTION = typer.Option(DEFAULT_DAEMON_HOST, "--host", envvar="TSUGITE_DAEMON_HOST", help="Daemon HTTP host")
19
+ PORT_OPTION = typer.Option(DEFAULT_DAEMON_PORT, "--port", "-p", envvar="TSUGITE_DAEMON_PORT", help="Daemon HTTP port")
20
+ TOKEN_OPTION = typer.Option(None, "--token", "-t", envvar="TSUGITE_DAEMON_TOKEN", help="Auth token")
21
+
16
22
 
17
23
  def _get_questionary_style():
18
24
  """Lazy load questionary style."""
@@ -394,9 +400,9 @@ def _daemon_request(method: str, host: str, port: int, path: str, token: Optiona
394
400
  @daemon_app.command("sessions")
395
401
  def list_sessions(
396
402
  agent: str = typer.Argument(help="Agent name"),
397
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
398
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
399
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
403
+ host: str = HOST_OPTION,
404
+ port: int = PORT_OPTION,
405
+ token: Optional[str] = TOKEN_OPTION,
400
406
  ):
401
407
  """List active sessions for a daemon agent."""
402
408
  data = _daemon_request("GET", host, port, f"/api/agents/{agent}/sessions", token)
@@ -421,9 +427,9 @@ def list_sessions(
421
427
  def compact_session(
422
428
  agent: str = typer.Argument(help="Agent name"),
423
429
  user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID to compact"),
424
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
425
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
426
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
430
+ host: str = HOST_OPTION,
431
+ port: int = PORT_OPTION,
432
+ token: Optional[str] = TOKEN_OPTION,
427
433
  ):
428
434
  """Force compact a daemon session."""
429
435
  data = _daemon_request(
@@ -451,9 +457,9 @@ daemon_app.add_typer(session_app, name="session")
451
457
  @session_app.command("set-primary")
452
458
  def session_set_primary(
453
459
  session_id: str = typer.Argument(help="Session ID to mark as primary"),
454
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
455
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
456
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
460
+ host: str = HOST_OPTION,
461
+ port: int = PORT_OPTION,
462
+ token: Optional[str] = TOKEN_OPTION,
457
463
  ):
458
464
  """Mark a session as the user's primary for its agent."""
459
465
  _daemon_request("POST", host, port, f"/api/sessions/{session_id}/set-primary", token, json={})
@@ -464,9 +470,9 @@ def session_set_primary(
464
470
  def session_clear_primary(
465
471
  agent: str = typer.Option(..., "--agent", "-a", help="Agent name"),
466
472
  user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID"),
467
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
468
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
469
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
473
+ host: str = HOST_OPTION,
474
+ port: int = PORT_OPTION,
475
+ token: Optional[str] = TOKEN_OPTION,
470
476
  ):
471
477
  """Remove the primary flag from any session for (user, agent)."""
472
478
  from urllib.parse import urlencode
@@ -478,9 +484,9 @@ def session_clear_primary(
478
484
 
479
485
  @session_app.command("list")
480
486
  def session_list(
481
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
482
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
483
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
487
+ host: str = HOST_OPTION,
488
+ port: int = PORT_OPTION,
489
+ token: Optional[str] = TOKEN_OPTION,
484
490
  ):
485
491
  """List all daemon sessions, indicating which one is primary."""
486
492
  from rich.table import Table
@@ -525,9 +531,9 @@ def _parse_every_to_cron(every: str) -> str:
525
531
 
526
532
  @schedule_app.command("list")
527
533
  def schedule_list(
528
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
529
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
530
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
534
+ host: str = HOST_OPTION,
535
+ port: int = PORT_OPTION,
536
+ token: Optional[str] = TOKEN_OPTION,
531
537
  ):
532
538
  """List all schedules."""
533
539
  from rich.table import Table
@@ -579,9 +585,9 @@ def schedule_add(
579
585
  "--target-session",
580
586
  help="Where the inject_history turn lands: primary, originating, none, name:<n>, or <session_id>",
581
587
  ),
582
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
583
- port: int = typer.Option(8321, "--port", help="Daemon HTTP port"),
584
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
588
+ host: str = HOST_OPTION,
589
+ port: int = PORT_OPTION,
590
+ token: Optional[str] = TOKEN_OPTION,
585
591
  ):
586
592
  """Add a new schedule."""
587
593
  spec_count = sum(1 for x in (cron, at, every) if x is not None)
@@ -617,9 +623,9 @@ def schedule_add(
617
623
 
618
624
  @schedule_app.command("cleanup")
619
625
  def schedule_cleanup(
620
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
621
- port: int = typer.Option(8321, "--port", help="Daemon HTTP port"),
622
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
626
+ host: str = HOST_OPTION,
627
+ port: int = PORT_OPTION,
628
+ token: Optional[str] = TOKEN_OPTION,
623
629
  ):
624
630
  """Remove all orphaned one-off schedules (disabled, already fired)."""
625
631
  data = _daemon_request("POST", host, port, "/api/schedules/cleanup", token)
@@ -635,9 +641,9 @@ def schedule_cleanup(
635
641
  @schedule_app.command("remove")
636
642
  def schedule_remove(
637
643
  schedule_id: str = typer.Argument(help="Schedule ID to remove"),
638
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
639
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
640
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
644
+ host: str = HOST_OPTION,
645
+ port: int = PORT_OPTION,
646
+ token: Optional[str] = TOKEN_OPTION,
641
647
  ):
642
648
  """Remove a schedule."""
643
649
  _daemon_request("DELETE", host, port, f"/api/schedules/{schedule_id}", token)
@@ -647,9 +653,9 @@ def schedule_remove(
647
653
  @schedule_app.command("enable")
648
654
  def schedule_enable(
649
655
  schedule_id: str = typer.Argument(help="Schedule ID"),
650
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
651
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
652
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
656
+ host: str = HOST_OPTION,
657
+ port: int = PORT_OPTION,
658
+ token: Optional[str] = TOKEN_OPTION,
653
659
  ):
654
660
  """Enable a schedule."""
655
661
  _daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/enable", token)
@@ -659,9 +665,9 @@ def schedule_enable(
659
665
  @schedule_app.command("disable")
660
666
  def schedule_disable(
661
667
  schedule_id: str = typer.Argument(help="Schedule ID"),
662
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
663
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
664
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
668
+ host: str = HOST_OPTION,
669
+ port: int = PORT_OPTION,
670
+ token: Optional[str] = TOKEN_OPTION,
665
671
  ):
666
672
  """Disable a schedule."""
667
673
  _daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/disable", token)
@@ -671,9 +677,9 @@ def schedule_disable(
671
677
  @schedule_app.command("run")
672
678
  def schedule_run(
673
679
  schedule_id: str = typer.Argument(help="Schedule ID to trigger now"),
674
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
675
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
676
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
680
+ host: str = HOST_OPTION,
681
+ port: int = PORT_OPTION,
682
+ token: Optional[str] = TOKEN_OPTION,
677
683
  ):
678
684
  """Trigger a schedule immediately."""
679
685
  _daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/run", token)
@@ -683,9 +689,9 @@ def schedule_run(
683
689
  @schedule_app.command("show")
684
690
  def schedule_show(
685
691
  schedule_id: str = typer.Argument(help="Schedule ID"),
686
- host: str = typer.Option("127.0.0.1", "--host", help="Daemon HTTP host"),
687
- port: int = typer.Option(8321, "--port", "-p", help="Daemon HTTP port"),
688
- token: Optional[str] = typer.Option(None, "--token", "-t", help="Auth token"),
692
+ host: str = HOST_OPTION,
693
+ port: int = PORT_OPTION,
694
+ token: Optional[str] = TOKEN_OPTION,
689
695
  ):
690
696
  """Show schedule details."""
691
697
  data = _daemon_request("GET", host, port, f"/api/schedules/{schedule_id}", token)
@@ -7,6 +7,12 @@ from typing import Callable, Dict, List, Optional
7
7
 
8
8
  from pydantic import BaseModel, ConfigDict, Field
9
9
 
10
+ # The address the daemon serves on by default. Defined here rather than in the
11
+ # daemon plugin so the server's bind default and the client's connect default
12
+ # cannot drift apart; tsugite_daemon.config.HTTPConfig imports these.
13
+ DEFAULT_DAEMON_HOST = "127.0.0.1"
14
+ DEFAULT_DAEMON_PORT = 8374
15
+
10
16
  # XDG Base Directory utilities
11
17
 
12
18
 
@@ -10,14 +10,9 @@ import time
10
10
  from dataclasses import asdict, dataclass, field, is_dataclass
11
11
  from typing import Any, Callable, Dict, List, Optional
12
12
 
13
- logger = logging.getLogger(__name__)
14
-
15
- # Cap the persisted return_value repr so a huge structured return doesn't bloat the event.
16
- RETURN_VALUE_REPR_MAX = 2048
17
-
18
- from tsugite.attachments.base import Attachment, AttachmentContentType, format_attachment_open_tag # noqa: E402
19
- from tsugite.cancellation import is_cancelled # noqa: E402
20
- from tsugite.events import ( # noqa: E402
13
+ from tsugite.attachments.base import Attachment, AttachmentContentType, format_attachment_open_tag
14
+ from tsugite.cancellation import is_cancelled
15
+ from tsugite.events import (
21
16
  CodeExecutionEvent,
22
17
  ContentBlockEvent,
23
18
  CostSummaryEvent,
@@ -36,17 +31,21 @@ from tsugite.events import ( # noqa: E402
36
31
  TaskStartEvent,
37
32
  WarningEvent,
38
33
  )
39
- from tsugite.providers.base import CompletionResponse as ProviderResponse # noqa: E402
40
- from tsugite.skill_discovery import Skill # noqa: E402
34
+ from tsugite.providers.base import CompletionResponse as ProviderResponse
35
+ from tsugite.skill_discovery import Skill
41
36
 
42
- from .content_blocks import extract_content_blocks # noqa: E402
43
- from .executor import Executor # noqa: E402
44
- from .executor_registry import get_executor_class # noqa: E402
45
- from .memory import AgentMemory, StepResult # noqa: E402
46
- from .tools import Tool # noqa: E402
37
+ from .content_blocks import extract_content_blocks
38
+ from .executor import Executor
39
+ from .executor_registry import get_executor_class
40
+ from .memory import AgentMemory, StepResult
41
+ from .tools import Tool
47
42
 
48
- # Agent execution constants
49
- DEFAULT_MAX_TURNS = 10 # Default maximum reasoning iterations before timeout
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Cap the persisted return_value repr so a huge structured return doesn't bloat the event.
46
+ RETURN_VALUE_REPR_MAX = 2048
47
+
48
+ DEFAULT_MAX_TURNS = 10 # Maximum reasoning iterations before the run is cut off
50
49
 
51
50
  _LLM_WAIT_HEARTBEAT_INTERVAL = 10.0
52
51
 
@@ -108,7 +107,7 @@ def build_system_prompt(tools: List[Tool], instructions: str = "") -> str:
108
107
  _EXEC_FENCE = "```python-exec"
109
108
  _CLOSE_FENCE = "\n```"
110
109
 
111
- # Start-of-line ```python fence whose info string is exactly "python" (bare) — i.e.
110
+ # Start-of-line ```python fence whose info string is exactly "python" (bare) - i.e.
112
111
  # NOT ```python-exec. Used to nudge the model toward the exec fence when it emits a
113
112
  # bare block. The negative lookahead keeps ```python-exec from matching.
114
113
  _BARE_PYTHON_FENCE = re.compile(r"(?:^|\n)```python(?!-exec)[ \t]*\r?\n")
@@ -450,7 +449,7 @@ class TsugiteAgent:
450
449
  await self.executor.inject_content_blocks(turn.content_blocks)
451
450
 
452
451
  # Multiple python blocks: the parser already took just the first
453
- # one as `code`. Surface the drop on two channels — a UI warning
452
+ # one as `code`. Surface the drop on two channels - a UI warning
454
453
  # for the human, and an in-conversation observation for the
455
454
  # model. Without the latter the model sees its full N-block
456
455
  # response in raw_content but only one execution result, and
@@ -621,7 +620,7 @@ class TsugiteAgent:
621
620
  from tsugite.agent_runner.history_integration import record_final_result, record_session_end
622
621
 
623
622
  # Durable answer record for run paths with no live SSE persist
624
- # (scheduled, subprocess, CLI) — the conversation view renders
623
+ # (scheduled, subprocess, CLI) - the conversation view renders
625
624
  # the answer bubble from this event.
626
625
  record_final_result(
627
626
  self.storage,
@@ -1187,46 +1186,30 @@ class TsugiteAgent:
1187
1186
  def _compute_token_breakdown(self, messages: List[Dict]) -> Dict:
1188
1187
  """Compute per-category token breakdown with individual item details."""
1189
1188
  est = estimate_content_tokens
1190
- categories = []
1191
-
1192
- # Instructions (system prompt minus tools)
1193
- instructions_tokens = est(self.instructions) if self.instructions else 0
1194
- categories.append({"name": "instructions", "tokens": instructions_tokens, "items": []})
1195
-
1196
- # Tools — per-tool breakdown
1197
- tool_items = []
1198
- for tool in self.tools:
1199
- tok = est(tool.to_code_prompt())
1200
- tool_items.append({"name": tool.name, "tokens": tok})
1201
- tool_items.sort(key=lambda x: x["tokens"], reverse=True)
1202
- tools_total = sum(t["tokens"] for t in tool_items)
1203
- categories.append({"name": "tools", "tokens": tools_total, "items": tool_items})
1204
-
1205
- # Attachments — per-attachment breakdown
1206
- att_items = []
1207
- for att in self.attachments:
1208
- tok = est(att.content) if att.content else 0
1209
- att_items.append({"name": att.name, "tokens": tok})
1210
- att_total = sum(a["tokens"] for a in att_items)
1211
- categories.append({"name": "attachments", "tokens": att_total, "items": att_items})
1212
-
1213
- # Skills — per-skill breakdown
1214
- skill_items = []
1215
- for skill in self.skills:
1216
- tok = est(skill.content) if skill.content else 0
1217
- skill_items.append({"name": skill.name, "tokens": tok})
1218
- skills_total = sum(s["tokens"] for s in skill_items)
1219
- categories.append({"name": "skills", "tokens": skills_total, "items": skill_items})
1220
-
1221
- # Hooks (pre_message hook captured output like rag_context)
1222
- hook_items = []
1223
- for name, content in self.hook_vars.items():
1224
- tok = est(content) if content else 0
1225
- hook_items.append({"name": name, "tokens": tok})
1226
- hooks_total = sum(h["tokens"] for h in hook_items)
1227
- categories.append({"name": "hooks", "tokens": hooks_total, "items": hook_items})
1228
-
1229
- # History — walk messages between context turn and task
1189
+
1190
+ def itemized(name: str, items: list) -> Dict:
1191
+ return {"name": name, "tokens": sum(i["tokens"] for i in items), "items": items}
1192
+
1193
+ # Tools sort largest-first; the other item lists keep their natural order.
1194
+ tool_items = sorted(
1195
+ ({"name": t.name, "tokens": est(t.to_code_prompt())} for t in self.tools),
1196
+ key=lambda i: i["tokens"],
1197
+ reverse=True,
1198
+ )
1199
+ categories = [
1200
+ # The system prompt minus tools; no per-item breakdown to give.
1201
+ {"name": "instructions", "tokens": est(self.instructions) if self.instructions else 0, "items": []},
1202
+ itemized("tools", tool_items),
1203
+ itemized(
1204
+ "attachments",
1205
+ [{"name": a.name, "tokens": est(a.content) if a.content else 0} for a in self.attachments],
1206
+ ),
1207
+ itemized("skills", [{"name": s.name, "tokens": est(s.content) if s.content else 0} for s in self.skills]),
1208
+ # Hooks carry pre_message captured output, e.g. rag_context.
1209
+ itemized("hooks", [{"name": n, "tokens": est(c) if c else 0} for n, c in self.hook_vars.items()]),
1210
+ ]
1211
+
1212
+ # History - walk messages between context turn and task
1230
1213
  history_tokens = 0
1231
1214
  i = 0
1232
1215
  n = len(messages)
@@ -22,6 +22,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Protocol, runt
22
22
 
23
23
  from tsugite.core.state import load_state, save_state
24
24
  from tsugite.exceptions import StateSerializationError
25
+ from tsugite.secrets.redaction import redact_sensitive_obj
25
26
 
26
27
  PPRINT_WIDTH = 100
27
28
 
@@ -114,15 +115,7 @@ class ExecutionResult:
114
115
  last_statement_type: Optional[str] = None # "expr" | "statement" | None (unparsed) - history replay metadata
115
116
 
116
117
  def to_xml(self, duration_ms: int = 0, max_output_kb: int = MAX_EXECUTION_OUTPUT_KB) -> str:
117
- """Convert execution result to structured XML format.
118
-
119
- Args:
120
- duration_ms: Execution duration in milliseconds
121
- max_output_kb: Maximum output size in KB before truncation
122
-
123
- Returns:
124
- XML string with execution result
125
- """
118
+ """Render the result as the XML observation replayed to the model."""
126
119
  from xml.sax.saxutils import escape
127
120
 
128
121
  from tsugite.secrets.registry import get_registry
@@ -148,7 +141,6 @@ class ExecutionResult:
148
141
 
149
142
  if self.error:
150
143
  parts.append(f"<error>{escape(_mask(self.error))}</error>")
151
- # Include traceback from stderr (last 10 lines)
152
144
  if self.stderr:
153
145
  tb_lines = _mask(self.stderr).strip().split("\n")[-10:]
154
146
  parts.append(f"<traceback>{escape(chr(10).join(tb_lines))}</traceback>")
@@ -217,14 +209,7 @@ def _summarize_mapping(items) -> Dict[str, str]:
217
209
 
218
210
 
219
211
  def _summarize_variable(value: Any) -> str:
220
- """Summarize a variable's type and size for display.
221
-
222
- Args:
223
- value: The variable value to summarize
224
-
225
- Returns:
226
- Summary string like "dict(3 keys)" or "list(5 items)"
227
- """
212
+ """Summarize a variable's type and size for display, e.g. "dict(3 keys)"."""
228
213
  t = type(value).__name__
229
214
  if isinstance(value, dict):
230
215
  return f"{t}({len(value)} keys)"
@@ -421,23 +406,13 @@ class LocalExecutor:
421
406
  return ns
422
407
 
423
408
  def _split_code_for_last_expr(self, code: str) -> tuple[str, Optional[str]]:
424
- """Split code into setup and last expression if applicable.
425
-
426
- If the last statement is an expression, return (setup, last_expr).
427
- Otherwise, return (code, None).
428
-
429
- Args:
430
- code: Python code to analyze
431
-
432
- Returns:
433
- Tuple of (setup_code, last_expression_or_none)
434
- """
409
+ """Split code into (setup, last_expression) when the last statement is an
410
+ expression, so it can be eval'd for REPL-style display. Otherwise (code, None)."""
435
411
  try:
436
412
  tree = ast.parse(code)
437
413
  if not tree.body:
438
414
  return (code, None)
439
415
 
440
- # Check if last statement is an expression
441
416
  last_node = tree.body[-1]
442
417
  if not isinstance(last_node, ast.Expr):
443
418
  return (code, None)
@@ -473,16 +448,7 @@ class LocalExecutor:
473
448
  return "expr" if isinstance(tree.body[-1], ast.Expr) else "statement"
474
449
 
475
450
  def _format_value(self, value: Any) -> str:
476
- """Format a value for display.
477
-
478
- Uses pprint for complex objects, repr for simple ones.
479
-
480
- Args:
481
- value: Value to format
482
-
483
- Returns:
484
- Formatted string representation
485
- """
451
+ """Format a value for display: pprint for containers, repr for everything else."""
486
452
  if isinstance(value, (dict, list, tuple, set)):
487
453
  return pprint.pformat(value, width=PPRINT_WIDTH, compact=False)
488
454
  return repr(value)
@@ -505,12 +471,6 @@ class LocalExecutor:
505
471
  Automatically displays the value of the last expression (REPL-like behavior).
506
472
  When a workspace is bound, chdir to it under a process-wide lock so raw
507
473
  Python file APIs (os.getcwd, open, Path.cwd) resolve against the workspace.
508
-
509
- Args:
510
- code: Python code to execute
511
-
512
- Returns:
513
- ExecutionResult with output, error, stdout, stderr, return_value, tools_called, and variables_set
514
474
  """
515
475
  self._return_value = None
516
476
  self._tools_called = []
@@ -666,10 +626,14 @@ class LocalExecutor:
666
626
  def tool_wrapper(*args, **kwargs):
667
627
  self._tools_called.append(tool_obj.name)
668
628
  convert_positional_to_kwargs(tool_obj, args, kwargs)
669
- record: Dict[str, Any] = {"tool": tool_obj.name, "arguments": _jsonable_call_args(kwargs)}
629
+ # Redact BEFORE _jsonable_call_args: it flattens nested values into
630
+ # capped repr() strings, and a token baked into one of those is no
631
+ # longer reachable by key or path.
632
+ audit_args = redact_sensitive_obj(kwargs, getattr(tool_obj, "sensitive_paths", ()))
633
+ record: Dict[str, Any] = {"tool": tool_obj.name, "arguments": _jsonable_call_args(audit_args)}
670
634
  self._tool_calls.append(record)
671
635
  if self.event_bus:
672
- self.event_bus.emit(ToolCallEvent(tool_name=tool_obj.name, arguments=kwargs))
636
+ self.event_bus.emit(ToolCallEvent(tool_name=tool_obj.name, arguments=audit_args))
673
637
  t0 = time.perf_counter()
674
638
  try:
675
639
  result = run_async_in_sync_context(tool_obj.execute(**kwargs))
@@ -718,14 +682,7 @@ class LocalExecutor:
718
682
  self.namespace.update(tools)
719
683
 
720
684
  def register_loaded_skill(self, name: str, content: str):
721
- """Register a skill loaded during this execution turn.
722
-
723
- Called by load_skill() tool to track skills for observation embedding.
724
-
725
- Args:
726
- name: Skill name
727
- content: Rendered skill content
728
- """
685
+ """Register a skill loaded during this turn. Called by the load_skill() tool."""
729
686
  self._loaded_skills_for_turn[name] = content
730
687
 
731
688
  def register_unloaded_skill(self, name: str):