soothe-cli 0.8.0__tar.gz → 0.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/PKG-INFO +2 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/pyproject.toml +2 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +37 -6
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/config_cmd.py +4 -7
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/cron_cmd.py +3 -3
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/loop_cmd.py +4 -61
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/run_cmd.py +9 -62
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/status_cmd.py +4 -7
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon.py +42 -79
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless.py +4 -6
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/main.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/cli_config.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor.py +2 -2
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor_state.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_result.py +2 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
- soothe_cli-0.8.2/src/soothe_cli/runtime/transport/session.py +68 -0
- soothe_cli-0.8.2/src/soothe_cli/runtime/turn/pipeline.py +35 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/message_text.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_model.py +7 -6
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_startup.py +8 -3
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/command_router.py +16 -3
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/config.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/model_config.py +16 -17
- soothe_cli-0.8.2/src/soothe_cli/tui/skills/invocation.py +70 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/theme.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/tool_display.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/update_check.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/chat_input.py +1 -1
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_data.py +38 -31
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_viewer.py +7 -4
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +1 -1
- soothe_cli-0.8.0/src/soothe_cli/runtime/transport/session.py +0 -773
- soothe_cli-0.8.0/src/soothe_cli/runtime/turn/pipeline.py +0 -404
- soothe_cli-0.8.0/src/soothe_cli/tui/skills/invocation.py +0 -61
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/.gitignore +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/README.md +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_events_debug.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_usage.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_execution.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/textual_adapter.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: Soothe CLI client - communicates with daemon via WebSocket
|
|
5
5
|
Project-URL: Homepage, https://github.com/mirasoth/soothe
|
|
6
6
|
Project-URL: Documentation, https://soothe.readthedocs.io
|
|
@@ -22,6 +22,7 @@ Requires-Dist: pillow<12.0.0,>=10.0.0
|
|
|
22
22
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
|
|
23
23
|
Requires-Dist: pyyaml<7.0.0,>=6.0.0
|
|
24
24
|
Requires-Dist: rich<15.0.0,>=13.0.0
|
|
25
|
+
Requires-Dist: soothe-client-python<1.0.0,>=0.10.0
|
|
25
26
|
Requires-Dist: soothe-sdk<1.0.0,>=0.5.25
|
|
26
27
|
Requires-Dist: textual<10.0.0,>=8.0.0
|
|
27
28
|
Requires-Dist: typer<2.0.0,>=0.9.0
|
|
@@ -23,7 +23,8 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"soothe-sdk>=0.5.25,<1.0.0", #
|
|
26
|
+
"soothe-sdk>=0.5.25,<1.0.0", # events/display/wire contracts
|
|
27
|
+
"soothe-client-python>=0.10.0,<1.0.0", # WebSocket transport client
|
|
27
28
|
"typer>=0.9.0,<2.0.0", # CLI framework
|
|
28
29
|
"textual>=8.0.0,<10.0.0", # TUI framework
|
|
29
30
|
"rich>=13.0.0,<15.0.0", # Console output
|
|
@@ -14,12 +14,12 @@ from collections import Counter
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
16
|
import typer
|
|
17
|
-
from
|
|
17
|
+
from soothe_client import (
|
|
18
|
+
command_client_from_config,
|
|
18
19
|
is_daemon_live,
|
|
19
20
|
websocket_url_from_config,
|
|
20
|
-
ws_command_client_from_config,
|
|
21
21
|
)
|
|
22
|
-
from soothe_sdk.
|
|
22
|
+
from soothe_sdk.wire.protocol import preview_first
|
|
23
23
|
|
|
24
24
|
app = typer.Typer(help="Autopilot mode — long-running autonomous agent control.")
|
|
25
25
|
|
|
@@ -46,7 +46,7 @@ def _require_daemon_ws():
|
|
|
46
46
|
err=True,
|
|
47
47
|
)
|
|
48
48
|
sys.exit(1)
|
|
49
|
-
return
|
|
49
|
+
return command_client_from_config(cfg)
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
@app.command("run")
|
|
@@ -325,10 +325,41 @@ def show_goal(
|
|
|
325
325
|
|
|
326
326
|
@app.command("cancel")
|
|
327
327
|
def cancel_goal(
|
|
328
|
-
goal_id: str = typer.Argument(
|
|
328
|
+
goal_id: str | None = typer.Argument(
|
|
329
|
+
None,
|
|
330
|
+
help="Goal ID to cancel (omit when using --all or --job).",
|
|
331
|
+
),
|
|
332
|
+
cancel_all: bool = typer.Option(
|
|
333
|
+
False,
|
|
334
|
+
"--all",
|
|
335
|
+
help="Cancel every open (non-terminal) autopilot goal.",
|
|
336
|
+
),
|
|
337
|
+
job_id: str | None = typer.Option(
|
|
338
|
+
None,
|
|
339
|
+
"--job",
|
|
340
|
+
help="Cancel a job root and all descendant goals.",
|
|
341
|
+
),
|
|
329
342
|
) -> None:
|
|
330
|
-
"""Cancel a goal via the daemon."""
|
|
343
|
+
"""Cancel a goal, a job subtree, or all open goals via the daemon."""
|
|
344
|
+
modes = sum(1 for flag in (bool(goal_id), cancel_all, bool(job_id)) if flag)
|
|
345
|
+
if modes != 1:
|
|
346
|
+
typer.echo(
|
|
347
|
+
"Specify exactly one of: GOAL_ID, --all, or --job <id>.",
|
|
348
|
+
err=True,
|
|
349
|
+
)
|
|
350
|
+
raise typer.Exit(1)
|
|
351
|
+
|
|
331
352
|
client = _require_daemon_ws()
|
|
353
|
+
if cancel_all:
|
|
354
|
+
result = client.autopilot_cancel_all()
|
|
355
|
+
count = result.get("cancelled_count", 0)
|
|
356
|
+
typer.echo(f"Cancelled {count} open goal(s).")
|
|
357
|
+
return
|
|
358
|
+
if job_id:
|
|
359
|
+
result = client.job_cancel(job_id)
|
|
360
|
+
typer.echo(f"Cancel job result: {result.get('status', result)}")
|
|
361
|
+
return
|
|
362
|
+
assert goal_id is not None
|
|
332
363
|
result = client.autopilot_cancel_goal(goal_id)
|
|
333
364
|
typer.echo(f"Cancel result: {result.get('status', result)}")
|
|
334
365
|
|
|
@@ -10,8 +10,8 @@ from typing import Annotated, Any
|
|
|
10
10
|
import typer
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
from rich.panel import Panel
|
|
13
|
-
from
|
|
14
|
-
|
|
13
|
+
from soothe_client import (
|
|
14
|
+
connected_websocket,
|
|
15
15
|
request_daemon_config_reload,
|
|
16
16
|
websocket_url_from_config,
|
|
17
17
|
)
|
|
@@ -34,14 +34,11 @@ async def _trigger_config_reload(ws_url: str, timeout: float = 5.0) -> dict[str,
|
|
|
34
34
|
Returns:
|
|
35
35
|
Response dict from daemon, or error dict on failure.
|
|
36
36
|
"""
|
|
37
|
-
client = WebSocketClient(url=ws_url)
|
|
38
37
|
try:
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
async with connected_websocket(ws_url, timeout=timeout) as client:
|
|
39
|
+
return await request_daemon_config_reload(client, timeout=timeout)
|
|
41
40
|
except Exception as e:
|
|
42
41
|
return {"success": False, "error": str(e)}
|
|
43
|
-
finally:
|
|
44
|
-
await client.close()
|
|
45
42
|
|
|
46
43
|
|
|
47
44
|
@config_app.command("reload")
|
|
@@ -21,10 +21,10 @@ import typer
|
|
|
21
21
|
from rich.console import Console
|
|
22
22
|
from rich.panel import Panel
|
|
23
23
|
from rich.table import Table
|
|
24
|
-
from
|
|
24
|
+
from soothe_client import (
|
|
25
|
+
command_client_from_config,
|
|
25
26
|
is_daemon_live,
|
|
26
27
|
websocket_url_from_config,
|
|
27
|
-
ws_command_client_from_config,
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
from soothe_cli.runtime import load_config
|
|
@@ -44,7 +44,7 @@ def _require_cron_client():
|
|
|
44
44
|
err=True,
|
|
45
45
|
)
|
|
46
46
|
sys.exit(1)
|
|
47
|
-
return
|
|
47
|
+
return command_client_from_config(cfg)
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _format_next_run(next_run: str) -> str:
|
|
@@ -17,10 +17,9 @@ from rich.console import Console
|
|
|
17
17
|
from rich.panel import Panel
|
|
18
18
|
from rich.prompt import Prompt
|
|
19
19
|
from rich.table import Table
|
|
20
|
-
from
|
|
21
|
-
ProtocolError,
|
|
22
|
-
WebSocketClient,
|
|
20
|
+
from soothe_client import (
|
|
23
21
|
is_daemon_live,
|
|
22
|
+
protocol1_rpc,
|
|
24
23
|
websocket_url_from_config,
|
|
25
24
|
)
|
|
26
25
|
|
|
@@ -47,64 +46,8 @@ async def _check_daemon(ws_url: str) -> bool:
|
|
|
47
46
|
return await is_daemon_live(ws_url, timeout=5.0)
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
method: str,
|
|
53
|
-
params: dict[str, Any] | None = None,
|
|
54
|
-
*,
|
|
55
|
-
mode: str = "request",
|
|
56
|
-
timeout: float = 30.0,
|
|
57
|
-
) -> dict[str, Any]:
|
|
58
|
-
"""Send a protocol-1 RPC to the daemon and return the response.
|
|
59
|
-
|
|
60
|
-
Uses the protocol-1 client API (RFC-450): ``request()`` for RPC,
|
|
61
|
-
``notify()`` for fire-and-forget, ``subscribe()`` for event streams.
|
|
62
|
-
The wrapper preserves the dict-based error contract used by command
|
|
63
|
-
handlers — callers check ``if "error" in response``.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
ws_url: WebSocket URL.
|
|
67
|
-
method: RPC method / notification target / subscription target
|
|
68
|
-
(e.g. ``"loop_get"``, ``"loop_input"``, ``"loop_events"``).
|
|
69
|
-
params: Structured parameters object.
|
|
70
|
-
mode: One of ``"request"`` (blocking RPC), ``"notify"``
|
|
71
|
-
(fire-and-forget), or ``"subscribe"`` (start a stream).
|
|
72
|
-
timeout: Maximum seconds to wait.
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
Response dict from daemon, or ``{"error": ...}`` on failure. For
|
|
76
|
-
``notify`` mode returns ``{}``; for ``subscribe`` mode returns
|
|
77
|
-
``{"subscription_id": <id>}``.
|
|
78
|
-
"""
|
|
79
|
-
client = WebSocketClient(url=ws_url)
|
|
80
|
-
try:
|
|
81
|
-
await client.connect()
|
|
82
|
-
await asyncio.wait_for(client.request_connection_init(), timeout=timeout)
|
|
83
|
-
await asyncio.wait_for(
|
|
84
|
-
client.wait_for_connection_ack(ack_timeout_s=timeout), timeout=timeout
|
|
85
|
-
)
|
|
86
|
-
if mode == "notify":
|
|
87
|
-
await asyncio.wait_for(client.notify(method, params or {}), timeout=timeout)
|
|
88
|
-
return {}
|
|
89
|
-
if mode == "subscribe":
|
|
90
|
-
sub_id = await asyncio.wait_for(
|
|
91
|
-
client.subscribe(method, params or {}, timeout=timeout),
|
|
92
|
-
timeout=timeout,
|
|
93
|
-
)
|
|
94
|
-
return {"subscription_id": sub_id}
|
|
95
|
-
result = await asyncio.wait_for(
|
|
96
|
-
client.request(method, params or {}, timeout=timeout),
|
|
97
|
-
timeout=timeout,
|
|
98
|
-
)
|
|
99
|
-
return result if isinstance(result, dict) else {"result": result}
|
|
100
|
-
except TimeoutError:
|
|
101
|
-
return {"error": "Timed out waiting for daemon response"}
|
|
102
|
-
except ProtocolError as exc:
|
|
103
|
-
return {"error": str(exc)}
|
|
104
|
-
except (ConnectionError, OSError) as exc:
|
|
105
|
-
return {"error": f"Connection error: {exc}"}
|
|
106
|
-
finally:
|
|
107
|
-
await client.close()
|
|
49
|
+
# Back-compat alias for tests and internal callers.
|
|
50
|
+
_rpc = protocol1_rpc
|
|
108
51
|
|
|
109
52
|
|
|
110
53
|
def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
|
|
@@ -4,10 +4,9 @@ import logging
|
|
|
4
4
|
import sys
|
|
5
5
|
import time
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
8
7
|
|
|
9
8
|
import typer
|
|
10
|
-
from soothe_sdk.
|
|
9
|
+
from soothe_sdk.paths import SOOTHE_HOME
|
|
11
10
|
from soothe_sdk.utils.logging import resolve_cli_log_level
|
|
12
11
|
|
|
13
12
|
from soothe_cli.cli.execution import run_headless, run_tui
|
|
@@ -37,7 +36,8 @@ def run_impl(
|
|
|
37
36
|
autonomous: Enable autonomous iteration mode
|
|
38
37
|
max_iterations: Max iterations for autonomous mode
|
|
39
38
|
tui_with_prompt: When True with a prompt, open the TUI instead of headless.
|
|
40
|
-
mcp_config:
|
|
39
|
+
mcp_config: Deprecated. MCP servers must be configured on the daemon;
|
|
40
|
+
passing this flag only emits a warning.
|
|
41
41
|
"""
|
|
42
42
|
startup_start = time.perf_counter()
|
|
43
43
|
use_headless: bool | None = None # Track execution mode for exit tip
|
|
@@ -45,9 +45,13 @@ def run_impl(
|
|
|
45
45
|
try:
|
|
46
46
|
cfg = load_config()
|
|
47
47
|
|
|
48
|
-
#
|
|
48
|
+
# MCP servers are owned by the daemon config; CLI cannot merge them post-split.
|
|
49
49
|
if mcp_config:
|
|
50
|
-
|
|
50
|
+
typer.echo(
|
|
51
|
+
"--mcp-config is ignored in the CLI; configure MCP servers on the daemon "
|
|
52
|
+
f"(~/.soothe/config). Requested file: {mcp_config}",
|
|
53
|
+
err=True,
|
|
54
|
+
)
|
|
51
55
|
log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
|
|
52
56
|
log_file = Path(SOOTHE_HOME) / "logs" / "cli.log"
|
|
53
57
|
setup_logging(log_level, log_file=log_file)
|
|
@@ -123,60 +127,3 @@ def run_impl(
|
|
|
123
127
|
|
|
124
128
|
typer.echo(f"Error: {format_cli_error(e)}", err=True)
|
|
125
129
|
sys.exit(1)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _merge_mcp_config(cfg: Any, mcp_config_path: str) -> None:
|
|
129
|
-
"""Load MCP server config from a JSON/YAML file and merge into SootheConfig.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
cfg: SootheConfig instance.
|
|
133
|
-
mcp_config_path: Path to MCP config file (JSON or YAML).
|
|
134
|
-
"""
|
|
135
|
-
import json
|
|
136
|
-
|
|
137
|
-
path = Path(mcp_config_path).expanduser().resolve()
|
|
138
|
-
if not path.is_file():
|
|
139
|
-
typer.echo(f"--mcp-config: file not found: {path}", err=True)
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
try:
|
|
143
|
-
raw = path.read_text(encoding="utf-8")
|
|
144
|
-
if path.suffix in (".yml", ".yaml"):
|
|
145
|
-
try:
|
|
146
|
-
import yaml
|
|
147
|
-
|
|
148
|
-
data = yaml.safe_load(raw)
|
|
149
|
-
except ImportError:
|
|
150
|
-
typer.echo("--mcp-config: PyYAML not installed; use JSON format", err=True)
|
|
151
|
-
return
|
|
152
|
-
else:
|
|
153
|
-
data = json.loads(raw)
|
|
154
|
-
except Exception as e:
|
|
155
|
-
typer.echo(f"--mcp-config: failed to parse {path}: {e}", err=True)
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
servers = data.get("mcp_servers") or data.get("servers") or []
|
|
159
|
-
if not isinstance(servers, list):
|
|
160
|
-
typer.echo("--mcp-config: expected 'mcp_servers' or 'servers' list", err=True)
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
from soothe.config.models import MCPServerConfig
|
|
164
|
-
|
|
165
|
-
new_servers: list[MCPServerConfig] = []
|
|
166
|
-
for entry in servers:
|
|
167
|
-
if isinstance(entry, dict):
|
|
168
|
-
try:
|
|
169
|
-
new_servers.append(MCPServerConfig(**entry))
|
|
170
|
-
except Exception as e:
|
|
171
|
-
logger.warning("Skipping invalid MCP server entry: %s", e)
|
|
172
|
-
|
|
173
|
-
if new_servers:
|
|
174
|
-
existing = list(cfg.mcp_servers) if cfg.mcp_servers else []
|
|
175
|
-
existing_names = {s.name for s in existing}
|
|
176
|
-
for s in new_servers:
|
|
177
|
-
if s.name in existing_names:
|
|
178
|
-
logger.warning("--mcp-config: server '%s' already in config, skipping", s.name)
|
|
179
|
-
else:
|
|
180
|
-
existing.append(s)
|
|
181
|
-
cfg.mcp_servers = existing
|
|
182
|
-
logger.info("--mcp-config: merged %d MCP server(s)", len(new_servers))
|
|
@@ -15,9 +15,9 @@ import typer
|
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
from rich.panel import Panel
|
|
17
17
|
from rich.table import Table
|
|
18
|
-
from
|
|
19
|
-
WebSocketClient,
|
|
18
|
+
from soothe_client import (
|
|
20
19
|
check_daemon_status,
|
|
20
|
+
connected_websocket,
|
|
21
21
|
is_daemon_live,
|
|
22
22
|
websocket_url_from_config,
|
|
23
23
|
)
|
|
@@ -40,14 +40,11 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
|
|
|
40
40
|
Returns:
|
|
41
41
|
Status dict from daemon, or error dict on failure.
|
|
42
42
|
"""
|
|
43
|
-
client = WebSocketClient(url=ws_url)
|
|
44
43
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
async with connected_websocket(ws_url, timeout=timeout) as client:
|
|
45
|
+
return await check_daemon_status(client, timeout=timeout)
|
|
47
46
|
except Exception as e:
|
|
48
47
|
return {"error": str(e)}
|
|
49
|
-
finally:
|
|
50
|
-
await client.close()
|
|
51
48
|
|
|
52
49
|
|
|
53
50
|
def _render_unified_status_table(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Daemon-based execution for headless mode.
|
|
2
2
|
|
|
3
|
-
Uses
|
|
4
|
-
|
|
3
|
+
Uses EventProcessor with HeadlessCliRenderer (stdout: loop-tagged answers only).
|
|
4
|
+
Session lifecycle goes through ``soothe_client.appkit.DaemonSession``.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -12,13 +12,12 @@ import signal
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
import typer
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
async_ws_command_client_from_config,
|
|
18
|
-
bootstrap_loop_session,
|
|
19
|
-
connect_websocket_with_retries,
|
|
15
|
+
from soothe_client import (
|
|
16
|
+
async_command_client_from_config,
|
|
20
17
|
websocket_url_from_config,
|
|
21
18
|
)
|
|
19
|
+
from soothe_client.appkit import DaemonSession
|
|
20
|
+
from soothe_client.appkit.events import is_loop_scoped_event, unwrap_next
|
|
22
21
|
|
|
23
22
|
from soothe_cli.cli.execution.daemon_errors import (
|
|
24
23
|
friendly_daemon_execution_error,
|
|
@@ -39,56 +38,16 @@ _HEADLESS_WORKER_LOST_RETRIES = 1
|
|
|
39
38
|
_CANCEL_SEND_TIMEOUT_S = 3.0
|
|
40
39
|
|
|
41
40
|
|
|
42
|
-
def _unwrap_next(event: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
43
|
-
"""Unwrap a protocol-1 ``next`` envelope to its inner streaming frame.
|
|
44
|
-
|
|
45
|
-
Under protocol-1 (RFC-450 §9.3) the daemon wraps free-form streaming
|
|
46
|
-
frames (``event``/``command_response``/card replay) in a
|
|
47
|
-
``{proto, type:"next", payload:{namespace, mode, data}}`` envelope. This
|
|
48
|
-
helper returns the inner ``data`` dict (the legacy frame) so the headless
|
|
49
|
-
loop and EventProcessor can branch on the same fields as before.
|
|
50
|
-
``status``/``error``/``response``/``complete`` are sent raw and pass
|
|
51
|
-
through unchanged.
|
|
52
|
-
"""
|
|
53
|
-
if not isinstance(event, dict):
|
|
54
|
-
return event
|
|
55
|
-
if event.get("type") != "next":
|
|
56
|
-
return event
|
|
57
|
-
payload = event.get("payload")
|
|
58
|
-
if not isinstance(payload, dict):
|
|
59
|
-
return event
|
|
60
|
-
data = payload.get("data")
|
|
61
|
-
return data if isinstance(data, dict) else event
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
|
|
65
|
-
"""Return whether a daemon frame belongs to the active StrangeLoop session.
|
|
66
|
-
|
|
67
|
-
Unwraps protocol-1 ``next`` envelopes first, then checks ``loop_id`` on the
|
|
68
|
-
inner streaming frame. Non-scoped types (``response``, ``error``,
|
|
69
|
-
``complete``, etc.) are always considered in-scope.
|
|
70
|
-
"""
|
|
71
|
-
event_type = event.get("type", "")
|
|
72
|
-
if event_type == "next":
|
|
73
|
-
inner = _unwrap_next(event)
|
|
74
|
-
if isinstance(inner, dict):
|
|
75
|
-
event_type = inner.get("type", "")
|
|
76
|
-
return event_type not in {"status", "event"} or (inner.get("loop_id") == active_loop_id)
|
|
77
|
-
if event_type not in {"status", "event"}:
|
|
78
|
-
return True
|
|
79
|
-
return event.get("loop_id") == active_loop_id
|
|
80
|
-
|
|
81
|
-
|
|
82
41
|
def _emit_headless_error(message: str) -> None:
|
|
83
42
|
"""Write a user-facing error line to stderr (headless renderer convention)."""
|
|
84
43
|
typer.echo(f"ERROR: {message}", err=True)
|
|
85
44
|
|
|
86
45
|
|
|
87
|
-
async def _send_cancel_to_daemon(
|
|
46
|
+
async def _send_cancel_to_daemon(session: DaemonSession) -> None:
|
|
88
47
|
"""Send /cancel to the daemon with a short timeout."""
|
|
89
48
|
try:
|
|
90
49
|
await asyncio.wait_for(
|
|
91
|
-
|
|
50
|
+
session.cancel_remote_query(),
|
|
92
51
|
timeout=_CANCEL_SEND_TIMEOUT_S,
|
|
93
52
|
)
|
|
94
53
|
except Exception:
|
|
@@ -120,16 +79,12 @@ async def _run_headless_session_once(
|
|
|
120
79
|
max_iterations: int | None = None,
|
|
121
80
|
) -> tuple[int, bool]:
|
|
122
81
|
"""Run one headless daemon session; return ``(exit_code, retry_on_worker_loss)``."""
|
|
123
|
-
from soothe_sdk.client import WebSocketClient
|
|
124
|
-
|
|
125
|
-
ws_url = websocket_url_from_config(cfg)
|
|
126
|
-
client = WebSocketClient(url=ws_url)
|
|
127
|
-
|
|
128
82
|
# Track whether the daemon was notified of cancellation.
|
|
129
83
|
cancel_sent = False
|
|
130
84
|
sigint_received = False
|
|
131
85
|
sigint_count = 0
|
|
132
86
|
original_sigint: Any = None
|
|
87
|
+
session: DaemonSession | None = None
|
|
133
88
|
|
|
134
89
|
cron_text = _parse_cron_slash_prompt(prompt)
|
|
135
90
|
if cron_text is not None:
|
|
@@ -137,7 +92,7 @@ async def _run_headless_session_once(
|
|
|
137
92
|
_emit_headless_error("Usage: /cron <natural language schedule>")
|
|
138
93
|
return 1, False
|
|
139
94
|
try:
|
|
140
|
-
ws_client =
|
|
95
|
+
ws_client = async_command_client_from_config(cfg)
|
|
141
96
|
result = await ws_client.cron_add(cron_text)
|
|
142
97
|
except RuntimeError as exc:
|
|
143
98
|
_emit_headless_error(str(exc))
|
|
@@ -150,27 +105,34 @@ async def _run_headless_session_once(
|
|
|
150
105
|
return 0, False
|
|
151
106
|
|
|
152
107
|
try:
|
|
153
|
-
await connect_websocket_with_retries(client)
|
|
154
108
|
cli_ws = resolve_cli_loop_workspace()
|
|
155
|
-
#
|
|
109
|
+
# Three first-class modes (batch / adaptive / streaming).
|
|
156
110
|
# Default is ``adaptive`` for headless runs as well — it gives smooth
|
|
157
111
|
# progress on long synthesis while keeping wire traffic bounded.
|
|
158
112
|
override = getattr(cfg, "output_streaming_mode", None)
|
|
159
113
|
stream_delivery = override if override in ("batch", "adaptive", "streaming") else "adaptive"
|
|
160
114
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
resume_loop_id=resume_loop_id,
|
|
164
|
-
stream_delivery=stream_delivery,
|
|
115
|
+
session = DaemonSession(
|
|
116
|
+
websocket_url_from_config(cfg),
|
|
165
117
|
workspace=cli_ws,
|
|
166
|
-
|
|
118
|
+
stream_delivery=stream_delivery,
|
|
167
119
|
)
|
|
120
|
+
try:
|
|
121
|
+
status_event = await asyncio.wait_for(
|
|
122
|
+
session.connect(resume_loop_id=resume_loop_id),
|
|
123
|
+
timeout=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
124
|
+
)
|
|
125
|
+
except RuntimeError as exc:
|
|
126
|
+
raw = str(exc)
|
|
127
|
+
_emit_headless_error(friendly_daemon_execution_error(raw))
|
|
128
|
+
return 1, is_daemon_worker_subprocess_lost(raw)
|
|
129
|
+
|
|
168
130
|
if status_event.get("type") == "error":
|
|
169
131
|
raw = str(status_event.get("message", "unknown"))
|
|
170
132
|
_emit_headless_error(friendly_daemon_execution_error(raw))
|
|
171
133
|
return 1, is_daemon_worker_subprocess_lost(raw)
|
|
172
134
|
|
|
173
|
-
active_loop_id = status_event.get("loop_id")
|
|
135
|
+
active_loop_id = session.loop_id or status_event.get("loop_id")
|
|
174
136
|
if not active_loop_id:
|
|
175
137
|
_emit_headless_error("No loop_id after session bootstrap")
|
|
176
138
|
return 1, False
|
|
@@ -179,8 +141,7 @@ async def _run_headless_session_once(
|
|
|
179
141
|
effective_prompt = cleaned_prompt if subagent_name else prompt
|
|
180
142
|
|
|
181
143
|
await asyncio.wait_for(
|
|
182
|
-
|
|
183
|
-
active_loop_id,
|
|
144
|
+
session.send_turn(
|
|
184
145
|
effective_prompt,
|
|
185
146
|
autonomous=autonomous,
|
|
186
147
|
max_iterations=max_iterations,
|
|
@@ -223,13 +184,14 @@ async def _run_headless_session_once(
|
|
|
223
184
|
)
|
|
224
185
|
|
|
225
186
|
query_started = False
|
|
187
|
+
client = session.client
|
|
226
188
|
|
|
227
189
|
while True:
|
|
228
190
|
# Check if SIGINT fired and send /cancel to the daemon.
|
|
229
191
|
if sigint_received and not cancel_sent:
|
|
230
192
|
cancel_sent = True
|
|
231
193
|
logger.info("Headless query interrupted; sending /cancel to daemon")
|
|
232
|
-
await _send_cancel_to_daemon(
|
|
194
|
+
await _send_cancel_to_daemon(session)
|
|
233
195
|
# After notifying the daemon, cancel the main task so
|
|
234
196
|
# asyncio.run() can unwind cleanly.
|
|
235
197
|
if main_task is not None and not main_task.done():
|
|
@@ -248,13 +210,13 @@ async def _run_headless_session_once(
|
|
|
248
210
|
break
|
|
249
211
|
|
|
250
212
|
event_type = event.get("type", "")
|
|
251
|
-
if not
|
|
213
|
+
if not is_loop_scoped_event(event, active_loop_id=active_loop_id):
|
|
252
214
|
continue
|
|
253
215
|
|
|
254
|
-
# Unwrap protocol-1 ``next`` envelopes to the inner streaming frame
|
|
255
|
-
#
|
|
216
|
+
# Unwrap protocol-1 ``next`` envelopes to the inner streaming frame.
|
|
217
|
+
# ``status``/``error`` arrive raw and pass through.
|
|
256
218
|
if event_type == "next":
|
|
257
|
-
inner =
|
|
219
|
+
inner = unwrap_next(event)
|
|
258
220
|
if isinstance(inner, dict):
|
|
259
221
|
event = inner
|
|
260
222
|
event_type = event.get("type", "")
|
|
@@ -288,12 +250,12 @@ async def _run_headless_session_once(
|
|
|
288
250
|
break
|
|
289
251
|
if not nxt:
|
|
290
252
|
break
|
|
291
|
-
if not
|
|
253
|
+
if not is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
|
|
292
254
|
continue
|
|
293
255
|
# Unwrap ``next`` envelopes before handing to the
|
|
294
256
|
# processor so it sees the legacy frame shape.
|
|
295
257
|
if nxt.get("type") == "next":
|
|
296
|
-
inner =
|
|
258
|
+
inner = unwrap_next(nxt)
|
|
297
259
|
if isinstance(inner, dict):
|
|
298
260
|
nxt = inner
|
|
299
261
|
processor.process_event(nxt)
|
|
@@ -304,18 +266,18 @@ async def _run_headless_session_once(
|
|
|
304
266
|
processor.process_event(event)
|
|
305
267
|
|
|
306
268
|
except KeyboardInterrupt:
|
|
307
|
-
if not cancel_sent:
|
|
269
|
+
if session is not None and not cancel_sent:
|
|
308
270
|
cancel_sent = True
|
|
309
271
|
logger.info("Headless query interrupted by user; sending /cancel to daemon")
|
|
310
|
-
await _send_cancel_to_daemon(
|
|
272
|
+
await _send_cancel_to_daemon(session)
|
|
311
273
|
return 1, False
|
|
312
274
|
except asyncio.CancelledError:
|
|
313
|
-
if not cancel_sent:
|
|
275
|
+
if session is not None and not cancel_sent:
|
|
314
276
|
cancel_sent = True
|
|
315
277
|
logger.info("Headless query cancelled; sending /cancel to daemon")
|
|
316
278
|
# Best-effort: the task is being cancelled so awaiting may fail.
|
|
317
279
|
try:
|
|
318
|
-
await asyncio.shield(_send_cancel_to_daemon(
|
|
280
|
+
await asyncio.shield(_send_cancel_to_daemon(session))
|
|
319
281
|
except (asyncio.CancelledError, Exception):
|
|
320
282
|
pass
|
|
321
283
|
raise
|
|
@@ -341,7 +303,8 @@ async def _run_headless_session_once(
|
|
|
341
303
|
signal.signal(signal.SIGINT, original_sigint)
|
|
342
304
|
except (ValueError, OSError, RuntimeError):
|
|
343
305
|
pass
|
|
344
|
-
|
|
306
|
+
if session is not None:
|
|
307
|
+
await session.close()
|
|
345
308
|
|
|
346
309
|
|
|
347
310
|
async def run_headless_via_daemon(
|
|
@@ -354,8 +317,8 @@ async def run_headless_via_daemon(
|
|
|
354
317
|
) -> int:
|
|
355
318
|
"""Run a single prompt by connecting to a running daemon.
|
|
356
319
|
|
|
357
|
-
Uses WebSocket transport for all connections
|
|
358
|
-
Headless output is
|
|
320
|
+
Uses WebSocket transport for all connections.
|
|
321
|
+
Headless output is loop-tagged main-graph assistant text only.
|
|
359
322
|
|
|
360
323
|
Retries once when the worker pool loses a subprocess mid-query (common idle-timeout
|
|
361
324
|
race or transient worker recycle).
|
|
@@ -4,8 +4,8 @@ import asyncio
|
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
|
-
from
|
|
8
|
-
|
|
7
|
+
from soothe_client import (
|
|
8
|
+
connected_websocket,
|
|
9
9
|
is_daemon_live,
|
|
10
10
|
request_daemon_shutdown,
|
|
11
11
|
websocket_url_from_config,
|
|
@@ -49,10 +49,8 @@ def run_headless(
|
|
|
49
49
|
if not daemon_live:
|
|
50
50
|
# Attempt cleanup if stale daemon (connection exists but daemon not responsive)
|
|
51
51
|
try:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await request_daemon_shutdown(client, timeout=10.0)
|
|
55
|
-
await client.close()
|
|
52
|
+
async with connected_websocket(ws_url, timeout=10.0) as client:
|
|
53
|
+
await request_daemon_shutdown(client, timeout=10.0)
|
|
56
54
|
except Exception:
|
|
57
55
|
pass # No daemon running or already stopped
|
|
58
56
|
|
|
@@ -10,7 +10,7 @@ from pathlib import Path # noqa: E402
|
|
|
10
10
|
from typing import Annotated # noqa: E402
|
|
11
11
|
|
|
12
12
|
import typer # noqa: E402
|
|
13
|
-
from soothe_sdk.
|
|
13
|
+
from soothe_sdk.paths import SOOTHE_HOME # noqa: E402
|
|
14
14
|
|
|
15
15
|
from soothe_cli.config.cli_config import CLIConfig # noqa: E402
|
|
16
16
|
from soothe_cli.config.loader import set_runtime_config # noqa: E402
|
|
@@ -35,7 +35,7 @@ from soothe_cli.runtime.policy.tui_trace_log import log_tui_trace
|
|
|
35
35
|
from soothe_cli.runtime.presentation.engine import PresentationEngine
|
|
36
36
|
|
|
37
37
|
if TYPE_CHECKING:
|
|
38
|
-
from
|
|
38
|
+
from soothe_client.schemas import Plan
|
|
39
39
|
|
|
40
40
|
from soothe_cli.runtime.presentation.renderer_protocol import RendererProtocol
|
|
41
41
|
|
|
@@ -962,7 +962,7 @@ class EventProcessor:
|
|
|
962
962
|
|
|
963
963
|
def _handle_plan_created(self, data: dict[str, Any]) -> None:
|
|
964
964
|
"""Handle plan creation event."""
|
|
965
|
-
from
|
|
965
|
+
from soothe_client.schemas import Plan, PlanStep
|
|
966
966
|
|
|
967
967
|
steps = [
|
|
968
968
|
PlanStep(
|