tsugite-cli 0.23.2__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.2 → tsugite_cli-0.24.0}/PKG-INFO +1 -1
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/pyproject.toml +1 -1
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_preparation.py +2 -2
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/history_integration.py +28 -20
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/runner.py +8 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/steps.py +1 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/agent_config.py +14 -14
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/scheduling/SKILL.md +2 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/daemon.py +64 -83
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/history.py +2 -2
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/agent.py +20 -6
- {tsugite_cli-0.23.2 → 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.2 → tsugite_cli-0.24.0}/tsugite/events/helpers.py +14 -3
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/models.py +1 -1
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/sqlite_backend.py +17 -7
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/permissions.py +31 -3
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/anthropic.py +5 -3
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/openai_compat.py +3 -1
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/skill_discovery.py +6 -0
- tsugite_cli-0.24.0/tsugite/tools/daemon_control.py +107 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/http.py +27 -2
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/jobs.py +21 -2
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/skills.py +19 -13
- tsugite_cli-0.23.2/tsugite/tools/daemon_control.py +0 -60
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/.gitignore +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/AGENTS.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/LICENSE +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/README.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/approval.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/config.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/context.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/models.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/options.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/py.typed +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/artifacts.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/usage/quota.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/utils.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/workspace/templates.py +0 -0
|
@@ -242,7 +242,7 @@ class AgentPreparer:
|
|
|
242
242
|
auto_load_skills = [s for s in (agent_config.auto_load_skills or []) if s not in suppressed_skills]
|
|
243
243
|
|
|
244
244
|
for skill_name in auto_load_skills:
|
|
245
|
-
result = skill_manager.load_skill(skill_name)
|
|
245
|
+
result = skill_manager.load_skill(skill_name, explicit=True)
|
|
246
246
|
if result.startswith("Failed") or result.startswith("Skill '"):
|
|
247
247
|
if event_bus:
|
|
248
248
|
event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
|
|
@@ -268,7 +268,7 @@ class AgentPreparer:
|
|
|
268
268
|
if name in auto_load_skills:
|
|
269
269
|
# Already loaded above; no need to double-load but it's still sticky.
|
|
270
270
|
continue
|
|
271
|
-
result = skill_manager.load_skill(name)
|
|
271
|
+
result = skill_manager.load_skill(name, explicit=True)
|
|
272
272
|
if result.startswith("Failed") or result.startswith("Skill '"):
|
|
273
273
|
if event_bus:
|
|
274
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,8 +96,8 @@ 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. A final_result also ends a turn
|
|
96
|
-
|
|
99
|
+
so this stays False for it. A final_result also ends a turn copied without
|
|
100
|
+
its session_end (branches, older compacted sessions).
|
|
97
101
|
"""
|
|
98
102
|
events = list(storage.iter_events(types=("user_input", "session_end", "final_result")))
|
|
99
103
|
return bool(events) and events[-1].type == "user_input"
|
|
@@ -129,8 +133,9 @@ def record_user_input(
|
|
|
129
133
|
data["attachments"] = [
|
|
130
134
|
{"name": a.name, "type": a.content_type.value, "source_url": a.source_url} for a in uploads
|
|
131
135
|
]
|
|
132
|
-
if
|
|
133
|
-
|
|
136
|
+
channel = {k: v for k, v in (channel_metadata or {}).items() if k != "tsugite_token"}
|
|
137
|
+
if channel:
|
|
138
|
+
data["channel"] = channel
|
|
134
139
|
# Structured client context, recorded alongside the folded <client_context> XML
|
|
135
140
|
# in `text` so the UI reads it back without re-parsing the prompt string.
|
|
136
141
|
if client_context_items:
|
|
@@ -244,8 +249,8 @@ def save_run_to_history(
|
|
|
244
249
|
agent_already_recorded = last_model_response > last_session_start
|
|
245
250
|
|
|
246
251
|
if agent_already_recorded:
|
|
247
|
-
# A continued conversation
|
|
248
|
-
# 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.
|
|
249
254
|
turn_start = max(last_session_start, last_user_input)
|
|
250
255
|
if last_session_end < turn_start:
|
|
251
256
|
record_session_end(storage, status=status, error_message=error_message)
|
|
@@ -264,19 +269,20 @@ def save_run_to_history(
|
|
|
264
269
|
tools_called=list(getattr(step, "tools_called", []) or []),
|
|
265
270
|
)
|
|
266
271
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
)
|
|
280
286
|
|
|
281
287
|
record_session_end(storage, status=status, error_message=error_message)
|
|
282
288
|
return storage.session_id
|
|
@@ -288,6 +294,7 @@ class ResumableSessionState:
|
|
|
288
294
|
|
|
289
295
|
session_id: str
|
|
290
296
|
compacted: bool = False
|
|
297
|
+
cumulative_cost: float = 0.0
|
|
291
298
|
|
|
292
299
|
|
|
293
300
|
def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessionState]:
|
|
@@ -324,6 +331,7 @@ def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessi
|
|
|
324
331
|
return ResumableSessionState(
|
|
325
332
|
session_id=session_id,
|
|
326
333
|
compacted=bool(state.get("compacted", False)),
|
|
334
|
+
cumulative_cost=state.get("cumulative_cost") or 0.0,
|
|
327
335
|
)
|
|
328
336
|
return None
|
|
329
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
|
|
|
@@ -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
|
|
@@ -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']}")
|
|
@@ -55,7 +55,7 @@ def _format_cost(cost: Optional[float]) -> str:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def _status_style(status: Optional[str]) -> str:
|
|
58
|
-
return {"success": "green", "error": "red", "interrupted": "yellow"}.get(status or "", "dim")
|
|
58
|
+
return {"success": "green", "error": "red", "no_action": "red", "interrupted": "yellow"}.get(status or "", "dim")
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def _format_created(dt: Optional[datetime]) -> str:
|
|
@@ -77,7 +77,7 @@ def _parse_date(date_str: Optional[str]) -> Optional[datetime]:
|
|
|
77
77
|
@history_app.command("list")
|
|
78
78
|
def history_list(
|
|
79
79
|
agent: str = typer.Option(None, "--agent", help="Filter by agent name"),
|
|
80
|
-
status: str = typer.Option(None, "--status", help="Filter by status (success, error, interrupted)"),
|
|
80
|
+
status: str = typer.Option(None, "--status", help="Filter by status (success, error, interrupted, no_action)"),
|
|
81
81
|
tool: str = typer.Option(None, "--tool", help="Filter by tool/function name"),
|
|
82
82
|
since: str = typer.Option(None, "--since", help="Only sessions after this date (YYYY-MM-DD)"),
|
|
83
83
|
before: str = typer.Option(None, "--before", help="Only sessions before this date (YYYY-MM-DD)"),
|