tsugite-cli 0.22.1__tar.gz → 0.23.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.22.1 → tsugite_cli-0.23.0}/.gitignore +3 -1
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/AGENTS.md +16 -9
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/CONTRIBUTING.md +9 -16
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/PKG-INFO +1 -1
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/pyproject.toml +1 -1
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_preparation.py +26 -30
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/exec_runner.py +4 -3
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/helpers.py +3 -7
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/runner.py +6 -1
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/steps.py +2 -2
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/agent_config.py +12 -7
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/auto_context.py +11 -20
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/agent.py +4 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/subprocess_executor.py +11 -3
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/exceptions.py +21 -1
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/anthropic.py +1 -2
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/openai_compat.py +5 -13
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/__init__.py +9 -16
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/agents.py +19 -38
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/artifacts.py +19 -18
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/fs.py +73 -103
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/http.py +45 -53
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/interactive.py +20 -47
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/jobs.py +20 -26
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/schedule.py +8 -7
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/shell_tools.py +5 -14
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/utils.py +3 -2
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/LICENSE +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/README.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/history_integration.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/approval.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/config.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/models.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/daemon_control.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/usage/quota.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.22.1 → tsugite_cli-0.23.0}/tsugite/workspace/templates.py +0 -0
|
@@ -193,4 +193,6 @@ plugins/tsugite-daemon/tsugite_daemon/web/
|
|
|
193
193
|
# Re-include frontend source that generic lib/ rules (incl. contributors' global
|
|
194
194
|
# gitignores with Python-style `lib/`) would otherwise silently exclude.
|
|
195
195
|
!plugins/tsugite-daemon/frontend/src/lib/
|
|
196
|
-
!plugins/tsugite-daemon/frontend/src/lib/**
|
|
196
|
+
!plugins/tsugite-daemon/frontend/src/lib/**
|
|
197
|
+
# Per-job git worktrees provisioned by the daemon.
|
|
198
|
+
.tsugite-jobs/
|
|
@@ -289,7 +289,7 @@ Tsugite supports vision (images), audio, and document understanding through the
|
|
|
289
289
|
**How It Works:**
|
|
290
290
|
|
|
291
291
|
1. **URL Attachments** (Images/Documents):
|
|
292
|
-
- The provider fetches the URL directly (no download overhead
|
|
292
|
+
- The provider fetches the URL directly (no download overhead, though support varies by provider and content type; see `tsugite/attachments/url.py`)
|
|
293
293
|
- Example: `tsu run -f https://example.com/chart.png "Describe this chart"`
|
|
294
294
|
- Automatically detected via HTTP HEAD request
|
|
295
295
|
|
|
@@ -425,7 +425,7 @@ See `examples/attachment_assign_demo.md` and `examples/attachment_index_demo.md`
|
|
|
425
425
|
|
|
426
426
|
### Swappable batteries
|
|
427
427
|
|
|
428
|
-
Subsystems are pluggable behind a uniform
|
|
428
|
+
Subsystems are pluggable behind a uniform interface: (1) a Protocol, (2) a `tsugite.<area>`
|
|
429
429
|
entry-point group, (3) a `config.<area>.backend` selector resolved via
|
|
430
430
|
`load_backend_entry_point` (`plugins.py`), with a built-in default. Areas: tools, providers,
|
|
431
431
|
adapters, secrets, hooks, event_subscribers, history, attachments, sandbox, executors.
|
|
@@ -448,12 +448,12 @@ For any non-trivial bug fix or feature, write a failing test **first**. Exceptio
|
|
|
448
448
|
|
|
449
449
|
The workflow:
|
|
450
450
|
|
|
451
|
-
1. **Reproduce before fixing.** Write the smallest test that demonstrates the symptom as reported. Run it. If it passes on master, the hypothesis is wrong
|
|
451
|
+
1. **Reproduce before fixing.** Write the smallest test that demonstrates the symptom as reported. Run it. If it passes on master, the hypothesis is wrong. Do not write a fix. Investigate further, ask the user for more detail, or add logging.
|
|
452
452
|
2. **Implement the fix.** Keep the change minimal. Do not refactor or add adjacent cleanup in the same diff.
|
|
453
|
-
3. **Verify the test is load-bearing.** Temp-revert the production change and re-run. The test must go red again. If it still passes, the test isn't
|
|
453
|
+
3. **Verify the test is load-bearing.** Temp-revert the production change and re-run. The test must go red again. If it still passes, the test isn't exercising the fix. Rework it.
|
|
454
454
|
4. **Cover adjacent cases before declaring done.** When touching a parser, protocol boundary, or state machine, enumerate at least two adjacent input shapes and add tests for them. Narrow tests let bugs sneak in through the cases you didn't think about.
|
|
455
455
|
5. **Run the local regression suite.** At minimum, the test files next to the modules you changed. Faster than running everything, catches silent breakage of existing tests.
|
|
456
|
-
6. **Honest uncertainty.** If a hypothesis doesn't reproduce, say so. Don't ship a "probably-this" fix with no failing test to anchor it
|
|
456
|
+
6. **Honest uncertainty.** If a hypothesis doesn't reproduce, say so. Don't ship a "probably-this" fix with no failing test to anchor it. That's how placebo fixes get merged.
|
|
457
457
|
|
|
458
458
|
Reproducing tests don't need to be elaborate. A 10-line test that flips red→green is worth more than a 100-line one that nobody understands.
|
|
459
459
|
|
|
@@ -461,7 +461,7 @@ Reproducing tests don't need to be elaborate. A 10-line test that flips red→gr
|
|
|
461
461
|
|
|
462
462
|
- **Unit tests**: Individual functions and classes
|
|
463
463
|
- **Pipeline tests**: Mock only `TsugiteAgent` / the provider's `acompletion` but exercise the full pipeline (parsing → rendering → preparation → tool expansion → execution). Most tests in the suite are this style.
|
|
464
|
-
- **Integration tests**: Live under `tests/integration/` (not collected by default
|
|
464
|
+
- **Integration tests**: Live under `tests/integration/` (not collected by default; see `pyproject.toml` `norecursedirs`). Run explicitly with `uv run pytest tests/integration/`. Good for concurrency, cwd, and daemon-wiring tests that need real threading or a real workspace.
|
|
465
465
|
- **Smoke tests**: `tests/smoke_test.sh` hits a real LLM API (requires `OPENAI_API_KEY`, not run in CI)
|
|
466
466
|
- **Fixtures**: `conftest.py` provides shared test data
|
|
467
467
|
- **Mocking**: Use `@pytest.fixture` for LLM responses
|
|
@@ -471,9 +471,16 @@ Reproducing tests don't need to be elaborate. A 10-line test that flips red→gr
|
|
|
471
471
|
## Code Style
|
|
472
472
|
|
|
473
473
|
- Python 3.11+ (type hints on public APIs)
|
|
474
|
-
- Line length: 120 characters (
|
|
474
|
+
- Line length: 120 characters (ruff)
|
|
475
475
|
- Import order: stdlib → third-party → local (blank lines between groups)
|
|
476
|
-
- Errors
|
|
476
|
+
- Errors, by condition:
|
|
477
|
+
- `ValueError` - input validation
|
|
478
|
+
- `RuntimeError` - execution failures
|
|
479
|
+
- `TimeoutError` - timeouts
|
|
480
|
+
- `PermissionError` - policy denials
|
|
481
|
+
- `ToolUnavailableError` - the tool cannot run in this context
|
|
482
|
+
- `FileNotFoundError` / `IsADirectoryError` / `NotADirectoryError` - paths
|
|
483
|
+
- Never wrap a tool body in a blanket `except Exception`
|
|
477
484
|
- Docstrings: Google style with `Args:` and `Returns:` sections
|
|
478
485
|
- Pydantic models: Use `extra="forbid"` to catch typos in YAML frontmatter
|
|
479
486
|
|
|
@@ -485,7 +492,7 @@ Reproducing tests don't need to be elaborate. A 10-line test that flips red→gr
|
|
|
485
492
|
4. **Don't hardcode paths**: Use XDG utilities (`get_xdg_config_path()`, etc.)
|
|
486
493
|
5. **Don't use blocking IO in async**: Use `asyncio.to_thread()` for sync tools
|
|
487
494
|
6. **Test both sync and async paths**: Many tools support both execution modes
|
|
488
|
-
7. **Don't embed prompts in adapters/code**: Use context variables + conditional blocks in `default.md` instead. Add new context vars in `_build_agent_context()` (base adapter) and default them in `agent_preparation.py`, then use `{% if var %}` in the agent template. This keeps all prompt content in one place and
|
|
495
|
+
7. **Don't embed prompts in adapters/code**: Use context variables + conditional blocks in `default.md` instead. Add new context vars in `_build_agent_context()` (base adapter) and default them in `agent_preparation.py`, then use `{% if var %}` in the agent template. This keeps all prompt content in one place and uses the existing rendering pipeline.
|
|
489
496
|
|
|
490
497
|
## Web UI
|
|
491
498
|
|
|
@@ -10,8 +10,8 @@ Use [Conventional Commits](https://www.conventionalcommits.org/). Format:
|
|
|
10
10
|
<type>(<scope>): <subject>
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Keep the subject on one line, lowercase, no trailing period
|
|
14
|
-
|
|
13
|
+
Keep the subject on one line, lowercase, no trailing period, and write it as the line a user would
|
|
14
|
+
read in the release notes. No body, no trailers.
|
|
15
15
|
|
|
16
16
|
### Allowed types
|
|
17
17
|
|
|
@@ -46,21 +46,14 @@ Scopes are optional, but should be limited to one of these:
|
|
|
46
46
|
|
|
47
47
|
### Release notes
|
|
48
48
|
|
|
49
|
-
git-cliff (`cliff.toml`) generates the release notes from the
|
|
50
|
-
|
|
49
|
+
git-cliff (`cliff.toml`) generates the release notes from the commit subjects, so the subject is the
|
|
50
|
+
release line. Write it as what the software does now for the person using it, and it needs nothing
|
|
51
|
+
else. `chore`, `docs`, `test` and `ci` commits never appear.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
One line, present tense, stating what the software does now. No "instead of", no because-clause. A
|
|
59
|
-
commit with nothing to tell a user (a test fix, an internal rename) carries no trailer and prints its
|
|
60
|
-
subject instead. `chore`, `docs`, `test` and `ci` commits never appear.
|
|
61
|
-
|
|
62
|
-
The trailer has to be the last paragraph of the message. When squashing, keep one `Release-Note:`
|
|
63
|
-
line at the end of the squashed message; a trailer buried mid-body is not parsed.
|
|
53
|
+
`cliff.toml` also prints a `Release-Note:` trailer in place of the subject when one is present. That
|
|
54
|
+
is an escape hatch for the rare commit whose subject cannot say what a user sees, not something to
|
|
55
|
+
add by default; almost every commit should have none. When one is needed it has to be the last
|
|
56
|
+
paragraph of the message, and a squash keeps at most one.
|
|
64
57
|
|
|
65
58
|
### Examples
|
|
66
59
|
|
|
@@ -89,7 +89,7 @@ class AgentPreparer:
|
|
|
89
89
|
_resolve_attachments caller + frontmatter attachments, removals, dedupe
|
|
90
90
|
_install_skill_manager workspace-aware SkillManager, made active
|
|
91
91
|
_run_prefetch frontmatter prefetch tools + agent-list injection
|
|
92
|
-
_resolve_paths
|
|
92
|
+
_resolve_paths CWD / INVOKED_FROM / WORKSPACE_DIR
|
|
93
93
|
_build_template_context the Jinja context and its framework defaults
|
|
94
94
|
_expand_tools tool specs + capability-based auto-injection
|
|
95
95
|
_load_skills auto-load, sticky TTL, trigger matching
|
|
@@ -143,14 +143,16 @@ class AgentPreparer:
|
|
|
143
143
|
)
|
|
144
144
|
|
|
145
145
|
def _resolve_attachments(
|
|
146
|
-
self,
|
|
146
|
+
self,
|
|
147
|
+
agent_config: AgentConfig,
|
|
148
|
+
attachments: Optional[List[Attachment]],
|
|
149
|
+
paths: Dict[str, Optional[str]],
|
|
147
150
|
) -> Tuple[List[Attachment], Dict[str, Any]]:
|
|
148
151
|
"""Merge caller attachments with the agent's own, honoring removals.
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Front-matter paths resolve via cwd (workspace_path is None in production).
|
|
153
|
+
A front-matter attachment defines its cache tier and replaces a same-named
|
|
154
|
+
caller attachment. Legacy `-filename` string entries drop a same-named
|
|
155
|
+
entry. Front-matter paths render with `paths` and resolve against its CWD.
|
|
154
156
|
"""
|
|
155
157
|
all_attachments = list(attachments or [])
|
|
156
158
|
|
|
@@ -158,7 +160,7 @@ class AgentPreparer:
|
|
|
158
160
|
if removals:
|
|
159
161
|
all_attachments = [a for a in all_attachments if a.name not in removals]
|
|
160
162
|
|
|
161
|
-
loaded, bindings = resolve_agent_config_attachments(keep_items,
|
|
163
|
+
loaded, bindings = resolve_agent_config_attachments(keep_items, Path(paths["CWD"]), paths)
|
|
162
164
|
all_attachments = loaded + all_attachments
|
|
163
165
|
|
|
164
166
|
seen_names: set[str] = set()
|
|
@@ -202,8 +204,7 @@ class AgentPreparer:
|
|
|
202
204
|
except Exception:
|
|
203
205
|
prefetch_context = {}
|
|
204
206
|
|
|
205
|
-
# Default agents call list_available_agents() on demand
|
|
206
|
-
# carrying the full list, so this stays opt-in.
|
|
207
|
+
# The agent list is opt-in. Default agents call list_available_agents() on demand.
|
|
207
208
|
if "available_agents" not in prefetch_context and (
|
|
208
209
|
agent_config.auto_load_agent_list or agent_config.auto_load_agents
|
|
209
210
|
):
|
|
@@ -255,7 +256,7 @@ class AgentPreparer:
|
|
|
255
256
|
continue
|
|
256
257
|
meta = registry.get(name)
|
|
257
258
|
if meta is None:
|
|
258
|
-
#
|
|
259
|
+
# The skill vanished (renamed or removed) between turns.
|
|
259
260
|
expired_sticky.append(name)
|
|
260
261
|
continue
|
|
261
262
|
effective_ttl = meta.ttl if meta.ttl is not None else ttl_default
|
|
@@ -299,9 +300,8 @@ class AgentPreparer:
|
|
|
299
300
|
) -> List[Tool]:
|
|
300
301
|
"""Expand the agent's tool specs and apply the capability-based auto-injections.
|
|
301
302
|
|
|
302
|
-
Interactive tools are added only when something can
|
|
303
|
-
|
|
304
|
-
nobody will see.
|
|
303
|
+
Interactive tools are added only when something can answer, and stripped
|
|
304
|
+
otherwise.
|
|
305
305
|
"""
|
|
306
306
|
from tsugite.core.tools import create_tool_from_tsugite
|
|
307
307
|
from tsugite.interaction import get_interaction_backend
|
|
@@ -333,8 +333,8 @@ class AgentPreparer:
|
|
|
333
333
|
raise RuntimeError(f"Failed to create tools: {e}") from e
|
|
334
334
|
|
|
335
335
|
@staticmethod
|
|
336
|
-
def _resolve_paths(path_context: Optional["PathContext"]) ->
|
|
337
|
-
"""
|
|
336
|
+
def _resolve_paths(path_context: Optional["PathContext"]) -> Dict[str, Optional[str]]:
|
|
337
|
+
"""CWD, INVOKED_FROM and WORKSPACE_DIR for the template context.
|
|
338
338
|
|
|
339
339
|
The daemon supplies an effective_cwd that differs from the process cwd.
|
|
340
340
|
"""
|
|
@@ -342,9 +342,11 @@ class AgentPreparer:
|
|
|
342
342
|
cwd = str(path_context.effective_cwd)
|
|
343
343
|
else:
|
|
344
344
|
cwd = str(Path.cwd())
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
345
|
+
return {
|
|
346
|
+
"CWD": cwd,
|
|
347
|
+
"INVOKED_FROM": str(path_context.invoked_from) if path_context else None,
|
|
348
|
+
"WORKSPACE_DIR": str(path_context.workspace_dir) if path_context and path_context.workspace_dir else None,
|
|
349
|
+
}
|
|
348
350
|
|
|
349
351
|
def _build_template_context(
|
|
350
352
|
self,
|
|
@@ -355,9 +357,7 @@ class AgentPreparer:
|
|
|
355
357
|
directive_context: Dict[str, Any],
|
|
356
358
|
attachment_bindings: Dict[str, Any],
|
|
357
359
|
interactive_mode: bool,
|
|
358
|
-
|
|
359
|
-
invoked_from: Optional[str],
|
|
360
|
-
workspace_dir: Optional[str],
|
|
360
|
+
paths: Dict[str, Optional[str]],
|
|
361
361
|
) -> Dict[str, Any]:
|
|
362
362
|
"""Assemble the Jinja context every agent template renders against.
|
|
363
363
|
|
|
@@ -387,9 +387,7 @@ class AgentPreparer:
|
|
|
387
387
|
"is_subagent": context.get("is_subagent", False),
|
|
388
388
|
"parent_agent": context.get("parent_agent", None),
|
|
389
389
|
"chat_history": context.get("chat_history", []),
|
|
390
|
-
|
|
391
|
-
"INVOKED_FROM": invoked_from,
|
|
392
|
-
"WORKSPACE_DIR": workspace_dir,
|
|
390
|
+
**paths,
|
|
393
391
|
}
|
|
394
392
|
|
|
395
393
|
# User-specified attachment `assign:` bindings win over built-in or
|
|
@@ -445,7 +443,8 @@ class AgentPreparer:
|
|
|
445
443
|
|
|
446
444
|
agent_config = agent.config
|
|
447
445
|
|
|
448
|
-
|
|
446
|
+
paths = self._resolve_paths(path_context)
|
|
447
|
+
all_attachments, attachment_bindings = self._resolve_attachments(agent_config, attachments, paths)
|
|
449
448
|
|
|
450
449
|
_skill_manager = self._install_skill_manager(agent_config, path_context)
|
|
451
450
|
|
|
@@ -470,7 +469,6 @@ class AgentPreparer:
|
|
|
470
469
|
)
|
|
471
470
|
|
|
472
471
|
interactive_mode = is_interactive()
|
|
473
|
-
cwd, invoked_from, workspace_dir = self._resolve_paths(path_context)
|
|
474
472
|
full_context = self._build_template_context(
|
|
475
473
|
agent_config=agent_config,
|
|
476
474
|
prompt=prompt,
|
|
@@ -478,9 +476,7 @@ class AgentPreparer:
|
|
|
478
476
|
directive_context={**prefetch_context, **tool_context, **exec_context},
|
|
479
477
|
attachment_bindings=attachment_bindings,
|
|
480
478
|
interactive_mode=interactive_mode,
|
|
481
|
-
|
|
482
|
-
invoked_from=invoked_from,
|
|
483
|
-
workspace_dir=workspace_dir,
|
|
479
|
+
paths=paths,
|
|
484
480
|
)
|
|
485
481
|
|
|
486
482
|
renderer = AgentRenderer()
|
|
@@ -536,7 +532,7 @@ class AgentPreparer:
|
|
|
536
532
|
|
|
537
533
|
system_message = build_system_prompt(tools, combined_instructions)
|
|
538
534
|
|
|
539
|
-
|
|
535
|
+
cwd, invoked_from = paths["CWD"], paths["INVOKED_FROM"]
|
|
540
536
|
if invoked_from and invoked_from != cwd:
|
|
541
537
|
env_block = f"""
|
|
542
538
|
## Environment
|
|
@@ -142,8 +142,9 @@ def run_python_block(
|
|
|
142
142
|
workspace_dir: Optional workspace path for sandbox bind mounts.
|
|
143
143
|
|
|
144
144
|
Raises:
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
TimeoutError: On timeout.
|
|
146
|
+
RuntimeError: On hard execution failure when not continuing, or on
|
|
147
|
+
non-coercible return-value types.
|
|
147
148
|
"""
|
|
148
149
|
safe_locals = _filter_json_safe(locals_dict)
|
|
149
150
|
|
|
@@ -162,7 +163,7 @@ def run_python_block(
|
|
|
162
163
|
try:
|
|
163
164
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
164
165
|
except subprocess.TimeoutExpired as e:
|
|
165
|
-
raise
|
|
166
|
+
raise TimeoutError(f"tsu:exec block timed out after {timeout}s") from e
|
|
166
167
|
|
|
167
168
|
result_data: Dict[str, Any] = {}
|
|
168
169
|
if os.path.exists(result_path):
|
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, List, Optional
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
10
|
from tsugite.console import get_stderr_console
|
|
11
|
+
from tsugite.exceptions import SandboxToolDeniedError # noqa: F401
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from tsugite.options import ExecutionOptions
|
|
@@ -24,8 +25,8 @@ _stderr_console = get_stderr_console()
|
|
|
24
25
|
_current_agent_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("current_agent", default=None)
|
|
25
26
|
|
|
26
27
|
# Active sandbox policy. Presence means "this agent is running sandboxed"; tools
|
|
27
|
-
# read it to propagate the same isolation or to refuse. A nested run
|
|
28
|
-
# teardown here
|
|
28
|
+
# read it to propagate the same isolation or to refuse. A nested run that leaks
|
|
29
|
+
# its teardown here fails OPEN.
|
|
29
30
|
_sandbox_context_var: contextvars.ContextVar[Optional["SandboxContext"]] = contextvars.ContextVar(
|
|
30
31
|
"sandbox_context", default=None
|
|
31
32
|
)
|
|
@@ -47,11 +48,6 @@ class SandboxContext:
|
|
|
47
48
|
workspace_dir: Optional[Path] = None
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
class SandboxToolDeniedError(RuntimeError):
|
|
51
|
-
"""Raised when a host-exec tool is refused because the agent runs sandboxed
|
|
52
|
-
(see the deny_when_sandboxed decorator)."""
|
|
53
|
-
|
|
54
|
-
|
|
55
51
|
def set_sandbox_context(ctx: Optional["SandboxContext"]) -> contextvars.Token:
|
|
56
52
|
"""Set (or clear, with None) the active sandbox policy. Returns a reset token."""
|
|
57
53
|
return _sandbox_context_var.set(ctx)
|
|
@@ -646,10 +646,15 @@ async def _execute_agent_with_prompt(
|
|
|
646
646
|
try:
|
|
647
647
|
from tsugite.usage import get_usage_store
|
|
648
648
|
|
|
649
|
+
meta = channel_metadata or {}
|
|
649
650
|
get_usage_store().record(
|
|
651
|
+
session_id=continue_conversation_id,
|
|
650
652
|
agent=prepared.agent_config.name,
|
|
651
653
|
model=model_string,
|
|
652
|
-
source="cli",
|
|
654
|
+
source=meta.get("source") or "cli",
|
|
655
|
+
schedule_name=meta.get("schedule_id"),
|
|
656
|
+
input_tokens=agent.prompt_tokens,
|
|
657
|
+
output_tokens=agent.completion_tokens,
|
|
653
658
|
total_tokens=agent.total_tokens,
|
|
654
659
|
cost_usd=agent.reported_cost,
|
|
655
660
|
cache_creation_tokens=agent.cache_creation_tokens,
|
|
@@ -355,8 +355,8 @@ async def _execute_step_with_retries(
|
|
|
355
355
|
step_duration = time.time() - step_start_time
|
|
356
356
|
return step_result, step_duration
|
|
357
357
|
|
|
358
|
-
except asyncio.TimeoutError:
|
|
359
|
-
error_msg = f"Step timed out after {step.timeout} seconds"
|
|
358
|
+
except asyncio.TimeoutError as e:
|
|
359
|
+
error_msg = str(e) or f"Step timed out after {step.timeout} seconds"
|
|
360
360
|
errors.append(error_msg)
|
|
361
361
|
code_executed_this_attempt = False
|
|
362
362
|
except AgentSkippedError:
|
|
@@ -17,12 +17,12 @@ from tsugite.utils import has_glob_chars
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _render_path(template: str) -> Optional[str]:
|
|
20
|
+
def _render_path(template: str, template_vars: Dict[str, Any]) -> Optional[str]:
|
|
21
21
|
"""Render a Jinja path template; return None on render failure."""
|
|
22
22
|
from tsugite.renderer import AgentRenderer
|
|
23
23
|
|
|
24
24
|
try:
|
|
25
|
-
return AgentRenderer().render_string(template)
|
|
25
|
+
return AgentRenderer().render_string(template, template_vars)
|
|
26
26
|
except Exception as e:
|
|
27
27
|
logger.debug("Failed to render attachment path %r: %s", template, e)
|
|
28
28
|
return None
|
|
@@ -67,6 +67,7 @@ def _expand_glob(pattern: str, workspace_path: Optional[Path]) -> List[Path]:
|
|
|
67
67
|
def resolve_agent_config_attachments(
|
|
68
68
|
items: List[Union[str, AttachmentSpec]],
|
|
69
69
|
workspace_path: Optional[Path] = None,
|
|
70
|
+
template_vars: Optional[Dict[str, Any]] = None,
|
|
70
71
|
) -> Tuple[List[Attachment], Dict[str, Any]]:
|
|
71
72
|
"""Resolve agent config attachment items to Attachment objects and Jinja bindings.
|
|
72
73
|
|
|
@@ -76,6 +77,7 @@ def resolve_agent_config_attachments(
|
|
|
76
77
|
Args:
|
|
77
78
|
items: List of strings or AttachmentSpec objects from agent_config.attachments
|
|
78
79
|
workspace_path: Optional workspace path for resolving relative paths
|
|
80
|
+
template_vars: Jinja variables for path templates (CWD, INVOKED_FROM, WORKSPACE_DIR)
|
|
79
81
|
|
|
80
82
|
Returns:
|
|
81
83
|
Tuple of (attachments to inject, dict of {assign_name: bound_value}).
|
|
@@ -90,13 +92,14 @@ def resolve_agent_config_attachments(
|
|
|
90
92
|
file_handler = FileHandler()
|
|
91
93
|
attachments: List[Attachment] = []
|
|
92
94
|
bindings: Dict[str, Any] = {}
|
|
95
|
+
template_vars = template_vars or {}
|
|
93
96
|
|
|
94
97
|
for item in items:
|
|
95
98
|
before = len(attachments)
|
|
96
99
|
if isinstance(item, str):
|
|
97
|
-
_resolve_string_item(item, workspace_path, file_handler, attachments)
|
|
100
|
+
_resolve_string_item(item, workspace_path, file_handler, attachments, template_vars)
|
|
98
101
|
else:
|
|
99
|
-
_resolve_spec(item, workspace_path, file_handler, attachments, bindings)
|
|
102
|
+
_resolve_spec(item, workspace_path, file_handler, attachments, bindings, template_vars)
|
|
100
103
|
# Tag everything this item produced with its cache tier (a spec's tier;
|
|
101
104
|
# plain strings are always tier 0). The context turn renders one cache
|
|
102
105
|
# block per tier so a volatile file only invalidates its own block.
|
|
@@ -113,9 +116,10 @@ def _resolve_string_item(
|
|
|
113
116
|
workspace_path: Optional[Path],
|
|
114
117
|
file_handler: "FileHandler",
|
|
115
118
|
attachments: List[Attachment],
|
|
119
|
+
template_vars: Dict[str, Any],
|
|
116
120
|
) -> None:
|
|
117
|
-
"""
|
|
118
|
-
rendered = _render_path(template)
|
|
121
|
+
"""Render the Jinja path, resolve it, fetch the single file."""
|
|
122
|
+
rendered = _render_path(template, template_vars)
|
|
119
123
|
if rendered is None:
|
|
120
124
|
return
|
|
121
125
|
resolved = _resolve_path(rendered, workspace_path)
|
|
@@ -134,9 +138,10 @@ def _resolve_spec(
|
|
|
134
138
|
file_handler: "FileHandler",
|
|
135
139
|
attachments: List[Attachment],
|
|
136
140
|
bindings: Dict[str, Any],
|
|
141
|
+
template_vars: Dict[str, Any],
|
|
137
142
|
) -> None:
|
|
138
143
|
"""Resolve a single AttachmentSpec, populating attachments and bindings."""
|
|
139
|
-
rendered = _render_path(spec.path)
|
|
144
|
+
rendered = _render_path(spec.path, template_vars)
|
|
140
145
|
if rendered is None:
|
|
141
146
|
if spec.assign:
|
|
142
147
|
bindings[spec.assign] = None
|
|
@@ -170,31 +170,22 @@ class AutoContextHandler(AttachmentHandler):
|
|
|
170
170
|
return dirs
|
|
171
171
|
|
|
172
172
|
def _discover_files(self, search_dirs: List[Path], context_files: List[str]) -> List[tuple[Path, str]]:
|
|
173
|
-
"""
|
|
173
|
+
"""Return every context file on the walk, outermost directory first.
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
search_dirs: Directories to search
|
|
179
|
-
context_files: List of filenames to search for
|
|
180
|
-
|
|
181
|
-
Returns:
|
|
182
|
-
List of (file_path, relative_name) tuples for found files
|
|
175
|
+
Names are relative to the outermost directory that has one. A nested
|
|
176
|
+
copy reads as `repos/app/AGENTS.md` next to the outer `AGENTS.md`.
|
|
183
177
|
"""
|
|
184
|
-
found = []
|
|
185
|
-
|
|
178
|
+
found: List[tuple[Path, str]] = []
|
|
179
|
+
outermost_dir: Optional[Path] = None
|
|
186
180
|
|
|
187
|
-
|
|
188
|
-
for directory in search_dirs:
|
|
181
|
+
for directory in reversed(search_dirs):
|
|
189
182
|
for filename in context_files:
|
|
190
|
-
# Skip if we've already found a file with this name
|
|
191
|
-
if filename in seen:
|
|
192
|
-
continue
|
|
193
|
-
|
|
194
183
|
file_path = directory / filename
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
if not file_path.is_file():
|
|
185
|
+
continue
|
|
186
|
+
if outermost_dir is None:
|
|
187
|
+
outermost_dir = directory
|
|
188
|
+
found.append((file_path, file_path.relative_to(outermost_dir).as_posix()))
|
|
198
189
|
|
|
199
190
|
return found
|
|
200
191
|
|
|
@@ -394,6 +394,8 @@ class TsugiteAgent:
|
|
|
394
394
|
# cost data at all" (interrupted turn) - only the latter records NULL.
|
|
395
395
|
self.cost_reported = False
|
|
396
396
|
self.total_tokens = 0
|
|
397
|
+
self.prompt_tokens = 0
|
|
398
|
+
self.completion_tokens = 0
|
|
397
399
|
self.last_input_tokens = 0
|
|
398
400
|
self.cache_creation_tokens = 0
|
|
399
401
|
self.cache_read_tokens = 0
|
|
@@ -1333,6 +1335,8 @@ class TsugiteAgent:
|
|
|
1333
1335
|
return cost or 0.0
|
|
1334
1336
|
|
|
1335
1337
|
self.total_tokens += usage.total_tokens
|
|
1338
|
+
self.prompt_tokens += usage.prompt_tokens
|
|
1339
|
+
self.completion_tokens += usage.completion_tokens
|
|
1336
1340
|
self.last_input_tokens = (
|
|
1337
1341
|
usage.prompt_tokens + (usage.cache_creation_input_tokens or 0) + (usage.cache_read_input_tokens or 0)
|
|
1338
1342
|
)
|
|
@@ -27,7 +27,6 @@ from .state import load_state, save_state
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
-
# Helpers re-exported so the harness can import them without duplicating code
|
|
31
30
|
_HARNESS_IMPORTS = textwrap.dedent("""\
|
|
32
31
|
import ast
|
|
33
32
|
import contextlib
|
|
@@ -65,6 +64,10 @@ _IPC_HELPER = textwrap.dedent("""\
|
|
|
65
64
|
safe[k] = _cap_call_text(repr(v), 500)
|
|
66
65
|
return safe
|
|
67
66
|
|
|
67
|
+
def _ipc_error(error_type, message):
|
|
68
|
+
from tsugite.exceptions import exception_from_name
|
|
69
|
+
return exception_from_name(error_type, message)
|
|
70
|
+
|
|
68
71
|
def _ipc_call(msg_type, **kwargs):
|
|
69
72
|
global _call_id_counter
|
|
70
73
|
_call_id_counter += 1
|
|
@@ -78,7 +81,7 @@ _IPC_HELPER = textwrap.dedent("""\
|
|
|
78
81
|
raise RuntimeError("IPC: parent closed connection")
|
|
79
82
|
resp = json.loads(resp_line)
|
|
80
83
|
if resp.get("error"):
|
|
81
|
-
raise
|
|
84
|
+
raise _ipc_error(resp.get("error_type"), resp["error"])
|
|
82
85
|
return resp.get("result")
|
|
83
86
|
|
|
84
87
|
def _ipc_audit(event, tool, **kwargs):
|
|
@@ -877,7 +880,12 @@ with open(RESULT_PATH, "w") as f:
|
|
|
877
880
|
result = self._ensure_json_serializable(result)
|
|
878
881
|
resp = {"call_id": call_id, "result": result, "error": None}
|
|
879
882
|
except Exception as e:
|
|
880
|
-
resp = {
|
|
883
|
+
resp = {
|
|
884
|
+
"call_id": call_id,
|
|
885
|
+
"result": None,
|
|
886
|
+
"error": str(e),
|
|
887
|
+
"error_type": type(e).__name__,
|
|
888
|
+
}
|
|
881
889
|
else:
|
|
882
890
|
resp = {"call_id": call_id, "result": None, "error": f"Unknown tool: {name}"}
|
|
883
891
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Custom exception classes for Tsugite."""
|
|
2
2
|
|
|
3
|
+
import builtins
|
|
3
4
|
from typing import Any, List, Optional
|
|
4
5
|
|
|
5
6
|
|
|
@@ -25,8 +26,16 @@ class StateSerializationError(RuntimeError):
|
|
|
25
26
|
self.reason = reason
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
class SandboxToolDeniedError(PermissionError):
|
|
30
|
+
"""Raised when a host-exec tool is refused because the agent runs sandboxed."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ToolUnavailableError(RuntimeError):
|
|
34
|
+
"""Raised when a tool cannot run in the current context."""
|
|
35
|
+
|
|
36
|
+
|
|
28
37
|
class AgentExecutionError(RuntimeError):
|
|
29
|
-
"""
|
|
38
|
+
"""Raised when agent execution fails.
|
|
30
39
|
|
|
31
40
|
Includes execution details for debugging and analysis.
|
|
32
41
|
|
|
@@ -75,3 +84,14 @@ def is_unresumable_history_error(error: BaseException | str) -> bool:
|
|
|
75
84
|
never succeed, so the caller must abandon it and start a fresh session.
|
|
76
85
|
"""
|
|
77
86
|
return "text content blocks must be non-empty" in str(error).lower()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def exception_from_name(name: Optional[str], message: str) -> Exception:
|
|
90
|
+
"""Rebuild an exception from a class name looked up in builtins, then this module."""
|
|
91
|
+
cls = (getattr(builtins, name, None) or globals().get(name)) if name else None
|
|
92
|
+
if isinstance(cls, type) and issubclass(cls, Exception):
|
|
93
|
+
try:
|
|
94
|
+
return cls(message)
|
|
95
|
+
except TypeError:
|
|
96
|
+
pass
|
|
97
|
+
return RuntimeError(f"{name}: {message}" if name else message)
|
|
@@ -39,10 +39,9 @@ _EFFORT_TO_BUDGET = {
|
|
|
39
39
|
# fmt: off
|
|
40
40
|
_ANTHROPIC_MODELS: dict[str, ModelInfo] = {
|
|
41
41
|
"anthropic/claude-fable-5": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=10.0, output_cost_per_million=50.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
42
|
+
"anthropic/claude-fable-5-1": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=10.0, output_cost_per_million=50.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
42
43
|
"anthropic/claude-haiku-4-5": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=1.0, output_cost_per_million=5.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
43
44
|
"anthropic/claude-haiku-4-5-20251001": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=1.0, output_cost_per_million=5.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
44
|
-
"anthropic/claude-opus-4-1": ModelInfo(max_input_tokens=200_000, max_output_tokens=32_000, input_cost_per_million=15.0, output_cost_per_million=75.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
45
|
-
"anthropic/claude-opus-4-1-20250805": ModelInfo(max_input_tokens=200_000, max_output_tokens=32_000, input_cost_per_million=15.0, output_cost_per_million=75.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
46
45
|
"anthropic/claude-opus-4-5": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
47
46
|
"anthropic/claude-opus-4-5-20251101": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
48
47
|
"anthropic/claude-opus-4-6": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|