soothe-cli 0.4.9__tar.gz → 0.5.1__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.9 → soothe_cli-0.5.1}/PKG-INFO +2 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/pyproject.toml +1 -1
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/autopilot_cmd.py +2 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/loop_cmd.py +18 -33
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/run_cmd.py +7 -5
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/daemon.py +22 -10
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/headless.py +2 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/launcher.py +2 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/main.py +1 -5
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/display_line.py +2 -5
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/formatter.py +3 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/pipeline.py +1 -1
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/__init__.py +0 -4
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/command_router.py +33 -14
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/slash_commands.py +15 -66
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/event_processor.py +12 -13
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/presentation_engine.py +4 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/processor_state.py +3 -3
- soothe_cli-0.5.1/src/soothe_cli/shared/duration_format.py +42 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/tui_trace_log.py +1 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/web.py +50 -1
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_app.py +15 -17
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_commands.py +15 -23
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_execution.py +24 -22
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_history.py +97 -104
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_messages_mixin.py +32 -49
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_model.py +69 -74
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_module_init.py +24 -30
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_startup.py +55 -72
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/app.tcss +10 -1
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/command_registry.py +2 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/config.py +4 -4
- soothe_cli-0.5.1/src/soothe_cli/tui/daemon_session.py +365 -0
- soothe_cli-0.5.1/src/soothe_cli/tui/formatting.py +14 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/model_config.py +17 -85
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/output.py +1 -1
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/preview_limits.py +5 -0
- soothe_cli-0.5.1/src/soothe_cli/tui/sessions.py +380 -0
- soothe_cli-0.5.1/src/soothe_cli/tui/textual_adapter/__init__.py +111 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -7
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_turn.py +7 -39
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +0 -3
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/theme.py +157 -8
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/approval.py +2 -2
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/history.py +1 -1
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/loop_selector.py +4 -5
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/message_store.py +13 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/messages.py +439 -99
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/welcome.py +49 -32
- soothe_cli-0.4.9/src/soothe_cli/cli/commands/thread_cmd.py +0 -412
- soothe_cli-0.4.9/src/soothe_cli/tui/daemon_session.py +0 -331
- soothe_cli-0.4.9/src/soothe_cli/tui/formatting.py +0 -28
- soothe_cli-0.4.9/src/soothe_cli/tui/sessions.py +0 -1359
- soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/__init__.py +0 -29
- soothe_cli-0.4.9/src/soothe_cli/tui/widgets/thread_selector.py +0 -1817
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/.gitignore +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/README.md +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/task_scope.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/config_loader.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/display_policy.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/essential_events.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/rendering/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/_utils.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/message_processing.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/rendering.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.4.9 → soothe_cli-0.5.1}/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.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -21,7 +21,7 @@ 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
|
|
24
|
-
Requires-Dist: soothe-sdk<1.0.0,>=0.
|
|
24
|
+
Requires-Dist: soothe-sdk<1.0.0,>=0.5.0
|
|
25
25
|
Requires-Dist: textual>=8.0.0
|
|
26
26
|
Requires-Dist: typer<1.0.0,>=0.9.0
|
|
27
27
|
Requires-Dist: websockets>=12.0
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"soothe-sdk>=0.
|
|
26
|
+
"soothe-sdk>=0.5.0,<1.0.0", # WebSocket client, protocol, types
|
|
27
27
|
"typer>=0.9.0,<1.0.0", # CLI framework
|
|
28
28
|
"textual>=8.0.0", # TUI framework
|
|
29
29
|
"rich>=13.0.0", # Console output
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
"""Loop management CLI commands for
|
|
2
|
-
|
|
3
|
-
Replaces thread-based commands with loop-based commands.
|
|
4
|
-
Users interact with loops (threads are internal implementation detail).
|
|
1
|
+
"""Loop management CLI commands for AgentLoop instances.
|
|
5
2
|
|
|
6
3
|
RFC-503: Loop-First User Experience
|
|
7
4
|
RFC-504: Loop Management CLI Commands
|
|
8
5
|
|
|
9
|
-
All loop operations
|
|
10
|
-
The daemon must be running for loop commands to work.
|
|
6
|
+
All loop operations use daemon WebSocket RPC; the daemon must be running.
|
|
11
7
|
"""
|
|
12
8
|
|
|
13
9
|
from __future__ import annotations
|
|
@@ -145,8 +141,6 @@ def list_loops(
|
|
|
145
141
|
) -> None:
|
|
146
142
|
"""List all AgentLoop instances.
|
|
147
143
|
|
|
148
|
-
Replaces: soothe thread list
|
|
149
|
-
|
|
150
144
|
Examples:
|
|
151
145
|
soothe loop list
|
|
152
146
|
soothe loop list --status running
|
|
@@ -178,7 +172,7 @@ def list_loops(
|
|
|
178
172
|
table = Table(title="AgentLoops")
|
|
179
173
|
table.add_column("Loop ID", style="cyan")
|
|
180
174
|
table.add_column("Status", style="green")
|
|
181
|
-
table.add_column("
|
|
175
|
+
table.add_column("Contexts", justify="right")
|
|
182
176
|
table.add_column("Goals", justify="right")
|
|
183
177
|
table.add_column("Switches", justify="right")
|
|
184
178
|
table.add_column("Created", style="dim")
|
|
@@ -206,8 +200,6 @@ def describe_loop(
|
|
|
206
200
|
) -> None:
|
|
207
201
|
"""Show detailed loop information.
|
|
208
202
|
|
|
209
|
-
Replaces: soothe thread describe
|
|
210
|
-
|
|
211
203
|
Example:
|
|
212
204
|
soothe loop show loop_abc123
|
|
213
205
|
soothe loop show loop_abc123 --verbose
|
|
@@ -245,13 +237,12 @@ def describe_loop(
|
|
|
245
237
|
)
|
|
246
238
|
)
|
|
247
239
|
|
|
248
|
-
#
|
|
249
|
-
# RFC-503: Hide thread IDs, show only thread count
|
|
240
|
+
# Internal checkpoint context counts from loop metadata
|
|
250
241
|
console.print(
|
|
251
242
|
Panel(
|
|
252
|
-
f"Internal
|
|
253
|
-
f"
|
|
254
|
-
title="
|
|
243
|
+
f"Internal contexts: {len(loop.get('thread_ids', []))}\n"
|
|
244
|
+
f"Context switches: {loop.get('total_thread_switches', 0)}",
|
|
245
|
+
title="Checkpoint contexts (internal)",
|
|
255
246
|
border_style="dim",
|
|
256
247
|
)
|
|
257
248
|
)
|
|
@@ -260,7 +251,7 @@ def describe_loop(
|
|
|
260
251
|
console.print(
|
|
261
252
|
Panel(
|
|
262
253
|
f"Goals Completed: {loop.get('total_goals_completed', 0)}\n"
|
|
263
|
-
f"
|
|
254
|
+
f"Context switches: {loop.get('total_thread_switches', 0)}\n"
|
|
264
255
|
f"Duration: {format_duration(loop.get('total_duration_ms', 0))}\n"
|
|
265
256
|
f"Tokens Used: {format_tokens(loop.get('total_tokens_used', 0))}",
|
|
266
257
|
title="Execution Summary",
|
|
@@ -419,9 +410,7 @@ def delete_loop(
|
|
|
419
410
|
) -> None:
|
|
420
411
|
"""Delete loop entirely.
|
|
421
412
|
|
|
422
|
-
Removes loop directory
|
|
423
|
-
|
|
424
|
-
Replaces: soothe thread delete
|
|
413
|
+
Removes this loop's run directory and related artifacts.
|
|
425
414
|
|
|
426
415
|
Example:
|
|
427
416
|
soothe loop delete loop_abc123
|
|
@@ -454,7 +443,7 @@ def delete_loop(
|
|
|
454
443
|
console.print(
|
|
455
444
|
f"[warning]Warning: This will permanently delete {loop_id} and all associated data:[/warning]"
|
|
456
445
|
)
|
|
457
|
-
console.print(f" - {len(loop.get('thread_ids', []))} internal
|
|
446
|
+
console.print(f" - {len(loop.get('thread_ids', []))} internal checkpoint contexts")
|
|
458
447
|
console.print(f" - {loop.get('total_goals_completed', 0)} goal execution records")
|
|
459
448
|
console.print(" - Working memory spills")
|
|
460
449
|
|
|
@@ -481,9 +470,7 @@ def delete_loop(
|
|
|
481
470
|
console.print(" Removed checkpoint database")
|
|
482
471
|
console.print(" Removed metadata")
|
|
483
472
|
console.print(" Removed working memory spills")
|
|
484
|
-
console.print(
|
|
485
|
-
"[dim] Preserved thread checkpoints (run `soothe thread delete` to remove)[/dim]"
|
|
486
|
-
)
|
|
473
|
+
console.print("[dim] LangGraph checkpoints may remain until pruned separately[/dim]")
|
|
487
474
|
|
|
488
475
|
|
|
489
476
|
# Helper functions
|
|
@@ -569,7 +556,7 @@ def format_anchor_summary(anchors: list[dict[str, Any]]) -> str:
|
|
|
569
556
|
line = f" iteration {anchor['iteration']}: [dim]{anchor['checkpoint_id']}[/dim] "
|
|
570
557
|
line += f"({anchor['anchor_type']})"
|
|
571
558
|
|
|
572
|
-
#
|
|
559
|
+
# Context refresh when loop scope (LangGraph thread_id) changes between anchors
|
|
573
560
|
if anchor["iteration"] > 0:
|
|
574
561
|
prev_anchors = [a for a in anchors if a["iteration"] == anchor["iteration"] - 1]
|
|
575
562
|
if prev_anchors and prev_anchors[0]["thread_id"] != anchor["thread_id"]:
|
|
@@ -587,10 +574,10 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
|
|
|
587
574
|
for iteration in main_line:
|
|
588
575
|
iter_num = iteration["iteration"]
|
|
589
576
|
|
|
590
|
-
#
|
|
577
|
+
# Iteration marker (IDs omitted in UI)
|
|
591
578
|
console.print(f" iteration {iter_num}")
|
|
592
579
|
|
|
593
|
-
#
|
|
580
|
+
# Context refresh when the tree marks a switch
|
|
594
581
|
if iteration.get("thread_switch"):
|
|
595
582
|
console.print(" [cyan][context refreshed][/cyan]")
|
|
596
583
|
|
|
@@ -610,7 +597,7 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
|
|
|
610
597
|
console.print("\n[bold red]Failed Branches:[/bold red]")
|
|
611
598
|
|
|
612
599
|
for branch in branches:
|
|
613
|
-
#
|
|
600
|
+
# Branch identity only (no per-anchor checkpoint id in UI)
|
|
614
601
|
console.print(f" [dim]{branch['branch_id']}[/dim] (iteration {branch['iteration']})")
|
|
615
602
|
console.print(f" ├─ [dim]{branch['root_checkpoint']}[/dim] [root] ← Rewind point")
|
|
616
603
|
|
|
@@ -689,8 +676,6 @@ def continue_loop(
|
|
|
689
676
|
) -> None:
|
|
690
677
|
"""Continue execution on existing loop.
|
|
691
678
|
|
|
692
|
-
Replaces: soothe thread continue <thread_id>
|
|
693
|
-
|
|
694
679
|
Behavior:
|
|
695
680
|
- Resolve target loop (explicit `LOOP_ID` or most-recent loop)
|
|
696
681
|
- Launch TUI on that loop
|
|
@@ -709,7 +694,7 @@ def continue_loop(
|
|
|
709
694
|
|
|
710
695
|
run_impl(
|
|
711
696
|
prompt=prompt,
|
|
712
|
-
|
|
697
|
+
resume_loop_id=resolved_loop_id,
|
|
713
698
|
no_tui=False,
|
|
714
699
|
autonomous=False,
|
|
715
700
|
max_iterations=None,
|
|
@@ -724,7 +709,7 @@ def detach_loop(
|
|
|
724
709
|
|
|
725
710
|
Behavior:
|
|
726
711
|
- Unsubscribe client from loop events
|
|
727
|
-
- Loop
|
|
712
|
+
- Loop keeps running on the daemon
|
|
728
713
|
- Loop checkpoint saved at detachment point
|
|
729
714
|
- Client can reattach later with 'soothe loop attach'
|
|
730
715
|
|
|
@@ -807,7 +792,7 @@ def attach_loop(
|
|
|
807
792
|
Panel(
|
|
808
793
|
f"Status: {loop.get('status', 'unknown')}\n"
|
|
809
794
|
f"Goals: {loop.get('total_goals_completed', 0)} completed\n"
|
|
810
|
-
f"Internal
|
|
795
|
+
f"Internal contexts: {len(loop.get('thread_ids', []))}",
|
|
811
796
|
title=f"Loop: {loop_id} (Reattached)",
|
|
812
797
|
)
|
|
813
798
|
)
|
|
@@ -17,18 +17,20 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
def run_impl(
|
|
19
19
|
prompt: str | None,
|
|
20
|
-
|
|
20
|
+
resume_loop_id: str | None,
|
|
21
21
|
no_tui: bool, # noqa: FBT001
|
|
22
22
|
autonomous: bool, # noqa: FBT001
|
|
23
23
|
max_iterations: int | None,
|
|
24
24
|
streaming_enabled: bool | None = None,
|
|
25
25
|
streaming_mode: str | None = None,
|
|
26
|
+
*,
|
|
27
|
+
config_path: str | None = None,
|
|
26
28
|
) -> None:
|
|
27
29
|
"""Core implementation for running Soothe agent.
|
|
28
30
|
|
|
29
31
|
Args:
|
|
30
32
|
prompt: Optional prompt for headless mode
|
|
31
|
-
|
|
33
|
+
resume_loop_id: Existing loop id to attach to (optional)
|
|
32
34
|
no_tui: Force headless mode
|
|
33
35
|
autonomous: Enable autonomous iteration mode
|
|
34
36
|
max_iterations: Max iterations for autonomous mode
|
|
@@ -38,7 +40,7 @@ def run_impl(
|
|
|
38
40
|
startup_start = time.perf_counter()
|
|
39
41
|
|
|
40
42
|
try:
|
|
41
|
-
cfg = load_config()
|
|
43
|
+
cfg = load_config(config_path)
|
|
42
44
|
log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
|
|
43
45
|
log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
|
|
44
46
|
setup_logging(log_level, log_file=log_file)
|
|
@@ -65,13 +67,13 @@ def run_impl(
|
|
|
65
67
|
run_headless(
|
|
66
68
|
cfg,
|
|
67
69
|
prompt or "",
|
|
68
|
-
|
|
70
|
+
resume_loop_id=resume_loop_id,
|
|
69
71
|
autonomous=autonomous,
|
|
70
72
|
max_iterations=max_iterations,
|
|
71
73
|
)
|
|
72
74
|
else:
|
|
73
75
|
# TUI mode (with optional initial prompt)
|
|
74
|
-
run_tui(cfg,
|
|
76
|
+
run_tui(cfg, resume_loop_id=resume_loop_id, initial_prompt=prompt)
|
|
75
77
|
|
|
76
78
|
run_elapsed_s = time.perf_counter() - run_start
|
|
77
79
|
typer.echo(f"Total running time: {run_elapsed_s:.2f}s", err=True)
|
|
@@ -13,7 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import typer
|
|
15
15
|
from soothe_sdk.client import (
|
|
16
|
-
|
|
16
|
+
bootstrap_loop_session,
|
|
17
17
|
connect_websocket_with_retries,
|
|
18
18
|
websocket_url_from_config,
|
|
19
19
|
)
|
|
@@ -26,15 +26,23 @@ from soothe_cli.shared.core.presentation_engine import PresentationEngine
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
_DAEMON_FALLBACK_EXIT_CODE = 42
|
|
29
|
-
_SESSION_BOOTSTRAP_TIMEOUT_S =
|
|
29
|
+
_SESSION_BOOTSTRAP_TIMEOUT_S = 30.0
|
|
30
30
|
_QUERY_START_TIMEOUT_S = 20.0
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
|
|
34
|
+
"""Return whether a daemon frame belongs to the active AgentLoop session."""
|
|
35
|
+
event_type = event.get("type", "")
|
|
36
|
+
if event_type not in {"status", "event"}:
|
|
37
|
+
return True
|
|
38
|
+
return event.get("loop_id") == active_loop_id
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
async def run_headless_via_daemon(
|
|
34
42
|
cfg: Any,
|
|
35
43
|
prompt: str,
|
|
36
44
|
*,
|
|
37
|
-
|
|
45
|
+
resume_loop_id: str | None = None,
|
|
38
46
|
autonomous: bool = False,
|
|
39
47
|
max_iterations: int | None = None,
|
|
40
48
|
) -> int:
|
|
@@ -51,27 +59,27 @@ async def run_headless_via_daemon(
|
|
|
51
59
|
try:
|
|
52
60
|
await connect_websocket_with_retries(client)
|
|
53
61
|
cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
|
|
54
|
-
status_event = await
|
|
62
|
+
status_event = await bootstrap_loop_session(
|
|
55
63
|
client,
|
|
56
|
-
|
|
64
|
+
resume_loop_id=resume_loop_id,
|
|
57
65
|
verbosity="normal",
|
|
58
66
|
workspace=cli_ws,
|
|
59
|
-
|
|
60
|
-
subscription_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
67
|
+
subscribe_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
61
68
|
)
|
|
62
69
|
if status_event.get("type") == "error":
|
|
63
70
|
typer.echo(f"Daemon error: {status_event.get('message', 'unknown')}", err=True)
|
|
64
71
|
return 1
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
if not
|
|
68
|
-
typer.echo("Error: No
|
|
73
|
+
active_loop_id = status_event.get("loop_id")
|
|
74
|
+
if not active_loop_id:
|
|
75
|
+
typer.echo("Error: No loop_id after session bootstrap", err=True)
|
|
69
76
|
return 1
|
|
70
77
|
|
|
71
78
|
subagent_name, cleaned_prompt = parse_subagent_from_input(prompt)
|
|
72
79
|
|
|
73
80
|
await asyncio.wait_for(
|
|
74
81
|
client.send_input(
|
|
82
|
+
active_loop_id,
|
|
75
83
|
cleaned_prompt if subagent_name else prompt,
|
|
76
84
|
autonomous=autonomous,
|
|
77
85
|
max_iterations=max_iterations,
|
|
@@ -104,6 +112,8 @@ async def run_headless_via_daemon(
|
|
|
104
112
|
break
|
|
105
113
|
|
|
106
114
|
event_type = event.get("type", "")
|
|
115
|
+
if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
|
|
116
|
+
continue
|
|
107
117
|
|
|
108
118
|
if event_type == "error":
|
|
109
119
|
typer.echo(f"Daemon error: {event.get('message', 'unknown')}", err=True)
|
|
@@ -132,6 +142,8 @@ async def run_headless_via_daemon(
|
|
|
132
142
|
break
|
|
133
143
|
if not nxt:
|
|
134
144
|
break
|
|
145
|
+
if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
|
|
146
|
+
continue
|
|
135
147
|
processor.process_event(nxt)
|
|
136
148
|
|
|
137
149
|
processor.process_event(event)
|
|
@@ -22,7 +22,7 @@ def run_headless(
|
|
|
22
22
|
cfg: CLIConfig,
|
|
23
23
|
prompt: str,
|
|
24
24
|
*,
|
|
25
|
-
|
|
25
|
+
resume_loop_id: str | None = None,
|
|
26
26
|
autonomous: bool = False,
|
|
27
27
|
max_iterations: int | None = None,
|
|
28
28
|
) -> None:
|
|
@@ -77,7 +77,7 @@ def run_headless(
|
|
|
77
77
|
return await run_headless_via_daemon(
|
|
78
78
|
cfg,
|
|
79
79
|
prompt,
|
|
80
|
-
|
|
80
|
+
resume_loop_id=resume_loop_id,
|
|
81
81
|
autonomous=autonomous,
|
|
82
82
|
max_iterations=max_iterations,
|
|
83
83
|
)
|
|
@@ -10,7 +10,7 @@ from soothe_cli.config import CLIConfig
|
|
|
10
10
|
def run_tui(
|
|
11
11
|
cfg: CLIConfig,
|
|
12
12
|
*,
|
|
13
|
-
|
|
13
|
+
resume_loop_id: str | None = None,
|
|
14
14
|
initial_prompt: str | None = None,
|
|
15
15
|
) -> None:
|
|
16
16
|
"""Launch the Textual TUI (with daemon auto-start)."""
|
|
@@ -19,7 +19,7 @@ def run_tui(
|
|
|
19
19
|
|
|
20
20
|
run_textual_tui(
|
|
21
21
|
config=cfg,
|
|
22
|
-
|
|
22
|
+
resume_loop_id=resume_loop_id,
|
|
23
23
|
initial_prompt=initial_prompt,
|
|
24
24
|
)
|
|
25
25
|
except ImportError:
|
|
@@ -106,7 +106,7 @@ def main(
|
|
|
106
106
|
|
|
107
107
|
run_impl(
|
|
108
108
|
prompt=prompt,
|
|
109
|
-
|
|
109
|
+
resume_loop_id=None,
|
|
110
110
|
no_tui=no_tui,
|
|
111
111
|
autonomous=False,
|
|
112
112
|
max_iterations=None,
|
|
@@ -118,15 +118,11 @@ def main(
|
|
|
118
118
|
# ---------------------------------------------------------------------------
|
|
119
119
|
# Sub-command groups (nested Typer apps)
|
|
120
120
|
# ---------------------------------------------------------------------------
|
|
121
|
-
# Thread: read-only diagnostics per RFC-503 (Loop-First UX). Lifecycle
|
|
122
|
-
# management lives under `soothe loop <subcommand>`.
|
|
123
121
|
|
|
124
122
|
from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
|
|
125
123
|
from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
|
|
126
|
-
from soothe_cli.cli.commands.thread_cmd import thread_app as _thread_app # noqa: E402
|
|
127
124
|
|
|
128
125
|
for _sub_app, _name in (
|
|
129
|
-
(_thread_app, "thread"),
|
|
130
126
|
(_loop_app, "loop"),
|
|
131
127
|
(_autopilot_app, "autopilot"),
|
|
132
128
|
):
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
from soothe_cli.shared.duration_format import format_duration_ms
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@dataclass
|
|
@@ -44,10 +44,7 @@ class DisplayLine:
|
|
|
44
44
|
parts.append(f" [{self.status}]")
|
|
45
45
|
|
|
46
46
|
if self.duration_ms is not None:
|
|
47
|
-
|
|
48
|
-
parts.append(f" ({self.duration_ms / _MS_PER_SECOND:.1f}s)")
|
|
49
|
-
else:
|
|
50
|
-
parts.append(f" ({self.duration_ms}ms)")
|
|
47
|
+
parts.append(f" ({format_duration_ms(self.duration_ms)})")
|
|
51
48
|
|
|
52
49
|
return "".join(parts)
|
|
53
50
|
|
|
@@ -7,6 +7,7 @@ from soothe_cli.cli.stream.task_scope import (
|
|
|
7
7
|
format_task_scope_prefix,
|
|
8
8
|
format_task_subagent_line,
|
|
9
9
|
)
|
|
10
|
+
from soothe_cli.shared.duration_format import format_duration_ms
|
|
10
11
|
|
|
11
12
|
# Emoji presentation for step-done success (U+2705 + VS16); distinct from ✓ tool rows.
|
|
12
13
|
_STEP_DONE_OK_MARK = "\u2705\ufe0f"
|
|
@@ -101,7 +102,7 @@ def format_subagent_done(
|
|
|
101
102
|
) -> DisplayLine:
|
|
102
103
|
"""Format a subagent completion line with metrics.
|
|
103
104
|
|
|
104
|
-
With Task scope: ``⚙ Task(type, \"…\") -> ✓ Completed (
|
|
105
|
+
With Task scope: ``⚙ Task(type, \"…\") -> ✓ Completed (human duration)`` using wire task description
|
|
105
106
|
when provided; falls back to summary text inside quotes.
|
|
106
107
|
Without scope: legacy ``✓ …`` row with triple markers.
|
|
107
108
|
|
|
@@ -123,7 +124,7 @@ def format_subagent_done(
|
|
|
123
124
|
ms = max(0, int(duration_s * 1000))
|
|
124
125
|
outcome = "✓ Completed" if task_done_success else "✗ Failed"
|
|
125
126
|
tail = (answer_summary or "").strip()
|
|
126
|
-
base = f"{quoted} -> {outcome} ({ms}
|
|
127
|
+
base = f"{quoted} -> {outcome} ({format_duration_ms(ms)})"
|
|
127
128
|
content = f"{base}: {tail}" if tail else base
|
|
128
129
|
return DisplayLine(
|
|
129
130
|
level=2,
|
|
@@ -159,7 +159,7 @@ class StreamDisplayPipeline:
|
|
|
159
159
|
def _task_scope_from_event(self, event: dict[str, Any]) -> tuple[str, str] | None:
|
|
160
160
|
"""Extract IG-334 ``(task_tool_call_id, subagent_type)`` when attached by the renderer."""
|
|
161
161
|
ts = event.get("task_scope")
|
|
162
|
-
if isinstance(ts, tuple) and len(ts) == 2:
|
|
162
|
+
if isinstance(ts, (list, tuple)) and len(ts) == 2:
|
|
163
163
|
a, b = ts
|
|
164
164
|
if isinstance(a, str) and isinstance(b, str):
|
|
165
165
|
return (a, b)
|
|
@@ -21,8 +21,6 @@ from soothe_sdk.utils import setup_logging
|
|
|
21
21
|
# Import from commands subdirectory
|
|
22
22
|
from soothe_cli.shared.commands.slash_commands import (
|
|
23
23
|
KEYBOARD_SHORTCUTS,
|
|
24
|
-
SLASH_COMMANDS,
|
|
25
|
-
parse_autonomous_command,
|
|
26
24
|
show_commands,
|
|
27
25
|
show_config,
|
|
28
26
|
show_history,
|
|
@@ -108,8 +106,6 @@ __all__ = [
|
|
|
108
106
|
"update_name_map_from_tool_calls",
|
|
109
107
|
# Slash commands (IG-176)
|
|
110
108
|
"KEYBOARD_SHORTCUTS",
|
|
111
|
-
"SLASH_COMMANDS",
|
|
112
|
-
"parse_autonomous_command",
|
|
113
109
|
"show_commands",
|
|
114
110
|
"show_config",
|
|
115
111
|
"show_history",
|
|
@@ -42,7 +42,7 @@ def parse_slash_command(input_text: str) -> tuple[str, str | None]:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def validate_command(
|
|
45
|
-
entry: dict[str, Any], command: str, query: str | None,
|
|
45
|
+
entry: dict[str, Any], command: str, query: str | None, loop_id: str | None
|
|
46
46
|
) -> tuple[bool, str | None]:
|
|
47
47
|
"""Validate command before routing.
|
|
48
48
|
|
|
@@ -50,14 +50,13 @@ def validate_command(
|
|
|
50
50
|
entry: Command registry entry
|
|
51
51
|
command: Command name
|
|
52
52
|
query: Query parameter (if present)
|
|
53
|
-
|
|
53
|
+
loop_id: Active AgentLoop id for this session
|
|
54
54
|
|
|
55
55
|
Returns:
|
|
56
56
|
Tuple of (is_valid, error_message)
|
|
57
57
|
"""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return (False, "No active thread")
|
|
58
|
+
if entry.get("requires_loop") and not loop_id:
|
|
59
|
+
return (False, "No active loop")
|
|
61
60
|
|
|
62
61
|
# Check query requirement for routing commands
|
|
63
62
|
if entry.get("requires_query") and not query:
|
|
@@ -110,7 +109,13 @@ def parse_command_params(entry: dict[str, Any], query: str) -> dict[str, Any]:
|
|
|
110
109
|
return params
|
|
111
110
|
|
|
112
111
|
|
|
113
|
-
async def route_slash_command(
|
|
112
|
+
async def route_slash_command(
|
|
113
|
+
cmd_input: str,
|
|
114
|
+
console: Console,
|
|
115
|
+
client: WebSocketClient,
|
|
116
|
+
*,
|
|
117
|
+
loop_id: str | None = None,
|
|
118
|
+
) -> bool:
|
|
114
119
|
"""Route slash command based on registry metadata (RFC-404).
|
|
115
120
|
|
|
116
121
|
Args:
|
|
@@ -137,7 +142,7 @@ async def route_slash_command(cmd_input: str, console: Console, client: WebSocke
|
|
|
137
142
|
return True # Handled (as error)
|
|
138
143
|
|
|
139
144
|
# Validate command
|
|
140
|
-
is_valid, error = validate_command(entry, command, query,
|
|
145
|
+
is_valid, error = validate_command(entry, command, query, loop_id)
|
|
141
146
|
if not is_valid:
|
|
142
147
|
console.print(f"[red]Error: {error}[/red]")
|
|
143
148
|
return True # Handled (as error)
|
|
@@ -151,13 +156,13 @@ async def route_slash_command(cmd_input: str, console: Console, client: WebSocke
|
|
|
151
156
|
return True
|
|
152
157
|
|
|
153
158
|
elif entry["location"] == "daemon" and entry.get("type") == "rpc":
|
|
154
|
-
# Daemon RPC: send command_request
|
|
155
|
-
await handle_rpc_command(entry, command, query, console, client)
|
|
159
|
+
# Daemon RPC: send command_request (scoped by loop_id)
|
|
160
|
+
await handle_rpc_command(entry, command, query, console, client, loop_id=loop_id)
|
|
156
161
|
return True
|
|
157
162
|
|
|
158
163
|
elif entry["location"] == "daemon" and entry.get("type") == "routing":
|
|
159
164
|
# Daemon routing: send as plain text input
|
|
160
|
-
await handle_routing_command(cmd_input, console, client)
|
|
165
|
+
await handle_routing_command(cmd_input, console, client, loop_id=loop_id)
|
|
161
166
|
return True
|
|
162
167
|
|
|
163
168
|
return False
|
|
@@ -169,6 +174,8 @@ async def handle_rpc_command(
|
|
|
169
174
|
query: str | None,
|
|
170
175
|
console: Console,
|
|
171
176
|
client: WebSocketClient,
|
|
177
|
+
*,
|
|
178
|
+
loop_id: str | None = None,
|
|
172
179
|
) -> None:
|
|
173
180
|
"""Handle daemon RPC command with structured request/response (RFC-404).
|
|
174
181
|
|
|
@@ -178,15 +185,17 @@ async def handle_rpc_command(
|
|
|
178
185
|
query: Query/params (if present)
|
|
179
186
|
console: Rich console
|
|
180
187
|
client: WebSocket client
|
|
188
|
+
loop_id: Active subscribed loop (required for daemon-side binding)
|
|
181
189
|
"""
|
|
182
190
|
daemon_command = entry["daemon_command"]
|
|
183
191
|
|
|
184
192
|
# Build request
|
|
185
|
-
request = {
|
|
193
|
+
request: dict[str, Any] = {
|
|
186
194
|
"type": "command_request",
|
|
187
195
|
"command": daemon_command,
|
|
188
|
-
"thread_id": client.thread_id,
|
|
189
196
|
}
|
|
197
|
+
if loop_id:
|
|
198
|
+
request["loop_id"] = loop_id
|
|
190
199
|
|
|
191
200
|
# Parse params if schema exists
|
|
192
201
|
if entry.get("params_schema") and query:
|
|
@@ -225,7 +234,13 @@ async def handle_rpc_command(
|
|
|
225
234
|
console.print(f"[red]Error: {exc}[/red]")
|
|
226
235
|
|
|
227
236
|
|
|
228
|
-
async def handle_routing_command(
|
|
237
|
+
async def handle_routing_command(
|
|
238
|
+
cmd_input: str,
|
|
239
|
+
console: Console,
|
|
240
|
+
client: WebSocketClient,
|
|
241
|
+
*,
|
|
242
|
+
loop_id: str | None = None,
|
|
243
|
+
) -> None:
|
|
229
244
|
"""Handle daemon routing command by sending input with optional subagent (RFC-404).
|
|
230
245
|
|
|
231
246
|
For ``/browser``, ``/claude``, ``/research``, and ``/explore``, sets the WebSocket
|
|
@@ -236,9 +251,13 @@ async def handle_routing_command(cmd_input: str, console: Console, client: WebSo
|
|
|
236
251
|
cmd_input: Full command input (e.g., "/browser AI trends")
|
|
237
252
|
console: Rich console
|
|
238
253
|
client: WebSocket client
|
|
254
|
+
loop_id: Subscribed loop to target (required for ``loop_input``)
|
|
239
255
|
"""
|
|
256
|
+
if not loop_id:
|
|
257
|
+
console.print("[red]Error: No active loop for routing command[/red]")
|
|
258
|
+
return
|
|
240
259
|
subagent_name, text = parse_subagent_from_input(cmd_input.strip())
|
|
241
|
-
await client.send_input(text, preferred_subagent=subagent_name)
|
|
260
|
+
await client.send_input(loop_id, text, preferred_subagent=subagent_name)
|
|
242
261
|
|
|
243
262
|
|
|
244
263
|
__all__ = [
|