tsugite-cli 0.23.1__tar.gz → 0.24.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.23.1 → tsugite_cli-0.24.0}/PKG-INFO +1 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/pyproject.toml +1 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_preparation.py +6 -4
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/history_integration.py +30 -25
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/runner.py +10 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/steps.py +1 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/agent_config.py +14 -14
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/scheduling/SKILL.md +2 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/agents.py +2 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/daemon.py +64 -83
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/history.py +2 -2
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/run.py +2 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/agent.py +20 -6
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/executor.py +9 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/subprocess_executor.py +12 -0
- tsugite_cli-0.24.0/tsugite/daemon_client.py +201 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/helpers.py +14 -3
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/models.py +1 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/sqlite_backend.py +17 -7
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/permissions.py +31 -3
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/anthropic.py +6 -3
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/model_registry.py +8 -4
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/openai_compat.py +5 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/skill_discovery.py +6 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/__init__.py +15 -0
- tsugite_cli-0.24.0/tsugite/tools/daemon_control.py +107 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/http.py +27 -2
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/jobs.py +21 -2
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/skills.py +19 -13
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/jsonl.py +14 -1
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/usage/quota.py +8 -10
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/utils.py +1 -0
- tsugite_cli-0.23.1/tsugite/tools/daemon_control.py +0 -60
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/.gitignore +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/AGENTS.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/LICENSE +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/README.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/approval.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/config.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/models.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/artifacts.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/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()
|
|
@@ -240,7 +242,7 @@ class AgentPreparer:
|
|
|
240
242
|
auto_load_skills = [s for s in (agent_config.auto_load_skills or []) if s not in suppressed_skills]
|
|
241
243
|
|
|
242
244
|
for skill_name in auto_load_skills:
|
|
243
|
-
result = skill_manager.load_skill(skill_name)
|
|
245
|
+
result = skill_manager.load_skill(skill_name, explicit=True)
|
|
244
246
|
if result.startswith("Failed") or result.startswith("Skill '"):
|
|
245
247
|
if event_bus:
|
|
246
248
|
event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
|
|
@@ -266,7 +268,7 @@ class AgentPreparer:
|
|
|
266
268
|
if name in auto_load_skills:
|
|
267
269
|
# Already loaded above; no need to double-load but it's still sticky.
|
|
268
270
|
continue
|
|
269
|
-
result = skill_manager.load_skill(name)
|
|
271
|
+
result = skill_manager.load_skill(name, explicit=True)
|
|
270
272
|
if result.startswith("Failed") or result.startswith("Skill '"):
|
|
271
273
|
if event_bus:
|
|
272
274
|
event_bus.emit(SkillLoadFailedEvent(skill_name=name, error_message=result))
|
|
@@ -73,7 +73,11 @@ def open_or_create_session(
|
|
|
73
73
|
backend = get_history_backend()
|
|
74
74
|
if continue_conversation_id:
|
|
75
75
|
if backend.exists(continue_conversation_id):
|
|
76
|
-
|
|
76
|
+
session = backend.load(continue_conversation_id)
|
|
77
|
+
# Daemon-spawned sessions are created without a model.
|
|
78
|
+
if session.summary().model is None:
|
|
79
|
+
session.record("session_start", agent=agent_name, model=model, workspace=workspace)
|
|
80
|
+
return session
|
|
77
81
|
return backend.create(
|
|
78
82
|
agent_name=agent_name,
|
|
79
83
|
model=model,
|
|
@@ -92,14 +96,11 @@ def _current_turn_already_has_user_input(storage: Session) -> bool:
|
|
|
92
96
|
session_end after it; the post-hoc save_run_to_history on the error path
|
|
93
97
|
would otherwise record a second, identical one (a duplicate user bubble).
|
|
94
98
|
A genuinely repeated message opens a new turn *after* the prior session_end,
|
|
95
|
-
so this stays False for it.
|
|
99
|
+
so this stays False for it. A final_result also ends a turn copied without
|
|
100
|
+
its session_end (branches, older compacted sessions).
|
|
96
101
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return False
|
|
100
|
-
if event.type == "user_input":
|
|
101
|
-
return True
|
|
102
|
-
return False
|
|
102
|
+
events = list(storage.iter_events(types=("user_input", "session_end", "final_result")))
|
|
103
|
+
return bool(events) and events[-1].type == "user_input"
|
|
103
104
|
|
|
104
105
|
|
|
105
106
|
def record_user_input(
|
|
@@ -132,8 +133,9 @@ def record_user_input(
|
|
|
132
133
|
data["attachments"] = [
|
|
133
134
|
{"name": a.name, "type": a.content_type.value, "source_url": a.source_url} for a in uploads
|
|
134
135
|
]
|
|
135
|
-
if
|
|
136
|
-
|
|
136
|
+
channel = {k: v for k, v in (channel_metadata or {}).items() if k != "tsugite_token"}
|
|
137
|
+
if channel:
|
|
138
|
+
data["channel"] = channel
|
|
137
139
|
# Structured client context, recorded alongside the folded <client_context> XML
|
|
138
140
|
# in `text` so the UI reads it back without re-parsing the prompt string.
|
|
139
141
|
if client_context_items:
|
|
@@ -247,8 +249,8 @@ def save_run_to_history(
|
|
|
247
249
|
agent_already_recorded = last_model_response > last_session_start
|
|
248
250
|
|
|
249
251
|
if agent_already_recorded:
|
|
250
|
-
# A continued conversation
|
|
251
|
-
# turn starts at its own user_input.
|
|
252
|
+
# A continued conversation's session_start can sit at index 0 or come from
|
|
253
|
+
# a backfill. The current turn starts at its own user_input.
|
|
252
254
|
turn_start = max(last_session_start, last_user_input)
|
|
253
255
|
if last_session_end < turn_start:
|
|
254
256
|
record_session_end(storage, status=status, error_message=error_message)
|
|
@@ -267,19 +269,20 @@ def save_run_to_history(
|
|
|
267
269
|
tools_called=list(getattr(step, "tools_called", []) or []),
|
|
268
270
|
)
|
|
269
271
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
272
|
+
if status == "success":
|
|
273
|
+
state_delta = provider_state if provider_state else None
|
|
274
|
+
# Split as the agent loop records it: bare id in `model`, prefix in `provider`.
|
|
275
|
+
model_provider, _, model_id = model.partition(":")
|
|
276
|
+
storage.record(
|
|
277
|
+
"model_response",
|
|
278
|
+
provider=model_provider if model_id else None,
|
|
279
|
+
model=model_id or model,
|
|
280
|
+
raw_content=result or "",
|
|
281
|
+
usage={"total_tokens": token_count} if token_count else None,
|
|
282
|
+
cost=cost,
|
|
283
|
+
duration_ms=duration_ms,
|
|
284
|
+
state_delta=state_delta,
|
|
285
|
+
)
|
|
283
286
|
|
|
284
287
|
record_session_end(storage, status=status, error_message=error_message)
|
|
285
288
|
return storage.session_id
|
|
@@ -291,6 +294,7 @@ class ResumableSessionState:
|
|
|
291
294
|
|
|
292
295
|
session_id: str
|
|
293
296
|
compacted: bool = False
|
|
297
|
+
cumulative_cost: float = 0.0
|
|
294
298
|
|
|
295
299
|
|
|
296
300
|
def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessionState]:
|
|
@@ -327,6 +331,7 @@ def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessi
|
|
|
327
331
|
return ResumableSessionState(
|
|
328
332
|
session_id=session_id,
|
|
329
333
|
compacted=bool(state.get("compacted", False)),
|
|
334
|
+
cumulative_cost=state.get("cumulative_cost") or 0.0,
|
|
330
335
|
)
|
|
331
336
|
return None
|
|
332
337
|
except Exception:
|
|
@@ -395,6 +395,7 @@ async def _execute_agent_with_prompt(
|
|
|
395
395
|
path_context: Optional[Any] = None,
|
|
396
396
|
resume_session: Optional[str] = None,
|
|
397
397
|
resume_after_compaction: bool = False,
|
|
398
|
+
resume_cost_baseline: float = 0.0,
|
|
398
399
|
hook_vars: Optional[Dict[str, str]] = None,
|
|
399
400
|
continue_conversation_id: Optional[str] = None,
|
|
400
401
|
user_input_for_history: Optional[str] = None,
|
|
@@ -610,9 +611,11 @@ async def _execute_agent_with_prompt(
|
|
|
610
611
|
previous_messages=previous_messages,
|
|
611
612
|
resume_session=resume_session,
|
|
612
613
|
resume_after_compaction=resume_after_compaction,
|
|
614
|
+
resume_cost_baseline=resume_cost_baseline,
|
|
613
615
|
hook_vars=hook_vars,
|
|
614
616
|
storage=session_storage,
|
|
615
617
|
pre_llm_call=pre_llm_call_cb,
|
|
618
|
+
expects_action=(channel_metadata or {}).get("source") == "scheduler",
|
|
616
619
|
)
|
|
617
620
|
# Set event_bus in context so tools can access it during execution
|
|
618
621
|
_setup_event_context(event_bus)
|
|
@@ -867,6 +870,7 @@ class _RunSetup:
|
|
|
867
870
|
previous_messages: List[Dict] = field(default_factory=list)
|
|
868
871
|
resume_session: Optional[str] = None
|
|
869
872
|
resume_after_compaction: bool = False
|
|
873
|
+
resume_cost_baseline: float = 0.0
|
|
870
874
|
conversation_id: Optional[str] = None
|
|
871
875
|
|
|
872
876
|
|
|
@@ -958,6 +962,7 @@ async def _run_unit(
|
|
|
958
962
|
previous_messages=setup.previous_messages,
|
|
959
963
|
resume_session=setup.resume_session,
|
|
960
964
|
resume_after_compaction=setup.resume_after_compaction,
|
|
965
|
+
resume_cost_baseline=setup.resume_cost_baseline,
|
|
961
966
|
**execute_kwargs,
|
|
962
967
|
)
|
|
963
968
|
except (RuntimeError, AgentExecutionError) as e:
|
|
@@ -1151,6 +1156,7 @@ async def run_agent_async(
|
|
|
1151
1156
|
previous_messages = []
|
|
1152
1157
|
resume_session = None
|
|
1153
1158
|
resume_after_compaction = False
|
|
1159
|
+
resume_cost_baseline = 0.0
|
|
1154
1160
|
if continue_conversation_id:
|
|
1155
1161
|
from tsugite.agent_runner.history_integration import (
|
|
1156
1162
|
get_resumable_session_state,
|
|
@@ -1162,6 +1168,7 @@ async def run_agent_async(
|
|
|
1162
1168
|
if session_info:
|
|
1163
1169
|
resume_session = session_info.session_id
|
|
1164
1170
|
resume_after_compaction = session_info.compacted
|
|
1171
|
+
resume_cost_baseline = session_info.cumulative_cost
|
|
1165
1172
|
logger.info(
|
|
1166
1173
|
"Resuming provider session %s (compacted=%s)",
|
|
1167
1174
|
resume_session,
|
|
@@ -1210,6 +1217,7 @@ async def run_agent_async(
|
|
|
1210
1217
|
previous_messages=previous_messages,
|
|
1211
1218
|
resume_session=resume_session,
|
|
1212
1219
|
resume_after_compaction=resume_after_compaction,
|
|
1220
|
+
resume_cost_baseline=resume_cost_baseline,
|
|
1213
1221
|
conversation_id=continue_conversation_id,
|
|
1214
1222
|
)
|
|
1215
1223
|
|
|
@@ -1281,6 +1289,7 @@ def preview_multistep_agent(
|
|
|
1281
1289
|
|
|
1282
1290
|
# Extract steps
|
|
1283
1291
|
from tsugite.md_agents import extract_step_directives, has_step_directives
|
|
1292
|
+
from tsugite.tools import format_tool_specs
|
|
1284
1293
|
|
|
1285
1294
|
if not has_step_directives(agent.content):
|
|
1286
1295
|
output("[yellow]This is a single-step agent (no step directives).[/yellow]", is_warning=True)
|
|
@@ -1302,7 +1311,7 @@ def preview_multistep_agent(
|
|
|
1302
1311
|
output(f"Prompt: {prompt}")
|
|
1303
1312
|
output(f"Steps: {len(steps)}")
|
|
1304
1313
|
output(f"Model: {resolve_effective_model(agent_model=agent.config.model) or 'unknown'}")
|
|
1305
|
-
output(f"Tools: {
|
|
1314
|
+
output(f"Tools: {format_tool_specs(agent.config.tools) if agent.config.tools else 'None'}")
|
|
1306
1315
|
output("")
|
|
1307
1316
|
|
|
1308
1317
|
# Show steps in table format
|
|
@@ -17,6 +17,12 @@ from tsugite.utils import has_glob_chars
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _warn_skipped(subject: str, detail: str) -> None:
|
|
21
|
+
from tsugite.events.helpers import emit_warning_event
|
|
22
|
+
|
|
23
|
+
emit_warning_event(f"Agent attachment skipped: {subject} ({detail})")
|
|
24
|
+
|
|
25
|
+
|
|
20
26
|
def _render_path(template: str, template_vars: Dict[str, Any]) -> Optional[str]:
|
|
21
27
|
"""Render a Jinja path template; return None on render failure."""
|
|
22
28
|
from tsugite.renderer import AgentRenderer
|
|
@@ -24,7 +30,7 @@ def _render_path(template: str, template_vars: Dict[str, Any]) -> Optional[str]:
|
|
|
24
30
|
try:
|
|
25
31
|
return AgentRenderer().render_string(template, template_vars)
|
|
26
32
|
except Exception as e:
|
|
27
|
-
|
|
33
|
+
_warn_skipped(template, f"path template failed to render: {e}")
|
|
28
34
|
return None
|
|
29
35
|
|
|
30
36
|
|
|
@@ -124,12 +130,10 @@ def _resolve_string_item(
|
|
|
124
130
|
return
|
|
125
131
|
resolved = _resolve_path(rendered, workspace_path)
|
|
126
132
|
if not resolved.exists():
|
|
127
|
-
|
|
133
|
+
_warn_skipped(template, f"not found: {resolved}")
|
|
128
134
|
return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
attachments.append(att)
|
|
132
|
-
logger.debug("Loaded agent attachment: %s", resolved)
|
|
135
|
+
attachments.append(file_handler.fetch(str(resolved)))
|
|
136
|
+
logger.debug("Loaded agent attachment: %s", resolved)
|
|
133
137
|
|
|
134
138
|
|
|
135
139
|
def _resolve_spec(
|
|
@@ -161,13 +165,12 @@ def _resolve_spec(
|
|
|
161
165
|
if not spec.attach and spec.assign:
|
|
162
166
|
try:
|
|
163
167
|
content = path.read_text(encoding="utf-8")
|
|
164
|
-
except (OSError, UnicodeDecodeError):
|
|
168
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
169
|
+
_warn_skipped(str(path), f"unreadable: {e}")
|
|
165
170
|
continue
|
|
166
171
|
bindings[spec.assign].append({"path": str(path), "content": content})
|
|
167
172
|
continue
|
|
168
173
|
att = file_handler.fetch(str(path))
|
|
169
|
-
if not att:
|
|
170
|
-
continue
|
|
171
174
|
if spec.attach:
|
|
172
175
|
attachments.append(att)
|
|
173
176
|
if spec.assign and att.content_type == AttachmentContentType.TEXT:
|
|
@@ -178,14 +181,11 @@ def _resolve_spec(
|
|
|
178
181
|
if not resolved.exists():
|
|
179
182
|
if spec.assign:
|
|
180
183
|
bindings[spec.assign] = None
|
|
181
|
-
|
|
184
|
+
else:
|
|
185
|
+
_warn_skipped(spec.path, f"not found: {resolved}")
|
|
182
186
|
return
|
|
183
187
|
|
|
184
188
|
att = file_handler.fetch(str(resolved))
|
|
185
|
-
if not att:
|
|
186
|
-
if spec.assign:
|
|
187
|
-
bindings[spec.assign] = None
|
|
188
|
-
return
|
|
189
189
|
if spec.attach:
|
|
190
190
|
attachments.append(att)
|
|
191
191
|
if spec.assign:
|
|
@@ -178,6 +178,8 @@ print(schedules)
|
|
|
178
178
|
|
|
179
179
|
Each schedule includes: `id`, `execution_type`, `schedule_type`, `enabled`, `next_run`, `last_run`, `last_status`, `last_error`.
|
|
180
180
|
|
|
181
|
+
`last_status` is `success`, `error`, `skipped`, or `no_action`.
|
|
182
|
+
|
|
181
183
|
## Managing Schedules
|
|
182
184
|
|
|
183
185
|
```python-exec
|
|
@@ -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}")
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
|
+
from contextlib import contextmanager
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Optional
|
|
9
10
|
|
|
@@ -11,15 +12,14 @@ import typer
|
|
|
11
12
|
from rich.console import Console
|
|
12
13
|
from rich.panel import Panel
|
|
13
14
|
|
|
14
|
-
from tsugite.
|
|
15
|
+
from tsugite.daemon_client import DaemonClient, DaemonError
|
|
15
16
|
|
|
16
17
|
daemon_app = typer.Typer(help="Daemon management commands")
|
|
17
18
|
console = Console()
|
|
18
19
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
PORT_OPTION = typer.Option(DEFAULT_DAEMON_PORT, "--port", "-p", envvar="TSUGITE_DAEMON_PORT", help="Daemon HTTP port")
|
|
20
|
+
# Precedence: flag > TSUGITE_DAEMON_HOST/PORT > TSUGITE_DAEMON_URL > default.
|
|
21
|
+
HOST_OPTION = typer.Option(None, "--host", envvar="TSUGITE_DAEMON_HOST", help="Daemon HTTP host")
|
|
22
|
+
PORT_OPTION = typer.Option(None, "--port", "-p", envvar="TSUGITE_DAEMON_PORT", help="Daemon HTTP port")
|
|
23
23
|
TOKEN_OPTION = typer.Option(None, "--token", "-t", envvar="TSUGITE_DAEMON_TOKEN", help="Auth token")
|
|
24
24
|
|
|
25
25
|
|
|
@@ -363,43 +363,25 @@ def init_daemon(
|
|
|
363
363
|
_show_next_steps(token_file, bot_name)
|
|
364
364
|
|
|
365
365
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
Returns the parsed JSON response body on success, or exits on failure.
|
|
370
|
-
"""
|
|
371
|
-
import httpx
|
|
372
|
-
|
|
373
|
-
url = f"http://{host}:{port}{path}"
|
|
374
|
-
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
375
|
-
|
|
366
|
+
@contextmanager
|
|
367
|
+
def _daemon_errors():
|
|
368
|
+
"""Print a daemon failure and exit 1."""
|
|
376
369
|
try:
|
|
377
|
-
|
|
378
|
-
except
|
|
379
|
-
console.print(f"[red]
|
|
380
|
-
raise typer.Exit(1)
|
|
381
|
-
|
|
382
|
-
if resp.status_code >= 400:
|
|
383
|
-
try:
|
|
384
|
-
msg = resp.json().get("error", resp.text)
|
|
385
|
-
except Exception:
|
|
386
|
-
msg = resp.text
|
|
387
|
-
console.print(f"[red]Error ({resp.status_code}):[/red] {msg}")
|
|
370
|
+
yield
|
|
371
|
+
except DaemonError as e:
|
|
372
|
+
console.print(f"[red]Error ({e.status}):[/red] {e}" if e.status else f"[red]{e}[/red]")
|
|
388
373
|
raise typer.Exit(1)
|
|
389
374
|
|
|
390
|
-
return resp.json()
|
|
391
|
-
|
|
392
375
|
|
|
393
376
|
@daemon_app.command("sessions")
|
|
394
377
|
def list_sessions(
|
|
395
|
-
host: str = HOST_OPTION,
|
|
396
|
-
port: int = PORT_OPTION,
|
|
378
|
+
host: Optional[str] = HOST_OPTION,
|
|
379
|
+
port: Optional[int] = PORT_OPTION,
|
|
397
380
|
token: Optional[str] = TOKEN_OPTION,
|
|
398
381
|
):
|
|
399
382
|
"""List active daemon sessions."""
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
sessions = data.get("sessions", [])
|
|
383
|
+
with _daemon_errors():
|
|
384
|
+
sessions = DaemonClient(host, port, token).list_chat_sessions()
|
|
403
385
|
if not sessions:
|
|
404
386
|
console.print("No active sessions")
|
|
405
387
|
return
|
|
@@ -418,20 +400,13 @@ def list_sessions(
|
|
|
418
400
|
@daemon_app.command("compact")
|
|
419
401
|
def compact_session(
|
|
420
402
|
user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID to compact"),
|
|
421
|
-
host: str = HOST_OPTION,
|
|
422
|
-
port: int = PORT_OPTION,
|
|
403
|
+
host: Optional[str] = HOST_OPTION,
|
|
404
|
+
port: Optional[int] = PORT_OPTION,
|
|
423
405
|
token: Optional[str] = TOKEN_OPTION,
|
|
424
406
|
):
|
|
425
407
|
"""Force compact a daemon session."""
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
host,
|
|
429
|
-
port,
|
|
430
|
-
"/api/chat/compact",
|
|
431
|
-
token,
|
|
432
|
-
json={"user_id": user_id},
|
|
433
|
-
timeout=120,
|
|
434
|
-
)
|
|
408
|
+
with _daemon_errors():
|
|
409
|
+
data = DaemonClient(host, port, token).compact_chat(user_id)
|
|
435
410
|
console.print(f"[green]✓[/green] Session compacted for user [cyan]{user_id}[/cyan]")
|
|
436
411
|
console.print(f" old: {data['old_conversation_id']}")
|
|
437
412
|
console.print(f" new: {data['new_conversation_id']}")
|
|
@@ -448,41 +423,40 @@ daemon_app.add_typer(session_app, name="session")
|
|
|
448
423
|
@session_app.command("set-primary")
|
|
449
424
|
def session_set_primary(
|
|
450
425
|
session_id: str = typer.Argument(help="Session ID to mark as primary"),
|
|
451
|
-
host: str = HOST_OPTION,
|
|
452
|
-
port: int = PORT_OPTION,
|
|
426
|
+
host: Optional[str] = HOST_OPTION,
|
|
427
|
+
port: Optional[int] = PORT_OPTION,
|
|
453
428
|
token: Optional[str] = TOKEN_OPTION,
|
|
454
429
|
):
|
|
455
430
|
"""Mark a session as the user's primary for its agent."""
|
|
456
|
-
|
|
431
|
+
with _daemon_errors():
|
|
432
|
+
DaemonClient(host, port, token).set_primary_session(session_id)
|
|
457
433
|
console.print(f"[green]✓[/green] Session [cyan]{session_id}[/cyan] is now primary")
|
|
458
434
|
|
|
459
435
|
|
|
460
436
|
@session_app.command("clear-primary")
|
|
461
437
|
def session_clear_primary(
|
|
462
438
|
user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID"),
|
|
463
|
-
host: str = HOST_OPTION,
|
|
464
|
-
port: int = PORT_OPTION,
|
|
439
|
+
host: Optional[str] = HOST_OPTION,
|
|
440
|
+
port: Optional[int] = PORT_OPTION,
|
|
465
441
|
token: Optional[str] = TOKEN_OPTION,
|
|
466
442
|
):
|
|
467
443
|
"""Remove the primary flag from any of the user's sessions."""
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
path = f"/api/sessions/clear-primary?{urlencode({'user_id': user_id})}"
|
|
471
|
-
_daemon_request("POST", host, port, path, token)
|
|
444
|
+
with _daemon_errors():
|
|
445
|
+
DaemonClient(host, port, token).clear_primary_session(user_id)
|
|
472
446
|
console.print(f"[green]✓[/green] Cleared primary for user [cyan]{user_id}[/cyan]")
|
|
473
447
|
|
|
474
448
|
|
|
475
449
|
@session_app.command("list")
|
|
476
450
|
def session_list(
|
|
477
|
-
host: str = HOST_OPTION,
|
|
478
|
-
port: int = PORT_OPTION,
|
|
451
|
+
host: Optional[str] = HOST_OPTION,
|
|
452
|
+
port: Optional[int] = PORT_OPTION,
|
|
479
453
|
token: Optional[str] = TOKEN_OPTION,
|
|
480
454
|
):
|
|
481
455
|
"""List all daemon sessions, indicating which one is primary."""
|
|
482
456
|
from rich.table import Table
|
|
483
457
|
|
|
484
|
-
|
|
485
|
-
|
|
458
|
+
with _daemon_errors():
|
|
459
|
+
sessions = DaemonClient(host, port, token).list_sessions()
|
|
486
460
|
if not sessions:
|
|
487
461
|
console.print("No sessions")
|
|
488
462
|
return
|
|
@@ -520,15 +494,15 @@ def _parse_every_to_cron(every: str) -> str:
|
|
|
520
494
|
|
|
521
495
|
@schedule_app.command("list")
|
|
522
496
|
def schedule_list(
|
|
523
|
-
host: str = HOST_OPTION,
|
|
524
|
-
port: int = PORT_OPTION,
|
|
497
|
+
host: Optional[str] = HOST_OPTION,
|
|
498
|
+
port: Optional[int] = PORT_OPTION,
|
|
525
499
|
token: Optional[str] = TOKEN_OPTION,
|
|
526
500
|
):
|
|
527
501
|
"""List all schedules."""
|
|
528
502
|
from rich.table import Table
|
|
529
503
|
|
|
530
|
-
|
|
531
|
-
|
|
504
|
+
with _daemon_errors():
|
|
505
|
+
schedules = DaemonClient(host, port, token).list_schedules()
|
|
532
506
|
if not schedules:
|
|
533
507
|
console.print("No schedules configured")
|
|
534
508
|
return
|
|
@@ -545,7 +519,7 @@ def schedule_list(
|
|
|
545
519
|
sched_str = s.get("cron_expr") or s.get("run_at") or ""
|
|
546
520
|
enabled = "[green]yes[/green]" if s.get("enabled") else "[red]no[/red]"
|
|
547
521
|
status = s.get("last_status") or "-"
|
|
548
|
-
if status
|
|
522
|
+
if status in ("error", "no_action"):
|
|
549
523
|
status = f"[red]{status}[/red]"
|
|
550
524
|
elif status == "success":
|
|
551
525
|
status = f"[green]{status}[/green]"
|
|
@@ -572,8 +546,8 @@ def schedule_add(
|
|
|
572
546
|
"--target-session",
|
|
573
547
|
help="Where the inject_history turn lands: primary, originating, none, name:<n>, or <session_id>",
|
|
574
548
|
),
|
|
575
|
-
host: str = HOST_OPTION,
|
|
576
|
-
port: int = PORT_OPTION,
|
|
549
|
+
host: Optional[str] = HOST_OPTION,
|
|
550
|
+
port: Optional[int] = PORT_OPTION,
|
|
577
551
|
token: Optional[str] = TOKEN_OPTION,
|
|
578
552
|
):
|
|
579
553
|
"""Add a new schedule."""
|
|
@@ -601,7 +575,8 @@ def schedule_add(
|
|
|
601
575
|
if target_session:
|
|
602
576
|
body["target_session"] = target_session
|
|
603
577
|
|
|
604
|
-
|
|
578
|
+
with _daemon_errors():
|
|
579
|
+
data = DaemonClient(host, port, token).create_schedule(**body)
|
|
605
580
|
console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] created")
|
|
606
581
|
if data.get("next_run"):
|
|
607
582
|
console.print(f" next run: {data['next_run']}")
|
|
@@ -609,12 +584,13 @@ def schedule_add(
|
|
|
609
584
|
|
|
610
585
|
@schedule_app.command("cleanup")
|
|
611
586
|
def schedule_cleanup(
|
|
612
|
-
host: str = HOST_OPTION,
|
|
613
|
-
port: int = PORT_OPTION,
|
|
587
|
+
host: Optional[str] = HOST_OPTION,
|
|
588
|
+
port: Optional[int] = PORT_OPTION,
|
|
614
589
|
token: Optional[str] = TOKEN_OPTION,
|
|
615
590
|
):
|
|
616
591
|
"""Remove all orphaned one-off schedules (disabled, already fired)."""
|
|
617
|
-
|
|
592
|
+
with _daemon_errors():
|
|
593
|
+
data = DaemonClient(host, port, token).cleanup_schedules()
|
|
618
594
|
removed = data.get("removed", [])
|
|
619
595
|
if not removed:
|
|
620
596
|
console.print("Nothing to clean up")
|
|
@@ -627,60 +603,65 @@ def schedule_cleanup(
|
|
|
627
603
|
@schedule_app.command("remove")
|
|
628
604
|
def schedule_remove(
|
|
629
605
|
schedule_id: str = typer.Argument(help="Schedule ID to remove"),
|
|
630
|
-
host: str = HOST_OPTION,
|
|
631
|
-
port: int = PORT_OPTION,
|
|
606
|
+
host: Optional[str] = HOST_OPTION,
|
|
607
|
+
port: Optional[int] = PORT_OPTION,
|
|
632
608
|
token: Optional[str] = TOKEN_OPTION,
|
|
633
609
|
):
|
|
634
610
|
"""Remove a schedule."""
|
|
635
|
-
|
|
611
|
+
with _daemon_errors():
|
|
612
|
+
DaemonClient(host, port, token).delete_schedule(schedule_id)
|
|
636
613
|
console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] removed")
|
|
637
614
|
|
|
638
615
|
|
|
639
616
|
@schedule_app.command("enable")
|
|
640
617
|
def schedule_enable(
|
|
641
618
|
schedule_id: str = typer.Argument(help="Schedule ID"),
|
|
642
|
-
host: str = HOST_OPTION,
|
|
643
|
-
port: int = PORT_OPTION,
|
|
619
|
+
host: Optional[str] = HOST_OPTION,
|
|
620
|
+
port: Optional[int] = PORT_OPTION,
|
|
644
621
|
token: Optional[str] = TOKEN_OPTION,
|
|
645
622
|
):
|
|
646
623
|
"""Enable a schedule."""
|
|
647
|
-
|
|
624
|
+
with _daemon_errors():
|
|
625
|
+
DaemonClient(host, port, token).enable_schedule(schedule_id)
|
|
648
626
|
console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] enabled")
|
|
649
627
|
|
|
650
628
|
|
|
651
629
|
@schedule_app.command("disable")
|
|
652
630
|
def schedule_disable(
|
|
653
631
|
schedule_id: str = typer.Argument(help="Schedule ID"),
|
|
654
|
-
host: str = HOST_OPTION,
|
|
655
|
-
port: int = PORT_OPTION,
|
|
632
|
+
host: Optional[str] = HOST_OPTION,
|
|
633
|
+
port: Optional[int] = PORT_OPTION,
|
|
656
634
|
token: Optional[str] = TOKEN_OPTION,
|
|
657
635
|
):
|
|
658
636
|
"""Disable a schedule."""
|
|
659
|
-
|
|
637
|
+
with _daemon_errors():
|
|
638
|
+
DaemonClient(host, port, token).disable_schedule(schedule_id)
|
|
660
639
|
console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] disabled")
|
|
661
640
|
|
|
662
641
|
|
|
663
642
|
@schedule_app.command("run")
|
|
664
643
|
def schedule_run(
|
|
665
644
|
schedule_id: str = typer.Argument(help="Schedule ID to trigger now"),
|
|
666
|
-
host: str = HOST_OPTION,
|
|
667
|
-
port: int = PORT_OPTION,
|
|
645
|
+
host: Optional[str] = HOST_OPTION,
|
|
646
|
+
port: Optional[int] = PORT_OPTION,
|
|
668
647
|
token: Optional[str] = TOKEN_OPTION,
|
|
669
648
|
):
|
|
670
649
|
"""Trigger a schedule immediately."""
|
|
671
|
-
|
|
650
|
+
with _daemon_errors():
|
|
651
|
+
DaemonClient(host, port, token).run_schedule(schedule_id)
|
|
672
652
|
console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] triggered")
|
|
673
653
|
|
|
674
654
|
|
|
675
655
|
@schedule_app.command("show")
|
|
676
656
|
def schedule_show(
|
|
677
657
|
schedule_id: str = typer.Argument(help="Schedule ID"),
|
|
678
|
-
host: str = HOST_OPTION,
|
|
679
|
-
port: int = PORT_OPTION,
|
|
658
|
+
host: Optional[str] = HOST_OPTION,
|
|
659
|
+
port: Optional[int] = PORT_OPTION,
|
|
680
660
|
token: Optional[str] = TOKEN_OPTION,
|
|
681
661
|
):
|
|
682
662
|
"""Show schedule details."""
|
|
683
|
-
|
|
663
|
+
with _daemon_errors():
|
|
664
|
+
data = DaemonClient(host, port, token).get_schedule(schedule_id)
|
|
684
665
|
|
|
685
666
|
console.print(f"[bold]{data['id']}[/bold]")
|
|
686
667
|
console.print(f" agent: {data['agent']}")
|