algo-cli-runtime 0.14.0__py3-none-any.whl
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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- ollama_cli/__init__.py +67 -0
algo_cli/display.py
ADDED
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
"""Rich terminal display for the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Iterator
|
|
12
|
+
|
|
13
|
+
from rich.align import Align
|
|
14
|
+
from rich.console import Capture, Console
|
|
15
|
+
from rich.console import Group
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.markdown import Markdown
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich import box
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
from rich.theme import Theme
|
|
23
|
+
|
|
24
|
+
from algo_cli.animations import (
|
|
25
|
+
AIState,
|
|
26
|
+
animation_for,
|
|
27
|
+
buddy_frame,
|
|
28
|
+
current_frame,
|
|
29
|
+
glyph,
|
|
30
|
+
register_spinners,
|
|
31
|
+
spinner_name,
|
|
32
|
+
state_line,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from algo_cli import __version__ as _CLI_VERSION
|
|
37
|
+
except Exception:
|
|
38
|
+
_CLI_VERSION = "dev"
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from algo_cli.config import CONFIG_DIR, _atomic_write_text
|
|
42
|
+
except Exception:
|
|
43
|
+
CONFIG_DIR = Path.home() / ".algo_cli"
|
|
44
|
+
_atomic_write_text = None # type: ignore[assignment]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
DEFAULT_THEME_NAME = (
|
|
48
|
+
os.environ.get("ALGO_CLI_THEME")
|
|
49
|
+
or os.environ.get("OLLAMA_CLI_THEME")
|
|
50
|
+
or "tokyo-night"
|
|
51
|
+
).strip().lower() or "tokyo-night"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _theme(styles: dict[str, str]) -> Theme:
|
|
55
|
+
return Theme(styles)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
THEME_COLORS: dict[str, dict[str, str]] = {
|
|
59
|
+
"tokyo-night": {
|
|
60
|
+
"primary": "#7aa2f7",
|
|
61
|
+
"secondary": "#bb9af7",
|
|
62
|
+
"accent": "#2ac3de",
|
|
63
|
+
"surface": "#111827",
|
|
64
|
+
"surface_alt": "#0f172a",
|
|
65
|
+
"border": "#1f2a44",
|
|
66
|
+
"border_accent": "#3b4b72",
|
|
67
|
+
"text": "#e5e9f0",
|
|
68
|
+
"muted": "#7b88a8",
|
|
69
|
+
"success": "#9ece6a",
|
|
70
|
+
"warning": "#e0af68",
|
|
71
|
+
"error": "#f7768e",
|
|
72
|
+
"info": "#7dcfff",
|
|
73
|
+
},
|
|
74
|
+
"catppuccin-mocha": {
|
|
75
|
+
"primary": "#89b4fa",
|
|
76
|
+
"secondary": "#cba6f7",
|
|
77
|
+
"accent": "#94e2d5",
|
|
78
|
+
"surface": "#11111b",
|
|
79
|
+
"surface_alt": "#1e1e2e",
|
|
80
|
+
"border": "#313244",
|
|
81
|
+
"border_accent": "#45475a",
|
|
82
|
+
"text": "#cdd6f4",
|
|
83
|
+
"muted": "#6c7086",
|
|
84
|
+
"success": "#a6e3a1",
|
|
85
|
+
"warning": "#f9e2af",
|
|
86
|
+
"error": "#f38ba8",
|
|
87
|
+
"info": "#89dceb",
|
|
88
|
+
},
|
|
89
|
+
"dracula": {
|
|
90
|
+
"primary": "#bd93f9",
|
|
91
|
+
"secondary": "#ff79c6",
|
|
92
|
+
"accent": "#8be9fd",
|
|
93
|
+
"surface": "#282a36",
|
|
94
|
+
"surface_alt": "#1e1f29",
|
|
95
|
+
"border": "#44475a",
|
|
96
|
+
"border_accent": "#6272a4",
|
|
97
|
+
"text": "#f8f8f2",
|
|
98
|
+
"muted": "#6272a4",
|
|
99
|
+
"success": "#50fa7b",
|
|
100
|
+
"warning": "#f1fa8c",
|
|
101
|
+
"error": "#ff5555",
|
|
102
|
+
"info": "#8be9fd",
|
|
103
|
+
},
|
|
104
|
+
"nord": {
|
|
105
|
+
"primary": "#88c0d0",
|
|
106
|
+
"secondary": "#b48ead",
|
|
107
|
+
"accent": "#81a1c1",
|
|
108
|
+
"surface": "#2e3440",
|
|
109
|
+
"surface_alt": "#3b4252",
|
|
110
|
+
"border": "#4c566a",
|
|
111
|
+
"border_accent": "#5e81ac",
|
|
112
|
+
"text": "#eceff4",
|
|
113
|
+
"muted": "#81a1c1",
|
|
114
|
+
"success": "#a3be8c",
|
|
115
|
+
"warning": "#ebcb8b",
|
|
116
|
+
"error": "#bf616a",
|
|
117
|
+
"info": "#8fbcbb",
|
|
118
|
+
},
|
|
119
|
+
"gruvbox": {
|
|
120
|
+
"primary": "#83a598",
|
|
121
|
+
"secondary": "#d3869b",
|
|
122
|
+
"accent": "#8ec07c",
|
|
123
|
+
"surface": "#282828",
|
|
124
|
+
"surface_alt": "#3c3836",
|
|
125
|
+
"border": "#504945",
|
|
126
|
+
"border_accent": "#665c54",
|
|
127
|
+
"text": "#ebdbb2",
|
|
128
|
+
"muted": "#928374",
|
|
129
|
+
"success": "#b8bb26",
|
|
130
|
+
"warning": "#fabd2f",
|
|
131
|
+
"error": "#fb4934",
|
|
132
|
+
"info": "#83a598",
|
|
133
|
+
},
|
|
134
|
+
# Brand theme matching the Redeye mark: bold red on near-black,
|
|
135
|
+
# high contrast, minimal mid-tones.
|
|
136
|
+
"redeye": {
|
|
137
|
+
"primary": "#e22b2b",
|
|
138
|
+
"secondary": "#ff6b6b",
|
|
139
|
+
"accent": "#ff3b3b",
|
|
140
|
+
"surface": "#0a0a0a",
|
|
141
|
+
"surface_alt": "#111111",
|
|
142
|
+
"border": "#2a1212",
|
|
143
|
+
"border_accent": "#7a1f1f",
|
|
144
|
+
"text": "#f2f2f2",
|
|
145
|
+
"muted": "#8a7070",
|
|
146
|
+
"success": "#6fcf6f",
|
|
147
|
+
"warning": "#ffb347",
|
|
148
|
+
"error": "#ff2e2e",
|
|
149
|
+
"info": "#ff8a8a",
|
|
150
|
+
},
|
|
151
|
+
"dolphie": {
|
|
152
|
+
"primary": "#bbc8e8",
|
|
153
|
+
"secondary": "#91abec",
|
|
154
|
+
"accent": "#8f9fc1",
|
|
155
|
+
"surface": "#0f1525",
|
|
156
|
+
"surface_alt": "#0a0e1b",
|
|
157
|
+
"border": "#1b233a",
|
|
158
|
+
"border_accent": "#32416a",
|
|
159
|
+
"text": "#e9e9e9",
|
|
160
|
+
"muted": "#5e6b87",
|
|
161
|
+
"success": "#54efae",
|
|
162
|
+
"warning": "#f0e357",
|
|
163
|
+
"error": "#f05757",
|
|
164
|
+
"info": "#8f9fc1",
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
THEME_MAP: dict[str, Theme] = {name: _theme(colors) for name, colors in THEME_COLORS.items()}
|
|
169
|
+
|
|
170
|
+
_base_theme_name = DEFAULT_THEME_NAME if DEFAULT_THEME_NAME in THEME_MAP else "tokyo-night"
|
|
171
|
+
_active_theme_name = _base_theme_name
|
|
172
|
+
_theme_pushed = False
|
|
173
|
+
console = Console(theme=THEME_MAP[_base_theme_name])
|
|
174
|
+
register_spinners() # make algo-* state spinners available to console.status
|
|
175
|
+
_stream_live: Live | None = None
|
|
176
|
+
_stream_buffer = ""
|
|
177
|
+
_stream_pending = ""
|
|
178
|
+
_last_render_time = 0.0
|
|
179
|
+
_RENDER_INTERVAL = 1.0 / 12 # cap Markdown re-parses at 12 fps regardless of token rate
|
|
180
|
+
_thinking_live: Live | None = None
|
|
181
|
+
_thinking_buffer = ""
|
|
182
|
+
_thinking_pending = ""
|
|
183
|
+
_thinking_last_render_time = 0.0
|
|
184
|
+
_thinking_started_at = 0.0
|
|
185
|
+
_THINKING_VISIBLE_CHARS = 1200
|
|
186
|
+
|
|
187
|
+
# One-shot JSON event sink. When set, display helpers route to it instead of
|
|
188
|
+
# rendering Rich panels. Installed by algo_cli.oneshot.run_oneshot().
|
|
189
|
+
_json_sink: Any = None
|
|
190
|
+
_console_capture_active: ContextVar[bool] = ContextVar(
|
|
191
|
+
"algo_cli_console_capture_active",
|
|
192
|
+
default=False,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def install_json_sink(sink: Any) -> None:
|
|
197
|
+
global _json_sink
|
|
198
|
+
_json_sink = sink
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def uninstall_json_sink() -> None:
|
|
202
|
+
global _json_sink
|
|
203
|
+
_json_sink = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def json_sink() -> Any:
|
|
207
|
+
return _json_sink
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def json_mode_active() -> bool:
|
|
211
|
+
"""True when --oneshot --json (or another bridge) owns stdout."""
|
|
212
|
+
return _json_sink is not None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@contextmanager
|
|
216
|
+
def capture_console_output() -> Iterator[Capture]:
|
|
217
|
+
"""Capture this context's Rich output, including JSON-suppressed notices.
|
|
218
|
+
|
|
219
|
+
Rich stores capture buffers per thread, and the ContextVar keeps async/task
|
|
220
|
+
contexts isolated. Direct interactive rendering is therefore unaffected.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
token = _console_capture_active.set(True)
|
|
224
|
+
try:
|
|
225
|
+
with console.capture() as captured:
|
|
226
|
+
yield captured
|
|
227
|
+
finally:
|
|
228
|
+
_console_capture_active.reset(token)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@contextmanager
|
|
232
|
+
def tool_execution_status(label: str, *, spinner: str | None = None) -> Iterator[None]:
|
|
233
|
+
"""Rich spinner for interactive mode only; no-op for JSON/bridge consumers.
|
|
234
|
+
|
|
235
|
+
Defaults to the branded DOING progress sweep; pass any rich spinner name
|
|
236
|
+
to override.
|
|
237
|
+
"""
|
|
238
|
+
if _json_sink is not None:
|
|
239
|
+
yield
|
|
240
|
+
return
|
|
241
|
+
doing = animation_for(AIState.DOING)
|
|
242
|
+
with console.status(label, spinner=spinner or spinner_name(AIState.DOING), spinner_style=doing.style):
|
|
243
|
+
yield
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def available_themes() -> list[str]:
|
|
247
|
+
return sorted(THEME_MAP)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def current_theme_name() -> str:
|
|
251
|
+
return _active_theme_name
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def set_theme(name: str) -> str:
|
|
255
|
+
global _active_theme_name, _theme_pushed
|
|
256
|
+
|
|
257
|
+
candidate = (name or "").strip().lower().replace("_", "-")
|
|
258
|
+
if not candidate:
|
|
259
|
+
candidate = "tokyo-night"
|
|
260
|
+
if candidate not in THEME_MAP:
|
|
261
|
+
raise ValueError(f"Unknown theme: {name}")
|
|
262
|
+
if candidate == _active_theme_name:
|
|
263
|
+
return candidate
|
|
264
|
+
if _theme_pushed:
|
|
265
|
+
console.pop_theme()
|
|
266
|
+
_theme_pushed = False
|
|
267
|
+
if candidate != _base_theme_name:
|
|
268
|
+
console.push_theme(THEME_MAP[candidate])
|
|
269
|
+
_theme_pushed = True
|
|
270
|
+
_active_theme_name = candidate
|
|
271
|
+
return candidate
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def theme_colors(name: str | None = None) -> dict[str, str]:
|
|
275
|
+
candidate = (name or _active_theme_name or _base_theme_name).strip().lower().replace("_", "-")
|
|
276
|
+
if candidate not in THEME_COLORS:
|
|
277
|
+
candidate = _base_theme_name
|
|
278
|
+
return THEME_COLORS[candidate]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Block logo (ansi_shadow-style), matching Hermes Agent banner weight.
|
|
282
|
+
_ALGO_CLI_LOGO: tuple[str, ...] = (
|
|
283
|
+
" █████╗ ██╗ ██████╗ ██████╗ ██████╗██╗ ██╗",
|
|
284
|
+
"██╔══██╗██║ ██╔════╝ ██╔═══██╗ ██╔════╝██║ ██║",
|
|
285
|
+
"███████║██║ ██║ ███╗██║ ██║ ██║ ██║ ██║",
|
|
286
|
+
"██╔══██║██║ ██║ ██║██║ ██║ ██║ ██║ ██║",
|
|
287
|
+
"██║ ██║███████╗╚██████╔╝╚██████╔╝ ╚██████╗███████╗██║",
|
|
288
|
+
"╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝╚══════╝╚═╝",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
_PRODUCT_TAGLINE = "Agent runtime for tools, durable context, and verified work."
|
|
292
|
+
_BANNER_CAPABILITIES: tuple[tuple[str, str], ...] = (
|
|
293
|
+
("Inference", "Ollama local/cloud · xAI Grok · ChatGPT/Codex"),
|
|
294
|
+
("Context", "skills · memory · repository intelligence · knowledge graph"),
|
|
295
|
+
("Workflows", "direct chat · routed agent pipelines · one-shot JSON"),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _banner_flow_line() -> Text:
|
|
300
|
+
line = Text()
|
|
301
|
+
for index, (label, style) in enumerate((
|
|
302
|
+
("understand", "secondary"),
|
|
303
|
+
("route", "primary"),
|
|
304
|
+
("act", "accent"),
|
|
305
|
+
("verify", "success"),
|
|
306
|
+
("remember", "secondary"),
|
|
307
|
+
)):
|
|
308
|
+
if index:
|
|
309
|
+
line.append(" → ", style="muted")
|
|
310
|
+
line.append(label, style=f"bold {style}")
|
|
311
|
+
return line
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _banner_body_layout() -> Group:
|
|
315
|
+
capabilities = Table.grid(padding=(0, 2), expand=True)
|
|
316
|
+
capabilities.add_column(width=10, style="muted", no_wrap=True)
|
|
317
|
+
capabilities.add_column(ratio=1, style="text")
|
|
318
|
+
for label, detail in _BANNER_CAPABILITIES:
|
|
319
|
+
capabilities.add_row(Text(label, style="bold secondary"), detail)
|
|
320
|
+
return Group(
|
|
321
|
+
Text(_PRODUCT_TAGLINE, style="text"),
|
|
322
|
+
Text(""),
|
|
323
|
+
_banner_flow_line(),
|
|
324
|
+
Text(""),
|
|
325
|
+
capabilities,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _print_algo_logo() -> None:
|
|
330
|
+
if console.width < 70:
|
|
331
|
+
console.print(Align.center(Text.assemble(
|
|
332
|
+
("ALGO", "bold primary"),
|
|
333
|
+
(" / ", "muted"),
|
|
334
|
+
("CLI", "bold secondary"),
|
|
335
|
+
)))
|
|
336
|
+
return
|
|
337
|
+
for line in _ALGO_CLI_LOGO:
|
|
338
|
+
console.print(Align.center(Text(line.rstrip(), style="bold primary")))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def render_opening_banner(*, version: str | None = None) -> Panel:
|
|
342
|
+
version_line = version or _CLI_VERSION
|
|
343
|
+
panel_title = (
|
|
344
|
+
f"[bold primary]Algo CLI[/] [muted]v{version_line}[/] "
|
|
345
|
+
f"[muted]·[/] [accent]agent runtime[/]"
|
|
346
|
+
)
|
|
347
|
+
return Panel(
|
|
348
|
+
_banner_body_layout(),
|
|
349
|
+
title=panel_title,
|
|
350
|
+
subtitle="[muted]ask naturally · type / for commands[/]",
|
|
351
|
+
border_style="border_accent",
|
|
352
|
+
box=box.ROUNDED,
|
|
353
|
+
padding=(1, 2),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def show_banner() -> None:
|
|
358
|
+
if json_mode_active():
|
|
359
|
+
return
|
|
360
|
+
console.print()
|
|
361
|
+
_print_algo_logo()
|
|
362
|
+
console.print()
|
|
363
|
+
|
|
364
|
+
console.print(render_opening_banner())
|
|
365
|
+
console.print(state_line(AIState.IDLE, "ready — ask naturally or type /"))
|
|
366
|
+
console.print()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def compact_path(path: str, limit: int = 42) -> str:
|
|
370
|
+
text = str(path).strip()
|
|
371
|
+
if len(text) <= limit:
|
|
372
|
+
return text
|
|
373
|
+
p = Path(text)
|
|
374
|
+
parts = p.parts
|
|
375
|
+
if len(parts) >= 2:
|
|
376
|
+
tail = Path(*parts[-2:]).as_posix().replace("/", "\\") if "\\" in text else Path(*parts[-2:]).as_posix()
|
|
377
|
+
if len(tail) + 1 < limit:
|
|
378
|
+
return f"...{tail}"
|
|
379
|
+
return f"{text[: max(1, limit - 3)]}..."
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _kv_table(rows: list[tuple[str, str]]) -> Table:
|
|
383
|
+
table = Table.grid(padding=(0, 1), expand=True)
|
|
384
|
+
table.add_column(style="muted", no_wrap=True)
|
|
385
|
+
table.add_column(style="text", overflow="ellipsis")
|
|
386
|
+
for label, value in rows:
|
|
387
|
+
table.add_row(f"{label}:", value)
|
|
388
|
+
return table
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _format_bytes(value: Any) -> str:
|
|
392
|
+
try:
|
|
393
|
+
size = int(value)
|
|
394
|
+
except Exception:
|
|
395
|
+
return str(value) if value is not None else "?"
|
|
396
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
397
|
+
amount = float(size)
|
|
398
|
+
for unit in units:
|
|
399
|
+
if amount < 1024 or unit == units[-1]:
|
|
400
|
+
return f"{amount:.1f} {unit}" if unit != "B" else f"{int(amount)} B"
|
|
401
|
+
amount /= 1024
|
|
402
|
+
return f"{size} B"
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _format_dt(value: Any) -> str:
|
|
406
|
+
if not value:
|
|
407
|
+
return "?"
|
|
408
|
+
try:
|
|
409
|
+
return value.strftime("%Y-%m-%d")
|
|
410
|
+
except Exception:
|
|
411
|
+
return str(value)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _message_preview(message: dict[str, Any], limit: int = 220) -> str:
|
|
415
|
+
content = str(message.get("content") or message.get("thinking") or "").strip()
|
|
416
|
+
if not content:
|
|
417
|
+
return "(empty)"
|
|
418
|
+
content = " ".join(content.split())
|
|
419
|
+
return content if len(content) <= limit else content[: limit - 1] + "..."
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _render_model_rows(items: list[dict[str, str]], *, max_rows: int = 4) -> Table:
|
|
423
|
+
table = Table.grid(padding=(0, 1), expand=True)
|
|
424
|
+
table.add_column(style="text", ratio=2, overflow="ellipsis")
|
|
425
|
+
table.add_column(style="text", justify="right", no_wrap=True)
|
|
426
|
+
table.add_column(style="text", justify="right", no_wrap=True)
|
|
427
|
+
for row in items[:max_rows]:
|
|
428
|
+
table.add_row(
|
|
429
|
+
row.get("name", "?"),
|
|
430
|
+
row.get("size", "?"),
|
|
431
|
+
row.get("quant", "?"),
|
|
432
|
+
)
|
|
433
|
+
if not items:
|
|
434
|
+
table.add_row("[muted]No models found[/]", "", "")
|
|
435
|
+
return table
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _render_running_rows(items: list[dict[str, str]], *, max_rows: int = 3) -> Table:
|
|
439
|
+
table = Table.grid(padding=(0, 1), expand=True)
|
|
440
|
+
table.add_column(style="text", ratio=2, overflow="ellipsis")
|
|
441
|
+
table.add_column(style="text", justify="right", no_wrap=True)
|
|
442
|
+
table.add_column(style="text", justify="right", no_wrap=True)
|
|
443
|
+
for row in items[:max_rows]:
|
|
444
|
+
table.add_row(
|
|
445
|
+
row.get("name", "?"),
|
|
446
|
+
row.get("size_vram", row.get("size", "?")),
|
|
447
|
+
row.get("context", "?"),
|
|
448
|
+
)
|
|
449
|
+
if not items:
|
|
450
|
+
table.add_row("[muted]No running models[/]", "", "")
|
|
451
|
+
return table
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _render_chat_panel(cfg: Any) -> Panel:
|
|
455
|
+
recent = cfg.messages[-3:]
|
|
456
|
+
blocks: list[Any] = []
|
|
457
|
+
if not recent:
|
|
458
|
+
blocks.append(
|
|
459
|
+
Panel(
|
|
460
|
+
"[muted]No messages yet.[/]\n[dim]Type a prompt to start the session.[/]",
|
|
461
|
+
border_style="border",
|
|
462
|
+
box=box.ROUNDED,
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
for message in recent:
|
|
467
|
+
role = str(message.get("role", "message")).upper()
|
|
468
|
+
title = role
|
|
469
|
+
if message.get("name"):
|
|
470
|
+
title = f"{role} {message['name']}"
|
|
471
|
+
border = "border_accent" if role == "ASSISTANT" else "border"
|
|
472
|
+
blocks.append(
|
|
473
|
+
Panel(
|
|
474
|
+
_message_preview(message),
|
|
475
|
+
title=title,
|
|
476
|
+
border_style=border,
|
|
477
|
+
box=box.ROUNDED,
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
num_ctx = getattr(cfg, "num_ctx", None)
|
|
482
|
+
ctx_label = str(num_ctx) if num_ctx is not None else "?"
|
|
483
|
+
hint_row = Text(
|
|
484
|
+
f"Tab panes Enter send Esc stop / filter temp {cfg.temperature} ctx {ctx_label}",
|
|
485
|
+
style="muted",
|
|
486
|
+
)
|
|
487
|
+
blocks.append(hint_row)
|
|
488
|
+
blocks.append(
|
|
489
|
+
Panel(
|
|
490
|
+
Text("draft: ready for a prompt or slash command", style="muted"),
|
|
491
|
+
border_style="border",
|
|
492
|
+
box=box.ROUNDED,
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
return Panel(Group(*blocks), title="Conversation", border_style="border_accent", box=box.ROUNDED)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _render_inspector_panel(
|
|
499
|
+
cfg: Any,
|
|
500
|
+
*,
|
|
501
|
+
installed_models: list[dict[str, str]],
|
|
502
|
+
running_models: list[dict[str, str]],
|
|
503
|
+
event_lines: list[str],
|
|
504
|
+
) -> Panel:
|
|
505
|
+
sections: list[Any] = []
|
|
506
|
+
|
|
507
|
+
session_box = _kv_table(
|
|
508
|
+
[
|
|
509
|
+
("Current model", f"[bold primary]{cfg.model}[/]"),
|
|
510
|
+
("Mode", f"[text]{'cloud' if cfg.cloud else 'local'}[/]"),
|
|
511
|
+
("System prompt", f"[text]{(cfg.system.splitlines()[0] if cfg.system else '').strip() or 'default'}[/]"),
|
|
512
|
+
("Parameters", f"[text]temp {cfg.temperature}, ctx {cfg.num_ctx}, reflect {max(1, int(cfg.tool_think_every))}[/]"),
|
|
513
|
+
("cwd", f"[muted]{compact_path(cfg.cwd, 34)}[/]"),
|
|
514
|
+
]
|
|
515
|
+
)
|
|
516
|
+
sections.append(Panel(session_box, title="Session", border_style="border", box=box.ROUNDED))
|
|
517
|
+
|
|
518
|
+
model_body = _render_model_rows(installed_models)
|
|
519
|
+
sections.append(Panel(model_body, title=f"Model {len(installed_models)} installed", border_style="border_accent", box=box.ROUNDED))
|
|
520
|
+
|
|
521
|
+
runtime_body = Table.grid(padding=(0, 1), expand=True)
|
|
522
|
+
runtime_body.add_column(style="text", ratio=2, overflow="ellipsis")
|
|
523
|
+
runtime_body.add_column(style="text", justify="right", no_wrap=True)
|
|
524
|
+
runtime_body.add_column(style="text", justify="right", no_wrap=True)
|
|
525
|
+
runtime_body.add_row("[bold]Server[/]", f"[info]{cfg.host}[/]", f"[text]{'cloud' if cfg.cloud else 'local'}[/]")
|
|
526
|
+
runtime_body.add_row("[bold]Context[/]", f"[text]{cfg.num_ctx}[/]", f"[text]{'summary' if cfg.session_summary.strip() else 'live'}[/]")
|
|
527
|
+
runtime_body.add_row("[bold]Running[/]", f"[text]{len(running_models)}[/]", f"[text]{'ready' if running_models else 'idle'}[/]")
|
|
528
|
+
if running_models:
|
|
529
|
+
runtime_body.add_row("", "", "")
|
|
530
|
+
runtime_body.add_row("[muted]Active[/]", f"[text]{running_models[0].get('name', '?')}[/]", f"[text]{running_models[0].get('size_vram', running_models[0].get('size', '?'))}[/]")
|
|
531
|
+
sections.append(Panel(runtime_body, title=f"Runtime {len(running_models)} running", border_style="border", box=box.ROUNDED))
|
|
532
|
+
|
|
533
|
+
actions_body = Text(
|
|
534
|
+
"/status Runtime summary\n"
|
|
535
|
+
"/doctor Readiness checks\n"
|
|
536
|
+
"/harness status Context health\n"
|
|
537
|
+
"/changes Last agent run\n"
|
|
538
|
+
"/help Command reference",
|
|
539
|
+
style="text",
|
|
540
|
+
)
|
|
541
|
+
sections.append(Panel(actions_body, title="Inspect", border_style="border", box=box.ROUNDED))
|
|
542
|
+
|
|
543
|
+
logs_body = Table.grid(padding=(0, 1), expand=True)
|
|
544
|
+
logs_body.add_column(style="muted", no_wrap=True)
|
|
545
|
+
logs_body.add_column(style="text", overflow="ellipsis")
|
|
546
|
+
if event_lines:
|
|
547
|
+
for index, line in enumerate(event_lines[:5], 1):
|
|
548
|
+
logs_body.add_row(f"{index:02d}", line)
|
|
549
|
+
else:
|
|
550
|
+
logs_body.add_row("01", "No events yet")
|
|
551
|
+
sections.append(Panel(logs_body, title="Logs", border_style="border", box=box.ROUNDED))
|
|
552
|
+
|
|
553
|
+
return Panel(Group(*sections), title="Runtime", border_style="border", box=box.ROUNDED)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def show_session_overview(
|
|
557
|
+
model: str,
|
|
558
|
+
host: str,
|
|
559
|
+
cwd: str,
|
|
560
|
+
theme_name: str,
|
|
561
|
+
cloud: bool,
|
|
562
|
+
auto_mode: bool,
|
|
563
|
+
safe_mode: bool,
|
|
564
|
+
temperature: float,
|
|
565
|
+
used_tokens: int,
|
|
566
|
+
total_tokens: int,
|
|
567
|
+
summary_active: bool,
|
|
568
|
+
tool_think_every: int,
|
|
569
|
+
max_tool_iterations: int,
|
|
570
|
+
memory_count: int,
|
|
571
|
+
system_prompt: str | None = None,
|
|
572
|
+
messages: list[dict[str, Any]] | None = None,
|
|
573
|
+
installed_models: list[dict[str, str]] | None = None,
|
|
574
|
+
running_models: list[dict[str, str]] | None = None,
|
|
575
|
+
event_lines: list[str] | None = None,
|
|
576
|
+
) -> None:
|
|
577
|
+
used = min(max(used_tokens, 0), total_tokens) if total_tokens > 0 else used_tokens
|
|
578
|
+
remaining = max(total_tokens - used, 0) if total_tokens > 0 else 0
|
|
579
|
+
context_line = f"{used}/{total_tokens} ({int(round((remaining / total_tokens) * 100))}% left)" if total_tokens > 0 else "unknown"
|
|
580
|
+
mode_label = "cloud" if cloud else "local"
|
|
581
|
+
execution_label = "Ollama Cloud API" if cloud else host
|
|
582
|
+
installed_models = installed_models or []
|
|
583
|
+
running_models = running_models or []
|
|
584
|
+
event_lines = event_lines or []
|
|
585
|
+
|
|
586
|
+
if console.width < 100:
|
|
587
|
+
header_status: Any = _kv_table(
|
|
588
|
+
[
|
|
589
|
+
("Connected", f"[text]{execution_label}[/]"),
|
|
590
|
+
("Model", f"[bold primary]{model}[/]"),
|
|
591
|
+
("Mode", f"[info]{mode_label}[/]"),
|
|
592
|
+
("Context", f"[text]{context_line}[/]"),
|
|
593
|
+
("Safety", f"[text]{'safe' if safe_mode else 'safe off'} · {'manual approval' if not auto_mode else 'auto approval'}[/]"),
|
|
594
|
+
("Memory", f"[text]{memory_count} saved · {'summary active' if summary_active else 'live context'}[/]"),
|
|
595
|
+
("Theme", f"[secondary]{theme_name}[/]"),
|
|
596
|
+
]
|
|
597
|
+
)
|
|
598
|
+
else:
|
|
599
|
+
header_status = Table.grid(expand=True)
|
|
600
|
+
header_status.add_column(ratio=2)
|
|
601
|
+
header_status.add_column(ratio=3)
|
|
602
|
+
header_status.add_column(ratio=2)
|
|
603
|
+
header_status.add_row(
|
|
604
|
+
Panel(
|
|
605
|
+
_kv_table(
|
|
606
|
+
[
|
|
607
|
+
("Connected", f"[text]{execution_label}[/]"),
|
|
608
|
+
("Session", f"[text]{model}[/]"),
|
|
609
|
+
]
|
|
610
|
+
),
|
|
611
|
+
border_style="border",
|
|
612
|
+
box=box.ROUNDED,
|
|
613
|
+
),
|
|
614
|
+
Panel(
|
|
615
|
+
_kv_table(
|
|
616
|
+
[
|
|
617
|
+
("Mode", f"[info]{mode_label}[/]"),
|
|
618
|
+
("Context", f"[text]{context_line}[/]"),
|
|
619
|
+
("Theme", f"[secondary]{theme_name}[/]"),
|
|
620
|
+
]
|
|
621
|
+
),
|
|
622
|
+
border_style="border",
|
|
623
|
+
box=box.ROUNDED,
|
|
624
|
+
),
|
|
625
|
+
Panel(
|
|
626
|
+
_kv_table(
|
|
627
|
+
[
|
|
628
|
+
("Auto", f"[text]{'on' if auto_mode else 'off'}[/]"),
|
|
629
|
+
("Safe", f"[text]{'on' if safe_mode else 'off'}[/]"),
|
|
630
|
+
("Memories", f"[text]{memory_count}[/]"),
|
|
631
|
+
]
|
|
632
|
+
),
|
|
633
|
+
border_style="border",
|
|
634
|
+
box=box.ROUNDED,
|
|
635
|
+
),
|
|
636
|
+
)
|
|
637
|
+
header = Panel(
|
|
638
|
+
Group(
|
|
639
|
+
Text("Runtime Overview", style="bold primary"),
|
|
640
|
+
Text("Model, context, safety, and agent activity at a glance.", style="muted"),
|
|
641
|
+
header_status,
|
|
642
|
+
),
|
|
643
|
+
border_style="border_accent",
|
|
644
|
+
box=box.ROUNDED,
|
|
645
|
+
padding=(1, 2),
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
chat_cfg = type(
|
|
649
|
+
"_ChatCfg",
|
|
650
|
+
(),
|
|
651
|
+
{
|
|
652
|
+
"messages": messages or [],
|
|
653
|
+
"temperature": temperature,
|
|
654
|
+
"num_ctx": total_tokens if total_tokens > 0 else None,
|
|
655
|
+
},
|
|
656
|
+
)()
|
|
657
|
+
inspector_cfg = type(
|
|
658
|
+
"_InspectorCfg",
|
|
659
|
+
(),
|
|
660
|
+
{
|
|
661
|
+
"model": model,
|
|
662
|
+
"cloud": cloud,
|
|
663
|
+
"system": system_prompt or "",
|
|
664
|
+
"temperature": temperature,
|
|
665
|
+
"num_ctx": total_tokens,
|
|
666
|
+
"tool_think_every": tool_think_every,
|
|
667
|
+
"cwd": cwd,
|
|
668
|
+
"host": host,
|
|
669
|
+
"session_summary": "active" if summary_active else "",
|
|
670
|
+
},
|
|
671
|
+
)()
|
|
672
|
+
|
|
673
|
+
left = _render_chat_panel(chat_cfg)
|
|
674
|
+
inspector = _render_inspector_panel(
|
|
675
|
+
inspector_cfg,
|
|
676
|
+
installed_models=installed_models,
|
|
677
|
+
running_models=running_models,
|
|
678
|
+
event_lines=event_lines or [
|
|
679
|
+
f"mode {mode_label}",
|
|
680
|
+
f"context {context_line}",
|
|
681
|
+
f"tool max {max_tool_iterations}",
|
|
682
|
+
f"reflect every {tool_think_every} calls",
|
|
683
|
+
],
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
layout = Table.grid(expand=True)
|
|
687
|
+
if console.width < 110:
|
|
688
|
+
layout.add_column(ratio=1)
|
|
689
|
+
layout.add_row(left)
|
|
690
|
+
layout.add_row(inspector)
|
|
691
|
+
else:
|
|
692
|
+
layout.add_column(ratio=3)
|
|
693
|
+
layout.add_column(ratio=2)
|
|
694
|
+
layout.add_row(left, inspector)
|
|
695
|
+
|
|
696
|
+
console.print(header)
|
|
697
|
+
console.print(layout)
|
|
698
|
+
console.print(
|
|
699
|
+
Panel(
|
|
700
|
+
"[bold primary]Work[/]: /agent TASK /route TASK /changes /diff\n"
|
|
701
|
+
"[bold primary]Context[/]: /status /context /harness /hsearch /memories\n"
|
|
702
|
+
"[bold primary]Safety[/]: /doctor /safe /auto /policy\n"
|
|
703
|
+
"[bold primary]Setup[/]: /model /models /theme /reload /actions",
|
|
704
|
+
title="Quick Actions",
|
|
705
|
+
border_style="border",
|
|
706
|
+
box=box.ROUNDED,
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
console.print()
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _short_value(value: Any, limit: int = 80) -> str:
|
|
713
|
+
text = str(value)
|
|
714
|
+
text = " ".join(text.split())
|
|
715
|
+
return text if len(text) <= limit else text[: limit - 1] + "..."
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def redact_tool_args(name: str, args: dict[str, Any]) -> dict[str, Any]:
|
|
719
|
+
"""Remove secret-bearing values before display, JSON events, or audit previews."""
|
|
720
|
+
|
|
721
|
+
sensitive_keys = {"api_key", "access_token", "refresh_token", "password", "secret", "token"}
|
|
722
|
+
if name == "credential_helpers_store":
|
|
723
|
+
sensitive_keys.add("value")
|
|
724
|
+
return {
|
|
725
|
+
key: "<redacted>" if key.lower() in sensitive_keys else value
|
|
726
|
+
for key, value in args.items()
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def show_tool_call(name: str, args: dict, *, call_id: str | None = None) -> None:
|
|
731
|
+
visible_args = redact_tool_args(
|
|
732
|
+
name,
|
|
733
|
+
{key: value for key, value in args.items() if key not in {"cwd", "safe_mode"}},
|
|
734
|
+
)
|
|
735
|
+
if _json_sink is not None:
|
|
736
|
+
cid = call_id or _json_sink.next_call_id()
|
|
737
|
+
_json_sink.tool_call(call_id=cid, name=name, args=visible_args)
|
|
738
|
+
return
|
|
739
|
+
rendered = " ".join(f"[secondary]{key}[/]={_short_value(value)}" for key, value in visible_args.items())
|
|
740
|
+
suffix = f" {rendered}" if rendered else ""
|
|
741
|
+
# DOING glyph (bolt) marks tool dispatch — matches the buddy's strike animation.
|
|
742
|
+
console.print(f"[accent]{glyph(AIState.DOING).plain}[/] [bold]{name}[/]{suffix}", highlight=False)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def show_tool_result(
|
|
746
|
+
name: str,
|
|
747
|
+
result: str,
|
|
748
|
+
approved: bool = True,
|
|
749
|
+
duration_ms: float | None = None,
|
|
750
|
+
*,
|
|
751
|
+
call_id: str | None = None,
|
|
752
|
+
) -> None:
|
|
753
|
+
if _json_sink is not None:
|
|
754
|
+
if not approved and str(result).strip().lower().startswith("user denied"):
|
|
755
|
+
_json_sink.tool_denied(call_id=call_id, name=name, reason="approval-mode never; tool requires approval")
|
|
756
|
+
else:
|
|
757
|
+
_json_sink.tool_result(call_id=call_id, name=name, result=result, duration_ms=duration_ms)
|
|
758
|
+
return
|
|
759
|
+
status = "[success]OK[/]" if approved else "[error]ERR[/]"
|
|
760
|
+
lines = str(result).splitlines()
|
|
761
|
+
byte_count = len(str(result).encode("utf-8", errors="replace"))
|
|
762
|
+
duration = f" {duration_ms:.0f}ms" if duration_ms is not None else ""
|
|
763
|
+
console.print(f"{status} [bold]{name}[/]{duration} {_format_bytes(byte_count)} {len(lines)} lines")
|
|
764
|
+
preview = lines[:5] or [str(result)[:160]]
|
|
765
|
+
for line in preview:
|
|
766
|
+
console.print(f" [muted]{_short_value(line, 180)}[/]", highlight=False)
|
|
767
|
+
if len(lines) > 5:
|
|
768
|
+
console.print(f" [muted]... {len(lines) - 5} more lines[/]")
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def show_recalled_context(blocks: list[dict[str, Any]]) -> None:
|
|
772
|
+
if not blocks:
|
|
773
|
+
return
|
|
774
|
+
if _json_sink is not None:
|
|
775
|
+
# Internal RAG detail — bridge consumers don't need this in the event stream.
|
|
776
|
+
return
|
|
777
|
+
lines: list[str] = []
|
|
778
|
+
for block in blocks[:5]:
|
|
779
|
+
block_type = str(block.get("type", "note")).upper()
|
|
780
|
+
block_id = str(block.get("id", "?"))
|
|
781
|
+
score = float(block.get("score", 0.0) or 0.0)
|
|
782
|
+
content = _short_value(block.get("content", ""), 180)
|
|
783
|
+
lines.append(f"[bold secondary][{block_type}][/bold secondary] [muted]{block_id}[/] [info]({score:.2f})[/]")
|
|
784
|
+
if content:
|
|
785
|
+
lines.append(f" [text]{content}[/]")
|
|
786
|
+
if len(blocks) > 5:
|
|
787
|
+
lines.append(f"[muted]... {len(blocks) - 5} more recalled block(s)[/]")
|
|
788
|
+
console.print(
|
|
789
|
+
Panel(
|
|
790
|
+
"\n".join(lines),
|
|
791
|
+
title=f"Recalled - {len(blocks)} block{'s' if len(blocks) != 1 else ''}",
|
|
792
|
+
border_style="border",
|
|
793
|
+
box=box.ROUNDED,
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
_DEFAULT_AGENT_PREVIEW_CHARS = 900
|
|
799
|
+
_AGENT_SECTION_HEADER = re.compile(
|
|
800
|
+
r"^(?:#{1,3}\s*)?"
|
|
801
|
+
r"(Assumptions|Risks|Concrete\s+Next\s+Steps|Next\s+Steps)"
|
|
802
|
+
r"\s*:?\s*$",
|
|
803
|
+
re.IGNORECASE,
|
|
804
|
+
)
|
|
805
|
+
_AGENT_BLOCK_OUTPUT_HEADER = re.compile(r"^#+\s*block\s+output\s*$", re.IGNORECASE)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _agent_preview_char_limit() -> int | None:
|
|
809
|
+
"""None means show full output (ALGO_CLI_AGENT_PREVIEW=0 or 'full')."""
|
|
810
|
+
raw = os.environ.get("ALGO_CLI_AGENT_PREVIEW", "").strip().lower()
|
|
811
|
+
if raw in {"", "default"}:
|
|
812
|
+
return _DEFAULT_AGENT_PREVIEW_CHARS
|
|
813
|
+
if raw in {"0", "full", "none", "off"}:
|
|
814
|
+
return None
|
|
815
|
+
try:
|
|
816
|
+
value = int(raw)
|
|
817
|
+
return None if value <= 0 else value
|
|
818
|
+
except ValueError:
|
|
819
|
+
return _DEFAULT_AGENT_PREVIEW_CHARS
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _agent_block_border_style(status: str) -> str:
|
|
823
|
+
normalized = (status or "").strip().lower()
|
|
824
|
+
if normalized == "complete":
|
|
825
|
+
return "success"
|
|
826
|
+
if normalized == "partial":
|
|
827
|
+
return "warning"
|
|
828
|
+
if normalized in {"failed", "error", "cancelled"}:
|
|
829
|
+
return "error"
|
|
830
|
+
return "border"
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _strip_block_output_header(text: str) -> str:
|
|
834
|
+
lines = text.splitlines()
|
|
835
|
+
if lines and _AGENT_BLOCK_OUTPUT_HEADER.match(lines[0].strip()):
|
|
836
|
+
return "\n".join(lines[1:]).strip()
|
|
837
|
+
return text.strip()
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _lines_to_bullets(lines: list[str]) -> list[str]:
|
|
841
|
+
bullets: list[str] = []
|
|
842
|
+
for line in lines:
|
|
843
|
+
stripped = line.strip()
|
|
844
|
+
if not stripped:
|
|
845
|
+
continue
|
|
846
|
+
stripped = re.sub(r"^[-*•]\s+", "", stripped)
|
|
847
|
+
stripped = re.sub(r"^\d+[.)]\s+", "", stripped)
|
|
848
|
+
if stripped:
|
|
849
|
+
bullets.append(stripped)
|
|
850
|
+
return bullets
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _parse_block_output_sections(text: str) -> dict[str, list[str]]:
|
|
854
|
+
"""Parse Assumptions / Risks / Next Steps style sections into bullet lists."""
|
|
855
|
+
body = _strip_block_output_header(text)
|
|
856
|
+
if not body:
|
|
857
|
+
return {}
|
|
858
|
+
sections: dict[str, list[str]] = {}
|
|
859
|
+
current_label = ""
|
|
860
|
+
buffer: list[str] = []
|
|
861
|
+
|
|
862
|
+
def flush() -> None:
|
|
863
|
+
nonlocal buffer, current_label
|
|
864
|
+
if not current_label:
|
|
865
|
+
return
|
|
866
|
+
bullets = _lines_to_bullets(buffer)
|
|
867
|
+
if bullets:
|
|
868
|
+
key = current_label
|
|
869
|
+
if key in sections:
|
|
870
|
+
sections[key].extend(bullets)
|
|
871
|
+
else:
|
|
872
|
+
sections[key] = bullets
|
|
873
|
+
buffer = []
|
|
874
|
+
|
|
875
|
+
for line in body.splitlines():
|
|
876
|
+
header_match = _AGENT_SECTION_HEADER.match(line.strip())
|
|
877
|
+
if header_match:
|
|
878
|
+
flush()
|
|
879
|
+
label = header_match.group(1).lower()
|
|
880
|
+
if "concrete" in label or label == "next steps":
|
|
881
|
+
current_label = "Next steps"
|
|
882
|
+
elif label.startswith("risk"):
|
|
883
|
+
current_label = "Risks"
|
|
884
|
+
else:
|
|
885
|
+
current_label = "Assumptions"
|
|
886
|
+
continue
|
|
887
|
+
if current_label:
|
|
888
|
+
buffer.append(line)
|
|
889
|
+
else:
|
|
890
|
+
buffer.append(line)
|
|
891
|
+
|
|
892
|
+
flush()
|
|
893
|
+
if not sections and buffer:
|
|
894
|
+
lead = _lines_to_bullets(buffer)
|
|
895
|
+
if lead:
|
|
896
|
+
sections["Summary"] = lead[:12]
|
|
897
|
+
return sections
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _write_agent_block_dump(role: str, output: str) -> Path | None:
|
|
901
|
+
text = (output or "").strip()
|
|
902
|
+
if not text:
|
|
903
|
+
return None
|
|
904
|
+
safe_role = re.sub(r"[^a-zA-Z0-9_-]+", "-", (role or "block").strip()) or "block"
|
|
905
|
+
try:
|
|
906
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
907
|
+
path = CONFIG_DIR / f"last-block-{safe_role}.md"
|
|
908
|
+
text_with_eol = text + ("\n" if not text.endswith("\n") else "")
|
|
909
|
+
if _atomic_write_text is not None:
|
|
910
|
+
_atomic_write_text(path, text_with_eol)
|
|
911
|
+
else:
|
|
912
|
+
path.write_text(text_with_eol, encoding="utf-8")
|
|
913
|
+
return path
|
|
914
|
+
except OSError:
|
|
915
|
+
return None
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _agent_block_title(
|
|
919
|
+
role: str,
|
|
920
|
+
*,
|
|
921
|
+
duration_ms: float,
|
|
922
|
+
tool_calls: int,
|
|
923
|
+
status: str,
|
|
924
|
+
status_code: str = "",
|
|
925
|
+
model: str = "",
|
|
926
|
+
write_count: int = 0,
|
|
927
|
+
) -> str:
|
|
928
|
+
duration_s = duration_ms / 1000.0
|
|
929
|
+
calls_label = "tool call" if tool_calls == 1 else "tool calls"
|
|
930
|
+
parts = [f"Agent · {role}", f"{duration_s:.1f}s", f"{tool_calls} {calls_label}", status]
|
|
931
|
+
if status_code:
|
|
932
|
+
parts.insert(-1, status_code)
|
|
933
|
+
if model:
|
|
934
|
+
parts.append(model)
|
|
935
|
+
if write_count > 0:
|
|
936
|
+
parts.append(f"writes:{write_count}")
|
|
937
|
+
return " — ".join(parts)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _render_structured_sections(sections: dict[str, list[str]]) -> Group:
|
|
941
|
+
blocks: list[Any] = []
|
|
942
|
+
for title, bullets in sections.items():
|
|
943
|
+
blocks.append(Text(title, style="bold primary"))
|
|
944
|
+
for item in bullets[:16]:
|
|
945
|
+
blocks.append(Text(f" • {item}", style="text"))
|
|
946
|
+
if len(bullets) > 16:
|
|
947
|
+
blocks.append(Text(f" [muted]… {len(bullets) - 16} more[/]", style="muted"))
|
|
948
|
+
blocks.append(Text(""))
|
|
949
|
+
return Group(*blocks)
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _render_agent_block_body(
|
|
953
|
+
role: str,
|
|
954
|
+
output: str,
|
|
955
|
+
*,
|
|
956
|
+
preview_limit: int | None,
|
|
957
|
+
) -> tuple[Any, Path | None]:
|
|
958
|
+
text = (output or "").strip() or "(no output produced)"
|
|
959
|
+
dump_path = _write_agent_block_dump(role, text if text != "(no output produced)" else "")
|
|
960
|
+
|
|
961
|
+
sections = _parse_block_output_sections(text)
|
|
962
|
+
use_structured = role == "plan" or len(sections) >= 2
|
|
963
|
+
|
|
964
|
+
if use_structured and sections:
|
|
965
|
+
body: Any = _render_structured_sections(sections)
|
|
966
|
+
remainder = _strip_block_output_header(text)
|
|
967
|
+
if preview_limit is not None and len(remainder) > preview_limit:
|
|
968
|
+
body = Group(
|
|
969
|
+
body,
|
|
970
|
+
Text(
|
|
971
|
+
f"[muted]Full text ({len(remainder)} chars) — see file or raise ALGO_CLI_AGENT_PREVIEW[/]",
|
|
972
|
+
style="muted",
|
|
973
|
+
),
|
|
974
|
+
)
|
|
975
|
+
return body, dump_path
|
|
976
|
+
|
|
977
|
+
if preview_limit is None:
|
|
978
|
+
return Markdown(text), dump_path
|
|
979
|
+
if len(text) <= preview_limit:
|
|
980
|
+
return Markdown(text), dump_path
|
|
981
|
+
preview = _short_value(text, preview_limit)
|
|
982
|
+
return (
|
|
983
|
+
Group(
|
|
984
|
+
Markdown(preview),
|
|
985
|
+
Text(
|
|
986
|
+
f"[muted]Preview ({preview_limit} chars). Set ALGO_CLI_AGENT_PREVIEW=0 for full panel text.[/]",
|
|
987
|
+
style="muted",
|
|
988
|
+
),
|
|
989
|
+
),
|
|
990
|
+
dump_path,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def show_agent_block_start(
|
|
995
|
+
role: str,
|
|
996
|
+
model: str,
|
|
997
|
+
tool_count: int,
|
|
998
|
+
policy_summary: str = "",
|
|
999
|
+
*,
|
|
1000
|
+
policy_enforced: bool = False,
|
|
1001
|
+
cwd: str = "",
|
|
1002
|
+
) -> None:
|
|
1003
|
+
if _json_sink is not None:
|
|
1004
|
+
return
|
|
1005
|
+
label = "Enforced policy" if policy_enforced else "Advisory policy"
|
|
1006
|
+
policy_line = f"\n[text]{label}:[/] {policy_summary}" if policy_summary else ""
|
|
1007
|
+
cwd_line = f"\n[text]Workspace:[/] [muted]{compact_path(cwd, 72)}[/]" if cwd else ""
|
|
1008
|
+
console.print(
|
|
1009
|
+
Panel(
|
|
1010
|
+
f"[text]Model:[/] [primary]{model}[/]\n[text]Runtime tools:[/] {tool_count}{cwd_line}{policy_line}",
|
|
1011
|
+
title=f"Agent · {role}",
|
|
1012
|
+
border_style="border_accent",
|
|
1013
|
+
box=box.ROUNDED,
|
|
1014
|
+
)
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def show_agent_block_complete(
|
|
1019
|
+
role: str,
|
|
1020
|
+
output: str,
|
|
1021
|
+
*,
|
|
1022
|
+
duration_ms: float,
|
|
1023
|
+
tool_calls: int,
|
|
1024
|
+
status: str,
|
|
1025
|
+
status_reason: str = "",
|
|
1026
|
+
verification_warning: str = "",
|
|
1027
|
+
status_code: str = "",
|
|
1028
|
+
model: str = "",
|
|
1029
|
+
policy_summary: str = "",
|
|
1030
|
+
successful_writes: list[str] | None = None,
|
|
1031
|
+
) -> None:
|
|
1032
|
+
if _json_sink is not None:
|
|
1033
|
+
return
|
|
1034
|
+
preview_limit = _agent_preview_char_limit()
|
|
1035
|
+
body_renderable, dump_path = _render_agent_block_body(
|
|
1036
|
+
role, output, preview_limit=preview_limit
|
|
1037
|
+
)
|
|
1038
|
+
writes = successful_writes or []
|
|
1039
|
+
write_count = len(writes)
|
|
1040
|
+
|
|
1041
|
+
header_lines: list[str] = []
|
|
1042
|
+
status_style = _agent_block_border_style(status)
|
|
1043
|
+
header_lines.append(f"[{status_style}]Status: {status.upper()}[/]")
|
|
1044
|
+
if status_code:
|
|
1045
|
+
header_lines.append(f"[muted]Code:[/] {status_code}")
|
|
1046
|
+
if status_reason:
|
|
1047
|
+
header_lines.append(f"[text]{_short_value(status_reason, 220)}[/]")
|
|
1048
|
+
if verification_warning and not status_reason:
|
|
1049
|
+
header_lines.append(
|
|
1050
|
+
f"[warning]Verification warning:[/] {_short_value(verification_warning, 200)}"
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
footer_parts = [
|
|
1054
|
+
f"[muted]{tool_calls} tool call{'s' if tool_calls != 1 else ''}[/]",
|
|
1055
|
+
f"[muted]{duration_ms / 1000:.1f}s[/]",
|
|
1056
|
+
]
|
|
1057
|
+
if policy_summary:
|
|
1058
|
+
footer_parts.append(f"[muted]{_short_value(policy_summary, 120)}[/]")
|
|
1059
|
+
if dump_path is not None:
|
|
1060
|
+
footer_parts.append(f"[info]Full output:[/] [accent]{dump_path}[/]")
|
|
1061
|
+
if write_count:
|
|
1062
|
+
footer_parts.append(f"[success]writes: {write_count}[/]")
|
|
1063
|
+
|
|
1064
|
+
header_renderable = (
|
|
1065
|
+
Text.from_markup("\n".join(header_lines)) if header_lines else Text("")
|
|
1066
|
+
)
|
|
1067
|
+
footer_renderable = Text.from_markup(" · ".join(footer_parts)) if footer_parts else Text("")
|
|
1068
|
+
panel_body = Group(
|
|
1069
|
+
header_renderable,
|
|
1070
|
+
Text(""),
|
|
1071
|
+
body_renderable,
|
|
1072
|
+
Text(""),
|
|
1073
|
+
footer_renderable,
|
|
1074
|
+
)
|
|
1075
|
+
if verification_warning and status_reason:
|
|
1076
|
+
panel_body = Group(
|
|
1077
|
+
panel_body,
|
|
1078
|
+
Text(""),
|
|
1079
|
+
Text.from_markup(
|
|
1080
|
+
f"[warning]Verification:[/] {_short_value(verification_warning, 200)}"
|
|
1081
|
+
),
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
console.print(
|
|
1085
|
+
Panel(
|
|
1086
|
+
panel_body,
|
|
1087
|
+
title=_agent_block_title(
|
|
1088
|
+
role,
|
|
1089
|
+
duration_ms=duration_ms,
|
|
1090
|
+
tool_calls=tool_calls,
|
|
1091
|
+
status=status,
|
|
1092
|
+
status_code=status_code,
|
|
1093
|
+
model=model,
|
|
1094
|
+
write_count=write_count,
|
|
1095
|
+
),
|
|
1096
|
+
border_style=status_style,
|
|
1097
|
+
box=box.ROUNDED,
|
|
1098
|
+
)
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def show_agent_recovery_start(role: str, reason: str, retry_iterations: int) -> None:
|
|
1103
|
+
console.print(
|
|
1104
|
+
Panel(
|
|
1105
|
+
Markdown(
|
|
1106
|
+
f"**Reason:** {reason}\n\n"
|
|
1107
|
+
f"**Recovery:** one tool-free replan, then one focused retry "
|
|
1108
|
+
f"with at most {retry_iterations} iterations."
|
|
1109
|
+
),
|
|
1110
|
+
title=f"Recovery - {role} retry",
|
|
1111
|
+
border_style="warning",
|
|
1112
|
+
box=box.ROUNDED,
|
|
1113
|
+
)
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def show_agent_pipeline_complete(output: str, *, block_count: int, duration_ms: float) -> None:
|
|
1118
|
+
console.print(
|
|
1119
|
+
Panel(
|
|
1120
|
+
Markdown(output.strip() or "(no final output produced)"),
|
|
1121
|
+
title=f"Pipeline complete - {block_count} blocks - {duration_ms / 1000:.1f}s",
|
|
1122
|
+
border_style="success",
|
|
1123
|
+
box=box.ROUNDED,
|
|
1124
|
+
)
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _estimated_tokens(text: str) -> int:
|
|
1129
|
+
return max(1, len(text) // 4) if text else 0
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _thinking_renderable(text: str, *, final: bool = False) -> Panel:
|
|
1133
|
+
visible = text[:_THINKING_VISIBLE_CHARS]
|
|
1134
|
+
token_count = _estimated_tokens(text)
|
|
1135
|
+
truncated = len(text) > _THINKING_VISIBLE_CHARS
|
|
1136
|
+
if truncated:
|
|
1137
|
+
visible = visible.rstrip() + f"\n\n[muted]... truncated, ~{token_count} tokens total[/]"
|
|
1138
|
+
elapsed = time.monotonic() - _thinking_started_at if _thinking_started_at else 0.0
|
|
1139
|
+
# Non-final title carries a time-derived frame: each Live update (token
|
|
1140
|
+
# batches arrive faster than the 90ms frame interval) advances the pulse,
|
|
1141
|
+
# so the panel animates without any extra timer.
|
|
1142
|
+
title = (
|
|
1143
|
+
f"Thinking - {elapsed:.1f}s - ~{token_count} tokens"
|
|
1144
|
+
if final
|
|
1145
|
+
else f"{current_frame(AIState.THINKING)} Thinking..."
|
|
1146
|
+
)
|
|
1147
|
+
return Panel(
|
|
1148
|
+
Text(visible or "Thinking...", style="secondary italic"),
|
|
1149
|
+
title=title,
|
|
1150
|
+
border_style="border",
|
|
1151
|
+
box=box.ROUNDED,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def start_thinking_block() -> None:
|
|
1156
|
+
global _thinking_live, _thinking_buffer, _thinking_pending, _thinking_last_render_time, _thinking_started_at
|
|
1157
|
+
if _json_sink is not None:
|
|
1158
|
+
return
|
|
1159
|
+
if _thinking_live is not None:
|
|
1160
|
+
return
|
|
1161
|
+
finish_streaming_response()
|
|
1162
|
+
_thinking_buffer = ""
|
|
1163
|
+
_thinking_pending = ""
|
|
1164
|
+
_thinking_last_render_time = 0.0
|
|
1165
|
+
_thinking_started_at = time.monotonic()
|
|
1166
|
+
_thinking_live = Live(_thinking_renderable(""), console=console, auto_refresh=False, transient=False)
|
|
1167
|
+
_thinking_live.start()
|
|
1168
|
+
_thinking_live.refresh()
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def show_thinking_text(text: str) -> None:
|
|
1172
|
+
global _thinking_buffer, _thinking_pending, _thinking_last_render_time
|
|
1173
|
+
if not text:
|
|
1174
|
+
return
|
|
1175
|
+
if _json_sink is not None:
|
|
1176
|
+
_json_sink.thinking(text)
|
|
1177
|
+
return
|
|
1178
|
+
if _thinking_live is None:
|
|
1179
|
+
start_thinking_block()
|
|
1180
|
+
_thinking_pending += text
|
|
1181
|
+
now = time.monotonic()
|
|
1182
|
+
if _thinking_live is not None and now - _thinking_last_render_time >= _RENDER_INTERVAL:
|
|
1183
|
+
_thinking_buffer += _thinking_pending
|
|
1184
|
+
_thinking_pending = ""
|
|
1185
|
+
_thinking_last_render_time = now
|
|
1186
|
+
_thinking_live.update(_thinking_renderable(_thinking_buffer), refresh=True)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def finish_thinking_block() -> None:
|
|
1190
|
+
global _thinking_live, _thinking_buffer, _thinking_pending, _thinking_started_at
|
|
1191
|
+
if _json_sink is not None:
|
|
1192
|
+
return
|
|
1193
|
+
if _thinking_live is not None:
|
|
1194
|
+
if _thinking_pending:
|
|
1195
|
+
_thinking_buffer += _thinking_pending
|
|
1196
|
+
_thinking_pending = ""
|
|
1197
|
+
_thinking_live.update(_thinking_renderable(_thinking_buffer, final=True), refresh=True)
|
|
1198
|
+
_thinking_live.stop()
|
|
1199
|
+
_thinking_live = None
|
|
1200
|
+
_thinking_buffer = ""
|
|
1201
|
+
_thinking_started_at = 0.0
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def show_response(text: str) -> None:
|
|
1205
|
+
if _json_sink is not None:
|
|
1206
|
+
_json_sink.content(text)
|
|
1207
|
+
return
|
|
1208
|
+
if text.strip():
|
|
1209
|
+
console.print(Markdown(text))
|
|
1210
|
+
console.print()
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def start_streaming_response() -> None:
|
|
1214
|
+
global _stream_live, _stream_buffer, _stream_pending, _last_render_time
|
|
1215
|
+
if _json_sink is not None:
|
|
1216
|
+
return
|
|
1217
|
+
finish_streaming_response()
|
|
1218
|
+
# Answering transition: a left-anchored eye+chevron rule separates the
|
|
1219
|
+
# response from tool chatter, so the state flip reads instantly.
|
|
1220
|
+
rule_title = Text.assemble(
|
|
1221
|
+
glyph(AIState.IDLE), (" ", ""), glyph(AIState.ANSWERING), (" answering", "primary")
|
|
1222
|
+
)
|
|
1223
|
+
console.rule(rule_title, style="border", align="left")
|
|
1224
|
+
_stream_buffer = ""
|
|
1225
|
+
_stream_pending = ""
|
|
1226
|
+
_last_render_time = 0.0
|
|
1227
|
+
_stream_live = Live(Markdown(""), console=console, auto_refresh=False, transient=False)
|
|
1228
|
+
_stream_live.start()
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def show_stream_text(text: str) -> None:
|
|
1232
|
+
"""Accumulate tokens and flush to the Live display at most 12 times per second.
|
|
1233
|
+
|
|
1234
|
+
Calling Markdown() on every token is O(n²) — the whole buffer is re-parsed
|
|
1235
|
+
each time. This coalescing buffer ensures Markdown() is called at most
|
|
1236
|
+
refresh_per_second times regardless of how fast tokens arrive.
|
|
1237
|
+
"""
|
|
1238
|
+
global _stream_buffer, _stream_pending, _last_render_time
|
|
1239
|
+
if _json_sink is not None:
|
|
1240
|
+
_json_sink.content(text)
|
|
1241
|
+
return
|
|
1242
|
+
if _stream_live is None:
|
|
1243
|
+
console.print(text, end="", highlight=False)
|
|
1244
|
+
return
|
|
1245
|
+
_stream_pending += text
|
|
1246
|
+
now = time.monotonic()
|
|
1247
|
+
if now - _last_render_time >= _RENDER_INTERVAL:
|
|
1248
|
+
_stream_buffer += _stream_pending
|
|
1249
|
+
_stream_pending = ""
|
|
1250
|
+
_last_render_time = now
|
|
1251
|
+
_stream_live.update(Markdown(_stream_buffer), refresh=True)
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def finish_streaming_response() -> None:
|
|
1255
|
+
global _stream_live, _stream_buffer, _stream_pending
|
|
1256
|
+
if _json_sink is not None:
|
|
1257
|
+
return
|
|
1258
|
+
if _stream_live is not None:
|
|
1259
|
+
if _stream_pending:
|
|
1260
|
+
_stream_buffer += _stream_pending
|
|
1261
|
+
_stream_pending = ""
|
|
1262
|
+
_stream_live.update(Markdown(_stream_buffer or ""), refresh=True)
|
|
1263
|
+
_stream_live.stop()
|
|
1264
|
+
_stream_live = None
|
|
1265
|
+
_stream_buffer = ""
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def show_thinking_token(text: str) -> None:
|
|
1269
|
+
show_thinking_text(text)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def show_error(msg: str) -> None:
|
|
1273
|
+
if _json_sink is not None and not _console_capture_active.get():
|
|
1274
|
+
_json_sink.error(error_class="internal", message=msg)
|
|
1275
|
+
return
|
|
1276
|
+
marker = Text.assemble(glyph(AIState.ERROR), (" Error: ", "bold error"))
|
|
1277
|
+
console.print(marker.append(msg, style="text"))
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def show_info(msg: str) -> None:
|
|
1281
|
+
if _json_sink is not None and not _console_capture_active.get():
|
|
1282
|
+
# info chatter is dropped in JSON mode; bridge consumers only want events.
|
|
1283
|
+
return
|
|
1284
|
+
console.print(f"[info]{msg}[/]")
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def show_status_footer(model: str, used_tokens: int, total_tokens: int, summary_active: bool = False) -> None:
|
|
1288
|
+
"""Fallback status line when prompt_toolkit toolbar is unavailable."""
|
|
1289
|
+
if json_mode_active():
|
|
1290
|
+
return
|
|
1291
|
+
# The companion eye fronts the footer; frame is wall-clock derived so
|
|
1292
|
+
# repeated footer redraws make it breathe/blink.
|
|
1293
|
+
idle_glyph = buddy_frame(AIState.IDLE).plain
|
|
1294
|
+
if total_tokens <= 0:
|
|
1295
|
+
console.print(f"[info]{idle_glyph} {model} · ctx unknown[/]")
|
|
1296
|
+
return
|
|
1297
|
+
used = min(max(used_tokens, 0), total_tokens)
|
|
1298
|
+
remaining = max(total_tokens - used, 0)
|
|
1299
|
+
pct_left = int(round((remaining / total_tokens) * 100))
|
|
1300
|
+
ctx_color = "info" if pct_left >= 50 else ("warning" if pct_left >= 20 else "error")
|
|
1301
|
+
summary_flag = " · summary" if summary_active else ""
|
|
1302
|
+
console.print(
|
|
1303
|
+
f"[info]{idle_glyph}[/] [bold]{model}[/] · "
|
|
1304
|
+
f"[{ctx_color}]▣ {used}/{total_tokens} ({pct_left}% left)[/]{summary_flag}"
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def show_memory(facts: list[str]) -> None:
|
|
1309
|
+
if not facts:
|
|
1310
|
+
console.print("[info]No memories stored.[/]")
|
|
1311
|
+
return
|
|
1312
|
+
table = Table(title="Memories", show_header=True, header_style="primary", box=box.ROUNDED)
|
|
1313
|
+
table.add_column("#", justify="right")
|
|
1314
|
+
table.add_column("Fact")
|
|
1315
|
+
for index, fact in enumerate(facts, 1):
|
|
1316
|
+
table.add_row(str(index), fact)
|
|
1317
|
+
console.print(table)
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def show_help() -> None:
|
|
1321
|
+
table = Table.grid(padding=(0, 2), expand=True)
|
|
1322
|
+
table.add_column(style="muted", no_wrap=True)
|
|
1323
|
+
table.add_column(style="primary", no_wrap=True)
|
|
1324
|
+
table.add_column(style="text")
|
|
1325
|
+
|
|
1326
|
+
from . import slash_dispatch
|
|
1327
|
+
|
|
1328
|
+
def _group(command: str) -> str:
|
|
1329
|
+
if command in {
|
|
1330
|
+
"/help",
|
|
1331
|
+
"/dashboard",
|
|
1332
|
+
"/status",
|
|
1333
|
+
"/actions",
|
|
1334
|
+
"/reload",
|
|
1335
|
+
"/theme",
|
|
1336
|
+
"/info",
|
|
1337
|
+
"/safe",
|
|
1338
|
+
"/auto",
|
|
1339
|
+
"/policy",
|
|
1340
|
+
"/thinking",
|
|
1341
|
+
"/verify",
|
|
1342
|
+
"/perf",
|
|
1343
|
+
"/metrics",
|
|
1344
|
+
"/doctor",
|
|
1345
|
+
"/selfcheck",
|
|
1346
|
+
"/exit",
|
|
1347
|
+
"/quit",
|
|
1348
|
+
}:
|
|
1349
|
+
return "Session"
|
|
1350
|
+
if command in {
|
|
1351
|
+
"/model",
|
|
1352
|
+
"/models",
|
|
1353
|
+
"/host",
|
|
1354
|
+
"/cloud",
|
|
1355
|
+
"/cloudauto",
|
|
1356
|
+
"/login",
|
|
1357
|
+
"/keepalive",
|
|
1358
|
+
"/ctx",
|
|
1359
|
+
"/temp",
|
|
1360
|
+
"/toolmax",
|
|
1361
|
+
"/thinkevery",
|
|
1362
|
+
"/system",
|
|
1363
|
+
}:
|
|
1364
|
+
return "Model"
|
|
1365
|
+
if command.startswith("/google"):
|
|
1366
|
+
return "Google"
|
|
1367
|
+
if command.startswith("/chatgpt"):
|
|
1368
|
+
return "ChatGPT"
|
|
1369
|
+
if command.startswith("/xai") or command.startswith("/x-account"):
|
|
1370
|
+
return "xAI"
|
|
1371
|
+
if (
|
|
1372
|
+
command.startswith("/harness")
|
|
1373
|
+
or command.startswith("/hsearch")
|
|
1374
|
+
or command.startswith("/hread")
|
|
1375
|
+
or command in {"/hs", "/hr"}
|
|
1376
|
+
):
|
|
1377
|
+
return "Harness"
|
|
1378
|
+
if command in {
|
|
1379
|
+
"/reason",
|
|
1380
|
+
"/reflex",
|
|
1381
|
+
"/goal",
|
|
1382
|
+
"/agent",
|
|
1383
|
+
"/route",
|
|
1384
|
+
"/icl",
|
|
1385
|
+
"/model-check",
|
|
1386
|
+
"/context",
|
|
1387
|
+
}:
|
|
1388
|
+
return "Agent"
|
|
1389
|
+
if command.startswith("/kernel"):
|
|
1390
|
+
return "Knowledge"
|
|
1391
|
+
if command in {
|
|
1392
|
+
"/identity",
|
|
1393
|
+
"/lesson",
|
|
1394
|
+
"/lessons",
|
|
1395
|
+
"/skills",
|
|
1396
|
+
"/remember",
|
|
1397
|
+
"/memories",
|
|
1398
|
+
"/forget",
|
|
1399
|
+
"/intuition",
|
|
1400
|
+
"/intelligence",
|
|
1401
|
+
"/intel",
|
|
1402
|
+
"/intelagence",
|
|
1403
|
+
}:
|
|
1404
|
+
return "Knowledge"
|
|
1405
|
+
if command in {"/cd", "/ls", "/read", "/save", "/load", "/diff", "/changes", "/clear", "/mode"}:
|
|
1406
|
+
return "Workspace"
|
|
1407
|
+
if command in {"/embed", "/vision", "/pdf"}:
|
|
1408
|
+
return "Media"
|
|
1409
|
+
if command in {"/plugins", "/credentials", "/url-scheme"}:
|
|
1410
|
+
return "Integrations"
|
|
1411
|
+
return "Other"
|
|
1412
|
+
|
|
1413
|
+
groups: dict[str, list[tuple[str, str]]] = {
|
|
1414
|
+
"Session": [],
|
|
1415
|
+
"Model": [],
|
|
1416
|
+
"Workspace": [],
|
|
1417
|
+
"Knowledge": [],
|
|
1418
|
+
"Agent": [],
|
|
1419
|
+
"xAI": [],
|
|
1420
|
+
"Google": [],
|
|
1421
|
+
"ChatGPT": [],
|
|
1422
|
+
"Harness": [],
|
|
1423
|
+
"Media": [],
|
|
1424
|
+
"Integrations": [],
|
|
1425
|
+
"Other": [],
|
|
1426
|
+
}
|
|
1427
|
+
for command, description in slash_dispatch.SLASH_COMMANDS:
|
|
1428
|
+
groups[_group(command)].append((command, description))
|
|
1429
|
+
|
|
1430
|
+
for group in [
|
|
1431
|
+
"Session",
|
|
1432
|
+
"Model",
|
|
1433
|
+
"Workspace",
|
|
1434
|
+
"Knowledge",
|
|
1435
|
+
"Agent",
|
|
1436
|
+
"xAI",
|
|
1437
|
+
"Google",
|
|
1438
|
+
"ChatGPT",
|
|
1439
|
+
"Harness",
|
|
1440
|
+
"Media",
|
|
1441
|
+
"Integrations",
|
|
1442
|
+
"Other",
|
|
1443
|
+
]:
|
|
1444
|
+
rows = groups[group]
|
|
1445
|
+
if not rows:
|
|
1446
|
+
continue
|
|
1447
|
+
for index, (command, description) in enumerate(rows):
|
|
1448
|
+
table.add_row(group if index == 0 else "", command, description)
|
|
1449
|
+
table.add_row("", "", "")
|
|
1450
|
+
intro = Text.assemble(
|
|
1451
|
+
("Ask naturally for ordinary work. ", "text"),
|
|
1452
|
+
("Slash commands control the runtime and session.\n", "muted"),
|
|
1453
|
+
("Start here ", "bold secondary"),
|
|
1454
|
+
("/status", "primary"),
|
|
1455
|
+
(" · ", "muted"),
|
|
1456
|
+
("/agent TASK", "primary"),
|
|
1457
|
+
(" · ", "muted"),
|
|
1458
|
+
("/hsearch QUERY", "primary"),
|
|
1459
|
+
(" · ", "muted"),
|
|
1460
|
+
("/doctor", "primary"),
|
|
1461
|
+
)
|
|
1462
|
+
console.print(
|
|
1463
|
+
Panel(
|
|
1464
|
+
Group(intro, Text(""), table),
|
|
1465
|
+
title="Command Reference",
|
|
1466
|
+
subtitle="[muted]type / to search inline[/]",
|
|
1467
|
+
border_style="border_accent",
|
|
1468
|
+
box=box.ROUNDED,
|
|
1469
|
+
)
|
|
1470
|
+
)
|