soothe-cli 0.7.12__tar.gz → 0.7.13__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.12 → soothe_cli-0.7.13}/PKG-INFO +1 -1
- soothe_cli-0.7.13/src/soothe_cli/runtime/token_events_debug.py +180 -0
- soothe_cli-0.7.13/src/soothe_cli/runtime/token_usage.py +113 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/wire/chunk_filter.py +3 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_app.py +10 -5
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_execution.py +8 -83
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_history.py +12 -2
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_messages_mixin.py +5 -2
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_model.py +25 -6
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_module_init.py +1 -1
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_startup.py +7 -2
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/_ui.py +148 -31
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/binding.py +5 -4
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/command_registry.py +26 -8
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/sessions.py +1 -1
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/textual_adapter.py +187 -95
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/tips.py +1 -1
- soothe_cli-0.7.13/src/soothe_cli/tui/widgets/context_data.py +157 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/context_viewer.py +117 -86
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/loading.py +30 -2
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/loop_selector.py +35 -13
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/assistant.py +2 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/status.py +3 -2
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/.gitignore +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/README.md +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/pyproject.toml +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.7.12 → soothe_cli-0.7.13}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Debug tracing for loop token events in the CLI TUI.
|
|
2
|
+
|
|
3
|
+
Enable with ``logging_level: DEBUG`` in CLI config (``~/.soothe/config.yml``)
|
|
4
|
+
or ``SOOTHE_LOG_LEVEL=DEBUG``. Grep ``cli.log`` for ``[token-events]``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("soothe_cli.token_events")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class TokenEventTrace:
|
|
17
|
+
"""Per-turn counters for token-related daemon/TUI events."""
|
|
18
|
+
|
|
19
|
+
stream_chunks: int = 0
|
|
20
|
+
stream_input_tokens: int = 0
|
|
21
|
+
stream_output_tokens: int = 0
|
|
22
|
+
plan_phase_events: int = 0
|
|
23
|
+
plan_phase_with_total_field: int = 0
|
|
24
|
+
plan_phase_missing_total_field: int = 0
|
|
25
|
+
step_completed_events: int = 0
|
|
26
|
+
step_completed_with_total_field: int = 0
|
|
27
|
+
authoritative_applied: int = 0
|
|
28
|
+
authoritative_unchanged: int = 0
|
|
29
|
+
display_refreshes: int = 0
|
|
30
|
+
anomalies: list[str] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def reset(self) -> None:
|
|
33
|
+
"""Clear counters for a new turn."""
|
|
34
|
+
self.stream_chunks = 0
|
|
35
|
+
self.stream_input_tokens = 0
|
|
36
|
+
self.stream_output_tokens = 0
|
|
37
|
+
self.plan_phase_events = 0
|
|
38
|
+
self.plan_phase_with_total_field = 0
|
|
39
|
+
self.plan_phase_missing_total_field = 0
|
|
40
|
+
self.step_completed_events = 0
|
|
41
|
+
self.step_completed_with_total_field = 0
|
|
42
|
+
self.authoritative_applied = 0
|
|
43
|
+
self.authoritative_unchanged = 0
|
|
44
|
+
self.display_refreshes = 0
|
|
45
|
+
self.anomalies.clear()
|
|
46
|
+
|
|
47
|
+
def note_stream_usage(
|
|
48
|
+
self, *, input_tokens: int, output_tokens: int, total_tokens: int
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Record a messages-stream chunk carrying provider usage."""
|
|
51
|
+
self.stream_chunks += 1
|
|
52
|
+
self.stream_input_tokens += max(0, input_tokens)
|
|
53
|
+
self.stream_output_tokens += max(0, output_tokens)
|
|
54
|
+
logger.debug(
|
|
55
|
+
"[token-events] stream usage chunk #%d in=%d out=%d total=%d",
|
|
56
|
+
self.stream_chunks,
|
|
57
|
+
input_tokens,
|
|
58
|
+
output_tokens,
|
|
59
|
+
total_tokens,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def note_plan_phase(
|
|
63
|
+
self, *, label: str, total_tokens_used: int | None, has_total_field: bool
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Record a StrangeLoop plan-phase lifecycle event."""
|
|
66
|
+
self.plan_phase_events += 1
|
|
67
|
+
if has_total_field:
|
|
68
|
+
self.plan_phase_with_total_field += 1
|
|
69
|
+
else:
|
|
70
|
+
self.plan_phase_missing_total_field += 1
|
|
71
|
+
self.anomalies.append(f"plan_phase missing total_tokens_used ({label!r})")
|
|
72
|
+
logger.debug(
|
|
73
|
+
"[token-events] plan_phase label=%r total=%s has_total_field=%s",
|
|
74
|
+
label,
|
|
75
|
+
total_tokens_used if has_total_field else "—",
|
|
76
|
+
has_total_field,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def note_step_completed(
|
|
80
|
+
self, *, step_id: str, total_tokens_used: int | None, has_total_field: bool
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Record step.completed with optional backend loop total."""
|
|
83
|
+
self.step_completed_events += 1
|
|
84
|
+
if has_total_field:
|
|
85
|
+
self.step_completed_with_total_field += 1
|
|
86
|
+
logger.debug(
|
|
87
|
+
"[token-events] step_completed step_id=%s total=%s has_total_field=%s",
|
|
88
|
+
step_id or "—",
|
|
89
|
+
total_tokens_used if has_total_field else "—",
|
|
90
|
+
has_total_field,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def note_authoritative_merge(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
source: str,
|
|
97
|
+
goal_run_total: int,
|
|
98
|
+
previous_goal_run: int,
|
|
99
|
+
applied: bool,
|
|
100
|
+
display_total: int,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Record backend total merged into the TUI loop counter."""
|
|
103
|
+
if applied:
|
|
104
|
+
self.authoritative_applied += 1
|
|
105
|
+
else:
|
|
106
|
+
self.authoritative_unchanged += 1
|
|
107
|
+
logger.debug(
|
|
108
|
+
"[token-events] authoritative source=%s goal_run=%d prev_goal_run=%d applied=%s display=%d",
|
|
109
|
+
source,
|
|
110
|
+
goal_run_total,
|
|
111
|
+
previous_goal_run,
|
|
112
|
+
applied,
|
|
113
|
+
display_total,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def note_display_refresh(self, *, display_total: int, target: str) -> None:
|
|
117
|
+
"""Record a push to the thinking row or status bar."""
|
|
118
|
+
self.display_refreshes += 1
|
|
119
|
+
logger.debug(
|
|
120
|
+
"[token-events] display refresh target=%s total=%d refresh#%d",
|
|
121
|
+
target,
|
|
122
|
+
display_total,
|
|
123
|
+
self.display_refreshes,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def finish_turn(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
loop_id: str | None,
|
|
130
|
+
baseline: int,
|
|
131
|
+
goal_run: int,
|
|
132
|
+
display_total: int,
|
|
133
|
+
turn_input: int,
|
|
134
|
+
turn_output: int,
|
|
135
|
+
approximate: bool,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Emit one summary line per turn; flags missing events."""
|
|
138
|
+
has_stream = self.stream_chunks > 0
|
|
139
|
+
has_backend_total = (
|
|
140
|
+
self.plan_phase_with_total_field > 0 or self.step_completed_with_total_field > 0
|
|
141
|
+
)
|
|
142
|
+
has_any_usage = display_total > 0 or turn_input > 0 or turn_output > 0
|
|
143
|
+
|
|
144
|
+
if has_any_usage and not has_stream and not has_backend_total:
|
|
145
|
+
self.anomalies.append("usage visible but no stream or backend total events")
|
|
146
|
+
if self.plan_phase_events > 0 and self.plan_phase_missing_total_field > 0:
|
|
147
|
+
self.anomalies.append(
|
|
148
|
+
f"{self.plan_phase_missing_total_field}/{self.plan_phase_events} plan_phase "
|
|
149
|
+
"events lacked total_tokens_used"
|
|
150
|
+
)
|
|
151
|
+
if not has_any_usage and (self.plan_phase_events or self.step_completed_events):
|
|
152
|
+
self.anomalies.append("lifecycle events seen but display total still zero")
|
|
153
|
+
|
|
154
|
+
status = "ok"
|
|
155
|
+
if self.anomalies:
|
|
156
|
+
status = "anomaly"
|
|
157
|
+
elif not has_stream and not has_backend_total and not has_any_usage:
|
|
158
|
+
status = "no_events"
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
"[token-events] turn summary loop=%s status=%s baseline=%d goal_run=%d "
|
|
162
|
+
"display=%d turn_in=%d turn_out=%d stream_chunks=%d plan_phase=%d "
|
|
163
|
+
"step_completed=%d authoritative_applied=%d approximate=%s anomalies=%s",
|
|
164
|
+
loop_id or "—",
|
|
165
|
+
status,
|
|
166
|
+
baseline,
|
|
167
|
+
goal_run,
|
|
168
|
+
display_total,
|
|
169
|
+
turn_input,
|
|
170
|
+
turn_output,
|
|
171
|
+
self.stream_chunks,
|
|
172
|
+
self.plan_phase_events,
|
|
173
|
+
self.step_completed_events,
|
|
174
|
+
self.authoritative_applied,
|
|
175
|
+
approximate,
|
|
176
|
+
"; ".join(self.anomalies) if self.anomalies else "none",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = ["TokenEventTrace", "logger"]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Token usage extraction and estimation helpers for the TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"extract_stream_message_token_usage",
|
|
12
|
+
"fetch_conversation_token_count",
|
|
13
|
+
"message_has_token_usage_metadata",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _coerce_int(value: Any) -> int:
|
|
18
|
+
try:
|
|
19
|
+
return max(0, int(value or 0))
|
|
20
|
+
except (TypeError, ValueError):
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _usage_from_mapping(usage: dict[str, Any]) -> tuple[int, int, int]:
|
|
25
|
+
"""Return ``(input_tokens, output_tokens, total_tokens)`` from a usage dict."""
|
|
26
|
+
input_toks = _coerce_int(usage.get("input_tokens") or usage.get("prompt_tokens"))
|
|
27
|
+
output_toks = _coerce_int(usage.get("output_tokens") or usage.get("completion_tokens"))
|
|
28
|
+
total_toks = _coerce_int(usage.get("total_tokens"))
|
|
29
|
+
if not total_toks and (input_toks or output_toks):
|
|
30
|
+
total_toks = input_toks + output_toks
|
|
31
|
+
return input_toks, output_toks, total_toks
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_stream_message_token_usage(message: Any) -> tuple[int, int, int]:
|
|
35
|
+
"""Extract token usage from a streamed LangChain or wire-format message.
|
|
36
|
+
|
|
37
|
+
Providers vary:
|
|
38
|
+
- LangChain ``usage_metadata`` (``input_tokens`` / ``output_tokens``)
|
|
39
|
+
- OpenAI-style ``response_metadata.token_usage`` (``prompt_tokens`` / ``completion_tokens``)
|
|
40
|
+
- Flat wire dicts with either shape
|
|
41
|
+
"""
|
|
42
|
+
if message is None:
|
|
43
|
+
return 0, 0, 0
|
|
44
|
+
|
|
45
|
+
usage = getattr(message, "usage_metadata", None)
|
|
46
|
+
if isinstance(usage, dict) and usage:
|
|
47
|
+
return _usage_from_mapping(usage)
|
|
48
|
+
|
|
49
|
+
response_metadata = getattr(message, "response_metadata", None)
|
|
50
|
+
if isinstance(response_metadata, dict):
|
|
51
|
+
for key in ("token_usage", "usage"):
|
|
52
|
+
nested = response_metadata.get(key)
|
|
53
|
+
if isinstance(nested, dict) and nested:
|
|
54
|
+
return _usage_from_mapping(nested)
|
|
55
|
+
|
|
56
|
+
if isinstance(message, dict):
|
|
57
|
+
from soothe_cli.runtime.wire.message_text import wire_message_body
|
|
58
|
+
|
|
59
|
+
bodies = [message]
|
|
60
|
+
nested = wire_message_body(message)
|
|
61
|
+
if nested is not message:
|
|
62
|
+
bodies.append(nested)
|
|
63
|
+
for body in bodies:
|
|
64
|
+
if not isinstance(body, dict):
|
|
65
|
+
continue
|
|
66
|
+
direct_usage = body.get("usage_metadata")
|
|
67
|
+
if isinstance(direct_usage, dict) and direct_usage:
|
|
68
|
+
return _usage_from_mapping(direct_usage)
|
|
69
|
+
response = body.get("response_metadata")
|
|
70
|
+
if isinstance(response, dict):
|
|
71
|
+
for key in ("token_usage", "usage"):
|
|
72
|
+
nested_usage = response.get(key)
|
|
73
|
+
if isinstance(nested_usage, dict) and nested_usage:
|
|
74
|
+
return _usage_from_mapping(nested_usage)
|
|
75
|
+
|
|
76
|
+
return 0, 0, 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def message_has_token_usage_metadata(message: Any) -> bool:
|
|
80
|
+
"""Return True when a stream message carries provider token usage."""
|
|
81
|
+
input_toks, output_toks, total_toks = extract_stream_message_token_usage(message)
|
|
82
|
+
return bool(input_toks or output_toks or total_toks)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def fetch_conversation_token_count(daemon_session: Any, loop_id: str | None) -> int | None:
|
|
86
|
+
"""Return approximate conversation-only token count from loop checkpoint messages."""
|
|
87
|
+
raw_loop_id = str(loop_id or "").strip()
|
|
88
|
+
if not raw_loop_id or daemon_session is None:
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
from langchain_core.messages import messages_from_dict
|
|
92
|
+
from langchain_core.messages.utils import count_tokens_approximately
|
|
93
|
+
|
|
94
|
+
snap = await daemon_session.aget_loop_state(raw_loop_id)
|
|
95
|
+
vals = getattr(snap, "values", None)
|
|
96
|
+
if not isinstance(vals, dict):
|
|
97
|
+
return None
|
|
98
|
+
raw = vals.get("messages")
|
|
99
|
+
if not isinstance(raw, list) or not raw:
|
|
100
|
+
return None
|
|
101
|
+
if isinstance(raw[0], dict):
|
|
102
|
+
messages = messages_from_dict(raw)
|
|
103
|
+
else:
|
|
104
|
+
messages = raw
|
|
105
|
+
count = count_tokens_approximately(messages)
|
|
106
|
+
return count if count > 0 else None
|
|
107
|
+
except Exception:
|
|
108
|
+
logger.debug(
|
|
109
|
+
"Failed to retrieve conversation token count for loop %s",
|
|
110
|
+
raw_loop_id,
|
|
111
|
+
exc_info=True,
|
|
112
|
+
)
|
|
113
|
+
return None
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from soothe_cli.runtime.token_usage import message_has_token_usage_metadata
|
|
7
8
|
from soothe_cli.runtime.wire.message_text import (
|
|
8
9
|
extract_plain_text_from_stream_message,
|
|
9
10
|
wire_message_body,
|
|
@@ -73,6 +74,8 @@ def message_chunk_is_non_actionable(data: Any) -> bool:
|
|
|
73
74
|
|
|
74
75
|
if isinstance(msg, ToolMessage):
|
|
75
76
|
return False
|
|
77
|
+
if message_has_token_usage_metadata(msg):
|
|
78
|
+
return False
|
|
76
79
|
if isinstance(msg, dict):
|
|
77
80
|
body = wire_message_body(msg)
|
|
78
81
|
raw = str(body.get("type") or msg.get("type") or "")
|
|
@@ -253,12 +253,17 @@ class SootheApp(
|
|
|
253
253
|
self._connect_spinner_start_mono: float | None = None
|
|
254
254
|
"""Monotonic anchor for startup daemon-connect elapsed time in the thinking row."""
|
|
255
255
|
|
|
256
|
-
self.
|
|
257
|
-
"""
|
|
256
|
+
self._loop_token_scope_id: str | None = None
|
|
257
|
+
"""Loop id that ``_loop_*_tokens`` counters belong to."""
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
self._loop_baseline_tokens: int = 0
|
|
260
|
+
"""Persisted loop usage total loaded from checkpoint (resume baseline)."""
|
|
261
|
+
|
|
262
|
+
self._loop_input_tokens: int = 0
|
|
263
|
+
"""Input tokens for the in-flight goal/turn (stream or backend)."""
|
|
264
|
+
|
|
265
|
+
self._loop_output_tokens: int = 0
|
|
266
|
+
"""Output tokens for the in-flight goal/turn (stream or backend)."""
|
|
262
267
|
|
|
263
268
|
self._tokens_approximate: bool = False
|
|
264
269
|
"""Whether the cached token count is stale (interrupted generation)."""
|
|
@@ -25,7 +25,7 @@ from soothe_cli.cli.execution.daemon_errors import (
|
|
|
25
25
|
from soothe_cli.cli.execution.daemon_errors import (
|
|
26
26
|
friendly_daemon_execution_error as _friendly_agent_execution_error,
|
|
27
27
|
)
|
|
28
|
-
from soothe_cli.runtime.state.session_stats import SessionStats
|
|
28
|
+
from soothe_cli.runtime.state.session_stats import SessionStats
|
|
29
29
|
from soothe_cli.tui import theme
|
|
30
30
|
from soothe_cli.tui._cli_context import CLIContext
|
|
31
31
|
from soothe_cli.tui.app._module_init import (
|
|
@@ -450,37 +450,6 @@ class _ExecutionMixin:
|
|
|
450
450
|
"""
|
|
451
451
|
return f"{prefix}: {loop_id}"
|
|
452
452
|
|
|
453
|
-
async def _get_conversation_token_count(self) -> int | None:
|
|
454
|
-
"""Return the approximate conversation-only token count.
|
|
455
|
-
|
|
456
|
-
Returns:
|
|
457
|
-
Token count as an integer, or `None` if state is unavailable.
|
|
458
|
-
"""
|
|
459
|
-
if not self._lc_loop_id:
|
|
460
|
-
return None
|
|
461
|
-
try:
|
|
462
|
-
from langchain_core.messages import messages_from_dict
|
|
463
|
-
from langchain_core.messages.utils import count_tokens_approximately
|
|
464
|
-
|
|
465
|
-
if self._daemon_session is None:
|
|
466
|
-
return None
|
|
467
|
-
snap = await self._daemon_session.aget_loop_state(self._lc_loop_id)
|
|
468
|
-
vals = getattr(snap, "values", None)
|
|
469
|
-
if not isinstance(vals, dict):
|
|
470
|
-
return None
|
|
471
|
-
raw = vals.get("messages")
|
|
472
|
-
if not isinstance(raw, list) or not raw:
|
|
473
|
-
return None
|
|
474
|
-
if isinstance(raw[0], dict):
|
|
475
|
-
messages = messages_from_dict(raw)
|
|
476
|
-
else:
|
|
477
|
-
messages = raw
|
|
478
|
-
|
|
479
|
-
return count_tokens_approximately(messages)
|
|
480
|
-
except Exception: # best-effort for /tokens display
|
|
481
|
-
logger.debug("Failed to retrieve conversation token count", exc_info=True)
|
|
482
|
-
return None
|
|
483
|
-
|
|
484
453
|
async def _handle_command(self, command: str) -> None:
|
|
485
454
|
"""Handle a slash command.
|
|
486
455
|
|
|
@@ -516,9 +485,11 @@ class _ExecutionMixin:
|
|
|
516
485
|
self.query_one("#chat", VerticalScroll).anchor()
|
|
517
486
|
return
|
|
518
487
|
|
|
488
|
+
from soothe_cli.tui.command_registry import resolve_command_head
|
|
519
489
|
from soothe_cli.tui.config import settings
|
|
520
490
|
|
|
521
491
|
cmd = command.lower().strip()
|
|
492
|
+
cmd_head = resolve_command_head(command)
|
|
522
493
|
|
|
523
494
|
if cmd in {"/quit", "/q"}:
|
|
524
495
|
self._detach_or_exit()
|
|
@@ -540,9 +511,7 @@ class _ExecutionMixin:
|
|
|
540
511
|
self._pending_messages.clear()
|
|
541
512
|
self._queued_widgets.clear()
|
|
542
513
|
await self._clear_messages()
|
|
543
|
-
self.
|
|
544
|
-
self._tokens_approximate = False
|
|
545
|
-
self._update_tokens(0)
|
|
514
|
+
self._reset_loop_token_usage(None)
|
|
546
515
|
# Clear status message (e.g., "Interrupted" from previous session)
|
|
547
516
|
self._update_status("")
|
|
548
517
|
if self._session_state:
|
|
@@ -564,6 +533,7 @@ class _ExecutionMixin:
|
|
|
564
533
|
)
|
|
565
534
|
self._session_state.loop_id = new_loop_id
|
|
566
535
|
self._lc_loop_id = new_loop_id
|
|
536
|
+
self._reset_loop_token_usage(new_loop_id)
|
|
567
537
|
try:
|
|
568
538
|
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
569
539
|
banner.update_loop_id(new_loop_id)
|
|
@@ -579,51 +549,8 @@ class _ExecutionMixin:
|
|
|
579
549
|
await self._handle_update_command()
|
|
580
550
|
elif cmd == "/auto-update":
|
|
581
551
|
await self._handle_auto_update_toggle()
|
|
582
|
-
elif
|
|
583
|
-
await self.
|
|
584
|
-
if self._context_tokens > 0:
|
|
585
|
-
count = self._context_tokens
|
|
586
|
-
formatted = format_token_count(count)
|
|
587
|
-
|
|
588
|
-
model_name = settings.model_name
|
|
589
|
-
context_limit = settings.model_context_limit
|
|
590
|
-
|
|
591
|
-
if context_limit is not None:
|
|
592
|
-
limit_str = format_token_count(context_limit)
|
|
593
|
-
pct = count / context_limit * 100
|
|
594
|
-
usage = f"{formatted} / {limit_str} tokens ({pct:.0f}%)"
|
|
595
|
-
else:
|
|
596
|
-
usage = f"{formatted} tokens used"
|
|
597
|
-
|
|
598
|
-
msg = f"{usage} \u00b7 {model_name}" if model_name else usage
|
|
599
|
-
|
|
600
|
-
conv_tokens = await self._get_conversation_token_count()
|
|
601
|
-
if conv_tokens is not None:
|
|
602
|
-
overhead = max(0, count - conv_tokens)
|
|
603
|
-
overhead_str = format_token_count(overhead)
|
|
604
|
-
conv_str = format_token_count(conv_tokens)
|
|
605
|
-
|
|
606
|
-
overhead_unit = " tokens" if overhead < 1000 else "" # noqa: PLR2004 # not bothersome, cosmetic
|
|
607
|
-
conv_unit = " tokens" if conv_tokens < 1000 else "" # noqa: PLR2004 # not bothersome, cosmetic
|
|
608
|
-
|
|
609
|
-
msg += (
|
|
610
|
-
f"\n\u251c System prompt + tools: ~{overhead_str}{overhead_unit} (fixed)" # noqa: E501
|
|
611
|
-
f"\n\u2514 Conversation: ~{conv_str}{conv_unit}"
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
await self._mount_message(AppMessage(msg))
|
|
615
|
-
else:
|
|
616
|
-
model_name = settings.model_name
|
|
617
|
-
context_limit = settings.model_context_limit
|
|
618
|
-
|
|
619
|
-
parts: list[str] = ["No token usage yet"]
|
|
620
|
-
if context_limit is not None:
|
|
621
|
-
limit_str = format_token_count(context_limit)
|
|
622
|
-
parts.append(f"{limit_str} token context window")
|
|
623
|
-
if model_name:
|
|
624
|
-
parts.append(model_name)
|
|
625
|
-
|
|
626
|
-
await self._mount_message(AppMessage(" · ".join(parts)))
|
|
552
|
+
elif cmd_head == "/context":
|
|
553
|
+
await self._show_context_viewer()
|
|
627
554
|
elif cmd == "/skill-creator" or cmd.startswith("/skill-creator "):
|
|
628
555
|
# Convenience alias for /skill:skill-creator — shorter and
|
|
629
556
|
# discoverable before skill loading completes.
|
|
@@ -654,8 +581,6 @@ class _ExecutionMixin:
|
|
|
654
581
|
)
|
|
655
582
|
return
|
|
656
583
|
await self._submit_cron_job(args, slash_input=command)
|
|
657
|
-
elif cmd == "/context":
|
|
658
|
-
await self._show_context_viewer()
|
|
659
584
|
elif cmd == "/mcp":
|
|
660
585
|
await self._show_mcp_viewer()
|
|
661
586
|
elif cmd == "/theme":
|
|
@@ -1046,7 +971,7 @@ class _ExecutionMixin:
|
|
|
1046
971
|
|
|
1047
972
|
# Ensure token display is restored (in case of early cancellation).
|
|
1048
973
|
# Pass the cached approximate flag so an interrupted "+" isn't clobbered.
|
|
1049
|
-
self.
|
|
974
|
+
self._refresh_token_displays(approximate=self._tokens_approximate)
|
|
1050
975
|
|
|
1051
976
|
try:
|
|
1052
977
|
await self._maybe_drain_deferred()
|
|
@@ -185,9 +185,12 @@ class _HistoryMixin:
|
|
|
185
185
|
)
|
|
186
186
|
return _LoopHistoryPayload([], context_tokens, goal_dicts)
|
|
187
187
|
|
|
188
|
-
from soothe_sdk.display.card_binder import
|
|
188
|
+
from soothe_sdk.display.card_binder import (
|
|
189
|
+
merge_consecutive_assistant_cards,
|
|
190
|
+
sanitize_resume_display_cards,
|
|
191
|
+
)
|
|
189
192
|
|
|
190
|
-
data = merge_consecutive_assistant_cards(data)
|
|
193
|
+
data = sanitize_resume_display_cards(merge_consecutive_assistant_cards(data))
|
|
191
194
|
|
|
192
195
|
return _LoopHistoryPayload(data, context_tokens, goal_dicts)
|
|
193
196
|
|
|
@@ -294,6 +297,7 @@ class _HistoryMixin:
|
|
|
294
297
|
|
|
295
298
|
logger.info("Starting background event consumer for subscribed loop")
|
|
296
299
|
from langchain_core.messages import AIMessage, AIMessageChunk, ToolMessage
|
|
300
|
+
from soothe_sdk.ux.loop_stream import LOOP_ASSISTANT_OUTPUT_PHASES, assistant_output_phase
|
|
297
301
|
|
|
298
302
|
assistant_cards_by_ns: dict[tuple[Any, ...], AssistantMessage] = {}
|
|
299
303
|
last_user_text_by_ns: dict[tuple[Any, ...], str] = {}
|
|
@@ -324,6 +328,9 @@ class _HistoryMixin:
|
|
|
324
328
|
message, _metadata = data
|
|
325
329
|
message = normalize_stream_message(message)
|
|
326
330
|
|
|
331
|
+
if self._is_loop_internal_checkpoint_message(message):
|
|
332
|
+
continue
|
|
333
|
+
|
|
327
334
|
user_text = extract_user_text_for_display(message)
|
|
328
335
|
if user_text is not None:
|
|
329
336
|
# Deduplicate immediate replayed user rows after reconnect/resubscribe.
|
|
@@ -338,6 +345,9 @@ class _HistoryMixin:
|
|
|
338
345
|
continue
|
|
339
346
|
|
|
340
347
|
if isinstance(message, (AIMessage, AIMessageChunk)):
|
|
348
|
+
phase = assistant_output_phase(message)
|
|
349
|
+
if phase is not None and phase not in LOOP_ASSISTANT_OUTPUT_PHASES:
|
|
350
|
+
continue
|
|
341
351
|
extracted = extract_ai_text_for_display(message)
|
|
342
352
|
is_terminal = is_stream_terminal(message)
|
|
343
353
|
if extracted:
|
|
@@ -96,9 +96,9 @@ class _MessagesMixin:
|
|
|
96
96
|
if not payload.messages:
|
|
97
97
|
return
|
|
98
98
|
|
|
99
|
-
# Seed token
|
|
99
|
+
# Seed loop token total from persisted checkpoint
|
|
100
100
|
if payload.context_tokens > 0:
|
|
101
|
-
self.
|
|
101
|
+
self._seed_loop_token_from_checkpoint(payload.context_tokens)
|
|
102
102
|
|
|
103
103
|
# Bulk load into store (sets visible window; replaces prior data).
|
|
104
104
|
_archived, visible = self._message_store.bulk_load(
|
|
@@ -113,9 +113,12 @@ class _MessagesMixin:
|
|
|
113
113
|
return
|
|
114
114
|
|
|
115
115
|
# Create and mount only visible widgets (max WINDOW_SIZE).
|
|
116
|
+
from soothe_sdk.display.card_binder import sanitize_resume_display_cards
|
|
117
|
+
|
|
116
118
|
from soothe_cli.tui.binding import message_to_widget
|
|
117
119
|
|
|
118
120
|
visible = self._dedupe_message_data_by_id(visible)
|
|
121
|
+
visible = sanitize_resume_display_cards(visible)
|
|
119
122
|
widgets = [message_to_widget(msg_data) for msg_data in visible]
|
|
120
123
|
if widgets:
|
|
121
124
|
await messages_container.mount(*widgets)
|
|
@@ -9,7 +9,7 @@ from contextlib import suppress
|
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
|
|
12
|
+
from soothe_cli.tui.widgets.context_data import TokenUsageSnapshot
|
|
13
13
|
|
|
14
14
|
from textual.app import ScreenStackError
|
|
15
15
|
from textual.containers import VerticalScroll
|
|
@@ -259,8 +259,24 @@ class _ModelMixin:
|
|
|
259
259
|
return None
|
|
260
260
|
return resp.get("servers")
|
|
261
261
|
|
|
262
|
+
async def _load_token_usage_snapshot(self) -> TokenUsageSnapshot:
|
|
263
|
+
"""Build the token usage snapshot for the context modal."""
|
|
264
|
+
from soothe_cli.tui.config import settings
|
|
265
|
+
from soothe_cli.tui.widgets.context_data import load_token_usage_snapshot
|
|
266
|
+
|
|
267
|
+
return await load_token_usage_snapshot(
|
|
268
|
+
context_tokens=self._loop_token_total(),
|
|
269
|
+
approximate=self._tokens_approximate,
|
|
270
|
+
loop_id=self._lc_loop_id,
|
|
271
|
+
daemon_session=self._daemon_session,
|
|
272
|
+
model_name=settings.model_name,
|
|
273
|
+
context_limit=settings.model_context_limit,
|
|
274
|
+
input_tokens=self._loop_baseline_tokens + self._loop_input_tokens,
|
|
275
|
+
output_tokens=self._loop_output_tokens,
|
|
276
|
+
)
|
|
277
|
+
|
|
262
278
|
async def _show_context_viewer(self) -> None:
|
|
263
|
-
"""Show context engine goal DAG
|
|
279
|
+
"""Show token usage and context engine goal DAG as a modal screen."""
|
|
264
280
|
from soothe_cli.tui.widgets.context_viewer import ContextViewerScreen
|
|
265
281
|
|
|
266
282
|
def handle_result(result: None) -> None: # noqa: ARG001
|
|
@@ -268,7 +284,12 @@ class _ModelMixin:
|
|
|
268
284
|
self._chat_input.focus_input()
|
|
269
285
|
|
|
270
286
|
loop_id = self._lc_loop_id
|
|
271
|
-
|
|
287
|
+
initial_snapshot = await self._load_token_usage_snapshot()
|
|
288
|
+
screen = ContextViewerScreen(
|
|
289
|
+
loop_id=loop_id,
|
|
290
|
+
load_token_snapshot=self._load_token_usage_snapshot,
|
|
291
|
+
initial_token_snapshot=initial_snapshot,
|
|
292
|
+
)
|
|
272
293
|
self.push_screen(screen, handle_result)
|
|
273
294
|
|
|
274
295
|
async def _show_help_screen(self) -> None:
|
|
@@ -459,9 +480,7 @@ class _ModelMixin:
|
|
|
459
480
|
self._pending_messages.clear()
|
|
460
481
|
self._queued_widgets.clear()
|
|
461
482
|
await self._clear_messages()
|
|
462
|
-
self.
|
|
463
|
-
self._tokens_approximate = False
|
|
464
|
-
self._update_tokens(0)
|
|
483
|
+
self._reset_loop_token_usage(None)
|
|
465
484
|
self._update_status("")
|
|
466
485
|
|
|
467
486
|
status = await self._daemon_session.switch_loop(loop_id)
|
|
@@ -300,7 +300,7 @@ class _LoopHistoryPayload:
|
|
|
300
300
|
"""Converted message data ready for bulk loading."""
|
|
301
301
|
|
|
302
302
|
context_tokens: int
|
|
303
|
-
"""Persisted
|
|
303
|
+
"""Persisted accumulated loop token usage from checkpoint (0 if absent)."""
|
|
304
304
|
|
|
305
305
|
goals: tuple[dict[str, Any], ...] = ()
|
|
306
306
|
"""Goal display snapshots from ``loop_history_fetch`` (RFC-631)."""
|
|
@@ -199,9 +199,14 @@ class _StartupMixin:
|
|
|
199
199
|
)
|
|
200
200
|
|
|
201
201
|
# Wire token display callbacks
|
|
202
|
-
self._ui_adapter.
|
|
202
|
+
self._ui_adapter._seed_loop_token_from_checkpoint = self._seed_loop_token_from_checkpoint
|
|
203
|
+
self._ui_adapter._on_turn_tokens = self._record_loop_turn_tokens
|
|
204
|
+
self._ui_adapter._get_loop_token_total = self._loop_token_total
|
|
205
|
+
self._ui_adapter._on_refresh_token_displays = self._refresh_token_displays
|
|
206
|
+
self._ui_adapter._get_loop_token_breakdown = self._loop_token_breakdown
|
|
207
|
+
self._ui_adapter._on_begin_loop_turn_tokens = self._begin_loop_turn_tokens
|
|
208
|
+
self._ui_adapter._apply_authoritative_loop_tokens = self._apply_authoritative_loop_tokens
|
|
203
209
|
self._ui_adapter._on_tokens_hide = self._hide_tokens
|
|
204
|
-
self._ui_adapter._on_tokens_show = self._show_tokens
|
|
205
210
|
self._dump_tui_debug_snapshot()
|
|
206
211
|
|
|
207
212
|
# Seed static slash commands now that the first frame has rendered.
|