tsugite-cli 0.23.0__tar.gz → 0.23.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.23.0 → tsugite_cli-0.23.2}/PKG-INFO +1 -1
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/pyproject.toml +1 -1
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_preparation.py +4 -2
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/history_integration.py +4 -7
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/runner.py +2 -1
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/approval.py +17 -10
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/agents.py +2 -1
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/run.py +2 -1
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/executor.py +9 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/record_store.py +10 -2
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/reconstruction.py +22 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/permissions.py +23 -11
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/anthropic.py +1 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/model_registry.py +8 -4
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/openai_compat.py +2 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/__init__.py +15 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/http.py +20 -3
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/jsonl.py +14 -1
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/usage/quota.py +8 -10
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/utils.py +1 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/.gitignore +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/AGENTS.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/LICENSE +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/README.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/steps.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/agent_config.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cache.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/config.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/console.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/constants.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/context.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/agent.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/subprocess_executor.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/models.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/options.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/py.typed +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/artifacts.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/daemon_control.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/jobs.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.23.0 → tsugite_cli-0.23.2}/tsugite/workspace/templates.py +0 -0
|
@@ -152,7 +152,8 @@ class AgentPreparer:
|
|
|
152
152
|
|
|
153
153
|
A front-matter attachment defines its cache tier and replaces a same-named
|
|
154
154
|
caller attachment. Legacy `-filename` string entries drop a same-named
|
|
155
|
-
entry. Front-matter paths render with `paths` and resolve against
|
|
155
|
+
entry. Front-matter paths render with `paths` and resolve against the
|
|
156
|
+
workspace, or the CWD when the run has no workspace.
|
|
156
157
|
"""
|
|
157
158
|
all_attachments = list(attachments or [])
|
|
158
159
|
|
|
@@ -160,7 +161,8 @@ class AgentPreparer:
|
|
|
160
161
|
if removals:
|
|
161
162
|
all_attachments = [a for a in all_attachments if a.name not in removals]
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
base = paths["WORKSPACE_DIR"] or paths["CWD"]
|
|
165
|
+
loaded, bindings = resolve_agent_config_attachments(keep_items, Path(base), paths)
|
|
164
166
|
all_attachments = loaded + all_attachments
|
|
165
167
|
|
|
166
168
|
seen_names: set[str] = set()
|
|
@@ -92,14 +92,11 @@ def _current_turn_already_has_user_input(storage: Session) -> bool:
|
|
|
92
92
|
session_end after it; the post-hoc save_run_to_history on the error path
|
|
93
93
|
would otherwise record a second, identical one (a duplicate user bubble).
|
|
94
94
|
A genuinely repeated message opens a new turn *after* the prior session_end,
|
|
95
|
-
so this stays False for it.
|
|
95
|
+
so this stays False for it. A final_result also ends a turn: compaction copies
|
|
96
|
+
retained turns into the successor without their session_end.
|
|
96
97
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return False
|
|
100
|
-
if event.type == "user_input":
|
|
101
|
-
return True
|
|
102
|
-
return False
|
|
98
|
+
events = list(storage.iter_events(types=("user_input", "session_end", "final_result")))
|
|
99
|
+
return bool(events) and events[-1].type == "user_input"
|
|
103
100
|
|
|
104
101
|
|
|
105
102
|
def record_user_input(
|
|
@@ -1281,6 +1281,7 @@ def preview_multistep_agent(
|
|
|
1281
1281
|
|
|
1282
1282
|
# Extract steps
|
|
1283
1283
|
from tsugite.md_agents import extract_step_directives, has_step_directives
|
|
1284
|
+
from tsugite.tools import format_tool_specs
|
|
1284
1285
|
|
|
1285
1286
|
if not has_step_directives(agent.content):
|
|
1286
1287
|
output("[yellow]This is a single-step agent (no step directives).[/yellow]", is_warning=True)
|
|
@@ -1302,7 +1303,7 @@ def preview_multistep_agent(
|
|
|
1302
1303
|
output(f"Prompt: {prompt}")
|
|
1303
1304
|
output(f"Steps: {len(steps)}")
|
|
1304
1305
|
output(f"Model: {resolve_effective_model(agent_model=agent.config.model) or 'unknown'}")
|
|
1305
|
-
output(f"Tools: {
|
|
1306
|
+
output(f"Tools: {format_tool_specs(agent.config.tools) if agent.config.tools else 'None'}")
|
|
1306
1307
|
output("")
|
|
1307
1308
|
|
|
1308
1309
|
# Show steps in table format
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Backend-driven approval primitive.
|
|
2
2
|
|
|
3
3
|
A human-in-the-loop gate that pauses a turn before an action and asks the user
|
|
4
|
-
to approve or deny it. It rides the same InteractionBackend
|
|
5
|
-
so an approval surfaces on whatever adapter the user is on
|
|
6
|
-
frame, etc.).
|
|
4
|
+
to approve or deny it, once or persistently. It rides the same InteractionBackend
|
|
5
|
+
used by ``ask_user``, so an approval surfaces on whatever adapter the user is on
|
|
6
|
+
(CLI prompt, web SSE frame, etc.).
|
|
7
7
|
|
|
8
8
|
Fail-closed by design: when no interactive surface is available (no backend, or a
|
|
9
9
|
non-interactive scheduler/headless backend), or when the reply is unrecognized,
|
|
@@ -15,34 +15,39 @@ from typing import Literal, Optional
|
|
|
15
15
|
|
|
16
16
|
from tsugite.interaction import NonInteractiveBackend, get_interaction_backend
|
|
17
17
|
|
|
18
|
-
ApprovalDecision = Literal["approve", "deny", "always"]
|
|
18
|
+
ApprovalDecision = Literal["approve", "deny", "always", "never"]
|
|
19
19
|
|
|
20
20
|
APPROVE_LABEL = "Approve"
|
|
21
21
|
DENY_LABEL = "Deny"
|
|
22
22
|
ALWAYS_LABEL = "Always allow"
|
|
23
|
+
NEVER_LABEL = "Always deny"
|
|
23
24
|
|
|
24
25
|
_LABEL_TO_DECISION = {
|
|
25
26
|
APPROVE_LABEL: "approve",
|
|
26
27
|
DENY_LABEL: "deny",
|
|
27
28
|
ALWAYS_LABEL: "always",
|
|
29
|
+
NEVER_LABEL: "never",
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
|
|
31
|
-
def request_approval(
|
|
33
|
+
def request_approval(
|
|
34
|
+
prompt: str, *, allow_always: bool = False, allow_never: bool = False, detail: Optional[str] = None
|
|
35
|
+
) -> str:
|
|
32
36
|
"""Ask the user to approve an action, returning an :data:`ApprovalDecision`.
|
|
33
37
|
|
|
34
|
-
Builds ``Approve``/``Deny`` options (plus ``Always allow`` when
|
|
35
|
-
``
|
|
36
|
-
``"approval"`` question, then maps the chosen label
|
|
37
|
-
|
|
38
|
+
Builds ``Approve``/``Deny`` options (plus ``Always allow`` when ``allow_always``
|
|
39
|
+
and ``Always deny`` when ``allow_never``) and routes them through the active
|
|
40
|
+
interaction backend as an ``"approval"`` question, then maps the chosen label
|
|
41
|
+
back to a decision.
|
|
38
42
|
|
|
39
43
|
Args:
|
|
40
44
|
prompt: The action to approve, phrased as a question.
|
|
41
45
|
allow_always: Offer an "Always allow" option (a persistent allow).
|
|
46
|
+
allow_never: Offer an "Always deny" option (a persistent deny).
|
|
42
47
|
detail: Extra context appended below the prompt for the user to read.
|
|
43
48
|
|
|
44
49
|
Returns:
|
|
45
|
-
One of ``"approve"``, ``"deny"``, or ``"
|
|
50
|
+
One of ``"approve"``, ``"deny"``, ``"always"``, or ``"never"``.
|
|
46
51
|
|
|
47
52
|
Fail-closed: returns ``"deny"`` WITHOUT prompting when there is no backend or
|
|
48
53
|
the backend is non-interactive, and for any unrecognized reply.
|
|
@@ -54,6 +59,8 @@ def request_approval(prompt: str, *, allow_always: bool = False, detail: Optiona
|
|
|
54
59
|
options = [APPROVE_LABEL, DENY_LABEL]
|
|
55
60
|
if allow_always:
|
|
56
61
|
options.append(ALWAYS_LABEL)
|
|
62
|
+
if allow_never:
|
|
63
|
+
options.append(NEVER_LABEL)
|
|
57
64
|
|
|
58
65
|
question = f"{prompt}\n\n{detail}" if detail else prompt
|
|
59
66
|
answer = backend.ask_user(question, "approval", options)
|
|
@@ -75,6 +75,7 @@ def agents_show(
|
|
|
75
75
|
"""
|
|
76
76
|
from tsugite.agent_inheritance import find_agent_file
|
|
77
77
|
from tsugite.md_agents import parse_agent_file
|
|
78
|
+
from tsugite.tools import format_tool_spec
|
|
78
79
|
|
|
79
80
|
try:
|
|
80
81
|
agent_file = Path(agent_path)
|
|
@@ -113,7 +114,7 @@ def agents_show(
|
|
|
113
114
|
if config.tools:
|
|
114
115
|
console.print(f"\n[bold]Tools ({len(config.tools)}):[/bold]")
|
|
115
116
|
for tool in config.tools:
|
|
116
|
-
console.print(f" • {tool}")
|
|
117
|
+
console.print(f" • {format_tool_spec(tool)}")
|
|
117
118
|
|
|
118
119
|
if config.extends:
|
|
119
120
|
console.print(f"\n[bold]Extends:[/bold] {config.extends}")
|
|
@@ -288,6 +288,7 @@ def run(
|
|
|
288
288
|
from tsugite.console import get_stderr_console
|
|
289
289
|
from tsugite.md_agents import validate_agent_execution
|
|
290
290
|
from tsugite.secrets import init_cli as init_secrets
|
|
291
|
+
from tsugite.tools import format_tool_specs
|
|
291
292
|
from tsugite.utils import should_use_plain_output
|
|
292
293
|
|
|
293
294
|
from . import console
|
|
@@ -562,7 +563,7 @@ def run(
|
|
|
562
563
|
"Directory": str(Path.cwd()),
|
|
563
564
|
"Model": resolve_effective_model(exec_opts.model_override, agent_info.get("model_raw")) or "unknown",
|
|
564
565
|
"Instructions": instruction_label,
|
|
565
|
-
"Tools":
|
|
566
|
+
"Tools": format_tool_specs(agent_info.get("tools", [])),
|
|
566
567
|
}
|
|
567
568
|
|
|
568
569
|
agent_attachments = agent_info.get("attachments")
|
|
@@ -98,6 +98,13 @@ def _jsonable_call_args(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
98
98
|
return safe
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
NAME_ERROR_NOTE = (
|
|
102
|
+
" (each code block starts with a fresh namespace: names from earlier blocks and"
|
|
103
|
+
" <content> blocks do not carry over, so re-import or recompute them, or keep"
|
|
104
|
+
" values in state['key'])"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
101
108
|
def truncate_observation(text: str, max_output_kb: int = MAX_EXECUTION_OUTPUT_KB) -> tuple[str, bool]:
|
|
102
109
|
"""Clip text to the observation cap, returning (text, was_truncated).
|
|
103
110
|
|
|
@@ -134,6 +141,8 @@ def build_execution_result(
|
|
|
134
141
|
|
|
135
142
|
children = [El("output", [output], inline=True)]
|
|
136
143
|
if error:
|
|
144
|
+
if error.startswith("NameError:"):
|
|
145
|
+
error += NAME_ERROR_NOTE
|
|
137
146
|
children.append(El("error", [error], inline=True))
|
|
138
147
|
if traceback:
|
|
139
148
|
children.append(El("traceback", [traceback], inline=True))
|
|
@@ -201,6 +201,7 @@ class RecordStore:
|
|
|
201
201
|
valid_transitions: dict[str, frozenset[str]]
|
|
202
202
|
terminal_states: frozenset[str]
|
|
203
203
|
transition_error_cls: type[ValueError] = ValueError
|
|
204
|
+
missing_error_cls: type[LookupError] = KeyError
|
|
204
205
|
|
|
205
206
|
def __init__(self, path: Path):
|
|
206
207
|
self._path = path # legacy JSON location; migration source only
|
|
@@ -237,11 +238,18 @@ class RecordStore:
|
|
|
237
238
|
with self._lock:
|
|
238
239
|
return [r for r in self._records.values() if r.parent_session_id == parent_session_id]
|
|
239
240
|
|
|
241
|
+
def require(self, record_id: str):
|
|
242
|
+
"""Raises `missing_error_cls` when no record has that id."""
|
|
243
|
+
record = self.get(record_id)
|
|
244
|
+
if record is None:
|
|
245
|
+
raise self.missing_error_cls(f"Unknown {self.record_label}: {record_id}")
|
|
246
|
+
return record
|
|
247
|
+
|
|
240
248
|
def update_state(self, record_id: str, new_state: str):
|
|
241
249
|
with self._lock:
|
|
242
250
|
record = self._records.get(record_id)
|
|
243
251
|
if record is None:
|
|
244
|
-
raise
|
|
252
|
+
raise self.missing_error_cls(f"Unknown {self.record_label}: {record_id}")
|
|
245
253
|
allowed = self.valid_transitions.get(record.state, frozenset())
|
|
246
254
|
if new_state not in allowed:
|
|
247
255
|
raise self.transition_error_cls(
|
|
@@ -270,7 +278,7 @@ class RecordStore:
|
|
|
270
278
|
with self._lock:
|
|
271
279
|
record = self._records.get(record_id)
|
|
272
280
|
if record is None:
|
|
273
|
-
raise
|
|
281
|
+
raise self.missing_error_cls(f"Unknown {self.record_label}: {record_id}")
|
|
274
282
|
for key, value in fields.items():
|
|
275
283
|
if not hasattr(record, key):
|
|
276
284
|
raise ValueError(f"Unknown {self.record_cls.__name__} field: {key}")
|
|
@@ -11,6 +11,7 @@ from datetime import datetime
|
|
|
11
11
|
from typing import Any, Dict, Iterable, List, Optional
|
|
12
12
|
from xml.sax.saxutils import escape, quoteattr
|
|
13
13
|
|
|
14
|
+
from tsugite.prompt_xml import El
|
|
14
15
|
from tsugite.renderer import format_prompt_ts, parse_iso_utc
|
|
15
16
|
|
|
16
17
|
from .models import Event
|
|
@@ -176,6 +177,11 @@ def _event_to_message(event: Event, executed: bool = False) -> Optional[Dict[str
|
|
|
176
177
|
return {"role": "user", "content": _format_error_xml(event.data)}
|
|
177
178
|
if event.type == "delivery":
|
|
178
179
|
return {"role": "user", "content": _delivery_xml(event.data, event.ts)}
|
|
180
|
+
if event.type == "error":
|
|
181
|
+
# send_failed is a notification that never left the daemon, not a turn failure.
|
|
182
|
+
if event.data.get("kind") == "send_failed":
|
|
183
|
+
return None
|
|
184
|
+
return {"role": "user", "content": _turn_failed_xml(event.data, event.ts)}
|
|
179
185
|
return None
|
|
180
186
|
|
|
181
187
|
|
|
@@ -223,6 +229,22 @@ def _delivery_xml(data: Dict[str, Any], ts: Optional[datetime] = None) -> str:
|
|
|
223
229
|
return "\n".join(parts)
|
|
224
230
|
|
|
225
231
|
|
|
232
|
+
_TURN_FAILED_NOTE = (
|
|
233
|
+
"The previous attempt failed before it finished. Continue from where it stopped rather than starting over."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _turn_failed_xml(data: Dict[str, Any], ts: datetime) -> str:
|
|
238
|
+
return El(
|
|
239
|
+
"tsugite_turn_failed",
|
|
240
|
+
[
|
|
241
|
+
El("note", [_TURN_FAILED_NOTE], inline=True),
|
|
242
|
+
El("error", [data.get("error")], inline=True),
|
|
243
|
+
],
|
|
244
|
+
{"ts": _format_event_ts(ts)},
|
|
245
|
+
).render()
|
|
246
|
+
|
|
247
|
+
|
|
226
248
|
def _format_error_xml(data: Dict[str, Any]) -> str:
|
|
227
249
|
reason = data.get("reason", "")
|
|
228
250
|
msg = (
|
|
@@ -3,23 +3,26 @@
|
|
|
3
3
|
Merges two YAML sources, the same way hooks does:
|
|
4
4
|
|
|
5
5
|
1. ``runtime_path``: a mutable ``permissions.yaml`` next to ``daemon.yaml``.
|
|
6
|
-
"Always allow" decisions append here, so they survive a restart.
|
|
6
|
+
"Always allow" and "Always deny" decisions append here, so they survive a restart.
|
|
7
7
|
2. ``workspace_dir``: an optional, read-only ``<workspace_dir>/.tsugite/permissions.yaml``,
|
|
8
8
|
hand-authored per agent (exactly like ``.tsugite/hooks.yaml``). The store never
|
|
9
9
|
writes it.
|
|
10
10
|
|
|
11
|
-
``
|
|
11
|
+
``is_listed`` is the union of both files. ``add`` only ever writes the runtime
|
|
12
12
|
file, atomically, preserving every unrelated section.
|
|
13
13
|
|
|
14
|
-
The file groups
|
|
14
|
+
The file groups lists by section, e.g.::
|
|
15
15
|
|
|
16
16
|
web:
|
|
17
17
|
fetch_allowlist:
|
|
18
18
|
- example.com
|
|
19
|
+
fetch_denylist:
|
|
20
|
+
- ads.example.com
|
|
19
21
|
|
|
20
|
-
The generic ``
|
|
21
|
-
|
|
22
|
-
layout so callers stay decoupled from it.
|
|
22
|
+
The generic ``is_listed(section, key)`` / ``add(section, key)`` address a list by
|
|
23
|
+
dotted path (``"web.fetch_allowlist"``). The ``web_fetch_*`` wrappers hide that
|
|
24
|
+
layout so callers stay decoupled from it. The store applies no precedence between
|
|
25
|
+
lists. The caller decides.
|
|
23
26
|
"""
|
|
24
27
|
|
|
25
28
|
import contextvars
|
|
@@ -31,6 +34,7 @@ import yaml
|
|
|
31
34
|
from tsugite.utils import atomic_write_text
|
|
32
35
|
|
|
33
36
|
WEB_FETCH_ALLOWLIST = "web.fetch_allowlist"
|
|
37
|
+
WEB_FETCH_DENYLIST = "web.fetch_denylist"
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
def _load_yaml_mapping(path: Optional[Path]) -> dict:
|
|
@@ -70,20 +74,20 @@ def _atomic_write_yaml(path: Path, data: dict) -> None:
|
|
|
70
74
|
|
|
71
75
|
|
|
72
76
|
class Permissions:
|
|
73
|
-
"""Union of a mutable runtime
|
|
77
|
+
"""Union of a mutable runtime permissions file and a read-only workspace one."""
|
|
74
78
|
|
|
75
79
|
def __init__(self, runtime_path: Path, workspace_dir: Optional[Path] = None):
|
|
76
80
|
self.runtime_path = Path(runtime_path)
|
|
77
81
|
self._workspace_path = (Path(workspace_dir) / ".tsugite" / "permissions.yaml") if workspace_dir else None
|
|
78
82
|
|
|
79
|
-
def
|
|
83
|
+
def is_listed(self, section: str, key: str) -> bool:
|
|
80
84
|
"""True if ``key`` is listed at dotted ``section`` in EITHER source."""
|
|
81
85
|
parts = section.split(".")
|
|
82
86
|
return key in _get_list(_load_yaml_mapping(self.runtime_path), parts) or key in _get_list(
|
|
83
87
|
_load_yaml_mapping(self._workspace_path), parts
|
|
84
88
|
)
|
|
85
89
|
|
|
86
|
-
def
|
|
90
|
+
def add(self, section: str, key: str) -> None:
|
|
87
91
|
"""Append ``key`` to the RUNTIME file's list at dotted ``section``.
|
|
88
92
|
|
|
89
93
|
Only ever touches the runtime file (never the workspace file), writes
|
|
@@ -102,11 +106,19 @@ class Permissions:
|
|
|
102
106
|
|
|
103
107
|
def web_fetch_allowed(self, domain: str) -> bool:
|
|
104
108
|
"""True if ``domain`` is on the web fetch allowlist (either source)."""
|
|
105
|
-
return self.
|
|
109
|
+
return self.is_listed(WEB_FETCH_ALLOWLIST, domain.lower())
|
|
106
110
|
|
|
107
111
|
def web_fetch_allow(self, domain: str) -> None:
|
|
108
112
|
"""Persist ``domain`` to the runtime web fetch allowlist."""
|
|
109
|
-
self.
|
|
113
|
+
self.add(WEB_FETCH_ALLOWLIST, domain.lower())
|
|
114
|
+
|
|
115
|
+
def web_fetch_denied(self, domain: str) -> bool:
|
|
116
|
+
"""True if ``domain`` is on the web fetch denylist (either source)."""
|
|
117
|
+
return self.is_listed(WEB_FETCH_DENYLIST, domain.lower())
|
|
118
|
+
|
|
119
|
+
def web_fetch_deny(self, domain: str) -> None:
|
|
120
|
+
"""Persist ``domain`` to the runtime web fetch denylist."""
|
|
121
|
+
self.add(WEB_FETCH_DENYLIST, domain.lower())
|
|
110
122
|
|
|
111
123
|
|
|
112
124
|
_permissions_var: contextvars.ContextVar[Optional[Permissions]] = contextvars.ContextVar("permissions", default=None)
|
|
@@ -48,6 +48,7 @@ _ANTHROPIC_MODELS: dict[str, ModelInfo] = {
|
|
|
48
48
|
"anthropic/claude-opus-4-7": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
49
49
|
"anthropic/claude-opus-4-8": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
50
50
|
"anthropic/claude-opus-5": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
51
|
+
"anthropic/claude-opus-5-5": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=4.0, output_cost_per_million=20.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
51
52
|
"anthropic/claude-sonnet-4-5": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
52
53
|
"anthropic/claude-sonnet-4-5-20250929": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
53
54
|
"anthropic/claude-sonnet-4-6": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
6
|
+
|
|
5
7
|
from .base import ModelInfo, Usage
|
|
6
8
|
|
|
7
9
|
_REGISTRY: dict[str, ModelInfo] = {}
|
|
@@ -22,11 +24,13 @@ def get_model_info(provider: str, model: str) -> ModelInfo | None:
|
|
|
22
24
|
for reg_key, info in _REGISTRY.items():
|
|
23
25
|
if not key.startswith(reg_key) or len(reg_key) <= best_len:
|
|
24
26
|
continue
|
|
25
|
-
# Only treat the prefix as the same model when the remainder is a
|
|
26
|
-
# continuation (e.g. "-20250805"), not a distinct variant
|
|
27
|
-
#
|
|
27
|
+
# Only treat the prefix as the same model when the remainder is a dated
|
|
28
|
+
# continuation (e.g. "-20250805", "-2024-05-13"), not a distinct variant
|
|
29
|
+
# ("-mini", "-turbo") or a point release ("-5"). An unlisted variant
|
|
30
|
+
# would otherwise silently inherit a sibling's pricing. A year is 4+
|
|
31
|
+
# digits, so require the digit run right after the dash to be that long.
|
|
28
32
|
suffix = key[len(reg_key) :]
|
|
29
|
-
if suffix and not
|
|
33
|
+
if suffix and not re.match(r"^-\d{4}", suffix):
|
|
30
34
|
continue
|
|
31
35
|
best_match = info
|
|
32
36
|
best_len = len(reg_key)
|
|
@@ -55,6 +55,8 @@ _OPENAI_MODELS: dict[str, ModelInfo] = {
|
|
|
55
55
|
"openai/gpt-5.6-sol": ModelInfo(max_input_tokens=1_050_000, max_output_tokens=128_000, input_cost_per_million=4.0, output_cost_per_million=20.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["none", "low", "medium", "high", "xhigh", "max"]),
|
|
56
56
|
"openai/gpt-5.6-terra": ModelInfo(max_input_tokens=1_050_000, max_output_tokens=128_000, input_cost_per_million=2.0, output_cost_per_million=12.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["none", "low", "medium", "high", "xhigh", "max"]),
|
|
57
57
|
"openai/gpt-6-astra": ModelInfo(max_input_tokens=1_050_000, max_output_tokens=128_000, input_cost_per_million=10.0, output_cost_per_million=50.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"]),
|
|
58
|
+
"openai/gpt-6-luna": ModelInfo(max_input_tokens=1_050_000, max_output_tokens=128_000, input_cost_per_million=0.1, output_cost_per_million=0.5, supports_vision=True, supports_reasoning=True, supported_effort_levels=["none", "low", "medium", "high", "xhigh", "max"]),
|
|
59
|
+
"openai/gpt-6-sol": ModelInfo(max_input_tokens=1_050_000, max_output_tokens=128_000, input_cost_per_million=2.0, output_cost_per_million=10.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["none", "low", "medium", "high", "xhigh", "max"]),
|
|
58
60
|
"openai/o1": ModelInfo(max_input_tokens=200_000, max_output_tokens=100_000, input_cost_per_million=15.0, output_cost_per_million=60.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high"]),
|
|
59
61
|
"openai/o1-pro": ModelInfo(max_input_tokens=200_000, max_output_tokens=100_000, input_cost_per_million=150.0, output_cost_per_million=600.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high"]),
|
|
60
62
|
"openai/o3": ModelInfo(max_input_tokens=200_000, max_output_tokens=100_000, input_cost_per_million=2.0, output_cost_per_million=8.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high"]),
|
|
@@ -376,6 +376,21 @@ def expand_tool_specs(tool_specs: List[str], strict: bool = False) -> List[str]:
|
|
|
376
376
|
return result
|
|
377
377
|
|
|
378
378
|
|
|
379
|
+
def format_tool_spec(spec: str) -> str:
|
|
380
|
+
"""Render one frontmatter tool spec for console output, with `@category` as `name (toolset)`.
|
|
381
|
+
|
|
382
|
+
An agent that shells out to tsugite reads this through its own tool layer, where `@name`
|
|
383
|
+
expands as a file mention if it matches a directory.
|
|
384
|
+
"""
|
|
385
|
+
prefix, name = ("-", spec[1:]) if spec.startswith("-") else ("", spec)
|
|
386
|
+
return f"{prefix}{name[1:]} (toolset)" if name.startswith("@") else spec
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def format_tool_specs(tool_specs: List[str]) -> str:
|
|
390
|
+
"""Render a frontmatter tool list as one comma-separated line."""
|
|
391
|
+
return ", ".join(format_tool_spec(spec) for spec in tool_specs)
|
|
392
|
+
|
|
393
|
+
|
|
379
394
|
def load_custom_shell_tools() -> None:
|
|
380
395
|
"""Register the shell tools defined in custom_tools.yaml.
|
|
381
396
|
|
|
@@ -18,6 +18,11 @@ _WEB_EXTRA_HINT = "Install it with: pip install 'tsugite-cli[web]'"
|
|
|
18
18
|
# reach anything the original request couldn't.
|
|
19
19
|
_MAX_REDIRECTS = 5
|
|
20
20
|
|
|
21
|
+
# readability emits a near-empty node when it finds no article. An extraction that is
|
|
22
|
+
# both short and a sliver of the whole page means extraction failed, not a short article.
|
|
23
|
+
_MIN_ARTICLE_CHARS = 200
|
|
24
|
+
_ARTICLE_PAGE_RATIO = 10
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
def _extract_article(html: str) -> str:
|
|
23
28
|
"""Extract the main article body from HTML using readability (optional dep)."""
|
|
@@ -150,9 +155,21 @@ def fetch_text(
|
|
|
150
155
|
if "text/html" not in content_type or (not strip_html and not extract_article):
|
|
151
156
|
return text
|
|
152
157
|
|
|
153
|
-
if extract_article:
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
if not extract_article:
|
|
159
|
+
return convert_html_to_markdown(text)
|
|
160
|
+
|
|
161
|
+
article = convert_html_to_markdown(_extract_article(text))
|
|
162
|
+
article_len = len(article.strip())
|
|
163
|
+
if article_len >= _MIN_ARTICLE_CHARS:
|
|
164
|
+
return article
|
|
165
|
+
|
|
166
|
+
page = convert_html_to_markdown(text)
|
|
167
|
+
page_len = len(page.strip())
|
|
168
|
+
if not page_len:
|
|
169
|
+
raise RuntimeError(f"{url} has no readable text; the page is likely rendered client-side")
|
|
170
|
+
if article_len * _ARTICLE_PAGE_RATIO < page_len:
|
|
171
|
+
return page
|
|
172
|
+
return article
|
|
156
173
|
|
|
157
174
|
|
|
158
175
|
# No sensitive_args: the built-in key rules already redact Authorization,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""JSONL UI handler for subprocess-based subagent communication."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Any, Dict
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
5
|
|
|
6
6
|
from tsugite.events import (
|
|
7
7
|
CodeExecutionEvent,
|
|
@@ -32,6 +32,7 @@ from tsugite.events import (
|
|
|
32
32
|
ToolResultEvent,
|
|
33
33
|
WarningEvent,
|
|
34
34
|
)
|
|
35
|
+
from tsugite.events.base import BaseEvent
|
|
35
36
|
from tsugite.ui.dispatch import EventDispatchMixin, handles
|
|
36
37
|
|
|
37
38
|
# Event types that share the same file-io payload shape
|
|
@@ -73,6 +74,17 @@ class JSONLUIHandler(EventDispatchMixin):
|
|
|
73
74
|
- Failed tool: {"type": "tool_result", "tool": "read_file", "success": false, "error": "..."}
|
|
74
75
|
"""
|
|
75
76
|
|
|
77
|
+
final_seen = False
|
|
78
|
+
latest_prompt_messages: Optional[list] = None
|
|
79
|
+
|
|
80
|
+
def handle_event(self, event: BaseEvent) -> None:
|
|
81
|
+
if isinstance(event, PromptSnapshotEvent):
|
|
82
|
+
if event.messages:
|
|
83
|
+
self.latest_prompt_messages = event.messages
|
|
84
|
+
if not event.token_breakdown:
|
|
85
|
+
return
|
|
86
|
+
super().handle_event(event)
|
|
87
|
+
|
|
76
88
|
@handles(TaskStartEvent)
|
|
77
89
|
def _handle_task_start(self, event: TaskStartEvent) -> None:
|
|
78
90
|
self._emit("init", {"agent": event.task, "model": event.model})
|
|
@@ -132,6 +144,7 @@ class JSONLUIHandler(EventDispatchMixin):
|
|
|
132
144
|
|
|
133
145
|
@handles(FinalAnswerEvent)
|
|
134
146
|
def _handle_final_answer(self, event: FinalAnswerEvent) -> None:
|
|
147
|
+
self.final_seen = True
|
|
135
148
|
self._emit(
|
|
136
149
|
"final_result",
|
|
137
150
|
{
|
|
@@ -3,9 +3,8 @@ subscription or budget the user has consumed ("week 73%, resets Aug 19").
|
|
|
3
3
|
|
|
4
4
|
A source is contributed by a plugin through the ``tsugite.usage_providers``
|
|
5
5
|
entry point (a module-only entry point whose import registers the source) and
|
|
6
|
-
fetches from
|
|
7
|
-
field
|
|
8
|
-
let the two disagree.
|
|
6
|
+
fetches from that provider's local state or API. The shape has no severity
|
|
7
|
+
field because the UI derives its own warn threshold.
|
|
9
8
|
"""
|
|
10
9
|
|
|
11
10
|
from __future__ import annotations
|
|
@@ -31,8 +30,8 @@ class QuotaWindow:
|
|
|
31
30
|
|
|
32
31
|
@dataclass(frozen=True)
|
|
33
32
|
class QuotaReport:
|
|
34
|
-
"""One provider's quota. ``error`` is set instead of windows when
|
|
35
|
-
|
|
33
|
+
"""One provider's quota. ``error`` is set instead of windows when the
|
|
34
|
+
source could not be read."""
|
|
36
35
|
|
|
37
36
|
provider: str
|
|
38
37
|
label: str
|
|
@@ -69,10 +68,9 @@ def reset_quota_sources() -> None:
|
|
|
69
68
|
|
|
70
69
|
|
|
71
70
|
def ensure_loaded() -> None:
|
|
72
|
-
"""Import the ``tsugite.usage_providers`` entry-point modules once,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
waits for it rather than seeing an empty registry."""
|
|
71
|
+
"""Import the ``tsugite.usage_providers`` entry-point modules once, before
|
|
72
|
+
the registry is read. A daemon worker thread racing the first load waits
|
|
73
|
+
for it."""
|
|
76
74
|
global _loaded
|
|
77
75
|
if _loaded:
|
|
78
76
|
return
|
|
@@ -102,7 +100,7 @@ def collect_quota_reports() -> list[dict]:
|
|
|
102
100
|
logger.warning("Quota source '%s' failed: %s", source.provider, e)
|
|
103
101
|
report = QuotaReport(provider=source.provider, label=source.label, error=str(e))
|
|
104
102
|
row = asdict(report)
|
|
105
|
-
# asdict keeps
|
|
103
|
+
# asdict keeps windows as a tuple and the wire shape is an array.
|
|
106
104
|
row["windows"] = list(row["windows"])
|
|
107
105
|
rows.append(row)
|
|
108
106
|
return rows
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|