tsugite-cli 0.22.0__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.0 → tsugite_cli-0.23.0}/.gitignore +3 -1
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/AGENTS.md +16 -9
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/CONTRIBUTING.md +9 -16
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/PKG-INFO +1 -1
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/pyproject.toml +1 -1
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_preparation.py +29 -32
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/exec_runner.py +4 -3
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/helpers.py +3 -7
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/history_integration.py +7 -1
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/runner.py +6 -1
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/steps.py +2 -2
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/agent_config.py +12 -7
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/auto_context.py +11 -20
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/base.py +2 -4
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_agents/default.md +2 -2
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/run.py +17 -2
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/agent.py +4 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/proxy.py +8 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/subprocess_executor.py +11 -3
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/exceptions.py +21 -1
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/md_agents.py +2 -2
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/anthropic.py +1 -2
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/openai_compat.py +5 -13
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/__init__.py +9 -16
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/agents.py +45 -50
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/artifacts.py +19 -18
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/fs.py +73 -103
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/http.py +45 -53
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/interactive.py +20 -47
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/jobs.py +25 -29
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/schedule.py +8 -7
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/shell_tools.py +5 -14
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/utils.py +3 -2
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/LICENSE +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/README.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/approval.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/config.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/models.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/daemon_control.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/usage/quota.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.22.0 → tsugite_cli-0.23.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.22.0 → 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
|
|
@@ -119,8 +119,9 @@ class AgentPreparer:
|
|
|
119
119
|
placeholders: Dict[str, str] = {}
|
|
120
120
|
modified = content
|
|
121
121
|
for d in directives:
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
for var in (d.assign_var, getattr(d, "stdout_assign", None)):
|
|
123
|
+
if var:
|
|
124
|
+
placeholders[var] = f"[{kind} directive: {d.name}(...) - not executed in render mode]"
|
|
124
125
|
if rewrite_to is not None:
|
|
125
126
|
modified = modified.replace(d.raw_match, rewrite_to(d))
|
|
126
127
|
return modified, placeholders
|
|
@@ -142,14 +143,16 @@ class AgentPreparer:
|
|
|
142
143
|
)
|
|
143
144
|
|
|
144
145
|
def _resolve_attachments(
|
|
145
|
-
self,
|
|
146
|
+
self,
|
|
147
|
+
agent_config: AgentConfig,
|
|
148
|
+
attachments: Optional[List[Attachment]],
|
|
149
|
+
paths: Dict[str, Optional[str]],
|
|
146
150
|
) -> Tuple[List[Attachment], Dict[str, Any]]:
|
|
147
151
|
"""Merge caller attachments with the agent's own, honoring removals.
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
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.
|
|
153
156
|
"""
|
|
154
157
|
all_attachments = list(attachments or [])
|
|
155
158
|
|
|
@@ -157,7 +160,7 @@ class AgentPreparer:
|
|
|
157
160
|
if removals:
|
|
158
161
|
all_attachments = [a for a in all_attachments if a.name not in removals]
|
|
159
162
|
|
|
160
|
-
loaded, bindings = resolve_agent_config_attachments(keep_items,
|
|
163
|
+
loaded, bindings = resolve_agent_config_attachments(keep_items, Path(paths["CWD"]), paths)
|
|
161
164
|
all_attachments = loaded + all_attachments
|
|
162
165
|
|
|
163
166
|
seen_names: set[str] = set()
|
|
@@ -201,8 +204,7 @@ class AgentPreparer:
|
|
|
201
204
|
except Exception:
|
|
202
205
|
prefetch_context = {}
|
|
203
206
|
|
|
204
|
-
# Default agents call list_available_agents() on demand
|
|
205
|
-
# carrying the full list, so this stays opt-in.
|
|
207
|
+
# The agent list is opt-in. Default agents call list_available_agents() on demand.
|
|
206
208
|
if "available_agents" not in prefetch_context and (
|
|
207
209
|
agent_config.auto_load_agent_list or agent_config.auto_load_agents
|
|
208
210
|
):
|
|
@@ -254,7 +256,7 @@ class AgentPreparer:
|
|
|
254
256
|
continue
|
|
255
257
|
meta = registry.get(name)
|
|
256
258
|
if meta is None:
|
|
257
|
-
#
|
|
259
|
+
# The skill vanished (renamed or removed) between turns.
|
|
258
260
|
expired_sticky.append(name)
|
|
259
261
|
continue
|
|
260
262
|
effective_ttl = meta.ttl if meta.ttl is not None else ttl_default
|
|
@@ -298,9 +300,8 @@ class AgentPreparer:
|
|
|
298
300
|
) -> List[Tool]:
|
|
299
301
|
"""Expand the agent's tool specs and apply the capability-based auto-injections.
|
|
300
302
|
|
|
301
|
-
Interactive tools are added only when something can
|
|
302
|
-
|
|
303
|
-
nobody will see.
|
|
303
|
+
Interactive tools are added only when something can answer, and stripped
|
|
304
|
+
otherwise.
|
|
304
305
|
"""
|
|
305
306
|
from tsugite.core.tools import create_tool_from_tsugite
|
|
306
307
|
from tsugite.interaction import get_interaction_backend
|
|
@@ -332,8 +333,8 @@ class AgentPreparer:
|
|
|
332
333
|
raise RuntimeError(f"Failed to create tools: {e}") from e
|
|
333
334
|
|
|
334
335
|
@staticmethod
|
|
335
|
-
def _resolve_paths(path_context: Optional["PathContext"]) ->
|
|
336
|
-
"""
|
|
336
|
+
def _resolve_paths(path_context: Optional["PathContext"]) -> Dict[str, Optional[str]]:
|
|
337
|
+
"""CWD, INVOKED_FROM and WORKSPACE_DIR for the template context.
|
|
337
338
|
|
|
338
339
|
The daemon supplies an effective_cwd that differs from the process cwd.
|
|
339
340
|
"""
|
|
@@ -341,9 +342,11 @@ class AgentPreparer:
|
|
|
341
342
|
cwd = str(path_context.effective_cwd)
|
|
342
343
|
else:
|
|
343
344
|
cwd = str(Path.cwd())
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
+
}
|
|
347
350
|
|
|
348
351
|
def _build_template_context(
|
|
349
352
|
self,
|
|
@@ -354,9 +357,7 @@ class AgentPreparer:
|
|
|
354
357
|
directive_context: Dict[str, Any],
|
|
355
358
|
attachment_bindings: Dict[str, Any],
|
|
356
359
|
interactive_mode: bool,
|
|
357
|
-
|
|
358
|
-
invoked_from: Optional[str],
|
|
359
|
-
workspace_dir: Optional[str],
|
|
360
|
+
paths: Dict[str, Optional[str]],
|
|
360
361
|
) -> Dict[str, Any]:
|
|
361
362
|
"""Assemble the Jinja context every agent template renders against.
|
|
362
363
|
|
|
@@ -386,9 +387,7 @@ class AgentPreparer:
|
|
|
386
387
|
"is_subagent": context.get("is_subagent", False),
|
|
387
388
|
"parent_agent": context.get("parent_agent", None),
|
|
388
389
|
"chat_history": context.get("chat_history", []),
|
|
389
|
-
|
|
390
|
-
"INVOKED_FROM": invoked_from,
|
|
391
|
-
"WORKSPACE_DIR": workspace_dir,
|
|
390
|
+
**paths,
|
|
392
391
|
}
|
|
393
392
|
|
|
394
393
|
# User-specified attachment `assign:` bindings win over built-in or
|
|
@@ -444,7 +443,8 @@ class AgentPreparer:
|
|
|
444
443
|
|
|
445
444
|
agent_config = agent.config
|
|
446
445
|
|
|
447
|
-
|
|
446
|
+
paths = self._resolve_paths(path_context)
|
|
447
|
+
all_attachments, attachment_bindings = self._resolve_attachments(agent_config, attachments, paths)
|
|
448
448
|
|
|
449
449
|
_skill_manager = self._install_skill_manager(agent_config, path_context)
|
|
450
450
|
|
|
@@ -469,7 +469,6 @@ class AgentPreparer:
|
|
|
469
469
|
)
|
|
470
470
|
|
|
471
471
|
interactive_mode = is_interactive()
|
|
472
|
-
cwd, invoked_from, workspace_dir = self._resolve_paths(path_context)
|
|
473
472
|
full_context = self._build_template_context(
|
|
474
473
|
agent_config=agent_config,
|
|
475
474
|
prompt=prompt,
|
|
@@ -477,9 +476,7 @@ class AgentPreparer:
|
|
|
477
476
|
directive_context={**prefetch_context, **tool_context, **exec_context},
|
|
478
477
|
attachment_bindings=attachment_bindings,
|
|
479
478
|
interactive_mode=interactive_mode,
|
|
480
|
-
|
|
481
|
-
invoked_from=invoked_from,
|
|
482
|
-
workspace_dir=workspace_dir,
|
|
479
|
+
paths=paths,
|
|
483
480
|
)
|
|
484
481
|
|
|
485
482
|
renderer = AgentRenderer()
|
|
@@ -535,7 +532,7 @@ class AgentPreparer:
|
|
|
535
532
|
|
|
536
533
|
system_message = build_system_prompt(tools, combined_instructions)
|
|
537
534
|
|
|
538
|
-
|
|
535
|
+
cwd, invoked_from = paths["CWD"], paths["INVOKED_FROM"]
|
|
539
536
|
if invoked_from and invoked_from != cwd:
|
|
540
537
|
env_block = f"""
|
|
541
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)
|
|
@@ -231,11 +231,14 @@ def save_run_to_history(
|
|
|
231
231
|
# agent ran to completion against this storage.
|
|
232
232
|
existing = list(storage.iter_events())
|
|
233
233
|
last_session_start = -1
|
|
234
|
+
last_user_input = -1
|
|
234
235
|
last_model_response = -1
|
|
235
236
|
last_session_end = -1
|
|
236
237
|
for i, e in enumerate(existing):
|
|
237
238
|
if e.type == "session_start":
|
|
238
239
|
last_session_start = i
|
|
240
|
+
elif e.type == "user_input":
|
|
241
|
+
last_user_input = i
|
|
239
242
|
elif e.type == "model_response":
|
|
240
243
|
last_model_response = i
|
|
241
244
|
elif e.type == "session_end":
|
|
@@ -244,7 +247,10 @@ def save_run_to_history(
|
|
|
244
247
|
agent_already_recorded = last_model_response > last_session_start
|
|
245
248
|
|
|
246
249
|
if agent_already_recorded:
|
|
247
|
-
|
|
250
|
+
# A continued conversation pins session_start at index 0, so the current
|
|
251
|
+
# turn starts at its own user_input.
|
|
252
|
+
turn_start = max(last_session_start, last_user_input)
|
|
253
|
+
if last_session_end < turn_start:
|
|
248
254
|
record_session_end(storage, status=status, error_message=error_message)
|
|
249
255
|
return storage.session_id
|
|
250
256
|
|
|
@@ -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
|
|
|
@@ -68,8 +68,7 @@ class Attachment:
|
|
|
68
68
|
|
|
69
69
|
``key`` is the stable id used for dedupe and the UI chip; ``label`` is the
|
|
70
70
|
human name the model and UI show; ``value`` is the text. Fills the text
|
|
71
|
-
content-type so context producers don't repeat it.
|
|
72
|
-
old ``ContextItem`` carried, now just an Attachment.
|
|
71
|
+
content-type so context producers don't repeat it.
|
|
73
72
|
"""
|
|
74
73
|
return cls(
|
|
75
74
|
name=label,
|
|
@@ -88,8 +87,7 @@ class Attachment:
|
|
|
88
87
|
|
|
89
88
|
@property
|
|
90
89
|
def value(self) -> str:
|
|
91
|
-
"""The text content (an alias of ``content`` when it is text, else "").
|
|
92
|
-
Lets context code read ``.value`` the way the old ``ContextItem`` did."""
|
|
90
|
+
"""The text content (an alias of ``content`` when it is text, else "")."""
|
|
93
91
|
return self.content if isinstance(self.content, str) else ""
|
|
94
92
|
|
|
95
93
|
def to_metadata(self) -> dict:
|
|
@@ -157,12 +157,12 @@ You are managing a shared channel. When a user asks for something that would ben
|
|
|
157
157
|
- **Inline** - single edit, one-shot read, small calculation. Don't spawn anything.
|
|
158
158
|
- **`spawn_job`** - background work: long-running tasks, parallel work, or delegating to a different agent/model. Fire-and-forget by default; add `acceptance_criteria=[...]` when "done" has a checkable shape (tests pass, copy satisfies criteria, refactor preserves behavior) and the verification loop becomes the value.
|
|
159
159
|
|
|
160
|
-
**Acceptance criteria**: pass each criterion as a plain string. The verifier reads
|
|
160
|
+
**Acceptance criteria**: pass each criterion as a plain string. A criterion starting with `cmd:<command>` (passes on exit 0), `exit_code:<command>:<n>` (passes on exit code `n`, default 0) or `file_exists:<path>` is checked in the Job's working directory with no verifier model. Write one whenever the check is mechanical. The verifier reads the rest verbatim and returns per-criterion pass/fail in `result.ac_results`:
|
|
161
161
|
```python-exec
|
|
162
162
|
spawn_job(
|
|
163
163
|
prompt="Refactor the agent loop to handle nested tool calls",
|
|
164
164
|
acceptance_criteria=[
|
|
165
|
-
"
|
|
165
|
+
"cmd:uv run pytest -q",
|
|
166
166
|
"Adds at least 2 new tests for nested calls",
|
|
167
167
|
"No new dependencies",
|
|
168
168
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CLI run command - execute agents."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from types import SimpleNamespace
|
|
4
5
|
from typing import Any, Dict, List, Optional, Tuple
|
|
5
6
|
|
|
6
7
|
import typer
|
|
@@ -137,11 +138,18 @@ def _execute_agent_with_ui(
|
|
|
137
138
|
"""Execute agent with appropriate UI mode.
|
|
138
139
|
|
|
139
140
|
Dispatch order:
|
|
140
|
-
1. --
|
|
141
|
-
2.
|
|
141
|
+
1. --log-json -> JSONL event stream on stdout
|
|
142
|
+
2. --headless / --final-only -> custom_agent_ui with silent progress
|
|
143
|
+
3. fall-through default -> plain logger (covers no flags, --plain, piped/NO_COLOR)
|
|
142
144
|
"""
|
|
143
145
|
from tsugite.ui import create_plain_logger, custom_agent_ui
|
|
144
146
|
|
|
147
|
+
if ui_opts.log_json:
|
|
148
|
+
from tsugite.ui.jsonl import JSONLUIHandler
|
|
149
|
+
|
|
150
|
+
executor_kwargs["custom_logger"] = SimpleNamespace(ui_handler=JSONLUIHandler())
|
|
151
|
+
return executor(**executor_kwargs)
|
|
152
|
+
|
|
145
153
|
if ui_opts.headless or ui_opts.final_only:
|
|
146
154
|
stderr_console = get_error_console(True, console)
|
|
147
155
|
show_progress_items = ui_opts.verbose and not ui_opts.final_only
|
|
@@ -188,6 +196,9 @@ def _display_result(result_str: str, ui_opts: UIOptions, stderr_console: Console
|
|
|
188
196
|
|
|
189
197
|
from tsugite.console import get_stdout_console
|
|
190
198
|
|
|
199
|
+
if ui_opts.log_json:
|
|
200
|
+
return
|
|
201
|
+
|
|
191
202
|
if not (ui_opts.headless or ui_opts.final_only):
|
|
192
203
|
stderr_console.print()
|
|
193
204
|
stderr_console.rule("[bold green]Agent Execution Complete[/bold green]")
|
|
@@ -358,6 +369,10 @@ def run(
|
|
|
358
369
|
|
|
359
370
|
if history_opts.storage_dir:
|
|
360
371
|
history_opts.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
372
|
+
from tsugite.history import set_history_backend
|
|
373
|
+
from tsugite.history.sqlite_backend import SqliteHistoryBackend
|
|
374
|
+
|
|
375
|
+
set_history_backend(SqliteHistoryBackend(history_opts.storage_dir / "history.db"))
|
|
361
376
|
|
|
362
377
|
if ui_opts.no_color:
|
|
363
378
|
console.no_color = True
|
|
@@ -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
|
)
|