tsugite-cli 0.24.1__tar.gz → 0.25.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.24.1 → tsugite_cli-0.25.0}/.gitignore +1 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/PKG-INFO +2 -1
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/pyproject.toml +2 -1
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_preparation.py +6 -5
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/__init__.py +0 -2
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/runner.py +22 -61
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/approval.py +14 -7
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_agents/default.md +3 -10
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_agents/job_verifier.md +3 -1
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +1 -1
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/response-patterns/SKILL.md +11 -14
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/helpers.py +2 -2
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/run.py +1 -52
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/agent.py +86 -2
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/base.py +2 -2
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/models.py +22 -9
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/sqlite_backend.py +15 -7
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/ui_events.py +1 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/permissions.py +34 -5
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/__init__.py +6 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/agents.py +2 -17
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/http.py +23 -11
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/jobs.py +5 -4
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/notify.py +21 -6
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/sessions.py +216 -6
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/shell_tools.py +6 -3
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/workspace/models.py +22 -6
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/AGENTS.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/LICENSE +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/README.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/history_integration.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/steps.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/agent_config.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/config.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/subprocess_executor.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/daemon_client.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/models.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/anthropic.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/openai_compat.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/artifacts.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/daemon_control.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/usage/quota.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/utils.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.24.1 → tsugite_cli-0.25.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.25.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
|
|
@@ -241,6 +241,7 @@ License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
|
241
241
|
License-File: LICENSE
|
|
242
242
|
Keywords: agents,automation,cli,jinja2
|
|
243
243
|
Requires-Python: >=3.11
|
|
244
|
+
Requires-Dist: certifi>=2026.10.23
|
|
244
245
|
Requires-Dist: cryptography>=46.0.5
|
|
245
246
|
Requires-Dist: html2text>=2020.1.16
|
|
246
247
|
Requires-Dist: httpx>=0.25
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tsugite-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.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"
|
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
|
12
12
|
"jinja2>=3.1",
|
|
13
13
|
"pyyaml>=6.0",
|
|
14
14
|
"httpx>=0.25",
|
|
15
|
+
"certifi>=2026.10.23",
|
|
15
16
|
"rich>=13.0",
|
|
16
17
|
"questionary>=2.1.1",
|
|
17
18
|
"html2text>=2020.1.16",
|
|
@@ -434,7 +434,6 @@ class AgentPreparer:
|
|
|
434
434
|
_combine_instructions,
|
|
435
435
|
execute_exec_directives,
|
|
436
436
|
execute_tool_directives,
|
|
437
|
-
get_default_instructions,
|
|
438
437
|
)
|
|
439
438
|
from tsugite.core.agent import build_system_prompt
|
|
440
439
|
from tsugite.renderer import AgentRenderer
|
|
@@ -506,12 +505,16 @@ class AgentPreparer:
|
|
|
506
505
|
skip_reason=skip_reason,
|
|
507
506
|
)
|
|
508
507
|
|
|
508
|
+
tools = self._expand_tools(agent_config, full_context, interactive_mode)
|
|
509
|
+
# Templates test membership against the names actually installed, not the
|
|
510
|
+
# frontmatter specs (`"@secrets"`).
|
|
511
|
+
full_context["tools"] = [t.name for t in tools]
|
|
512
|
+
|
|
509
513
|
try:
|
|
510
514
|
rendered_prompt = renderer.render(modified_content, full_context)
|
|
511
515
|
except Exception as e:
|
|
512
516
|
raise RuntimeError(f"Template rendering failed: {e}") from e
|
|
513
517
|
|
|
514
|
-
base_instructions = get_default_instructions()
|
|
515
518
|
agent_instructions = getattr(agent_config, "instructions", "")
|
|
516
519
|
|
|
517
520
|
# Render agent instructions as Jinja2 template
|
|
@@ -521,9 +524,7 @@ class AgentPreparer:
|
|
|
521
524
|
except Exception as e:
|
|
522
525
|
raise RuntimeError(f"Failed to render agent instructions: {e}") from e
|
|
523
526
|
|
|
524
|
-
combined_instructions = _combine_instructions(
|
|
525
|
-
|
|
526
|
-
tools = self._expand_tools(agent_config, full_context, interactive_mode)
|
|
527
|
+
combined_instructions = _combine_instructions(agent_instructions)
|
|
527
528
|
|
|
528
529
|
skill_load = self._load_skills(agent_config, prompt, full_context, _skill_manager, event_bus)
|
|
529
530
|
skills, expiring_skills = skill_load.skills, skill_load.expiring
|
|
@@ -31,7 +31,6 @@ from tsugite.agent_runner.runner import ( # noqa: F401
|
|
|
31
31
|
_execute_agent_with_prompt,
|
|
32
32
|
execute_prefetch,
|
|
33
33
|
execute_tool_directives,
|
|
34
|
-
get_default_instructions,
|
|
35
34
|
preview_multistep_agent,
|
|
36
35
|
run_agent,
|
|
37
36
|
run_agent_async,
|
|
@@ -47,7 +46,6 @@ __all__ = [
|
|
|
47
46
|
"execute_exec_directives",
|
|
48
47
|
"run_python_block",
|
|
49
48
|
"ExecBlockResult",
|
|
50
|
-
"get_default_instructions",
|
|
51
49
|
"AgentExecutionResult",
|
|
52
50
|
"StepMetrics",
|
|
53
51
|
"display_step_metrics",
|
|
@@ -155,31 +155,6 @@ def _setup_event_context(event_bus: Optional["EventBus"]) -> None:
|
|
|
155
155
|
set_ui_context(event_bus=event_bus)
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
def get_default_instructions() -> str:
|
|
159
|
-
"""Get minimal default instructions. Detailed guidance comes from skills.
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
Default instructions for code execution mode
|
|
163
|
-
"""
|
|
164
|
-
base = "You accomplish tasks by writing Python code.\n\n"
|
|
165
|
-
|
|
166
|
-
output = (
|
|
167
|
-
"## Output\n\n"
|
|
168
|
-
"- `print(x)` - See in next turn (internal)\n"
|
|
169
|
-
"- `send_message(msg)` - Show user progress\n"
|
|
170
|
-
"- `final_answer(msg)` - Final response (stops execution)\n\n"
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
rules = (
|
|
174
|
-
"## Rules\n\n"
|
|
175
|
-
"1. Always respond with Python code blocks\n"
|
|
176
|
-
"2. Call `final_answer()` when done\n"
|
|
177
|
-
"3. Variables persist between turns\n"
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
return base + output + rules
|
|
181
|
-
|
|
182
|
-
|
|
183
158
|
def _render_args(value: Any, renderer: AgentRenderer, context: Dict[str, Any]) -> Any:
|
|
184
159
|
"""Recursively render Jinja in string leaves of a tool args structure.
|
|
185
160
|
|
|
@@ -320,32 +295,17 @@ def execute_tool_directives(
|
|
|
320
295
|
return modified_content, new_context
|
|
321
296
|
|
|
322
297
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
For models like Claude/Deepseek that expose reasoning_content, displays the actual reasoning.
|
|
327
|
-
|
|
328
|
-
Args:
|
|
329
|
-
agent: The TsugiteAgent instance that just completed execution
|
|
330
|
-
custom_logger: Custom logger to display reasoning content
|
|
331
|
-
"""
|
|
332
|
-
if not hasattr(agent, "memory") or not agent.memory.reasoning_history:
|
|
333
|
-
return
|
|
298
|
+
# Daemon chat sources with a person at the other end; any other source counts as background.
|
|
299
|
+
INTERACTIVE_SOURCES = frozenset({"http", "discord"})
|
|
334
300
|
|
|
335
|
-
# Display each reasoning entry
|
|
336
|
-
for reasoning_content in agent.memory.reasoning_history:
|
|
337
|
-
if reasoning_content and custom_logger:
|
|
338
|
-
# Check if custom_logger has ui_handler (custom UI mode)
|
|
339
|
-
ui_handler = get_ui_handler(custom_logger)
|
|
340
|
-
if ui_handler:
|
|
341
|
-
from tsugite.events import EventBus, ReasoningContentEvent
|
|
342
301
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
302
|
+
def _run_is_interactive(channel_metadata: Optional[Dict[str, Any]]) -> bool:
|
|
303
|
+
"""Whether a person is present: the session source in the daemon, the TTY otherwise."""
|
|
304
|
+
source = (channel_metadata or {}).get("source")
|
|
305
|
+
return source in INTERACTIVE_SOURCES if source else is_interactive()
|
|
346
306
|
|
|
347
307
|
|
|
348
|
-
def _make_pre_llm_call_callback(hooks_dir: Path, agent_name: str):
|
|
308
|
+
def _make_pre_llm_call_callback(hooks_dir: Path, agent_name: str, interactive: bool):
|
|
349
309
|
"""Build an async callback that fires pre_llm_call hooks to mutate outgoing
|
|
350
310
|
messages in place. Returns None when no such hooks exist (keeps the hot path clean)."""
|
|
351
311
|
from tsugite.hooks import fire_hooks, load_hooks_config
|
|
@@ -359,7 +319,7 @@ def _make_pre_llm_call_callback(hooks_dir: Path, agent_name: str):
|
|
|
359
319
|
hooks_dir,
|
|
360
320
|
"pre_llm_call",
|
|
361
321
|
{"messages": messages, "model": model, "agent": agent_name},
|
|
362
|
-
interactive=
|
|
322
|
+
interactive=interactive,
|
|
363
323
|
)
|
|
364
324
|
|
|
365
325
|
return _callback
|
|
@@ -449,8 +409,9 @@ async def _execute_agent_with_prompt(
|
|
|
449
409
|
hooks_dir = path_context.effective_cwd
|
|
450
410
|
else:
|
|
451
411
|
hooks_dir = Path.cwd()
|
|
452
|
-
|
|
453
|
-
|
|
412
|
+
interactive = _run_is_interactive(channel_metadata)
|
|
413
|
+
setup_hook_handler(hooks_dir, event_bus, interactive=interactive)
|
|
414
|
+
pre_llm_call_cb = _make_pre_llm_call_callback(hooks_dir, agent_config.name, interactive)
|
|
454
415
|
|
|
455
416
|
# Start with tools from prepared agent
|
|
456
417
|
tools = list(prepared.tools) # Make a copy
|
|
@@ -637,9 +598,6 @@ async def _execute_agent_with_prompt(
|
|
|
637
598
|
stream=exec_options.stream,
|
|
638
599
|
)
|
|
639
600
|
|
|
640
|
-
# Extract and display reasoning content if present
|
|
641
|
-
_extract_reasoning_content(agent, custom_logger)
|
|
642
|
-
|
|
643
601
|
if usage_sink is not None:
|
|
644
602
|
usage_sink.update(
|
|
645
603
|
token_count=agent.total_tokens,
|
|
@@ -839,15 +797,17 @@ def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
|
|
|
839
797
|
|
|
840
798
|
|
|
841
799
|
def _resolve_workspace_dir(workspace: Optional[Any], path_context: Optional[Any]) -> Optional[Path]:
|
|
842
|
-
"""Resolve the executor
|
|
800
|
+
"""Resolve the directory the executor runs code in.
|
|
843
801
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
Workspace), so without this fallback the sandbox would bind/chdir the daemon's
|
|
847
|
-
CWD instead of the agent's workspace (or job worktree).
|
|
802
|
+
Checks: an explicit Workspace -> the directory bound with set_workspace_dir
|
|
803
|
+
(the daemon binds the session's working directory) -> path_context.workspace_dir.
|
|
848
804
|
"""
|
|
805
|
+
from tsugite.cli.helpers import get_workspace_dir
|
|
806
|
+
|
|
849
807
|
if workspace:
|
|
850
808
|
return workspace.path
|
|
809
|
+
if bound := get_workspace_dir():
|
|
810
|
+
return bound
|
|
851
811
|
if path_context and getattr(path_context, "workspace_dir", None):
|
|
852
812
|
return path_context.workspace_dir
|
|
853
813
|
return None
|
|
@@ -934,7 +894,7 @@ async def _run_unit(
|
|
|
934
894
|
"rendered_prompt": prepared.rendered_prompt[:500] if prepared.rendered_prompt else "",
|
|
935
895
|
"tools": [t.name for t in prepared.tools] if prepared.tools else [],
|
|
936
896
|
},
|
|
937
|
-
interactive=
|
|
897
|
+
interactive=_run_is_interactive(setup.channel_metadata),
|
|
938
898
|
on_status=setup.on_status,
|
|
939
899
|
on_result=setup.on_hook_result,
|
|
940
900
|
)
|
|
@@ -1140,10 +1100,11 @@ async def run_agent_async(
|
|
|
1140
1100
|
on_status = None
|
|
1141
1101
|
on_hook_result = None
|
|
1142
1102
|
|
|
1103
|
+
interactive = _run_is_interactive(channel_metadata)
|
|
1143
1104
|
hook_vars = await fire_pre_message_hooks(
|
|
1144
1105
|
hooks_dir,
|
|
1145
1106
|
{"message": hook_message, "agent_name": agent_path.stem},
|
|
1146
|
-
interactive=
|
|
1107
|
+
interactive=interactive,
|
|
1147
1108
|
on_status=on_status,
|
|
1148
1109
|
on_result=on_hook_result,
|
|
1149
1110
|
)
|
|
@@ -1157,7 +1118,7 @@ async def run_agent_async(
|
|
|
1157
1118
|
hooks_dir,
|
|
1158
1119
|
"pre_context_build",
|
|
1159
1120
|
{"message": hook_message, "agent_name": agent_path.stem, **context, "blocks": context_blocks},
|
|
1160
|
-
interactive=
|
|
1121
|
+
interactive=interactive,
|
|
1161
1122
|
on_status=on_status,
|
|
1162
1123
|
on_result=on_hook_result,
|
|
1163
1124
|
)
|
|
@@ -31,23 +31,28 @@ _LABEL_TO_DECISION = {
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def request_approval(
|
|
34
|
-
prompt: str,
|
|
34
|
+
prompt: str,
|
|
35
|
+
*,
|
|
36
|
+
allow_always: bool = False,
|
|
37
|
+
allow_never: bool = False,
|
|
38
|
+
extra_options: Optional[dict[str, str]] = None,
|
|
39
|
+
detail: Optional[str] = None,
|
|
35
40
|
) -> str:
|
|
36
41
|
"""Ask the user to approve an action, returning an :data:`ApprovalDecision`.
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
back to a decision.
|
|
43
|
+
Offers ``Approve``/``Deny`` plus the optional choices below as an
|
|
44
|
+
``"approval"`` question on the active interaction backend, and maps the
|
|
45
|
+
chosen label back to a decision.
|
|
42
46
|
|
|
43
47
|
Args:
|
|
44
48
|
prompt: The action to approve, phrased as a question.
|
|
45
49
|
allow_always: Offer an "Always allow" option (a persistent allow).
|
|
46
50
|
allow_never: Offer an "Always deny" option (a persistent deny).
|
|
51
|
+
extra_options: Caller-specific choices, as ``{label: decision}``.
|
|
47
52
|
detail: Extra context appended below the prompt for the user to read.
|
|
48
53
|
|
|
49
54
|
Returns:
|
|
50
|
-
One of ``"approve"``, ``"deny"``, ``"always"``,
|
|
55
|
+
One of ``"approve"``, ``"deny"``, ``"always"``, ``"never"``, or a value of ``extra_options``.
|
|
51
56
|
|
|
52
57
|
Fail-closed: returns ``"deny"`` WITHOUT prompting when there is no backend or
|
|
53
58
|
the backend is non-interactive, and for any unrecognized reply.
|
|
@@ -61,7 +66,9 @@ def request_approval(
|
|
|
61
66
|
options.append(ALWAYS_LABEL)
|
|
62
67
|
if allow_never:
|
|
63
68
|
options.append(NEVER_LABEL)
|
|
69
|
+
decisions = _LABEL_TO_DECISION | (extra_options or {})
|
|
70
|
+
options.extend(extra_options or ())
|
|
64
71
|
|
|
65
72
|
question = f"{prompt}\n\n{detail}" if detail else prompt
|
|
66
73
|
answer = backend.ask_user(question, "approval", options)
|
|
67
|
-
return
|
|
74
|
+
return decisions.get(answer, "deny")
|
|
@@ -73,7 +73,7 @@ instructions: |
|
|
|
73
73
|
- Break down complex tasks into clear steps
|
|
74
74
|
- Ask clarifying questions when the task is ambiguous
|
|
75
75
|
- Write Python code to accomplish tasks
|
|
76
|
-
-
|
|
76
|
+
- When you've completed the task, reply in plain text, or call return_value() for structured output
|
|
77
77
|
- If you intend to call multiple tools and there are no dependencies between the calls, make all independent calls in a single code block
|
|
78
78
|
- Never guess about code you have not read. Use read_file or list_files to investigate before answering questions about code
|
|
79
79
|
|
|
@@ -235,13 +235,6 @@ You have running tmux sessions that you can interact with using tmux tools (tmux
|
|
|
235
235
|
{% endfor %}
|
|
236
236
|
{% endif %}
|
|
237
237
|
|
|
238
|
-
{% if "run" in tools and "tmux_create" in tools %}
|
|
239
|
-
## Shell vs Tmux
|
|
240
|
-
|
|
241
|
-
- Use `run()` for commands that produce output and exit: `run("ls -la")`, `run("git status")`, `run("python script.py")`
|
|
242
|
-
- Use tmux tools for interactive programs (htop, k9s, psql, python REPL), long-running processes you want to monitor, or when you need a persistent shell session where state carries across commands
|
|
243
|
-
{% endif %}
|
|
244
|
-
|
|
245
238
|
{% if subagent_instructions is defined and subagent_instructions %}
|
|
246
239
|
{{ subagent_instructions }}
|
|
247
240
|
{% endif %}
|
|
@@ -299,11 +292,11 @@ Load these skills for specialized knowledge:
|
|
|
299
292
|
- **{{ skill.name }}** - {{ skill.description }}
|
|
300
293
|
{% endfor %}
|
|
301
294
|
|
|
302
|
-
Call `load_skill("skill_name")` and stop. The skill content appears next turn as `<
|
|
295
|
+
Call `load_skill("skill_name")` and stop. The skill content appears next turn as `<skill_content>`. Then use what you learned.
|
|
303
296
|
|
|
304
297
|
Do not act in the same turn you load a skill. Skip loading if you already know how to do the task.
|
|
305
298
|
|
|
306
|
-
Skills may show bash commands. Translate to Python: `
|
|
299
|
+
Skills may show bash commands. Translate to Python: `run("kubectl get pods")`
|
|
307
300
|
</available_skills>
|
|
308
301
|
{% endif %}
|
|
309
302
|
|
|
@@ -76,7 +76,9 @@ is `true` only if every `ac_results[i].pass` is `true`.
|
|
|
76
76
|
|
|
77
77
|
`reason` opens with the basis for the verdict, then one sentence:
|
|
78
78
|
|
|
79
|
-
- `inspected:` - you read the artifact
|
|
79
|
+
- `inspected:` - you read the artifact or file yourself with a tool call.
|
|
80
|
+
- `read Output:` - the deliverable is inline text and you judged the `## Output`
|
|
81
|
+
text itself, with no tool call.
|
|
80
82
|
- `ran <command>:` - you executed the command and judged its exit status.
|
|
81
83
|
- `accepted worker claim:` - you could not check it, so the verdict rests on
|
|
82
84
|
the worker's report.
|
{tsugite_cli-0.24.1 → tsugite_cli-0.25.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md
RENAMED
|
@@ -365,7 +365,7 @@ If available via custom tools configuration, consider using:
|
|
|
365
365
|
tools:
|
|
366
366
|
- name: ast_search
|
|
367
367
|
description: Search code using AST patterns
|
|
368
|
-
command: "ast-grep -p
|
|
368
|
+
command: "ast-grep -p {pattern} {path}"
|
|
369
369
|
parameters:
|
|
370
370
|
pattern: {required: true}
|
|
371
371
|
path: "."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: response-patterns
|
|
3
|
-
description: How to respond effectively - when to use print/send_message/
|
|
3
|
+
description: How to respond effectively - when to use print/send_message/return_value/ask_user
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Response Patterns
|
|
@@ -11,21 +11,18 @@ description: How to respond effectively - when to use print/send_message/final_a
|
|
|
11
11
|
|----------|----------|-----------|---------|
|
|
12
12
|
| `print(x)` | You (next turn) | Continues | Intermediate data |
|
|
13
13
|
| `send_message(msg)` | User | Continues | Progress updates |
|
|
14
|
-
| `
|
|
14
|
+
| `return_value(value)` | User | **Stops** | Structured final result |
|
|
15
15
|
| `ask_user(q)` | User | **Blocks** | Get user input |
|
|
16
16
|
|
|
17
17
|
## Simple Responses
|
|
18
18
|
|
|
19
|
-
No functions needed?
|
|
19
|
+
No functions needed? Reply in plain text:
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
- "Thanks" replies `You're welcome!`
|
|
22
|
+
- "What's 2+2?" replies `4`
|
|
23
|
+
- "What can you do?" replies `I can read/write files, run commands, and help with tasks.`
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
- "Thanks" → `final_answer("You're welcome!")`
|
|
27
|
-
- "What's 2+2?" → `final_answer("4")`
|
|
28
|
-
- "What can you do?" → `final_answer("I can read/write files, run commands, search code, and help with tasks.")`
|
|
25
|
+
Call `return_value(value)` only when the caller needs a structured result.
|
|
29
26
|
|
|
30
27
|
## Progress Updates
|
|
31
28
|
|
|
@@ -33,14 +30,14 @@ For longer tasks, keep user informed:
|
|
|
33
30
|
|
|
34
31
|
```python-exec
|
|
35
32
|
send_message("Searching codebase...")
|
|
36
|
-
results =
|
|
33
|
+
results = run("grep -rn 'def main' .")
|
|
37
34
|
print(results) # You'll see this next turn
|
|
38
35
|
```
|
|
39
36
|
|
|
40
37
|
```python-exec
|
|
41
|
-
send_message(f"Found {len(results)} matches. Analyzing...")
|
|
38
|
+
send_message(f"Found {len(results.splitlines())} matches. Analyzing...")
|
|
42
39
|
# ... more work ...
|
|
43
|
-
|
|
40
|
+
return_value("Here's what I found: ...")
|
|
44
41
|
```
|
|
45
42
|
|
|
46
43
|
## Asking the User
|
|
@@ -87,7 +84,7 @@ Use `ask_user` when:
|
|
|
87
84
|
- Ask unnecessary questions when a reasonable default exists
|
|
88
85
|
|
|
89
86
|
✅ Do:
|
|
90
|
-
-
|
|
87
|
+
- Plain text immediately for simple responses
|
|
91
88
|
- `send_message()` for progress on tasks > 5 seconds
|
|
92
89
|
- `ask_user()` when you genuinely need user input
|
|
93
90
|
- `print()` only for data YOU need to see
|
|
@@ -500,8 +500,8 @@ class PathContext:
|
|
|
500
500
|
effective_cwd: Path
|
|
501
501
|
|
|
502
502
|
|
|
503
|
-
#
|
|
504
|
-
# that don't receive PathContext explicitly. Task-local so concurrent agent runs
|
|
503
|
+
# The directory the running agent's code and fs tools work in, for call sites
|
|
504
|
+
# (fs tools, shell, subprocesses, the executor) that don't receive PathContext explicitly. Task-local so concurrent agent runs
|
|
505
505
|
# don't share state.
|
|
506
506
|
_workspace_dir_cv: contextvars.ContextVar[Optional[Path]] = contextvars.ContextVar("workspace_dir", default=None)
|
|
507
507
|
|
|
@@ -257,7 +257,6 @@ 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
|
-
join_daemon: bool = typer.Option(False, "--daemon", "-d", help="Join the daemon's most recent session"),
|
|
261
260
|
sandbox: bool = typer.Option(False, "--sandbox", help="Run agent code in bubblewrap sandbox"),
|
|
262
261
|
no_sandbox: bool = typer.Option(False, "--no-sandbox", help="Disable sandbox (overrides config)"),
|
|
263
262
|
allow_domain: Optional[List[str]] = typer.Option(
|
|
@@ -282,7 +281,6 @@ def run(
|
|
|
282
281
|
tsu run --continue "prompt" # Continue latest conversation (auto-detect agent)
|
|
283
282
|
tsu run +assistant --continue "prompt" # Continue latest with specific agent
|
|
284
283
|
tsu run --continue --conversation-id CONV_ID "prompt" # Continue specific conversation
|
|
285
|
-
tsu run --daemon "follow up message" # Join the daemon's most recent session
|
|
286
284
|
"""
|
|
287
285
|
from tsugite.agent_runner import get_agent_info, run_agent
|
|
288
286
|
from tsugite.console import get_stderr_console
|
|
@@ -381,7 +379,7 @@ def run(
|
|
|
381
379
|
# Resolve UI mode to update ui_opts
|
|
382
380
|
ui_opts = _resolve_ui_mode(ui, ui_opts, stderr_console)
|
|
383
381
|
|
|
384
|
-
# Handle subagent mode
|
|
382
|
+
# Handle subagent mode
|
|
385
383
|
import os
|
|
386
384
|
|
|
387
385
|
if subagent_mode:
|
|
@@ -393,54 +391,6 @@ def run(
|
|
|
393
391
|
history_opts.enabled = False
|
|
394
392
|
os.environ["TSUGITE_SUBAGENT_MODE"] = "1"
|
|
395
393
|
|
|
396
|
-
daemon_metadata = None
|
|
397
|
-
if join_daemon:
|
|
398
|
-
from tsugite.agent_runner.history_integration import get_latest_conversation
|
|
399
|
-
|
|
400
|
-
try:
|
|
401
|
-
from tsugite_daemon.config import load_daemon_config
|
|
402
|
-
|
|
403
|
-
# Probe: the daemon battery has to be installed and configured.
|
|
404
|
-
load_daemon_config()
|
|
405
|
-
except ModuleNotFoundError as e:
|
|
406
|
-
stderr_console.print(
|
|
407
|
-
"[red]The daemon battery is not installed.[/red] Install it with: pip install tsugite-cli[daemon]"
|
|
408
|
-
)
|
|
409
|
-
raise typer.Exit(1) from e
|
|
410
|
-
except ValueError as e:
|
|
411
|
-
stderr_console.print(f"[red]Daemon config not found: {e}[/red]")
|
|
412
|
-
stderr_console.print("[dim]Run 'tsugite daemon' to start daemon first[/dim]")
|
|
413
|
-
raise typer.Exit(1)
|
|
414
|
-
|
|
415
|
-
user_id = os.environ.get("USER", "cli-user")
|
|
416
|
-
|
|
417
|
-
latest_conv_id = get_latest_conversation()
|
|
418
|
-
if latest_conv_id:
|
|
419
|
-
stderr_console.print(f"[cyan]Joining daemon session: {latest_conv_id}[/cyan]")
|
|
420
|
-
history_opts.continue_id = latest_conv_id
|
|
421
|
-
else:
|
|
422
|
-
stderr_console.print("[yellow]No active daemon session found[/yellow]")
|
|
423
|
-
stderr_console.print("[dim]Creating new daemon-managed session...[/dim]")
|
|
424
|
-
|
|
425
|
-
# Build CLI metadata for history
|
|
426
|
-
from datetime import datetime, timezone
|
|
427
|
-
|
|
428
|
-
daemon_metadata = {
|
|
429
|
-
"source": "cli",
|
|
430
|
-
"channel_id": None,
|
|
431
|
-
"user_id": user_id,
|
|
432
|
-
"reply_to": "cli",
|
|
433
|
-
"is_daemon_managed": True,
|
|
434
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
all_attachments = list(attachment) if attachment else []
|
|
438
|
-
attach_opts = AttachmentOptions(
|
|
439
|
-
sources=all_attachments,
|
|
440
|
-
refresh_cache=refresh_cache,
|
|
441
|
-
auto_context=auto_context,
|
|
442
|
-
)
|
|
443
|
-
|
|
444
394
|
# Handle conversation continuation - check before parsing args
|
|
445
395
|
if continue_conversation and not history_opts.continue_id:
|
|
446
396
|
from tsugite.agent_runner.history_integration import get_latest_conversation
|
|
@@ -641,7 +591,6 @@ def run(
|
|
|
641
591
|
continue_conversation_id=target_session,
|
|
642
592
|
system_prompt=system_prompt,
|
|
643
593
|
attachments=attachments,
|
|
644
|
-
channel_metadata=daemon_metadata,
|
|
645
594
|
status=status,
|
|
646
595
|
error_message=error_message,
|
|
647
596
|
)
|
|
@@ -173,6 +173,40 @@ def _has_tool_call_markup(content: str) -> bool:
|
|
|
173
173
|
return bool(content) and _TOOL_CALL_MARKUP.search(_CODE_MARKUP.sub("", content)) is not None
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
# A single response holding this many characters of one whitespace run, or of one
|
|
177
|
+
# short chunk repeated back to back, is a degenerate loop, not output. Real
|
|
178
|
+
# content (long reports, tables, code) never gets near it. Not a length cap.
|
|
179
|
+
_RUNAWAY_MIN_CHARS = 5000
|
|
180
|
+
_RUNAWAY_CHECK_STRIDE = 1000
|
|
181
|
+
_WHITESPACE_RUN = re.compile(rf"\s{{{_RUNAWAY_MIN_CHARS},}}")
|
|
182
|
+
_MAX_CHUNK = 256
|
|
183
|
+
_REPEATED_CHUNK = re.compile(rf"(.{{1,{_MAX_CHUNK}}}?)\1+", re.DOTALL)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class RunawayOutputError(Exception):
|
|
187
|
+
"""A streamed response degenerated into a whitespace run or a repeated chunk."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, reason: str, content: str):
|
|
190
|
+
super().__init__(reason)
|
|
191
|
+
self.reason = reason
|
|
192
|
+
self.content = content
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _find_runaway(content: str) -> Optional[tuple[str, int]]:
|
|
196
|
+
"""Return (reason, start offset) of a degenerate run in the tail of `content`."""
|
|
197
|
+
window_start = max(0, len(content) - (_RUNAWAY_MIN_CHARS + _MAX_CHUNK + _RUNAWAY_CHECK_STRIDE))
|
|
198
|
+
window = content[window_start:]
|
|
199
|
+
if m := _WHITESPACE_RUN.search(window):
|
|
200
|
+
return f"a whitespace run of at least {_RUNAWAY_MIN_CHARS} characters", window_start + m.start()
|
|
201
|
+
tail = window[-_MAX_CHUNK:]
|
|
202
|
+
# Prefilter: skip the regex unless the tail text occurred earlier in the window.
|
|
203
|
+
if window.find(tail) < len(window) - len(tail):
|
|
204
|
+
for m in _REPEATED_CHUNK.finditer(window):
|
|
205
|
+
if len(m.group(0)) >= _RUNAWAY_MIN_CHARS:
|
|
206
|
+
return f"one chunk repeated for at least {_RUNAWAY_MIN_CHARS} characters", window_start + m.start()
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
176
210
|
# Tags the runtime injects into the model's NEXT user message after executing
|
|
177
211
|
# its code (the execution result, multi-block warning, and budget hints). A
|
|
178
212
|
# well-behaved model never writes these itself; one that does is impersonating
|
|
@@ -233,6 +267,15 @@ def _build_unexecuted_tool_call_notice_xml() -> str:
|
|
|
233
267
|
)
|
|
234
268
|
|
|
235
269
|
|
|
270
|
+
def _build_runaway_notice_xml(reason: str) -> str:
|
|
271
|
+
return (
|
|
272
|
+
"\n<tsugite_runaway_output_notice>"
|
|
273
|
+
f"Your response was cut off because it contained {reason}. "
|
|
274
|
+
"Nothing in it ran. Reply again with prose or exactly one ```python-exec block."
|
|
275
|
+
"</tsugite_runaway_output_notice>"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
236
279
|
def _build_multi_block_warning_xml(count: int) -> str:
|
|
237
280
|
"""Model-visible note appended to a turn's observation when the agent
|
|
238
281
|
received N>1 ```python blocks in one response.
|
|
@@ -480,7 +523,39 @@ class TsugiteAgent:
|
|
|
480
523
|
|
|
481
524
|
self._record_prompt_snapshot(messages, turn_num)
|
|
482
525
|
|
|
483
|
-
|
|
526
|
+
try:
|
|
527
|
+
turn = await self._provider_turn(messages, turn_num, stream)
|
|
528
|
+
except RunawayOutputError as e:
|
|
529
|
+
logger.warning("Aborted runaway response (step %d): %s", turn_num + 1, e.reason)
|
|
530
|
+
aborted_text, _ = escape_runtime_injection_tags(e.content)
|
|
531
|
+
last_response_text = aborted_text
|
|
532
|
+
self._previous_turn_had_error = True
|
|
533
|
+
if self.event_bus:
|
|
534
|
+
self.event_bus.emit(
|
|
535
|
+
WarningEvent(
|
|
536
|
+
message=f"Response aborted: it contained {e.reason}.",
|
|
537
|
+
category="runaway_output",
|
|
538
|
+
step=turn_num + 1,
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
self.memory.add_step(
|
|
542
|
+
thought="",
|
|
543
|
+
code="",
|
|
544
|
+
output="",
|
|
545
|
+
error=e.reason,
|
|
546
|
+
tools_called=[],
|
|
547
|
+
raw_content=aborted_text,
|
|
548
|
+
xml_observation=_build_runaway_notice_xml(e.reason),
|
|
549
|
+
)
|
|
550
|
+
self._record_model_response(
|
|
551
|
+
turn_num,
|
|
552
|
+
raw_content=aborted_text,
|
|
553
|
+
parsed=ParsedResponse(thought=aborted_text, code=""),
|
|
554
|
+
usage=None,
|
|
555
|
+
cost=None,
|
|
556
|
+
response=None,
|
|
557
|
+
)
|
|
558
|
+
continue
|
|
484
559
|
thought, code = turn.thought, turn.code
|
|
485
560
|
last_response_text = turn.response.content if turn.response else (thought or "")
|
|
486
561
|
|
|
@@ -855,11 +930,20 @@ class TsugiteAgent:
|
|
|
855
930
|
chunks = await self._provider.acompletion(
|
|
856
931
|
messages=messages, model=self._model_id, stream=True, **self._model_kwargs
|
|
857
932
|
)
|
|
933
|
+
checked_len = 0
|
|
858
934
|
async for chunk in chunks:
|
|
859
935
|
if chunk.content:
|
|
860
936
|
content += chunk.content
|
|
861
937
|
if self.event_bus:
|
|
862
938
|
self.event_bus.emit(StreamChunkEvent(chunk=chunk.content))
|
|
939
|
+
if len(content) - checked_len >= _RUNAWAY_CHECK_STRIDE:
|
|
940
|
+
checked_len = len(content)
|
|
941
|
+
if runaway := _find_runaway(content):
|
|
942
|
+
reason, start = runaway
|
|
943
|
+
await chunks.aclose()
|
|
944
|
+
if self.event_bus:
|
|
945
|
+
self.event_bus.emit(StreamCompleteEvent())
|
|
946
|
+
raise RunawayOutputError(reason, content[:start])
|
|
863
947
|
if getattr(chunk, "reasoning_content", ""):
|
|
864
948
|
reasoning += chunk.reasoning_content
|
|
865
949
|
if self.event_bus:
|
|
@@ -1188,7 +1272,7 @@ class TsugiteAgent:
|
|
|
1188
1272
|
{"role": "assistant", "content": "previous response 1"},
|
|
1189
1273
|
{"role": "user", "content": task},
|
|
1190
1274
|
{"role": "assistant", "content": "```python-exec\\n...```"},
|
|
1191
|
-
{"role": "user", "content": <
|
|
1275
|
+
{"role": "user", "content": <skill_content>...</skill_content>\\n<observation>..."},
|
|
1192
1276
|
...
|
|
1193
1277
|
]
|
|
1194
1278
|
"""
|
|
@@ -18,9 +18,9 @@ class Session(Protocol):
|
|
|
18
18
|
|
|
19
19
|
session_id: str
|
|
20
20
|
|
|
21
|
-
def record(self, type: str, *, ts: Optional[datetime] = None, **data: Any) ->
|
|
21
|
+
def record(self, type: str, *, ts: Optional[datetime] = None, **data: Any) -> int: ...
|
|
22
22
|
|
|
23
|
-
def record_many(self, events: Iterable[Event]) ->
|
|
23
|
+
def record_many(self, events: Iterable[Event]) -> List[int]: ...
|
|
24
24
|
|
|
25
25
|
def iter_events(self, types: Optional[Iterable[str]] = None) -> Iterator[Event]: ...
|
|
26
26
|
|