soothe-cli 0.4.1__tar.gz → 0.4.3__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.4.1 → soothe_cli-0.4.3}/.gitignore +1 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/PKG-INFO +10 -12
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/README.md +8 -10
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/pyproject.toml +2 -2
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -4
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/run_cmd.py +12 -8
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/thread_cmd.py +10 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/daemon.py +21 -56
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/headless.py +6 -11
- soothe_cli-0.4.3/src/soothe_cli/cli/execution/headless_renderer.py +107 -0
- soothe_cli-0.4.3/src/soothe_cli/cli/main.py +151 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/stream/__init__.py +1 -2
- soothe_cli-0.4.3/src/soothe_cli/cli/stream/context.py +65 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/stream/display_line.py +1 -9
- soothe_cli-0.4.3/src/soothe_cli/cli/stream/formatter.py +290 -0
- soothe_cli-0.4.3/src/soothe_cli/cli/stream/pipeline.py +500 -0
- soothe_cli-0.4.3/src/soothe_cli/cli/stream/task_scope.py +46 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/config/cli_config.py +25 -35
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/shared/__init__.py +31 -21
- soothe_cli-0.4.3/src/soothe_cli/shared/commands/__init__.py +37 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/commands}/command_router.py +6 -6
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/commands}/slash_commands.py +1 -1
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/shared/config_loader.py +1 -1
- soothe_cli-0.4.3/src/soothe_cli/shared/core/__init__.py +13 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/event_processor.py +413 -109
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/presentation_engine.py +42 -8
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/processor_state.py +28 -4
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/renderer_protocol.py +36 -1
- soothe_cli-0.4.3/src/soothe_cli/shared/events/__init__.py +15 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/events}/display_policy.py +12 -145
- soothe_cli-0.4.3/src/soothe_cli/shared/events/explore_task_display.py +89 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/events/stream_accumulator.py +144 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/presentation_engine.py +5 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/renderer_base.py +5 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/rendering/__init__.py +9 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/rendering/async_renderer_protocol.py +192 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/rendering/renderer_base.py +72 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/stream_accumulator.py +13 -0
- soothe_cli-0.4.1/src/soothe_cli/cli/commands/subagent_names.py → soothe_cli-0.4.3/src/soothe_cli/shared/subagent_routing.py +2 -4
- soothe_cli-0.4.3/src/soothe_cli/shared/tools/__init__.py +3 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/message_processing.py +1 -1
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/rendering.py +1 -1
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_call_resolution.py +8 -7
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_card_payload.py +22 -5
- soothe_cli-0.4.3/src/soothe_cli/shared/tools/tool_card_visibility.py +116 -0
- soothe_cli-0.4.3/src/soothe_cli/shared/tools/tool_formatters/__init__.py +29 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/base.py +1 -1
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/execution.py +18 -5
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/fallback.py +3 -3
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/file_ops.py +6 -3
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/goal_formatter.py +2 -2
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/media.py +2 -2
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/structured.py +2 -2
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/subagent.py +2 -2
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/web.py +2 -2
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_message_format.py +41 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_output_formatter.py +2 -2
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/app.py +20 -7
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/config.py +26 -1
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/daemon_session.py +29 -4
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/preview_limits.py +4 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/textual_adapter.py +542 -166
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/tool_display.py +27 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/message_store.py +0 -5
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/messages.py +312 -136
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/welcome.py +2 -2
- soothe_cli-0.4.1/src/soothe_cli/cli/commands/config_cmd.py +0 -282
- soothe_cli-0.4.1/src/soothe_cli/cli/commands/status_cmd.py +0 -121
- soothe_cli-0.4.1/src/soothe_cli/cli/main.py +0 -407
- soothe_cli-0.4.1/src/soothe_cli/cli/renderer.py +0 -462
- soothe_cli-0.4.1/src/soothe_cli/cli/stream/context.py +0 -142
- soothe_cli-0.4.1/src/soothe_cli/cli/stream/formatter.py +0 -499
- soothe_cli-0.4.1/src/soothe_cli/cli/stream/pipeline.py +0 -810
- soothe_cli-0.4.1/src/soothe_cli/cli/utils.py +0 -46
- soothe_cli-0.4.1/src/soothe_cli/shared/suppression_state.py +0 -189
- soothe_cli-0.4.1/src/soothe_cli/shared/tool_formatters/__init__.py +0 -29
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
- /soothe_cli-0.4.1/src/soothe_cli/loop_commands.py → /soothe_cli-0.4.3/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/commands}/subagent_routing.py +0 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/events}/essential_events.py +0 -0
- {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/events}/tui_trace_log.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/app.tcss +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/formatting.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Soothe CLI client - communicates with daemon via WebSocket
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenSoothe/soothe
|
|
6
6
|
Project-URL: Documentation, https://soothe.readthedocs.io
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
-
Requires-Python: <
|
|
20
|
+
Requires-Python: <4.0,>=3.11
|
|
21
21
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
|
|
22
22
|
Requires-Dist: pyyaml<7.0.0,>=6.0.0
|
|
23
23
|
Requires-Dist: rich>=13.0.0
|
|
@@ -57,9 +57,6 @@ soothe -p "Research AI advances"
|
|
|
57
57
|
# Loop management
|
|
58
58
|
soothe loop list
|
|
59
59
|
soothe loop continue loop_abc123
|
|
60
|
-
|
|
61
|
-
# Configuration
|
|
62
|
-
soothe config show
|
|
63
60
|
```
|
|
64
61
|
|
|
65
62
|
## Architecture
|
|
@@ -82,13 +79,14 @@ This package is the **client** component that communicates with the Soothe daemo
|
|
|
82
79
|
CLI uses `cli_config.yml`:
|
|
83
80
|
|
|
84
81
|
```yaml
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
daemon:
|
|
83
|
+
transports:
|
|
84
|
+
websocket:
|
|
85
|
+
host: "127.0.0.1"
|
|
86
|
+
port: 8765
|
|
87
|
+
|
|
88
|
+
# Optional: logging_level for ~/.soothe/logs/soothe-cli.log (DEBUG, INFO, …)
|
|
89
|
+
# logging_level: INFO
|
|
92
90
|
|
|
93
91
|
tui:
|
|
94
92
|
theme: "default"
|
|
@@ -22,9 +22,6 @@ soothe -p "Research AI advances"
|
|
|
22
22
|
# Loop management
|
|
23
23
|
soothe loop list
|
|
24
24
|
soothe loop continue loop_abc123
|
|
25
|
-
|
|
26
|
-
# Configuration
|
|
27
|
-
soothe config show
|
|
28
25
|
```
|
|
29
26
|
|
|
30
27
|
## Architecture
|
|
@@ -47,13 +44,14 @@ This package is the **client** component that communicates with the Soothe daemo
|
|
|
47
44
|
CLI uses `cli_config.yml`:
|
|
48
45
|
|
|
49
46
|
```yaml
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
daemon:
|
|
48
|
+
transports:
|
|
49
|
+
websocket:
|
|
50
|
+
host: "127.0.0.1"
|
|
51
|
+
port: 8765
|
|
52
|
+
|
|
53
|
+
# Optional: logging_level for ~/.soothe/logs/soothe-cli.log (DEBUG, INFO, …)
|
|
54
|
+
# logging_level: INFO
|
|
57
55
|
|
|
58
56
|
tui:
|
|
59
57
|
theme: "default"
|
|
@@ -8,7 +8,7 @@ dynamic = ["version"]
|
|
|
8
8
|
description = "Soothe CLI client - communicates with daemon via WebSocket"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
|
-
requires-python = ">=3.11,<
|
|
11
|
+
requires-python = ">=3.11,<4.0"
|
|
12
12
|
keywords = ["soothe", "cli", "client", "tui", "websocket"]
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Development Status :: 3 - Alpha",
|
|
@@ -76,7 +76,7 @@ path = "../../VERSION"
|
|
|
76
76
|
pattern = "^(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+)$"
|
|
77
77
|
|
|
78
78
|
[tool.mypy]
|
|
79
|
-
python_version = "3.
|
|
79
|
+
python_version = "3.12"
|
|
80
80
|
warn_return_any = true
|
|
81
81
|
warn_unused_configs = true
|
|
82
82
|
disallow_untyped_defs = true
|
|
@@ -21,9 +21,6 @@ def run(
|
|
|
21
21
|
max_iterations: int | None = typer.Option(
|
|
22
22
|
None, "--max-iterations", help="Maximum autonomous iterations."
|
|
23
23
|
),
|
|
24
|
-
output_format: str = typer.Option(
|
|
25
|
-
"text", "--format", "-f", help="Output format: text or jsonl."
|
|
26
|
-
),
|
|
27
24
|
) -> None:
|
|
28
25
|
"""Run autonomous agent loop for complex tasks.
|
|
29
26
|
|
|
@@ -39,7 +36,6 @@ def run(
|
|
|
39
36
|
no_tui=True,
|
|
40
37
|
autonomous=True,
|
|
41
38
|
max_iterations=max_iterations,
|
|
42
|
-
output_format=output_format,
|
|
43
39
|
)
|
|
44
40
|
|
|
45
41
|
|
|
@@ -17,30 +17,29 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
def run_impl(
|
|
19
19
|
prompt: str | None,
|
|
20
|
-
config: str | None,
|
|
21
20
|
thread_id: str | None,
|
|
22
21
|
no_tui: bool, # noqa: FBT001
|
|
23
22
|
autonomous: bool, # noqa: FBT001
|
|
24
23
|
max_iterations: int | None,
|
|
25
|
-
|
|
24
|
+
streaming_enabled: bool | None = None,
|
|
25
|
+
streaming_mode: str | None = None,
|
|
26
26
|
) -> None:
|
|
27
27
|
"""Core implementation for running Soothe agent.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
30
|
prompt: Optional prompt for headless mode
|
|
31
|
-
config: Deprecated; passed through for ``--config`` compatibility (ignored for
|
|
32
|
-
client settings; see ``load_config``).
|
|
33
31
|
thread_id: Thread ID to resume
|
|
34
32
|
no_tui: Force headless mode
|
|
35
33
|
autonomous: Enable autonomous iteration mode
|
|
36
34
|
max_iterations: Max iterations for autonomous mode
|
|
37
|
-
|
|
35
|
+
streaming_enabled: Override daemon streaming enabled setting (RFC-614)
|
|
36
|
+
streaming_mode: Override daemon streaming mode ('streaming' or 'batch')
|
|
38
37
|
"""
|
|
39
38
|
startup_start = time.perf_counter()
|
|
40
39
|
|
|
41
40
|
try:
|
|
42
|
-
cfg = load_config(
|
|
43
|
-
log_level = resolve_cli_log_level(
|
|
41
|
+
cfg = load_config()
|
|
42
|
+
log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
|
|
44
43
|
log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
|
|
45
44
|
setup_logging(log_level, log_file=log_file)
|
|
46
45
|
|
|
@@ -50,6 +49,12 @@ def run_impl(
|
|
|
50
49
|
if checkpointer == "postgresql":
|
|
51
50
|
logger.info("PostgreSQL checkpointer configured; ensure server is running.")
|
|
52
51
|
|
|
52
|
+
# Apply CLI streaming overrides (RFC-614)
|
|
53
|
+
if streaming_enabled is not None:
|
|
54
|
+
cfg.output_streaming_enabled = streaming_enabled
|
|
55
|
+
if streaming_mode is not None:
|
|
56
|
+
cfg.output_streaming_mode = streaming_mode
|
|
57
|
+
|
|
53
58
|
startup_elapsed_ms = (time.perf_counter() - startup_start) * 1000
|
|
54
59
|
logger.info("[Startup] ✓ Ready (%.1fms)", startup_elapsed_ms)
|
|
55
60
|
|
|
@@ -61,7 +66,6 @@ def run_impl(
|
|
|
61
66
|
cfg,
|
|
62
67
|
prompt or "",
|
|
63
68
|
thread_id=thread_id,
|
|
64
|
-
output_format=output_format,
|
|
65
69
|
autonomous=autonomous,
|
|
66
70
|
max_iterations=max_iterations,
|
|
67
71
|
)
|
|
@@ -19,6 +19,11 @@ from soothe_sdk.client import WebSocketClient, is_daemon_live, websocket_url_fro
|
|
|
19
19
|
|
|
20
20
|
from soothe_cli.shared import load_config
|
|
21
21
|
|
|
22
|
+
thread_app = typer.Typer(
|
|
23
|
+
name="thread",
|
|
24
|
+
help="Inspect conversation threads (read-only diagnostics)",
|
|
25
|
+
)
|
|
26
|
+
|
|
22
27
|
# Display limits for thread list
|
|
23
28
|
_TOPIC_DISPLAY_LIMIT = 30 # Max chars for last human message
|
|
24
29
|
_TOPIC_TRUNCATE_KEEP = 27 # Leave room for "..."
|
|
@@ -116,6 +121,7 @@ def _echo_thread_table(rows: list[dict[str, object]]) -> None:
|
|
|
116
121
|
typer.echo(f"{tid:<20} {t_status:<10} {created:<19} {last_msg:<19} {topic:<30}")
|
|
117
122
|
|
|
118
123
|
|
|
124
|
+
@thread_app.command("list")
|
|
119
125
|
def thread_list(
|
|
120
126
|
config: Annotated[
|
|
121
127
|
str | None,
|
|
@@ -186,6 +192,7 @@ def thread_list(
|
|
|
186
192
|
asyncio.run(_list())
|
|
187
193
|
|
|
188
194
|
|
|
195
|
+
@thread_app.command("show")
|
|
189
196
|
def thread_show(
|
|
190
197
|
thread_id: Annotated[str, typer.Argument(help="Thread ID to show.")],
|
|
191
198
|
config: Annotated[
|
|
@@ -239,6 +246,7 @@ def thread_show(
|
|
|
239
246
|
asyncio.run(_show())
|
|
240
247
|
|
|
241
248
|
|
|
249
|
+
@thread_app.command("export")
|
|
242
250
|
def thread_export(
|
|
243
251
|
thread_id: Annotated[str, typer.Argument(help="Thread ID to export.")],
|
|
244
252
|
output: Annotated[
|
|
@@ -309,6 +317,7 @@ def thread_export(
|
|
|
309
317
|
asyncio.run(_export())
|
|
310
318
|
|
|
311
319
|
|
|
320
|
+
@thread_app.command("stats")
|
|
312
321
|
def thread_stats(
|
|
313
322
|
thread_id: Annotated[str, typer.Argument(help="Thread ID.")],
|
|
314
323
|
config: Annotated[
|
|
@@ -368,6 +377,7 @@ def thread_stats(
|
|
|
368
377
|
asyncio.run(_stats())
|
|
369
378
|
|
|
370
379
|
|
|
380
|
+
@thread_app.command("artifacts")
|
|
371
381
|
def thread_artifacts(
|
|
372
382
|
thread_id: Annotated[str, typer.Argument(help="Thread ID to list artifacts for.")],
|
|
373
383
|
config: Annotated[
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""Daemon-based execution for headless mode.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Uses RFC-0019 EventProcessor with HeadlessCliRenderer (stdout: loop-tagged answers only).
|
|
4
4
|
Uses WebSocket transport (RFC-0013).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
7
9
|
import asyncio
|
|
8
|
-
import json
|
|
9
10
|
import logging
|
|
10
|
-
import
|
|
11
|
+
import os
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
import typer
|
|
@@ -17,7 +18,7 @@ from soothe_sdk.client import (
|
|
|
17
18
|
websocket_url_from_config,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
|
-
from soothe_cli.cli.
|
|
21
|
+
from soothe_cli.cli.execution.headless_renderer import HeadlessCliRenderer
|
|
21
22
|
from soothe_cli.shared import EventProcessor
|
|
22
23
|
from soothe_cli.shared.presentation_engine import PresentationEngine
|
|
23
24
|
from soothe_cli.shared.subagent_routing import parse_subagent_from_input
|
|
@@ -34,27 +35,28 @@ async def run_headless_via_daemon(
|
|
|
34
35
|
prompt: str,
|
|
35
36
|
*,
|
|
36
37
|
thread_id: str | None = None,
|
|
37
|
-
output_format: str = "text",
|
|
38
38
|
autonomous: bool = False,
|
|
39
39
|
max_iterations: int | None = None,
|
|
40
40
|
) -> int:
|
|
41
41
|
"""Run a single prompt by connecting to a running daemon.
|
|
42
42
|
|
|
43
43
|
Uses WebSocket transport for all connections (RFC-0013).
|
|
44
|
-
|
|
44
|
+
Headless output is RFC-614 loop-tagged main-graph assistant text only (IG-343).
|
|
45
45
|
"""
|
|
46
46
|
from soothe_sdk.client import WebSocketClient
|
|
47
47
|
|
|
48
48
|
ws_url = websocket_url_from_config(cfg)
|
|
49
49
|
client = WebSocketClient(url=ws_url)
|
|
50
|
-
|
|
50
|
+
final_output_mode = getattr(cfg, "final_output_mode", "streaming")
|
|
51
51
|
|
|
52
52
|
try:
|
|
53
53
|
await connect_websocket_with_retries(client)
|
|
54
|
+
cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
|
|
54
55
|
status_event = await bootstrap_thread_session(
|
|
55
56
|
client,
|
|
56
57
|
resume_thread_id=thread_id,
|
|
57
|
-
verbosity=
|
|
58
|
+
verbosity="normal",
|
|
59
|
+
workspace=cli_ws,
|
|
58
60
|
thread_status_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
59
61
|
subscription_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
60
62
|
)
|
|
@@ -69,25 +71,26 @@ async def run_headless_via_daemon(
|
|
|
69
71
|
|
|
70
72
|
subagent_name, cleaned_prompt = parse_subagent_from_input(prompt)
|
|
71
73
|
|
|
72
|
-
# Send the input
|
|
73
74
|
await asyncio.wait_for(
|
|
74
75
|
client.send_input(
|
|
75
76
|
cleaned_prompt if subagent_name else prompt,
|
|
76
77
|
autonomous=autonomous,
|
|
77
78
|
max_iterations=max_iterations,
|
|
78
|
-
|
|
79
|
+
preferred_subagent=subagent_name,
|
|
79
80
|
),
|
|
80
81
|
timeout=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
81
82
|
)
|
|
82
83
|
|
|
83
|
-
# Initialize RFC-0019 unified event processor with one PresentationEngine
|
|
84
|
-
# for pipeline + message gating (RFC-502).
|
|
85
84
|
presentation = PresentationEngine()
|
|
86
|
-
renderer =
|
|
87
|
-
processor = EventProcessor(
|
|
85
|
+
renderer = HeadlessCliRenderer()
|
|
86
|
+
processor = EventProcessor(
|
|
87
|
+
renderer,
|
|
88
|
+
final_output_mode=final_output_mode,
|
|
89
|
+
presentation_engine=presentation,
|
|
90
|
+
headless_output=True,
|
|
91
|
+
)
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
query_started = False # Track if we've seen the query start running
|
|
93
|
+
query_started = False
|
|
91
94
|
|
|
92
95
|
while True:
|
|
93
96
|
try:
|
|
@@ -104,13 +107,10 @@ async def run_headless_via_daemon(
|
|
|
104
107
|
|
|
105
108
|
event_type = event.get("type", "")
|
|
106
109
|
|
|
107
|
-
# IMMEDIATE error check - exit before any other processing
|
|
108
|
-
# This ensures errors before query starts return immediately (IG-181)
|
|
109
110
|
if event_type == "error":
|
|
110
111
|
typer.echo(f"Daemon error: {event.get('message', 'unknown')}", err=True)
|
|
111
112
|
return 1
|
|
112
113
|
|
|
113
|
-
# Check for soothe.error.* events before query starts
|
|
114
114
|
ev_data = event.get("data")
|
|
115
115
|
if (
|
|
116
116
|
not query_started
|
|
@@ -120,14 +120,11 @@ async def run_headless_via_daemon(
|
|
|
120
120
|
typer.echo(f"Daemon error: {ev_data.get('error', 'unknown')}", err=True)
|
|
121
121
|
return 1
|
|
122
122
|
|
|
123
|
-
# Handle status changes (need to track query_started for timeout)
|
|
124
123
|
if event_type == "status":
|
|
125
124
|
state = event.get("state", "")
|
|
126
125
|
if state == "running":
|
|
127
126
|
query_started = True
|
|
128
127
|
elif (state == "idle" and query_started) or state == "stopped":
|
|
129
|
-
# loop.completed (and stray message chunks) may arrive *after* idle on the
|
|
130
|
-
# WebSocket stream; draining avoids dropping completion + final stdout (test-case1).
|
|
131
128
|
loop_clock = asyncio.get_event_loop()
|
|
132
129
|
drain_deadline = loop_clock.time() + 2.5
|
|
133
130
|
while loop_clock.time() < drain_deadline:
|
|
@@ -137,45 +134,13 @@ async def run_headless_via_daemon(
|
|
|
137
134
|
break
|
|
138
135
|
if not nxt:
|
|
139
136
|
break
|
|
140
|
-
if output_format == "jsonl":
|
|
141
|
-
namespace = nxt.get("namespace", [])
|
|
142
|
-
mode = nxt.get("mode", "")
|
|
143
|
-
data = nxt.get("data")
|
|
144
|
-
sys.stdout.write(
|
|
145
|
-
json.dumps(
|
|
146
|
-
{"namespace": list(namespace), "mode": mode, "data": data},
|
|
147
|
-
default=str,
|
|
148
|
-
)
|
|
149
|
-
+ "\n"
|
|
150
|
-
)
|
|
151
|
-
sys.stdout.flush()
|
|
152
|
-
continue
|
|
153
137
|
processor.process_event(nxt)
|
|
154
138
|
|
|
155
|
-
processor.process_event(event)
|
|
139
|
+
processor.process_event(event)
|
|
156
140
|
break
|
|
157
141
|
|
|
158
|
-
# JSONL output bypass processor
|
|
159
|
-
if output_format == "jsonl":
|
|
160
|
-
namespace = event.get("namespace", [])
|
|
161
|
-
mode = event.get("mode", "")
|
|
162
|
-
data = event.get("data")
|
|
163
|
-
sys.stdout.write(
|
|
164
|
-
json.dumps(
|
|
165
|
-
{"namespace": list(namespace), "mode": mode, "data": data}, default=str
|
|
166
|
-
)
|
|
167
|
-
+ "\n"
|
|
168
|
-
)
|
|
169
|
-
sys.stdout.flush()
|
|
170
|
-
continue
|
|
171
|
-
|
|
172
|
-
# Delegate to unified event processor
|
|
173
142
|
processor.process_event(event)
|
|
174
143
|
|
|
175
|
-
# Note: Final newline is handled by renderer.on_turn_end() called
|
|
176
|
-
# when status changes to idle/stopped in _handle_status().
|
|
177
|
-
# Daemon lifecycle remains silent in normal headless mode.
|
|
178
|
-
|
|
179
144
|
except (ConnectionError, OSError, TimeoutError) as e:
|
|
180
145
|
logger.exception("Daemon connection failed")
|
|
181
146
|
from soothe_sdk.utils import format_cli_error
|
|
@@ -189,6 +154,6 @@ async def run_headless_via_daemon(
|
|
|
189
154
|
typer.echo(f"Error: {format_cli_error(e)}", err=True)
|
|
190
155
|
return 1
|
|
191
156
|
else:
|
|
192
|
-
return
|
|
157
|
+
return 0
|
|
193
158
|
finally:
|
|
194
159
|
await client.close()
|
|
@@ -23,13 +23,12 @@ def run_headless(
|
|
|
23
23
|
prompt: str,
|
|
24
24
|
*,
|
|
25
25
|
thread_id: str | None = None,
|
|
26
|
-
output_format: str = "text",
|
|
27
26
|
autonomous: bool = False,
|
|
28
27
|
max_iterations: int | None = None,
|
|
29
28
|
) -> None:
|
|
30
29
|
"""Run a single prompt with streaming output and progress events.
|
|
31
30
|
|
|
32
|
-
Connects to running daemon via WebSocket if available to avoid
|
|
31
|
+
Connects to running daemon via WebSocket if available to avoid database lock conflicts.
|
|
33
32
|
Auto-starts daemon if not running (RFC-0013 daemon lifecycle).
|
|
34
33
|
|
|
35
34
|
Note (RFC-0013): Daemon persists after request completion. Use 'soothed stop'
|
|
@@ -41,8 +40,8 @@ def run_headless(
|
|
|
41
40
|
ws_url = websocket_url_from_config(cfg)
|
|
42
41
|
|
|
43
42
|
# Auto-start daemon if not running (RFC-0013) - WebSocket RPC checks (IG-174 Phase 1)
|
|
44
|
-
async def
|
|
45
|
-
"""
|
|
43
|
+
async def _run_headless_pipeline() -> int:
|
|
44
|
+
"""Ensure daemon is reachable, then run the headless daemon session."""
|
|
46
45
|
daemon_live = await is_daemon_live(ws_url, timeout=5.0)
|
|
47
46
|
|
|
48
47
|
if not daemon_live:
|
|
@@ -75,19 +74,15 @@ def run_headless(
|
|
|
75
74
|
# Note: We don't fail here - let the connection attempt handle errors
|
|
76
75
|
# This allows tests and edge cases to proceed with mocked daemons
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# Connect to daemon and execute
|
|
81
|
-
daemon_exit_code = asyncio.run(
|
|
82
|
-
run_headless_via_daemon(
|
|
77
|
+
return await run_headless_via_daemon(
|
|
83
78
|
cfg,
|
|
84
79
|
prompt,
|
|
85
80
|
thread_id=thread_id,
|
|
86
|
-
output_format=output_format,
|
|
87
81
|
autonomous=autonomous,
|
|
88
82
|
max_iterations=max_iterations,
|
|
89
83
|
)
|
|
90
|
-
|
|
84
|
+
|
|
85
|
+
daemon_exit_code = asyncio.run(_run_headless_pipeline())
|
|
91
86
|
|
|
92
87
|
# Handle daemon fallback (unresponsive daemon)
|
|
93
88
|
if daemon_exit_code == _DAEMON_FALLBACK_EXIT_CODE:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Minimal stdout-only renderer for headless CLI (IG-343).
|
|
2
|
+
|
|
3
|
+
Emits RFC-614 loop-tagged assistant text for the main graph (empty LangGraph namespace)
|
|
4
|
+
and loop-tagged finals (including replayed ``goal_completion`` from IG-355). Subgraph
|
|
5
|
+
namespaced prose is suppressed unless loop-tagged. Stderr is used for errors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from soothe_cli.shared.renderer_base import RendererBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HeadlessCliRenderer(RendererBase):
|
|
19
|
+
"""Headless mode: clean assistant output on stdout, errors on stderr."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.console = Console()
|
|
24
|
+
|
|
25
|
+
def on_assistant_text(
|
|
26
|
+
self,
|
|
27
|
+
text: str,
|
|
28
|
+
*,
|
|
29
|
+
is_main: bool,
|
|
30
|
+
is_streaming: bool,
|
|
31
|
+
task_scope: tuple[str, str] | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
if not is_main or task_scope:
|
|
34
|
+
return
|
|
35
|
+
payload = text if is_streaming else self.repair_concatenated_output(text)
|
|
36
|
+
if not payload:
|
|
37
|
+
return
|
|
38
|
+
sys.stdout.write(payload)
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
|
|
41
|
+
def on_streaming_output(
|
|
42
|
+
self,
|
|
43
|
+
event_type: str,
|
|
44
|
+
text: str,
|
|
45
|
+
*,
|
|
46
|
+
is_chunk: bool,
|
|
47
|
+
namespace: tuple[str, ...],
|
|
48
|
+
) -> None:
|
|
49
|
+
self.on_assistant_text(text, is_main=True, is_streaming=is_chunk)
|
|
50
|
+
|
|
51
|
+
def on_tool_call(
|
|
52
|
+
self,
|
|
53
|
+
name: str,
|
|
54
|
+
args: dict[str, Any],
|
|
55
|
+
tool_call_id: str,
|
|
56
|
+
*,
|
|
57
|
+
is_main: bool,
|
|
58
|
+
task_scope: tuple[str, str] | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
del name, args, tool_call_id, is_main, task_scope
|
|
61
|
+
|
|
62
|
+
def on_tool_result(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
result: str,
|
|
66
|
+
tool_call_id: str,
|
|
67
|
+
*,
|
|
68
|
+
is_error: bool,
|
|
69
|
+
is_main: bool,
|
|
70
|
+
task_scope: tuple[str, str] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
del name, result, tool_call_id, is_error, is_main, task_scope
|
|
73
|
+
|
|
74
|
+
def on_status_change(self, state: str) -> None:
|
|
75
|
+
del state
|
|
76
|
+
|
|
77
|
+
def on_error(self, error: str, *, context: str | None = None) -> None:
|
|
78
|
+
prefix = f"[{context}] " if context else ""
|
|
79
|
+
sys.stderr.write(f"{prefix}ERROR: {error}\n")
|
|
80
|
+
sys.stderr.flush()
|
|
81
|
+
|
|
82
|
+
def on_progress_event(
|
|
83
|
+
self,
|
|
84
|
+
event_type: str,
|
|
85
|
+
data: dict[str, Any],
|
|
86
|
+
*,
|
|
87
|
+
namespace: tuple[str, ...],
|
|
88
|
+
task_scope: tuple[str, str] | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
del event_type, data, namespace, task_scope
|
|
91
|
+
|
|
92
|
+
def on_plan_created(self, plan: Any) -> None:
|
|
93
|
+
del plan
|
|
94
|
+
|
|
95
|
+
def on_plan_step_started(self, step_id: str, description: str) -> None:
|
|
96
|
+
del step_id, description
|
|
97
|
+
|
|
98
|
+
def on_plan_step_completed(
|
|
99
|
+
self,
|
|
100
|
+
step_id: str,
|
|
101
|
+
success: bool,
|
|
102
|
+
duration_ms: int,
|
|
103
|
+
) -> None:
|
|
104
|
+
del step_id, success, duration_ms
|
|
105
|
+
|
|
106
|
+
def on_turn_end(self) -> None:
|
|
107
|
+
"""End of turn; headless does not append synthetic newlines to stdout."""
|