tsugite-cli 0.8.2__tar.gz → 0.9.1__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.1}/PKG-INFO +1 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/pyproject.toml +1 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/history_integration.py +18 -7
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/runner.py +7 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/agent.py +7 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/claude_code.py +2 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/base.py +25 -3
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/discord.py +61 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/http.py +71 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/scheduler_adapter.py +1 -0
- tsugite_cli-0.9.1/tsugite/daemon/commands.py +168 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/config.py +1 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/memory.py +50 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/session_runner.py +17 -3
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/session_store.py +4 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/styles.css +23 -1
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/index.html +105 -11
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/conversations.js +198 -23
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/sessions.py +16 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/uv.lock +13 -13
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/copilot-instructions.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/workflows/ci.yml +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/workflows/docker-publish.yml +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/workflows/pypi-publish.yml +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.gitignore +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/AGENTS.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/CLAUDE.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/LICENSE +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/README.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/scripts/regenerate_schema.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/README.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/conftest.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_agent.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_agent_ui_events.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_content_blocks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_executor.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_memory.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_proxy.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_sandbox.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_subprocess_executor.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/daemon/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/daemon/test_http_adapter.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/events/test_event_consolidation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/smoke_test.sh +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_file_hot_loading.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_inheritance.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_parser.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_sessions.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_skills.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_utils.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agents_tool.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_attachment_deduplication.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_attachments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_auto_context_handler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_auto_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_background_task_status.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_background_tasks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_builtin_agent_paths.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_builtin_agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cache_control.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_chat_cli.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_chat_error_handling.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_claude_code_attachments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_claude_code_provider.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli_arguments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli_rendering.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli_subcommands.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_continuation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_custom_shell_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_custom_ui.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_compaction_scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_history_persistence.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_memory.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_push.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_session_isolation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_unified_sessions.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_discord_progress.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_error_display.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_file_references.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_file_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_integration.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_performance.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_hooks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_http_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_interaction_backends.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_interactive_context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_interactive_tool.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_jsonl_ui.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_kvstore.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_list_agents_tool.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_mcp_client.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_mcp_server.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_multi_agent.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_multistep_agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_orchestrator_heartbeat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_plugins.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_reasoning_models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_renderer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_rendering_scenarios.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_repl_commands.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_repl_completer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_repl_handler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_retry_system.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_run_if.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_schedule_model_override.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_scheduler_history_injection.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_schema.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_security_phase1.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_send_message.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_skill_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_skill_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_stdin.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_subagent_subprocess.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_tmux_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_tool_directives.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_tool_registry.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_workspace_auto_continue.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_workspace_cwd.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_workspace_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/youtube.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/codebase_exploration.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/python_math.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/response_patterns.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/scheduling.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/skill_authoring.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_agent_basics.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_jinja_reference.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_skill_basics.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cache.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/mcp.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/serve.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/console.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/constants.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/subprocess_executor.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/compaction_scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/gateway.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/push.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/scheduler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/responsive.css +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/theme.css +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-192.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/api.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/app.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/utils.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/dashboard.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/file-editor.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/kvstore.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/schedules.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/webhooks.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/workspace.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/manifest.json +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/sw.js +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/webhook_store.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/storage.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/kvstore/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/kvstore/backend.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/kvstore/sqlite.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/mcp_client.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/mcp_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/mcp_server.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/options.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/http.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/kv.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/tmux.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/utils.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/context.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/templates.py +0 -0
|
@@ -241,16 +241,27 @@ def get_claude_code_session_info(conversation_id: str) -> Optional["ClaudeCodeSe
|
|
|
241
241
|
storage = SessionStorage(session_path)
|
|
242
242
|
records = storage.load_records()
|
|
243
243
|
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
|
|
244
|
+
# Find the last CompactionSummary index (if any).
|
|
245
|
+
# Session IDs from retained turns (carried over from pre-compaction)
|
|
246
|
+
# are stale, but session IDs from turns AFTER the compaction are valid.
|
|
247
|
+
compaction_idx = -1
|
|
248
|
+
for i, record in enumerate(records):
|
|
247
249
|
if isinstance(record, CompactionSummary):
|
|
248
|
-
|
|
250
|
+
compaction_idx = i
|
|
251
|
+
|
|
252
|
+
for record in reversed(records):
|
|
249
253
|
if isinstance(record, Turn) and record.metadata:
|
|
250
254
|
session_id = record.metadata.get("claude_code_session_id")
|
|
251
|
-
if session_id:
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
if not session_id:
|
|
256
|
+
continue
|
|
257
|
+
# If this turn is a retained turn from before compaction
|
|
258
|
+
# (carried over with stale session ID), skip it
|
|
259
|
+
if compaction_idx >= 0:
|
|
260
|
+
turn_idx = records.index(record)
|
|
261
|
+
if turn_idx <= compaction_idx:
|
|
262
|
+
continue
|
|
263
|
+
compacted = record.metadata.get("claude_code_compacted", False)
|
|
264
|
+
return ClaudeCodeSessionInfo(session_id, compacted)
|
|
254
265
|
return None
|
|
255
266
|
except Exception:
|
|
256
267
|
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
|
|
@@ -779,6 +782,9 @@ async def run_agent_async(
|
|
|
779
782
|
if session_info:
|
|
780
783
|
claude_code_resume_session = session_info.session_id
|
|
781
784
|
claude_code_resume_after_compaction = session_info.compacted
|
|
785
|
+
logger.info("Resuming Claude Code session %s (compacted=%s)", claude_code_resume_session, claude_code_resume_after_compaction)
|
|
786
|
+
else:
|
|
787
|
+
logger.debug("No Claude Code session to resume for %s", continue_conversation_id)
|
|
782
788
|
|
|
783
789
|
if not claude_code_resume_session:
|
|
784
790
|
# No Claude Code session to resume -- load history for serialization
|
|
@@ -838,7 +844,7 @@ async def run_agent_async(
|
|
|
838
844
|
)
|
|
839
845
|
except (RuntimeError, AgentExecutionError) as e:
|
|
840
846
|
err_str = str(e).lower()
|
|
841
|
-
if claude_code_resume_session and ("process ended" in err_str or "no conversation found" in err_str):
|
|
847
|
+
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
848
|
logger.warning("Claude Code resume failed (%s), retrying with full history", e)
|
|
843
849
|
try:
|
|
844
850
|
previous_messages = load_and_apply_history(continue_conversation_id)
|
|
@@ -210,6 +210,7 @@ class TsugiteAgent:
|
|
|
210
210
|
self._claude_code_model = self.litellm_params.get("model") if self._is_claude_code else None
|
|
211
211
|
self._claude_code_session_id: Optional[str] = None
|
|
212
212
|
self._claude_code_last_turn_tokens: int = 0
|
|
213
|
+
self._claude_code_context_tokens: int = 0
|
|
213
214
|
self._claude_code_context_window: Optional[int] = None
|
|
214
215
|
self._claude_code_cache_creation_tokens: int = 0
|
|
215
216
|
self._claude_code_cache_read_tokens: int = 0
|
|
@@ -627,9 +628,10 @@ class TsugiteAgent:
|
|
|
627
628
|
self._claude_code_session_id = claude_process.session_id
|
|
628
629
|
|
|
629
630
|
if return_full_result:
|
|
631
|
+
context_tokens = self._claude_code_context_tokens or total_tokens
|
|
630
632
|
return AgentResult(
|
|
631
633
|
output=exec_result.final_answer,
|
|
632
|
-
token_usage=
|
|
634
|
+
token_usage=context_tokens,
|
|
633
635
|
cost=self.total_cost if self.total_cost > 0 else None,
|
|
634
636
|
steps=self.memory.steps,
|
|
635
637
|
claude_code_session_id=self._claude_code_session_id,
|
|
@@ -747,6 +749,7 @@ class TsugiteAgent:
|
|
|
747
749
|
output_tokens = event.get("output_tokens") or 0
|
|
748
750
|
turn_total = input_tokens + cache_creation + cache_read + output_tokens
|
|
749
751
|
self._claude_code_last_turn_tokens = turn_total
|
|
752
|
+
self._claude_code_context_tokens = input_tokens + cache_creation + cache_read
|
|
750
753
|
self._claude_code_cache_creation_tokens += cache_creation
|
|
751
754
|
self._claude_code_cache_read_tokens += cache_read
|
|
752
755
|
self.total_tokens += turn_total
|
|
@@ -756,6 +759,9 @@ class TsugiteAgent:
|
|
|
756
759
|
if stream and self.event_bus:
|
|
757
760
|
self.event_bus.emit(StreamCompleteEvent())
|
|
758
761
|
|
|
762
|
+
if accumulated.strip().lower() == "prompt is too long":
|
|
763
|
+
raise RuntimeError(f"Claude Code prompt too long (session={self._claude_code_session_id})")
|
|
764
|
+
|
|
759
765
|
parsed = self._parse_response_from_text(accumulated)
|
|
760
766
|
|
|
761
767
|
# If no code block was found, preserve the raw text as thought so the
|
|
@@ -132,7 +132,8 @@ class ClaudeCodeProcess:
|
|
|
132
132
|
|
|
133
133
|
# Start draining stderr in background to prevent pipe buffer deadlock
|
|
134
134
|
self._stderr_task = asyncio.create_task(self._drain_stderr())
|
|
135
|
-
|
|
135
|
+
mode = f"resume={resume_session}" if resume_session else "new session"
|
|
136
|
+
logger.info("Claude Code subprocess started (pid=%d, model=%s, %s)", self._process.pid, model, mode)
|
|
136
137
|
|
|
137
138
|
async def send_message(self, content: str) -> AsyncIterator[dict]:
|
|
138
139
|
"""Write user message to stdin and yield streaming events from stdout.
|
|
@@ -430,6 +430,8 @@ class BaseAdapter(ABC):
|
|
|
430
430
|
# Get the new session ID after compaction
|
|
431
431
|
session = self.session_store.get_or_create_interactive(user_id, self.agent_name)
|
|
432
432
|
conv_id = session.id
|
|
433
|
+
if self.event_bus:
|
|
434
|
+
self.event_bus.emit("session_update", {"action": "compacted", "id": conv_id})
|
|
433
435
|
|
|
434
436
|
metadata = channel_context.to_dict()
|
|
435
437
|
metadata["daemon_agent"] = self.agent_name
|
|
@@ -520,12 +522,29 @@ class BaseAdapter(ABC):
|
|
|
520
522
|
self.session_store.update_context_limit(self.agent_name, result.context_window)
|
|
521
523
|
self.agent_config.context_limit = result.context_window
|
|
522
524
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
+
self.session_store.update_token_count(conv_id, result.token_count or 0)
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
session = self.session_store.get_session(conv_id)
|
|
529
|
+
if session and session.message_count <= 1 and not session.title:
|
|
530
|
+
asyncio.ensure_future(self._auto_title_session(conv_id, message, str(result)))
|
|
531
|
+
except Exception as e:
|
|
532
|
+
logger.debug("Auto-title check failed for session '%s': %s", conv_id, e)
|
|
525
533
|
|
|
526
534
|
return str(result)
|
|
527
535
|
|
|
528
|
-
async def
|
|
536
|
+
async def _auto_title_session(self, session_id: str, user_message: str, assistant_response: str) -> None:
|
|
537
|
+
try:
|
|
538
|
+
from tsugite.daemon.memory import auto_title_session
|
|
539
|
+
|
|
540
|
+
await auto_title_session(
|
|
541
|
+
session_id, user_message, assistant_response,
|
|
542
|
+
self.resolve_model(), self.session_store, self.event_bus,
|
|
543
|
+
)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.debug("Auto-title failed for session '%s': %s", session_id, e)
|
|
546
|
+
|
|
547
|
+
async def _compact_session(self, session_id: str, instructions: str | None = None) -> None:
|
|
529
548
|
"""Compact session when approaching context limit.
|
|
530
549
|
|
|
531
550
|
Uses a sliding window: recent turns are kept verbatim while older
|
|
@@ -613,6 +632,9 @@ class BaseAdapter(ABC):
|
|
|
613
632
|
|
|
614
633
|
old_messages.extend(msg for turn in old_turns for msg in turn.messages)
|
|
615
634
|
|
|
635
|
+
if instructions:
|
|
636
|
+
old_messages.append({"role": "user", "content": f"<compaction_instructions>{instructions}</compaction_instructions>"})
|
|
637
|
+
|
|
616
638
|
try:
|
|
617
639
|
summary = await summarize_session(
|
|
618
640
|
old_messages, model=model, max_context_tokens=self.agent_config.context_limit
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Discord bot adapter."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import inspect
|
|
4
5
|
import logging
|
|
5
6
|
import re
|
|
6
7
|
from types import SimpleNamespace
|
|
@@ -422,10 +423,25 @@ class DiscordAdapter(BaseAdapter):
|
|
|
422
423
|
intents.message_content = True
|
|
423
424
|
|
|
424
425
|
self.bot = commands.Bot(command_prefix=bot_config.command_prefix, intents=intents)
|
|
426
|
+
self._register_commands()
|
|
425
427
|
|
|
426
428
|
@self.bot.event
|
|
427
429
|
async def on_ready():
|
|
428
|
-
|
|
430
|
+
try:
|
|
431
|
+
if bot_config.guild_id:
|
|
432
|
+
guild = discord.Object(id=int(bot_config.guild_id))
|
|
433
|
+
self.bot.tree.copy_global_to(guild=guild)
|
|
434
|
+
await self.bot.tree.sync(guild=guild)
|
|
435
|
+
else:
|
|
436
|
+
await self.bot.tree.sync()
|
|
437
|
+
synced = len(self.bot.tree.get_commands())
|
|
438
|
+
logger.info(
|
|
439
|
+
"Discord bot '%s' logged in as %s (agent: %s, %d app commands synced)",
|
|
440
|
+
bot_config.name, self.bot.user, agent_name, synced,
|
|
441
|
+
)
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.error("Failed to sync app commands for '%s': %s", bot_config.name, e)
|
|
444
|
+
logger.info("Discord bot '%s' logged in as %s (agent: %s)", bot_config.name, self.bot.user, agent_name)
|
|
429
445
|
|
|
430
446
|
@self.bot.event
|
|
431
447
|
async def on_error(event_method, *args, **kwargs):
|
|
@@ -473,6 +489,50 @@ class DiscordAdapter(BaseAdapter):
|
|
|
473
489
|
task = asyncio.create_task(self._process_message(message, user_msg, bot_config.name))
|
|
474
490
|
task.add_done_callback(lambda t: _handle_async_exception(t, bot_config.name))
|
|
475
491
|
|
|
492
|
+
def _register_commands(self):
|
|
493
|
+
"""Auto-register adapter commands from the shared registry as Discord app commands."""
|
|
494
|
+
from tsugite.daemon.commands import get_commands
|
|
495
|
+
|
|
496
|
+
for cmd in get_commands().values():
|
|
497
|
+
self._add_app_command(cmd)
|
|
498
|
+
|
|
499
|
+
def _add_app_command(self, cmd: "AdapterCommand"):
|
|
500
|
+
"""Convert an AdapterCommand to a discord app_commands.Command and add to the bot tree."""
|
|
501
|
+
adapter = self
|
|
502
|
+
|
|
503
|
+
# user_id is auto-injected from the interaction, so hide it from the Discord UI
|
|
504
|
+
visible_params = [p for p in cmd.params if p.name != "user_id"]
|
|
505
|
+
auto_inject_user_id = len(visible_params) < len(cmd.params)
|
|
506
|
+
|
|
507
|
+
sig_params = [
|
|
508
|
+
inspect.Parameter("interaction", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=discord.Interaction)
|
|
509
|
+
]
|
|
510
|
+
for p in visible_params:
|
|
511
|
+
ann = Optional[p.type] if not p.required else p.type
|
|
512
|
+
default = inspect.Parameter.empty if p.required else None
|
|
513
|
+
sig_params.append(inspect.Parameter(p.name, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=ann, default=default))
|
|
514
|
+
|
|
515
|
+
async def callback(interaction: discord.Interaction, **kwargs):
|
|
516
|
+
await interaction.response.defer()
|
|
517
|
+
if auto_inject_user_id:
|
|
518
|
+
kwargs.setdefault("user_id", str(interaction.user.id))
|
|
519
|
+
try:
|
|
520
|
+
result = await cmd.handler(adapter, **kwargs)
|
|
521
|
+
except Exception as e:
|
|
522
|
+
logger.error("App command '%s' failed: %s", cmd.name, e)
|
|
523
|
+
result = f"Command failed: {e}"
|
|
524
|
+
await interaction.followup.send(str(result)[:2000])
|
|
525
|
+
|
|
526
|
+
callback.__signature__ = inspect.Signature(sig_params)
|
|
527
|
+
callback.__annotations__ = {p.name: p.annotation for p in sig_params}
|
|
528
|
+
|
|
529
|
+
descriptions = {p.name: p.description for p in visible_params}
|
|
530
|
+
if descriptions:
|
|
531
|
+
callback = discord.app_commands.describe(**descriptions)(callback)
|
|
532
|
+
|
|
533
|
+
app_cmd = discord.app_commands.Command(name=cmd.name, description=cmd.description, callback=callback)
|
|
534
|
+
self.bot.tree.add_command(app_cmd)
|
|
535
|
+
|
|
476
536
|
async def _process_message(self, message, user_msg: str, bot_name: str):
|
|
477
537
|
"""Process a message in an isolated task."""
|
|
478
538
|
is_thread = isinstance(message.channel, discord.Thread)
|
|
@@ -365,6 +365,7 @@ class HTTPServer:
|
|
|
365
365
|
Route("/api/sessions", self._api_list_sessions, methods=["GET"]),
|
|
366
366
|
Route("/api/sessions", self._api_start_session, methods=["POST"]),
|
|
367
367
|
Route("/api/sessions/{session_id}", self._api_get_session, methods=["GET"]),
|
|
368
|
+
Route("/api/sessions/{session_id}", self._api_update_session, methods=["PATCH"]),
|
|
368
369
|
Route("/api/sessions/{session_id}/cancel", self._api_cancel_session, methods=["POST"]),
|
|
369
370
|
Route("/api/sessions/{session_id}/restart", self._api_restart_session, methods=["POST"]),
|
|
370
371
|
Route("/api/sessions/{session_id}/events", self._api_session_events, methods=["GET"]),
|
|
@@ -389,6 +390,8 @@ class HTTPServer:
|
|
|
389
390
|
Route("/api/kv/namespaces", self._kv_namespaces, methods=["GET"]),
|
|
390
391
|
Route("/api/kv/{namespace}/keys", self._kv_keys, methods=["GET"]),
|
|
391
392
|
Route("/api/kv/{namespace}/keys/{key:path}", self._kv_get, methods=["GET"]),
|
|
393
|
+
Route("/api/commands", self._list_commands, methods=["GET"]),
|
|
394
|
+
Route("/api/agents/{agent}/commands/{command_name}", self._run_command, methods=["POST"]),
|
|
392
395
|
Mount("/static", app=StaticFiles(directory=str(WEB_DIR)), name="static"),
|
|
393
396
|
Route("/", self._serve_ui, methods=["GET"]),
|
|
394
397
|
]
|
|
@@ -397,6 +400,53 @@ class HTTPServer:
|
|
|
397
400
|
async def _health(self, request: Request) -> JSONResponse:
|
|
398
401
|
return JSONResponse({"status": "ok", "agents": list(self.adapters.keys())})
|
|
399
402
|
|
|
403
|
+
async def _list_commands(self, request: Request) -> JSONResponse:
|
|
404
|
+
if err := self._check_auth(request):
|
|
405
|
+
return err
|
|
406
|
+
from tsugite.daemon.commands import get_commands
|
|
407
|
+
|
|
408
|
+
return JSONResponse({"commands": [
|
|
409
|
+
{
|
|
410
|
+
"name": cmd.name,
|
|
411
|
+
"description": cmd.description,
|
|
412
|
+
"params": [
|
|
413
|
+
{"name": p.name, "type": p.type.__name__, "description": p.description, "required": p.required, **({"choices": p.choices} if p.choices else {})}
|
|
414
|
+
for p in cmd.params
|
|
415
|
+
],
|
|
416
|
+
}
|
|
417
|
+
for cmd in get_commands().values()
|
|
418
|
+
]})
|
|
419
|
+
|
|
420
|
+
async def _run_command(self, request: Request) -> JSONResponse:
|
|
421
|
+
adapter, err = self._get_adapter(request)
|
|
422
|
+
if err:
|
|
423
|
+
return err
|
|
424
|
+
from tsugite.daemon.commands import get_commands
|
|
425
|
+
|
|
426
|
+
command_name = request.path_params["command_name"]
|
|
427
|
+
commands = get_commands()
|
|
428
|
+
if command_name not in commands:
|
|
429
|
+
return JSONResponse({"error": f"Unknown command: {command_name}"}, status_code=404)
|
|
430
|
+
|
|
431
|
+
cmd = commands[command_name]
|
|
432
|
+
try:
|
|
433
|
+
body = await request.json()
|
|
434
|
+
except Exception:
|
|
435
|
+
body = {}
|
|
436
|
+
|
|
437
|
+
allowed_keys = {p.name for p in cmd.params}
|
|
438
|
+
filtered = {k: v for k, v in body.items() if k in allowed_keys}
|
|
439
|
+
|
|
440
|
+
missing = [p.name for p in cmd.params if p.required and p.name not in filtered]
|
|
441
|
+
if missing:
|
|
442
|
+
return JSONResponse({"error": f"Missing required params: {', '.join(missing)}"}, status_code=400)
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
result = await cmd.handler(adapter, **filtered)
|
|
446
|
+
except Exception as e:
|
|
447
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
448
|
+
return JSONResponse({"result": result})
|
|
449
|
+
|
|
400
450
|
async def _list_agents(self, request: Request) -> JSONResponse:
|
|
401
451
|
if err := self._check_auth(request):
|
|
402
452
|
return err
|
|
@@ -452,6 +502,7 @@ class HTTPServer:
|
|
|
452
502
|
"model": s.model,
|
|
453
503
|
"error": s.error,
|
|
454
504
|
"result": s.result,
|
|
505
|
+
"title": s.title,
|
|
455
506
|
}
|
|
456
507
|
)
|
|
457
508
|
|
|
@@ -725,6 +776,8 @@ class HTTPServer:
|
|
|
725
776
|
|
|
726
777
|
new_session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
|
|
727
778
|
self.event_bus.emit("agent_status", {"agent": agent_name})
|
|
779
|
+
if new_session:
|
|
780
|
+
self.event_bus.emit("session_update", {"action": "compacted", "id": new_session.id})
|
|
728
781
|
return JSONResponse(
|
|
729
782
|
{
|
|
730
783
|
"status": "compacted",
|
|
@@ -1143,6 +1196,7 @@ class HTTPServer:
|
|
|
1143
1196
|
"created_at": s.created_at,
|
|
1144
1197
|
"updated_at": s.last_active,
|
|
1145
1198
|
"error": s.error,
|
|
1199
|
+
"title": s.title,
|
|
1146
1200
|
}
|
|
1147
1201
|
for s in sessions
|
|
1148
1202
|
]
|
|
@@ -1196,6 +1250,23 @@ class HTTPServer:
|
|
|
1196
1250
|
except ValueError as e:
|
|
1197
1251
|
return JSONResponse({"error": str(e)}, status_code=404)
|
|
1198
1252
|
|
|
1253
|
+
async def _api_update_session(self, request: Request) -> JSONResponse:
|
|
1254
|
+
if err := self._require_auth_and_sessions(request):
|
|
1255
|
+
return err
|
|
1256
|
+
session_id = request.path_params["session_id"]
|
|
1257
|
+
try:
|
|
1258
|
+
body = await request.json()
|
|
1259
|
+
except Exception:
|
|
1260
|
+
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
|
1261
|
+
title = body.get("title")
|
|
1262
|
+
if title is None:
|
|
1263
|
+
return JSONResponse({"error": "No updatable fields provided"}, status_code=400)
|
|
1264
|
+
try:
|
|
1265
|
+
self.session_runner.store.update_session(session_id, title=title)
|
|
1266
|
+
return JSONResponse({"ok": True, "title": title})
|
|
1267
|
+
except ValueError as e:
|
|
1268
|
+
return JSONResponse({"error": str(e)}, status_code=404)
|
|
1269
|
+
|
|
1199
1270
|
async def _api_cancel_session(self, request: Request) -> JSONResponse:
|
|
1200
1271
|
if err := self._require_auth_and_sessions(request):
|
|
1201
1272
|
return err
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Adapter command registry — define commands once, auto-register across all adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Callable
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from tsugite.daemon.adapters.base import BaseAdapter
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_COMMANDS: dict[str, AdapterCommand] = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CommandParam:
|
|
19
|
+
name: str
|
|
20
|
+
type: type
|
|
21
|
+
description: str
|
|
22
|
+
required: bool = True
|
|
23
|
+
choices: list[str] | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AdapterCommand:
|
|
28
|
+
name: str
|
|
29
|
+
description: str
|
|
30
|
+
handler: Callable
|
|
31
|
+
params: list[CommandParam] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def adapter_command(
|
|
35
|
+
name: str,
|
|
36
|
+
description: str,
|
|
37
|
+
params: list[CommandParam] | None = None,
|
|
38
|
+
):
|
|
39
|
+
"""Decorator to register an adapter command."""
|
|
40
|
+
|
|
41
|
+
def decorator(fn: Callable) -> Callable:
|
|
42
|
+
if name in _COMMANDS:
|
|
43
|
+
logger.warning("Overwriting existing adapter command '%s'", name)
|
|
44
|
+
_COMMANDS[name] = AdapterCommand(
|
|
45
|
+
name=name,
|
|
46
|
+
description=description,
|
|
47
|
+
handler=fn,
|
|
48
|
+
params=params or [],
|
|
49
|
+
)
|
|
50
|
+
return fn
|
|
51
|
+
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_commands() -> dict[str, AdapterCommand]:
|
|
56
|
+
return _COMMANDS
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Built-in commands
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@adapter_command(
|
|
65
|
+
name="bg",
|
|
66
|
+
description="Run a task in the background",
|
|
67
|
+
params=[
|
|
68
|
+
CommandParam("prompt", str, "The task to run"),
|
|
69
|
+
CommandParam("agent", str, "Target agent", required=False),
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
async def cmd_bg(adapter: BaseAdapter, prompt: str, agent: str | None = None) -> str:
|
|
73
|
+
"""Start a background session with the given prompt."""
|
|
74
|
+
from tsugite.daemon.session_store import Session, SessionSource
|
|
75
|
+
from tsugite.tools.sessions import _session_runner
|
|
76
|
+
|
|
77
|
+
if not _session_runner:
|
|
78
|
+
return "Background sessions require the daemon session runner to be enabled."
|
|
79
|
+
|
|
80
|
+
target_agent = agent or adapter.agent_name
|
|
81
|
+
|
|
82
|
+
session = Session(
|
|
83
|
+
id="",
|
|
84
|
+
agent=target_agent,
|
|
85
|
+
source=SessionSource.BACKGROUND.value,
|
|
86
|
+
prompt=prompt,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = _session_runner.start_session(session)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return f"Failed to start background session: {e}"
|
|
93
|
+
|
|
94
|
+
return f"Background session started (ID: {result.id})"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@adapter_command(
|
|
98
|
+
name="compact",
|
|
99
|
+
description="Compact the current conversation to free context space",
|
|
100
|
+
params=[
|
|
101
|
+
CommandParam("user_id", str, "User whose session to compact"),
|
|
102
|
+
CommandParam("message", str, "Extra instructions for compaction (e.g. remember/forget specific things)", required=False),
|
|
103
|
+
],
|
|
104
|
+
)
|
|
105
|
+
async def cmd_compact(adapter: BaseAdapter, user_id: str, message: str | None = None) -> str:
|
|
106
|
+
"""Compact the interactive session for the given user."""
|
|
107
|
+
session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
|
|
108
|
+
|
|
109
|
+
if session.message_count == 0:
|
|
110
|
+
return "No conversation to compact."
|
|
111
|
+
|
|
112
|
+
if not adapter.session_store.begin_compaction(user_id, adapter.agent_name):
|
|
113
|
+
return "Compaction already in progress."
|
|
114
|
+
|
|
115
|
+
old_id = session.id
|
|
116
|
+
adapter._broadcast_compaction(adapter.agent_name, started=True)
|
|
117
|
+
try:
|
|
118
|
+
await adapter._compact_session(session.id, instructions=message)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return f"Compaction failed: {e}"
|
|
121
|
+
finally:
|
|
122
|
+
adapter.session_store.end_compaction(user_id, adapter.agent_name)
|
|
123
|
+
adapter._broadcast_compaction(adapter.agent_name, started=False)
|
|
124
|
+
|
|
125
|
+
new_session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
|
|
126
|
+
return f"Session compacted (old: {old_id[:12]}, new: {new_session.id[:12]})"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@adapter_command(
|
|
130
|
+
name="status",
|
|
131
|
+
description="Show agent status and context usage",
|
|
132
|
+
params=[CommandParam("user_id", str, "User to check status for")],
|
|
133
|
+
)
|
|
134
|
+
async def cmd_status(adapter: BaseAdapter, user_id: str) -> str:
|
|
135
|
+
"""Show current agent status, token usage, and context window info."""
|
|
136
|
+
session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
|
|
137
|
+
context_limit = adapter.session_store.get_context_limit(adapter.agent_name)
|
|
138
|
+
tokens = session.cumulative_tokens
|
|
139
|
+
pct = int(tokens / context_limit * 100) if context_limit else 0
|
|
140
|
+
compacting = adapter.session_store.is_compacting(user_id, adapter.agent_name)
|
|
141
|
+
|
|
142
|
+
lines = [
|
|
143
|
+
f"Model: {adapter.resolve_model()}",
|
|
144
|
+
f"Context: {tokens:,} / {context_limit:,} tokens ({pct}%)",
|
|
145
|
+
f"Messages: {session.message_count}",
|
|
146
|
+
]
|
|
147
|
+
if compacting:
|
|
148
|
+
lines.append("Compaction: in progress")
|
|
149
|
+
return "\n".join(lines)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@adapter_command(
|
|
153
|
+
name="sessions",
|
|
154
|
+
description="List active and recent background sessions",
|
|
155
|
+
params=[CommandParam("status", str, "Filter by status (running, completed, failed)", required=False)],
|
|
156
|
+
)
|
|
157
|
+
async def cmd_sessions(adapter: BaseAdapter, status: str | None = None) -> str:
|
|
158
|
+
"""List background sessions for the current agent."""
|
|
159
|
+
sessions = adapter.session_store.list_sessions(agent=adapter.agent_name, status=status)
|
|
160
|
+
if not sessions:
|
|
161
|
+
return "No sessions found."
|
|
162
|
+
lines = []
|
|
163
|
+
for s in sessions[:10]:
|
|
164
|
+
label = s.title or (s.prompt or "")[:60]
|
|
165
|
+
lines.append(f"[{s.status}] {s.id[:12]} — {label}")
|
|
166
|
+
if len(sessions) > 10:
|
|
167
|
+
lines.append(f"... and {len(sessions) - 10} more")
|
|
168
|
+
return "\n".join(lines)
|
|
@@ -42,6 +42,7 @@ class DiscordBotConfig(BaseModel):
|
|
|
42
42
|
token: str
|
|
43
43
|
agent: str # References agents key
|
|
44
44
|
command_prefix: str = "!"
|
|
45
|
+
guild_id: Optional[str] = None # Sync app commands to this guild only (instant; good for dev)
|
|
45
46
|
dm_policy: Literal["allowlist", "open"] = "allowlist"
|
|
46
47
|
allow_from: List[str] = Field(default_factory=list)
|
|
47
48
|
|
|
@@ -266,6 +266,56 @@ async def summarize_session(
|
|
|
266
266
|
return await _combine_summaries(chunk_summaries, model)
|
|
267
267
|
|
|
268
268
|
|
|
269
|
+
TITLE_SYSTEM_PROMPT = (
|
|
270
|
+
"Generate a short title (3-6 words) for this conversation. "
|
|
271
|
+
"Return only the title, nothing else. No quotes, no punctuation at the end."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
TITLE_TIMEOUT = 30
|
|
275
|
+
SHORT_TITLE_THRESHOLD = 60
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def generate_session_title(messages: list[dict], model: str) -> str:
|
|
279
|
+
"""Generate a short title from conversation messages using a cheap model."""
|
|
280
|
+
text_parts = []
|
|
281
|
+
for msg in messages:
|
|
282
|
+
content = _message_text(msg)
|
|
283
|
+
if content:
|
|
284
|
+
text_parts.append(f"{msg.get('role', 'user').upper()}: {content[:500]}")
|
|
285
|
+
if not text_parts:
|
|
286
|
+
return ""
|
|
287
|
+
convo_text = "\n\n".join(text_parts)
|
|
288
|
+
title = await asyncio.wait_for(
|
|
289
|
+
_llm_complete(TITLE_SYSTEM_PROMPT, convo_text, model),
|
|
290
|
+
timeout=TITLE_TIMEOUT,
|
|
291
|
+
)
|
|
292
|
+
return title.strip().strip('"\'')[:80]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
async def auto_title_session(
|
|
296
|
+
session_id: str,
|
|
297
|
+
user_content: str,
|
|
298
|
+
assistant_content: str,
|
|
299
|
+
agent_model: str,
|
|
300
|
+
store,
|
|
301
|
+
event_bus=None,
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Generate and store a title for a session. Skips LLM call for short prompts."""
|
|
304
|
+
if len(user_content) <= SHORT_TITLE_THRESHOLD:
|
|
305
|
+
store.update_session(session_id, title=user_content)
|
|
306
|
+
else:
|
|
307
|
+
model = infer_compaction_model(agent_model)
|
|
308
|
+
messages = [
|
|
309
|
+
{"role": "user", "content": user_content},
|
|
310
|
+
{"role": "assistant", "content": assistant_content},
|
|
311
|
+
]
|
|
312
|
+
title = await generate_session_title(messages, model)
|
|
313
|
+
if title:
|
|
314
|
+
store.update_session(session_id, title=title)
|
|
315
|
+
if event_bus:
|
|
316
|
+
event_bus.emit("session_update", {"action": "titled", "id": session_id})
|
|
317
|
+
|
|
318
|
+
|
|
269
319
|
def extract_file_paths_from_turns(turns: "list[Turn]") -> list[str]:
|
|
270
320
|
"""Extract file paths mentioned in tool calls across turns.
|
|
271
321
|
|