tsugite-cli 0.19.0__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 (180) hide show
  1. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/AGENTS.md +9 -9
  2. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/PKG-INFO +1 -1
  3. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/pyproject.toml +1 -1
  4. tsugite_cli-0.19.2/tsugite/agent_preparation.py +566 -0
  5. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/__init__.py +8 -4
  6. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/helpers.py +83 -45
  7. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/history_integration.py +7 -5
  8. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/runner.py +205 -944
  9. tsugite_cli-0.19.2/tsugite/agent_runner/steps.py +686 -0
  10. tsugite_cli-0.19.2/tsugite/attachments/agent_config.py +349 -0
  11. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/file.py +39 -1
  12. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/daemon.py +46 -40
  13. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/history.py +1 -92
  14. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/run.py +12 -21
  15. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/config.py +6 -0
  16. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/agent.py +91 -119
  17. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/executor.py +13 -56
  18. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/proxy.py +34 -7
  19. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/subprocess_executor.py +28 -22
  20. tsugite_cli-0.19.2/tsugite/core/tools.py +184 -0
  21. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/events.py +5 -1
  22. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/__init__.py +2 -13
  23. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/base.py +3 -7
  24. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/models.py +55 -1
  25. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/registry.py +2 -32
  26. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/sqlite_backend.py +17 -47
  27. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/ui_events.py +4 -43
  28. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/hooks.py +7 -1
  29. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/anthropic.py +27 -0
  30. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/openai_compat.py +12 -1
  31. tsugite_cli-0.19.2/tsugite/secrets/redaction.py +86 -0
  32. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/__init__.py +33 -17
  33. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/http.py +9 -2
  34. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/base.py +24 -34
  35. tsugite_cli-0.19.2/tsugite/ui/dispatch.py +76 -0
  36. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/jsonl.py +25 -35
  37. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_handler.py +19 -25
  38. tsugite_cli-0.19.0/tsugite/agent_preparation.py +0 -808
  39. tsugite_cli-0.19.0/tsugite/core/tools.py +0 -270
  40. tsugite_cli-0.19.0/tsugite/history/storage.py +0 -311
  41. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/.gitignore +0 -0
  42. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/CONTRIBUTING.md +0 -0
  43. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/LICENSE +0 -0
  44. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/README.md +0 -0
  45. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/__init__.py +0 -0
  46. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_inheritance.py +0 -0
  47. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_directives.py +0 -0
  48. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_runner.py +0 -0
  49. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/metrics.py +0 -0
  50. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/models.py +0 -0
  51. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/validation.py +0 -0
  52. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_utils.py +0 -0
  53. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/approval.py +0 -0
  54. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/__init__.py +0 -0
  55. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/auto_context.py +0 -0
  56. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/base.py +0 -0
  57. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/delegation.py +0 -0
  58. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/inline.py +0 -0
  59. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/storage.py +0 -0
  60. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/url.py +0 -0
  61. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/.gitkeep +0 -0
  62. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/code_searcher.md +0 -0
  63. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/default.md +0 -0
  64. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/file_searcher.md +0 -0
  65. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_verifier.md +0 -0
  66. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_worker.md +0 -0
  67. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/onboard.md +0 -0
  68. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_context.py +0 -0
  69. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/.gitkeep +0 -0
  70. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  71. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  72. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  73. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  74. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
  75. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  76. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  77. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  78. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  79. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cache.py +0 -0
  80. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cancellation.py +0 -0
  81. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/__init__.py +0 -0
  82. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/agents.py +0 -0
  83. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/attachments.py +0 -0
  84. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/cache.py +0 -0
  85. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/chat.py +0 -0
  86. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/config.py +0 -0
  87. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/exec.py +0 -0
  88. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/helpers.py +0 -0
  89. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/init.py +0 -0
  90. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/models.py +0 -0
  91. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/plugins.py +0 -0
  92. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/render.py +0 -0
  93. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/secrets.py +0 -0
  94. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/skills.py +0 -0
  95. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/tools.py +0 -0
  96. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/usage.py +0 -0
  97. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/validate.py +0 -0
  98. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/workspace.py +0 -0
  99. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/console.py +0 -0
  100. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/constants.py +0 -0
  101. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/context.py +0 -0
  102. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/__init__.py +0 -0
  103. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/content_blocks.py +0 -0
  104. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/executor_registry.py +0 -0
  105. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/memory.py +0 -0
  106. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/record_store.py +0 -0
  107. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/sandbox.py +0 -0
  108. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/state.py +0 -0
  109. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/__init__.py +0 -0
  110. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/base.py +0 -0
  111. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/bus.py +0 -0
  112. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/helpers.py +0 -0
  113. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/exceptions.py +0 -0
  114. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/reconstruction.py +0 -0
  115. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/sqlite_conn.py +0 -0
  116. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/sqlite_schema.py +0 -0
  117. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/interaction.py +0 -0
  118. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/md_agents.py +0 -0
  119. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/models.py +0 -0
  120. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/options.py +0 -0
  121. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/permissions.py +0 -0
  122. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/plugins.py +0 -0
  123. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/__init__.py +0 -0
  124. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/base.py +0 -0
  125. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/model_cache.py +0 -0
  126. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/model_registry.py +0 -0
  127. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/ollama.py +0 -0
  128. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/openrouter.py +0 -0
  129. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/py.typed +0 -0
  130. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/renderer.py +0 -0
  131. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/schemas/__init__.py +0 -0
  132. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/schemas/agent.schema.json +0 -0
  133. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/__init__.py +0 -0
  134. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/backend.py +0 -0
  135. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/env.py +0 -0
  136. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/exec.py +0 -0
  137. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/file.py +0 -0
  138. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/masking.py +0 -0
  139. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/registry.py +0 -0
  140. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/sqlite.py +0 -0
  141. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/shell_tool_config.py +0 -0
  142. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/skill_discovery.py +0 -0
  143. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/AGENTS.md +0 -0
  144. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/IDENTITY.md +0 -0
  145. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/MEMORY.md +0 -0
  146. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/USER.md +0 -0
  147. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/personas/casual-technical.md +0 -0
  148. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/personas/marvin.md +0 -0
  149. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/personas/minimal.md +0 -0
  150. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/agents.py +0 -0
  151. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/fs.py +0 -0
  152. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/history.py +0 -0
  153. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/interactive.py +0 -0
  154. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/jobs.py +0 -0
  155. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/notify.py +0 -0
  156. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/schedule.py +0 -0
  157. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/scratchpad.py +0 -0
  158. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/secrets.py +0 -0
  159. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/sessions.py +0 -0
  160. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/shell.py +0 -0
  161. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/shell_tools.py +0 -0
  162. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/skills.py +0 -0
  163. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/time.py +0 -0
  164. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tsugite.py +0 -0
  165. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/__init__.py +0 -0
  166. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/chat.py +0 -0
  167. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/helpers.py +0 -0
  168. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/plain.py +0 -0
  169. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_chat.py +0 -0
  170. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_commands.py +0 -0
  171. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_completer.py +0 -0
  172. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui_context.py +0 -0
  173. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/usage/__init__.py +0 -0
  174. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/usage/store.py +0 -0
  175. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/user_agent.py +0 -0
  176. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/utils.py +0 -0
  177. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/workspace/__init__.py +0 -0
  178. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/workspace/models.py +0 -0
  179. {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/workspace/session.py +0 -0
  180. {tsugite_cli-0.19.0 → 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
 
@@ -266,11 +266,11 @@ tsugite run +coordinator +researcher +writer "task"
266
266
 
267
267
  ### Prompt Caching
268
268
 
269
- Automatic caching for supported providers (OpenAI, Anthropic, Bedrock, Deepseek):
270
-
271
- - Attachments sent as separate system content blocks with `cache_control` markers
272
- - Skills also receive cache markers for reuse across turns
273
- - Cache markers added automatically by `tsugite/core/agent.py`
269
+ - `tsugite/core/agent.py` marks each context turn (attachments, plus skills on the last tier) with a
270
+ provider-neutral `cache_control` hint
271
+ - Each provider translates the hint for its own API: Anthropic moves it onto a content block, capped at the
272
+ API's 4 breakpoints; OpenAI-schema endpoints cache automatically and drop it, since strict ones reject the
273
+ unknown field
274
274
 
275
275
  ### Context Management (Daemon Mode)
276
276
 
@@ -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.0
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.0"
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"
@@ -0,0 +1,566 @@
1
+ """Agent preparation pipeline - unified logic for render and execution."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ if TYPE_CHECKING:
11
+ from tsugite.cli.helpers import PathContext
12
+ from tsugite.tools.skills import SkillManager
13
+
14
+ from tsugite.attachments.agent_config import ( # noqa: E402
15
+ resolve_agent_config_attachments,
16
+ split_attachment_removals,
17
+ )
18
+ from tsugite.attachments.base import Attachment # noqa: E402
19
+ from tsugite.core.tools import Tool # noqa: E402
20
+ from tsugite.md_agents import Agent, AgentConfig # noqa: E402
21
+ from tsugite.skill_discovery import Skill # noqa: E402
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class SkillLoad:
26
+ """What one turn's skill resolution produced.
27
+
28
+ `expired`/`triggered`/`auto_loaded` are read back by the daemon to update its
29
+ sticky state after the turn, so they are real outputs rather than bookkeeping;
30
+ `prepare()` publishes them onto the template context explicitly.
31
+ """
32
+
33
+ skills: List[Skill]
34
+ expiring: Dict[str, int]
35
+ expired: List[str]
36
+ triggered: List[str]
37
+ auto_loaded: List[str]
38
+
39
+
40
+ @dataclass
41
+ class PreparedAgent:
42
+ """Fully prepared agent ready for execution or display.
43
+
44
+ This dataclass contains everything needed to either:
45
+ 1. Display what will be sent to the LLM (render command)
46
+ 2. Execute the agent (run command)
47
+
48
+ Attributes:
49
+ agent: Parsed agent object with content and config
50
+ agent_config: Agent configuration (model, tools, etc.)
51
+ system_message: Complete system message sent to LLM
52
+ user_message: Complete user message sent to LLM
53
+ rendered_prompt: Rendered template (before building system message)
54
+ original_prompt: The user's prompt as typed (pre-template-rendering)
55
+ tools: List of Tool objects ready for agent execution
56
+ context: Full template rendering context
57
+ combined_instructions: Combined default + agent instructions
58
+ attachments: List of Attachment objects for multi-modal inputs
59
+ skills: List of Skill objects for loaded skills
60
+ """
61
+
62
+ agent: Agent
63
+ agent_config: AgentConfig
64
+ system_message: str
65
+ user_message: str
66
+ rendered_prompt: str
67
+ tools: List[Tool]
68
+ context: Dict[str, Any]
69
+ combined_instructions: str
70
+ attachments: List[Attachment]
71
+ original_prompt: str = ""
72
+ skills: List[Skill] = field(default_factory=list)
73
+ # Map of sticky skill name -> turns remaining before auto-unload. Only
74
+ # populated on the turn a skill is about to expire (turns_remaining <= 0).
75
+ # The agent surfaces these as <skill_expiring> blocks in the context turn.
76
+ expiring_skills: Dict[str, int] = field(default_factory=dict)
77
+ skipped: bool = False
78
+ skip_reason: str | None = None
79
+
80
+
81
+ class AgentPreparer:
82
+ """Prepares agents for execution or rendering.
83
+
84
+ This class consolidates all agent preparation logic that was previously
85
+ duplicated across render command, run_agent, and _execute_agent_with_prompt.
86
+
87
+ `prepare()` orchestrates, one named step per phase:
88
+
89
+ _resolve_attachments caller + frontmatter attachments, removals, dedupe
90
+ _install_skill_manager workspace-aware SkillManager, made active
91
+ _run_prefetch frontmatter prefetch tools + agent-list injection
92
+ _resolve_paths cwd / invoked_from / workspace_dir
93
+ _build_template_context the Jinja context and its framework defaults
94
+ _expand_tools tool specs + capability-based auto-injection
95
+ _load_skills auto-load, sticky TTL, trigger matching
96
+
97
+ Both `render` and `run` go through here, so render shows EXACTLY what run
98
+ executes.
99
+ """
100
+
101
+ def _build_directive_placeholders(
102
+ self,
103
+ content: str,
104
+ extractor: Callable[[str], List[Any]],
105
+ kind: str,
106
+ rewrite_to: Optional[Callable[[Any], str]] = None,
107
+ ) -> Tuple[str, Dict[str, str]]:
108
+ """Build {assign_var: placeholder} for directives that won't actually run.
109
+
110
+ Used by render-mode shortcuts so the template still has all its variables
111
+ defined. When `rewrite_to` is given, the directive substring is also
112
+ replaced in `content` with the callable's per-directive output.
113
+ """
114
+ try:
115
+ directives = extractor(content)
116
+ except Exception:
117
+ return content, {}
118
+
119
+ placeholders: Dict[str, str] = {}
120
+ modified = content
121
+ for d in directives:
122
+ if d.assign_var:
123
+ placeholders[d.assign_var] = f"[{kind} directive: {d.name}(...) - not executed in render mode]"
124
+ if rewrite_to is not None:
125
+ modified = modified.replace(d.raw_match, rewrite_to(d))
126
+ return modified, placeholders
127
+
128
+ def _extract_tool_directive_placeholders(self, content: str) -> Dict[str, str]:
129
+ from tsugite.md_agents import extract_tool_directives
130
+
131
+ _, placeholders = self._build_directive_placeholders(content, extract_tool_directives, "Tool")
132
+ return placeholders
133
+
134
+ def _extract_exec_directive_placeholders(self, content: str) -> Tuple[str, Dict[str, str]]:
135
+ from tsugite.md_agents import extract_exec_directives
136
+
137
+ return self._build_directive_placeholders(
138
+ content,
139
+ extract_exec_directives,
140
+ "Exec",
141
+ rewrite_to=lambda d: f"<!-- Exec '{d.name}' skipped (--no-exec) -->",
142
+ )
143
+
144
+ def _resolve_attachments(
145
+ self, agent_config: AgentConfig, attachments: Optional[List[Attachment]]
146
+ ) -> Tuple[List[Attachment], Dict[str, Any]]:
147
+ """Merge caller attachments with the agent's own, honoring removals.
148
+
149
+ Front-matter attachments carry the cache tiers and are the intended
150
+ source, so they dedupe ahead of any same-named attachment the caller
151
+ passed in. Legacy `-filename` string entries drop a same-named entry.
152
+ Front-matter paths resolve via cwd (workspace_path is None in production).
153
+ """
154
+ all_attachments = list(attachments or [])
155
+
156
+ removals, keep_items = split_attachment_removals(agent_config.attachments or [])
157
+ if removals:
158
+ all_attachments = [a for a in all_attachments if a.name not in removals]
159
+
160
+ loaded, bindings = resolve_agent_config_attachments(keep_items, None)
161
+ all_attachments = loaded + all_attachments
162
+
163
+ seen_names: set[str] = set()
164
+ deduped: List[Attachment] = []
165
+ for att in all_attachments:
166
+ if att.name not in seen_names:
167
+ seen_names.add(att.name)
168
+ deduped.append(att)
169
+ return deduped, bindings
170
+
171
+ def _install_skill_manager(
172
+ self, agent_config: AgentConfig, path_context: Optional["PathContext"]
173
+ ) -> "SkillManager":
174
+ """Build the workspace-aware skill manager and make it the active one.
175
+
176
+ The workspace comes from path_context.workspace_dir so daemon and chat
177
+ callers still get workspace skills.
178
+ """
179
+ from tsugite.config import load_config
180
+ from tsugite.tools.skills import SkillManager, set_skill_manager
181
+ from tsugite.workspace.models import Workspace
182
+
183
+ workspace = Workspace.try_load(path_context.workspace_dir if path_context else None)
184
+ extra_paths = (agent_config.skill_paths or []) + (load_config().skill_paths or [])
185
+ manager = SkillManager(workspace=workspace, extra_paths=extra_paths or None)
186
+ set_skill_manager(manager)
187
+ return manager
188
+
189
+ def _run_prefetch(self, agent_config: AgentConfig) -> Dict[str, Any]:
190
+ """Run frontmatter prefetch tools, plus the opt-in agent-list injection.
191
+
192
+ A failing prefetch yields an empty context rather than failing the run:
193
+ prefetch is supplementary context, not a precondition.
194
+ """
195
+ from tsugite.agent_runner import execute_prefetch
196
+
197
+ prefetch_context: Dict[str, Any] = {}
198
+ if agent_config.prefetch:
199
+ try:
200
+ prefetch_context = execute_prefetch(agent_config.prefetch)
201
+ except Exception:
202
+ prefetch_context = {}
203
+
204
+ # Default agents call list_available_agents() on demand instead of
205
+ # carrying the full list, so this stays opt-in.
206
+ if "available_agents" not in prefetch_context and (
207
+ agent_config.auto_load_agent_list or agent_config.auto_load_agents
208
+ ):
209
+ from tsugite.tools.agents import discover_agents, format_agents_markdown
210
+
211
+ agents = discover_agents()
212
+ if agent_config.auto_load_agents:
213
+ wanted = set(agent_config.auto_load_agents)
214
+ agents = [a for a in agents if a["name"] in wanted]
215
+ prefetch_context["available_agents"] = format_agents_markdown(agents)
216
+
217
+ return prefetch_context
218
+
219
+ def _load_skills(
220
+ self,
221
+ agent_config: AgentConfig,
222
+ prompt: str,
223
+ full_context: Dict[str, Any],
224
+ skill_manager: "SkillManager",
225
+ event_bus: Optional[Any],
226
+ ) -> SkillLoad:
227
+ """Load auto, sticky and trigger-matched skills for this turn."""
228
+ from tsugite.events.events import SkillLoadFailedEvent
229
+
230
+ # Skills the user explicitly removed this session (populated by the daemon).
231
+ suppressed_skills = set(full_context.get("suppressed_skills") or [])
232
+
233
+ # Sticky skills carried over from prior turns on this session (daemon-only).
234
+ # Shape: {skill_name: turns_unused_counter}
235
+ sticky_counters: Dict[str, int] = dict(full_context.get("sticky_skills") or {})
236
+ ttl_default = int(full_context.get("skill_ttl_default") or 10)
237
+
238
+ auto_load_skills = [s for s in (agent_config.auto_load_skills or []) if s not in suppressed_skills]
239
+
240
+ for skill_name in auto_load_skills:
241
+ result = skill_manager.load_skill(skill_name)
242
+ if result.startswith("Failed") or result.startswith("Skill '"):
243
+ if event_bus:
244
+ event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
245
+
246
+ # unless the counter already exceeded the skill's effective TTL (expired).
247
+ skill_manager._ensure_registry_initialized()
248
+ registry = skill_manager._skill_registry
249
+ expiring_skills: Dict[str, int] = {}
250
+ expired_sticky: List[str] = []
251
+ for name, counter in sticky_counters.items():
252
+ if name in suppressed_skills:
253
+ expired_sticky.append(name)
254
+ continue
255
+ meta = registry.get(name)
256
+ if meta is None:
257
+ # Skill vanished (renamed/removed) between turns — drop it.
258
+ expired_sticky.append(name)
259
+ continue
260
+ effective_ttl = meta.ttl if meta.ttl is not None else ttl_default
261
+ if effective_ttl > 0 and counter > effective_ttl:
262
+ expired_sticky.append(name)
263
+ continue
264
+ if name in auto_load_skills:
265
+ # Already loaded above; no need to double-load but it's still sticky.
266
+ continue
267
+ result = skill_manager.load_skill(name)
268
+ if result.startswith("Failed") or result.startswith("Skill '"):
269
+ if event_bus:
270
+ event_bus.emit(SkillLoadFailedEvent(skill_name=name, error_message=result))
271
+ continue
272
+ if effective_ttl > 0:
273
+ remaining = effective_ttl - counter
274
+ if remaining <= 1:
275
+ expiring_skills[name] = max(remaining, 0)
276
+
277
+ triggered_skill_names = [
278
+ name for name in skill_manager.get_triggered_skills(prompt) if name not in suppressed_skills
279
+ ]
280
+ for skill_name in triggered_skill_names:
281
+ logger.info(f"Trigger-loading skill '{skill_name}' based on user prompt")
282
+ result = skill_manager.load_skill(skill_name)
283
+ if result.startswith("Failed") or result.startswith("Skill '"):
284
+ if event_bus:
285
+ event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
286
+
287
+ loaded_skills_dict = skill_manager.get_loaded_skills()
288
+ return SkillLoad(
289
+ skills=[Skill(name=name, content=content) for name, content in loaded_skills_dict.items()],
290
+ expiring=expiring_skills,
291
+ expired=expired_sticky,
292
+ triggered=list(triggered_skill_names),
293
+ auto_loaded=list(auto_load_skills),
294
+ )
295
+
296
+ def _expand_tools(
297
+ self, agent_config: AgentConfig, full_context: Dict[str, Any], interactive_mode: bool
298
+ ) -> List[Tool]:
299
+ """Expand the agent's tool specs and apply the capability-based auto-injections.
300
+
301
+ Interactive tools are added only when something can actually answer, and
302
+ stripped otherwise, so a scheduled run cannot offer the model a prompt
303
+ nobody will see.
304
+ """
305
+ from tsugite.core.tools import create_tool_from_tsugite
306
+ from tsugite.interaction import get_interaction_backend
307
+ from tsugite.tools import _tools, expand_tool_specs
308
+
309
+ try:
310
+ expanded = (
311
+ expand_tool_specs(agent_config.tools, strict=agent_config.strict_tools) if agent_config.tools else []
312
+ )
313
+
314
+ interactive_tool_names = ["ask_user", "ask_user_batch"]
315
+ has_interaction = (
316
+ interactive_mode or get_interaction_backend() is not None or full_context.get("is_daemon", False)
317
+ )
318
+ if has_interaction:
319
+ for name in interactive_tool_names:
320
+ if name not in expanded and name in _tools:
321
+ expanded.append(name)
322
+ else:
323
+ expanded = [t for t in expanded if t not in interactive_tool_names]
324
+
325
+ # Scheduled tasks get a way to reach the user even if the agent didn't ask.
326
+ if full_context.get("has_notify_tool", False):
327
+ if "notify_user" not in expanded and "notify_user" in _tools:
328
+ expanded.append("notify_user")
329
+
330
+ return [create_tool_from_tsugite(name) for name in expanded]
331
+ except Exception as e:
332
+ raise RuntimeError(f"Failed to create tools: {e}") from e
333
+
334
+ @staticmethod
335
+ def _resolve_paths(path_context: Optional["PathContext"]) -> Tuple[str, Optional[str], Optional[str]]:
336
+ """Return (cwd, invoked_from, workspace_dir) as strings for the template context.
337
+
338
+ The daemon supplies an effective_cwd that differs from the process cwd.
339
+ """
340
+ if path_context and path_context.effective_cwd:
341
+ cwd = str(path_context.effective_cwd)
342
+ else:
343
+ cwd = str(Path.cwd())
344
+ invoked_from = str(path_context.invoked_from) if path_context else None
345
+ workspace_dir = str(path_context.workspace_dir) if path_context and path_context.workspace_dir else None
346
+ return cwd, invoked_from, workspace_dir
347
+
348
+ def _build_template_context(
349
+ self,
350
+ *,
351
+ agent_config: AgentConfig,
352
+ prompt: str,
353
+ context: Dict[str, Any],
354
+ directive_context: Dict[str, Any],
355
+ attachment_bindings: Dict[str, Any],
356
+ interactive_mode: bool,
357
+ cwd: str,
358
+ invoked_from: Optional[str],
359
+ workspace_dir: Optional[str],
360
+ ) -> Dict[str, Any]:
361
+ """Assemble the Jinja context every agent template renders against.
362
+
363
+ Framework flags default here rather than in the template so a caller that
364
+ supplies none of them still renders under StrictUndefined.
365
+ """
366
+ from tsugite.tools import list_tools
367
+
368
+ full_context = {
369
+ **context,
370
+ **directive_context,
371
+ "user_prompt": prompt,
372
+ "agent_name": agent_config.name,
373
+ "is_interactive": interactive_mode,
374
+ "is_daemon": context.get("is_daemon", False),
375
+ "is_scheduled": context.get("is_scheduled", False),
376
+ "schedule_id": context.get("schedule_id", ""),
377
+ "has_notify_tool": context.get("has_notify_tool", False),
378
+ "running_tasks": context.get("running_tasks", []),
379
+ "tsugite_url": context.get("tsugite_url", ""),
380
+ "tsugite_token": context.get("tsugite_token", ""),
381
+ "tools": agent_config.tools,
382
+ # Tool names actually installed, so templates can conditionally mention optional
383
+ # tools, e.g. `{% if 'web_search' in available_tools %}`.
384
+ "available_tools": list_tools(),
385
+ "is_subagent": context.get("is_subagent", False),
386
+ "parent_agent": context.get("parent_agent", None),
387
+ "chat_history": context.get("chat_history", []),
388
+ "CWD": cwd,
389
+ "INVOKED_FROM": invoked_from,
390
+ "WORKSPACE_DIR": workspace_dir,
391
+ }
392
+
393
+ # User-specified attachment `assign:` bindings win over built-in or
394
+ # prefetch values; warn so collisions surface rather than confuse.
395
+ for name, value in attachment_bindings.items():
396
+ if name in full_context:
397
+ logger.warning("Attachment binding %r overrides existing context variable", name)
398
+ full_context[name] = value
399
+
400
+ return full_context
401
+
402
+ def prepare(
403
+ self,
404
+ agent: Agent,
405
+ prompt: str,
406
+ context: Optional[Dict[str, Any]] = None,
407
+ skip_tool_directives: bool = False,
408
+ skip_exec_directives: bool = False,
409
+ attachments: Optional[List[Attachment]] = None,
410
+ event_bus: Optional[Any] = None,
411
+ path_context: Optional["PathContext"] = None,
412
+ ) -> PreparedAgent:
413
+ """Prepare agent with all context, tools, and instructions.
414
+
415
+ Args:
416
+ agent: Parsed agent object
417
+ prompt: User prompt/task
418
+ context: Additional context variables
419
+ skip_tool_directives: Skip executing tool directives (for render)
420
+ skip_exec_directives: Skip executing exec directives (for render --no-exec)
421
+ attachments: List of Attachment objects for multi-modal inputs
422
+ event_bus: Optional event bus for emitting skill load events
423
+ path_context: Optional PathContext with invoked_from, workspace_dir, effective_cwd
424
+
425
+ Returns:
426
+ PreparedAgent ready for execution or display
427
+
428
+ Raises:
429
+ RuntimeError: If preparation fails
430
+ """
431
+ from tsugite.agent_runner import (
432
+ _combine_instructions,
433
+ execute_exec_directives,
434
+ execute_tool_directives,
435
+ get_default_instructions,
436
+ )
437
+ from tsugite.core.agent import build_system_prompt
438
+ from tsugite.renderer import AgentRenderer
439
+ from tsugite.utils import is_interactive
440
+
441
+ if context is None:
442
+ context = {}
443
+
444
+ agent_config = agent.config
445
+
446
+ all_attachments, attachment_bindings = self._resolve_attachments(agent_config, attachments)
447
+
448
+ _skill_manager = self._install_skill_manager(agent_config, path_context)
449
+
450
+ prefetch_context = self._run_prefetch(agent_config)
451
+
452
+ if skip_tool_directives:
453
+ modified_content = agent.content
454
+ # Extract tool directive variable names and provide placeholders
455
+ tool_context = self._extract_tool_directive_placeholders(agent.content)
456
+ else:
457
+ modified_content, tool_context = execute_tool_directives(agent.content, prefetch_context)
458
+
459
+ # reflects what the LLM would see; `--no-exec` opts out for side-effecty blocks.
460
+ if skip_exec_directives:
461
+ modified_content, exec_context = self._extract_exec_directive_placeholders(modified_content)
462
+ else:
463
+ exec_locals: Dict[str, Any] = {**context, **prefetch_context, **tool_context}
464
+ modified_content, exec_context = execute_exec_directives(
465
+ modified_content,
466
+ existing_context=exec_locals,
467
+ event_bus=event_bus,
468
+ )
469
+
470
+ interactive_mode = is_interactive()
471
+ cwd, invoked_from, workspace_dir = self._resolve_paths(path_context)
472
+ full_context = self._build_template_context(
473
+ agent_config=agent_config,
474
+ prompt=prompt,
475
+ context=context,
476
+ directive_context={**prefetch_context, **tool_context, **exec_context},
477
+ attachment_bindings=attachment_bindings,
478
+ interactive_mode=interactive_mode,
479
+ cwd=cwd,
480
+ invoked_from=invoked_from,
481
+ workspace_dir=workspace_dir,
482
+ )
483
+
484
+ renderer = AgentRenderer()
485
+
486
+ if agent_config.run_if:
487
+ skip_reason = None
488
+ try:
489
+ guard_result = renderer.render("{{ " + agent_config.run_if + " }}", full_context)
490
+ if guard_result.strip().lower() in ("", "false", "none", "0"):
491
+ skip_reason = f"run_if guard '{agent_config.run_if}' evaluated to false"
492
+ except Exception as e:
493
+ skip_reason = f"run_if guard error: {e}"
494
+ if skip_reason:
495
+ return PreparedAgent(
496
+ agent=agent,
497
+ agent_config=agent_config,
498
+ system_message="",
499
+ user_message="",
500
+ rendered_prompt="",
501
+ tools=[],
502
+ context=full_context,
503
+ combined_instructions="",
504
+ attachments=all_attachments,
505
+ skipped=True,
506
+ skip_reason=skip_reason,
507
+ )
508
+
509
+ try:
510
+ rendered_prompt = renderer.render(modified_content, full_context)
511
+ except Exception as e:
512
+ raise RuntimeError(f"Template rendering failed: {e}") from e
513
+
514
+ base_instructions = get_default_instructions()
515
+ agent_instructions = getattr(agent_config, "instructions", "")
516
+
517
+ # Render agent instructions as Jinja2 template
518
+ if agent_instructions:
519
+ try:
520
+ agent_instructions = renderer.render(agent_instructions, full_context)
521
+ except Exception as e:
522
+ raise RuntimeError(f"Failed to render agent instructions: {e}") from e
523
+
524
+ combined_instructions = _combine_instructions(base_instructions, agent_instructions)
525
+
526
+ tools = self._expand_tools(agent_config, full_context, interactive_mode)
527
+
528
+ skill_load = self._load_skills(agent_config, prompt, full_context, _skill_manager, event_bus)
529
+ skills, expiring_skills = skill_load.skills, skill_load.expiring
530
+ # The daemon reads these back off the context to update sticky state.
531
+ full_context["_expired_sticky_skills"] = skill_load.expired
532
+ full_context["_triggered_skill_names"] = skill_load.triggered
533
+ full_context["_auto_loaded_skill_names"] = skill_load.auto_loaded
534
+
535
+ system_message = build_system_prompt(tools, combined_instructions)
536
+
537
+ # Add environment context when invoked_from differs from CWD
538
+ if invoked_from and invoked_from != cwd:
539
+ env_block = f"""
540
+ ## Environment
541
+
542
+ Working directory: {cwd}
543
+ Invoked from: {invoked_from}
544
+
545
+ When the user refers to "this folder", "current directory", or "here",
546
+ they typically mean the invoked location ({invoked_from}).
547
+ """
548
+ system_message = system_message + env_block
549
+
550
+ # User message is the rendered prompt
551
+ user_message = rendered_prompt
552
+
553
+ return PreparedAgent(
554
+ agent=agent,
555
+ agent_config=agent_config,
556
+ system_message=system_message,
557
+ user_message=user_message,
558
+ rendered_prompt=rendered_prompt,
559
+ original_prompt=prompt,
560
+ tools=tools,
561
+ context=full_context,
562
+ combined_instructions=combined_instructions,
563
+ attachments=all_attachments,
564
+ skills=skills,
565
+ expiring_skills=expiring_skills,
566
+ )
@@ -15,6 +15,10 @@ from tsugite.agent_runner.helpers import ( # noqa: F401
15
15
  get_display_console,
16
16
  get_sandbox_context,
17
17
  get_ui_handler,
18
+ reset_allowed_agents,
19
+ reset_allowed_secrets,
20
+ reset_current_agent,
21
+ reset_sandbox_context,
18
22
  sandbox_context_to_override,
19
23
  set_allowed_agents,
20
24
  set_allowed_secrets,
@@ -33,16 +37,12 @@ from tsugite.agent_runner.runner import ( # noqa: F401
33
37
  preview_multistep_agent,
34
38
  run_agent,
35
39
  run_agent_async,
36
- run_multistep_agent,
37
- run_multistep_agent_async,
38
40
  )
39
41
  from tsugite.agent_runner.validation import get_agent_info, validate_agent_file # noqa: F401
40
42
 
41
43
  __all__ = [
42
44
  "run_agent",
43
45
  "run_agent_async",
44
- "run_multistep_agent",
45
- "run_multistep_agent_async",
46
46
  "preview_multistep_agent",
47
47
  "execute_prefetch",
48
48
  "execute_tool_directives",
@@ -72,5 +72,9 @@ __all__ = [
72
72
  "get_sandbox_context",
73
73
  "set_sandbox_context",
74
74
  "clear_sandbox_context",
75
+ "reset_allowed_agents",
76
+ "reset_allowed_secrets",
77
+ "reset_current_agent",
78
+ "reset_sandbox_context",
75
79
  "sandbox_context_to_override",
76
80
  ]