tsugite-cli 0.19.0__tar.gz → 0.19.2__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.19.0 → tsugite_cli-0.19.2}/AGENTS.md +9 -9
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/PKG-INFO +1 -1
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/pyproject.toml +1 -1
- tsugite_cli-0.19.2/tsugite/agent_preparation.py +566 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/__init__.py +8 -4
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/helpers.py +83 -45
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/history_integration.py +7 -5
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/runner.py +205 -944
- tsugite_cli-0.19.2/tsugite/agent_runner/steps.py +686 -0
- tsugite_cli-0.19.2/tsugite/attachments/agent_config.py +349 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/file.py +39 -1
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/daemon.py +46 -40
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/history.py +1 -92
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/run.py +12 -21
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/config.py +6 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/agent.py +91 -119
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/executor.py +13 -56
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/proxy.py +34 -7
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/subprocess_executor.py +28 -22
- tsugite_cli-0.19.2/tsugite/core/tools.py +184 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/events.py +5 -1
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/__init__.py +2 -13
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/base.py +3 -7
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/models.py +55 -1
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/registry.py +2 -32
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/sqlite_backend.py +17 -47
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/ui_events.py +4 -43
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/hooks.py +7 -1
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/anthropic.py +27 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/openai_compat.py +12 -1
- tsugite_cli-0.19.2/tsugite/secrets/redaction.py +86 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/__init__.py +33 -17
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/http.py +9 -2
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/base.py +24 -34
- tsugite_cli-0.19.2/tsugite/ui/dispatch.py +76 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/jsonl.py +25 -35
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_handler.py +19 -25
- tsugite_cli-0.19.0/tsugite/agent_preparation.py +0 -808
- tsugite_cli-0.19.0/tsugite/core/tools.py +0 -270
- tsugite_cli-0.19.0/tsugite/history/storage.py +0 -311
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/.gitignore +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/LICENSE +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/README.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/approval.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cache.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/console.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/constants.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/context.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/models.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/options.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/py.typed +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/jobs.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/scratchpad.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/utils.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.19.0 → tsugite_cli-0.19.2}/tsugite/workspace/templates.py +0 -0
|
@@ -20,8 +20,8 @@ uv run pytest tests/test_file.py::test_function
|
|
|
20
20
|
# Run tests with coverage
|
|
21
21
|
uv run pytest --cov=tsugite --cov-report=html
|
|
22
22
|
|
|
23
|
-
# Lint and format
|
|
24
|
-
uv run
|
|
23
|
+
# Lint and format (CI gates on these, repo-wide)
|
|
24
|
+
uv run ruff format .
|
|
25
25
|
uv run ruff check .
|
|
26
26
|
uv run pylint tsugite
|
|
27
27
|
|
|
@@ -266,11 +266,11 @@ tsugite run +coordinator +researcher +writer "task"
|
|
|
266
266
|
|
|
267
267
|
### Prompt Caching
|
|
268
268
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
- `tsugite/core/agent.py` marks each context turn (attachments, plus skills on the last tier) with a
|
|
270
|
+
provider-neutral `cache_control` hint
|
|
271
|
+
- Each provider translates the hint for its own API: Anthropic moves it onto a content block, capped at the
|
|
272
|
+
API's 4 breakpoints; OpenAI-schema endpoints cache automatically and drop it, since strict ones reject the
|
|
273
|
+
unknown field
|
|
274
274
|
|
|
275
275
|
### Context Management (Daemon Mode)
|
|
276
276
|
|
|
@@ -504,7 +504,7 @@ Source lives at `plugins/tsugite-daemon/frontend/` (**Svelte 5 runes + TypeScrip
|
|
|
504
504
|
|
|
505
505
|
- `mise run web-dev` - Vite dev server on :5173 with HMR, proxying `/api` to a local daemon on 127.0.0.1:8374 (`uv run tsu daemon`).
|
|
506
506
|
- `mise run web-build` - production build into `tsugite_daemon/web/` (gitignored; never committed).
|
|
507
|
-
- `mise run web-check` - svelte-check + vitest + prettier check.
|
|
507
|
+
- `mise run web-check` - svelte-check + vitest unit project + prettier check. It does not run component tests (`*.svelte.test.ts`); those need `npm --prefix plugins/tsugite-daemon/frontend run test:browser`.
|
|
508
508
|
|
|
509
509
|
### Theme tokens
|
|
510
510
|
|
|
@@ -513,7 +513,7 @@ Source lives at `plugins/tsugite-daemon/frontend/` (**Svelte 5 runes + TypeScrip
|
|
|
513
513
|
|
|
514
514
|
### Testing
|
|
515
515
|
|
|
516
|
-
- e2e selectors use `data-testid` values from `frontend/src/lib/testids.ts` (a frozen contract - extend, don't rename). App readiness marker: `[data-testid="app-ready"]
|
|
516
|
+
- e2e selectors use `data-testid` values from `frontend/src/lib/testids.ts` (a frozen contract - extend, don't rename). App readiness marker: `[data-testid="app-ready"]`. There are no window-global test hooks; drive the real DOM and seed over HTTP.
|
|
517
517
|
- Frontend behavioral logic (stores, SSE replay, parsers/reducers) is vitest-tested (`frontend/src/**/*.test.ts`), failing-test-first.
|
|
518
518
|
- Browser verification on UI changes is mandatory. Daemon spinup + Playwright wiring recipe: project memory `project_daemon_ui_smoketest.md` (its DOM selector list predates the Svelte rebuild - trust the testids contract instead).
|
|
519
519
|
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"""Agent preparation pipeline - unified logic for render and execution."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from tsugite.cli.helpers import PathContext
|
|
12
|
+
from tsugite.tools.skills import SkillManager
|
|
13
|
+
|
|
14
|
+
from tsugite.attachments.agent_config import ( # noqa: E402
|
|
15
|
+
resolve_agent_config_attachments,
|
|
16
|
+
split_attachment_removals,
|
|
17
|
+
)
|
|
18
|
+
from tsugite.attachments.base import Attachment # noqa: E402
|
|
19
|
+
from tsugite.core.tools import Tool # noqa: E402
|
|
20
|
+
from tsugite.md_agents import Agent, AgentConfig # noqa: E402
|
|
21
|
+
from tsugite.skill_discovery import Skill # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SkillLoad:
|
|
26
|
+
"""What one turn's skill resolution produced.
|
|
27
|
+
|
|
28
|
+
`expired`/`triggered`/`auto_loaded` are read back by the daemon to update its
|
|
29
|
+
sticky state after the turn, so they are real outputs rather than bookkeeping;
|
|
30
|
+
`prepare()` publishes them onto the template context explicitly.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
skills: List[Skill]
|
|
34
|
+
expiring: Dict[str, int]
|
|
35
|
+
expired: List[str]
|
|
36
|
+
triggered: List[str]
|
|
37
|
+
auto_loaded: List[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PreparedAgent:
|
|
42
|
+
"""Fully prepared agent ready for execution or display.
|
|
43
|
+
|
|
44
|
+
This dataclass contains everything needed to either:
|
|
45
|
+
1. Display what will be sent to the LLM (render command)
|
|
46
|
+
2. Execute the agent (run command)
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
agent: Parsed agent object with content and config
|
|
50
|
+
agent_config: Agent configuration (model, tools, etc.)
|
|
51
|
+
system_message: Complete system message sent to LLM
|
|
52
|
+
user_message: Complete user message sent to LLM
|
|
53
|
+
rendered_prompt: Rendered template (before building system message)
|
|
54
|
+
original_prompt: The user's prompt as typed (pre-template-rendering)
|
|
55
|
+
tools: List of Tool objects ready for agent execution
|
|
56
|
+
context: Full template rendering context
|
|
57
|
+
combined_instructions: Combined default + agent instructions
|
|
58
|
+
attachments: List of Attachment objects for multi-modal inputs
|
|
59
|
+
skills: List of Skill objects for loaded skills
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
agent: Agent
|
|
63
|
+
agent_config: AgentConfig
|
|
64
|
+
system_message: str
|
|
65
|
+
user_message: str
|
|
66
|
+
rendered_prompt: str
|
|
67
|
+
tools: List[Tool]
|
|
68
|
+
context: Dict[str, Any]
|
|
69
|
+
combined_instructions: str
|
|
70
|
+
attachments: List[Attachment]
|
|
71
|
+
original_prompt: str = ""
|
|
72
|
+
skills: List[Skill] = field(default_factory=list)
|
|
73
|
+
# Map of sticky skill name -> turns remaining before auto-unload. Only
|
|
74
|
+
# populated on the turn a skill is about to expire (turns_remaining <= 0).
|
|
75
|
+
# The agent surfaces these as <skill_expiring> blocks in the context turn.
|
|
76
|
+
expiring_skills: Dict[str, int] = field(default_factory=dict)
|
|
77
|
+
skipped: bool = False
|
|
78
|
+
skip_reason: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AgentPreparer:
|
|
82
|
+
"""Prepares agents for execution or rendering.
|
|
83
|
+
|
|
84
|
+
This class consolidates all agent preparation logic that was previously
|
|
85
|
+
duplicated across render command, run_agent, and _execute_agent_with_prompt.
|
|
86
|
+
|
|
87
|
+
`prepare()` orchestrates, one named step per phase:
|
|
88
|
+
|
|
89
|
+
_resolve_attachments caller + frontmatter attachments, removals, dedupe
|
|
90
|
+
_install_skill_manager workspace-aware SkillManager, made active
|
|
91
|
+
_run_prefetch frontmatter prefetch tools + agent-list injection
|
|
92
|
+
_resolve_paths cwd / invoked_from / workspace_dir
|
|
93
|
+
_build_template_context the Jinja context and its framework defaults
|
|
94
|
+
_expand_tools tool specs + capability-based auto-injection
|
|
95
|
+
_load_skills auto-load, sticky TTL, trigger matching
|
|
96
|
+
|
|
97
|
+
Both `render` and `run` go through here, so render shows EXACTLY what run
|
|
98
|
+
executes.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def _build_directive_placeholders(
|
|
102
|
+
self,
|
|
103
|
+
content: str,
|
|
104
|
+
extractor: Callable[[str], List[Any]],
|
|
105
|
+
kind: str,
|
|
106
|
+
rewrite_to: Optional[Callable[[Any], str]] = None,
|
|
107
|
+
) -> Tuple[str, Dict[str, str]]:
|
|
108
|
+
"""Build {assign_var: placeholder} for directives that won't actually run.
|
|
109
|
+
|
|
110
|
+
Used by render-mode shortcuts so the template still has all its variables
|
|
111
|
+
defined. When `rewrite_to` is given, the directive substring is also
|
|
112
|
+
replaced in `content` with the callable's per-directive output.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
directives = extractor(content)
|
|
116
|
+
except Exception:
|
|
117
|
+
return content, {}
|
|
118
|
+
|
|
119
|
+
placeholders: Dict[str, str] = {}
|
|
120
|
+
modified = content
|
|
121
|
+
for d in directives:
|
|
122
|
+
if d.assign_var:
|
|
123
|
+
placeholders[d.assign_var] = f"[{kind} directive: {d.name}(...) - not executed in render mode]"
|
|
124
|
+
if rewrite_to is not None:
|
|
125
|
+
modified = modified.replace(d.raw_match, rewrite_to(d))
|
|
126
|
+
return modified, placeholders
|
|
127
|
+
|
|
128
|
+
def _extract_tool_directive_placeholders(self, content: str) -> Dict[str, str]:
|
|
129
|
+
from tsugite.md_agents import extract_tool_directives
|
|
130
|
+
|
|
131
|
+
_, placeholders = self._build_directive_placeholders(content, extract_tool_directives, "Tool")
|
|
132
|
+
return placeholders
|
|
133
|
+
|
|
134
|
+
def _extract_exec_directive_placeholders(self, content: str) -> Tuple[str, Dict[str, str]]:
|
|
135
|
+
from tsugite.md_agents import extract_exec_directives
|
|
136
|
+
|
|
137
|
+
return self._build_directive_placeholders(
|
|
138
|
+
content,
|
|
139
|
+
extract_exec_directives,
|
|
140
|
+
"Exec",
|
|
141
|
+
rewrite_to=lambda d: f"<!-- Exec '{d.name}' skipped (--no-exec) -->",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _resolve_attachments(
|
|
145
|
+
self, agent_config: AgentConfig, attachments: Optional[List[Attachment]]
|
|
146
|
+
) -> Tuple[List[Attachment], Dict[str, Any]]:
|
|
147
|
+
"""Merge caller attachments with the agent's own, honoring removals.
|
|
148
|
+
|
|
149
|
+
Front-matter attachments carry the cache tiers and are the intended
|
|
150
|
+
source, so they dedupe ahead of any same-named attachment the caller
|
|
151
|
+
passed in. Legacy `-filename` string entries drop a same-named entry.
|
|
152
|
+
Front-matter paths resolve via cwd (workspace_path is None in production).
|
|
153
|
+
"""
|
|
154
|
+
all_attachments = list(attachments or [])
|
|
155
|
+
|
|
156
|
+
removals, keep_items = split_attachment_removals(agent_config.attachments or [])
|
|
157
|
+
if removals:
|
|
158
|
+
all_attachments = [a for a in all_attachments if a.name not in removals]
|
|
159
|
+
|
|
160
|
+
loaded, bindings = resolve_agent_config_attachments(keep_items, None)
|
|
161
|
+
all_attachments = loaded + all_attachments
|
|
162
|
+
|
|
163
|
+
seen_names: set[str] = set()
|
|
164
|
+
deduped: List[Attachment] = []
|
|
165
|
+
for att in all_attachments:
|
|
166
|
+
if att.name not in seen_names:
|
|
167
|
+
seen_names.add(att.name)
|
|
168
|
+
deduped.append(att)
|
|
169
|
+
return deduped, bindings
|
|
170
|
+
|
|
171
|
+
def _install_skill_manager(
|
|
172
|
+
self, agent_config: AgentConfig, path_context: Optional["PathContext"]
|
|
173
|
+
) -> "SkillManager":
|
|
174
|
+
"""Build the workspace-aware skill manager and make it the active one.
|
|
175
|
+
|
|
176
|
+
The workspace comes from path_context.workspace_dir so daemon and chat
|
|
177
|
+
callers still get workspace skills.
|
|
178
|
+
"""
|
|
179
|
+
from tsugite.config import load_config
|
|
180
|
+
from tsugite.tools.skills import SkillManager, set_skill_manager
|
|
181
|
+
from tsugite.workspace.models import Workspace
|
|
182
|
+
|
|
183
|
+
workspace = Workspace.try_load(path_context.workspace_dir if path_context else None)
|
|
184
|
+
extra_paths = (agent_config.skill_paths or []) + (load_config().skill_paths or [])
|
|
185
|
+
manager = SkillManager(workspace=workspace, extra_paths=extra_paths or None)
|
|
186
|
+
set_skill_manager(manager)
|
|
187
|
+
return manager
|
|
188
|
+
|
|
189
|
+
def _run_prefetch(self, agent_config: AgentConfig) -> Dict[str, Any]:
|
|
190
|
+
"""Run frontmatter prefetch tools, plus the opt-in agent-list injection.
|
|
191
|
+
|
|
192
|
+
A failing prefetch yields an empty context rather than failing the run:
|
|
193
|
+
prefetch is supplementary context, not a precondition.
|
|
194
|
+
"""
|
|
195
|
+
from tsugite.agent_runner import execute_prefetch
|
|
196
|
+
|
|
197
|
+
prefetch_context: Dict[str, Any] = {}
|
|
198
|
+
if agent_config.prefetch:
|
|
199
|
+
try:
|
|
200
|
+
prefetch_context = execute_prefetch(agent_config.prefetch)
|
|
201
|
+
except Exception:
|
|
202
|
+
prefetch_context = {}
|
|
203
|
+
|
|
204
|
+
# Default agents call list_available_agents() on demand instead of
|
|
205
|
+
# carrying the full list, so this stays opt-in.
|
|
206
|
+
if "available_agents" not in prefetch_context and (
|
|
207
|
+
agent_config.auto_load_agent_list or agent_config.auto_load_agents
|
|
208
|
+
):
|
|
209
|
+
from tsugite.tools.agents import discover_agents, format_agents_markdown
|
|
210
|
+
|
|
211
|
+
agents = discover_agents()
|
|
212
|
+
if agent_config.auto_load_agents:
|
|
213
|
+
wanted = set(agent_config.auto_load_agents)
|
|
214
|
+
agents = [a for a in agents if a["name"] in wanted]
|
|
215
|
+
prefetch_context["available_agents"] = format_agents_markdown(agents)
|
|
216
|
+
|
|
217
|
+
return prefetch_context
|
|
218
|
+
|
|
219
|
+
def _load_skills(
|
|
220
|
+
self,
|
|
221
|
+
agent_config: AgentConfig,
|
|
222
|
+
prompt: str,
|
|
223
|
+
full_context: Dict[str, Any],
|
|
224
|
+
skill_manager: "SkillManager",
|
|
225
|
+
event_bus: Optional[Any],
|
|
226
|
+
) -> SkillLoad:
|
|
227
|
+
"""Load auto, sticky and trigger-matched skills for this turn."""
|
|
228
|
+
from tsugite.events.events import SkillLoadFailedEvent
|
|
229
|
+
|
|
230
|
+
# Skills the user explicitly removed this session (populated by the daemon).
|
|
231
|
+
suppressed_skills = set(full_context.get("suppressed_skills") or [])
|
|
232
|
+
|
|
233
|
+
# Sticky skills carried over from prior turns on this session (daemon-only).
|
|
234
|
+
# Shape: {skill_name: turns_unused_counter}
|
|
235
|
+
sticky_counters: Dict[str, int] = dict(full_context.get("sticky_skills") or {})
|
|
236
|
+
ttl_default = int(full_context.get("skill_ttl_default") or 10)
|
|
237
|
+
|
|
238
|
+
auto_load_skills = [s for s in (agent_config.auto_load_skills or []) if s not in suppressed_skills]
|
|
239
|
+
|
|
240
|
+
for skill_name in auto_load_skills:
|
|
241
|
+
result = skill_manager.load_skill(skill_name)
|
|
242
|
+
if result.startswith("Failed") or result.startswith("Skill '"):
|
|
243
|
+
if event_bus:
|
|
244
|
+
event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
|
|
245
|
+
|
|
246
|
+
# unless the counter already exceeded the skill's effective TTL (expired).
|
|
247
|
+
skill_manager._ensure_registry_initialized()
|
|
248
|
+
registry = skill_manager._skill_registry
|
|
249
|
+
expiring_skills: Dict[str, int] = {}
|
|
250
|
+
expired_sticky: List[str] = []
|
|
251
|
+
for name, counter in sticky_counters.items():
|
|
252
|
+
if name in suppressed_skills:
|
|
253
|
+
expired_sticky.append(name)
|
|
254
|
+
continue
|
|
255
|
+
meta = registry.get(name)
|
|
256
|
+
if meta is None:
|
|
257
|
+
# Skill vanished (renamed/removed) between turns — drop it.
|
|
258
|
+
expired_sticky.append(name)
|
|
259
|
+
continue
|
|
260
|
+
effective_ttl = meta.ttl if meta.ttl is not None else ttl_default
|
|
261
|
+
if effective_ttl > 0 and counter > effective_ttl:
|
|
262
|
+
expired_sticky.append(name)
|
|
263
|
+
continue
|
|
264
|
+
if name in auto_load_skills:
|
|
265
|
+
# Already loaded above; no need to double-load but it's still sticky.
|
|
266
|
+
continue
|
|
267
|
+
result = skill_manager.load_skill(name)
|
|
268
|
+
if result.startswith("Failed") or result.startswith("Skill '"):
|
|
269
|
+
if event_bus:
|
|
270
|
+
event_bus.emit(SkillLoadFailedEvent(skill_name=name, error_message=result))
|
|
271
|
+
continue
|
|
272
|
+
if effective_ttl > 0:
|
|
273
|
+
remaining = effective_ttl - counter
|
|
274
|
+
if remaining <= 1:
|
|
275
|
+
expiring_skills[name] = max(remaining, 0)
|
|
276
|
+
|
|
277
|
+
triggered_skill_names = [
|
|
278
|
+
name for name in skill_manager.get_triggered_skills(prompt) if name not in suppressed_skills
|
|
279
|
+
]
|
|
280
|
+
for skill_name in triggered_skill_names:
|
|
281
|
+
logger.info(f"Trigger-loading skill '{skill_name}' based on user prompt")
|
|
282
|
+
result = skill_manager.load_skill(skill_name)
|
|
283
|
+
if result.startswith("Failed") or result.startswith("Skill '"):
|
|
284
|
+
if event_bus:
|
|
285
|
+
event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
|
|
286
|
+
|
|
287
|
+
loaded_skills_dict = skill_manager.get_loaded_skills()
|
|
288
|
+
return SkillLoad(
|
|
289
|
+
skills=[Skill(name=name, content=content) for name, content in loaded_skills_dict.items()],
|
|
290
|
+
expiring=expiring_skills,
|
|
291
|
+
expired=expired_sticky,
|
|
292
|
+
triggered=list(triggered_skill_names),
|
|
293
|
+
auto_loaded=list(auto_load_skills),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def _expand_tools(
|
|
297
|
+
self, agent_config: AgentConfig, full_context: Dict[str, Any], interactive_mode: bool
|
|
298
|
+
) -> List[Tool]:
|
|
299
|
+
"""Expand the agent's tool specs and apply the capability-based auto-injections.
|
|
300
|
+
|
|
301
|
+
Interactive tools are added only when something can actually answer, and
|
|
302
|
+
stripped otherwise, so a scheduled run cannot offer the model a prompt
|
|
303
|
+
nobody will see.
|
|
304
|
+
"""
|
|
305
|
+
from tsugite.core.tools import create_tool_from_tsugite
|
|
306
|
+
from tsugite.interaction import get_interaction_backend
|
|
307
|
+
from tsugite.tools import _tools, expand_tool_specs
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
expanded = (
|
|
311
|
+
expand_tool_specs(agent_config.tools, strict=agent_config.strict_tools) if agent_config.tools else []
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
interactive_tool_names = ["ask_user", "ask_user_batch"]
|
|
315
|
+
has_interaction = (
|
|
316
|
+
interactive_mode or get_interaction_backend() is not None or full_context.get("is_daemon", False)
|
|
317
|
+
)
|
|
318
|
+
if has_interaction:
|
|
319
|
+
for name in interactive_tool_names:
|
|
320
|
+
if name not in expanded and name in _tools:
|
|
321
|
+
expanded.append(name)
|
|
322
|
+
else:
|
|
323
|
+
expanded = [t for t in expanded if t not in interactive_tool_names]
|
|
324
|
+
|
|
325
|
+
# Scheduled tasks get a way to reach the user even if the agent didn't ask.
|
|
326
|
+
if full_context.get("has_notify_tool", False):
|
|
327
|
+
if "notify_user" not in expanded and "notify_user" in _tools:
|
|
328
|
+
expanded.append("notify_user")
|
|
329
|
+
|
|
330
|
+
return [create_tool_from_tsugite(name) for name in expanded]
|
|
331
|
+
except Exception as e:
|
|
332
|
+
raise RuntimeError(f"Failed to create tools: {e}") from e
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _resolve_paths(path_context: Optional["PathContext"]) -> Tuple[str, Optional[str], Optional[str]]:
|
|
336
|
+
"""Return (cwd, invoked_from, workspace_dir) as strings for the template context.
|
|
337
|
+
|
|
338
|
+
The daemon supplies an effective_cwd that differs from the process cwd.
|
|
339
|
+
"""
|
|
340
|
+
if path_context and path_context.effective_cwd:
|
|
341
|
+
cwd = str(path_context.effective_cwd)
|
|
342
|
+
else:
|
|
343
|
+
cwd = str(Path.cwd())
|
|
344
|
+
invoked_from = str(path_context.invoked_from) if path_context else None
|
|
345
|
+
workspace_dir = str(path_context.workspace_dir) if path_context and path_context.workspace_dir else None
|
|
346
|
+
return cwd, invoked_from, workspace_dir
|
|
347
|
+
|
|
348
|
+
def _build_template_context(
|
|
349
|
+
self,
|
|
350
|
+
*,
|
|
351
|
+
agent_config: AgentConfig,
|
|
352
|
+
prompt: str,
|
|
353
|
+
context: Dict[str, Any],
|
|
354
|
+
directive_context: Dict[str, Any],
|
|
355
|
+
attachment_bindings: Dict[str, Any],
|
|
356
|
+
interactive_mode: bool,
|
|
357
|
+
cwd: str,
|
|
358
|
+
invoked_from: Optional[str],
|
|
359
|
+
workspace_dir: Optional[str],
|
|
360
|
+
) -> Dict[str, Any]:
|
|
361
|
+
"""Assemble the Jinja context every agent template renders against.
|
|
362
|
+
|
|
363
|
+
Framework flags default here rather than in the template so a caller that
|
|
364
|
+
supplies none of them still renders under StrictUndefined.
|
|
365
|
+
"""
|
|
366
|
+
from tsugite.tools import list_tools
|
|
367
|
+
|
|
368
|
+
full_context = {
|
|
369
|
+
**context,
|
|
370
|
+
**directive_context,
|
|
371
|
+
"user_prompt": prompt,
|
|
372
|
+
"agent_name": agent_config.name,
|
|
373
|
+
"is_interactive": interactive_mode,
|
|
374
|
+
"is_daemon": context.get("is_daemon", False),
|
|
375
|
+
"is_scheduled": context.get("is_scheduled", False),
|
|
376
|
+
"schedule_id": context.get("schedule_id", ""),
|
|
377
|
+
"has_notify_tool": context.get("has_notify_tool", False),
|
|
378
|
+
"running_tasks": context.get("running_tasks", []),
|
|
379
|
+
"tsugite_url": context.get("tsugite_url", ""),
|
|
380
|
+
"tsugite_token": context.get("tsugite_token", ""),
|
|
381
|
+
"tools": agent_config.tools,
|
|
382
|
+
# Tool names actually installed, so templates can conditionally mention optional
|
|
383
|
+
# tools, e.g. `{% if 'web_search' in available_tools %}`.
|
|
384
|
+
"available_tools": list_tools(),
|
|
385
|
+
"is_subagent": context.get("is_subagent", False),
|
|
386
|
+
"parent_agent": context.get("parent_agent", None),
|
|
387
|
+
"chat_history": context.get("chat_history", []),
|
|
388
|
+
"CWD": cwd,
|
|
389
|
+
"INVOKED_FROM": invoked_from,
|
|
390
|
+
"WORKSPACE_DIR": workspace_dir,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# User-specified attachment `assign:` bindings win over built-in or
|
|
394
|
+
# prefetch values; warn so collisions surface rather than confuse.
|
|
395
|
+
for name, value in attachment_bindings.items():
|
|
396
|
+
if name in full_context:
|
|
397
|
+
logger.warning("Attachment binding %r overrides existing context variable", name)
|
|
398
|
+
full_context[name] = value
|
|
399
|
+
|
|
400
|
+
return full_context
|
|
401
|
+
|
|
402
|
+
def prepare(
|
|
403
|
+
self,
|
|
404
|
+
agent: Agent,
|
|
405
|
+
prompt: str,
|
|
406
|
+
context: Optional[Dict[str, Any]] = None,
|
|
407
|
+
skip_tool_directives: bool = False,
|
|
408
|
+
skip_exec_directives: bool = False,
|
|
409
|
+
attachments: Optional[List[Attachment]] = None,
|
|
410
|
+
event_bus: Optional[Any] = None,
|
|
411
|
+
path_context: Optional["PathContext"] = None,
|
|
412
|
+
) -> PreparedAgent:
|
|
413
|
+
"""Prepare agent with all context, tools, and instructions.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
agent: Parsed agent object
|
|
417
|
+
prompt: User prompt/task
|
|
418
|
+
context: Additional context variables
|
|
419
|
+
skip_tool_directives: Skip executing tool directives (for render)
|
|
420
|
+
skip_exec_directives: Skip executing exec directives (for render --no-exec)
|
|
421
|
+
attachments: List of Attachment objects for multi-modal inputs
|
|
422
|
+
event_bus: Optional event bus for emitting skill load events
|
|
423
|
+
path_context: Optional PathContext with invoked_from, workspace_dir, effective_cwd
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
PreparedAgent ready for execution or display
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
RuntimeError: If preparation fails
|
|
430
|
+
"""
|
|
431
|
+
from tsugite.agent_runner import (
|
|
432
|
+
_combine_instructions,
|
|
433
|
+
execute_exec_directives,
|
|
434
|
+
execute_tool_directives,
|
|
435
|
+
get_default_instructions,
|
|
436
|
+
)
|
|
437
|
+
from tsugite.core.agent import build_system_prompt
|
|
438
|
+
from tsugite.renderer import AgentRenderer
|
|
439
|
+
from tsugite.utils import is_interactive
|
|
440
|
+
|
|
441
|
+
if context is None:
|
|
442
|
+
context = {}
|
|
443
|
+
|
|
444
|
+
agent_config = agent.config
|
|
445
|
+
|
|
446
|
+
all_attachments, attachment_bindings = self._resolve_attachments(agent_config, attachments)
|
|
447
|
+
|
|
448
|
+
_skill_manager = self._install_skill_manager(agent_config, path_context)
|
|
449
|
+
|
|
450
|
+
prefetch_context = self._run_prefetch(agent_config)
|
|
451
|
+
|
|
452
|
+
if skip_tool_directives:
|
|
453
|
+
modified_content = agent.content
|
|
454
|
+
# Extract tool directive variable names and provide placeholders
|
|
455
|
+
tool_context = self._extract_tool_directive_placeholders(agent.content)
|
|
456
|
+
else:
|
|
457
|
+
modified_content, tool_context = execute_tool_directives(agent.content, prefetch_context)
|
|
458
|
+
|
|
459
|
+
# reflects what the LLM would see; `--no-exec` opts out for side-effecty blocks.
|
|
460
|
+
if skip_exec_directives:
|
|
461
|
+
modified_content, exec_context = self._extract_exec_directive_placeholders(modified_content)
|
|
462
|
+
else:
|
|
463
|
+
exec_locals: Dict[str, Any] = {**context, **prefetch_context, **tool_context}
|
|
464
|
+
modified_content, exec_context = execute_exec_directives(
|
|
465
|
+
modified_content,
|
|
466
|
+
existing_context=exec_locals,
|
|
467
|
+
event_bus=event_bus,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
interactive_mode = is_interactive()
|
|
471
|
+
cwd, invoked_from, workspace_dir = self._resolve_paths(path_context)
|
|
472
|
+
full_context = self._build_template_context(
|
|
473
|
+
agent_config=agent_config,
|
|
474
|
+
prompt=prompt,
|
|
475
|
+
context=context,
|
|
476
|
+
directive_context={**prefetch_context, **tool_context, **exec_context},
|
|
477
|
+
attachment_bindings=attachment_bindings,
|
|
478
|
+
interactive_mode=interactive_mode,
|
|
479
|
+
cwd=cwd,
|
|
480
|
+
invoked_from=invoked_from,
|
|
481
|
+
workspace_dir=workspace_dir,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
renderer = AgentRenderer()
|
|
485
|
+
|
|
486
|
+
if agent_config.run_if:
|
|
487
|
+
skip_reason = None
|
|
488
|
+
try:
|
|
489
|
+
guard_result = renderer.render("{{ " + agent_config.run_if + " }}", full_context)
|
|
490
|
+
if guard_result.strip().lower() in ("", "false", "none", "0"):
|
|
491
|
+
skip_reason = f"run_if guard '{agent_config.run_if}' evaluated to false"
|
|
492
|
+
except Exception as e:
|
|
493
|
+
skip_reason = f"run_if guard error: {e}"
|
|
494
|
+
if skip_reason:
|
|
495
|
+
return PreparedAgent(
|
|
496
|
+
agent=agent,
|
|
497
|
+
agent_config=agent_config,
|
|
498
|
+
system_message="",
|
|
499
|
+
user_message="",
|
|
500
|
+
rendered_prompt="",
|
|
501
|
+
tools=[],
|
|
502
|
+
context=full_context,
|
|
503
|
+
combined_instructions="",
|
|
504
|
+
attachments=all_attachments,
|
|
505
|
+
skipped=True,
|
|
506
|
+
skip_reason=skip_reason,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
rendered_prompt = renderer.render(modified_content, full_context)
|
|
511
|
+
except Exception as e:
|
|
512
|
+
raise RuntimeError(f"Template rendering failed: {e}") from e
|
|
513
|
+
|
|
514
|
+
base_instructions = get_default_instructions()
|
|
515
|
+
agent_instructions = getattr(agent_config, "instructions", "")
|
|
516
|
+
|
|
517
|
+
# Render agent instructions as Jinja2 template
|
|
518
|
+
if agent_instructions:
|
|
519
|
+
try:
|
|
520
|
+
agent_instructions = renderer.render(agent_instructions, full_context)
|
|
521
|
+
except Exception as e:
|
|
522
|
+
raise RuntimeError(f"Failed to render agent instructions: {e}") from e
|
|
523
|
+
|
|
524
|
+
combined_instructions = _combine_instructions(base_instructions, agent_instructions)
|
|
525
|
+
|
|
526
|
+
tools = self._expand_tools(agent_config, full_context, interactive_mode)
|
|
527
|
+
|
|
528
|
+
skill_load = self._load_skills(agent_config, prompt, full_context, _skill_manager, event_bus)
|
|
529
|
+
skills, expiring_skills = skill_load.skills, skill_load.expiring
|
|
530
|
+
# The daemon reads these back off the context to update sticky state.
|
|
531
|
+
full_context["_expired_sticky_skills"] = skill_load.expired
|
|
532
|
+
full_context["_triggered_skill_names"] = skill_load.triggered
|
|
533
|
+
full_context["_auto_loaded_skill_names"] = skill_load.auto_loaded
|
|
534
|
+
|
|
535
|
+
system_message = build_system_prompt(tools, combined_instructions)
|
|
536
|
+
|
|
537
|
+
# Add environment context when invoked_from differs from CWD
|
|
538
|
+
if invoked_from and invoked_from != cwd:
|
|
539
|
+
env_block = f"""
|
|
540
|
+
## Environment
|
|
541
|
+
|
|
542
|
+
Working directory: {cwd}
|
|
543
|
+
Invoked from: {invoked_from}
|
|
544
|
+
|
|
545
|
+
When the user refers to "this folder", "current directory", or "here",
|
|
546
|
+
they typically mean the invoked location ({invoked_from}).
|
|
547
|
+
"""
|
|
548
|
+
system_message = system_message + env_block
|
|
549
|
+
|
|
550
|
+
# User message is the rendered prompt
|
|
551
|
+
user_message = rendered_prompt
|
|
552
|
+
|
|
553
|
+
return PreparedAgent(
|
|
554
|
+
agent=agent,
|
|
555
|
+
agent_config=agent_config,
|
|
556
|
+
system_message=system_message,
|
|
557
|
+
user_message=user_message,
|
|
558
|
+
rendered_prompt=rendered_prompt,
|
|
559
|
+
original_prompt=prompt,
|
|
560
|
+
tools=tools,
|
|
561
|
+
context=full_context,
|
|
562
|
+
combined_instructions=combined_instructions,
|
|
563
|
+
attachments=all_attachments,
|
|
564
|
+
skills=skills,
|
|
565
|
+
expiring_skills=expiring_skills,
|
|
566
|
+
)
|
|
@@ -15,6 +15,10 @@ from tsugite.agent_runner.helpers import ( # noqa: F401
|
|
|
15
15
|
get_display_console,
|
|
16
16
|
get_sandbox_context,
|
|
17
17
|
get_ui_handler,
|
|
18
|
+
reset_allowed_agents,
|
|
19
|
+
reset_allowed_secrets,
|
|
20
|
+
reset_current_agent,
|
|
21
|
+
reset_sandbox_context,
|
|
18
22
|
sandbox_context_to_override,
|
|
19
23
|
set_allowed_agents,
|
|
20
24
|
set_allowed_secrets,
|
|
@@ -33,16 +37,12 @@ from tsugite.agent_runner.runner import ( # noqa: F401
|
|
|
33
37
|
preview_multistep_agent,
|
|
34
38
|
run_agent,
|
|
35
39
|
run_agent_async,
|
|
36
|
-
run_multistep_agent,
|
|
37
|
-
run_multistep_agent_async,
|
|
38
40
|
)
|
|
39
41
|
from tsugite.agent_runner.validation import get_agent_info, validate_agent_file # noqa: F401
|
|
40
42
|
|
|
41
43
|
__all__ = [
|
|
42
44
|
"run_agent",
|
|
43
45
|
"run_agent_async",
|
|
44
|
-
"run_multistep_agent",
|
|
45
|
-
"run_multistep_agent_async",
|
|
46
46
|
"preview_multistep_agent",
|
|
47
47
|
"execute_prefetch",
|
|
48
48
|
"execute_tool_directives",
|
|
@@ -72,5 +72,9 @@ __all__ = [
|
|
|
72
72
|
"get_sandbox_context",
|
|
73
73
|
"set_sandbox_context",
|
|
74
74
|
"clear_sandbox_context",
|
|
75
|
+
"reset_allowed_agents",
|
|
76
|
+
"reset_allowed_secrets",
|
|
77
|
+
"reset_current_agent",
|
|
78
|
+
"reset_sandbox_context",
|
|
75
79
|
"sandbox_context_to_override",
|
|
76
80
|
]
|