soothe-cli 0.7.0__tar.gz → 0.7.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.7.0 → soothe_cli-0.7.2}/.gitignore +9 -3
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/PKG-INFO +1 -1
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/execution/daemon.py +1 -1
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/execution/daemon_errors.py +18 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/main.py +27 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/config/cli_config.py +3 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/policy/essential_events.py +3 -1
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/state/session_stats.py +60 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/transport/session.py +30 -9
- soothe_cli-0.7.2/src/soothe_cli/runtime/turn/pipeline.py +404 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/turn/prepare.py +16 -1
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_app.py +11 -2
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_execution.py +37 -1
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_messages_mixin.py +20 -12
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_model.py +23 -39
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_startup.py +137 -60
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_ui.py +40 -5
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/app.tcss +6 -6
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/binding.py +0 -2
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/commands/slash_commands.py +0 -7
- soothe_cli-0.7.2/src/soothe_cli/tui/markdown_theme.py +358 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/model_config.py +78 -24
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/sessions.py +8 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/textual_adapter.py +304 -38
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/tool_display.py +54 -1
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +3 -8
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/context_viewer.py +1 -1
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/loading.py +60 -15
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/loop_selector.py +22 -131
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -2
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -42
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/assistant.py +4 -5
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +3 -10
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/cognition_step.py +5 -4
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +1 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/skill.py +2 -4
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/user.py +20 -12
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/welcome.py +1 -15
- soothe_cli-0.7.0/src/soothe_cli/runtime/turn/pipeline.py +0 -166
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/README.md +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/pyproject.toml +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.7.0 → soothe_cli-0.7.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
|
@@ -191,6 +191,15 @@ cython_debug/
|
|
|
191
191
|
# PyPI configuration file
|
|
192
192
|
.pypirc
|
|
193
193
|
|
|
194
|
+
# Soothe local runtime data
|
|
195
|
+
.soothe/
|
|
196
|
+
|
|
197
|
+
# Tool backup files (created by file editing tools)
|
|
198
|
+
.backups/
|
|
199
|
+
|
|
200
|
+
# Large tool result cache
|
|
201
|
+
large_tool_results/
|
|
202
|
+
|
|
194
203
|
# Cursor
|
|
195
204
|
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
196
205
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
@@ -206,7 +215,6 @@ __marimo__/
|
|
|
206
215
|
.python-version
|
|
207
216
|
thirdparty/
|
|
208
217
|
.cursor/
|
|
209
|
-
large_tool_results/
|
|
210
218
|
plot_*
|
|
211
219
|
.claude/
|
|
212
220
|
.DS_Store
|
|
@@ -216,5 +224,3 @@ manifest.json
|
|
|
216
224
|
_bmad
|
|
217
225
|
__MACOSX
|
|
218
226
|
.qoder
|
|
219
|
-
.soothe
|
|
220
|
-
.backups/
|
|
@@ -280,7 +280,7 @@ async def _run_headless_session_once(
|
|
|
280
280
|
query_started = True
|
|
281
281
|
elif (state == "idle" and query_started) or state == "stopped":
|
|
282
282
|
loop_clock = asyncio.get_event_loop()
|
|
283
|
-
drain_deadline = loop_clock.time() +
|
|
283
|
+
drain_deadline = loop_clock.time() + 30.0
|
|
284
284
|
while loop_clock.time() < drain_deadline:
|
|
285
285
|
try:
|
|
286
286
|
nxt = await asyncio.wait_for(client.read_event(), timeout=0.25)
|
|
@@ -5,11 +5,19 @@ from __future__ import annotations
|
|
|
5
5
|
# Partial match for pool_runner RuntimeError when an OS worker exits mid-turn.
|
|
6
6
|
DAEMON_WORKER_SUBPROCESS_LOST = "Worker subprocess exited unexpectedly during query execution"
|
|
7
7
|
|
|
8
|
+
# Partial match for thread_runner RuntimeError when a worker thread dies mid-turn.
|
|
9
|
+
DAEMON_WORKER_THREAD_LOST = "Worker thread exited unexpectedly during query execution"
|
|
10
|
+
|
|
8
11
|
_FRIENDLY_WORKER_SUBPROCESS_LOST = (
|
|
9
12
|
"The daemon execution worker stopped unexpectedly (for example after the pool "
|
|
10
13
|
"recycled an idle subprocess). Send your message again."
|
|
11
14
|
)
|
|
12
15
|
|
|
16
|
+
_FRIENDLY_WORKER_THREAD_LOST = (
|
|
17
|
+
"The daemon execution worker stopped unexpectedly during your request. "
|
|
18
|
+
"Send your message again, or use /resume to recover the loop."
|
|
19
|
+
)
|
|
20
|
+
|
|
13
21
|
_FRIENDLY_DAEMON_CONNECTION_LOST = (
|
|
14
22
|
"Daemon connection lost (the daemon may have restarted). "
|
|
15
23
|
"Send your message again to reconnect and continue this loop."
|
|
@@ -45,9 +53,13 @@ def friendly_daemon_execution_error(exc: BaseException | str) -> str:
|
|
|
45
53
|
return _FRIENDLY_DAEMON_CONNECTION_LOST
|
|
46
54
|
if isinstance(exc, RuntimeError) and DAEMON_WORKER_SUBPROCESS_LOST in str(exc):
|
|
47
55
|
return _FRIENDLY_WORKER_SUBPROCESS_LOST
|
|
56
|
+
if isinstance(exc, RuntimeError) and DAEMON_WORKER_THREAD_LOST in str(exc):
|
|
57
|
+
return _FRIENDLY_WORKER_THREAD_LOST
|
|
48
58
|
text = str(exc)
|
|
49
59
|
if DAEMON_WORKER_SUBPROCESS_LOST in text:
|
|
50
60
|
return _FRIENDLY_WORKER_SUBPROCESS_LOST
|
|
61
|
+
if DAEMON_WORKER_THREAD_LOST in text:
|
|
62
|
+
return _FRIENDLY_WORKER_THREAD_LOST
|
|
51
63
|
return text if isinstance(exc, str) else str(exc)
|
|
52
64
|
|
|
53
65
|
|
|
@@ -55,3 +67,9 @@ def is_daemon_worker_subprocess_lost(exc: BaseException | str) -> bool:
|
|
|
55
67
|
"""Return whether an error indicates a pool worker process exited mid-query."""
|
|
56
68
|
text = str(exc)
|
|
57
69
|
return DAEMON_WORKER_SUBPROCESS_LOST in text
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_daemon_worker_thread_lost(exc: BaseException | str) -> bool:
|
|
73
|
+
"""Return whether an error indicates a pool worker thread exited mid-query."""
|
|
74
|
+
text = str(exc)
|
|
75
|
+
return DAEMON_WORKER_THREAD_LOST in text
|
|
@@ -15,6 +15,12 @@ from soothe_sdk.client.config import SOOTHE_HOME # noqa: E402
|
|
|
15
15
|
|
|
16
16
|
from soothe_cli.config.cli_config import CLIConfig # noqa: E402
|
|
17
17
|
from soothe_cli.config.loader import set_runtime_config # noqa: E402
|
|
18
|
+
from soothe_cli.tui.markdown_theme import ( # noqa: E402
|
|
19
|
+
DEFAULT_MARKDOWN_THEME,
|
|
20
|
+
REGISTRY,
|
|
21
|
+
load_markdown_theme_preference,
|
|
22
|
+
markdown_theme_help,
|
|
23
|
+
)
|
|
18
24
|
|
|
19
25
|
app = typer.Typer(
|
|
20
26
|
name="soothe",
|
|
@@ -101,6 +107,16 @@ def main(
|
|
|
101
107
|
help="Render assistant messages as Markdown in the TUI.",
|
|
102
108
|
),
|
|
103
109
|
] = True,
|
|
110
|
+
markdown_theme: Annotated[
|
|
111
|
+
str | None,
|
|
112
|
+
typer.Option(
|
|
113
|
+
"--markdown-theme",
|
|
114
|
+
help=(
|
|
115
|
+
"Markdown appearance preset for TUI cards "
|
|
116
|
+
f"({markdown_theme_help()}). Default: {DEFAULT_MARKDOWN_THEME}."
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
] = None,
|
|
104
120
|
soothe_home: Annotated[
|
|
105
121
|
str | None,
|
|
106
122
|
typer.Option("--soothe-home", help="Soothe home directory (default: ~/.soothe)."),
|
|
@@ -168,11 +184,22 @@ def main(
|
|
|
168
184
|
if mode is not None and mode not in ("manual", "auto"):
|
|
169
185
|
typer.echo(f"Invalid --mode {mode!r}; expected 'manual' or 'auto'.", err=True)
|
|
170
186
|
raise typer.Exit(code=2)
|
|
187
|
+
if markdown_theme is not None and markdown_theme not in REGISTRY:
|
|
188
|
+
typer.echo(
|
|
189
|
+
f"Invalid --markdown-theme {markdown_theme!r}; "
|
|
190
|
+
f"expected one of: {markdown_theme_help()}.",
|
|
191
|
+
err=True,
|
|
192
|
+
)
|
|
193
|
+
raise typer.Exit(code=2)
|
|
194
|
+
resolved_markdown_theme = (
|
|
195
|
+
markdown_theme if markdown_theme is not None else load_markdown_theme_preference()
|
|
196
|
+
)
|
|
171
197
|
cli_cfg = CLIConfig(
|
|
172
198
|
daemon_host=daemon_host,
|
|
173
199
|
daemon_port=daemon_port,
|
|
174
200
|
logging_level=log_level,
|
|
175
201
|
render_markdown=render_markdown,
|
|
202
|
+
markdown_theme=resolved_markdown_theme,
|
|
176
203
|
output_streaming_enabled=streaming,
|
|
177
204
|
output_streaming_mode=streaming_mode,
|
|
178
205
|
clarification_mode=mode,
|
|
@@ -29,6 +29,9 @@ class CLIConfig:
|
|
|
29
29
|
render_markdown: bool = True
|
|
30
30
|
"""Render assistant messages as Markdown in TUI (default True)."""
|
|
31
31
|
|
|
32
|
+
markdown_theme: str = "match-app"
|
|
33
|
+
"""Markdown appearance preset (``match-app``, ``langchain``, ``standard``, …)."""
|
|
34
|
+
|
|
32
35
|
# Output streaming overrides (RFC-614)
|
|
33
36
|
output_streaming_enabled: bool | None = None
|
|
34
37
|
"""Override daemon streaming enabled setting."""
|
|
@@ -24,12 +24,13 @@ STEP_COMPLETE_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.strange_loop.reasoned"
|
|
27
|
+
INTENT_CLASSIFIED_EVENT_TYPE: Final[str] = "soothe.cognition.intent.classified"
|
|
27
28
|
|
|
28
29
|
ESSENTIAL_PROGRESS_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
29
30
|
set(GOAL_START_EVENT_TYPES)
|
|
30
31
|
| set(STEP_START_EVENT_TYPES)
|
|
31
32
|
| set(STEP_COMPLETE_EVENT_TYPES)
|
|
32
|
-
| {LOOP_REASON_EVENT_TYPE}
|
|
33
|
+
| {LOOP_REASON_EVENT_TYPE, INTENT_CLASSIFIED_EVENT_TYPE}
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
|
|
@@ -56,6 +57,7 @@ def is_step_complete_event_type(event_type: str) -> bool:
|
|
|
56
57
|
__all__ = [
|
|
57
58
|
"ESSENTIAL_PROGRESS_EVENT_TYPES",
|
|
58
59
|
"GOAL_START_EVENT_TYPES",
|
|
60
|
+
"INTENT_CLASSIFIED_EVENT_TYPE",
|
|
59
61
|
"LOOP_REASON_EVENT_TYPE",
|
|
60
62
|
"STEP_COMPLETE_EVENT_TYPES",
|
|
61
63
|
"STEP_START_EVENT_TYPES",
|
|
@@ -8,6 +8,7 @@ config, no widget imports) so that `app.py` can import `SessionStats` and
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import time
|
|
11
12
|
from dataclasses import dataclass, field
|
|
12
13
|
|
|
13
14
|
SpinnerStatus = str | None
|
|
@@ -106,6 +107,52 @@ class SessionStats:
|
|
|
106
107
|
self.event_stats.merge(other.event_stats)
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
@dataclass
|
|
111
|
+
class TurnLatencyStats:
|
|
112
|
+
"""IG-534 Phase 3: end-to-end turn latency observability.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
turn_start_monotonic: Monotonic timestamp when the turn began.
|
|
116
|
+
time_to_first_chunk_ms: Milliseconds from turn start to first applied chunk.
|
|
117
|
+
synthesis_visible_ms: Milliseconds from turn start to first goal_completion apply.
|
|
118
|
+
goal_completion_applied: Whether a goal_completion chunk was applied.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
turn_start_monotonic: float = 0.0
|
|
122
|
+
time_to_first_chunk_ms: float | None = None
|
|
123
|
+
synthesis_visible_ms: float | None = None
|
|
124
|
+
goal_completion_applied: bool = False
|
|
125
|
+
|
|
126
|
+
def record_first_chunk(self) -> None:
|
|
127
|
+
"""Record time-to-first-chunk once per turn."""
|
|
128
|
+
if self.time_to_first_chunk_ms is not None or self.turn_start_monotonic <= 0:
|
|
129
|
+
return
|
|
130
|
+
self.time_to_first_chunk_ms = (time.monotonic() - self.turn_start_monotonic) * 1000.0
|
|
131
|
+
|
|
132
|
+
def record_goal_completion(self) -> None:
|
|
133
|
+
"""Record synthesis-visible latency once per turn."""
|
|
134
|
+
if self.goal_completion_applied or self.turn_start_monotonic <= 0:
|
|
135
|
+
return
|
|
136
|
+
self.goal_completion_applied = True
|
|
137
|
+
self.synthesis_visible_ms = (time.monotonic() - self.turn_start_monotonic) * 1000.0
|
|
138
|
+
|
|
139
|
+
def merge(self, other: TurnLatencyStats) -> None:
|
|
140
|
+
"""Merge another stats object (keeps earliest first-chunk / synthesis times)."""
|
|
141
|
+
if other.time_to_first_chunk_ms is not None:
|
|
142
|
+
if (
|
|
143
|
+
self.time_to_first_chunk_ms is None
|
|
144
|
+
or other.time_to_first_chunk_ms < self.time_to_first_chunk_ms
|
|
145
|
+
):
|
|
146
|
+
self.time_to_first_chunk_ms = other.time_to_first_chunk_ms
|
|
147
|
+
if other.synthesis_visible_ms is not None:
|
|
148
|
+
if (
|
|
149
|
+
self.synthesis_visible_ms is None
|
|
150
|
+
or other.synthesis_visible_ms < self.synthesis_visible_ms
|
|
151
|
+
):
|
|
152
|
+
self.synthesis_visible_ms = other.synthesis_visible_ms
|
|
153
|
+
self.goal_completion_applied = self.goal_completion_applied or other.goal_completion_applied
|
|
154
|
+
|
|
155
|
+
|
|
109
156
|
@dataclass
|
|
110
157
|
class TurnEventStats:
|
|
111
158
|
"""Event counts accumulated over a single daemon turn.
|
|
@@ -139,6 +186,8 @@ class TurnEventStats:
|
|
|
139
186
|
text_chunks: int = 0
|
|
140
187
|
heartbeats_dropped: int = 0
|
|
141
188
|
post_idle_drained: int = 0
|
|
189
|
+
inbound_dropped: int = 0
|
|
190
|
+
latency: TurnLatencyStats | None = None
|
|
142
191
|
|
|
143
192
|
def record(
|
|
144
193
|
self,
|
|
@@ -187,6 +236,11 @@ class TurnEventStats:
|
|
|
187
236
|
self.heartbeats_dropped += other.heartbeats_dropped
|
|
188
237
|
self.post_idle_drained += other.post_idle_drained
|
|
189
238
|
self.filtered_early += other.filtered_early
|
|
239
|
+
self.inbound_dropped += other.inbound_dropped
|
|
240
|
+
if other.latency is not None:
|
|
241
|
+
if self.latency is None:
|
|
242
|
+
self.latency = TurnLatencyStats()
|
|
243
|
+
self.latency.merge(other.latency)
|
|
190
244
|
|
|
191
245
|
def summary_line(self) -> str:
|
|
192
246
|
"""Return a one-line summary suitable for structured logging.
|
|
@@ -220,6 +274,12 @@ class TurnEventStats:
|
|
|
220
274
|
detail_parts.append(f"{self.heartbeats_dropped} hb-drop")
|
|
221
275
|
if self.post_idle_drained:
|
|
222
276
|
detail_parts.append(f"{self.post_idle_drained} post-idle")
|
|
277
|
+
if self.inbound_dropped:
|
|
278
|
+
detail_parts.append(f"{self.inbound_dropped} inbound-drop")
|
|
279
|
+
if self.latency is not None and self.latency.time_to_first_chunk_ms is not None:
|
|
280
|
+
detail_parts.append(f"ttfc={self.latency.time_to_first_chunk_ms:.0f}ms")
|
|
281
|
+
if self.latency is not None and self.latency.synthesis_visible_ms is not None:
|
|
282
|
+
detail_parts.append(f"synth={self.latency.synthesis_visible_ms:.0f}ms")
|
|
223
283
|
if detail_parts:
|
|
224
284
|
parts.append("; ".join(detail_parts))
|
|
225
285
|
|
|
@@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
|
|
|
26
26
|
|
|
27
27
|
# Match headless daemon client: brief read window after ``idle`` so stream events
|
|
28
28
|
# that arrive slightly after status are not dropped (``cli/execution/daemon.py``).
|
|
29
|
-
_POST_IDLE_DRAIN_DEADLINE_S =
|
|
29
|
+
_POST_IDLE_DRAIN_DEADLINE_S = 30.0
|
|
30
30
|
|
|
31
31
|
# Align with ``bootstrap_loop_session`` daemon-ready wait (RFC-450 §8.2).
|
|
32
32
|
_RPC_HANDSHAKE_TIMEOUT_S = 20.0
|
|
@@ -81,7 +81,6 @@ class TuiDaemonSession:
|
|
|
81
81
|
self._client = WebSocketClient(url=ws_url)
|
|
82
82
|
self._rpc_client = WebSocketClient(url=ws_url)
|
|
83
83
|
self._loop_id: str | None = None
|
|
84
|
-
self._autopilot_mode: str | None = None
|
|
85
84
|
self._read_lock = asyncio.Lock()
|
|
86
85
|
self._rpc_lock = asyncio.Lock()
|
|
87
86
|
self._rpc_connected = False
|
|
@@ -91,17 +90,13 @@ class TuiDaemonSession:
|
|
|
91
90
|
self.turn_event_stats = TurnEventStats()
|
|
92
91
|
self.last_turn_end_state: str | None = None
|
|
93
92
|
self.last_turn_cancellation_seen: bool = False
|
|
93
|
+
self.last_turn_error_message: str | None = None
|
|
94
94
|
|
|
95
95
|
@property
|
|
96
96
|
def loop_id(self) -> str | None:
|
|
97
97
|
"""Active StrangeLoop id for this WebSocket session."""
|
|
98
98
|
return self._loop_id
|
|
99
99
|
|
|
100
|
-
@property
|
|
101
|
-
def autopilot_mode(self) -> str | None:
|
|
102
|
-
"""Active loop Solo/Autopilot mode when known."""
|
|
103
|
-
return self._autopilot_mode
|
|
104
|
-
|
|
105
100
|
async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
|
|
106
101
|
"""Connect and bootstrap a daemon loop session."""
|
|
107
102
|
await connect_websocket_with_retries(self._client)
|
|
@@ -122,8 +117,6 @@ class TuiDaemonSession:
|
|
|
122
117
|
if status_event.get("type") == "error":
|
|
123
118
|
raise RuntimeError(str(status_event.get("message", "daemon bootstrap failed")))
|
|
124
119
|
self._loop_id = status_event.get("loop_id")
|
|
125
|
-
mode = status_event.get("autopilot_mode")
|
|
126
|
-
self._autopilot_mode = str(mode) if mode in ("solo", "autopilot") else None
|
|
127
120
|
return status_event
|
|
128
121
|
|
|
129
122
|
def _resolve_stream_delivery_mode(self) -> str:
|
|
@@ -248,6 +241,14 @@ class TuiDaemonSession:
|
|
|
248
241
|
"""Ask the daemon to cancel the in-flight query (same wire path as ``/cancel``)."""
|
|
249
242
|
await self._client.notify("slash_command", {"cmd": "/cancel"})
|
|
250
243
|
|
|
244
|
+
async def cancel_active_turn(self) -> None:
|
|
245
|
+
"""Cancel the in-flight query on the active loop (IG-533 ordering contract).
|
|
246
|
+
|
|
247
|
+
Call before switching ``loop_id`` (e.g. ``/clear``) so synthesis on the
|
|
248
|
+
prior loop is torn down server-side instead of filtered client-side.
|
|
249
|
+
"""
|
|
250
|
+
await self.cancel_remote_query()
|
|
251
|
+
|
|
251
252
|
async def _drain_stream_events_after_idle(
|
|
252
253
|
self,
|
|
253
254
|
*,
|
|
@@ -316,6 +317,8 @@ class TuiDaemonSession:
|
|
|
316
317
|
self.turn_event_stats = TurnEventStats()
|
|
317
318
|
self.last_turn_end_state = None
|
|
318
319
|
self.last_turn_cancellation_seen = False
|
|
320
|
+
self.last_turn_error_message = None
|
|
321
|
+
inbound_dropped_baseline = getattr(self._client, "inbound_dropped", 0)
|
|
319
322
|
query_started = False
|
|
320
323
|
expected_loop_id = self._loop_id
|
|
321
324
|
self._streaming = True
|
|
@@ -431,8 +434,15 @@ class TuiDaemonSession:
|
|
|
431
434
|
# Graph auto-resumes LangGraph interrupts server-side; keep consuming events.
|
|
432
435
|
if mode == "updates" and isinstance(data, dict) and "__interrupt__" in data:
|
|
433
436
|
continue
|
|
437
|
+
except Exception as exc:
|
|
438
|
+
self.last_turn_error_message = str(exc)
|
|
439
|
+
raise
|
|
434
440
|
finally:
|
|
435
441
|
self._streaming = False
|
|
442
|
+
self.turn_event_stats.inbound_dropped = max(
|
|
443
|
+
0,
|
|
444
|
+
getattr(self._client, "inbound_dropped", 0) - inbound_dropped_baseline,
|
|
445
|
+
)
|
|
436
446
|
|
|
437
447
|
async def list_skills(self) -> list[dict[str, Any]]:
|
|
438
448
|
"""Return skill rows from the daemon catalog (no filesystem paths)."""
|
|
@@ -651,5 +661,16 @@ class TuiDaemonSession:
|
|
|
651
661
|
return []
|
|
652
662
|
return [m for m in raw if isinstance(m, dict)]
|
|
653
663
|
|
|
664
|
+
async def fetch_goal_completion_text(self, loop_id: str) -> str | None:
|
|
665
|
+
"""Return the latest persisted ``goal_completion`` body for a loop, if any."""
|
|
666
|
+
rows = await self.fetch_conversation_log(loop_id, limit=200, include_events=False)
|
|
667
|
+
for row in reversed(rows):
|
|
668
|
+
if row.get("phase") != "goal_completion":
|
|
669
|
+
continue
|
|
670
|
+
text = row.get("text") or row.get("content") or ""
|
|
671
|
+
if isinstance(text, str) and text.strip():
|
|
672
|
+
return text.strip()
|
|
673
|
+
return None
|
|
674
|
+
|
|
654
675
|
|
|
655
676
|
DaemonSession = TuiDaemonSession
|