tsugite-cli 0.19.1__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.1 → tsugite_cli-0.19.2}/AGENTS.md +4 -4
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/PKG-INFO +1 -1
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/pyproject.toml +1 -1
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/runner.py +11 -8
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/file.py +39 -1
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/daemon.py +46 -40
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/config.py +6 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/agent.py +43 -60
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/executor.py +13 -56
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/proxy.py +34 -7
- {tsugite_cli-0.19.1 → 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.1 → tsugite_cli-0.19.2}/tsugite/events/events.py +5 -1
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/hooks.py +7 -1
- tsugite_cli-0.19.2/tsugite/secrets/redaction.py +86 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/__init__.py +18 -1
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/http.py +9 -2
- tsugite_cli-0.19.1/tsugite/core/tools.py +0 -270
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/.gitignore +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/LICENSE +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/README.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/history_integration.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/steps.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/approval.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/agent_config.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cache.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/console.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/constants.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/context.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/models.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/options.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/anthropic.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/openai_compat.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/py.typed +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/jobs.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/scratchpad.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/utils.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.19.1 → tsugite_cli-0.19.2}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.19.1 → 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
|
|
|
@@ -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
|
|
|
@@ -180,7 +180,7 @@ def get_default_instructions() -> str:
|
|
|
180
180
|
return base + output + rules
|
|
181
181
|
|
|
182
182
|
|
|
183
|
-
def _render_args(value: Any, renderer: AgentRenderer) -> Any:
|
|
183
|
+
def _render_args(value: Any, renderer: AgentRenderer, context: Dict[str, Any]) -> Any:
|
|
184
184
|
"""Recursively render Jinja in string leaves of a tool args structure.
|
|
185
185
|
|
|
186
186
|
Non-string values pass through unchanged. Strings without Jinja markers
|
|
@@ -189,13 +189,13 @@ def _render_args(value: Any, renderer: AgentRenderer) -> Any:
|
|
|
189
189
|
if isinstance(value, str):
|
|
190
190
|
if "{{" not in value and "{%" not in value:
|
|
191
191
|
return value
|
|
192
|
-
return renderer.render_string(value,
|
|
192
|
+
return renderer.render_string(value, context)
|
|
193
193
|
if isinstance(value, dict):
|
|
194
|
-
return {k: _render_args(v, renderer) for k, v in value.items()}
|
|
194
|
+
return {k: _render_args(v, renderer, context) for k, v in value.items()}
|
|
195
195
|
if isinstance(value, list):
|
|
196
|
-
return [_render_args(v, renderer) for v in value]
|
|
196
|
+
return [_render_args(v, renderer, context) for v in value]
|
|
197
197
|
if isinstance(value, tuple):
|
|
198
|
-
return tuple(_render_args(v, renderer) for v in value)
|
|
198
|
+
return tuple(_render_args(v, renderer, context) for v in value)
|
|
199
199
|
return value
|
|
200
200
|
|
|
201
201
|
|
|
@@ -219,7 +219,7 @@ def execute_prefetch(prefetch_config: List[Dict[str, Any]], event_bus: Optional[
|
|
|
219
219
|
continue
|
|
220
220
|
|
|
221
221
|
try:
|
|
222
|
-
rendered_args = _render_args(args, renderer)
|
|
222
|
+
rendered_args = _render_args(args, renderer, context)
|
|
223
223
|
except Exception as e:
|
|
224
224
|
raise RuntimeError(f"Prefetch render failed for tool '{tool_name}': {e}") from e
|
|
225
225
|
|
|
@@ -245,7 +245,7 @@ def execute_tool_directives(
|
|
|
245
245
|
|
|
246
246
|
Args:
|
|
247
247
|
content: Markdown content with tool directives
|
|
248
|
-
existing_context: Current template context
|
|
248
|
+
existing_context: Current template context; directive args render against it
|
|
249
249
|
event_bus: Optional event bus for emitting warnings
|
|
250
250
|
|
|
251
251
|
Returns:
|
|
@@ -289,8 +289,11 @@ def execute_tool_directives(
|
|
|
289
289
|
renderer = AgentRenderer()
|
|
290
290
|
|
|
291
291
|
for directive in directives:
|
|
292
|
+
# Directives run in order, so each one renders against the caller's
|
|
293
|
+
# context (step assignments, prefetch results) plus whatever earlier
|
|
294
|
+
# directives in this same content have assigned.
|
|
292
295
|
try:
|
|
293
|
-
rendered_args = _render_args(directive.args, renderer)
|
|
296
|
+
rendered_args = _render_args(directive.args, renderer, {**existing_context, **new_context})
|
|
294
297
|
except Exception as e:
|
|
295
298
|
raise RuntimeError(f"Tool directive render failed for '{directive.name}': {e}") from e
|
|
296
299
|
|
|
@@ -47,6 +47,39 @@ class FileHandler(AttachmentHandler):
|
|
|
47
47
|
),
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
# `mimetypes` maps a number of plain-text formats into `application/*`
|
|
51
|
+
# (`.sh` -> `application/x-sh`, `.json` -> `application/json`), and which
|
|
52
|
+
# ones depends on the platform's mime.types. Reading those as documents
|
|
53
|
+
# base64-encodes source code the model was meant to read, so match them
|
|
54
|
+
# back to text. `+zip` and friends are deliberately absent from the
|
|
55
|
+
# structured suffixes: `application/epub+zip` is not text.
|
|
56
|
+
TEXT_MIME_TYPES = frozenset(
|
|
57
|
+
{
|
|
58
|
+
"application/ecmascript",
|
|
59
|
+
"application/javascript",
|
|
60
|
+
"application/json",
|
|
61
|
+
"application/sql",
|
|
62
|
+
"application/toml",
|
|
63
|
+
"application/x-csh",
|
|
64
|
+
"application/x-javascript",
|
|
65
|
+
"application/x-latex",
|
|
66
|
+
"application/x-perl",
|
|
67
|
+
"application/x-php",
|
|
68
|
+
"application/x-powershell",
|
|
69
|
+
"application/x-python",
|
|
70
|
+
"application/x-python-code",
|
|
71
|
+
"application/x-ruby",
|
|
72
|
+
"application/x-sh",
|
|
73
|
+
"application/x-shellscript",
|
|
74
|
+
"application/x-tcl",
|
|
75
|
+
"application/x-yaml",
|
|
76
|
+
"application/xml",
|
|
77
|
+
"application/yaml",
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
TEXT_MIME_SUFFIXES = ("+json", "+xml", "+yaml")
|
|
82
|
+
|
|
50
83
|
def can_handle(self, source: str) -> bool:
|
|
51
84
|
"""Check if source is a file path.
|
|
52
85
|
|
|
@@ -92,12 +125,17 @@ class FileHandler(AttachmentHandler):
|
|
|
92
125
|
return (mime_type, AttachmentContentType.IMAGE)
|
|
93
126
|
elif mime_type.startswith("audio/"):
|
|
94
127
|
return (mime_type, AttachmentContentType.AUDIO)
|
|
95
|
-
elif mime_type.startswith("application/"):
|
|
128
|
+
elif mime_type.startswith("application/") and not self._is_text_mime(mime_type):
|
|
96
129
|
return (mime_type, AttachmentContentType.DOCUMENT)
|
|
97
130
|
|
|
98
131
|
# Default to text
|
|
99
132
|
return ("text/plain", AttachmentContentType.TEXT)
|
|
100
133
|
|
|
134
|
+
@classmethod
|
|
135
|
+
def _is_text_mime(cls, mime_type: str) -> bool:
|
|
136
|
+
"""Whether an `application/*` MIME type actually carries text."""
|
|
137
|
+
return mime_type in cls.TEXT_MIME_TYPES or mime_type.endswith(cls.TEXT_MIME_SUFFIXES)
|
|
138
|
+
|
|
101
139
|
def fetch(self, source: str) -> Attachment:
|
|
102
140
|
"""Read file content.
|
|
103
141
|
|
|
@@ -8,11 +8,17 @@ import typer
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.panel import Panel
|
|
10
10
|
|
|
11
|
-
from tsugite.config import get_xdg_data_path, get_xdg_write_path
|
|
11
|
+
from tsugite.config import DEFAULT_DAEMON_HOST, DEFAULT_DAEMON_PORT, get_xdg_data_path, get_xdg_write_path
|
|
12
12
|
|
|
13
13
|
daemon_app = typer.Typer(help="Daemon management commands")
|
|
14
14
|
console = Console()
|
|
15
15
|
|
|
16
|
+
# Connection flags shared by every daemon client command. Typer resolves
|
|
17
|
+
# flag > env var > default natively, so there is nothing to hand-roll here.
|
|
18
|
+
HOST_OPTION = typer.Option(DEFAULT_DAEMON_HOST, "--host", envvar="TSUGITE_DAEMON_HOST", help="Daemon HTTP host")
|
|
19
|
+
PORT_OPTION = typer.Option(DEFAULT_DAEMON_PORT, "--port", "-p", envvar="TSUGITE_DAEMON_PORT", help="Daemon HTTP port")
|
|
20
|
+
TOKEN_OPTION = typer.Option(None, "--token", "-t", envvar="TSUGITE_DAEMON_TOKEN", help="Auth token")
|
|
21
|
+
|
|
16
22
|
|
|
17
23
|
def _get_questionary_style():
|
|
18
24
|
"""Lazy load questionary style."""
|
|
@@ -394,9 +400,9 @@ def _daemon_request(method: str, host: str, port: int, path: str, token: Optiona
|
|
|
394
400
|
@daemon_app.command("sessions")
|
|
395
401
|
def list_sessions(
|
|
396
402
|
agent: str = typer.Argument(help="Agent name"),
|
|
397
|
-
host: str =
|
|
398
|
-
port: int =
|
|
399
|
-
token: Optional[str] =
|
|
403
|
+
host: str = HOST_OPTION,
|
|
404
|
+
port: int = PORT_OPTION,
|
|
405
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
400
406
|
):
|
|
401
407
|
"""List active sessions for a daemon agent."""
|
|
402
408
|
data = _daemon_request("GET", host, port, f"/api/agents/{agent}/sessions", token)
|
|
@@ -421,9 +427,9 @@ def list_sessions(
|
|
|
421
427
|
def compact_session(
|
|
422
428
|
agent: str = typer.Argument(help="Agent name"),
|
|
423
429
|
user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID to compact"),
|
|
424
|
-
host: str =
|
|
425
|
-
port: int =
|
|
426
|
-
token: Optional[str] =
|
|
430
|
+
host: str = HOST_OPTION,
|
|
431
|
+
port: int = PORT_OPTION,
|
|
432
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
427
433
|
):
|
|
428
434
|
"""Force compact a daemon session."""
|
|
429
435
|
data = _daemon_request(
|
|
@@ -451,9 +457,9 @@ daemon_app.add_typer(session_app, name="session")
|
|
|
451
457
|
@session_app.command("set-primary")
|
|
452
458
|
def session_set_primary(
|
|
453
459
|
session_id: str = typer.Argument(help="Session ID to mark as primary"),
|
|
454
|
-
host: str =
|
|
455
|
-
port: int =
|
|
456
|
-
token: Optional[str] =
|
|
460
|
+
host: str = HOST_OPTION,
|
|
461
|
+
port: int = PORT_OPTION,
|
|
462
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
457
463
|
):
|
|
458
464
|
"""Mark a session as the user's primary for its agent."""
|
|
459
465
|
_daemon_request("POST", host, port, f"/api/sessions/{session_id}/set-primary", token, json={})
|
|
@@ -464,9 +470,9 @@ def session_set_primary(
|
|
|
464
470
|
def session_clear_primary(
|
|
465
471
|
agent: str = typer.Option(..., "--agent", "-a", help="Agent name"),
|
|
466
472
|
user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID"),
|
|
467
|
-
host: str =
|
|
468
|
-
port: int =
|
|
469
|
-
token: Optional[str] =
|
|
473
|
+
host: str = HOST_OPTION,
|
|
474
|
+
port: int = PORT_OPTION,
|
|
475
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
470
476
|
):
|
|
471
477
|
"""Remove the primary flag from any session for (user, agent)."""
|
|
472
478
|
from urllib.parse import urlencode
|
|
@@ -478,9 +484,9 @@ def session_clear_primary(
|
|
|
478
484
|
|
|
479
485
|
@session_app.command("list")
|
|
480
486
|
def session_list(
|
|
481
|
-
host: str =
|
|
482
|
-
port: int =
|
|
483
|
-
token: Optional[str] =
|
|
487
|
+
host: str = HOST_OPTION,
|
|
488
|
+
port: int = PORT_OPTION,
|
|
489
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
484
490
|
):
|
|
485
491
|
"""List all daemon sessions, indicating which one is primary."""
|
|
486
492
|
from rich.table import Table
|
|
@@ -525,9 +531,9 @@ def _parse_every_to_cron(every: str) -> str:
|
|
|
525
531
|
|
|
526
532
|
@schedule_app.command("list")
|
|
527
533
|
def schedule_list(
|
|
528
|
-
host: str =
|
|
529
|
-
port: int =
|
|
530
|
-
token: Optional[str] =
|
|
534
|
+
host: str = HOST_OPTION,
|
|
535
|
+
port: int = PORT_OPTION,
|
|
536
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
531
537
|
):
|
|
532
538
|
"""List all schedules."""
|
|
533
539
|
from rich.table import Table
|
|
@@ -579,9 +585,9 @@ def schedule_add(
|
|
|
579
585
|
"--target-session",
|
|
580
586
|
help="Where the inject_history turn lands: primary, originating, none, name:<n>, or <session_id>",
|
|
581
587
|
),
|
|
582
|
-
host: str =
|
|
583
|
-
port: int =
|
|
584
|
-
token: Optional[str] =
|
|
588
|
+
host: str = HOST_OPTION,
|
|
589
|
+
port: int = PORT_OPTION,
|
|
590
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
585
591
|
):
|
|
586
592
|
"""Add a new schedule."""
|
|
587
593
|
spec_count = sum(1 for x in (cron, at, every) if x is not None)
|
|
@@ -617,9 +623,9 @@ def schedule_add(
|
|
|
617
623
|
|
|
618
624
|
@schedule_app.command("cleanup")
|
|
619
625
|
def schedule_cleanup(
|
|
620
|
-
host: str =
|
|
621
|
-
port: int =
|
|
622
|
-
token: Optional[str] =
|
|
626
|
+
host: str = HOST_OPTION,
|
|
627
|
+
port: int = PORT_OPTION,
|
|
628
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
623
629
|
):
|
|
624
630
|
"""Remove all orphaned one-off schedules (disabled, already fired)."""
|
|
625
631
|
data = _daemon_request("POST", host, port, "/api/schedules/cleanup", token)
|
|
@@ -635,9 +641,9 @@ def schedule_cleanup(
|
|
|
635
641
|
@schedule_app.command("remove")
|
|
636
642
|
def schedule_remove(
|
|
637
643
|
schedule_id: str = typer.Argument(help="Schedule ID to remove"),
|
|
638
|
-
host: str =
|
|
639
|
-
port: int =
|
|
640
|
-
token: Optional[str] =
|
|
644
|
+
host: str = HOST_OPTION,
|
|
645
|
+
port: int = PORT_OPTION,
|
|
646
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
641
647
|
):
|
|
642
648
|
"""Remove a schedule."""
|
|
643
649
|
_daemon_request("DELETE", host, port, f"/api/schedules/{schedule_id}", token)
|
|
@@ -647,9 +653,9 @@ def schedule_remove(
|
|
|
647
653
|
@schedule_app.command("enable")
|
|
648
654
|
def schedule_enable(
|
|
649
655
|
schedule_id: str = typer.Argument(help="Schedule ID"),
|
|
650
|
-
host: str =
|
|
651
|
-
port: int =
|
|
652
|
-
token: Optional[str] =
|
|
656
|
+
host: str = HOST_OPTION,
|
|
657
|
+
port: int = PORT_OPTION,
|
|
658
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
653
659
|
):
|
|
654
660
|
"""Enable a schedule."""
|
|
655
661
|
_daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/enable", token)
|
|
@@ -659,9 +665,9 @@ def schedule_enable(
|
|
|
659
665
|
@schedule_app.command("disable")
|
|
660
666
|
def schedule_disable(
|
|
661
667
|
schedule_id: str = typer.Argument(help="Schedule ID"),
|
|
662
|
-
host: str =
|
|
663
|
-
port: int =
|
|
664
|
-
token: Optional[str] =
|
|
668
|
+
host: str = HOST_OPTION,
|
|
669
|
+
port: int = PORT_OPTION,
|
|
670
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
665
671
|
):
|
|
666
672
|
"""Disable a schedule."""
|
|
667
673
|
_daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/disable", token)
|
|
@@ -671,9 +677,9 @@ def schedule_disable(
|
|
|
671
677
|
@schedule_app.command("run")
|
|
672
678
|
def schedule_run(
|
|
673
679
|
schedule_id: str = typer.Argument(help="Schedule ID to trigger now"),
|
|
674
|
-
host: str =
|
|
675
|
-
port: int =
|
|
676
|
-
token: Optional[str] =
|
|
680
|
+
host: str = HOST_OPTION,
|
|
681
|
+
port: int = PORT_OPTION,
|
|
682
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
677
683
|
):
|
|
678
684
|
"""Trigger a schedule immediately."""
|
|
679
685
|
_daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/run", token)
|
|
@@ -683,9 +689,9 @@ def schedule_run(
|
|
|
683
689
|
@schedule_app.command("show")
|
|
684
690
|
def schedule_show(
|
|
685
691
|
schedule_id: str = typer.Argument(help="Schedule ID"),
|
|
686
|
-
host: str =
|
|
687
|
-
port: int =
|
|
688
|
-
token: Optional[str] =
|
|
692
|
+
host: str = HOST_OPTION,
|
|
693
|
+
port: int = PORT_OPTION,
|
|
694
|
+
token: Optional[str] = TOKEN_OPTION,
|
|
689
695
|
):
|
|
690
696
|
"""Show schedule details."""
|
|
691
697
|
data = _daemon_request("GET", host, port, f"/api/schedules/{schedule_id}", token)
|
|
@@ -7,6 +7,12 @@ from typing import Callable, Dict, List, Optional
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
9
9
|
|
|
10
|
+
# The address the daemon serves on by default. Defined here rather than in the
|
|
11
|
+
# daemon plugin so the server's bind default and the client's connect default
|
|
12
|
+
# cannot drift apart; tsugite_daemon.config.HTTPConfig imports these.
|
|
13
|
+
DEFAULT_DAEMON_HOST = "127.0.0.1"
|
|
14
|
+
DEFAULT_DAEMON_PORT = 8374
|
|
15
|
+
|
|
10
16
|
# XDG Base Directory utilities
|
|
11
17
|
|
|
12
18
|
|
|
@@ -10,14 +10,9 @@ import time
|
|
|
10
10
|
from dataclasses import asdict, dataclass, field, is_dataclass
|
|
11
11
|
from typing import Any, Callable, Dict, List, Optional
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
RETURN_VALUE_REPR_MAX = 2048
|
|
17
|
-
|
|
18
|
-
from tsugite.attachments.base import Attachment, AttachmentContentType, format_attachment_open_tag # noqa: E402
|
|
19
|
-
from tsugite.cancellation import is_cancelled # noqa: E402
|
|
20
|
-
from tsugite.events import ( # noqa: E402
|
|
13
|
+
from tsugite.attachments.base import Attachment, AttachmentContentType, format_attachment_open_tag
|
|
14
|
+
from tsugite.cancellation import is_cancelled
|
|
15
|
+
from tsugite.events import (
|
|
21
16
|
CodeExecutionEvent,
|
|
22
17
|
ContentBlockEvent,
|
|
23
18
|
CostSummaryEvent,
|
|
@@ -36,17 +31,21 @@ from tsugite.events import ( # noqa: E402
|
|
|
36
31
|
TaskStartEvent,
|
|
37
32
|
WarningEvent,
|
|
38
33
|
)
|
|
39
|
-
from tsugite.providers.base import CompletionResponse as ProviderResponse
|
|
40
|
-
from tsugite.skill_discovery import Skill
|
|
34
|
+
from tsugite.providers.base import CompletionResponse as ProviderResponse
|
|
35
|
+
from tsugite.skill_discovery import Skill
|
|
41
36
|
|
|
42
|
-
from .content_blocks import extract_content_blocks
|
|
43
|
-
from .executor import Executor
|
|
44
|
-
from .executor_registry import get_executor_class
|
|
45
|
-
from .memory import AgentMemory, StepResult
|
|
46
|
-
from .tools import Tool
|
|
37
|
+
from .content_blocks import extract_content_blocks
|
|
38
|
+
from .executor import Executor
|
|
39
|
+
from .executor_registry import get_executor_class
|
|
40
|
+
from .memory import AgentMemory, StepResult
|
|
41
|
+
from .tools import Tool
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# Cap the persisted return_value repr so a huge structured return doesn't bloat the event.
|
|
46
|
+
RETURN_VALUE_REPR_MAX = 2048
|
|
47
|
+
|
|
48
|
+
DEFAULT_MAX_TURNS = 10 # Maximum reasoning iterations before the run is cut off
|
|
50
49
|
|
|
51
50
|
_LLM_WAIT_HEARTBEAT_INTERVAL = 10.0
|
|
52
51
|
|
|
@@ -108,7 +107,7 @@ def build_system_prompt(tools: List[Tool], instructions: str = "") -> str:
|
|
|
108
107
|
_EXEC_FENCE = "```python-exec"
|
|
109
108
|
_CLOSE_FENCE = "\n```"
|
|
110
109
|
|
|
111
|
-
# Start-of-line ```python fence whose info string is exactly "python" (bare)
|
|
110
|
+
# Start-of-line ```python fence whose info string is exactly "python" (bare) - i.e.
|
|
112
111
|
# NOT ```python-exec. Used to nudge the model toward the exec fence when it emits a
|
|
113
112
|
# bare block. The negative lookahead keeps ```python-exec from matching.
|
|
114
113
|
_BARE_PYTHON_FENCE = re.compile(r"(?:^|\n)```python(?!-exec)[ \t]*\r?\n")
|
|
@@ -450,7 +449,7 @@ class TsugiteAgent:
|
|
|
450
449
|
await self.executor.inject_content_blocks(turn.content_blocks)
|
|
451
450
|
|
|
452
451
|
# Multiple python blocks: the parser already took just the first
|
|
453
|
-
# one as `code`. Surface the drop on two channels
|
|
452
|
+
# one as `code`. Surface the drop on two channels - a UI warning
|
|
454
453
|
# for the human, and an in-conversation observation for the
|
|
455
454
|
# model. Without the latter the model sees its full N-block
|
|
456
455
|
# response in raw_content but only one execution result, and
|
|
@@ -621,7 +620,7 @@ class TsugiteAgent:
|
|
|
621
620
|
from tsugite.agent_runner.history_integration import record_final_result, record_session_end
|
|
622
621
|
|
|
623
622
|
# Durable answer record for run paths with no live SSE persist
|
|
624
|
-
# (scheduled, subprocess, CLI)
|
|
623
|
+
# (scheduled, subprocess, CLI) - the conversation view renders
|
|
625
624
|
# the answer bubble from this event.
|
|
626
625
|
record_final_result(
|
|
627
626
|
self.storage,
|
|
@@ -1187,46 +1186,30 @@ class TsugiteAgent:
|
|
|
1187
1186
|
def _compute_token_breakdown(self, messages: List[Dict]) -> Dict:
|
|
1188
1187
|
"""Compute per-category token breakdown with individual item details."""
|
|
1189
1188
|
est = estimate_content_tokens
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
#
|
|
1214
|
-
skill_items = []
|
|
1215
|
-
for skill in self.skills:
|
|
1216
|
-
tok = est(skill.content) if skill.content else 0
|
|
1217
|
-
skill_items.append({"name": skill.name, "tokens": tok})
|
|
1218
|
-
skills_total = sum(s["tokens"] for s in skill_items)
|
|
1219
|
-
categories.append({"name": "skills", "tokens": skills_total, "items": skill_items})
|
|
1220
|
-
|
|
1221
|
-
# Hooks (pre_message hook captured output like rag_context)
|
|
1222
|
-
hook_items = []
|
|
1223
|
-
for name, content in self.hook_vars.items():
|
|
1224
|
-
tok = est(content) if content else 0
|
|
1225
|
-
hook_items.append({"name": name, "tokens": tok})
|
|
1226
|
-
hooks_total = sum(h["tokens"] for h in hook_items)
|
|
1227
|
-
categories.append({"name": "hooks", "tokens": hooks_total, "items": hook_items})
|
|
1228
|
-
|
|
1229
|
-
# History — walk messages between context turn and task
|
|
1189
|
+
|
|
1190
|
+
def itemized(name: str, items: list) -> Dict:
|
|
1191
|
+
return {"name": name, "tokens": sum(i["tokens"] for i in items), "items": items}
|
|
1192
|
+
|
|
1193
|
+
# Tools sort largest-first; the other item lists keep their natural order.
|
|
1194
|
+
tool_items = sorted(
|
|
1195
|
+
({"name": t.name, "tokens": est(t.to_code_prompt())} for t in self.tools),
|
|
1196
|
+
key=lambda i: i["tokens"],
|
|
1197
|
+
reverse=True,
|
|
1198
|
+
)
|
|
1199
|
+
categories = [
|
|
1200
|
+
# The system prompt minus tools; no per-item breakdown to give.
|
|
1201
|
+
{"name": "instructions", "tokens": est(self.instructions) if self.instructions else 0, "items": []},
|
|
1202
|
+
itemized("tools", tool_items),
|
|
1203
|
+
itemized(
|
|
1204
|
+
"attachments",
|
|
1205
|
+
[{"name": a.name, "tokens": est(a.content) if a.content else 0} for a in self.attachments],
|
|
1206
|
+
),
|
|
1207
|
+
itemized("skills", [{"name": s.name, "tokens": est(s.content) if s.content else 0} for s in self.skills]),
|
|
1208
|
+
# Hooks carry pre_message captured output, e.g. rag_context.
|
|
1209
|
+
itemized("hooks", [{"name": n, "tokens": est(c) if c else 0} for n, c in self.hook_vars.items()]),
|
|
1210
|
+
]
|
|
1211
|
+
|
|
1212
|
+
# History - walk messages between context turn and task
|
|
1230
1213
|
history_tokens = 0
|
|
1231
1214
|
i = 0
|
|
1232
1215
|
n = len(messages)
|
|
@@ -22,6 +22,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Protocol, runt
|
|
|
22
22
|
|
|
23
23
|
from tsugite.core.state import load_state, save_state
|
|
24
24
|
from tsugite.exceptions import StateSerializationError
|
|
25
|
+
from tsugite.secrets.redaction import redact_sensitive_obj
|
|
25
26
|
|
|
26
27
|
PPRINT_WIDTH = 100
|
|
27
28
|
|
|
@@ -114,15 +115,7 @@ class ExecutionResult:
|
|
|
114
115
|
last_statement_type: Optional[str] = None # "expr" | "statement" | None (unparsed) - history replay metadata
|
|
115
116
|
|
|
116
117
|
def to_xml(self, duration_ms: int = 0, max_output_kb: int = MAX_EXECUTION_OUTPUT_KB) -> str:
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
duration_ms: Execution duration in milliseconds
|
|
121
|
-
max_output_kb: Maximum output size in KB before truncation
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
XML string with execution result
|
|
125
|
-
"""
|
|
118
|
+
"""Render the result as the XML observation replayed to the model."""
|
|
126
119
|
from xml.sax.saxutils import escape
|
|
127
120
|
|
|
128
121
|
from tsugite.secrets.registry import get_registry
|
|
@@ -148,7 +141,6 @@ class ExecutionResult:
|
|
|
148
141
|
|
|
149
142
|
if self.error:
|
|
150
143
|
parts.append(f"<error>{escape(_mask(self.error))}</error>")
|
|
151
|
-
# Include traceback from stderr (last 10 lines)
|
|
152
144
|
if self.stderr:
|
|
153
145
|
tb_lines = _mask(self.stderr).strip().split("\n")[-10:]
|
|
154
146
|
parts.append(f"<traceback>{escape(chr(10).join(tb_lines))}</traceback>")
|
|
@@ -217,14 +209,7 @@ def _summarize_mapping(items) -> Dict[str, str]:
|
|
|
217
209
|
|
|
218
210
|
|
|
219
211
|
def _summarize_variable(value: Any) -> str:
|
|
220
|
-
"""Summarize a variable's type and size for display.
|
|
221
|
-
|
|
222
|
-
Args:
|
|
223
|
-
value: The variable value to summarize
|
|
224
|
-
|
|
225
|
-
Returns:
|
|
226
|
-
Summary string like "dict(3 keys)" or "list(5 items)"
|
|
227
|
-
"""
|
|
212
|
+
"""Summarize a variable's type and size for display, e.g. "dict(3 keys)"."""
|
|
228
213
|
t = type(value).__name__
|
|
229
214
|
if isinstance(value, dict):
|
|
230
215
|
return f"{t}({len(value)} keys)"
|
|
@@ -421,23 +406,13 @@ class LocalExecutor:
|
|
|
421
406
|
return ns
|
|
422
407
|
|
|
423
408
|
def _split_code_for_last_expr(self, code: str) -> tuple[str, Optional[str]]:
|
|
424
|
-
"""Split code into setup
|
|
425
|
-
|
|
426
|
-
If the last statement is an expression, return (setup, last_expr).
|
|
427
|
-
Otherwise, return (code, None).
|
|
428
|
-
|
|
429
|
-
Args:
|
|
430
|
-
code: Python code to analyze
|
|
431
|
-
|
|
432
|
-
Returns:
|
|
433
|
-
Tuple of (setup_code, last_expression_or_none)
|
|
434
|
-
"""
|
|
409
|
+
"""Split code into (setup, last_expression) when the last statement is an
|
|
410
|
+
expression, so it can be eval'd for REPL-style display. Otherwise (code, None)."""
|
|
435
411
|
try:
|
|
436
412
|
tree = ast.parse(code)
|
|
437
413
|
if not tree.body:
|
|
438
414
|
return (code, None)
|
|
439
415
|
|
|
440
|
-
# Check if last statement is an expression
|
|
441
416
|
last_node = tree.body[-1]
|
|
442
417
|
if not isinstance(last_node, ast.Expr):
|
|
443
418
|
return (code, None)
|
|
@@ -473,16 +448,7 @@ class LocalExecutor:
|
|
|
473
448
|
return "expr" if isinstance(tree.body[-1], ast.Expr) else "statement"
|
|
474
449
|
|
|
475
450
|
def _format_value(self, value: Any) -> str:
|
|
476
|
-
"""Format a value for display.
|
|
477
|
-
|
|
478
|
-
Uses pprint for complex objects, repr for simple ones.
|
|
479
|
-
|
|
480
|
-
Args:
|
|
481
|
-
value: Value to format
|
|
482
|
-
|
|
483
|
-
Returns:
|
|
484
|
-
Formatted string representation
|
|
485
|
-
"""
|
|
451
|
+
"""Format a value for display: pprint for containers, repr for everything else."""
|
|
486
452
|
if isinstance(value, (dict, list, tuple, set)):
|
|
487
453
|
return pprint.pformat(value, width=PPRINT_WIDTH, compact=False)
|
|
488
454
|
return repr(value)
|
|
@@ -505,12 +471,6 @@ class LocalExecutor:
|
|
|
505
471
|
Automatically displays the value of the last expression (REPL-like behavior).
|
|
506
472
|
When a workspace is bound, chdir to it under a process-wide lock so raw
|
|
507
473
|
Python file APIs (os.getcwd, open, Path.cwd) resolve against the workspace.
|
|
508
|
-
|
|
509
|
-
Args:
|
|
510
|
-
code: Python code to execute
|
|
511
|
-
|
|
512
|
-
Returns:
|
|
513
|
-
ExecutionResult with output, error, stdout, stderr, return_value, tools_called, and variables_set
|
|
514
474
|
"""
|
|
515
475
|
self._return_value = None
|
|
516
476
|
self._tools_called = []
|
|
@@ -666,10 +626,14 @@ class LocalExecutor:
|
|
|
666
626
|
def tool_wrapper(*args, **kwargs):
|
|
667
627
|
self._tools_called.append(tool_obj.name)
|
|
668
628
|
convert_positional_to_kwargs(tool_obj, args, kwargs)
|
|
669
|
-
|
|
629
|
+
# Redact BEFORE _jsonable_call_args: it flattens nested values into
|
|
630
|
+
# capped repr() strings, and a token baked into one of those is no
|
|
631
|
+
# longer reachable by key or path.
|
|
632
|
+
audit_args = redact_sensitive_obj(kwargs, getattr(tool_obj, "sensitive_paths", ()))
|
|
633
|
+
record: Dict[str, Any] = {"tool": tool_obj.name, "arguments": _jsonable_call_args(audit_args)}
|
|
670
634
|
self._tool_calls.append(record)
|
|
671
635
|
if self.event_bus:
|
|
672
|
-
self.event_bus.emit(ToolCallEvent(tool_name=tool_obj.name, arguments=
|
|
636
|
+
self.event_bus.emit(ToolCallEvent(tool_name=tool_obj.name, arguments=audit_args))
|
|
673
637
|
t0 = time.perf_counter()
|
|
674
638
|
try:
|
|
675
639
|
result = run_async_in_sync_context(tool_obj.execute(**kwargs))
|
|
@@ -718,14 +682,7 @@ class LocalExecutor:
|
|
|
718
682
|
self.namespace.update(tools)
|
|
719
683
|
|
|
720
684
|
def register_loaded_skill(self, name: str, content: str):
|
|
721
|
-
"""Register a skill loaded during this
|
|
722
|
-
|
|
723
|
-
Called by load_skill() tool to track skills for observation embedding.
|
|
724
|
-
|
|
725
|
-
Args:
|
|
726
|
-
name: Skill name
|
|
727
|
-
content: Rendered skill content
|
|
728
|
-
"""
|
|
685
|
+
"""Register a skill loaded during this turn. Called by the load_skill() tool."""
|
|
729
686
|
self._loaded_skills_for_turn[name] = content
|
|
730
687
|
|
|
731
688
|
def register_unloaded_skill(self, name: str):
|