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