soothe-cli 0.5.0__tar.gz → 0.5.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.5.0 → soothe_cli-0.5.2}/PKG-INFO +1 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/daemon.py +1 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/display_line.py +2 -5
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/formatter.py +3 -2
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/pipeline.py +1 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/presentation_engine.py +3 -1
- soothe_cli-0.5.2/src/soothe_cli/shared/duration_format.py +42 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/web.py +50 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_app.py +16 -96
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_commands.py +27 -30
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_execution.py +13 -50
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_history.py +146 -93
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_messages_mixin.py +2 -2
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_model.py +27 -79
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_module_init.py +7 -47
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_startup.py +14 -228
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/app.tcss +10 -1
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/config.py +10 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/daemon_session.py +170 -6
- soothe_cli-0.5.2/src/soothe_cli/tui/formatting.py +14 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/preview_limits.py +5 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -7
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_turn.py +53 -102
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +56 -50
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/theme.py +157 -8
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/message_store.py +13 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/messages.py +444 -105
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/welcome.py +17 -68
- soothe_cli-0.5.0/src/soothe_cli/tui/formatting.py +0 -28
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/.gitignore +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/README.md +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/pyproject.toml +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/cli/stream/task_scope.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/command_router.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/config_loader.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/event_processor.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/processor_state.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/display_policy.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/essential_events.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/rendering/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/_utils.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/message_processing.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/rendering.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.5.0 → soothe_cli-0.5.2}/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
|
+
)
|
|
@@ -10,7 +10,6 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from langgraph.pregel import Pregel
|
|
14
13
|
from textual.app import ComposeResult
|
|
15
14
|
from textual.worker import Worker
|
|
16
15
|
|
|
@@ -133,22 +132,8 @@ class SootheApp(
|
|
|
133
132
|
"""App-level keybindings for interrupt, quit, toggles, and approval menu
|
|
134
133
|
navigation."""
|
|
135
134
|
|
|
136
|
-
class ServerReady(Message):
|
|
137
|
-
"""Posted by the background server-startup worker on success."""
|
|
138
|
-
|
|
139
|
-
def __init__( # noqa: D107
|
|
140
|
-
self,
|
|
141
|
-
agent: Any, # noqa: ANN401
|
|
142
|
-
server_proc: Any, # noqa: ANN401
|
|
143
|
-
mcp_server_info: list[Any] | None,
|
|
144
|
-
) -> None:
|
|
145
|
-
super().__init__()
|
|
146
|
-
self.agent = agent
|
|
147
|
-
self.server_proc = server_proc
|
|
148
|
-
self.mcp_server_info = mcp_server_info
|
|
149
|
-
|
|
150
135
|
class ServerStartFailed(Message):
|
|
151
|
-
"""Posted
|
|
136
|
+
"""Posted when daemon bootstrap or background connection fails."""
|
|
152
137
|
|
|
153
138
|
def __init__(self, error: Exception) -> None: # noqa: D107
|
|
154
139
|
super().__init__()
|
|
@@ -165,63 +150,30 @@ class SootheApp(
|
|
|
165
150
|
def __init__(
|
|
166
151
|
self,
|
|
167
152
|
*,
|
|
168
|
-
|
|
153
|
+
daemon_config: Any,
|
|
169
154
|
assistant_id: str | None = None,
|
|
170
155
|
auto_approve: bool = False,
|
|
171
156
|
cwd: str | Path | None = None,
|
|
172
157
|
resume_loop_id: str | None = None,
|
|
173
|
-
resume_loop_intent: str | None = None,
|
|
174
158
|
initial_prompt: str | None = None,
|
|
175
159
|
initial_skill: str | None = None,
|
|
176
160
|
mcp_server_info: list[dict[str, Any]] | None = None,
|
|
177
161
|
profile_override: dict[str, Any] | None = None,
|
|
178
|
-
server_proc: Any | None = None,
|
|
179
|
-
server_kwargs: dict[str, Any] | None = None,
|
|
180
|
-
mcp_preload_kwargs: dict[str, Any] | None = None,
|
|
181
|
-
model_kwargs: dict[str, Any] | None = None,
|
|
182
|
-
daemon_config: Any | None = None,
|
|
183
162
|
**kwargs: Any,
|
|
184
163
|
) -> None:
|
|
185
|
-
"""Initialize the
|
|
164
|
+
"""Initialize the Textual application (daemon-backed execution only).
|
|
186
165
|
|
|
187
166
|
Args:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`None` when `resume_loop_intent` is provided (resolved asynchronously).
|
|
196
|
-
resume_loop_intent: Raw resume intent from `-r` flag.
|
|
197
|
-
|
|
198
|
-
`'__MOST_RECENT__'` for bare `-r`, a loop id for
|
|
199
|
-
`-r <id>`, or `None` for new sessions.
|
|
200
|
-
|
|
201
|
-
Resolved via `_resolve_resume_loop_intent`
|
|
202
|
-
during `_start_server_background`.
|
|
203
|
-
|
|
204
|
-
Requires `server_kwargs` to be set; ignored otherwise.
|
|
205
|
-
initial_prompt: Optional prompt to auto-submit when session starts
|
|
167
|
+
daemon_config: Loaded Soothe configuration (WebSocket URL, etc.).
|
|
168
|
+
assistant_id: Agent identifier for memory storage.
|
|
169
|
+
auto_approve: Whether to start with auto-approve enabled.
|
|
170
|
+
cwd: Current working directory to display.
|
|
171
|
+
resume_loop_id: Initial AgentLoop id when attaching to an existing loop.
|
|
172
|
+
initial_prompt: Optional prompt to auto-submit when session starts.
|
|
206
173
|
initial_skill: Optional skill name to invoke when session starts.
|
|
207
174
|
mcp_server_info: MCP server metadata for the `/mcp` viewer.
|
|
208
|
-
profile_override: Extra profile fields from
|
|
209
|
-
|
|
210
|
-
the CLI override, including model selection details and
|
|
211
|
-
on-demand `create_model()` calls.
|
|
212
|
-
server_proc: LangGraph server process for the interactive session.
|
|
213
|
-
server_kwargs: When provided, server startup is deferred.
|
|
214
|
-
|
|
215
|
-
The app shows a "Connecting..." state and starts the server in
|
|
216
|
-
the background using these kwargs
|
|
217
|
-
for `start_server_and_get_agent`.
|
|
218
|
-
mcp_preload_kwargs: Kwargs for `_preload_session_mcp_server_info`,
|
|
219
|
-
run concurrently with server startup when `server_kwargs` is set.
|
|
220
|
-
model_kwargs: Kwargs for deferred `create_model()`.
|
|
221
|
-
|
|
222
|
-
When provided, model creation runs in a background worker after
|
|
223
|
-
first paint instead of blocking startup.
|
|
224
|
-
**kwargs: Additional arguments passed to parent
|
|
175
|
+
profile_override: Extra profile fields from ``--profile-override``.
|
|
176
|
+
**kwargs: Additional arguments passed to the Textual ``App``.
|
|
225
177
|
"""
|
|
226
178
|
super().__init__(**kwargs)
|
|
227
179
|
|
|
@@ -230,8 +182,6 @@ class SootheApp(
|
|
|
230
182
|
# Apply saved theme preference (or default)
|
|
231
183
|
self.theme = _load_theme_preference()
|
|
232
184
|
|
|
233
|
-
self._agent = agent
|
|
234
|
-
|
|
235
185
|
self._assistant_id = assistant_id
|
|
236
186
|
|
|
237
187
|
self._auto_approve = auto_approve
|
|
@@ -242,8 +192,6 @@ class SootheApp(
|
|
|
242
192
|
# Named `_lc_loop_id` to avoid colliding with Textual's App._thread_id.
|
|
243
193
|
self._lc_loop_id = resume_loop_id
|
|
244
194
|
|
|
245
|
-
self._resume_loop_intent = resume_loop_intent
|
|
246
|
-
|
|
247
195
|
self._initial_prompt = initial_prompt
|
|
248
196
|
|
|
249
197
|
self._initial_skill = (
|
|
@@ -254,14 +202,6 @@ class SootheApp(
|
|
|
254
202
|
|
|
255
203
|
self._profile_override = profile_override
|
|
256
204
|
|
|
257
|
-
self._server_proc = server_proc
|
|
258
|
-
|
|
259
|
-
self._server_kwargs = server_kwargs
|
|
260
|
-
|
|
261
|
-
self._mcp_preload_kwargs = mcp_preload_kwargs
|
|
262
|
-
|
|
263
|
-
self._model_kwargs = model_kwargs
|
|
264
|
-
|
|
265
205
|
self._daemon_config = daemon_config
|
|
266
206
|
|
|
267
207
|
self._daemon_session: Any | None = None
|
|
@@ -269,14 +209,9 @@ class SootheApp(
|
|
|
269
209
|
self._daemon_skills_wire: list[dict[str, Any]] = []
|
|
270
210
|
"""Cached ``skills_list_response`` rows when the TUI uses ``TuiDaemonSession``."""
|
|
271
211
|
|
|
272
|
-
self._connecting =
|
|
273
|
-
# Extract sandbox type from server kwargs for trace metadata.
|
|
274
|
-
# ServerConfig.__post_init__ normalizes "none" → None, but server_kwargs carries
|
|
275
|
-
# the raw argparse value, so guard against both.
|
|
212
|
+
self._connecting = True
|
|
276
213
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
self._sandbox_type: str | None = raw if raw and raw != "none" else None
|
|
214
|
+
self._sandbox_type: str | None = None
|
|
280
215
|
|
|
281
216
|
self._model_override: str | None = None
|
|
282
217
|
|
|
@@ -304,11 +239,7 @@ class SootheApp(
|
|
|
304
239
|
self._agent_running = False
|
|
305
240
|
|
|
306
241
|
self._server_startup_error: str | None = None
|
|
307
|
-
"""Set when
|
|
308
|
-
session lifetime (server failure is terminal).
|
|
309
|
-
|
|
310
|
-
Shown in place of the generic 'Agent not configured' message.
|
|
311
|
-
"""
|
|
242
|
+
"""Set when daemon bootstrap fails; persists for the session lifetime."""
|
|
312
243
|
|
|
313
244
|
self._shell_process: asyncio.subprocess.Process | None = None
|
|
314
245
|
"""Shell command process tracking for interruption (! commands)."""
|
|
@@ -391,18 +322,9 @@ class SootheApp(
|
|
|
391
322
|
|
|
392
323
|
self._image_tracker = MediaTracker()
|
|
393
324
|
|
|
394
|
-
def _remote_agent(self) -> Any: # noqa: ANN401
|
|
395
|
-
"""Return the agent if it appears to be a remote agent, or `None`.
|
|
396
|
-
|
|
397
|
-
Returns `None` when no agent is configured or the agent is a local graph.
|
|
398
|
-
"""
|
|
399
|
-
# RemoteAgent module doesn't exist in this package; always return None.
|
|
400
|
-
# When the SDK provides a RemoteAgent class, this can be re-implemented.
|
|
401
|
-
return None
|
|
402
|
-
|
|
403
325
|
def _runtime_backend_ready(self) -> bool:
|
|
404
|
-
"""Return whether the app has a
|
|
405
|
-
return self._daemon_session is not None
|
|
326
|
+
"""Return whether the app has a connected daemon session."""
|
|
327
|
+
return self._daemon_session is not None
|
|
406
328
|
|
|
407
329
|
def get_theme_variable_defaults(self) -> dict[str, str]:
|
|
408
330
|
"""Return custom CSS variable defaults for the current theme.
|
|
@@ -434,8 +356,6 @@ class SootheApp(
|
|
|
434
356
|
mcp_tool_count=self._mcp_tool_count,
|
|
435
357
|
workspace_path=self._cwd,
|
|
436
358
|
connecting=self._connecting,
|
|
437
|
-
resuming=self._resume_loop_intent is not None,
|
|
438
|
-
local_server=self._server_kwargs is not None,
|
|
439
359
|
id="welcome-banner",
|
|
440
360
|
)
|
|
441
361
|
yield Container(id="messages")
|
|
@@ -9,7 +9,6 @@ from contextlib import suppress
|
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from langchain_core.runnables import RunnableConfig
|
|
13
12
|
from textual.content import Content
|
|
14
13
|
|
|
15
14
|
from textual.app import ScreenStackError
|
|
@@ -191,24 +190,25 @@ class _CommandsMixin:
|
|
|
191
190
|
self._update_tokens(0)
|
|
192
191
|
# Clear status message (e.g., "Interrupted" from previous session)
|
|
193
192
|
self._update_status("")
|
|
194
|
-
# New AgentLoop (daemon) or new local loop id
|
|
195
193
|
if self._session_state:
|
|
196
|
-
if self._daemon_session is
|
|
194
|
+
if self._daemon_session is None:
|
|
195
|
+
await self._mount_message(
|
|
196
|
+
AppMessage("Not connected to the daemon; cannot start a new loop.")
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
197
199
|
status_event = await self._daemon_session.new_loop()
|
|
198
200
|
new_loop_id = (
|
|
199
201
|
str(status_event.get("loop_id", "")) or self._session_state.reset_loop()
|
|
200
202
|
)
|
|
201
203
|
self._session_state.loop_id = new_loop_id
|
|
202
204
|
self._lc_loop_id = new_loop_id
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
self._clear_loop_model_override()
|
|
211
|
-
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
205
|
+
try:
|
|
206
|
+
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
207
|
+
banner.update_loop_id(new_loop_id)
|
|
208
|
+
except NoMatches:
|
|
209
|
+
pass
|
|
210
|
+
self._clear_loop_model_override()
|
|
211
|
+
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
212
212
|
elif cmd == "/editor":
|
|
213
213
|
await self.action_open_editor()
|
|
214
214
|
elif cmd == "/loops":
|
|
@@ -367,13 +367,6 @@ class _CommandsMixin:
|
|
|
367
367
|
report += "\nTheme registry reload failed. Check config.yml for errors."
|
|
368
368
|
await self._mount_message(AppMessage(report))
|
|
369
369
|
|
|
370
|
-
# Re-discover skills so autocomplete reflects any new/removed skills
|
|
371
|
-
if self._daemon_config is None:
|
|
372
|
-
self.run_worker(
|
|
373
|
-
self._discover_skills(),
|
|
374
|
-
exclusive=True,
|
|
375
|
-
group="startup-skill-discovery",
|
|
376
|
-
)
|
|
377
370
|
if self._daemon_session is not None:
|
|
378
371
|
self.run_worker(
|
|
379
372
|
self._refresh_daemon_skills_catalog(),
|
|
@@ -424,22 +417,26 @@ class _CommandsMixin:
|
|
|
424
417
|
Returns:
|
|
425
418
|
Token count as an integer, or `None` if state is unavailable.
|
|
426
419
|
"""
|
|
427
|
-
if not self.
|
|
420
|
+
if not self._lc_loop_id:
|
|
428
421
|
return None
|
|
429
422
|
try:
|
|
430
|
-
from langchain_core.messages
|
|
431
|
-
|
|
432
|
-
)
|
|
423
|
+
from langchain_core.messages import messages_from_dict
|
|
424
|
+
from langchain_core.messages.utils import count_tokens_approximately
|
|
433
425
|
|
|
434
|
-
|
|
435
|
-
"configurable": {"thread_id": self._lc_loop_id},
|
|
436
|
-
}
|
|
437
|
-
state = await self._agent.aget_state(config)
|
|
438
|
-
if not state or not state.values:
|
|
426
|
+
if self._daemon_session is None:
|
|
439
427
|
return None
|
|
440
|
-
|
|
441
|
-
|
|
428
|
+
snap = await self._daemon_session.aget_loop_state(self._lc_loop_id)
|
|
429
|
+
vals = getattr(snap, "values", None)
|
|
430
|
+
if not isinstance(vals, dict):
|
|
442
431
|
return None
|
|
432
|
+
raw = vals.get("messages")
|
|
433
|
+
if not isinstance(raw, list) or not raw:
|
|
434
|
+
return None
|
|
435
|
+
if isinstance(raw[0], dict):
|
|
436
|
+
messages = messages_from_dict(raw)
|
|
437
|
+
else:
|
|
438
|
+
messages = raw
|
|
439
|
+
|
|
443
440
|
return count_tokens_approximately(messages)
|
|
444
441
|
except Exception: # best-effort for /tokens display
|
|
445
442
|
logger.debug("Failed to retrieve conversation token count", exc_info=True)
|
|
@@ -10,10 +10,7 @@ import sys
|
|
|
10
10
|
import time
|
|
11
11
|
import webbrowser
|
|
12
12
|
from contextlib import suppress
|
|
13
|
-
from typing import
|
|
14
|
-
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from langchain_core.runnables import RunnableConfig
|
|
13
|
+
from typing import Any, Literal
|
|
17
14
|
|
|
18
15
|
from textual.app import ScreenStackError
|
|
19
16
|
from textual.containers import VerticalScroll
|
|
@@ -532,24 +529,25 @@ class _ExecutionMixin:
|
|
|
532
529
|
self._update_tokens(0)
|
|
533
530
|
# Clear status message (e.g., "Interrupted" from previous session)
|
|
534
531
|
self._update_status("")
|
|
535
|
-
# New AgentLoop (daemon) or new local loop id
|
|
536
532
|
if self._session_state:
|
|
537
|
-
if self._daemon_session is
|
|
533
|
+
if self._daemon_session is None:
|
|
534
|
+
await self._mount_message(
|
|
535
|
+
AppMessage("Not connected to the daemon; cannot start a new loop.")
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
538
|
status_event = await self._daemon_session.new_loop()
|
|
539
539
|
new_loop_id = (
|
|
540
540
|
str(status_event.get("loop_id", "")) or self._session_state.reset_loop()
|
|
541
541
|
)
|
|
542
542
|
self._session_state.loop_id = new_loop_id
|
|
543
543
|
self._lc_loop_id = new_loop_id
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
self._clear_loop_model_override()
|
|
552
|
-
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
544
|
+
try:
|
|
545
|
+
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
546
|
+
banner.update_loop_id(new_loop_id)
|
|
547
|
+
except NoMatches:
|
|
548
|
+
pass
|
|
549
|
+
self._clear_loop_model_override()
|
|
550
|
+
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
553
551
|
elif cmd == "/editor":
|
|
554
552
|
await self.action_open_editor()
|
|
555
553
|
elif cmd == "/loops":
|
|
@@ -708,13 +706,6 @@ class _ExecutionMixin:
|
|
|
708
706
|
report += "\nTheme registry reload failed. Check config.yml for errors."
|
|
709
707
|
await self._mount_message(AppMessage(report))
|
|
710
708
|
|
|
711
|
-
# Re-discover skills so autocomplete reflects any new/removed skills
|
|
712
|
-
if self._daemon_config is None:
|
|
713
|
-
self.run_worker(
|
|
714
|
-
self._discover_skills(),
|
|
715
|
-
exclusive=True,
|
|
716
|
-
group="startup-skill-discovery",
|
|
717
|
-
)
|
|
718
709
|
if self._daemon_session is not None:
|
|
719
710
|
self.run_worker(
|
|
720
711
|
self._refresh_daemon_skills_catalog(),
|
|
@@ -759,33 +750,6 @@ class _ExecutionMixin:
|
|
|
759
750
|
AppMessage("Skills require a daemon connection. Connect to a daemon first.")
|
|
760
751
|
)
|
|
761
752
|
|
|
762
|
-
async def _get_conversation_token_count(self) -> int | None:
|
|
763
|
-
"""Return the approximate conversation-only token count.
|
|
764
|
-
|
|
765
|
-
Returns:
|
|
766
|
-
Token count as an integer, or `None` if state is unavailable.
|
|
767
|
-
"""
|
|
768
|
-
if not self._agent:
|
|
769
|
-
return None
|
|
770
|
-
try:
|
|
771
|
-
from langchain_core.messages.utils import (
|
|
772
|
-
count_tokens_approximately,
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
config: RunnableConfig = {
|
|
776
|
-
"configurable": {"thread_id": self._lc_loop_id},
|
|
777
|
-
}
|
|
778
|
-
state = await self._agent.aget_state(config)
|
|
779
|
-
if not state or not state.values:
|
|
780
|
-
return None
|
|
781
|
-
messages = state.values.get("messages", [])
|
|
782
|
-
if not messages:
|
|
783
|
-
return None
|
|
784
|
-
return count_tokens_approximately(messages)
|
|
785
|
-
except Exception: # best-effort for /tokens display
|
|
786
|
-
logger.debug("Failed to retrieve conversation token count", exc_info=True)
|
|
787
|
-
return None
|
|
788
|
-
|
|
789
753
|
async def _handle_user_message(self, message: str) -> None:
|
|
790
754
|
"""Handle a user message to send to the agent.
|
|
791
755
|
|
|
@@ -878,7 +842,6 @@ class _ExecutionMixin:
|
|
|
878
842
|
try:
|
|
879
843
|
await execute_task_textual(
|
|
880
844
|
user_input=message,
|
|
881
|
-
agent=self._agent,
|
|
882
845
|
daemon_session=self._daemon_session,
|
|
883
846
|
assistant_id=self._assistant_id,
|
|
884
847
|
session_state=self._session_state,
|