tsugite-cli 0.19.2__tar.gz → 0.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/AGENTS.md +1 -1
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/PKG-INFO +2 -2
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/pyproject.toml +4 -1
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_inheritance.py +21 -25
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/helpers.py +9 -6
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/runner.py +6 -8
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/__init__.py +23 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/daemon.py +20 -1
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/config.py +2 -1
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/agent.py +86 -1
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/executor.py +70 -3
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/subprocess_executor.py +100 -3
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/events/__init__.py +4 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/events/base.py +2 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/events/events.py +31 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/plugins.py +105 -70
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/__init__.py +12 -7
- tsugite_cli-0.20.0/tsugite/tools/daemon_control.py +60 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/jobs.py +1 -4
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/notify.py +9 -6
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/schedule.py +1 -4
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/sessions.py +1 -4
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/shell_tools.py +14 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/skills.py +13 -9
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/base.py +6 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/jsonl.py +23 -0
- tsugite_cli-0.20.0/tsugite/ui_surfaces.py +105 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/.gitignore +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/LICENSE +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/README.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/history_integration.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/steps.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/approval.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/agent_config.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/models.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/anthropic.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/openai_compat.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/http.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/scratchpad.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/utils.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.19.2 → tsugite_cli-0.20.0}/tsugite/workspace/templates.py +0 -0
|
@@ -83,7 +83,7 @@ Tsugite is an agentic CLI that executes AI agents defined as markdown files with
|
|
|
83
83
|
- `./{name}.md` (current directory)
|
|
84
84
|
- `builtin_agents/{name}.md` (package-provided)
|
|
85
85
|
- Global directories (`~/.config/tsugite/agents/`)
|
|
86
|
-
- Merges agent inheritance chains (scalars override, lists merge
|
|
86
|
+
- Merges agent inheritance chains (scalars and dicts override, lists merge)
|
|
87
87
|
|
|
88
88
|
3. **Agent Parsing** (`tsugite/md_agents.py`)
|
|
89
89
|
- Parses YAML frontmatter → `AgentConfig` (Pydantic model)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tsugite-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.20.0"
|
|
4
4
|
description = "Micro-agent runner for task automation using markdown definitions"
|
|
5
5
|
authors = [{ name = "Justyn Shull" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -93,6 +93,7 @@ tsugite-pty = { workspace = true }
|
|
|
93
93
|
tsugite-daemon = { workspace = true }
|
|
94
94
|
tsugite-discord = { workspace = true }
|
|
95
95
|
tsugite-cc-driver = { workspace = true }
|
|
96
|
+
tsugite-onlyoffice = { workspace = true }
|
|
96
97
|
|
|
97
98
|
[tool.ruff]
|
|
98
99
|
target-version = "py311"
|
|
@@ -170,6 +171,7 @@ paths = [
|
|
|
170
171
|
"plugins/tsugite-codex-cli/tsugite_codex_cli",
|
|
171
172
|
"plugins/tsugite-daemon/tsugite_daemon",
|
|
172
173
|
"plugins/tsugite-discord/tsugite_discord",
|
|
174
|
+
"plugins/tsugite-onlyoffice/tsugite_onlyoffice",
|
|
173
175
|
"plugins/tsugite-pty/tsugite_pty",
|
|
174
176
|
"plugins/tsugite-sandbox/tsugite_sandbox",
|
|
175
177
|
"plugins/tsugite-tmux/tsugite_tmux",
|
|
@@ -268,4 +270,5 @@ dev = [
|
|
|
268
270
|
"tsugite-daemon",
|
|
269
271
|
"tsugite-discord",
|
|
270
272
|
"tsugite-cc-driver",
|
|
273
|
+
"tsugite-onlyoffice",
|
|
271
274
|
]
|
|
@@ -293,18 +293,16 @@ def merge_agent_configs(parent, child):
|
|
|
293
293
|
Returns:
|
|
294
294
|
Merged AgentConfig with child taking precedence
|
|
295
295
|
|
|
296
|
-
Merge rules:
|
|
297
|
-
-
|
|
298
|
-
- Lists (tools, attachments): merge and deduplicate
|
|
296
|
+
Merge rules, applied in order so the later ones win:
|
|
297
|
+
- Every field: child overwrites parent, dicts included (model_kwargs, network, sandbox)
|
|
298
|
+
- Lists (tools, attachments, skill_paths): merge and deduplicate
|
|
299
299
|
- Lists (prefetch): concatenate (parent first, no deduplication)
|
|
300
300
|
- Lists of dicts (custom_tools): merge and deduplicate by "name" field
|
|
301
301
|
- Strings (instructions): concatenate with newline
|
|
302
302
|
"""
|
|
303
303
|
from .md_agents import AgentConfig
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
merged_data = {}
|
|
307
|
-
merged_data.update(merge_scalar_fields(parent, child))
|
|
305
|
+
merged_data = merge_all_fields(parent, child)
|
|
308
306
|
merged_data.update(merge_list_fields(parent, child))
|
|
309
307
|
merged_data["instructions"] = merge_instructions(parent, child)
|
|
310
308
|
|
|
@@ -389,33 +387,29 @@ def _get_default_base_agent_name() -> Optional[str]:
|
|
|
389
387
|
return "default"
|
|
390
388
|
|
|
391
389
|
|
|
392
|
-
def
|
|
393
|
-
"""Merge
|
|
390
|
+
def merge_all_fields(parent, child) -> Dict[str, Any]:
|
|
391
|
+
"""Merge every AgentConfig field, so a newly added one is never dropped.
|
|
394
392
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
parent: Parent AgentConfig
|
|
399
|
-
child: Child AgentConfig
|
|
400
|
-
|
|
401
|
-
Returns:
|
|
402
|
-
Dict of merged scalar fields
|
|
393
|
+
A child takes precedence when it set the field to something other than `None`
|
|
394
|
+
or `""`, both of which read as "leave this to the parent". `merge_agent_configs`
|
|
395
|
+
then overwrites the fields that have their own strategy.
|
|
403
396
|
"""
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
397
|
+
merged = {}
|
|
398
|
+
for field_name in type(child).model_fields:
|
|
399
|
+
value = getattr(child, field_name)
|
|
400
|
+
child_wins = field_name in child.model_fields_set and value not in (None, "")
|
|
401
|
+
merged[field_name] = value if child_wins else getattr(parent, field_name)
|
|
402
|
+
|
|
403
|
+
# `extends` names this agent's own parent, so it describes the child alone.
|
|
404
|
+
merged["extends"] = child.extends
|
|
405
|
+
return merged
|
|
412
406
|
|
|
413
407
|
|
|
414
408
|
def merge_list_fields(parent, child) -> Dict[str, List]:
|
|
415
409
|
"""Merge list fields from parent and child configs.
|
|
416
410
|
|
|
417
411
|
Different list types have different merge strategies:
|
|
418
|
-
- tools, attachments: merge and deduplicate
|
|
412
|
+
- tools, attachments, skill_paths: merge and deduplicate
|
|
419
413
|
- prefetch: concatenate (parent first)
|
|
420
414
|
- custom_tools: deduplicate by "name" field
|
|
421
415
|
|
|
@@ -449,6 +443,7 @@ def merge_list_fields(parent, child) -> Dict[str, List]:
|
|
|
449
443
|
merged_attachments = _merge_attachments(parent.attachments, child.attachments)
|
|
450
444
|
merged_skills = _merge_dedup(parent.auto_load_skills, child.auto_load_skills)
|
|
451
445
|
merged_auto_load_agents = _merge_dedup(parent.auto_load_agents, child.auto_load_agents)
|
|
446
|
+
merged_skill_paths = _merge_dedup(parent.skill_paths, child.skill_paths)
|
|
452
447
|
|
|
453
448
|
# Custom tools - deduplicate by "name" field (child overrides parent)
|
|
454
449
|
custom_tool_dict = {}
|
|
@@ -462,6 +457,7 @@ def merge_list_fields(parent, child) -> Dict[str, List]:
|
|
|
462
457
|
"custom_tools": list(custom_tool_dict.values()),
|
|
463
458
|
"auto_load_skills": merged_skills,
|
|
464
459
|
"auto_load_agents": merged_auto_load_agents,
|
|
460
|
+
"skill_paths": merged_skill_paths,
|
|
465
461
|
}
|
|
466
462
|
|
|
467
463
|
|
|
@@ -302,15 +302,18 @@ def get_ui_handler(custom_logger: Optional[Any]) -> Optional[Any]:
|
|
|
302
302
|
def set_multistep_ui_context(custom_logger: Optional[Any], step_number: int, step_name: str, total_steps: int) -> None:
|
|
303
303
|
"""Set multistep context in UI handler if available.
|
|
304
304
|
|
|
305
|
+
Only a handler that renders a live progress display tracks this, and the handler
|
|
306
|
+
contract is `handle_event` alone - a plugin may ship one subclassing nothing.
|
|
307
|
+
|
|
305
308
|
Args:
|
|
306
309
|
custom_logger: Custom logger instance (may be None)
|
|
307
310
|
step_number: Current step number
|
|
308
311
|
step_name: Name of current step
|
|
309
312
|
total_steps: Total number of steps
|
|
310
313
|
"""
|
|
311
|
-
|
|
312
|
-
if
|
|
313
|
-
|
|
314
|
+
setter = getattr(get_ui_handler(custom_logger), "set_multistep_context", None)
|
|
315
|
+
if setter:
|
|
316
|
+
setter(step_number, step_name, total_steps)
|
|
314
317
|
|
|
315
318
|
|
|
316
319
|
def clear_multistep_ui_context(custom_logger: Optional[Any]) -> None:
|
|
@@ -319,9 +322,9 @@ def clear_multistep_ui_context(custom_logger: Optional[Any]) -> None:
|
|
|
319
322
|
Args:
|
|
320
323
|
custom_logger: Custom logger instance (may be None)
|
|
321
324
|
"""
|
|
322
|
-
|
|
323
|
-
if
|
|
324
|
-
|
|
325
|
+
clearer = getattr(get_ui_handler(custom_logger), "clear_multistep_context", None)
|
|
326
|
+
if clearer:
|
|
327
|
+
clearer()
|
|
325
328
|
|
|
326
329
|
|
|
327
330
|
def print_step_progress(
|
|
@@ -461,19 +461,17 @@ async def _execute_agent_with_prompt(
|
|
|
461
461
|
# Register per-agent custom shell tools (if any)
|
|
462
462
|
if agent_config.custom_tools:
|
|
463
463
|
from tsugite.shell_tool_config import parse_tool_definition_from_dict
|
|
464
|
-
from tsugite.tools.shell_tools import
|
|
464
|
+
from tsugite.tools.shell_tools import bind_custom_tools
|
|
465
465
|
|
|
466
466
|
try:
|
|
467
467
|
custom_tool_definitions = [
|
|
468
468
|
parse_tool_definition_from_dict(tool_dict) for tool_dict in agent_config.custom_tools
|
|
469
469
|
]
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
tools.append(create_tool_from_tsugite(tool_def.name))
|
|
470
|
+
# This run's own definitions win over anything the registry resolved
|
|
471
|
+
# under the same name for an earlier agent.
|
|
472
|
+
for custom in bind_custom_tools(custom_tool_definitions):
|
|
473
|
+
tools = [t for t in tools if t.name != custom.name]
|
|
474
|
+
tools.append(custom)
|
|
477
475
|
except Exception as e:
|
|
478
476
|
event_bus.emit(WarningEvent(message=f"Failed to register custom tools: {e}"))
|
|
479
477
|
|
|
@@ -17,6 +17,29 @@ app = typer.Typer(
|
|
|
17
17
|
console = Console()
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _print_version(value: bool) -> None:
|
|
21
|
+
if not value:
|
|
22
|
+
return
|
|
23
|
+
from tsugite import __version__
|
|
24
|
+
|
|
25
|
+
console.print(f"Tsugite version {__version__}")
|
|
26
|
+
raise typer.Exit()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.callback()
|
|
30
|
+
def main(
|
|
31
|
+
version: bool = typer.Option(
|
|
32
|
+
False,
|
|
33
|
+
"--version",
|
|
34
|
+
"-V",
|
|
35
|
+
callback=_print_version,
|
|
36
|
+
is_eager=True,
|
|
37
|
+
help="Show version information.",
|
|
38
|
+
),
|
|
39
|
+
) -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
20
43
|
@app.command()
|
|
21
44
|
def version():
|
|
22
45
|
"""Show version information."""
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Daemon management CLI commands."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
3
5
|
import re
|
|
6
|
+
import sys
|
|
4
7
|
from pathlib import Path
|
|
5
8
|
from typing import Optional
|
|
6
9
|
|
|
@@ -792,6 +795,14 @@ def token_revoke(
|
|
|
792
795
|
raise typer.Exit(1)
|
|
793
796
|
|
|
794
797
|
|
|
798
|
+
def _reexec(start_cwd: str) -> None:
|
|
799
|
+
"""Replace this process with a fresh daemon; exec discards buffered log records, so flush first."""
|
|
800
|
+
console.print("[cyan]Restarting daemon...[/cyan]")
|
|
801
|
+
os.chdir(start_cwd)
|
|
802
|
+
logging.shutdown()
|
|
803
|
+
os.execv(sys.executable, [sys.executable, *sys.argv])
|
|
804
|
+
|
|
805
|
+
|
|
795
806
|
@daemon_app.callback(invoke_without_command=True)
|
|
796
807
|
def daemon_main(
|
|
797
808
|
ctx: typer.Context,
|
|
@@ -827,12 +838,20 @@ def daemon_main(
|
|
|
827
838
|
if log_to_console is not None:
|
|
828
839
|
overrides["log_to_console"] = log_to_console
|
|
829
840
|
|
|
841
|
+
restarting = False
|
|
842
|
+
# Agent code execution chdirs the whole process around, so the cwd at exec
|
|
843
|
+
# time is not necessarily the one the daemon was started from.
|
|
844
|
+
start_cwd = os.getcwd()
|
|
845
|
+
|
|
830
846
|
try:
|
|
831
|
-
asyncio.run(run_daemon(config, config_overrides=overrides or None))
|
|
847
|
+
restarting = asyncio.run(run_daemon(config, config_overrides=overrides or None))
|
|
832
848
|
except (KeyboardInterrupt, SystemExit, RuntimeError):
|
|
833
849
|
pass
|
|
834
850
|
finally:
|
|
835
851
|
console.print("\n[yellow]Daemon stopped[/yellow]")
|
|
836
852
|
|
|
853
|
+
if restarting:
|
|
854
|
+
_reexec(start_cwd)
|
|
855
|
+
|
|
837
856
|
|
|
838
857
|
__all__ = ["daemon_app"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Callable, Dict, List, Optional
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
9
9
|
|
|
@@ -149,6 +149,7 @@ class Config(BaseModel):
|
|
|
149
149
|
skill_paths: List[str] = Field(default_factory=list)
|
|
150
150
|
agent_paths: List[str] = Field(default_factory=list)
|
|
151
151
|
skill_ttl_default: int = 10
|
|
152
|
+
plugins: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
|
|
152
153
|
secrets: Optional[SecretsConfig] = None
|
|
153
154
|
history: Optional[HistoryConfig] = None
|
|
154
155
|
sandbox: Optional[SandboxSettings] = None
|
|
@@ -152,6 +152,25 @@ def _has_bare_python_fence(cleaned: str) -> bool:
|
|
|
152
152
|
return _BARE_PYTHON_FENCE.search(cleaned) is not None
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
# Tool-call envelopes a model may emit as text instead of a ```python-exec
|
|
156
|
+
# block. Nothing here executes them, so such a reply did no work. An envelope
|
|
157
|
+
# carries structure (`<invoke name="...">`, or an opening tag closed by its own
|
|
158
|
+
# counterpart); prose naming the tags is not one.
|
|
159
|
+
_TOOL_CALL_MARKUP = re.compile(
|
|
160
|
+
r"<(?:[a-z]+:)?invoke\s+name\s*="
|
|
161
|
+
r"|<(?:[a-z]+:)?(function_calls|tool_call|tool_use)\b[^>]*>.*?</(?:[a-z]+:)?\1\s*>",
|
|
162
|
+
re.IGNORECASE | re.DOTALL,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Fenced blocks and inline spans - markup quoted as code is shown, not issued.
|
|
166
|
+
_CODE_MARKUP = re.compile(r"^```.*?^```|`[^`\n]+`", re.MULTILINE | re.DOTALL)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _has_tool_call_markup(content: str) -> bool:
|
|
170
|
+
"""True if the text contains a native tool-call envelope outside of code."""
|
|
171
|
+
return bool(content) and _TOOL_CALL_MARKUP.search(_CODE_MARKUP.sub("", content)) is not None
|
|
172
|
+
|
|
173
|
+
|
|
155
174
|
# Tags the runtime injects into the model's NEXT user message after executing
|
|
156
175
|
# its code (the execution result, multi-block warning, and budget hints). A
|
|
157
176
|
# well-behaved model never writes these itself; one that does is impersonating
|
|
@@ -198,6 +217,20 @@ def _build_spoofed_runtime_tag_warning() -> str:
|
|
|
198
217
|
)
|
|
199
218
|
|
|
200
219
|
|
|
220
|
+
def _build_unexecuted_tool_call_notice_xml() -> str:
|
|
221
|
+
"""Model-visible correction when a no-code response was built out of native
|
|
222
|
+
tool-call markup. Nothing in it ran, so its completion summary describes work
|
|
223
|
+
that never happened; this hands the turn back pointing at the fence that
|
|
224
|
+
actually executes."""
|
|
225
|
+
return (
|
|
226
|
+
"\n<tsugite_unexecuted_tool_call_notice>"
|
|
227
|
+
"Your response contained native tool-call markup. This runtime does not execute it - "
|
|
228
|
+
"nothing ran, and any result or count your response reported is not real. "
|
|
229
|
+
"Call the tools from Python instead: re-emit the work as exactly one ```python-exec block."
|
|
230
|
+
"</tsugite_unexecuted_tool_call_notice>"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
201
234
|
def _build_multi_block_warning_xml(count: int) -> str:
|
|
202
235
|
"""Model-visible note appended to a turn's observation when the agent
|
|
203
236
|
received N>1 ```python blocks in one response.
|
|
@@ -409,6 +442,8 @@ class TsugiteAgent:
|
|
|
409
442
|
last_response_text: str = ""
|
|
410
443
|
turn_num = 0
|
|
411
444
|
cancelled = False
|
|
445
|
+
corrected_tool_call_markup = False
|
|
446
|
+
unexecuted_tool_call_run = False
|
|
412
447
|
|
|
413
448
|
try:
|
|
414
449
|
for turn_num in range(self.max_turns):
|
|
@@ -467,9 +502,21 @@ class TsugiteAgent:
|
|
|
467
502
|
)
|
|
468
503
|
)
|
|
469
504
|
|
|
505
|
+
# Neutralizing the tag is silent otherwise, so a model impersonating
|
|
506
|
+
# the runtime leaves no trace for an operator reading logs or events.
|
|
507
|
+
if turn.spoofed_runtime_tag:
|
|
508
|
+
logger.warning("Response wrote runtime-only tags; neutralized them (step %d)", turn_num + 1)
|
|
509
|
+
if self.event_bus:
|
|
510
|
+
self.event_bus.emit(
|
|
511
|
+
WarningEvent(
|
|
512
|
+
message="Response wrote runtime-only tags; they were neutralized.",
|
|
513
|
+
category="spoofed_runtime_tag",
|
|
514
|
+
step=turn_num + 1,
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
|
|
470
518
|
# No code = the model is done. Its raw text is the answer.
|
|
471
519
|
if not code or not code.strip():
|
|
472
|
-
final_value = last_response_text
|
|
473
520
|
trailing_notice = ""
|
|
474
521
|
if multi_block_count:
|
|
475
522
|
trailing_notice += _build_multi_block_warning_xml(multi_block_count)
|
|
@@ -479,6 +526,10 @@ class TsugiteAgent:
|
|
|
479
526
|
# it wasn't executed. Nudge it toward the exec fence for next turn.
|
|
480
527
|
if turn.has_bare_python:
|
|
481
528
|
trailing_notice += _build_bare_python_notice_xml()
|
|
529
|
+
# Correct the model once rather than accept a fabricated summary.
|
|
530
|
+
unexecuted_tool_call = _has_tool_call_markup(last_response_text)
|
|
531
|
+
if unexecuted_tool_call:
|
|
532
|
+
trailing_notice += _build_unexecuted_tool_call_notice_xml()
|
|
482
533
|
self.memory.add_step(
|
|
483
534
|
thought=thought,
|
|
484
535
|
code="",
|
|
@@ -488,6 +539,22 @@ class TsugiteAgent:
|
|
|
488
539
|
raw_content=last_response_text,
|
|
489
540
|
xml_observation=trailing_notice or None,
|
|
490
541
|
)
|
|
542
|
+
if unexecuted_tool_call and not corrected_tool_call_markup:
|
|
543
|
+
corrected_tool_call_markup = True
|
|
544
|
+
if self.event_bus:
|
|
545
|
+
self.event_bus.emit(
|
|
546
|
+
WarningEvent(
|
|
547
|
+
message=(
|
|
548
|
+
"Response used tool-call markup, which is not executed; "
|
|
549
|
+
"asking the model to re-emit it as a python-exec block."
|
|
550
|
+
),
|
|
551
|
+
category="unexecuted_tool_call",
|
|
552
|
+
step=turn_num + 1,
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
continue
|
|
556
|
+
unexecuted_tool_call_run = unexecuted_tool_call
|
|
557
|
+
final_value = last_response_text
|
|
491
558
|
break
|
|
492
559
|
|
|
493
560
|
# Cooperative cancel checkpoint (before running a tool/code block):
|
|
@@ -575,6 +642,13 @@ class TsugiteAgent:
|
|
|
575
642
|
error_message = f"max_turns ({self.max_turns}) reached"
|
|
576
643
|
if self.event_bus:
|
|
577
644
|
self.event_bus.emit(WarningEvent(message=error_message, step=turn_num + 1))
|
|
645
|
+
# The model answered with tool-call markup again after being corrected,
|
|
646
|
+
# so the run finished having executed nothing it claimed to do.
|
|
647
|
+
elif unexecuted_tool_call_run:
|
|
648
|
+
status = "error"
|
|
649
|
+
error_message = "model answered with unexecuted tool-call markup; no code ran"
|
|
650
|
+
if self.event_bus:
|
|
651
|
+
self.event_bus.emit(WarningEvent(message=error_message, step=turn_num + 1))
|
|
578
652
|
else:
|
|
579
653
|
status = "success"
|
|
580
654
|
error_message = None
|
|
@@ -685,6 +759,7 @@ class TsugiteAgent:
|
|
|
685
759
|
duration_ms=duration_ms,
|
|
686
760
|
tools_called=list(exec_result.tools_called) if exec_result.tools_called else None,
|
|
687
761
|
tool_calls=tool_calls,
|
|
762
|
+
groups=[{k: _mask_str(v) for k, v in g.items()} for g in exec_result.groups] or None,
|
|
688
763
|
last_statement_type=exec_result.last_statement_type,
|
|
689
764
|
return_value_repr=return_value_repr,
|
|
690
765
|
return_value_type=return_value_type,
|
|
@@ -1438,6 +1513,14 @@ config = read_file("config.yaml")
|
|
|
1438
1513
|
print(config)
|
|
1439
1514
|
```
|
|
1440
1515
|
|
|
1516
|
+
When a block does several calls toward one goal, label them with `tsu_group`:
|
|
1517
|
+
|
|
1518
|
+
```python-exec
|
|
1519
|
+
with tsu_group("read the config files"):
|
|
1520
|
+
for name in ("config.yaml", "defaults.yaml"):
|
|
1521
|
+
print(name, len(read_file(name)))
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1441
1524
|
Only ```python-exec blocks are executed. A plain ```python block is treated as
|
|
1442
1525
|
illustration — it is shown to the user but NOT run — so you can quote or explain
|
|
1443
1526
|
Python without executing it.
|
|
@@ -1477,6 +1560,8 @@ the real result next turn.
|
|
|
1477
1560
|
Only JSON-serializable values.
|
|
1478
1561
|
- For a structured (non-string) return: `return_value({{"status": "ok"}})` — ends the run
|
|
1479
1562
|
and returns the value as-is. For a plain text answer, just stop using code blocks.
|
|
1563
|
+
- Group per phase of work, not per call: one `tsu_group` around several related calls,
|
|
1564
|
+
and none at all for a block that does one thing.
|
|
1480
1565
|
{tools_section}
|
|
1481
1566
|
## Rules
|
|
1482
1567
|
|
|
@@ -16,6 +16,8 @@ import pprint
|
|
|
16
16
|
import sys
|
|
17
17
|
import threading
|
|
18
18
|
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from contextlib import contextmanager
|
|
19
21
|
from dataclasses import dataclass, field
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
from typing import Any, Callable, Dict, Iterator, List, Optional, Protocol, runtime_checkable
|
|
@@ -74,6 +76,8 @@ MAX_EXECUTION_OUTPUT_KB = 50
|
|
|
74
76
|
# not model input, so they stay small.
|
|
75
77
|
TOOL_CALL_ARG_MAX = 500
|
|
76
78
|
TOOL_CALL_OUTPUT_MAX = 2000
|
|
79
|
+
# Titles are LLM-authored and nothing else bounds them before history.
|
|
80
|
+
GROUP_TITLE_MAX = 200
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
def _cap_text(text: str, limit: int) -> str:
|
|
@@ -106,6 +110,9 @@ class ExecutionResult:
|
|
|
106
110
|
# Per-call records ({tool, arguments, success, duration_ms, output|error}),
|
|
107
111
|
# capped via TOOL_CALL_ARG_MAX / TOOL_CALL_OUTPUT_MAX.
|
|
108
112
|
tool_calls: List[Dict[str, Any]] = field(default_factory=list)
|
|
113
|
+
# Named `tsu_group` sections opened this turn, in the order they opened:
|
|
114
|
+
# {group_id, title, parent_group_id, success, duration_ms, error?}.
|
|
115
|
+
groups: List[Dict[str, Any]] = field(default_factory=list)
|
|
109
116
|
variables_set: Dict[str, str] = field(default_factory=dict) # name -> "type(size)"
|
|
110
117
|
state_keys: Dict[str, str] = field(default_factory=dict) # persisted state: name -> "type(size)"
|
|
111
118
|
loaded_skills: Dict[str, str] = field(default_factory=dict) # name -> content
|
|
@@ -129,6 +136,13 @@ class ExecutionResult:
|
|
|
129
136
|
output = output[:max_bytes]
|
|
130
137
|
self.truncated = True
|
|
131
138
|
|
|
139
|
+
# An explicit return_value() is the other way a turn hands back megabytes;
|
|
140
|
+
# the last-expression path prints instead, so it already rides `output`.
|
|
141
|
+
return_value = None if self.return_value is None else _mask(str(self.return_value))
|
|
142
|
+
if return_value is not None and len(return_value) > max_bytes:
|
|
143
|
+
return_value = return_value[:max_bytes]
|
|
144
|
+
self.truncated = True
|
|
145
|
+
|
|
132
146
|
status = "error" if self.error else "success"
|
|
133
147
|
attrs = f'status="{status}"'
|
|
134
148
|
if duration_ms:
|
|
@@ -156,8 +170,8 @@ class ExecutionResult:
|
|
|
156
170
|
state_list = ", ".join(f"{escape(k)}={escape(_mask(v))}" for k, v in self.state_keys.items())
|
|
157
171
|
parts.append(f"<state>{state_list}</state>")
|
|
158
172
|
|
|
159
|
-
if
|
|
160
|
-
parts.append(f"<return_value>{escape(
|
|
173
|
+
if return_value is not None:
|
|
174
|
+
parts.append(f"<return_value>{escape(return_value)}</return_value>")
|
|
161
175
|
|
|
162
176
|
parts.append("</tsugite_execution_result>")
|
|
163
177
|
return "\n".join(parts)
|
|
@@ -328,6 +342,8 @@ class LocalExecutor:
|
|
|
328
342
|
self._return_value = None
|
|
329
343
|
self._tools_called = []
|
|
330
344
|
self._tool_calls: List[Dict[str, Any]] = []
|
|
345
|
+
self._group_stack: List[str] = []
|
|
346
|
+
self._groups: List[Dict[str, Any]] = []
|
|
331
347
|
self._loaded_skills_for_turn: Dict[str, str] = {}
|
|
332
348
|
self._unloaded_skills_for_turn: List[str] = []
|
|
333
349
|
self.workspace_dir = workspace_dir
|
|
@@ -377,6 +393,7 @@ class LocalExecutor:
|
|
|
377
393
|
return f"Message sent: {msg}"
|
|
378
394
|
|
|
379
395
|
ns["send_message"] = send_message
|
|
396
|
+
ns["tsu_group"] = self._tsu_group
|
|
380
397
|
|
|
381
398
|
def _blocked_open(*args, **kwargs):
|
|
382
399
|
raise RuntimeError(
|
|
@@ -475,6 +492,8 @@ class LocalExecutor:
|
|
|
475
492
|
self._return_value = None
|
|
476
493
|
self._tools_called = []
|
|
477
494
|
self._tool_calls = []
|
|
495
|
+
self._group_stack = []
|
|
496
|
+
self._groups = []
|
|
478
497
|
self._loaded_skills_for_turn = {}
|
|
479
498
|
self._unloaded_skills_for_turn = []
|
|
480
499
|
|
|
@@ -565,6 +584,7 @@ class LocalExecutor:
|
|
|
565
584
|
return_value=None if exec_error else self._return_value,
|
|
566
585
|
tools_called=self._tools_called.copy(),
|
|
567
586
|
tool_calls=[dict(c) for c in self._tool_calls],
|
|
587
|
+
groups=[dict(g) for g in self._groups],
|
|
568
588
|
variables_set=variables_set,
|
|
569
589
|
state_keys=state_keys,
|
|
570
590
|
loaded_skills=self._loaded_skills_for_turn.copy(),
|
|
@@ -619,6 +639,50 @@ class LocalExecutor:
|
|
|
619
639
|
wrappers = {t.name: self._make_tool_wrapper(t) for t in tools if t.name not in EXECUTOR_BUILTIN_TOOLS}
|
|
620
640
|
self.register_tools(wrappers)
|
|
621
641
|
|
|
642
|
+
@contextmanager
|
|
643
|
+
def _tsu_group(self, title: str):
|
|
644
|
+
"""Bracket the block as a named group on the event stream and the execution result."""
|
|
645
|
+
from tsugite.events import ExecutionGroupEndEvent, ExecutionGroupStartEvent
|
|
646
|
+
|
|
647
|
+
record: Dict[str, Any] = {
|
|
648
|
+
"group_id": uuid.uuid4().hex[:12],
|
|
649
|
+
"title": _cap_text(str(title), GROUP_TITLE_MAX),
|
|
650
|
+
"parent_group_id": self._current_group(),
|
|
651
|
+
}
|
|
652
|
+
self._groups.append(record)
|
|
653
|
+
if self.event_bus:
|
|
654
|
+
self.event_bus.emit(
|
|
655
|
+
ExecutionGroupStartEvent(
|
|
656
|
+
group_id=record["group_id"],
|
|
657
|
+
title=record["title"],
|
|
658
|
+
parent_group_id=record["parent_group_id"],
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
self._group_stack.append(record["group_id"])
|
|
662
|
+
t0 = time.perf_counter()
|
|
663
|
+
try:
|
|
664
|
+
yield
|
|
665
|
+
record["success"] = True
|
|
666
|
+
except BaseException as exc:
|
|
667
|
+
record["success"] = False
|
|
668
|
+
record["error"] = _cap_text(f"{type(exc).__name__}: {exc}", TOOL_CALL_ARG_MAX)
|
|
669
|
+
raise
|
|
670
|
+
finally:
|
|
671
|
+
self._group_stack.pop()
|
|
672
|
+
record["duration_ms"] = int((time.perf_counter() - t0) * 1000)
|
|
673
|
+
if self.event_bus:
|
|
674
|
+
self.event_bus.emit(
|
|
675
|
+
ExecutionGroupEndEvent(
|
|
676
|
+
group_id=record["group_id"],
|
|
677
|
+
success=record.get("success", False),
|
|
678
|
+
duration_ms=record["duration_ms"],
|
|
679
|
+
error=record.get("error"),
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def _current_group(self) -> Optional[str]:
|
|
684
|
+
return self._group_stack[-1] if self._group_stack else None
|
|
685
|
+
|
|
622
686
|
def _make_tool_wrapper(self, tool_obj):
|
|
623
687
|
"""Build the sync namespace wrapper that calls a tool's async execute()."""
|
|
624
688
|
from tsugite.events import ToolCallEvent, ToolResultEvent
|
|
@@ -630,10 +694,13 @@ class LocalExecutor:
|
|
|
630
694
|
# capped repr() strings, and a token baked into one of those is no
|
|
631
695
|
# longer reachable by key or path.
|
|
632
696
|
audit_args = redact_sensitive_obj(kwargs, getattr(tool_obj, "sensitive_paths", ()))
|
|
697
|
+
group = self._current_group()
|
|
633
698
|
record: Dict[str, Any] = {"tool": tool_obj.name, "arguments": _jsonable_call_args(audit_args)}
|
|
699
|
+
if group:
|
|
700
|
+
record["group_id"] = group
|
|
634
701
|
self._tool_calls.append(record)
|
|
635
702
|
if self.event_bus:
|
|
636
|
-
self.event_bus.emit(ToolCallEvent(tool_name=tool_obj.name, arguments=audit_args))
|
|
703
|
+
self.event_bus.emit(ToolCallEvent(tool_name=tool_obj.name, arguments=audit_args, group_id=group))
|
|
637
704
|
t0 = time.perf_counter()
|
|
638
705
|
try:
|
|
639
706
|
result = run_async_in_sync_context(tool_obj.execute(**kwargs))
|