soothe-cli 0.5.0__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.5.0 → soothe_cli-0.5.1}/PKG-INFO +1 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/daemon.py +1 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/display_line.py +2 -5
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/formatter.py +3 -2
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/pipeline.py +1 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/presentation_engine.py +3 -1
- soothe_cli-0.5.1/src/soothe_cli/shared/duration_format.py +42 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/web.py +50 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_history.py +6 -8
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_module_init.py +1 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_startup.py +0 -3
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/app.tcss +10 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/daemon_session.py +161 -2
- soothe_cli-0.5.1/src/soothe_cli/tui/formatting.py +14 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/preview_limits.py +5 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -7
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_turn.py +6 -38
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +0 -3
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/theme.py +157 -8
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/message_store.py +13 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/messages.py +439 -99
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/welcome.py +10 -29
- soothe_cli-0.5.0/src/soothe_cli/tui/formatting.py +0 -28
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/.gitignore +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/README.md +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/pyproject.toml +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/cli/stream/task_scope.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/command_router.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/config_loader.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/event_processor.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/processor_state.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/display_policy.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/essential_events.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/rendering/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/_utils.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/message_processing.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/rendering.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_execution.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.1}/src/soothe_cli/tui/widgets/tools.py +0 -0
|
@@ -26,7 +26,7 @@ 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
|
|
|
@@ -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)
|
|
@@ -13,6 +13,8 @@ from dataclasses import dataclass
|
|
|
13
13
|
from soothe_sdk.core.verbosity import VerbosityTier, should_show
|
|
14
14
|
from soothe_sdk.utils import log_preview
|
|
15
15
|
|
|
16
|
+
from soothe_cli.shared.duration_format import format_duration_ms
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
@dataclass
|
|
18
20
|
class PresentationState:
|
|
@@ -152,7 +154,7 @@ class PresentationEngine:
|
|
|
152
154
|
icon = "✗" if is_error else "✓"
|
|
153
155
|
result_line = f"{icon} {summarized}"
|
|
154
156
|
if duration_ms > 0:
|
|
155
|
-
result_line += f" ({duration_ms}
|
|
157
|
+
result_line += f" ({format_duration_ms(duration_ms)})"
|
|
156
158
|
return result_line
|
|
157
159
|
|
|
158
160
|
@staticmethod
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Human-readable duration strings for CLI and TUI (no Textual imports)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_duration(seconds: float) -> str:
|
|
7
|
+
"""Format a duration in seconds into a human-readable string.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
seconds: Duration in seconds.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Formatted string like `"5s"`, `"2.3s"`, `"5m 12s"`, or `"1h 23m 4s"`.
|
|
14
|
+
"""
|
|
15
|
+
rounded = round(seconds, 1)
|
|
16
|
+
if rounded < 60: # noqa: PLR2004
|
|
17
|
+
if rounded % 1 == 0:
|
|
18
|
+
return f"{int(rounded)}s"
|
|
19
|
+
return f"{rounded:.1f}s"
|
|
20
|
+
minutes, secs = divmod(int(rounded), 60)
|
|
21
|
+
if minutes < 60: # noqa: PLR2004
|
|
22
|
+
return f"{minutes}m {secs}s"
|
|
23
|
+
hours, minutes = divmod(minutes, 60)
|
|
24
|
+
return f"{hours}h {minutes}m {secs}s"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_duration_ms(milliseconds: int) -> str:
|
|
28
|
+
"""Format a wall-clock duration in milliseconds for status lines and cards.
|
|
29
|
+
|
|
30
|
+
Values under one second stay in milliseconds for precision; longer durations
|
|
31
|
+
reuse :func:`format_duration` (seconds, minutes, hours).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
milliseconds: Elapsed time in milliseconds (negative values are treated as 0).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Strings such as ``\"0ms\"``, ``\"240ms\"``, ``\"1.5s\"``, or ``\"2m 15s\"``.
|
|
38
|
+
"""
|
|
39
|
+
ms = max(0, int(milliseconds))
|
|
40
|
+
if ms < 1000: # noqa: PLR2004
|
|
41
|
+
return f"{ms}ms"
|
|
42
|
+
return format_duration(ms / 1000.0)
|
|
@@ -42,9 +42,13 @@ class WebFormatter(BaseFormatter):
|
|
|
42
42
|
# Route to specific formatter (handle both legacy and wizsearch naming)
|
|
43
43
|
if normalized in ("search_web", "wizsearch_search"):
|
|
44
44
|
return self._format_search_web(result)
|
|
45
|
-
if normalized in ("crawl_web", "wizsearch_crawl"):
|
|
45
|
+
if normalized in ("crawl_web", "wizsearch_crawl", "fetch_url"):
|
|
46
46
|
return self._format_crawl_web(result)
|
|
47
47
|
|
|
48
|
+
# Handle HTTP request tools (IG-339)
|
|
49
|
+
if normalized.startswith("requests_"):
|
|
50
|
+
return self._format_http_request(normalized, result)
|
|
51
|
+
|
|
48
52
|
msg = f"Unknown web tool: {tool_name}"
|
|
49
53
|
raise ValueError(msg)
|
|
50
54
|
|
|
@@ -142,3 +146,48 @@ class WebFormatter(BaseFormatter):
|
|
|
142
146
|
detail=detail,
|
|
143
147
|
metrics={"size_bytes": size_bytes, "words": words, "lines": lines},
|
|
144
148
|
)
|
|
149
|
+
|
|
150
|
+
def _format_http_request(self, tool_name: str, result: Any) -> ToolBrief: # noqa: ANN401
|
|
151
|
+
r"""Format HTTP request tool result (requests_get, requests_post, etc.).
|
|
152
|
+
|
|
153
|
+
Shows HTTP method and response status/size.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
tool_name: Name of the HTTP tool (requests_get, requests_post, etc.).
|
|
157
|
+
result: Tool result (string with response content or error).
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
ToolBrief with HTTP request summary.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> brief = formatter._format_http_request("requests_get", '{"data": 123}')
|
|
164
|
+
>>> brief.summary
|
|
165
|
+
'GET 12 B'
|
|
166
|
+
"""
|
|
167
|
+
# Extract method from tool name
|
|
168
|
+
method = tool_name.replace("requests_", "").upper()
|
|
169
|
+
|
|
170
|
+
# Check for error
|
|
171
|
+
if text_looks_like_error(result):
|
|
172
|
+
return ToolBrief(
|
|
173
|
+
icon="✗",
|
|
174
|
+
summary=f"{method} failed",
|
|
175
|
+
detail=self._truncate_text(result, 80),
|
|
176
|
+
metrics={"error": True},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Calculate size
|
|
180
|
+
if isinstance(result, str):
|
|
181
|
+
size_bytes = len(result.encode("utf-8"))
|
|
182
|
+
else:
|
|
183
|
+
size_bytes = len(str(result).encode("utf-8"))
|
|
184
|
+
|
|
185
|
+
size_str = self._format_size(size_bytes)
|
|
186
|
+
summary = f"{method} {size_str}"
|
|
187
|
+
|
|
188
|
+
return ToolBrief(
|
|
189
|
+
icon="✓",
|
|
190
|
+
summary=summary,
|
|
191
|
+
detail=None,
|
|
192
|
+
metrics={"method": method, "size_bytes": size_bytes},
|
|
193
|
+
)
|
|
@@ -165,8 +165,7 @@ class _HistoryMixin:
|
|
|
165
165
|
State values keyed by channel name, or empty when none are available.
|
|
166
166
|
"""
|
|
167
167
|
if self._daemon_session is not None:
|
|
168
|
-
|
|
169
|
-
snapshot = await self._daemon_session.aget_state(config)
|
|
168
|
+
snapshot = await self._daemon_session.aget_loop_state(loop_id)
|
|
170
169
|
values = dict(snapshot.values)
|
|
171
170
|
recovered = await self._recover_missing_checkpoint_messages(
|
|
172
171
|
loop_id=loop_id,
|
|
@@ -239,9 +238,8 @@ class _HistoryMixin:
|
|
|
239
238
|
try:
|
|
240
239
|
from langchain_core.messages.base import messages_to_dict
|
|
241
240
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
config,
|
|
241
|
+
await self._daemon_session.aupdate_loop_state(
|
|
242
|
+
loop_id,
|
|
245
243
|
{"messages": messages_to_dict(recovered_messages)},
|
|
246
244
|
timeout=10.0,
|
|
247
245
|
)
|
|
@@ -746,8 +744,8 @@ class _HistoryMixin:
|
|
|
746
744
|
# Use iter_turn_chunks to read events (same as active turn execution)
|
|
747
745
|
chunk_source = self._daemon_session.iter_turn_chunks()
|
|
748
746
|
async for chunk in chunk_source:
|
|
749
|
-
if not isinstance(chunk, tuple) or len(chunk) != 3:
|
|
750
|
-
logger.debug("Skipping
|
|
747
|
+
if not isinstance(chunk, (list, tuple)) or len(chunk) != 3:
|
|
748
|
+
logger.debug("Skipping invalid stream chunk: %s", type(chunk).__name__)
|
|
751
749
|
continue
|
|
752
750
|
|
|
753
751
|
namespace, mode, data = chunk
|
|
@@ -762,7 +760,7 @@ class _HistoryMixin:
|
|
|
762
760
|
continue
|
|
763
761
|
|
|
764
762
|
if mode == "messages":
|
|
765
|
-
if not isinstance(data, tuple) or len(data) != 2:
|
|
763
|
+
if not isinstance(data, (list, tuple)) or len(data) != 2:
|
|
766
764
|
continue
|
|
767
765
|
message, _metadata = data
|
|
768
766
|
message = normalize_stream_message(message)
|
|
@@ -17,7 +17,7 @@ from soothe_cli.tui._session_stats import (
|
|
|
17
17
|
SessionStats,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# Keep module-level imports minimal before first paint.
|
|
21
21
|
# All other config imports — settings, create_model, detect_provider, etc. — are
|
|
22
22
|
# deferred to local imports at their call sites since they are only accessed
|
|
23
23
|
# after user interaction begins.
|
|
@@ -18,7 +18,6 @@ from soothe_cli.tui._version import CHANGELOG_URL
|
|
|
18
18
|
from soothe_cli.tui.app._module_init import (
|
|
19
19
|
TextualSessionState,
|
|
20
20
|
)
|
|
21
|
-
from soothe_cli.tui.config import is_ascii_mode
|
|
22
21
|
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
23
22
|
from soothe_cli.tui.widgets.messages import (
|
|
24
23
|
AppMessage,
|
|
@@ -50,8 +49,6 @@ class _StartupMixin:
|
|
|
50
49
|
|
|
51
50
|
chat = self.query_one("#chat", VerticalScroll)
|
|
52
51
|
chat.anchor()
|
|
53
|
-
if is_ascii_mode():
|
|
54
|
-
chat.styles.scrollbar_size_vertical = 0
|
|
55
52
|
|
|
56
53
|
self._status_bar = self.query_one("#status-bar", StatusBar)
|
|
57
54
|
self._chat_input = self.query_one("#input-area", ChatInput)
|
|
@@ -6,9 +6,18 @@ Screen {
|
|
|
6
6
|
layers: base autocomplete;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/*
|
|
9
|
+
/* Scrollable regions keep wheel / key scrolling; scrollbar chrome is hidden. */
|
|
10
10
|
* {
|
|
11
|
+
scrollbar-visibility: hidden;
|
|
11
12
|
scrollbar-size-vertical: 1;
|
|
13
|
+
scrollbar-size-horizontal: 1;
|
|
14
|
+
scrollbar-background: $background;
|
|
15
|
+
scrollbar-background-hover: $background;
|
|
16
|
+
scrollbar-background-active: $background;
|
|
17
|
+
scrollbar-color: $foreground-muted;
|
|
18
|
+
scrollbar-color-hover: $text-muted;
|
|
19
|
+
scrollbar-color-active: $primary-muted;
|
|
20
|
+
scrollbar-corner-color: $background;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
/* Main content goes on base layer by default */
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import logging
|
|
7
|
+
from types import SimpleNamespace
|
|
7
8
|
from typing import TYPE_CHECKING, Any
|
|
8
9
|
|
|
9
10
|
from soothe_sdk.client import (
|
|
@@ -12,12 +13,17 @@ from soothe_sdk.client import (
|
|
|
12
13
|
connect_websocket_with_retries,
|
|
13
14
|
websocket_url_from_config,
|
|
14
15
|
)
|
|
16
|
+
from soothe_sdk.client.protocol import _serialize_for_json
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING:
|
|
17
19
|
pass
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
23
|
+
# Match headless daemon client: brief read window after ``idle`` so stream events
|
|
24
|
+
# that arrive slightly after status are not dropped (``cli/execution/daemon.py``).
|
|
25
|
+
_POST_IDLE_DRAIN_DEADLINE_S = 2.5
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
class TuiDaemonSession:
|
|
23
29
|
"""Own the daemon websocket session used by the TUI."""
|
|
@@ -113,6 +119,51 @@ class TuiDaemonSession:
|
|
|
113
119
|
raise RuntimeError("No active loop for interrupt resume")
|
|
114
120
|
await self._client.send_resume_interrupts(self._loop_id, resume_payload)
|
|
115
121
|
|
|
122
|
+
async def _drain_stream_events_after_idle(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
expected_loop_id: str | None,
|
|
126
|
+
) -> Any:
|
|
127
|
+
"""Yield stream chunks that arrive just after ``idle`` (headless client parity)."""
|
|
128
|
+
loop = asyncio.get_running_loop()
|
|
129
|
+
deadline = loop.time() + _POST_IDLE_DRAIN_DEADLINE_S
|
|
130
|
+
exp = expected_loop_id
|
|
131
|
+
while loop.time() < deadline:
|
|
132
|
+
try:
|
|
133
|
+
event = await asyncio.wait_for(self._client.read_event(), timeout=0.25)
|
|
134
|
+
except TimeoutError:
|
|
135
|
+
break
|
|
136
|
+
if not event:
|
|
137
|
+
break
|
|
138
|
+
event_type = event.get("type", "")
|
|
139
|
+
event_loop_id = event.get("loop_id")
|
|
140
|
+
if exp and isinstance(event_loop_id, str) and event_loop_id and event_loop_id != exp:
|
|
141
|
+
logger.debug(
|
|
142
|
+
"Skipping daemon event for non-active loop %s (active=%s, type=%s)",
|
|
143
|
+
event_loop_id,
|
|
144
|
+
exp,
|
|
145
|
+
event_type,
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
if event_type == "error":
|
|
149
|
+
raise RuntimeError(str(event.get("message", "daemon error")))
|
|
150
|
+
if event_type == "status":
|
|
151
|
+
loop_ev = event.get("loop_id")
|
|
152
|
+
if isinstance(loop_ev, str) and loop_ev:
|
|
153
|
+
self._loop_id = loop_ev
|
|
154
|
+
exp = loop_ev
|
|
155
|
+
continue
|
|
156
|
+
if event_type != "event":
|
|
157
|
+
continue
|
|
158
|
+
data = event.get("data")
|
|
159
|
+
if isinstance(data, dict) and data.get("type") == "soothe.system.daemon.heartbeat":
|
|
160
|
+
continue
|
|
161
|
+
namespace = tuple(event.get("namespace", []) or [])
|
|
162
|
+
mode = str(event.get("mode", ""))
|
|
163
|
+
yield (namespace, mode, data)
|
|
164
|
+
if mode == "updates" and isinstance(data, dict) and "__interrupt__" in data:
|
|
165
|
+
return
|
|
166
|
+
|
|
116
167
|
async def iter_turn_chunks(self) -> Any:
|
|
117
168
|
"""Yield `(namespace, mode, data)` chunks for the active daemon turn."""
|
|
118
169
|
query_started = False
|
|
@@ -149,12 +200,17 @@ class TuiDaemonSession:
|
|
|
149
200
|
loop_ev = event.get("loop_id")
|
|
150
201
|
if isinstance(loop_ev, str) and loop_ev:
|
|
151
202
|
self._loop_id = loop_ev
|
|
152
|
-
|
|
153
|
-
|
|
203
|
+
# Keep filter aligned with daemon-canonical loop_id whenever
|
|
204
|
+
# status carries it (avoids dropping subsequent events).
|
|
205
|
+
expected_loop_id = loop_ev
|
|
154
206
|
state = event.get("state", "")
|
|
155
207
|
if state == "running":
|
|
156
208
|
query_started = True
|
|
157
209
|
elif query_started and state in {"idle", "stopped"}:
|
|
210
|
+
async for chunk in self._drain_stream_events_after_idle(
|
|
211
|
+
expected_loop_id=expected_loop_id,
|
|
212
|
+
):
|
|
213
|
+
yield chunk
|
|
158
214
|
break
|
|
159
215
|
continue
|
|
160
216
|
|
|
@@ -204,3 +260,106 @@ class TuiDaemonSession:
|
|
|
204
260
|
return
|
|
205
261
|
await connect_websocket_with_retries(self._rpc_client)
|
|
206
262
|
self._rpc_connected = True
|
|
263
|
+
|
|
264
|
+
async def aget_loop_state(self, loop_id: str) -> Any:
|
|
265
|
+
"""Load agent-loop state channels from the daemon (``loop_state_get`` RPC).
|
|
266
|
+
|
|
267
|
+
Returns a namespace with a ``values`` mapping so history code can share the
|
|
268
|
+
same consumption pattern as the in-process agent snapshot, without passing
|
|
269
|
+
graph config objects over the wire.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
loop_id: AgentLoop id.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
``types.SimpleNamespace`` with ``values: dict[str, Any]``.
|
|
276
|
+
"""
|
|
277
|
+
lid = str(loop_id or "").strip()
|
|
278
|
+
if not lid:
|
|
279
|
+
return SimpleNamespace(values={})
|
|
280
|
+
|
|
281
|
+
async with self._rpc_lock:
|
|
282
|
+
await self._ensure_rpc_connected()
|
|
283
|
+
try:
|
|
284
|
+
resp = await self._rpc_client.request_response(
|
|
285
|
+
{"type": "loop_state_get", "loop_id": lid},
|
|
286
|
+
response_type="loop_state_get_response",
|
|
287
|
+
timeout=30.0,
|
|
288
|
+
)
|
|
289
|
+
except Exception:
|
|
290
|
+
logger.warning(
|
|
291
|
+
"loop_state_get failed for loop %s",
|
|
292
|
+
lid[:16],
|
|
293
|
+
exc_info=True,
|
|
294
|
+
)
|
|
295
|
+
return SimpleNamespace(values={})
|
|
296
|
+
|
|
297
|
+
raw = resp.get("values")
|
|
298
|
+
values: dict[str, Any] = dict(raw) if isinstance(raw, dict) else {}
|
|
299
|
+
return SimpleNamespace(values=values)
|
|
300
|
+
|
|
301
|
+
async def aupdate_loop_state(
|
|
302
|
+
self,
|
|
303
|
+
loop_id: str,
|
|
304
|
+
values: dict[str, Any],
|
|
305
|
+
*,
|
|
306
|
+
timeout: float = 10.0,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Merge partial state into the loop on the daemon host (``loop_state_update`` RPC).
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
loop_id: AgentLoop id.
|
|
312
|
+
values: Channel updates (e.g. ``messages``) in JSON-serializable form.
|
|
313
|
+
timeout: RPC wait budget in seconds.
|
|
314
|
+
"""
|
|
315
|
+
lid = str(loop_id or "").strip()
|
|
316
|
+
if not lid:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
payload_values = _serialize_for_json(values)
|
|
320
|
+
if not isinstance(payload_values, dict):
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
async with self._rpc_lock:
|
|
324
|
+
await self._ensure_rpc_connected()
|
|
325
|
+
await self._rpc_client.request_response(
|
|
326
|
+
{
|
|
327
|
+
"type": "loop_state_update",
|
|
328
|
+
"loop_id": lid,
|
|
329
|
+
"values": payload_values,
|
|
330
|
+
},
|
|
331
|
+
response_type="loop_state_update_response",
|
|
332
|
+
timeout=timeout,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
async def fetch_conversation_log(
|
|
336
|
+
self,
|
|
337
|
+
loop_id: str,
|
|
338
|
+
*,
|
|
339
|
+
limit: int = 100,
|
|
340
|
+
offset: int = 0,
|
|
341
|
+
include_events: bool = False,
|
|
342
|
+
) -> list[dict[str, Any]]:
|
|
343
|
+
"""Load persisted rows for a loop from the daemon (conversation + optional events)."""
|
|
344
|
+
lid = str(loop_id or "").strip()
|
|
345
|
+
if not lid:
|
|
346
|
+
return []
|
|
347
|
+
|
|
348
|
+
async with self._rpc_lock:
|
|
349
|
+
await self._ensure_rpc_connected()
|
|
350
|
+
resp = await self._rpc_client.request_response(
|
|
351
|
+
{
|
|
352
|
+
"type": "loop_messages",
|
|
353
|
+
"loop_id": lid,
|
|
354
|
+
"limit": limit,
|
|
355
|
+
"offset": offset,
|
|
356
|
+
"include_events": include_events,
|
|
357
|
+
},
|
|
358
|
+
response_type="loop_messages_response",
|
|
359
|
+
timeout=10.0,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
raw = resp.get("messages")
|
|
363
|
+
if not isinstance(raw, list):
|
|
364
|
+
return []
|
|
365
|
+
return [m for m in raw if isinstance(m, dict)]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Lightweight text-formatting helpers.
|
|
2
|
+
|
|
3
|
+
Keep this module free of heavy dependencies so it can be imported anywhere
|
|
4
|
+
in the CLI without pulling in large frameworks.
|
|
5
|
+
|
|
6
|
+
Implementation lives in :mod:`soothe_cli.shared.duration_format` so shared code
|
|
7
|
+
does not need to import the ``soothe_cli.tui`` package (avoids import cycles).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from soothe_cli.shared.duration_format import format_duration, format_duration_ms
|
|
13
|
+
|
|
14
|
+
__all__ = ["format_duration", "format_duration_ms"]
|
|
@@ -12,6 +12,11 @@ from typing import Final
|
|
|
12
12
|
ASSISTANT_MESSAGE_PREVIEW_LINES: Final[int] = 10
|
|
13
13
|
ASSISTANT_MESSAGE_PREVIEW_CHARS: Final[int] = 800
|
|
14
14
|
|
|
15
|
+
# --- Step / Task cognition cards (`CognitionStepMessage`, task `ToolCallMessage`) ---
|
|
16
|
+
# When estimated body lines exceed this count, the card auto-collapses (strict `>`).
|
|
17
|
+
# Matches the step/task activity preview row cap (`_STEP_TOOL_PREVIEW_ROWS`).
|
|
18
|
+
STEP_TASK_CARD_COLLAPSE_LINE_THRESHOLD: Final[int] = 3
|
|
19
|
+
|
|
15
20
|
# --- Tool call cards (`ToolCallMessage` collapsed output) ---
|
|
16
21
|
TOOL_CARD_PREVIEW_LINES: Final[int] = 1
|
|
17
22
|
TOOL_CARD_PREVIEW_CHARS: Final[int] = 120
|
|
@@ -47,7 +47,6 @@ from soothe_cli.tui._session_stats import (
|
|
|
47
47
|
format_token_count as format_token_count,
|
|
48
48
|
)
|
|
49
49
|
from soothe_cli.tui.widgets.messages import (
|
|
50
|
-
CognitionGoalTreeMessage,
|
|
51
50
|
CognitionStepMessage,
|
|
52
51
|
ToolCallMessage,
|
|
53
52
|
)
|
|
@@ -171,9 +170,6 @@ class TextualUIAdapter:
|
|
|
171
170
|
self._current_step_messages: dict[str, CognitionStepMessage] = {}
|
|
172
171
|
"""Map of agent-loop act step IDs to step card widgets."""
|
|
173
172
|
|
|
174
|
-
self._goal_tree_by_namespace: dict[tuple[Any, ...], CognitionGoalTreeMessage] = {}
|
|
175
|
-
"""Live Goal→steps tree card per stream namespace (agentic Layer 2)."""
|
|
176
|
-
|
|
177
173
|
self._step_by_namespace: dict[tuple[Any, ...], CognitionStepMessage] = {}
|
|
178
174
|
"""Active step card per stream namespace (main-agent tool aggregation, IG-402)."""
|
|
179
175
|
|
|
@@ -254,9 +250,6 @@ class TextualUIAdapter:
|
|
|
254
250
|
for step_msg in list(self._current_step_messages.values()):
|
|
255
251
|
step_msg.set_interrupted(message)
|
|
256
252
|
self._current_step_messages.clear()
|
|
257
|
-
for tree in list(self._goal_tree_by_namespace.values()):
|
|
258
|
-
tree.set_interrupted(message)
|
|
259
|
-
self._goal_tree_by_namespace.clear()
|
|
260
253
|
self._tool_to_step.clear()
|
|
261
254
|
self._step_by_namespace.clear()
|
|
262
255
|
self._task_inner_tool_pending_lines.clear()
|
|
@@ -91,7 +91,6 @@ from soothe_cli.tui.textual_adapter._turn_helpers import (
|
|
|
91
91
|
from soothe_cli.tui.widgets.messages import (
|
|
92
92
|
AppMessage,
|
|
93
93
|
AssistantMessage,
|
|
94
|
-
CognitionGoalTreeMessage,
|
|
95
94
|
CognitionPlanReasonMessage,
|
|
96
95
|
CognitionStepMessage,
|
|
97
96
|
DiffMessage,
|
|
@@ -340,8 +339,8 @@ async def execute_task_textual(
|
|
|
340
339
|
chunk_source = daemon_session.iter_turn_chunks()
|
|
341
340
|
|
|
342
341
|
async for chunk in chunk_source:
|
|
343
|
-
if not isinstance(chunk, tuple) or len(chunk) != 3: # noqa: PLR2004
|
|
344
|
-
logger.debug("Skipping
|
|
342
|
+
if not isinstance(chunk, (list, tuple)) or len(chunk) != 3: # noqa: PLR2004
|
|
343
|
+
logger.debug("Skipping invalid stream chunk: %s", type(chunk).__name__)
|
|
345
344
|
continue
|
|
346
345
|
|
|
347
346
|
namespace, current_stream_mode, data = chunk
|
|
@@ -399,9 +398,9 @@ async def execute_task_textual(
|
|
|
399
398
|
ns_key,
|
|
400
399
|
)
|
|
401
400
|
|
|
402
|
-
if not isinstance(data, tuple) or len(data) != 2: # noqa: PLR2004
|
|
401
|
+
if not isinstance(data, (list, tuple)) or len(data) != 2: # noqa: PLR2004
|
|
403
402
|
logger.debug(
|
|
404
|
-
"Skipping non-
|
|
403
|
+
"Skipping non-pair message data: type=%s",
|
|
405
404
|
type(data).__name__,
|
|
406
405
|
)
|
|
407
406
|
continue
|
|
@@ -1272,8 +1271,6 @@ async def execute_task_textual(
|
|
|
1272
1271
|
continue
|
|
1273
1272
|
|
|
1274
1273
|
if event_type == AGENT_LOOP_GOAL_STARTED:
|
|
1275
|
-
goal = str(data.get("goal", "")).strip()
|
|
1276
|
-
max_it = int(data.get("max_iterations", 0))
|
|
1277
1274
|
if not ns_key:
|
|
1278
1275
|
adapter._last_completed_main_step_execute_prose = ""
|
|
1279
1276
|
adapter._last_main_flushed_assistant_prose = ""
|
|
@@ -1288,27 +1285,10 @@ async def execute_task_textual(
|
|
|
1288
1285
|
)
|
|
1289
1286
|
pending_text_by_namespace[ns_key] = ""
|
|
1290
1287
|
assistant_message_by_namespace.pop(ns_key, None)
|
|
1291
|
-
tree = CognitionGoalTreeMessage(
|
|
1292
|
-
goal=goal or "(goal)",
|
|
1293
|
-
max_iterations=max_it,
|
|
1294
|
-
id=f"goaltree-{uuid.uuid4().hex[:8]}",
|
|
1295
|
-
)
|
|
1296
|
-
adapter._goal_tree_by_namespace[ns_key] = tree
|
|
1297
|
-
await adapter._mount_message(tree)
|
|
1298
1288
|
continue
|
|
1299
1289
|
|
|
1300
1290
|
if event_type == AGENT_LOOP_GOAL_COMPLETED:
|
|
1301
|
-
|
|
1302
|
-
if tr is not None:
|
|
1303
|
-
tr.set_loop_finished(
|
|
1304
|
-
status=str(data.get("status", "")),
|
|
1305
|
-
goal_progress=str(
|
|
1306
|
-
data.get("goal_progress", "none")
|
|
1307
|
-
), # IG-399: descriptive level
|
|
1308
|
-
completion_summary=str(data.get("completion_summary", "")),
|
|
1309
|
-
total_steps=int(data.get("total_steps", 0)),
|
|
1310
|
-
)
|
|
1311
|
-
adapter._goal_tree_by_namespace.pop(ns_key, None)
|
|
1291
|
+
continue
|
|
1312
1292
|
|
|
1313
1293
|
if event_type == AGENT_LOOP_STEP_STARTED:
|
|
1314
1294
|
step_id = str(data.get("step_id", "")).strip()
|
|
@@ -1325,9 +1305,6 @@ async def execute_task_textual(
|
|
|
1325
1305
|
)
|
|
1326
1306
|
pending_text_by_namespace[ns_key] = ""
|
|
1327
1307
|
assistant_message_by_namespace.pop(ns_key, None)
|
|
1328
|
-
goal_tree = adapter._goal_tree_by_namespace.get(ns_key)
|
|
1329
|
-
if goal_tree is not None:
|
|
1330
|
-
goal_tree.add_step_running(step_id, description or "(step)")
|
|
1331
1308
|
step_widget = CognitionStepMessage(
|
|
1332
1309
|
step_id=step_id,
|
|
1333
1310
|
description=description or "(step)",
|
|
@@ -1386,15 +1363,6 @@ async def execute_task_textual(
|
|
|
1386
1363
|
)
|
|
1387
1364
|
if not summary.strip():
|
|
1388
1365
|
summary = "Failed" if not success else "Done"
|
|
1389
|
-
goal_tree = adapter._goal_tree_by_namespace.get(ns_key)
|
|
1390
|
-
if goal_tree is not None:
|
|
1391
|
-
goal_tree.complete_step(
|
|
1392
|
-
step_id,
|
|
1393
|
-
success,
|
|
1394
|
-
duration_ms,
|
|
1395
|
-
tool_call_count,
|
|
1396
|
-
summary,
|
|
1397
|
-
)
|
|
1398
1366
|
widget = adapter._current_step_messages.pop(step_id, None)
|
|
1399
1367
|
if widget is not None:
|
|
1400
1368
|
if adapter._step_by_namespace.get(ns_key) is widget:
|
|
@@ -1417,7 +1385,7 @@ async def execute_task_textual(
|
|
|
1417
1385
|
adapter._last_completed_main_step_execute_prose = (
|
|
1418
1386
|
widget.last_completed_execute_prose
|
|
1419
1387
|
)
|
|
1420
|
-
|
|
1388
|
+
else:
|
|
1421
1389
|
ev = dict(data)
|
|
1422
1390
|
ev["namespace"] = list(ns_key)
|
|
1423
1391
|
for line in progress_pipeline.process(ev):
|
|
@@ -211,9 +211,6 @@ async def _handle_interrupt_cleanup(
|
|
|
211
211
|
adapter._step_by_namespace.clear()
|
|
212
212
|
adapter._pending_main_tools.clear()
|
|
213
213
|
|
|
214
|
-
for gt in list(adapter._goal_tree_by_namespace.values()):
|
|
215
|
-
gt.set_interrupted("Interrupted by user")
|
|
216
|
-
adapter._goal_tree_by_namespace.clear()
|
|
217
214
|
adapter._last_completed_main_step_execute_prose = ""
|
|
218
215
|
adapter._last_main_flushed_assistant_prose = ""
|
|
219
216
|
|