tsugite-cli 0.9.1__tar.gz → 0.9.4__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 (283) hide show
  1. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/PKG-INFO +2 -2
  2. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/pyproject.toml +2 -2
  3. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_discord_progress.py +25 -0
  4. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_history_integration.py +174 -0
  5. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_history_tools.py +19 -0
  6. tsugite_cli-0.9.4/tests/test_session_orchestrator_tools.py +150 -0
  7. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_runner/history_integration.py +4 -0
  8. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_runner/runner.py +7 -0
  9. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_agents/default.md +1 -0
  10. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/response_patterns.md +14 -1
  11. tsugite_cli-0.9.4/tsugite/cli/history.py +411 -0
  12. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/run.py +31 -21
  13. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/agent.py +3 -4
  14. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/executor.py +14 -0
  15. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/subprocess_executor.py +21 -5
  16. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/adapters/base.py +57 -37
  17. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/adapters/discord.py +26 -2
  18. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/adapters/http.py +112 -57
  19. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/memory.py +10 -18
  20. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/session_runner.py +10 -5
  21. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/session_store.py +36 -3
  22. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/css/responsive.css +12 -24
  23. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/css/styles.css +6 -1
  24. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/index.html +82 -64
  25. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/app.js +18 -0
  26. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/views/conversations.js +72 -2
  27. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/views/workspace.js +32 -15
  28. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/manifest.json +1 -0
  29. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/events/__init__.py +3 -0
  30. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/events/base.py +3 -0
  31. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/events/events.py +8 -0
  32. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/history/__init__.py +2 -0
  33. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/history/models.py +14 -1
  34. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/history/storage.py +71 -5
  35. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/__init__.py +22 -7
  36. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/history.py +86 -29
  37. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/interactive.py +30 -1
  38. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/sessions.py +1 -1
  39. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/base.py +7 -0
  40. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/jsonl.py +5 -0
  41. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/repl_handler.py +7 -0
  42. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/uv.lock +2 -2
  43. tsugite_cli-0.9.1/tsugite/cli/history.py +0 -205
  44. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/.github/copilot-instructions.md +0 -0
  45. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/.github/workflows/ci.yml +0 -0
  46. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/.github/workflows/docker-publish.yml +0 -0
  47. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/.github/workflows/pypi-publish.yml +0 -0
  48. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/.gitignore +0 -0
  49. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/AGENTS.md +0 -0
  50. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/CLAUDE.md +0 -0
  51. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/LICENSE +0 -0
  52. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/README.md +0 -0
  53. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/scripts/regenerate_schema.py +0 -0
  54. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/README.md +0 -0
  55. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/__init__.py +0 -0
  56. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/conftest.py +0 -0
  57. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/__init__.py +0 -0
  58. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_agent.py +0 -0
  59. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_agent_ui_events.py +0 -0
  60. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_content_blocks.py +0 -0
  61. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_executor.py +0 -0
  62. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_memory.py +0 -0
  63. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_proxy.py +0 -0
  64. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_sandbox.py +0 -0
  65. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_subprocess_executor.py +0 -0
  66. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/core/test_tools.py +0 -0
  67. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/daemon/__init__.py +0 -0
  68. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/daemon/test_http_adapter.py +0 -0
  69. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/events/test_event_consolidation.py +0 -0
  70. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/smoke_test.sh +0 -0
  71. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_agent_file_hot_loading.py +0 -0
  72. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_agent_inheritance.py +0 -0
  73. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_agent_parser.py +0 -0
  74. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_agent_sessions.py +0 -0
  75. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_agent_skills.py +0 -0
  76. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_agent_utils.py +0 -0
  77. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_agents_tool.py +0 -0
  78. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_attachment_deduplication.py +0 -0
  79. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_attachments.py +0 -0
  80. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_auto_context_handler.py +0 -0
  81. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_auto_discovery.py +0 -0
  82. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_background_task_status.py +0 -0
  83. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_background_tasks.py +0 -0
  84. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_builtin_agent_paths.py +0 -0
  85. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_builtin_agents.py +0 -0
  86. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_cache_control.py +0 -0
  87. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_chat_cli.py +0 -0
  88. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_chat_error_handling.py +0 -0
  89. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_claude_code_attachments.py +0 -0
  90. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_claude_code_provider.py +0 -0
  91. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_cli.py +0 -0
  92. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_cli_arguments.py +0 -0
  93. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_cli_rendering.py +0 -0
  94. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_cli_subcommands.py +0 -0
  95. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_config.py +0 -0
  96. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_continuation.py +0 -0
  97. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_custom_shell_tools.py +0 -0
  98. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_custom_ui.py +0 -0
  99. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_compaction_scheduler.py +0 -0
  100. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_config.py +0 -0
  101. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_history_persistence.py +0 -0
  102. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_memory.py +0 -0
  103. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_push.py +0 -0
  104. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_scheduler.py +0 -0
  105. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_session_isolation.py +0 -0
  106. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_daemon_unified_sessions.py +0 -0
  107. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_error_display.py +0 -0
  108. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_file_references.py +0 -0
  109. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_file_tools.py +0 -0
  110. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_history.py +0 -0
  111. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_history_models.py +0 -0
  112. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_history_performance.py +0 -0
  113. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_hooks.py +0 -0
  114. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_http_tools.py +0 -0
  115. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_interaction_backends.py +0 -0
  116. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_interactive_context.py +0 -0
  117. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_interactive_tool.py +0 -0
  118. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_jsonl_ui.py +0 -0
  119. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_kvstore.py +0 -0
  120. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_list_agents_tool.py +0 -0
  121. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_mcp_client.py +0 -0
  122. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_mcp_server.py +0 -0
  123. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_models.py +0 -0
  124. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_multi_agent.py +0 -0
  125. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_multistep_agents.py +0 -0
  126. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_orchestrator_heartbeat.py +0 -0
  127. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_plugins.py +0 -0
  128. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_reasoning_models.py +0 -0
  129. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_renderer.py +0 -0
  130. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_rendering_scenarios.py +0 -0
  131. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_repl_commands.py +0 -0
  132. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_repl_completer.py +0 -0
  133. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_repl_handler.py +0 -0
  134. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_retry_system.py +0 -0
  135. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_run_if.py +0 -0
  136. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_schedule_model_override.py +0 -0
  137. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_scheduler_history_injection.py +0 -0
  138. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_schema.py +0 -0
  139. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_security_phase1.py +0 -0
  140. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_send_message.py +0 -0
  141. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_skill_discovery.py +0 -0
  142. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_skill_tools.py +0 -0
  143. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_stdin.py +0 -0
  144. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_subagent_subprocess.py +0 -0
  145. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_tmux_tools.py +0 -0
  146. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_tool_directives.py +0 -0
  147. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_tool_registry.py +0 -0
  148. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_workspace_auto_continue.py +0 -0
  149. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_workspace_cwd.py +0 -0
  150. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tests/test_workspace_discovery.py +0 -0
  151. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/__init__.py +0 -0
  152. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_inheritance.py +0 -0
  153. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_preparation.py +0 -0
  154. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_runner/__init__.py +0 -0
  155. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_runner/helpers.py +0 -0
  156. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_runner/metrics.py +0 -0
  157. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_runner/models.py +0 -0
  158. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_runner/validation.py +0 -0
  159. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/agent_utils.py +0 -0
  160. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/__init__.py +0 -0
  161. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/auto_context.py +0 -0
  162. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/base.py +0 -0
  163. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/file.py +0 -0
  164. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/inline.py +0 -0
  165. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/storage.py +0 -0
  166. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/url.py +0 -0
  167. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/attachments/youtube.py +0 -0
  168. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_agents/.gitkeep +0 -0
  169. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_agents/code_searcher.md +0 -0
  170. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_agents/file_searcher.md +0 -0
  171. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_agents/onboard.md +0 -0
  172. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/.gitkeep +0 -0
  173. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/codebase_exploration.md +0 -0
  174. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/python_math.md +0 -0
  175. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/scheduling.md +0 -0
  176. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/skill_authoring.md +0 -0
  177. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/tsugite_agent_basics.md +0 -0
  178. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/tsugite_jinja_reference.md +0 -0
  179. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/builtin_skills/tsugite_skill_basics.md +0 -0
  180. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cache.py +0 -0
  181. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/__init__.py +0 -0
  182. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/agents.py +0 -0
  183. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/attachments.py +0 -0
  184. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/cache.py +0 -0
  185. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/chat.py +0 -0
  186. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/config.py +0 -0
  187. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/daemon.py +0 -0
  188. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/helpers.py +0 -0
  189. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/init.py +0 -0
  190. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/mcp.py +0 -0
  191. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/plugins.py +0 -0
  192. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/render.py +0 -0
  193. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/serve.py +0 -0
  194. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/tools.py +0 -0
  195. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/validate.py +0 -0
  196. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/cli/workspace.py +0 -0
  197. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/config.py +0 -0
  198. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/console.py +0 -0
  199. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/constants.py +0 -0
  200. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/__init__.py +0 -0
  201. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/claude_code.py +0 -0
  202. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/content_blocks.py +0 -0
  203. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/memory.py +0 -0
  204. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/proxy.py +0 -0
  205. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/sandbox.py +0 -0
  206. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/core/tools.py +0 -0
  207. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/__init__.py +0 -0
  208. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/adapters/__init__.py +0 -0
  209. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/adapters/scheduler_adapter.py +0 -0
  210. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/commands.py +0 -0
  211. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/compaction_scheduler.py +0 -0
  212. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/config.py +0 -0
  213. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/gateway.py +0 -0
  214. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/push.py +0 -0
  215. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/scheduler.py +0 -0
  216. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/css/theme.css +0 -0
  217. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/icons/icon-192.png +0 -0
  218. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
  219. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/icons/icon-512.png +0 -0
  220. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
  221. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
  222. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/api.js +0 -0
  223. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/utils.js +0 -0
  224. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/views/dashboard.js +0 -0
  225. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/views/file-editor.js +0 -0
  226. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/views/kvstore.js +0 -0
  227. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/views/schedules.js +0 -0
  228. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/js/views/webhooks.js +0 -0
  229. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/web/sw.js +0 -0
  230. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/daemon/webhook_store.py +0 -0
  231. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/events/bus.py +0 -0
  232. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/events/helpers.py +0 -0
  233. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/exceptions.py +0 -0
  234. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/history/reconstruction.py +0 -0
  235. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/hooks.py +0 -0
  236. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/interaction.py +0 -0
  237. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/kvstore/__init__.py +0 -0
  238. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/kvstore/backend.py +0 -0
  239. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/kvstore/sqlite.py +0 -0
  240. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/mcp_client.py +0 -0
  241. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/mcp_config.py +0 -0
  242. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/mcp_server.py +0 -0
  243. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/md_agents.py +0 -0
  244. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/models.py +0 -0
  245. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/options.py +0 -0
  246. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/plugins.py +0 -0
  247. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/renderer.py +0 -0
  248. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/schemas/__init__.py +0 -0
  249. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/schemas/agent.schema.json +0 -0
  250. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/shell_tool_config.py +0 -0
  251. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/skill_discovery.py +0 -0
  252. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/templates/AGENTS.md +0 -0
  253. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/templates/IDENTITY.md +0 -0
  254. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/templates/MEMORY.md +0 -0
  255. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/templates/USER.md +0 -0
  256. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/templates/personas/casual-technical.md +0 -0
  257. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/templates/personas/marvin.md +0 -0
  258. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/templates/personas/minimal.md +0 -0
  259. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/agents.py +0 -0
  260. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/fs.py +0 -0
  261. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/http.py +0 -0
  262. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/kv.py +0 -0
  263. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/notify.py +0 -0
  264. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/schedule.py +0 -0
  265. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/shell.py +0 -0
  266. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/shell_tools.py +0 -0
  267. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/skills.py +0 -0
  268. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tools/tmux.py +0 -0
  269. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/tsugite.py +0 -0
  270. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/__init__.py +0 -0
  271. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/chat.py +0 -0
  272. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/helpers.py +0 -0
  273. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/plain.py +0 -0
  274. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/repl_chat.py +0 -0
  275. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/repl_commands.py +0 -0
  276. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui/repl_completer.py +0 -0
  277. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/ui_context.py +0 -0
  278. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/utils.py +0 -0
  279. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/workspace/__init__.py +0 -0
  280. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/workspace/context.py +0 -0
  281. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/workspace/models.py +0 -0
  282. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/workspace/session.py +0 -0
  283. {tsugite_cli-0.9.1 → tsugite_cli-0.9.4}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.9.1
3
+ Version: 0.9.4
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
@@ -245,7 +245,7 @@ Requires-Dist: ddgs>=9.6.0
245
245
  Requires-Dist: html2text>=2020.1.16
246
246
  Requires-Dist: httpx>=0.25
247
247
  Requires-Dist: jinja2>=3.1
248
- Requires-Dist: litellm>=1.77.7
248
+ Requires-Dist: litellm<1.82.7,>=1.77.7
249
249
  Requires-Dist: mcp>=1.0
250
250
  Requires-Dist: nest-asyncio>=1.6.0
251
251
  Requires-Dist: pathspec>=0.11.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.9.1"
3
+ version = "0.9.4"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -18,7 +18,7 @@ dependencies = [
18
18
  "questionary>=2.1.1",
19
19
  "youtube-transcript-api>=0.6.0",
20
20
  "html2text>=2020.1.16",
21
- "litellm>=1.77.7",
21
+ "litellm>=1.77.7,<1.82.7", # pinned: 1.82.7+ compromised (supply chain attack 2026-03-24)
22
22
  "nest-asyncio>=1.6.0",
23
23
  "pydantic>=2.12.3",
24
24
  "pathspec>=0.11.0",
@@ -21,6 +21,7 @@ from tsugite.events import ( # noqa: E402
21
21
  CodeExecutionEvent,
22
22
  ErrorEvent,
23
23
  FinalAnswerEvent,
24
+ InfoEvent,
24
25
  ObservationEvent,
25
26
  ReasoningContentEvent,
26
27
  StepStartEvent,
@@ -222,6 +223,30 @@ async def test_progress_handler_summary():
222
223
  assert "✅ Done (3 turns)" in content
223
224
 
224
225
 
226
+ @pytest.mark.asyncio
227
+ async def test_progress_handler_info_event():
228
+ """Test InfoEvent sends standalone message to channel."""
229
+ channel = MockChannel()
230
+ handler = DiscordProgressHandler(channel, asyncio.get_running_loop())
231
+
232
+ await handler._handle_event_async(StepStartEvent(step=1, max_turns=10))
233
+ assert len(channel.messages) == 1 # progress message
234
+
235
+ await handler._handle_event_async(InfoEvent(message="Processing step 1 of 5..."))
236
+ assert len(channel.messages) == 2 # progress + info message
237
+ assert channel.messages[1].content == "Processing step 1 of 5..."
238
+
239
+
240
+ @pytest.mark.asyncio
241
+ async def test_progress_handler_info_event_empty():
242
+ """Test empty InfoEvent is silently ignored."""
243
+ channel = MockChannel()
244
+ handler = DiscordProgressHandler(channel, asyncio.get_running_loop())
245
+
246
+ await handler._handle_event_async(InfoEvent(message=""))
247
+ assert len(channel.messages) == 0
248
+
249
+
225
250
  class TestCodeBlockChunking:
226
251
  """Tests for code block aware message chunking."""
227
252
 
@@ -464,3 +464,177 @@ class TestBuildTurnMessages:
464
464
  assistant_msgs = [m for m in messages if m["role"] == "assistant"]
465
465
  code_msg = assistant_msgs[0]["content"]
466
466
  assert code_msg == "```python\nx = 1\n```"
467
+
468
+
469
+ class TestSessionStatus:
470
+ """Tests for session status recording."""
471
+
472
+ def test_save_run_records_success_status(self, tmp_path, sample_agent_file):
473
+ with (
474
+ patch("tsugite.config.load_config") as mock_config,
475
+ patch("tsugite.history.storage.get_history_dir", return_value=tmp_path),
476
+ patch("tsugite.history.storage.get_machine_name", return_value="test_machine"),
477
+ patch("tsugite.md_agents.parse_agent_file") as mock_parse,
478
+ ):
479
+ mock_config.return_value = MagicMock(history_enabled=True)
480
+ mock_agent = MagicMock()
481
+ mock_agent.config = MagicMock(disable_history=False)
482
+ mock_parse.return_value = mock_agent
483
+
484
+ conv_id = save_run_to_history(
485
+ agent_path=sample_agent_file,
486
+ agent_name="test_agent",
487
+ prompt="test prompt",
488
+ result="test result",
489
+ model="openai:gpt-4o",
490
+ status="success",
491
+ )
492
+
493
+ from tsugite.history import SessionStorage
494
+
495
+ storage = SessionStorage.load(tmp_path / f"{conv_id}.jsonl")
496
+ assert storage.status == "success"
497
+ assert storage.error_message is None
498
+
499
+ def test_save_run_records_error_status(self, tmp_path, sample_agent_file):
500
+ with (
501
+ patch("tsugite.config.load_config") as mock_config,
502
+ patch("tsugite.history.storage.get_history_dir", return_value=tmp_path),
503
+ patch("tsugite.history.storage.get_machine_name", return_value="test_machine"),
504
+ patch("tsugite.md_agents.parse_agent_file") as mock_parse,
505
+ ):
506
+ mock_config.return_value = MagicMock(history_enabled=True)
507
+ mock_agent = MagicMock()
508
+ mock_agent.config = MagicMock(disable_history=False)
509
+ mock_parse.return_value = mock_agent
510
+
511
+ conv_id = save_run_to_history(
512
+ agent_path=sample_agent_file,
513
+ agent_name="test_agent",
514
+ prompt="test prompt",
515
+ result="",
516
+ model="openai:gpt-4o",
517
+ status="error",
518
+ error_message="Connection timeout",
519
+ )
520
+
521
+ from tsugite.history import SessionStorage
522
+
523
+ storage = SessionStorage.load(tmp_path / f"{conv_id}.jsonl")
524
+ assert storage.status == "error"
525
+ assert storage.error_message == "Connection timeout"
526
+
527
+ def test_save_run_records_interrupted_status(self, tmp_path, sample_agent_file):
528
+ with (
529
+ patch("tsugite.config.load_config") as mock_config,
530
+ patch("tsugite.history.storage.get_history_dir", return_value=tmp_path),
531
+ patch("tsugite.history.storage.get_machine_name", return_value="test_machine"),
532
+ patch("tsugite.md_agents.parse_agent_file") as mock_parse,
533
+ ):
534
+ mock_config.return_value = MagicMock(history_enabled=True)
535
+ mock_agent = MagicMock()
536
+ mock_agent.config = MagicMock(disable_history=False)
537
+ mock_parse.return_value = mock_agent
538
+
539
+ conv_id = save_run_to_history(
540
+ agent_path=sample_agent_file,
541
+ agent_name="test_agent",
542
+ prompt="test prompt",
543
+ result="",
544
+ model="openai:gpt-4o",
545
+ status="interrupted",
546
+ )
547
+
548
+ from tsugite.history import SessionStorage
549
+
550
+ storage = SessionStorage.load(tmp_path / f"{conv_id}.jsonl")
551
+ assert storage.status == "interrupted"
552
+
553
+ def test_default_status_is_success(self, tmp_path, sample_agent_file):
554
+ with (
555
+ patch("tsugite.config.load_config") as mock_config,
556
+ patch("tsugite.history.storage.get_history_dir", return_value=tmp_path),
557
+ patch("tsugite.history.storage.get_machine_name", return_value="test_machine"),
558
+ patch("tsugite.md_agents.parse_agent_file") as mock_parse,
559
+ ):
560
+ mock_config.return_value = MagicMock(history_enabled=True)
561
+ mock_agent = MagicMock()
562
+ mock_agent.config = MagicMock(disable_history=False)
563
+ mock_parse.return_value = mock_agent
564
+
565
+ conv_id = save_run_to_history(
566
+ agent_path=sample_agent_file,
567
+ agent_name="test_agent",
568
+ prompt="test prompt",
569
+ result="test result",
570
+ model="openai:gpt-4o",
571
+ )
572
+
573
+ from tsugite.history import SessionStorage
574
+
575
+ storage = SessionStorage.load(tmp_path / f"{conv_id}.jsonl")
576
+ assert storage.status == "success"
577
+
578
+
579
+ class TestSessionStorageAggregates:
580
+ """Tests for aggregated storage properties (duration, functions)."""
581
+
582
+ def test_total_duration_ms(self, tmp_path):
583
+ from tsugite.history import SessionStorage
584
+
585
+ with patch("tsugite.history.storage.get_machine_name", return_value="test"):
586
+ storage = SessionStorage.create("agent", "model", session_path=tmp_path / "test.jsonl")
587
+ storage.record_turn(
588
+ messages=[{"role": "user", "content": "hi"}],
589
+ final_answer="hello",
590
+ duration_ms=1500,
591
+ )
592
+ storage.record_turn(
593
+ messages=[{"role": "user", "content": "bye"}],
594
+ final_answer="goodbye",
595
+ duration_ms=2500,
596
+ )
597
+ assert storage.total_duration_ms == 4000
598
+
599
+ def test_all_functions_called(self, tmp_path):
600
+ from tsugite.history import SessionStorage
601
+
602
+ with patch("tsugite.history.storage.get_machine_name", return_value="test"):
603
+ storage = SessionStorage.create("agent", "model", session_path=tmp_path / "test.jsonl")
604
+ storage.record_turn(
605
+ messages=[{"role": "user", "content": "hi"}],
606
+ functions_called=["read_file", "write_file"],
607
+ )
608
+ storage.record_turn(
609
+ messages=[{"role": "user", "content": "bye"}],
610
+ functions_called=["web_search", "read_file"],
611
+ )
612
+ assert storage.all_functions_called == ["read_file", "web_search", "write_file"]
613
+
614
+ def test_old_sessions_have_unknown_status(self, tmp_path):
615
+ from tsugite.history import SessionStorage
616
+
617
+ with patch("tsugite.history.storage.get_machine_name", return_value="test"):
618
+ storage = SessionStorage.create("agent", "model", session_path=tmp_path / "test.jsonl")
619
+ storage.record_turn(
620
+ messages=[{"role": "user", "content": "hi"}],
621
+ final_answer="hello",
622
+ )
623
+ # No record_status call - simulates old session
624
+ reloaded = SessionStorage.load(tmp_path / "test.jsonl")
625
+ assert reloaded.status is None
626
+
627
+ def test_load_meta_fast(self, tmp_path):
628
+ from tsugite.history import SessionStorage
629
+
630
+ with patch("tsugite.history.storage.get_machine_name", return_value="test"):
631
+ storage = SessionStorage.create("my_agent", "my_model", session_path=tmp_path / "test.jsonl")
632
+ storage.record_turn(
633
+ messages=[{"role": "user", "content": "hi"}],
634
+ final_answer="hello",
635
+ )
636
+
637
+ meta = SessionStorage.load_meta_fast(tmp_path / "test.jsonl")
638
+ assert meta is not None
639
+ assert meta.agent == "my_agent"
640
+ assert meta.model == "my_model"
@@ -32,6 +32,10 @@ def mock_storage():
32
32
  storage.turn_count = 2
33
33
  storage.total_tokens = 200
34
34
  storage.total_cost = 0.004
35
+ storage.total_duration_ms = 0
36
+ storage.all_functions_called = ["read_file"]
37
+ storage.status = "success"
38
+ storage.error_message = None
35
39
 
36
40
  turn1 = Turn(
37
41
  timestamp=datetime(2025, 11, 10, 12, 0, 5, tzinfo=timezone.utc),
@@ -79,6 +83,10 @@ def mock_storage_list():
79
83
  storage1.turn_count = 2
80
84
  storage1.total_tokens = 200
81
85
  storage1.total_cost = 0.004
86
+ storage1.total_duration_ms = 0
87
+ storage1.all_functions_called = []
88
+ storage1.status = "success"
89
+ storage1.error_message = None
82
90
  storages.append(storage1)
83
91
 
84
92
  storage2 = MagicMock(spec=SessionStorage)
@@ -90,6 +98,10 @@ def mock_storage_list():
90
98
  storage2.turn_count = 5
91
99
  storage2.total_tokens = 1000
92
100
  storage2.total_cost = 0.02
101
+ storage2.total_duration_ms = 0
102
+ storage2.all_functions_called = []
103
+ storage2.status = "success"
104
+ storage2.error_message = None
93
105
  storages.append(storage2)
94
106
 
95
107
  return storages
@@ -222,6 +234,13 @@ def test_read_conversation_with_missing_optional_fields(history_tools):
222
234
  storage.model = "openai:gpt-4o-mini"
223
235
  storage.machine = "test-machine"
224
236
  storage.created_at = datetime(2025, 11, 10, 12, 0, 0, tzinfo=timezone.utc)
237
+ storage.turn_count = 1
238
+ storage.total_tokens = 0
239
+ storage.total_cost = 0.0
240
+ storage.total_duration_ms = 0
241
+ storage.all_functions_called = []
242
+ storage.status = None
243
+ storage.error_message = None
225
244
 
226
245
  turn = Turn(
227
246
  timestamp=datetime(2025, 11, 10, 12, 0, 5, tzinfo=timezone.utc),
@@ -0,0 +1,150 @@
1
+ """Tests for new session tools: session_events_since, session_summary, updated_since filter."""
2
+
3
+ import json
4
+ from datetime import datetime, timezone, timedelta
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from tsugite.daemon.session_store import (
10
+ Session,
11
+ SessionSource,
12
+ SessionStatus,
13
+ SessionStore,
14
+ )
15
+
16
+
17
+ @pytest.fixture
18
+ def store(tmp_path):
19
+ store_path = tmp_path / "session_store.json"
20
+ return SessionStore(store_path)
21
+
22
+
23
+ @pytest.fixture
24
+ def session_with_events(store):
25
+ """Create a session and add some events to it."""
26
+ session = Session(
27
+ id="test-session-1",
28
+ agent="odyn",
29
+ source=SessionSource.BACKGROUND.value,
30
+ status=SessionStatus.COMPLETED.value,
31
+ prompt="Do something useful",
32
+ result="Done doing something useful",
33
+ )
34
+ store.create_session(session)
35
+
36
+ # Add events with increasing timestamps
37
+ base = datetime(2026, 3, 15, 10, 0, 0, tzinfo=timezone.utc)
38
+ events = [
39
+ {"type": "session_start", "timestamp": (base).isoformat(), "agent": "odyn", "prompt": "Do something"},
40
+ {"type": "tool_call", "timestamp": (base + timedelta(seconds=10)).isoformat(), "name": "read_file"},
41
+ {"type": "tool_call", "timestamp": (base + timedelta(seconds=20)).isoformat(), "name": "write_file"},
42
+ {"type": "tool_call", "timestamp": (base + timedelta(seconds=30)).isoformat(), "name": "read_file"},
43
+ {"type": "session_complete", "timestamp": (base + timedelta(seconds=40)).isoformat(), "result_preview": "Done"},
44
+ ]
45
+ for event in events:
46
+ store.append_event("test-session-1", event)
47
+
48
+ return session, events, base
49
+
50
+
51
+ class TestUpdatedSinceFilter:
52
+ def test_list_sessions_no_filter(self, store):
53
+ s1 = Session(id="s1", agent="a", last_active="2026-03-15T10:00:00+00:00")
54
+ s2 = Session(id="s2", agent="a", last_active="2026-03-15T12:00:00+00:00")
55
+ store.create_session(s1)
56
+ store.create_session(s2)
57
+
58
+ result = store.list_sessions()
59
+ assert len(result) == 2
60
+
61
+ def test_list_sessions_updated_since(self, store):
62
+ s1 = Session(id="s1", agent="a", last_active="2026-03-15T10:00:00+00:00")
63
+ s2 = Session(id="s2", agent="a", last_active="2026-03-15T12:00:00+00:00")
64
+ s3 = Session(id="s3", agent="a", last_active="2026-03-15T14:00:00+00:00")
65
+ store.create_session(s1)
66
+ store.create_session(s2)
67
+ store.create_session(s3)
68
+
69
+ result = store.list_sessions(updated_since="2026-03-15T11:00:00+00:00")
70
+ ids = {s.id for s in result}
71
+ assert ids == {"s2", "s3"}
72
+
73
+ def test_list_sessions_updated_since_none_match(self, store):
74
+ s1 = Session(id="s1", agent="a", last_active="2026-03-15T10:00:00+00:00")
75
+ store.create_session(s1)
76
+
77
+ result = store.list_sessions(updated_since="2026-03-15T23:00:00+00:00")
78
+ assert len(result) == 0
79
+
80
+ def test_list_sessions_updated_since_combined_with_status(self, store):
81
+ s1 = Session(id="s1", agent="a", status="completed", last_active="2026-03-15T12:00:00+00:00")
82
+ s2 = Session(id="s2", agent="a", status="running", last_active="2026-03-15T12:00:00+00:00")
83
+ store.create_session(s1)
84
+ store.create_session(s2)
85
+
86
+ result = store.list_sessions(updated_since="2026-03-15T11:00:00+00:00", status="completed")
87
+ assert len(result) == 1
88
+ assert result[0].id == "s1"
89
+
90
+
91
+ class TestSessionEventsSince:
92
+ def test_all_events(self, store, session_with_events):
93
+ _, events, _ = session_with_events
94
+ result = store.session_events_since("test-session-1")
95
+ assert len(result) == 5
96
+
97
+ def test_events_since_timestamp(self, store, session_with_events):
98
+ _, events, base = session_with_events
99
+ since = (base + timedelta(seconds=15)).isoformat()
100
+ result = store.session_events_since("test-session-1", since=since)
101
+ assert len(result) == 3 # 20s, 30s, 40s events
102
+ assert result[0]["type"] == "tool_call"
103
+ assert result[-1]["type"] == "session_complete"
104
+
105
+ def test_events_since_future(self, store, session_with_events):
106
+ result = store.session_events_since("test-session-1", since="2099-01-01T00:00:00+00:00")
107
+ assert len(result) == 0
108
+
109
+ def test_events_nonexistent_session(self, store):
110
+ # read_events returns [] for missing sessions, so this should work
111
+ result = store.session_events_since("nonexistent")
112
+ assert result == []
113
+
114
+
115
+ class TestSessionSummary:
116
+ def test_basic_summary(self, store, session_with_events):
117
+ session, _, _ = session_with_events
118
+ summary = store.session_summary("test-session-1")
119
+
120
+ assert summary["id"] == "test-session-1"
121
+ assert summary["agent"] == "odyn"
122
+ assert summary["source"] == "background"
123
+ assert summary["status"] == "completed"
124
+ assert "Do something useful" in summary["prompt"]
125
+ assert "Done doing something useful" in summary["result"]
126
+ assert summary["event_count"] == 5
127
+ assert sorted(summary["tools_used"]) == ["read_file", "write_file"]
128
+
129
+ def test_summary_no_events(self, store):
130
+ session = Session(id="empty-session", agent="test", source="background", prompt="test")
131
+ store.create_session(session)
132
+
133
+ summary = store.session_summary("empty-session")
134
+ assert summary["event_count"] == 0
135
+ assert summary["tools_used"] == []
136
+
137
+ def test_summary_with_error(self, store):
138
+ session = Session(
139
+ id="error-session", agent="test", source="background",
140
+ status="failed", prompt="fail task", error="Something broke",
141
+ )
142
+ store.create_session(session)
143
+
144
+ summary = store.session_summary("error-session")
145
+ assert summary["status"] == "failed"
146
+ assert summary["error"] == "Something broke"
147
+
148
+ def test_summary_nonexistent_raises(self, store):
149
+ with pytest.raises(ValueError):
150
+ store.session_summary("nonexistent")
@@ -78,6 +78,8 @@ def save_run_to_history(
78
78
  duration_ms: Optional[int] = None,
79
79
  claude_code_session_id: Optional[str] = None,
80
80
  claude_code_compacted: bool = False,
81
+ status: str = "success",
82
+ error_message: Optional[str] = None,
81
83
  ) -> Optional[str]:
82
84
  """Save a single agent run to history.
83
85
 
@@ -169,6 +171,8 @@ def save_run_to_history(
169
171
  metadata=metadata or None,
170
172
  )
171
173
 
174
+ storage.record_status(status, error_message)
175
+
172
176
  return storage.session_id
173
177
 
174
178
  except Exception as e:
@@ -420,6 +420,13 @@ async def _execute_agent_with_prompt(
420
420
  if os.environ.get("TSUGITE_SUBAGENT_MODE") == "1":
421
421
  tools = [t for t in tools if t.name not in ["ask_user", "ask_user_batch"]]
422
422
 
423
+ # Filter out interactive_only tools when no UI handler (e.g. scheduled tasks)
424
+ if not ui_handler:
425
+ from tsugite.tools import get_interactive_only_names
426
+
427
+ interactive_names = get_interactive_only_names()
428
+ tools = [t for t in tools if t.name not in interactive_names]
429
+
423
430
  # Register per-agent custom shell tools (if any)
424
431
  if agent_config.custom_tools:
425
432
  from tsugite.shell_tool_config import parse_tool_definition_from_dict
@@ -23,6 +23,7 @@ tools:
23
23
  - list_available_skills
24
24
  - final_answer
25
25
  - send_message
26
+ - react_to_message
26
27
  - web_search
27
28
  - fetch_text
28
29
  - http_request
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: response_patterns
3
- description: How to respond effectively - when to use print/send_message/final_answer/ask_user
3
+ description: How to respond effectively - when to use print/send_message/react_to_message/final_answer/ask_user
4
4
  ---
5
5
 
6
6
  # Response Patterns
@@ -11,6 +11,7 @@ description: How to respond effectively - when to use print/send_message/final_a
11
11
  |----------|----------|-----------|---------|
12
12
  | `print(x)` | You (next turn) | Continues | Intermediate data |
13
13
  | `send_message(msg)` | User | Continues | Progress updates |
14
+ | `react_to_message(emoji)` | User | Continues | Emoji acknowledgment |
14
15
  | `final_answer(msg)` | User | **Stops** | Final response |
15
16
  | `ask_user(q)` | User | **Blocks** | Get user input |
16
17
 
@@ -27,6 +28,17 @@ More examples:
27
28
  - "What's 2+2?" → `final_answer("4")`
28
29
  - "What can you do?" → `final_answer("I can read/write files, run commands, search code, and help with tasks.")`
29
30
 
31
+ ## Reactions
32
+
33
+ Acknowledge messages with emoji instead of (or before) a text response:
34
+
35
+ ```python
36
+ react_to_message("👍") # React to the most recent user message
37
+ react_to_message("✅") # Checkmark when task is done
38
+ ```
39
+
40
+ Use reactions for lightweight acknowledgment — when a thumbs-up says enough and a text reply would be noise. You can react and still send a text response, or react and call `final_answer("")` for reaction-only.
41
+
30
42
  ## Progress Updates
31
43
 
32
44
  For longer tasks, keep user informed:
@@ -88,6 +100,7 @@ Use `ask_user` when:
88
100
 
89
101
  ✅ Do:
90
102
  - `final_answer()` immediately for simple responses
103
+ - `react_to_message()` for quick acknowledgment (👍, ✅, 👀)
91
104
  - `send_message()` for progress on tasks > 5 seconds
92
105
  - `ask_user()` when you genuinely need user input
93
106
  - `print()` only for data YOU need to see