tsugite-cli 0.14.1__tar.gz → 0.14.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 (205) hide show
  1. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/PKG-INFO +1 -1
  2. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/pyproject.toml +2 -2
  3. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_inheritance.py +3 -0
  4. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_preparation.py +13 -0
  5. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_agents/default.md +9 -3
  6. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/executor.py +60 -12
  7. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/adapters/base.py +19 -11
  8. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/adapters/http.py +72 -41
  9. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/commands.py +5 -5
  10. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/compaction_scheduler.py +5 -4
  11. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/scheduler.py +26 -11
  12. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/session_runner.py +1 -1
  13. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/session_store.py +125 -88
  14. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/index.html +4 -4
  15. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/conversation/history.js +36 -26
  16. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/conversation/input.js +1 -1
  17. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/conversation/sessions.js +13 -18
  18. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/conversation/streaming.js +51 -24
  19. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/conversations.js +155 -82
  20. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/events/events.py +2 -0
  21. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/events/helpers.py +17 -2
  22. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/md_agents.py +2 -0
  23. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/schemas/agent.schema.json +12 -0
  24. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/agents.py +44 -43
  25. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/jsonl.py +8 -2
  26. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/.gitignore +0 -0
  27. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/AGENTS.md +0 -0
  28. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/CONTRIBUTING.md +0 -0
  29. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/LICENSE +0 -0
  30. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/README.md +0 -0
  31. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/__init__.py +0 -0
  32. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/__init__.py +0 -0
  33. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/exec_directives.py +0 -0
  34. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/exec_runner.py +0 -0
  35. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/helpers.py +0 -0
  36. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/history_integration.py +0 -0
  37. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/metrics.py +0 -0
  38. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/models.py +0 -0
  39. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/runner.py +0 -0
  40. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_runner/validation.py +0 -0
  41. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/agent_utils.py +0 -0
  42. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/__init__.py +0 -0
  43. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/auto_context.py +0 -0
  44. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/base.py +0 -0
  45. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/file.py +0 -0
  46. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/inline.py +0 -0
  47. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/storage.py +0 -0
  48. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/url.py +0 -0
  49. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/attachments/youtube.py +0 -0
  50. {tsugite_cli-0.14.1/tsugite/builtin_skills → tsugite_cli-0.14.2/tsugite/builtin_agents}/.gitkeep +0 -0
  51. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_agents/code_searcher.md +0 -0
  52. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_agents/file_searcher.md +0 -0
  53. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_agents/onboard.md +0 -0
  54. {tsugite_cli-0.14.1/tsugite/builtin_agents → tsugite_cli-0.14.2/tsugite/builtin_skills}/.gitkeep +0 -0
  55. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  56. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  57. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  58. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
  59. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  60. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  61. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  62. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  63. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cache.py +0 -0
  64. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/__init__.py +0 -0
  65. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/agents.py +0 -0
  66. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/attachments.py +0 -0
  67. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/cache.py +0 -0
  68. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/chat.py +0 -0
  69. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/config.py +0 -0
  70. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/daemon.py +0 -0
  71. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/helpers.py +0 -0
  72. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/history.py +0 -0
  73. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/init.py +0 -0
  74. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/models.py +0 -0
  75. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/plugins.py +0 -0
  76. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/render.py +0 -0
  77. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/run.py +0 -0
  78. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/secrets.py +0 -0
  79. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/skills.py +0 -0
  80. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/tools.py +0 -0
  81. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/usage.py +0 -0
  82. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/validate.py +0 -0
  83. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/cli/workspace.py +0 -0
  84. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/config.py +0 -0
  85. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/console.py +0 -0
  86. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/constants.py +0 -0
  87. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/__init__.py +0 -0
  88. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/agent.py +0 -0
  89. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/claude_code.py +0 -0
  90. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/content_blocks.py +0 -0
  91. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/memory.py +0 -0
  92. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/proxy.py +0 -0
  93. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/sandbox.py +0 -0
  94. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/state.py +0 -0
  95. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/subprocess_executor.py +0 -0
  96. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/core/tools.py +0 -0
  97. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/__init__.py +0 -0
  98. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/adapters/__init__.py +0 -0
  99. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/adapters/discord.py +0 -0
  100. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/adapters/scheduler_adapter.py +0 -0
  101. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/auth.py +0 -0
  102. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/config.py +0 -0
  103. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/gateway.py +0 -0
  104. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/memory.py +0 -0
  105. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/push.py +0 -0
  106. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/css/console.css +0 -0
  107. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/css/responsive.css +0 -0
  108. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/css/styles.css +0 -0
  109. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/css/theme.css +0 -0
  110. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/icons/icon-192.png +0 -0
  111. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
  112. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/icons/icon-512.png +0 -0
  113. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
  114. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
  115. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/api.js +0 -0
  116. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/app.js +0 -0
  117. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/utils.js +0 -0
  118. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/vendor/marked.LICENSE.md +0 -0
  119. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/vendor/marked.esm.min.js +0 -0
  120. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/conversation/attachments.js +0 -0
  121. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/conversation/event_types.js +0 -0
  122. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/file-editor.js +0 -0
  123. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/schedules.js +0 -0
  124. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/usage.js +0 -0
  125. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/webhooks.js +0 -0
  126. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/js/views/workspace.js +0 -0
  127. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/manifest.json +0 -0
  128. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/web/sw.js +0 -0
  129. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/daemon/webhook_store.py +0 -0
  130. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/events/__init__.py +0 -0
  131. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/events/base.py +0 -0
  132. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/events/bus.py +0 -0
  133. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/exceptions.py +0 -0
  134. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/history/__init__.py +0 -0
  135. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/history/models.py +0 -0
  136. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/history/reconstruction.py +0 -0
  137. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/history/storage.py +0 -0
  138. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/hooks.py +0 -0
  139. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/interaction.py +0 -0
  140. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/models.py +0 -0
  141. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/options.py +0 -0
  142. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/plugins.py +0 -0
  143. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/__init__.py +0 -0
  144. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/anthropic.py +0 -0
  145. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/base.py +0 -0
  146. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/claude_code.py +0 -0
  147. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/model_cache.py +0 -0
  148. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/model_registry.py +0 -0
  149. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/ollama.py +0 -0
  150. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/openai_compat.py +0 -0
  151. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/providers/openrouter.py +0 -0
  152. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/renderer.py +0 -0
  153. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/schemas/__init__.py +0 -0
  154. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/__init__.py +0 -0
  155. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/backend.py +0 -0
  156. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/env.py +0 -0
  157. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/exec.py +0 -0
  158. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/file.py +0 -0
  159. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/masking.py +0 -0
  160. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/registry.py +0 -0
  161. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/secrets/sqlite.py +0 -0
  162. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/shell_tool_config.py +0 -0
  163. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/skill_discovery.py +0 -0
  164. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/templates/AGENTS.md +0 -0
  165. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/templates/IDENTITY.md +0 -0
  166. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/templates/MEMORY.md +0 -0
  167. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/templates/USER.md +0 -0
  168. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/templates/personas/casual-technical.md +0 -0
  169. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/templates/personas/marvin.md +0 -0
  170. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/templates/personas/minimal.md +0 -0
  171. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/__init__.py +0 -0
  172. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/fs.py +0 -0
  173. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/history.py +0 -0
  174. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/http.py +0 -0
  175. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/interactive.py +0 -0
  176. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/notify.py +0 -0
  177. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/schedule.py +0 -0
  178. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/scratchpad.py +0 -0
  179. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/secrets.py +0 -0
  180. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/sessions.py +0 -0
  181. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/shell.py +0 -0
  182. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/shell_tools.py +0 -0
  183. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/skills.py +0 -0
  184. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tools/time.py +0 -0
  185. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/tsugite.py +0 -0
  186. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/__init__.py +0 -0
  187. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/base.py +0 -0
  188. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/chat.py +0 -0
  189. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/helpers.py +0 -0
  190. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/live.py +0 -0
  191. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/plain.py +0 -0
  192. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/repl_chat.py +0 -0
  193. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/repl_commands.py +0 -0
  194. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/repl_completer.py +0 -0
  195. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui/repl_handler.py +0 -0
  196. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/ui_context.py +0 -0
  197. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/usage/__init__.py +0 -0
  198. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/usage/store.py +0 -0
  199. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/user_agent.py +0 -0
  200. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/utils.py +0 -0
  201. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/workspace/__init__.py +0 -0
  202. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/workspace/context.py +0 -0
  203. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/workspace/models.py +0 -0
  204. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/workspace/session.py +0 -0
  205. {tsugite_cli-0.14.1 → tsugite_cli-0.14.2}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.14.1
3
+ Version: 0.14.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.14.1"
3
+ version = "0.14.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"
@@ -85,7 +85,7 @@ line-length = 120
85
85
 
86
86
  [tool.pytest.ini_options]
87
87
  testpaths = ["tests", "plugins"]
88
- addopts = "-n auto --dist loadscope --cov=tsugite --cov-report=term-missing --cov-report=html --cov-report=xml"
88
+ addopts = "-n auto --dist loadfile --cov=tsugite --cov-report=term-missing --cov-report=html --cov-report=xml"
89
89
  asyncio_mode = "strict"
90
90
  asyncio_default_fixture_loop_scope = "function"
91
91
  filterwarnings = ["ignore::RuntimeWarning"]
@@ -281,6 +281,7 @@ def merge_scalar_fields(parent, child) -> Dict[str, Any]:
281
281
  "model": child.model if child.model else parent.model,
282
282
  "max_turns": child.max_turns if child.max_turns != 5 else parent.max_turns,
283
283
  "reasoning_effort": child.reasoning_effort if child.reasoning_effort else parent.reasoning_effort,
284
+ "auto_load_agent_list": child.auto_load_agent_list or parent.auto_load_agent_list,
284
285
  }
285
286
 
286
287
 
@@ -321,6 +322,7 @@ def merge_list_fields(parent, child) -> Dict[str, List]:
321
322
  merged_tools = _merge_dedup(parent.tools, child.tools)
322
323
  merged_attachments = _merge_attachments(parent.attachments, child.attachments)
323
324
  merged_skills = _merge_dedup(parent.auto_load_skills, child.auto_load_skills)
325
+ merged_auto_load_agents = _merge_dedup(parent.auto_load_agents, child.auto_load_agents)
324
326
 
325
327
  # Custom tools - deduplicate by "name" field (child overrides parent)
326
328
  custom_tool_dict = {}
@@ -333,6 +335,7 @@ def merge_list_fields(parent, child) -> Dict[str, List]:
333
335
  "prefetch": (parent.prefetch or []) + (child.prefetch or []),
334
336
  "custom_tools": list(custom_tool_dict.values()),
335
337
  "auto_load_skills": merged_skills,
338
+ "auto_load_agents": merged_auto_load_agents,
336
339
  }
337
340
 
338
341
 
@@ -547,6 +547,19 @@ class AgentPreparer:
547
547
  # Silently continue if prefetch fails
548
548
  prefetch_context = {}
549
549
 
550
+ # Step 1b: Opt-in <available_agents> injection. Default agents call
551
+ # list_available_agents() on demand instead of carrying the full list.
552
+ if "available_agents" not in prefetch_context and (
553
+ agent_config.auto_load_agent_list or agent_config.auto_load_agents
554
+ ):
555
+ from tsugite.tools.agents import discover_agents, format_agents_markdown
556
+
557
+ agents = discover_agents()
558
+ if agent_config.auto_load_agents:
559
+ wanted = set(agent_config.auto_load_agents)
560
+ agents = [a for a in agents if a["name"] in wanted]
561
+ prefetch_context["available_agents"] = format_agents_markdown(agents)
562
+
550
563
  # Step 2: Execute tool directives (unless skip_tool_directives=True for render)
551
564
  if skip_tool_directives:
552
565
  modified_content = agent.content
@@ -15,6 +15,7 @@ attachments:
15
15
  max_turns: 10
16
16
  tools:
17
17
  - spawn_agent
18
+ - list_available_agents
18
19
  - read_file
19
20
  - list_files
20
21
  - write_file
@@ -36,9 +37,6 @@ tools:
36
37
  auto_load_skills:
37
38
  - response-patterns
38
39
  prefetch:
39
- - tool: list_agents
40
- args: {}
41
- assign: available_agents
42
40
  - tool: get_skills_for_template
43
41
  args: {}
44
42
  assign: available_skills
@@ -54,6 +52,14 @@ instructions: |
54
52
  Before starting work, check the `<available_skills>` section and load one if it matches.
55
53
  Call `load_skill("skill_name")` and wait for the next turn before proceeding.
56
54
 
55
+ ## Delegating to Sub-Agents
56
+
57
+ The list of available sub-agents is not auto-attached to your context. When a task
58
+ clearly benefits from a specialized agent (and isn't something you can handle inline),
59
+ call `list_available_agents()` to discover them, then `spawn_agent(path, prompt)` to
60
+ delegate. Skip both for simple tasks or single-file edits - most work doesn't need
61
+ delegation.
62
+
57
63
  ## Guidelines
58
64
 
59
65
  - Be concise and direct in your responses
@@ -8,18 +8,47 @@ injected `state` object persist across turns.
8
8
  """
9
9
 
10
10
  import ast
11
+ import contextlib
11
12
  import io
13
+ import os
12
14
  import pprint
13
15
  import sys
16
+ import threading
14
17
  from dataclasses import dataclass, field
15
18
  from pathlib import Path
16
- from typing import Any, Callable, Dict, List, Optional
19
+ from typing import Any, Callable, Dict, Iterator, List, Optional
17
20
 
18
21
  from tsugite.core.state import load_state, save_state
19
22
  from tsugite.exceptions import StateSerializationError
20
23
 
21
24
  PPRINT_WIDTH = 100
22
25
 
26
+ # CWD is process-global. Concurrent execute() calls in different threads must
27
+ # serialize their chdir+exec+restore window; the lock is held across the whole
28
+ # user-code exec(), so heavy parallel LocalExecutor turns serialize end-to-end.
29
+ # Accepted trade-off: silent disagreement between raw `os.getcwd()` and tool
30
+ # path resolution is worse than the serialization. `spawn_session` returns
31
+ # immediately so a parent holding the lock doesn't starve a nested child.
32
+ _chdir_lock = threading.Lock()
33
+
34
+
35
+ @contextlib.contextmanager
36
+ def _locked_chdir(target: Optional[Path]) -> Iterator[None]:
37
+ """chdir to target under `_chdir_lock`; restore CWD and release on exit.
38
+
39
+ No-op when target is None.
40
+ """
41
+ if target is None:
42
+ yield
43
+ return
44
+ with _chdir_lock:
45
+ previous = os.getcwd()
46
+ os.chdir(str(target))
47
+ try:
48
+ yield
49
+ finally:
50
+ os.chdir(previous)
51
+
23
52
 
24
53
  def _looks_html_escaped(source: str) -> bool:
25
54
  """True if `source` is HTML-entity-escaped XML (observation content leaked into exec)."""
@@ -203,6 +232,11 @@ class LocalExecutor:
203
232
  self._sticky_injections: Dict[str, Any] = {}
204
233
  self._content_blocks: Dict[str, str] = {}
205
234
 
235
+ target = workspace_dir
236
+ if target is None and path_context is not None:
237
+ target = path_context.effective_cwd or path_context.workspace_dir
238
+ self._chdir_target_resolved: Optional[Path] = Path(target).resolve() if target is not None else None
239
+
206
240
  self.namespace: Dict[str, Any] = self._build_turn_namespace()
207
241
 
208
242
  def _build_turn_namespace(self) -> Dict[str, Any]:
@@ -317,11 +351,24 @@ class LocalExecutor:
317
351
  return pprint.pformat(value, width=PPRINT_WIDTH, compact=False)
318
352
  return repr(value)
319
353
 
354
+ def _workspace_chdir_target(self) -> Optional[Path]:
355
+ """Target for the per-execute chdir, or None if CWD already matches (or no workspace)."""
356
+ target = self._chdir_target_resolved
357
+ if target is None:
358
+ return None
359
+ try:
360
+ if target == Path.cwd().resolve():
361
+ return None
362
+ except FileNotFoundError:
363
+ pass
364
+ return target
365
+
320
366
  async def execute(self, code: str) -> ExecutionResult:
321
367
  """Execute code using exec().
322
368
 
323
369
  Automatically displays the value of the last expression (REPL-like behavior).
324
- CWD is managed at CLI level - no directory changes here.
370
+ When a workspace is bound, chdir to it under a process-wide lock so raw
371
+ Python file APIs (os.getcwd, open, Path.cwd) resolve against the workspace.
325
372
 
326
373
  Args:
327
374
  code: Python code to execute
@@ -367,19 +414,20 @@ class LocalExecutor:
367
414
  sys.stdout = stdout_capture
368
415
  sys.stderr = stderr_capture
369
416
 
370
- setup_code, last_expr = self._split_code_for_last_expr(code)
417
+ with _locked_chdir(self._workspace_chdir_target()):
418
+ setup_code, last_expr = self._split_code_for_last_expr(code)
371
419
 
372
- if last_expr:
373
- if setup_code.strip():
374
- exec(setup_code, self.namespace)
420
+ if last_expr:
421
+ if setup_code.strip():
422
+ exec(setup_code, self.namespace)
375
423
 
376
- result = eval(last_expr, self.namespace)
424
+ result = eval(last_expr, self.namespace)
377
425
 
378
- if result is not None:
379
- formatted = self._format_value(result)
380
- print(formatted)
381
- else:
382
- exec(code, self.namespace)
426
+ if result is not None:
427
+ formatted = self._format_value(result)
428
+ print(formatted)
429
+ else:
430
+ exec(code, self.namespace)
383
431
 
384
432
  except Exception as e:
385
433
  exec_error = f"{type(e).__name__}: {str(e)}"
@@ -83,7 +83,7 @@ _BROADCAST_SKIP_EVENTS = frozenset({"stream_chunk", "stream_complete", "prompt_s
83
83
 
84
84
  # Event types persisted to the session JSONL by the SSE handler so the web UI
85
85
  # can replay them after a reload (the agent already records execution events).
86
- _PERSIST_EVENT_TYPES = frozenset({"prompt_snapshot", "reaction", "final_result", "error", "cancelled"})
86
+ _PERSIST_EVENT_TYPES = frozenset({"prompt_snapshot", "reaction", "final_result", "error", "cancelled", "info"})
87
87
 
88
88
 
89
89
  class SSEBroadcastHandler(JSONLUIHandler):
@@ -355,12 +355,17 @@ class BaseAdapter(ABC):
355
355
  if custom_logger and hasattr(custom_logger, "ui_handler"):
356
356
  custom_logger.ui_handler._emit(event_type, {})
357
357
 
358
- def _broadcast_compaction(self, event_type: str, agent: str, **payload: Any) -> None:
359
- """Broadcast a compaction lifecycle/progress event to SSE subscribers."""
358
+ def _broadcast_compaction(self, event_type: str, agent: str, session_id: str, **payload: Any) -> None:
359
+ """Broadcast a compaction lifecycle/progress event to SSE subscribers.
360
+
361
+ session_id is required so per-session UI state (the "compacting…"
362
+ spinner, composer-disabled flag) can scope itself to the actual
363
+ compacting session instead of bleeding to every session in the agent.
364
+ """
360
365
  if not self.event_bus:
361
366
  return
362
367
  try:
363
- self.event_bus.emit(event_type, {"agent": agent, **payload})
368
+ self.event_bus.emit(event_type, {"agent": agent, "session_id": session_id, **payload})
364
369
  except Exception:
365
370
  logger.debug("Failed to broadcast %s", event_type)
366
371
 
@@ -511,7 +516,10 @@ class BaseAdapter(ABC):
511
516
  if not self.event_bus:
512
517
  return
513
518
  try:
514
- self.event_bus.emit("history_update", {"agent": self.agent_name})
519
+ payload: Dict[str, Any] = {"agent": self.agent_name}
520
+ if conv_id:
521
+ payload["session_id"] = conv_id
522
+ self.event_bus.emit("history_update", payload)
515
523
  if conv_id:
516
524
  self.event_bus.emit("session_update", {"action": "updated", "id": conv_id})
517
525
  except Exception as e:
@@ -740,18 +748,18 @@ class BaseAdapter(ABC):
740
748
  for non-default or non-interactive sources.
741
749
  """
742
750
  new_session: Optional[Session] = None
743
- if self.session_store.begin_compaction(user_id, self.agent_name):
751
+ if self.session_store.begin_compaction(user_id, self.agent_name, session_id=conv_id):
744
752
  self._emit_ui(custom_logger, "compacting")
745
- self._broadcast_compaction("compaction_started", self.agent_name)
753
+ self._broadcast_compaction("compaction_started", self.agent_name, conv_id)
746
754
 
747
755
  def progress_cb(payload: Dict[str, Any]) -> None:
748
- self._broadcast_compaction("compaction_progress", self.agent_name, **payload)
756
+ self._broadcast_compaction("compaction_progress", self.agent_name, conv_id, **payload)
749
757
 
750
758
  try:
751
759
  new_session = await self._compact_session(conv_id, reason=reason, progress_callback=progress_cb)
752
760
  finally:
753
- self.session_store.end_compaction(user_id, self.agent_name)
754
- self._broadcast_compaction("compaction_finished", self.agent_name)
761
+ self.session_store.end_compaction(user_id, self.agent_name, session_id=conv_id)
762
+ self._broadcast_compaction("compaction_finished", self.agent_name, conv_id)
755
763
  else:
756
764
  self._emit_ui(custom_logger, "compacting_waiting")
757
765
  done = await asyncio.to_thread(self.session_store.wait_for_compaction, user_id, self.agent_name)
@@ -836,7 +844,7 @@ class BaseAdapter(ABC):
836
844
  self.session_store.drop_sticky(conv_id, name)
837
845
  if self.event_bus:
838
846
  try:
839
- self.event_bus.emit(SkillUnloadedEvent(skill_name=name))
847
+ self.event_bus.emit(SkillUnloadedEvent(skill_name=name, session_id=conv_id))
840
848
  except Exception:
841
849
  logger.debug("Failed to emit SkillUnloadedEvent for %s", name, exc_info=True)
842
850
  except Exception:
@@ -7,12 +7,15 @@ import mimetypes
7
7
  import re
8
8
  import shutil
9
9
  import threading
10
- from dataclasses import asdict
10
+ from dataclasses import asdict, dataclass
11
11
  from dataclasses import fields as dataclass_fields
12
12
  from datetime import datetime, timezone
13
13
  from pathlib import Path
14
14
  from types import SimpleNamespace
15
- from typing import Any, Callable, Optional
15
+ from typing import TYPE_CHECKING, Any, Callable, Optional
16
+
17
+ if TYPE_CHECKING:
18
+ from tsugite.daemon.session_store import SessionStore
16
19
 
17
20
  from starlette.applications import Starlette
18
21
  from starlette.requests import Request
@@ -23,7 +26,7 @@ from starlette.staticfiles import StaticFiles
23
26
  from tsugite.agent_inheritance import get_builtin_agents_path, get_global_agents_paths
24
27
  from tsugite.attachments.base import AttachmentContentType
25
28
  from tsugite.attachments.file import FileHandler
26
- from tsugite.daemon.adapters.base import BaseAdapter, ChannelContext
29
+ from tsugite.daemon.adapters.base import _PERSIST_EVENT_TYPES, BaseAdapter, ChannelContext
27
30
  from tsugite.daemon.config import AgentConfig, HTTPConfig
28
31
  from tsugite.daemon.scheduler import ScheduleEntry
29
32
  from tsugite.daemon.webhook_store import WebhookStore
@@ -36,6 +39,27 @@ from tsugite.utils import parse_yaml_frontmatter
36
39
  WEB_DIR = Path(__file__).resolve().parent.parent / "web"
37
40
 
38
41
 
42
+ def build_session_event_persister(session_store: "SessionStore", session_id: str) -> Callable:
43
+ """Persist selected agent events to the per-session JSONL, and sync
44
+ `Session.cumulative_tokens` to `prompt_snapshot.token_breakdown.total` so
45
+ the UI badge matches the prompt inspector. See `tests/test_displayed_token_count.py`
46
+ for the failure modes this addresses.
47
+ """
48
+
49
+ def _persist(payload: dict[str, Any]) -> None:
50
+ session_store.append_event(
51
+ session_id,
52
+ {**payload, "timestamp": datetime.now(timezone.utc).isoformat()},
53
+ )
54
+ if payload.get("type") != "prompt_snapshot":
55
+ return
56
+ total = (payload.get("token_breakdown") or {}).get("total")
57
+ if isinstance(total, int) and total > 0:
58
+ session_store.set_cumulative_tokens(session_id, total)
59
+
60
+ return _persist
61
+
62
+
39
63
  def _resolve_full_model_id(model: str) -> str:
40
64
  """Return 'provider:full-model-id' so UI can show e.g. claude_code:opus -> claude-opus-4-7.
41
65
 
@@ -243,9 +267,9 @@ async def sse_stream(queue: asyncio.Queue, keepalive_interval: float = 15.0):
243
267
  # fires history_update on the global event bus after the turn settles, which
244
268
  # triggers loadHistory() in those tabs and rebuilds the message list from JSONL.
245
269
  # Broadcasting turn-end events too would race the active tab's per-chat reader
246
- # (sendingBySession is cleared in streaming.js's finally block, leaving a window
247
- # where late-arriving session_event(final_result) bypasses the dedup guard and
248
- # pushes a duplicate bubble until the next reload).
270
+ # (the session's `sending` flag in sessionsState is cleared in streaming.js's
271
+ # finally block, leaving a window where late-arriving session_event(final_result)
272
+ # bypasses the dedup guard and pushes a duplicate bubble until the next reload).
249
273
  _BROADCAST_SKIP_EVENTS = frozenset(
250
274
  {
251
275
  "stream_chunk",
@@ -312,7 +336,7 @@ class SSEProgressHandler(JSONLUIHandler):
312
336
  else:
313
337
  self.queue.put_nowait(payload)
314
338
 
315
- if event_type in ("prompt_snapshot", "reaction", "final_result", "error", "cancelled") and self._persist_event:
339
+ if event_type in _PERSIST_EVENT_TYPES and self._persist_event:
316
340
  self._persist_event(payload)
317
341
 
318
342
  if self._broadcaster and self._session_id and event_type not in _BROADCAST_SKIP_EVENTS:
@@ -393,6 +417,20 @@ class HTTPAgentAdapter(BaseAdapter):
393
417
  pass
394
418
 
395
419
 
420
+ @dataclass
421
+ class ActiveChat:
422
+ """In-flight chat interaction. One per (agent, user, session_id) triple.
423
+
424
+ Consolidates what used to be three parallel maps keyed by the same triple
425
+ so adding a fourth piece of per-chat state doesn't mean adding a fourth map
426
+ and remembering to keep their lifecycles in sync.
427
+ """
428
+
429
+ backend: HTTPInteractionBackend
430
+ progress: SSEProgressHandler
431
+ task: Optional[asyncio.Task] = None
432
+
433
+
396
434
  class HTTPServer:
397
435
  """Runs a Starlette ASGI app with uvicorn for the HTTP API."""
398
436
 
@@ -416,9 +454,7 @@ class HTTPServer:
416
454
  self.session_runner = None # Set by Gateway after SessionRunner is created
417
455
  self.push_store = None # Set by Gateway if web-push is configured
418
456
  self.vapid_public_key = None # Set by Gateway if web-push is configured
419
- self._active_backends: dict[tuple[str, str, str], HTTPInteractionBackend] = {}
420
- self._active_chat_tasks: dict[tuple[str, str, str], asyncio.Task] = {}
421
- self._active_progress: dict[tuple[str, str, str], SSEProgressHandler] = {}
457
+ self._active_chats: dict[tuple[str, str, str], ActiveChat] = {}
422
458
  self.event_bus = SSEBroadcaster()
423
459
  self.app = self._build_app()
424
460
 
@@ -650,7 +686,7 @@ class HTTPServer:
650
686
  if err := self._check_auth(request):
651
687
  return err
652
688
  running_by_agent: dict[str, set[str]] = {}
653
- for agent_name, _user_id, session_id in self._active_backends:
689
+ for agent_name, _user_id, session_id in self._active_chats:
654
690
  running_by_agent.setdefault(agent_name, set()).add(session_id)
655
691
  agents = [
656
692
  {
@@ -790,7 +826,8 @@ class HTTPServer:
790
826
  tokens = session.cumulative_tokens
791
827
  message_count = session.message_count
792
828
  session_metadata = session.metadata or {}
793
- backend = self._active_backends.get((adapter.agent_name, user_id, session.id))
829
+ chat = self._active_chats.get((adapter.agent_name, user_id, session.id))
830
+ backend = chat.backend if chat else None
794
831
  else:
795
832
  tokens, message_count, session_metadata, backend = 0, 0, {}, None
796
833
 
@@ -802,7 +839,9 @@ class HTTPServer:
802
839
  "context_limit": adapter.session_store.get_context_limit(adapter.agent_name),
803
840
  "threshold": adapter.session_store.get_compaction_threshold(adapter.agent_name),
804
841
  "message_count": message_count,
805
- "compacting": adapter.session_store.is_compacting(user_id, adapter.agent_name),
842
+ "compacting": adapter.session_store.is_compacting(
843
+ user_id, adapter.agent_name, session_id=session.id if session else None
844
+ ),
806
845
  "metadata": session_metadata,
807
846
  "busy": backend is not None,
808
847
  "pending_message": backend.pending_message if backend else None,
@@ -950,7 +989,8 @@ class HTTPServer:
950
989
  breakdown = snapshots[-1].get("token_breakdown", {}) if snapshots else {}
951
990
 
952
991
  backend_key = (agent_name, user_id, session_id)
953
- live_progress = self._active_progress.get(backend_key)
992
+ chat = self._active_chats.get(backend_key)
993
+ live_progress = chat.progress if chat else None
954
994
  if live_progress and live_progress.latest_prompt_messages:
955
995
  return JSONResponse(
956
996
  {
@@ -993,10 +1033,10 @@ class HTTPServer:
993
1033
  agent_name = request.path_params["agent"]
994
1034
  old_conv_id = session.id
995
1035
 
996
- if not adapter.session_store.begin_compaction(user_id, adapter.agent_name):
1036
+ if not adapter.session_store.begin_compaction(user_id, adapter.agent_name, session_id=old_conv_id):
997
1037
  return JSONResponse({"error": "compaction already in progress"}, status_code=409)
998
1038
 
999
- adapter._broadcast_compaction("compaction_started", agent_name)
1039
+ adapter._broadcast_compaction("compaction_started", agent_name, old_conv_id)
1000
1040
  new_session = None
1001
1041
  try:
1002
1042
  instructions = body.get("instructions")
@@ -1006,8 +1046,8 @@ class HTTPServer:
1006
1046
  logger.exception("Compaction failed for agent %s", adapter.agent_name)
1007
1047
  return JSONResponse({"error": f"compaction failed: {msg}"}, status_code=500)
1008
1048
  finally:
1009
- adapter.session_store.end_compaction(user_id, adapter.agent_name)
1010
- adapter._broadcast_compaction("compaction_finished", agent_name)
1049
+ adapter.session_store.end_compaction(user_id, adapter.agent_name, session_id=old_conv_id)
1050
+ adapter._broadcast_compaction("compaction_finished", agent_name, old_conv_id)
1011
1051
 
1012
1052
  self.event_bus.emit("agent_status", {"agent": agent_name})
1013
1053
  if new_session:
@@ -1220,11 +1260,11 @@ class HTTPServer:
1220
1260
  logger.info("[%s] respond from user_id=%s session_id=%s", agent_name, user_id, session_id)
1221
1261
 
1222
1262
  key = (agent_name, user_id, session_id)
1223
- backend = self._active_backends.get(key)
1224
- if not backend:
1263
+ chat = self._active_chats.get(key)
1264
+ if not chat:
1225
1265
  return JSONResponse({"error": "no pending question for this session"}, status_code=404)
1226
1266
 
1227
- backend.submit_response(response)
1267
+ chat.backend.submit_response(response)
1228
1268
  return JSONResponse({"status": "ok"})
1229
1269
 
1230
1270
  async def _upload(self, request: Request) -> JSONResponse:
@@ -1370,7 +1410,8 @@ class HTTPServer:
1370
1410
  target_session_id = target_session.id
1371
1411
 
1372
1412
  backend_key = (agent_name, user_id, target_session_id)
1373
- existing_task = self._active_chat_tasks.get(backend_key)
1413
+ existing_chat = self._active_chats.get(backend_key)
1414
+ existing_task = existing_chat.task if existing_chat else None
1374
1415
  if existing_task is not None and not existing_task.done():
1375
1416
  return JSONResponse(
1376
1417
  {"error": "a turn is already running for this session"},
@@ -1389,21 +1430,13 @@ class HTTPServer:
1389
1430
  progress.set_loop(asyncio.get_running_loop())
1390
1431
  progress.set_session_id(target_session_id)
1391
1432
  progress.set_broadcaster(self.event_bus)
1392
- progress.set_event_persister(
1393
- lambda payload: adapter.session_store.append_event(
1394
- target_session_id,
1395
- {
1396
- **payload,
1397
- "timestamp": datetime.now(timezone.utc).isoformat(),
1398
- },
1399
- )
1400
- )
1433
+ progress.set_event_persister(build_session_event_persister(adapter.session_store, target_session_id))
1401
1434
  custom_logger = SimpleNamespace(ui_handler=progress)
1402
1435
 
1403
1436
  interaction_backend = HTTPInteractionBackend(progress)
1404
1437
  interaction_backend.pending_message = message
1405
- self._active_backends[backend_key] = interaction_backend
1406
- self._active_progress[backend_key] = progress
1438
+ chat_state = ActiveChat(backend=interaction_backend, progress=progress)
1439
+ self._active_chats[backend_key] = chat_state
1407
1440
 
1408
1441
  async def run_agent():
1409
1442
  from tsugite.interaction import set_interaction_backend
@@ -1424,7 +1457,7 @@ class HTTPServer:
1424
1457
  progress._emit("final_result", {"result": response})
1425
1458
 
1426
1459
  self.event_bus.emit("agent_status", {"agent": agent_name})
1427
- self.event_bus.emit("history_update", {"agent": agent_name})
1460
+ self.event_bus.emit("history_update", {"agent": agent_name, "session_id": target_session_id})
1428
1461
 
1429
1462
  try:
1430
1463
  refreshed = adapter.session_store.get_session(target_session_id)
@@ -1450,13 +1483,11 @@ class HTTPServer:
1450
1483
  logger.exception("[%s] Chat error", adapter.agent_name)
1451
1484
  progress._emit("error", {"error": str(e)})
1452
1485
  finally:
1453
- self._active_backends.pop(backend_key, None)
1454
- self._active_chat_tasks.pop(backend_key, None)
1455
- self._active_progress.pop(backend_key, None)
1486
+ self._active_chats.pop(backend_key, None)
1456
1487
  progress.signal_done()
1457
1488
 
1458
1489
  task = asyncio.create_task(run_agent())
1459
- self._active_chat_tasks[backend_key] = task
1490
+ chat_state.task = task
1460
1491
 
1461
1492
  return StreamingResponse(
1462
1493
  progress.event_generator(),
@@ -1483,9 +1514,9 @@ class HTTPServer:
1483
1514
  if not session_id:
1484
1515
  return JSONResponse({"error": "session_id is required"}, status_code=400)
1485
1516
  backend_key = (request.path_params["agent"], user_id, session_id)
1486
- task = self._active_chat_tasks.get(backend_key)
1487
- if task and not task.done():
1488
- task.cancel()
1517
+ chat = self._active_chats.get(backend_key)
1518
+ if chat and chat.task and not chat.task.done():
1519
+ chat.task.cancel()
1489
1520
  return JSONResponse({"status": "cancelled"})
1490
1521
  return JSONResponse({"error": "no active chat"}, status_code=404)
1491
1522
 
@@ -110,19 +110,19 @@ async def cmd_compact(adapter: BaseAdapter, user_id: str, message: str | None =
110
110
  if session is None or session.message_count == 0:
111
111
  return "No conversation to compact."
112
112
 
113
- if not adapter.session_store.begin_compaction(user_id, adapter.agent_name):
113
+ old_id = session.id
114
+ if not adapter.session_store.begin_compaction(user_id, adapter.agent_name, session_id=old_id):
114
115
  return "Compaction already in progress."
115
116
 
116
- old_id = session.id
117
- adapter._broadcast_compaction("compaction_started", adapter.agent_name)
117
+ adapter._broadcast_compaction("compaction_started", adapter.agent_name, old_id)
118
118
  new_session = None
119
119
  try:
120
120
  new_session = await adapter._compact_session(session.id, instructions=message, reason="manual")
121
121
  except Exception as e:
122
122
  return f"Compaction failed: {e}"
123
123
  finally:
124
- adapter.session_store.end_compaction(user_id, adapter.agent_name)
125
- adapter._broadcast_compaction("compaction_finished", adapter.agent_name)
124
+ adapter.session_store.end_compaction(user_id, adapter.agent_name, session_id=old_id)
125
+ adapter._broadcast_compaction("compaction_finished", adapter.agent_name, old_id)
126
126
 
127
127
  if new_session is None:
128
128
  return f"Nothing to compact (id: {old_id[:12]})"
@@ -112,7 +112,8 @@ class CompactionScheduler:
112
112
  continue
113
113
 
114
114
  user_id = session.user_id or ""
115
- if not self._session_store.begin_compaction(user_id, agent_name):
115
+ sid = session.id
116
+ if not self._session_store.begin_compaction(user_id, agent_name, session_id=sid):
116
117
  logger.debug("[%s] Compaction already in progress, skipping", agent_name)
117
118
  continue
118
119
 
@@ -123,17 +124,17 @@ class CompactionScheduler:
123
124
  )
124
125
 
125
126
  try:
126
- new_session = await adapter._compact_session(session.id, reason="scheduled")
127
+ new_session = await adapter._compact_session(sid, reason="scheduled")
127
128
  if new_session is None:
128
129
  logger.info("[%s] Scheduled compaction skipped (nothing to compact)", agent_name)
129
130
  else:
130
131
  logger.info(
131
132
  "[%s] Scheduled compaction completed (old=%s new=%s)",
132
133
  agent_name,
133
- session.id,
134
+ sid,
134
135
  new_session.id,
135
136
  )
136
137
  except Exception:
137
138
  logger.exception("[%s] Scheduled compaction failed", agent_name)
138
139
  finally:
139
- self._session_store.end_compaction(user_id, agent_name)
140
+ self._session_store.end_compaction(user_id, agent_name, session_id=sid)