tsugite-cli 0.8.2__tar.gz → 0.9.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/PKG-INFO +2 -2
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/pyproject.toml +2 -2
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_discord_progress.py +25 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_history_integration.py +174 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_history_tools.py +19 -0
- tsugite_cli-0.9.3/tests/test_session_orchestrator_tools.py +150 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_runner/history_integration.py +22 -7
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_runner/runner.py +14 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_agents/default.md +1 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/response_patterns.md +14 -1
- tsugite_cli-0.9.3/tsugite/cli/history.py +411 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/run.py +31 -21
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/agent.py +10 -5
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/claude_code.py +2 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/executor.py +14 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/subprocess_executor.py +21 -5
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/adapters/base.py +75 -33
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/adapters/discord.py +87 -3
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/adapters/http.py +125 -28
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/adapters/scheduler_adapter.py +1 -0
- tsugite_cli-0.9.3/tsugite/daemon/commands.py +168 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/config.py +1 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/memory.py +42 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/session_runner.py +22 -3
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/session_store.py +34 -4
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/css/responsive.css +12 -24
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/css/styles.css +27 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/index.html +122 -12
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/app.js +18 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/views/conversations.js +240 -23
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/manifest.json +1 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/events/__init__.py +3 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/events/base.py +3 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/events/events.py +8 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/history/__init__.py +2 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/history/models.py +14 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/history/storage.py +71 -5
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/__init__.py +22 -7
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/history.py +86 -29
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/interactive.py +30 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/sessions.py +16 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/base.py +7 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/jsonl.py +5 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/repl_handler.py +7 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/uv.lock +14 -14
- tsugite_cli-0.8.2/tsugite/cli/history.py +0 -205
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/.github/copilot-instructions.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/.github/workflows/ci.yml +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/.github/workflows/docker-publish.yml +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/.github/workflows/pypi-publish.yml +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/.gitignore +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/AGENTS.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/CLAUDE.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/LICENSE +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/README.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/scripts/regenerate_schema.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/README.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/conftest.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_agent.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_agent_ui_events.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_content_blocks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_executor.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_memory.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_proxy.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_sandbox.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_subprocess_executor.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/core/test_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/daemon/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/daemon/test_http_adapter.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/events/test_event_consolidation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/smoke_test.sh +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_agent_file_hot_loading.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_agent_inheritance.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_agent_parser.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_agent_sessions.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_agent_skills.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_agent_utils.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_agents_tool.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_attachment_deduplication.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_attachments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_auto_context_handler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_auto_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_background_task_status.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_background_tasks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_builtin_agent_paths.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_builtin_agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_cache_control.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_chat_cli.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_chat_error_handling.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_claude_code_attachments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_claude_code_provider.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_cli.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_cli_arguments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_cli_rendering.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_cli_subcommands.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_continuation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_custom_shell_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_custom_ui.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_compaction_scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_history_persistence.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_memory.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_push.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_session_isolation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_daemon_unified_sessions.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_error_display.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_file_references.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_file_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_history.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_history_models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_history_performance.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_hooks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_http_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_interaction_backends.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_interactive_context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_interactive_tool.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_jsonl_ui.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_kvstore.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_list_agents_tool.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_mcp_client.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_mcp_server.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_multi_agent.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_multistep_agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_orchestrator_heartbeat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_plugins.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_reasoning_models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_renderer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_rendering_scenarios.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_repl_commands.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_repl_completer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_repl_handler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_retry_system.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_run_if.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_schedule_model_override.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_scheduler_history_injection.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_schema.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_security_phase1.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_send_message.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_skill_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_skill_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_stdin.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_subagent_subprocess.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_tmux_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_tool_directives.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_tool_registry.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_workspace_auto_continue.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_workspace_cwd.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tests/test_workspace_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/attachments/youtube.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/codebase_exploration.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/python_math.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/scheduling.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/skill_authoring.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/tsugite_agent_basics.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/tsugite_jinja_reference.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/builtin_skills/tsugite_skill_basics.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cache.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/mcp.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/serve.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/console.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/constants.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/adapters/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/compaction_scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/gateway.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/push.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/css/theme.css +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/icons/icon-192.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/icons/icon-512.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/api.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/utils.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/views/dashboard.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/views/file-editor.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/views/kvstore.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/views/schedules.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/views/webhooks.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/js/views/workspace.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/web/sw.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/daemon/webhook_store.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/kvstore/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/kvstore/backend.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/kvstore/sqlite.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/mcp_client.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/mcp_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/mcp_server.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/options.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/http.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/kv.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tools/tmux.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/utils.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/workspace/context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.3}/tsugite/workspace/templates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tsugite-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Micro-agent runner for task automation using markdown definitions
|
|
5
5
|
Author: Justyn Shull
|
|
6
6
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
@@ -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
|
|
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.
|
|
3
|
+
version = "0.9.3"
|
|
4
4
|
description = "Micro-agent runner for task automation using markdown definitions"
|
|
5
5
|
authors = [{ name = "Justyn Shull" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -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:
|
|
@@ -241,16 +245,27 @@ def get_claude_code_session_info(conversation_id: str) -> Optional["ClaudeCodeSe
|
|
|
241
245
|
storage = SessionStorage(session_path)
|
|
242
246
|
records = storage.load_records()
|
|
243
247
|
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
|
|
248
|
+
# Find the last CompactionSummary index (if any).
|
|
249
|
+
# Session IDs from retained turns (carried over from pre-compaction)
|
|
250
|
+
# are stale, but session IDs from turns AFTER the compaction are valid.
|
|
251
|
+
compaction_idx = -1
|
|
252
|
+
for i, record in enumerate(records):
|
|
247
253
|
if isinstance(record, CompactionSummary):
|
|
248
|
-
|
|
254
|
+
compaction_idx = i
|
|
255
|
+
|
|
256
|
+
for record in reversed(records):
|
|
249
257
|
if isinstance(record, Turn) and record.metadata:
|
|
250
258
|
session_id = record.metadata.get("claude_code_session_id")
|
|
251
|
-
if session_id:
|
|
252
|
-
|
|
253
|
-
|
|
259
|
+
if not session_id:
|
|
260
|
+
continue
|
|
261
|
+
# If this turn is a retained turn from before compaction
|
|
262
|
+
# (carried over with stale session ID), skip it
|
|
263
|
+
if compaction_idx >= 0:
|
|
264
|
+
turn_idx = records.index(record)
|
|
265
|
+
if turn_idx <= compaction_idx:
|
|
266
|
+
continue
|
|
267
|
+
compacted = record.metadata.get("claude_code_compacted", False)
|
|
268
|
+
return ClaudeCodeSessionInfo(session_id, compacted)
|
|
254
269
|
return None
|
|
255
270
|
except Exception:
|
|
256
271
|
return None
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""Agent execution engine using TsugiteAgent."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
4
5
|
import time
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
5
8
|
from pathlib import Path
|
|
6
9
|
from types import SimpleNamespace
|
|
7
10
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
@@ -417,6 +420,13 @@ async def _execute_agent_with_prompt(
|
|
|
417
420
|
if os.environ.get("TSUGITE_SUBAGENT_MODE") == "1":
|
|
418
421
|
tools = [t for t in tools if t.name not in ["ask_user", "ask_user_batch"]]
|
|
419
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
|
+
|
|
420
430
|
# Register per-agent custom shell tools (if any)
|
|
421
431
|
if agent_config.custom_tools:
|
|
422
432
|
from tsugite.shell_tool_config import parse_tool_definition_from_dict
|
|
@@ -779,6 +789,9 @@ async def run_agent_async(
|
|
|
779
789
|
if session_info:
|
|
780
790
|
claude_code_resume_session = session_info.session_id
|
|
781
791
|
claude_code_resume_after_compaction = session_info.compacted
|
|
792
|
+
logger.info("Resuming Claude Code session %s (compacted=%s)", claude_code_resume_session, claude_code_resume_after_compaction)
|
|
793
|
+
else:
|
|
794
|
+
logger.debug("No Claude Code session to resume for %s", continue_conversation_id)
|
|
782
795
|
|
|
783
796
|
if not claude_code_resume_session:
|
|
784
797
|
# No Claude Code session to resume -- load history for serialization
|
|
@@ -838,7 +851,7 @@ async def run_agent_async(
|
|
|
838
851
|
)
|
|
839
852
|
except (RuntimeError, AgentExecutionError) as e:
|
|
840
853
|
err_str = str(e).lower()
|
|
841
|
-
if claude_code_resume_session and ("process ended" in err_str or "no conversation found" in err_str):
|
|
854
|
+
if claude_code_resume_session and ("process ended" in err_str or "no conversation found" in err_str or "prompt too long" in err_str):
|
|
842
855
|
logger.warning("Claude Code resume failed (%s), retrying with full history", e)
|
|
843
856
|
try:
|
|
844
857
|
previous_messages = load_and_apply_history(continue_conversation_id)
|
|
@@ -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
|