tsugite-cli 0.25.4__tar.gz → 0.27.0__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.25.4 → tsugite_cli-0.27.0}/.gitignore +3 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/PKG-INFO +15 -4
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/README.md +13 -3
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/pyproject.toml +12 -2
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_preparation.py +11 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/exec_directives.py +0 -6
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/exec_runner.py +3 -28
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/helpers.py +29 -2
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/history_integration.py +27 -7
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/runner.py +50 -16
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/steps.py +12 -19
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/default.md +21 -15
- tsugite_cli-0.27.0/tsugite/builtin_agents/job_reviewer.md +50 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/scheduling/SKILL.md +1 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/exec.py +1 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/run.py +6 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/usage.py +72 -4
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/config.py +11 -3
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/context_block.py +43 -4
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/agent.py +118 -52
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/record_store.py +4 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/sandbox.py +6 -7
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/subprocess_executor.py +27 -43
- tsugite_cli-0.27.0/tsugite/egress_filter.py +66 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/models.py +1 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/sqlite_backend.py +13 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/hooks.py +27 -0
- tsugite_cli-0.27.0/tsugite/link_previews.py +79 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/md_agents.py +5 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/models.py +12 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/plugins.py +17 -9
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/renderer.py +2 -2
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/schemas/agent.schema.json +24 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/__init__.py +5 -2
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/agents.py +1 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/daemon_control.py +4 -3
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/history.py +27 -61
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/http.py +7 -7
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/jobs.py +62 -9
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/schedule.py +13 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/secrets.py +8 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/sessions.py +4 -45
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/skills.py +4 -4
- tsugite_cli-0.27.0/tsugite/tools/tool_access.py +154 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui_surfaces.py +47 -16
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/usage/__init__.py +2 -0
- tsugite_cli-0.27.0/tsugite/usage/anomalies.py +85 -0
- tsugite_cli-0.27.0/tsugite/usage/cache.py +59 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/usage/store.py +34 -3
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/user_agent.py +1 -1
- tsugite_cli-0.25.4/tsugite/core/proxy.py +0 -246
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/AGENTS.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/LICENSE +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/approval.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/agent_config.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/daemon_client.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/anthropic.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/openai_compat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/artifacts.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/usage/quota.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/utils.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/templates.py +0 -0
|
@@ -183,7 +183,6 @@ benchmarks/
|
|
|
183
183
|
docker-compose.test.yml
|
|
184
184
|
#### TODO ^^^
|
|
185
185
|
|
|
186
|
-
plugins/tsugite-daemon/frontend/node_modules/
|
|
187
186
|
# Vite dep-optimizer cache. Normally under node_modules/.vite, but lands at the
|
|
188
187
|
# cwd's `.vite/` when vite/vitest is run from the repo root, so ignore it anywhere.
|
|
189
188
|
.vite/
|
|
@@ -197,3 +196,6 @@ plugins/tsugite-daemon/tsugite_daemon/web/
|
|
|
197
196
|
# Per-job git worktrees provisioned by the daemon.
|
|
198
197
|
.tsugite-jobs/
|
|
199
198
|
/packaging/runtime/.cache/
|
|
199
|
+
# Corporate CA certs dropped in for docker build.
|
|
200
|
+
/docker/ca-certificates/*
|
|
201
|
+
!/docker/ca-certificates/.gitkeep
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tsugite-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.0
|
|
4
4
|
Summary: Micro-agent runner for task automation using markdown definitions
|
|
5
5
|
Author: Justyn Shull
|
|
6
6
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
@@ -261,6 +261,7 @@ Provides-Extra: all
|
|
|
261
261
|
Requires-Dist: tsugite-acp; extra == 'all'
|
|
262
262
|
Requires-Dist: tsugite-claude-code; extra == 'all'
|
|
263
263
|
Requires-Dist: tsugite-codex-cli; extra == 'all'
|
|
264
|
+
Requires-Dist: tsugite-cursor; extra == 'all'
|
|
264
265
|
Requires-Dist: tsugite-daemon; extra == 'all'
|
|
265
266
|
Requires-Dist: tsugite-discord; extra == 'all'
|
|
266
267
|
Requires-Dist: tsugite-pty; extra == 'all'
|
|
@@ -339,7 +340,7 @@ pip install tsugite-cli # or plain pip
|
|
|
339
340
|
| --- | --- |
|
|
340
341
|
| `tsugite-cli[daemon]` | Daemon: HTTP API, web UI, scheduler, jobs, terminals (pty), sandbox, Discord |
|
|
341
342
|
| `tsugite-cli[web]` | `web_search` + YouTube transcripts |
|
|
342
|
-
| `tsugite-cli[sandbox]` |
|
|
343
|
+
| `tsugite-cli[sandbox]` | [Hozo](https://github.com/justyns/hozo) sandbox backend (needs `bwrap` on Linux; macOS uses the built-in `sandbox-exec`) |
|
|
343
344
|
| `tsugite-cli[all]` | Everything above plus the optional tmux + provider plugins |
|
|
344
345
|
|
|
345
346
|
```bash
|
|
@@ -348,6 +349,16 @@ uv tool install "tsugite-cli[daemon]" # full server install
|
|
|
348
349
|
|
|
349
350
|
The package is `tsugite-cli`, the command is `tsugite` (or `tsu` for short).
|
|
350
351
|
|
|
352
|
+
### Desktop app
|
|
353
|
+
|
|
354
|
+
On Apple Silicon Macs (macOS 13 or later), install the desktop app from the Homebrew tap:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
brew install --cask justyns/tap/tsugite
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Linux builds ship as an AppImage on the [releases page](https://github.com/justyns/tsugite/releases).
|
|
361
|
+
|
|
351
362
|
## Quick start
|
|
352
363
|
|
|
353
364
|
```bash
|
|
@@ -384,7 +395,7 @@ in nested tmux / non-Rich-friendly shells. Pick a richer or quieter mode when yo
|
|
|
384
395
|
- **Sub-agents** that can spawn other agents for specific subtasks
|
|
385
396
|
- **Skills** directory-based knowledge modules (mostly) following the [agentskills.io](https://agentskills.io/) SKILL.md format
|
|
386
397
|
- **Hooks** that fire shell commands on lifecycle events (post-tool, pre-message, pre/post-compact)
|
|
387
|
-
- **Sandbox** (
|
|
398
|
+
- **Sandbox** (Linux and macOS) via [Hozo](https://github.com/justyns/hozo) with filesystem and network isolation
|
|
388
399
|
|
|
389
400
|
## Agents in more detail
|
|
390
401
|
|
|
@@ -417,7 +428,7 @@ For a complete example, check the built-in [default agent](tsugite/builtin_agent
|
|
|
417
428
|
|
|
418
429
|
## Sandbox
|
|
419
430
|
|
|
420
|
-
|
|
431
|
+
Agent code runs inside a [Hozo](https://github.com/justyns/hozo) sandbox (bubblewrap on Linux, `sandbox-exec` on macOS) when you pass `--sandbox`:
|
|
421
432
|
|
|
422
433
|
```bash
|
|
423
434
|
tsu run +default "task" --sandbox --allow-domain "github.com"
|
|
@@ -55,7 +55,7 @@ pip install tsugite-cli # or plain pip
|
|
|
55
55
|
| --- | --- |
|
|
56
56
|
| `tsugite-cli[daemon]` | Daemon: HTTP API, web UI, scheduler, jobs, terminals (pty), sandbox, Discord |
|
|
57
57
|
| `tsugite-cli[web]` | `web_search` + YouTube transcripts |
|
|
58
|
-
| `tsugite-cli[sandbox]` |
|
|
58
|
+
| `tsugite-cli[sandbox]` | [Hozo](https://github.com/justyns/hozo) sandbox backend (needs `bwrap` on Linux; macOS uses the built-in `sandbox-exec`) |
|
|
59
59
|
| `tsugite-cli[all]` | Everything above plus the optional tmux + provider plugins |
|
|
60
60
|
|
|
61
61
|
```bash
|
|
@@ -64,6 +64,16 @@ uv tool install "tsugite-cli[daemon]" # full server install
|
|
|
64
64
|
|
|
65
65
|
The package is `tsugite-cli`, the command is `tsugite` (or `tsu` for short).
|
|
66
66
|
|
|
67
|
+
### Desktop app
|
|
68
|
+
|
|
69
|
+
On Apple Silicon Macs (macOS 13 or later), install the desktop app from the Homebrew tap:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
brew install --cask justyns/tap/tsugite
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Linux builds ship as an AppImage on the [releases page](https://github.com/justyns/tsugite/releases).
|
|
76
|
+
|
|
67
77
|
## Quick start
|
|
68
78
|
|
|
69
79
|
```bash
|
|
@@ -100,7 +110,7 @@ in nested tmux / non-Rich-friendly shells. Pick a richer or quieter mode when yo
|
|
|
100
110
|
- **Sub-agents** that can spawn other agents for specific subtasks
|
|
101
111
|
- **Skills** directory-based knowledge modules (mostly) following the [agentskills.io](https://agentskills.io/) SKILL.md format
|
|
102
112
|
- **Hooks** that fire shell commands on lifecycle events (post-tool, pre-message, pre/post-compact)
|
|
103
|
-
- **Sandbox** (
|
|
113
|
+
- **Sandbox** (Linux and macOS) via [Hozo](https://github.com/justyns/hozo) with filesystem and network isolation
|
|
104
114
|
|
|
105
115
|
## Agents in more detail
|
|
106
116
|
|
|
@@ -133,7 +143,7 @@ For a complete example, check the built-in [default agent](tsugite/builtin_agent
|
|
|
133
143
|
|
|
134
144
|
## Sandbox
|
|
135
145
|
|
|
136
|
-
|
|
146
|
+
Agent code runs inside a [Hozo](https://github.com/justyns/hozo) sandbox (bubblewrap on Linux, `sandbox-exec` on macOS) when you pass `--sandbox`:
|
|
137
147
|
|
|
138
148
|
```bash
|
|
139
149
|
tsu run +default "task" --sandbox --allow-domain "github.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tsugite-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.27.0"
|
|
4
4
|
description = "Micro-agent runner for task automation using markdown definitions"
|
|
5
5
|
authors = [{ name = "Justyn Shull" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -55,6 +55,7 @@ all = [
|
|
|
55
55
|
"tsugite-acp",
|
|
56
56
|
"tsugite-claude-code",
|
|
57
57
|
"tsugite-codex-cli",
|
|
58
|
+
"tsugite-cursor",
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
[project.scripts]
|
|
@@ -87,6 +88,7 @@ tsugite-tmux = { workspace = true }
|
|
|
87
88
|
tsugite-acp = { workspace = true }
|
|
88
89
|
tsugite-claude-code = { workspace = true }
|
|
89
90
|
tsugite-codex-cli = { workspace = true }
|
|
91
|
+
tsugite-cursor = { workspace = true }
|
|
90
92
|
tsugite-web = { workspace = true }
|
|
91
93
|
tsugite-youtube = { workspace = true }
|
|
92
94
|
tsugite-sandbox = { workspace = true }
|
|
@@ -95,6 +97,8 @@ tsugite-daemon = { workspace = true }
|
|
|
95
97
|
tsugite-discord = { workspace = true }
|
|
96
98
|
tsugite-cc-driver = { workspace = true }
|
|
97
99
|
tsugite-onlyoffice = { workspace = true }
|
|
100
|
+
tsugite-mcp = { workspace = true }
|
|
101
|
+
tsugite-forge = { workspace = true }
|
|
98
102
|
|
|
99
103
|
[tool.ruff]
|
|
100
104
|
target-version = "py311"
|
|
@@ -170,8 +174,11 @@ paths = [
|
|
|
170
174
|
"plugins/tsugite-cc-driver/tsugite_cc_driver",
|
|
171
175
|
"plugins/tsugite-claude-code/tsugite_claude_code",
|
|
172
176
|
"plugins/tsugite-codex-cli/tsugite_codex_cli",
|
|
177
|
+
"plugins/tsugite-cursor/tsugite_cursor",
|
|
173
178
|
"plugins/tsugite-daemon/tsugite_daemon",
|
|
174
179
|
"plugins/tsugite-discord/tsugite_discord",
|
|
180
|
+
"plugins/tsugite-forge/tsugite_forge",
|
|
181
|
+
"plugins/tsugite-mcp/tsugite_mcp",
|
|
175
182
|
"plugins/tsugite-onlyoffice/tsugite_onlyoffice",
|
|
176
183
|
"plugins/tsugite-pty/tsugite_pty",
|
|
177
184
|
"plugins/tsugite-sandbox/tsugite_sandbox",
|
|
@@ -238,7 +245,7 @@ ignore_names = [
|
|
|
238
245
|
"get_completions", # prompt_toolkit Completer API, dispatched by the session
|
|
239
246
|
"complete_event", # mandatory prompt_toolkit get_completions() parameter
|
|
240
247
|
"language", # CodeExecutionEvent field, read by the web UI via serialized events
|
|
241
|
-
"
|
|
248
|
+
"HozoSandbox", # plugin entry point (tsugite.sandbox: hozo), loaded via get_sandbox_class
|
|
242
249
|
# Public API / test-infra reached only from tests (vulture excludes tests).
|
|
243
250
|
"subscribe_filtered", # imperative EventBus filtered-subscription API (docs/plugin-events.md)
|
|
244
251
|
"unsubscribe_filtered",
|
|
@@ -264,6 +271,7 @@ dev = [
|
|
|
264
271
|
"tsugite-acp",
|
|
265
272
|
"tsugite-claude-code",
|
|
266
273
|
"tsugite-codex-cli",
|
|
274
|
+
"tsugite-cursor",
|
|
267
275
|
"tsugite-web",
|
|
268
276
|
"tsugite-youtube",
|
|
269
277
|
"tsugite-sandbox",
|
|
@@ -272,4 +280,6 @@ dev = [
|
|
|
272
280
|
"tsugite-discord",
|
|
273
281
|
"tsugite-cc-driver",
|
|
274
282
|
"tsugite-onlyoffice",
|
|
283
|
+
"tsugite-mcp",
|
|
284
|
+
"tsugite-forge",
|
|
275
285
|
]
|
|
@@ -330,6 +330,17 @@ class AgentPreparer:
|
|
|
330
330
|
if "notify_user" not in expanded and "notify_user" in _tools:
|
|
331
331
|
expanded.append("notify_user")
|
|
332
332
|
|
|
333
|
+
if agent_config.deferred_tools:
|
|
334
|
+
from tsugite.permissions import get_permissions
|
|
335
|
+
from tsugite.tools.tool_access import TOOL_ACCESS_TOOLS, start_turn
|
|
336
|
+
|
|
337
|
+
perms = get_permissions()
|
|
338
|
+
granted = perms.listed(f"agents.{agent_config.name}.tools") if perms else []
|
|
339
|
+
deferred = expand_tool_specs(agent_config.deferred_tools) + granted
|
|
340
|
+
deferred = [t for t in deferred if t in _tools and (has_interaction or t not in interactive_tool_names)]
|
|
341
|
+
access = start_turn(agent_config.name, expanded, deferred)
|
|
342
|
+
expanded += [t for t in TOOL_ACCESS_TOOLS + access.loaded if t in _tools and t not in expanded]
|
|
343
|
+
|
|
333
344
|
return [create_tool_from_tsugite(name) for name in expanded]
|
|
334
345
|
except Exception as e:
|
|
335
346
|
raise RuntimeError(f"Failed to create tools: {e}") from e
|
|
@@ -41,8 +41,6 @@ def _success_note(directive: ExecDirective) -> str:
|
|
|
41
41
|
def execute_exec_directives(
|
|
42
42
|
content: str,
|
|
43
43
|
existing_context: Optional[Dict[str, Any]] = None,
|
|
44
|
-
sandbox_config: Optional[Any] = None,
|
|
45
|
-
workspace_dir: Optional[str] = None,
|
|
46
44
|
event_bus: Optional[Any] = None,
|
|
47
45
|
) -> Tuple[str, Dict[str, Any]]:
|
|
48
46
|
"""Execute exec directives in `content` and return (modified_content, exec_context).
|
|
@@ -52,8 +50,6 @@ def execute_exec_directives(
|
|
|
52
50
|
existing_context: Vars from prior pipeline stages (prefetch, tool directives,
|
|
53
51
|
caller). Made available to each exec block as Python locals. Vars assigned
|
|
54
52
|
by an earlier exec block in the same content are also visible to later ones.
|
|
55
|
-
sandbox_config: Optional SandboxConfig; when set, blocks run inside bubblewrap.
|
|
56
|
-
workspace_dir: Optional workspace path for sandbox bind mounts.
|
|
57
53
|
event_bus: Optional EventBus for emitting warnings on continue_on_error paths.
|
|
58
54
|
|
|
59
55
|
Returns:
|
|
@@ -89,8 +85,6 @@ def execute_exec_directives(
|
|
|
89
85
|
locals_dict=block_locals,
|
|
90
86
|
timeout=directive.timeout,
|
|
91
87
|
continue_on_error=directive.continue_on_error,
|
|
92
|
-
sandbox_config=sandbox_config,
|
|
93
|
-
workspace_dir=workspace_dir,
|
|
94
88
|
)
|
|
95
89
|
except Exception as e:
|
|
96
90
|
if not directive.continue_on_error:
|
|
@@ -4,15 +4,10 @@ This is a deliberate parallel path to `tsugite.core.subprocess_executor.Subproce
|
|
|
4
4
|
Both run user Python in a subprocess, but they differ:
|
|
5
5
|
|
|
6
6
|
- `SubprocessExecutor` is async, turn-stateful, blocks `open()`, forces tool usage,
|
|
7
|
-
and
|
|
8
|
-
where each turn is a long-running session.
|
|
7
|
+
and runs inside the sandbox backend when sandboxed. It's designed for the LLM agent
|
|
8
|
+
loop where each turn is a long-running session.
|
|
9
9
|
- `run_python_block` (this module) is synchronous, one-shot, allows plain Python
|
|
10
10
|
(`open`, `os.listdir`, etc.), and is invoked at agent-prep time before the LLM runs.
|
|
11
|
-
|
|
12
|
-
Sandbox plumbing is deferred: passing `sandbox_config` raises `NotImplementedError`.
|
|
13
|
-
The async path's bubblewrap+proxy lifecycle does not translate cleanly to the
|
|
14
|
-
synchronous one-shot model; a future shared `build_python_subprocess_command` helper
|
|
15
|
-
would converge both paths. Any sandbox-related change must update both call sites.
|
|
16
11
|
"""
|
|
17
12
|
|
|
18
13
|
import json
|
|
@@ -126,8 +121,6 @@ def run_python_block(
|
|
|
126
121
|
locals_dict: Dict[str, Any],
|
|
127
122
|
timeout: int = 30,
|
|
128
123
|
continue_on_error: bool = False,
|
|
129
|
-
sandbox_config: Optional[Any] = None,
|
|
130
|
-
workspace_dir: Optional[str] = None,
|
|
131
124
|
) -> ExecBlockResult:
|
|
132
125
|
"""Run a Python block synchronously in a one-shot subprocess.
|
|
133
126
|
|
|
@@ -138,8 +131,6 @@ def run_python_block(
|
|
|
138
131
|
timeout: Wall-clock timeout in seconds.
|
|
139
132
|
continue_on_error: If True, exceptions are captured to result.error and
|
|
140
133
|
return_value is None instead of re-raising.
|
|
141
|
-
sandbox_config: Optional SandboxConfig; when set, blocks run in bwrap.
|
|
142
|
-
workspace_dir: Optional workspace path for sandbox bind mounts.
|
|
143
134
|
|
|
144
135
|
Raises:
|
|
145
136
|
TimeoutError: On timeout.
|
|
@@ -158,7 +149,7 @@ def run_python_block(
|
|
|
158
149
|
with open(payload_path, "w") as f:
|
|
159
150
|
json.dump({"locals": safe_locals, "code": code}, f)
|
|
160
151
|
|
|
161
|
-
cmd =
|
|
152
|
+
cmd = [sys.executable, "-u", harness_path, payload_path, result_path]
|
|
162
153
|
|
|
163
154
|
try:
|
|
164
155
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
@@ -205,19 +196,3 @@ def _filter_json_safe(d: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
205
196
|
except (TypeError, ValueError, OverflowError):
|
|
206
197
|
continue
|
|
207
198
|
return safe
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _build_command(
|
|
211
|
-
harness_path: str,
|
|
212
|
-
payload_path: str,
|
|
213
|
-
result_path: str,
|
|
214
|
-
sandbox_config: Optional[Any],
|
|
215
|
-
workspace_dir: Optional[str],
|
|
216
|
-
) -> list[str]:
|
|
217
|
-
if sandbox_config is None:
|
|
218
|
-
return [sys.executable, "-u", harness_path, payload_path, result_path]
|
|
219
|
-
raise NotImplementedError(
|
|
220
|
-
"sandbox_config support for tsu:exec is not implemented yet - the existing async "
|
|
221
|
-
"SubprocessExecutor path manages a network proxy that doesn't translate cleanly to "
|
|
222
|
-
"this synchronous one-shot path. Track in a follow-up."
|
|
223
|
-
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Shared helper functions for agent execution."""
|
|
2
2
|
|
|
3
3
|
import contextvars
|
|
4
|
+
import uuid
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import TYPE_CHECKING, Any, List, Optional
|
|
@@ -105,7 +106,7 @@ def build_sandbox_policy(
|
|
|
105
106
|
`tsu exec` so the two never drift. Agent frontmatter (network/sandbox) can only
|
|
106
107
|
tighten the CLI/daemon ceiling, never loosen it.
|
|
107
108
|
|
|
108
|
-
Raises RuntimeError if the sandbox is requested but
|
|
109
|
+
Raises RuntimeError if the sandbox is requested but no backend is available.
|
|
109
110
|
"""
|
|
110
111
|
from tsugite.agent_runner.runner import resolve_effective_sandbox
|
|
111
112
|
from tsugite.core.sandbox import SandboxConfig, sandbox_available
|
|
@@ -121,7 +122,9 @@ def build_sandbox_policy(
|
|
|
121
122
|
return None, None
|
|
122
123
|
|
|
123
124
|
if not sandbox_available():
|
|
124
|
-
raise RuntimeError(
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"No sandbox backend available. Install tsugite-sandbox (and bubblewrap on Linux), or use --no-sandbox."
|
|
127
|
+
)
|
|
125
128
|
|
|
126
129
|
ctx = SandboxContext(
|
|
127
130
|
allow_domains=allow_domains,
|
|
@@ -197,6 +200,30 @@ def get_allowed_secrets() -> Optional[List[str]]:
|
|
|
197
200
|
return _allowed_secrets_var.get()
|
|
198
201
|
|
|
199
202
|
|
|
203
|
+
_turn_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("turn_id", default=None)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def start_turn() -> contextvars.Token:
|
|
207
|
+
"""Mint a fresh turn id for the history events recorded in this context. Returns a reset token."""
|
|
208
|
+
return _turn_id_var.set(uuid.uuid4().hex)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def reset_turn(token: contextvars.Token) -> None:
|
|
212
|
+
"""Restore whatever turn id was current before the matching start_turn."""
|
|
213
|
+
_turn_id_var.reset(token)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_turn_id() -> Optional[str]:
|
|
217
|
+
"""Return the current turn's id, or None outside a turn."""
|
|
218
|
+
return _turn_id_var.get()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def turn_fields() -> dict:
|
|
222
|
+
"""The turn_id field for an event recorded now, or nothing outside a turn."""
|
|
223
|
+
turn_id = _turn_id_var.get()
|
|
224
|
+
return {"turn_id": turn_id} if turn_id else {}
|
|
225
|
+
|
|
226
|
+
|
|
200
227
|
def clear_current_agent() -> None:
|
|
201
228
|
"""Clear the currently executing agent (tests and top-level teardown)."""
|
|
202
229
|
_current_agent_var.set(None)
|
|
@@ -13,6 +13,7 @@ from dataclasses import dataclass
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any, Dict, List, Optional
|
|
15
15
|
|
|
16
|
+
from tsugite.agent_runner.helpers import get_turn_id, turn_fields
|
|
16
17
|
from tsugite.attachments.base import Attachment
|
|
17
18
|
from tsugite.config import load_config
|
|
18
19
|
from tsugite.history import (
|
|
@@ -88,6 +89,14 @@ def open_or_create_session(
|
|
|
88
89
|
return backend.create(agent_name=agent_name, model=model, workspace=workspace)
|
|
89
90
|
|
|
90
91
|
|
|
92
|
+
def _turn_has(storage: Session, event_type: str) -> Optional[bool]:
|
|
93
|
+
"""Whether the current turn recorded an event_type; None outside a turn."""
|
|
94
|
+
turn_id = get_turn_id()
|
|
95
|
+
if turn_id is None:
|
|
96
|
+
return None
|
|
97
|
+
return any(e.data.get("turn_id") == turn_id for e in storage.iter_events(types=(event_type,)))
|
|
98
|
+
|
|
99
|
+
|
|
91
100
|
def _current_turn_already_has_user_input(storage: Session) -> bool:
|
|
92
101
|
"""True if the in-progress turn already recorded a user_input.
|
|
93
102
|
|
|
@@ -98,7 +107,13 @@ def _current_turn_already_has_user_input(storage: Session) -> bool:
|
|
|
98
107
|
A genuinely repeated message opens a new turn *after* the prior session_end,
|
|
99
108
|
so this stays False for it. A final_result also ends a turn copied without
|
|
100
109
|
its session_end (branches, older compacted sessions).
|
|
110
|
+
|
|
111
|
+
That event-order rule covers callers with no turn id. With one set, only a
|
|
112
|
+
user_input carrying it counts.
|
|
101
113
|
"""
|
|
114
|
+
has = _turn_has(storage, "user_input")
|
|
115
|
+
if has is not None:
|
|
116
|
+
return has
|
|
102
117
|
events = list(storage.iter_events(types=("user_input", "session_end", "final_result")))
|
|
103
118
|
return bool(events) and events[-1].type == "user_input"
|
|
104
119
|
|
|
@@ -122,7 +137,7 @@ def record_user_input(
|
|
|
122
137
|
"""
|
|
123
138
|
if _current_turn_already_has_user_input(storage):
|
|
124
139
|
return
|
|
125
|
-
data: Dict[str, Any] = {"text": text}
|
|
140
|
+
data: Dict[str, Any] = {"text": text, **turn_fields()}
|
|
126
141
|
# Only files the user actually uploaded ride here: this field is display-only
|
|
127
142
|
# (the web UI renders them as clickable `uploads/<name>` chips). The agent's
|
|
128
143
|
# auto-included context (workspace memory files like USER.md, config
|
|
@@ -158,11 +173,15 @@ def record_final_result(
|
|
|
158
173
|
the agent reaches its end-of-run recording). Skip in that case so the
|
|
159
174
|
conversation view doesn't render the answer twice.
|
|
160
175
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
176
|
+
has = _turn_has(storage, "final_result")
|
|
177
|
+
if has:
|
|
178
|
+
return
|
|
179
|
+
if has is None:
|
|
180
|
+
for event in reversed(list(storage.iter_events(types=("user_input", "final_result")))):
|
|
181
|
+
if event.type == "user_input":
|
|
182
|
+
break
|
|
183
|
+
if event.type == "final_result":
|
|
184
|
+
return
|
|
166
185
|
storage.record(
|
|
167
186
|
"final_result",
|
|
168
187
|
result=result,
|
|
@@ -170,6 +189,7 @@ def record_final_result(
|
|
|
170
189
|
turns=turns,
|
|
171
190
|
tokens=tokens,
|
|
172
191
|
cost=cost,
|
|
192
|
+
**turn_fields(),
|
|
173
193
|
)
|
|
174
194
|
|
|
175
195
|
|
|
@@ -189,7 +209,7 @@ def record_session_end(
|
|
|
189
209
|
liveness. The two stores tracking different things (last-turn outcome vs
|
|
190
210
|
conversation open/closed) is expected, not a desync.
|
|
191
211
|
"""
|
|
192
|
-
storage.record("session_end", status=status, error_message=error_message)
|
|
212
|
+
storage.record("session_end", status=status, error_message=error_message, **turn_fields())
|
|
193
213
|
|
|
194
214
|
|
|
195
215
|
def save_run_to_history(
|
|
@@ -12,7 +12,6 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional # noqa: E402
|
|
|
12
12
|
|
|
13
13
|
from tsugite.core.agent import NO_ACTION_MESSAGE, AgentResult, TsugiteAgent # noqa: E402
|
|
14
14
|
from tsugite.core.executor_registry import get_executor_class # noqa: E402
|
|
15
|
-
from tsugite.core.proxy import _parse_pattern # noqa: E402
|
|
16
15
|
from tsugite.core.state import session_state_path # noqa: E402
|
|
17
16
|
from tsugite.exceptions import ( # noqa: E402
|
|
18
17
|
AgentExecutionError,
|
|
@@ -29,13 +28,16 @@ from .helpers import ( # noqa: E402
|
|
|
29
28
|
_stderr_console,
|
|
30
29
|
build_sandbox_policy,
|
|
31
30
|
get_display_console,
|
|
31
|
+
get_turn_id,
|
|
32
32
|
get_ui_handler,
|
|
33
33
|
reset_allowed_secrets,
|
|
34
34
|
reset_current_agent,
|
|
35
35
|
reset_sandbox_context,
|
|
36
|
+
reset_turn,
|
|
36
37
|
set_allowed_secrets,
|
|
37
38
|
set_current_agent,
|
|
38
39
|
set_sandbox_context,
|
|
40
|
+
start_turn,
|
|
39
41
|
)
|
|
40
42
|
from .models import AgentExecutionResult, AgentSkippedError # noqa: E402
|
|
41
43
|
|
|
@@ -497,7 +499,7 @@ async def _execute_agent_with_prompt(
|
|
|
497
499
|
)
|
|
498
500
|
|
|
499
501
|
if sandbox_config is not None:
|
|
500
|
-
#
|
|
502
|
+
# Sandboxing is a subprocess feature, so a sandboxed run always uses
|
|
501
503
|
# the subprocess executor regardless of the configured default backend.
|
|
502
504
|
from tsugite.core.subprocess_executor import SubprocessExecutor
|
|
503
505
|
|
|
@@ -582,6 +584,10 @@ async def _execute_agent_with_prompt(
|
|
|
582
584
|
pre_llm_call=pre_llm_call_cb,
|
|
583
585
|
expects_action=requires_action or (channel_metadata or {}).get("source") == "scheduler",
|
|
584
586
|
)
|
|
587
|
+
if agent_config.deferred_tools:
|
|
588
|
+
from tsugite.tools.tool_access import get_access
|
|
589
|
+
|
|
590
|
+
get_access().agent = agent
|
|
585
591
|
# Set event_bus in context so tools can access it during execution
|
|
586
592
|
_setup_event_context(event_bus)
|
|
587
593
|
|
|
@@ -609,13 +615,14 @@ async def _execute_agent_with_prompt(
|
|
|
609
615
|
)
|
|
610
616
|
|
|
611
617
|
try:
|
|
618
|
+
from tsugite.models import resolve_usage_model
|
|
612
619
|
from tsugite.usage import get_usage_store
|
|
613
620
|
|
|
614
621
|
meta = channel_metadata or {}
|
|
615
622
|
get_usage_store().record(
|
|
616
623
|
session_id=continue_conversation_id,
|
|
617
624
|
agent=prepared.agent_config.name,
|
|
618
|
-
model=model_string,
|
|
625
|
+
model=resolve_usage_model(model_string),
|
|
619
626
|
source=meta.get("source") or "cli",
|
|
620
627
|
schedule_name=meta.get("schedule_id"),
|
|
621
628
|
input_tokens=agent.prompt_tokens,
|
|
@@ -750,8 +757,8 @@ def resolve_effective_sandbox(
|
|
|
750
757
|
desired = set(fm_network.get("domains") or []) | set(fm_sandbox.get("allow_domains") or [])
|
|
751
758
|
base = list(daemon_domains or [])
|
|
752
759
|
if desired:
|
|
753
|
-
# Keep each desired pattern only if the daemon ceiling permits it, using
|
|
754
|
-
#
|
|
760
|
+
# Keep each desired pattern only if the daemon ceiling permits it, using
|
|
761
|
+
# glob semantics (so agent "api.github.com" is kept under daemon
|
|
755
762
|
# "*.github.com"). An empty ceiling permits everything.
|
|
756
763
|
effective = sorted(d for d in desired if _domain_within_ceiling(d, base))
|
|
757
764
|
if not effective:
|
|
@@ -764,10 +771,25 @@ def resolve_effective_sandbox(
|
|
|
764
771
|
return enabled, allow_domains, no_network
|
|
765
772
|
|
|
766
773
|
|
|
774
|
+
def _parse_pattern(pattern: str) -> tuple[str, set[int]]:
|
|
775
|
+
"""No port means {80, 443} and `:*` means any port (empty set). IPv6 takes a port
|
|
776
|
+
only in brackets (`[fd00::/8]:443`), as Hozo's proxy does."""
|
|
777
|
+
if pattern.startswith("["):
|
|
778
|
+
domain, _, port_str = pattern[1:].partition("]")
|
|
779
|
+
port_str = port_str.removeprefix(":")
|
|
780
|
+
elif pattern.count(":") == 1:
|
|
781
|
+
domain, port_str = pattern.split(":")
|
|
782
|
+
else:
|
|
783
|
+
domain, port_str = pattern, ""
|
|
784
|
+
if not port_str:
|
|
785
|
+
return (domain, {80, 443})
|
|
786
|
+
return (domain, set() if port_str == "*" else {int(port_str)})
|
|
787
|
+
|
|
788
|
+
|
|
767
789
|
def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
|
|
768
790
|
"""True if a desired domain:port pattern is permitted by the daemon ceiling.
|
|
769
791
|
|
|
770
|
-
Uses
|
|
792
|
+
Uses glob + port semantics so e.g. 'api.github.com' is within
|
|
771
793
|
'*.github.com', but 'github.com:22' is NOT within 'github.com' (which only allows
|
|
772
794
|
the default 80/443). The ceiling is a union: the desired ports must be covered by
|
|
773
795
|
the union of port sets from ALL ceiling patterns whose domain glob matches (so
|
|
@@ -777,9 +799,6 @@ def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
|
|
|
777
799
|
if not ceiling:
|
|
778
800
|
return True
|
|
779
801
|
d_domain, d_ports = _parse_pattern(desired_pattern.lower())
|
|
780
|
-
# The proxy treats the allowlist as a union, so collect the ports from EVERY
|
|
781
|
-
# ceiling pattern whose domain glob matches and check the desired ports against
|
|
782
|
-
# their union (e.g. ["github.com:80","github.com:443"] together cover 80/443).
|
|
783
802
|
matched = False
|
|
784
803
|
allowed_ports: set = set()
|
|
785
804
|
for c in ceiling:
|
|
@@ -1082,7 +1101,21 @@ async def run_agent_async(
|
|
|
1082
1101
|
if context is None:
|
|
1083
1102
|
context = {}
|
|
1084
1103
|
|
|
1085
|
-
from tsugite.hooks import fire_pre_message_hooks
|
|
1104
|
+
from tsugite.hooks import fire_pre_message_hooks, reset_skip_hooks, resolve_skip_hooks, set_skip_hooks
|
|
1105
|
+
|
|
1106
|
+
# Parse agent configuration (with inheritance resolution)
|
|
1107
|
+
try:
|
|
1108
|
+
agent = parse_agent_file(agent_path)
|
|
1109
|
+
agent_config = agent.config
|
|
1110
|
+
except Exception as e:
|
|
1111
|
+
raise ValueError(f"Failed to parse agent file: {e}")
|
|
1112
|
+
|
|
1113
|
+
skip_hooks_token = set_skip_hooks(
|
|
1114
|
+
{
|
|
1115
|
+
*resolve_skip_hooks(agent_config.hooks, agent_config.skip_hooks),
|
|
1116
|
+
*(channel_metadata or {}).get("skip_hooks", []),
|
|
1117
|
+
}
|
|
1118
|
+
)
|
|
1086
1119
|
|
|
1087
1120
|
hooks_dir = workspace.path if workspace else (path_context.effective_cwd if path_context else Path.cwd())
|
|
1088
1121
|
hook_message = context.pop("raw_message", prompt)
|
|
@@ -1160,17 +1193,14 @@ async def run_agent_async(
|
|
|
1160
1193
|
# New conversation (e.g., fresh workspace session) - start with empty history
|
|
1161
1194
|
pass
|
|
1162
1195
|
|
|
1163
|
-
|
|
1164
|
-
try:
|
|
1165
|
-
agent = parse_agent_file(agent_path)
|
|
1166
|
-
agent_config = agent.config
|
|
1167
|
-
except Exception as e:
|
|
1168
|
-
raise ValueError(f"Failed to parse agent file: {e}")
|
|
1196
|
+
from tsugite.tools.tool_access import current_access
|
|
1169
1197
|
|
|
1170
1198
|
# Scope this run's ambient state with tokens: an agent hook nests a full run
|
|
1171
1199
|
# inside this one and must restore what it found rather than clobber it.
|
|
1172
1200
|
agent_token = set_current_agent(agent_config.name)
|
|
1173
1201
|
secrets_token = set_allowed_secrets(agent_config.allowed_secrets)
|
|
1202
|
+
turn_token = start_turn() if get_turn_id() is None else None
|
|
1203
|
+
access_token = current_access.set(None)
|
|
1174
1204
|
|
|
1175
1205
|
setup = _RunSetup(
|
|
1176
1206
|
exec_options=exec_options,
|
|
@@ -1205,6 +1235,10 @@ async def run_agent_async(
|
|
|
1205
1235
|
finally:
|
|
1206
1236
|
reset_current_agent(agent_token)
|
|
1207
1237
|
reset_allowed_secrets(secrets_token)
|
|
1238
|
+
reset_skip_hooks(skip_hooks_token)
|
|
1239
|
+
current_access.reset(access_token)
|
|
1240
|
+
if turn_token is not None:
|
|
1241
|
+
reset_turn(turn_token)
|
|
1208
1242
|
|
|
1209
1243
|
|
|
1210
1244
|
def preview_multistep_agent(
|