tsugite-cli 0.25.4__tar.gz → 0.26.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.26.0}/PKG-INFO +4 -4
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/README.md +3 -3
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/pyproject.toml +2 -2
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/exec_directives.py +0 -6
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/exec_runner.py +3 -28
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/helpers.py +29 -2
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/history_integration.py +27 -7
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/runner.py +27 -9
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/steps.py +12 -19
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_agents/default.md +2 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/exec.py +1 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/run.py +6 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/usage.py +72 -4
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/config.py +1 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/agent.py +77 -42
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/sandbox.py +6 -7
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/subprocess_executor.py +27 -43
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/md_agents.py +1 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/models.py +12 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/schemas/agent.schema.json +5 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/agents.py +1 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/http.py +2 -3
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/jobs.py +8 -1
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/sessions.py +2 -2
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/usage/__init__.py +2 -0
- tsugite_cli-0.26.0/tsugite/usage/anomalies.py +85 -0
- tsugite_cli-0.26.0/tsugite/usage/cache.py +59 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/usage/store.py +34 -3
- tsugite_cli-0.25.4/tsugite/core/proxy.py +0 -246
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/.gitignore +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/AGENTS.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/LICENSE +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/approval.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/agent_config.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/daemon_client.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/anthropic.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/openai_compat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/artifacts.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/daemon_control.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/usage/quota.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/utils.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.25.4 → tsugite_cli-0.26.0}/tsugite/workspace/templates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tsugite-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.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
|
|
@@ -339,7 +339,7 @@ pip install tsugite-cli # or plain pip
|
|
|
339
339
|
| --- | --- |
|
|
340
340
|
| `tsugite-cli[daemon]` | Daemon: HTTP API, web UI, scheduler, jobs, terminals (pty), sandbox, Discord |
|
|
341
341
|
| `tsugite-cli[web]` | `web_search` + YouTube transcripts |
|
|
342
|
-
| `tsugite-cli[sandbox]` |
|
|
342
|
+
| `tsugite-cli[sandbox]` | [Hozo](https://github.com/justyns/hozo) sandbox backend (needs `bwrap` on Linux; macOS uses the built-in `sandbox-exec`) |
|
|
343
343
|
| `tsugite-cli[all]` | Everything above plus the optional tmux + provider plugins |
|
|
344
344
|
|
|
345
345
|
```bash
|
|
@@ -384,7 +384,7 @@ in nested tmux / non-Rich-friendly shells. Pick a richer or quieter mode when yo
|
|
|
384
384
|
- **Sub-agents** that can spawn other agents for specific subtasks
|
|
385
385
|
- **Skills** directory-based knowledge modules (mostly) following the [agentskills.io](https://agentskills.io/) SKILL.md format
|
|
386
386
|
- **Hooks** that fire shell commands on lifecycle events (post-tool, pre-message, pre/post-compact)
|
|
387
|
-
- **Sandbox** (
|
|
387
|
+
- **Sandbox** (Linux and macOS) via [Hozo](https://github.com/justyns/hozo) with filesystem and network isolation
|
|
388
388
|
|
|
389
389
|
## Agents in more detail
|
|
390
390
|
|
|
@@ -417,7 +417,7 @@ For a complete example, check the built-in [default agent](tsugite/builtin_agent
|
|
|
417
417
|
|
|
418
418
|
## Sandbox
|
|
419
419
|
|
|
420
|
-
|
|
420
|
+
Agent code runs inside a [Hozo](https://github.com/justyns/hozo) sandbox (bubblewrap on Linux, `sandbox-exec` on macOS) when you pass `--sandbox`:
|
|
421
421
|
|
|
422
422
|
```bash
|
|
423
423
|
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
|
|
@@ -100,7 +100,7 @@ in nested tmux / non-Rich-friendly shells. Pick a richer or quieter mode when yo
|
|
|
100
100
|
- **Sub-agents** that can spawn other agents for specific subtasks
|
|
101
101
|
- **Skills** directory-based knowledge modules (mostly) following the [agentskills.io](https://agentskills.io/) SKILL.md format
|
|
102
102
|
- **Hooks** that fire shell commands on lifecycle events (post-tool, pre-message, pre/post-compact)
|
|
103
|
-
- **Sandbox** (
|
|
103
|
+
- **Sandbox** (Linux and macOS) via [Hozo](https://github.com/justyns/hozo) with filesystem and network isolation
|
|
104
104
|
|
|
105
105
|
## Agents in more detail
|
|
106
106
|
|
|
@@ -133,7 +133,7 @@ For a complete example, check the built-in [default agent](tsugite/builtin_agent
|
|
|
133
133
|
|
|
134
134
|
## Sandbox
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
Agent code runs inside a [Hozo](https://github.com/justyns/hozo) sandbox (bubblewrap on Linux, `sandbox-exec` on macOS) when you pass `--sandbox`:
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
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.26.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"
|
|
@@ -238,7 +238,7 @@ ignore_names = [
|
|
|
238
238
|
"get_completions", # prompt_toolkit Completer API, dispatched by the session
|
|
239
239
|
"complete_event", # mandatory prompt_toolkit get_completions() parameter
|
|
240
240
|
"language", # CodeExecutionEvent field, read by the web UI via serialized events
|
|
241
|
-
"
|
|
241
|
+
"HozoSandbox", # plugin entry point (tsugite.sandbox: hozo), loaded via get_sandbox_class
|
|
242
242
|
# Public API / test-infra reached only from tests (vulture excludes tests).
|
|
243
243
|
"subscribe_filtered", # imperative EventBus filtered-subscription API (docs/plugin-events.md)
|
|
244
244
|
"unsubscribe_filtered",
|
|
@@ -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
|
|
|
@@ -609,13 +611,14 @@ async def _execute_agent_with_prompt(
|
|
|
609
611
|
)
|
|
610
612
|
|
|
611
613
|
try:
|
|
614
|
+
from tsugite.models import resolve_usage_model
|
|
612
615
|
from tsugite.usage import get_usage_store
|
|
613
616
|
|
|
614
617
|
meta = channel_metadata or {}
|
|
615
618
|
get_usage_store().record(
|
|
616
619
|
session_id=continue_conversation_id,
|
|
617
620
|
agent=prepared.agent_config.name,
|
|
618
|
-
model=model_string,
|
|
621
|
+
model=resolve_usage_model(model_string),
|
|
619
622
|
source=meta.get("source") or "cli",
|
|
620
623
|
schedule_name=meta.get("schedule_id"),
|
|
621
624
|
input_tokens=agent.prompt_tokens,
|
|
@@ -750,8 +753,8 @@ def resolve_effective_sandbox(
|
|
|
750
753
|
desired = set(fm_network.get("domains") or []) | set(fm_sandbox.get("allow_domains") or [])
|
|
751
754
|
base = list(daemon_domains or [])
|
|
752
755
|
if desired:
|
|
753
|
-
# Keep each desired pattern only if the daemon ceiling permits it, using
|
|
754
|
-
#
|
|
756
|
+
# Keep each desired pattern only if the daemon ceiling permits it, using
|
|
757
|
+
# glob semantics (so agent "api.github.com" is kept under daemon
|
|
755
758
|
# "*.github.com"). An empty ceiling permits everything.
|
|
756
759
|
effective = sorted(d for d in desired if _domain_within_ceiling(d, base))
|
|
757
760
|
if not effective:
|
|
@@ -764,10 +767,25 @@ def resolve_effective_sandbox(
|
|
|
764
767
|
return enabled, allow_domains, no_network
|
|
765
768
|
|
|
766
769
|
|
|
770
|
+
def _parse_pattern(pattern: str) -> tuple[str, set[int]]:
|
|
771
|
+
"""No port means {80, 443} and `:*` means any port (empty set). IPv6 takes a port
|
|
772
|
+
only in brackets (`[fd00::/8]:443`), as Hozo's proxy does."""
|
|
773
|
+
if pattern.startswith("["):
|
|
774
|
+
domain, _, port_str = pattern[1:].partition("]")
|
|
775
|
+
port_str = port_str.removeprefix(":")
|
|
776
|
+
elif pattern.count(":") == 1:
|
|
777
|
+
domain, port_str = pattern.split(":")
|
|
778
|
+
else:
|
|
779
|
+
domain, port_str = pattern, ""
|
|
780
|
+
if not port_str:
|
|
781
|
+
return (domain, {80, 443})
|
|
782
|
+
return (domain, set() if port_str == "*" else {int(port_str)})
|
|
783
|
+
|
|
784
|
+
|
|
767
785
|
def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
|
|
768
786
|
"""True if a desired domain:port pattern is permitted by the daemon ceiling.
|
|
769
787
|
|
|
770
|
-
Uses
|
|
788
|
+
Uses glob + port semantics so e.g. 'api.github.com' is within
|
|
771
789
|
'*.github.com', but 'github.com:22' is NOT within 'github.com' (which only allows
|
|
772
790
|
the default 80/443). The ceiling is a union: the desired ports must be covered by
|
|
773
791
|
the union of port sets from ALL ceiling patterns whose domain glob matches (so
|
|
@@ -777,9 +795,6 @@ def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
|
|
|
777
795
|
if not ceiling:
|
|
778
796
|
return True
|
|
779
797
|
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
798
|
matched = False
|
|
784
799
|
allowed_ports: set = set()
|
|
785
800
|
for c in ceiling:
|
|
@@ -1171,6 +1186,7 @@ async def run_agent_async(
|
|
|
1171
1186
|
# inside this one and must restore what it found rather than clobber it.
|
|
1172
1187
|
agent_token = set_current_agent(agent_config.name)
|
|
1173
1188
|
secrets_token = set_allowed_secrets(agent_config.allowed_secrets)
|
|
1189
|
+
turn_token = start_turn() if get_turn_id() is None else None
|
|
1174
1190
|
|
|
1175
1191
|
setup = _RunSetup(
|
|
1176
1192
|
exec_options=exec_options,
|
|
@@ -1205,6 +1221,8 @@ async def run_agent_async(
|
|
|
1205
1221
|
finally:
|
|
1206
1222
|
reset_current_agent(agent_token)
|
|
1207
1223
|
reset_allowed_secrets(secrets_token)
|
|
1224
|
+
if turn_token is not None:
|
|
1225
|
+
reset_turn(turn_token)
|
|
1208
1226
|
|
|
1209
1227
|
|
|
1210
1228
|
def preview_multistep_agent(
|
|
@@ -17,7 +17,9 @@ from .helpers import (
|
|
|
17
17
|
clear_multistep_ui_context,
|
|
18
18
|
get_ui_handler,
|
|
19
19
|
print_step_progress,
|
|
20
|
+
reset_turn,
|
|
20
21
|
set_multistep_ui_context,
|
|
22
|
+
start_turn,
|
|
21
23
|
)
|
|
22
24
|
from .metrics import StepMetrics, display_step_metrics
|
|
23
25
|
from .models import AgentExecutionResult, AgentSkippedError
|
|
@@ -29,22 +31,6 @@ if TYPE_CHECKING:
|
|
|
29
31
|
logger = logging.getLogger(__name__)
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
class ExecutionContext:
|
|
33
|
-
"""Namespace for tsugite-provided execution context.
|
|
34
|
-
|
|
35
|
-
Provides access to runtime metadata via attribute access (ctx.user_prompt, ctx.tasks, etc.)
|
|
36
|
-
while keeping user-assigned step variables as top-level names in the execution namespace.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self, **kwargs):
|
|
40
|
-
for k, v in kwargs.items():
|
|
41
|
-
setattr(self, k, v)
|
|
42
|
-
|
|
43
|
-
def __repr__(self):
|
|
44
|
-
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
|
|
45
|
-
return f"ExecutionContext({attrs})"
|
|
46
|
-
|
|
47
|
-
|
|
48
34
|
def _build_step_error_message(
|
|
49
35
|
error_type: str,
|
|
50
36
|
step_name: str,
|
|
@@ -92,7 +78,7 @@ def _build_step_error_message(
|
|
|
92
78
|
def _build_injectable_vars(step_context: Dict[str, Any], assigned_vars: Optional[set] = None) -> Dict[str, Any]:
|
|
93
79
|
"""Build variables for injection into Python execution namespace.
|
|
94
80
|
|
|
95
|
-
Creates a `ctx`
|
|
81
|
+
Creates a `ctx` dict containing everything. Only user-assigned step variables
|
|
96
82
|
(from `assign="varname"`) are exposed at top-level to avoid namespace pollution.
|
|
97
83
|
|
|
98
84
|
Args:
|
|
@@ -101,9 +87,9 @@ def _build_injectable_vars(step_context: Dict[str, Any], assigned_vars: Optional
|
|
|
101
87
|
If None, no variables are exposed at top-level.
|
|
102
88
|
|
|
103
89
|
Returns:
|
|
104
|
-
Dictionary with 'ctx'
|
|
90
|
+
Dictionary with 'ctx' dict and user-assigned variables at top-level
|
|
105
91
|
"""
|
|
106
|
-
ctx =
|
|
92
|
+
ctx = dict(step_context)
|
|
107
93
|
|
|
108
94
|
# Only user-assigned step variables at top-level (not tsugite metadata)
|
|
109
95
|
if assigned_vars:
|
|
@@ -576,6 +562,8 @@ async def run_steps(
|
|
|
576
562
|
except Exception as e:
|
|
577
563
|
logger.debug("Could not pre-create session for multi-step run: %s", e)
|
|
578
564
|
|
|
565
|
+
# The first step continues the run's turn. Each later step or iteration starts its own.
|
|
566
|
+
turn_token = None
|
|
579
567
|
try:
|
|
580
568
|
# Execute each step sequentially
|
|
581
569
|
final_result = None
|
|
@@ -600,6 +588,9 @@ async def run_steps(
|
|
|
600
588
|
step_context["iteration"] = iteration
|
|
601
589
|
step_context["max_iterations"] = step.max_iterations
|
|
602
590
|
step_context["is_looping_step"] = step_is_looping
|
|
591
|
+
if i > 1 or iteration > 1:
|
|
592
|
+
token = start_turn()
|
|
593
|
+
turn_token = turn_token or token
|
|
603
594
|
|
|
604
595
|
# Show step progress (unless in debug mode which has its own output)
|
|
605
596
|
if step_is_looping:
|
|
@@ -714,4 +705,6 @@ async def run_steps(
|
|
|
714
705
|
session_id=setup.conversation_id,
|
|
715
706
|
)
|
|
716
707
|
finally:
|
|
708
|
+
if turn_token is not None:
|
|
709
|
+
reset_turn(turn_token)
|
|
717
710
|
clear_multistep_ui_context(custom_logger)
|
|
@@ -179,6 +179,8 @@ spawn_job(
|
|
|
179
179
|
|
|
180
180
|
**Notification cost**: `notify_when` defaults to `"never"`. Set it to `"stuck"` for a wake-up only when the Job needs user intervention, or `"terminal"` for any final state. Every notification adds a turn to this conversation - prefer polling `get_job` over `notify_when="terminal"` for routine tracking.
|
|
181
181
|
|
|
182
|
+
**Long Jobs**: pass `notify_after_minutes=N` for one check-in message with the Job's state and last activity when N minutes pass and it has not finished. It does not stop the Job and does not interact with `timeout_minutes` or `notify_when`. Use it in place of polling `get_job`.
|
|
183
|
+
|
|
182
184
|
**When a Job goes `stuck`** (max_attempts verifier rejections), the user can retry-with-hint or mark-done-anyway from the UI; you usually don't need to act. If asked to diagnose, `get_job(job_id)` returns the worker session id and each `ac_results[].reason` - read those, suggest a different approach, or open the worker session via `session_status` to inspect what went wrong.
|
|
183
185
|
|
|
184
186
|
**A Job spawned by the user via `/job`** appears in your conversation's job list once it resolves. You don't create the Job in that case - the user did. Your role is to make use of its result when relevant.
|
|
@@ -40,7 +40,7 @@ def exec_cmd(
|
|
|
40
40
|
workspace: Optional[str] = typer.Option(
|
|
41
41
|
None, "--workspace", "-w", help="Working directory for the snippet and its tools"
|
|
42
42
|
),
|
|
43
|
-
sandbox: bool = typer.Option(False, "--sandbox", help="Run the snippet in a
|
|
43
|
+
sandbox: bool = typer.Option(False, "--sandbox", help="Run the snippet in a sandbox"),
|
|
44
44
|
no_sandbox: bool = typer.Option(False, "--no-sandbox", help="Disable sandbox (overrides config)"),
|
|
45
45
|
allow_domain: Optional[List[str]] = typer.Option(
|
|
46
46
|
None, "--allow-domain", help="Domain(s) allowed in sandbox (implies --sandbox)"
|
|
@@ -257,7 +257,7 @@ def run(
|
|
|
257
257
|
),
|
|
258
258
|
no_workspace: bool = typer.Option(False, "--no-workspace", help="Disable workspace (ignore default workspace)"),
|
|
259
259
|
new_session: bool = typer.Option(False, "--new-session", help="Start fresh session (ignore workspace session)"),
|
|
260
|
-
sandbox: bool = typer.Option(False, "--sandbox", help="Run agent code in
|
|
260
|
+
sandbox: bool = typer.Option(False, "--sandbox", help="Run agent code in a sandbox"),
|
|
261
261
|
no_sandbox: bool = typer.Option(False, "--no-sandbox", help="Disable sandbox (overrides config)"),
|
|
262
262
|
allow_domain: Optional[List[str]] = typer.Option(
|
|
263
263
|
None,
|
|
@@ -283,6 +283,7 @@ def run(
|
|
|
283
283
|
tsu run --continue --conversation-id CONV_ID "prompt" # Continue specific conversation
|
|
284
284
|
"""
|
|
285
285
|
from tsugite.agent_runner import get_agent_info, run_agent
|
|
286
|
+
from tsugite.agent_runner.helpers import reset_turn, start_turn
|
|
286
287
|
from tsugite.console import get_stderr_console
|
|
287
288
|
from tsugite.md_agents import validate_agent_execution
|
|
288
289
|
from tsugite.secrets import init_cli as init_secrets
|
|
@@ -597,6 +598,8 @@ def run(
|
|
|
597
598
|
except Exception:
|
|
598
599
|
pass
|
|
599
600
|
|
|
601
|
+
# One turn id for the run and the save_run_to_history after it.
|
|
602
|
+
turn_token = start_turn()
|
|
600
603
|
try:
|
|
601
604
|
executor_kwargs = _build_executor_kwargs(
|
|
602
605
|
agent_file,
|
|
@@ -653,3 +656,5 @@ def run(
|
|
|
653
656
|
if not ui_opts.log_json:
|
|
654
657
|
stderr_console.print("\n[dim]Use --log-json for machine-readable output[/dim]")
|
|
655
658
|
raise typer.Exit(1)
|
|
659
|
+
finally:
|
|
660
|
+
reset_turn(turn_token)
|