elidia-agent-cli 0.1.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.
- elidia/__init__.py +0 -0
- elidia/__main__.py +4 -0
- elidia/agent/__init__.py +3 -0
- elidia/agent/loop.py +449 -0
- elidia/agent/personas.py +130 -0
- elidia/agent/portal.py +115 -0
- elidia/api/__init__.py +0 -0
- elidia/api/client.py +221 -0
- elidia/api/streaming.py +55 -0
- elidia/auth/__init__.py +0 -0
- elidia/auth/keychain.py +86 -0
- elidia/cache/__init__.py +1 -0
- elidia/cache/lru.py +158 -0
- elidia/cli/__init__.py +0 -0
- elidia/cli/commands.py +122 -0
- elidia/cli/main.py +293 -0
- elidia/cli/pager.py +104 -0
- elidia/cli/progress.py +173 -0
- elidia/cli/renderer.py +32 -0
- elidia/cli/repl.py +1234 -0
- elidia/cli/themes.py +214 -0
- elidia/config/__init__.py +0 -0
- elidia/config/defaults.py +5 -0
- elidia/config/rules.py +48 -0
- elidia/config/settings.py +217 -0
- elidia/creative/__init__.py +1 -0
- elidia/creative/audio.py +213 -0
- elidia/creative/display.py +260 -0
- elidia/creative/image.py +142 -0
- elidia/creative/local.py +163 -0
- elidia/creative/video.py +181 -0
- elidia/daemon/__init__.py +0 -0
- elidia/daemon/manager.py +349 -0
- elidia/db/__init__.py +0 -0
- elidia/db/database.py +87 -0
- elidia/mcp/__init__.py +0 -0
- elidia/mcp/client.py +198 -0
- elidia/mcp/config.py +93 -0
- elidia/mcp/registry.py +101 -0
- elidia/mcp/types.py +43 -0
- elidia/memory/__init__.py +3 -0
- elidia/memory/auto.py +164 -0
- elidia/memory/compaction.py +101 -0
- elidia/memory/embeddings.py +47 -0
- elidia/memory/outcomes.py +131 -0
- elidia/memory/patterns.py +79 -0
- elidia/memory/store.py +338 -0
- elidia/models/__init__.py +0 -0
- elidia/models/adaptive.py +53 -0
- elidia/models/router.py +115 -0
- elidia/modes/__init__.py +1 -0
- elidia/modes/autonomous.py +318 -0
- elidia/modes/budget.py +182 -0
- elidia/modes/classifier.py +169 -0
- elidia/modes/consensus.py +210 -0
- elidia/modes/deep_think.py +190 -0
- elidia/modes/swarm.py +229 -0
- elidia/modes/thinking.py +153 -0
- elidia/permissions/__init__.py +0 -0
- elidia/permissions/audit.py +138 -0
- elidia/permissions/manager.py +199 -0
- elidia/permissions/trust.py +63 -0
- elidia/rag/__init__.py +4 -0
- elidia/rag/chunker.py +216 -0
- elidia/rag/engine.py +345 -0
- elidia/rag/ingest.py +154 -0
- elidia/rag/portal_bridge.py +84 -0
- elidia/rag/watcher.py +98 -0
- elidia/research/__init__.py +1 -0
- elidia/research/export.py +237 -0
- elidia/research/orchestrator.py +310 -0
- elidia/research/sources.py +223 -0
- elidia/session/__init__.py +0 -0
- elidia/session/history.py +119 -0
- elidia/session/manager.py +82 -0
- elidia/tools/__init__.py +28 -0
- elidia/tools/base.py +70 -0
- elidia/tools/fetch.py +100 -0
- elidia/tools/filesystem.py +204 -0
- elidia/tools/git.py +156 -0
- elidia/tools/search.py +99 -0
- elidia/tools/terminal.py +77 -0
- elidia/widgets/__init__.py +1 -0
- elidia/widgets/protocol.py +248 -0
- elidia/widgets/renderer.py +225 -0
- elidia/workflow/__init__.py +1 -0
- elidia/workflow/engine.py +377 -0
- elidia_agent_cli-0.1.0.dist-info/METADATA +344 -0
- elidia_agent_cli-0.1.0.dist-info/RECORD +93 -0
- elidia_agent_cli-0.1.0.dist-info/WHEEL +5 -0
- elidia_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- elidia_agent_cli-0.1.0.dist-info/licenses/LICENSE +57 -0
- elidia_agent_cli-0.1.0.dist-info/top_level.txt +1 -0
elidia/cli/repl.py
ADDED
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit import PromptSession
|
|
7
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
8
|
+
from prompt_toolkit.history import FileHistory
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from elidia.agent.loop import AgentLoop
|
|
16
|
+
from elidia.agent.personas import PersonaEngine
|
|
17
|
+
from elidia.api.client import AiUtilsClient, ChatMessage
|
|
18
|
+
from elidia.auth.keychain import get_api_key, mask_api_key
|
|
19
|
+
from elidia.cache.lru import ResponseCache
|
|
20
|
+
from elidia.cli.commands import CommandRegistry, build_default_commands
|
|
21
|
+
from elidia.cli.pager import AutoPager
|
|
22
|
+
from elidia.cli.themes import ThemeManager
|
|
23
|
+
from elidia.config.defaults import VERSION
|
|
24
|
+
from elidia.config.rules import load_project_rules
|
|
25
|
+
from elidia.config.settings import ELIDIA_HOME, ElidiaConfig, ensure_elidia_home, load_config
|
|
26
|
+
from elidia.creative.audio import generate_music, generate_speech, list_audio_models
|
|
27
|
+
from elidia.creative.display import display_image
|
|
28
|
+
from elidia.creative.image import generate_image, list_image_models
|
|
29
|
+
from elidia.creative.video import generate_video, list_video_models
|
|
30
|
+
from elidia.daemon.manager import DaemonManager
|
|
31
|
+
from elidia.db.database import Database
|
|
32
|
+
from elidia.mcp.registry import MCPRegistry
|
|
33
|
+
from elidia.memory.auto import AutoMemory
|
|
34
|
+
from elidia.memory.store import MemoryStore, MemoryTier
|
|
35
|
+
from elidia.models.router import ModelRouter
|
|
36
|
+
from elidia.modes.budget import BudgetGovernor
|
|
37
|
+
from elidia.modes.thinking import ThinkingLevel, describe_level, parse_thinking_level
|
|
38
|
+
from elidia.permissions.audit import AuditLogger
|
|
39
|
+
from elidia.permissions.manager import PermissionManager
|
|
40
|
+
from elidia.permissions.trust import TrustEngine
|
|
41
|
+
from elidia.research.export import export_html, export_markdown
|
|
42
|
+
from elidia.research.orchestrator import ResearchOrchestrator
|
|
43
|
+
from elidia.research.sources import ResearchSources
|
|
44
|
+
from elidia.session.history import HistorySearch
|
|
45
|
+
from elidia.session.manager import SessionManager
|
|
46
|
+
from elidia.tools import ToolRegistry, create_default_registry
|
|
47
|
+
from elidia.widgets.renderer import CliWidgetRenderer
|
|
48
|
+
from elidia.workflow.engine import WorkflowExecutor, parse_workflow
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ElidiaRepl:
|
|
54
|
+
def __init__(self, forced_model: str | None = None, mode: str = "chat"):
|
|
55
|
+
logger.debug(f"Entered into ElidiaRepl.__init__: mode={mode}")
|
|
56
|
+
self._config: ElidiaConfig | None = None
|
|
57
|
+
self._client: AiUtilsClient | None = None
|
|
58
|
+
self._db: Database | None = None
|
|
59
|
+
self._session_mgr: SessionManager | None = None
|
|
60
|
+
self._router: ModelRouter | None = None
|
|
61
|
+
self._session_id: str | None = None
|
|
62
|
+
self._mode = mode
|
|
63
|
+
self._forced_model = forced_model
|
|
64
|
+
self._console = Console()
|
|
65
|
+
self._messages: list[ChatMessage] = []
|
|
66
|
+
self._total_cost_dt: float = 0.0
|
|
67
|
+
self._total_tokens_in: int = 0
|
|
68
|
+
self._total_tokens_out: int = 0
|
|
69
|
+
|
|
70
|
+
self._tool_registry: ToolRegistry | None = None
|
|
71
|
+
self._mcp_registry: MCPRegistry | None = None
|
|
72
|
+
self._audit: AuditLogger | None = None
|
|
73
|
+
self._permission_mgr: PermissionManager | None = None
|
|
74
|
+
self._trust_engine: TrustEngine | None = None
|
|
75
|
+
self._persona_engine: PersonaEngine | None = None
|
|
76
|
+
self._agent_loop: AgentLoop | None = None
|
|
77
|
+
self._command_registry: CommandRegistry | None = None
|
|
78
|
+
self._memory_store: MemoryStore | None = None
|
|
79
|
+
self._auto_memory: AutoMemory | None = None
|
|
80
|
+
self._history_search: HistorySearch | None = None
|
|
81
|
+
self._project_rules: str | None = None
|
|
82
|
+
|
|
83
|
+
self._budget: BudgetGovernor | None = None
|
|
84
|
+
self._thinking_level: ThinkingLevel = ThinkingLevel.MEDIUM
|
|
85
|
+
self._daemon: DaemonManager | None = None
|
|
86
|
+
self._widget_renderer: CliWidgetRenderer | None = None
|
|
87
|
+
self._research_sources: ResearchSources | None = None
|
|
88
|
+
|
|
89
|
+
self._theme_manager: ThemeManager | None = None
|
|
90
|
+
self._pager: AutoPager | None = None
|
|
91
|
+
self._cache: ResponseCache | None = None
|
|
92
|
+
|
|
93
|
+
async def initialize(self) -> None:
|
|
94
|
+
logger.debug("Entered into initialize")
|
|
95
|
+
ensure_elidia_home()
|
|
96
|
+
self._config = load_config()
|
|
97
|
+
|
|
98
|
+
api_key = get_api_key()
|
|
99
|
+
if not api_key and self._config.api.key:
|
|
100
|
+
api_key = self._config.api.key
|
|
101
|
+
|
|
102
|
+
if not api_key:
|
|
103
|
+
self._console.print("[red]No API key found.[/red] Run: [bold]elidia auth login[/bold]")
|
|
104
|
+
raise SystemExit(1)
|
|
105
|
+
|
|
106
|
+
self._client = AiUtilsClient(
|
|
107
|
+
api_key=api_key,
|
|
108
|
+
base_url=self._config.api.base_url,
|
|
109
|
+
timeout=self._config.api.timeout_seconds,
|
|
110
|
+
max_retries=self._config.api.max_retries,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
model_overrides: dict[str, str] = {}
|
|
114
|
+
mc = self._config.models
|
|
115
|
+
for attr in ("code", "reasoning", "creative", "vision", "cheap"):
|
|
116
|
+
val = getattr(mc, attr, None)
|
|
117
|
+
if val and val != "auto":
|
|
118
|
+
model_overrides[attr] = val
|
|
119
|
+
|
|
120
|
+
self._router = ModelRouter(config_models=model_overrides)
|
|
121
|
+
if self._forced_model:
|
|
122
|
+
self._router.force_model(self._forced_model)
|
|
123
|
+
|
|
124
|
+
self._db = Database()
|
|
125
|
+
self._db.connect()
|
|
126
|
+
self._session_mgr = SessionManager(self._db)
|
|
127
|
+
|
|
128
|
+
last_sid = self._session_mgr.get_last_session_id()
|
|
129
|
+
if last_sid:
|
|
130
|
+
self._session_id = last_sid
|
|
131
|
+
history = self._session_mgr.get_messages(last_sid, limit=50)
|
|
132
|
+
for msg in history:
|
|
133
|
+
self._messages.append(ChatMessage(role=msg["role"], content=msg["content"]))
|
|
134
|
+
else:
|
|
135
|
+
self._session_id = self._session_mgr.create_session(mode=self._mode)
|
|
136
|
+
|
|
137
|
+
self._tool_registry = create_default_registry()
|
|
138
|
+
|
|
139
|
+
self._audit = AuditLogger()
|
|
140
|
+
self._audit.open()
|
|
141
|
+
|
|
142
|
+
self._permission_mgr = PermissionManager(
|
|
143
|
+
config=self._config.permissions,
|
|
144
|
+
audit=self._audit,
|
|
145
|
+
prompt_fn=self._prompt_user_permission,
|
|
146
|
+
)
|
|
147
|
+
self._permission_mgr.set_project_root(Path.cwd())
|
|
148
|
+
|
|
149
|
+
self._trust_engine = TrustEngine(config=self._config.permissions)
|
|
150
|
+
|
|
151
|
+
self._mcp_registry = MCPRegistry()
|
|
152
|
+
try:
|
|
153
|
+
await self._mcp_registry.load_and_connect()
|
|
154
|
+
connected = self._mcp_registry.get_connected_servers()
|
|
155
|
+
if connected:
|
|
156
|
+
logger.info(f"MCP servers connected: {connected}")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.warning(f"MCP initialization failed (non-fatal): {e}")
|
|
159
|
+
|
|
160
|
+
self._persona_engine = PersonaEngine()
|
|
161
|
+
self._command_registry = build_default_commands()
|
|
162
|
+
|
|
163
|
+
self._memory_store = MemoryStore()
|
|
164
|
+
self._memory_store.open()
|
|
165
|
+
self._auto_memory = AutoMemory(self._memory_store)
|
|
166
|
+
self._history_search = HistorySearch(self._db)
|
|
167
|
+
|
|
168
|
+
self._project_rules = load_project_rules()
|
|
169
|
+
|
|
170
|
+
self._budget = BudgetGovernor(
|
|
171
|
+
session_limit_dt=self._config.budget.session_limit_dt if hasattr(self._config, "budget") else 50000.0,
|
|
172
|
+
)
|
|
173
|
+
self._daemon = DaemonManager()
|
|
174
|
+
self._widget_renderer = CliWidgetRenderer(console=self._console)
|
|
175
|
+
self._research_sources = ResearchSources(mcp_registry=self._mcp_registry)
|
|
176
|
+
|
|
177
|
+
self._theme_manager = ThemeManager()
|
|
178
|
+
theme_name = getattr(self._config, "theme", None)
|
|
179
|
+
if theme_name and isinstance(theme_name, str):
|
|
180
|
+
self._theme_manager.set_theme(theme_name)
|
|
181
|
+
self._console = self._theme_manager.create_console()
|
|
182
|
+
self._widget_renderer = CliWidgetRenderer(console=self._console)
|
|
183
|
+
|
|
184
|
+
self._pager = AutoPager(console=self._console)
|
|
185
|
+
self._cache = ResponseCache(max_size=256, default_ttl=600)
|
|
186
|
+
|
|
187
|
+
self._agent_loop = AgentLoop(
|
|
188
|
+
client=self._client,
|
|
189
|
+
tool_registry=self._tool_registry,
|
|
190
|
+
mcp_registry=self._mcp_registry,
|
|
191
|
+
model_router=self._router,
|
|
192
|
+
permission_manager=self._permission_mgr,
|
|
193
|
+
audit=self._audit,
|
|
194
|
+
budget=self._budget,
|
|
195
|
+
thinking_level=self._thinking_level,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def cleanup(self) -> None:
|
|
199
|
+
logger.debug("Entered into cleanup")
|
|
200
|
+
if self._daemon:
|
|
201
|
+
await self._daemon.stop()
|
|
202
|
+
if self._mcp_registry:
|
|
203
|
+
await self._mcp_registry.disconnect_all()
|
|
204
|
+
if self._client:
|
|
205
|
+
await self._client.close()
|
|
206
|
+
if self._memory_store:
|
|
207
|
+
self._memory_store.close()
|
|
208
|
+
if self._audit:
|
|
209
|
+
self._audit.close()
|
|
210
|
+
if self._db:
|
|
211
|
+
self._db.close()
|
|
212
|
+
|
|
213
|
+
def _prompt_user_permission(self, description: str) -> bool:
|
|
214
|
+
logger.debug(f"Entered into _prompt_user_permission: {description}")
|
|
215
|
+
self._console.print(f"\n[yellow]Permission required:[/yellow] {description}")
|
|
216
|
+
try:
|
|
217
|
+
answer = input(" Allow? [y/N] ").strip().lower()
|
|
218
|
+
except (EOFError, KeyboardInterrupt):
|
|
219
|
+
return False
|
|
220
|
+
approved = answer in ("y", "yes")
|
|
221
|
+
if self._trust_engine:
|
|
222
|
+
action = description.split(":")[0].strip() if ":" in description else description
|
|
223
|
+
self._trust_engine.record_decision(action, approved)
|
|
224
|
+
return approved
|
|
225
|
+
|
|
226
|
+
def _print_banner(self) -> None:
|
|
227
|
+
logger.debug("Entered into _print_banner")
|
|
228
|
+
api_key = get_api_key() or (self._config.api.key if self._config else "")
|
|
229
|
+
masked = mask_api_key(api_key) if api_key else "not set"
|
|
230
|
+
model = self._forced_model or "auto"
|
|
231
|
+
msg_count = len(self._messages)
|
|
232
|
+
|
|
233
|
+
tool_count = len(self._tool_registry.list_tools()) if self._tool_registry else 0
|
|
234
|
+
mcp_servers = self._mcp_registry.get_connected_servers() if self._mcp_registry else {}
|
|
235
|
+
mcp_tool_count = sum(mcp_servers.values())
|
|
236
|
+
persona = self._persona_engine.active.name if self._persona_engine and self._persona_engine.active else "none"
|
|
237
|
+
|
|
238
|
+
banner = Text()
|
|
239
|
+
banner.append(f"ELIDIA v{VERSION}", style="bold cyan")
|
|
240
|
+
banner.append(f" --- {masked}", style="dim")
|
|
241
|
+
banner.append(f"\n Model: {model}", style="")
|
|
242
|
+
banner.append(f" | Mode: {self._mode}", style="")
|
|
243
|
+
banner.append(f" | Persona: {persona}", style="")
|
|
244
|
+
banner.append(f"\n Tools: {tool_count} built-in", style="dim")
|
|
245
|
+
if mcp_servers:
|
|
246
|
+
banner.append(f" + {mcp_tool_count} MCP ({len(mcp_servers)} servers)", style="dim")
|
|
247
|
+
banner.append(f" | Messages: {msg_count}", style="dim")
|
|
248
|
+
|
|
249
|
+
self._console.print(Panel(banner, border_style="cyan", padding=(0, 1)))
|
|
250
|
+
self._console.print("[dim]Type your message. Ctrl+C to cancel, Ctrl+D to exit. /help for commands.[/dim]")
|
|
251
|
+
self._console.print()
|
|
252
|
+
|
|
253
|
+
async def run(self) -> None:
|
|
254
|
+
logger.debug("Entered into run")
|
|
255
|
+
self._print_banner()
|
|
256
|
+
|
|
257
|
+
history_file = ELIDIA_HOME / "history"
|
|
258
|
+
prompt_session: PromptSession[str] = PromptSession(
|
|
259
|
+
history=FileHistory(str(history_file)),
|
|
260
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
261
|
+
multiline=False,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
while True:
|
|
265
|
+
try:
|
|
266
|
+
user_input = await prompt_session.prompt_async("> ")
|
|
267
|
+
except (EOFError, KeyboardInterrupt):
|
|
268
|
+
self._console.print("\n[dim]Goodbye.[/dim]")
|
|
269
|
+
break
|
|
270
|
+
|
|
271
|
+
user_input = user_input.strip()
|
|
272
|
+
if not user_input:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
if user_input.startswith("/"):
|
|
277
|
+
handled = await self._handle_slash_command(user_input)
|
|
278
|
+
if handled:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
await self.send_message(user_input, interactive=True)
|
|
282
|
+
except EOFError:
|
|
283
|
+
self._console.print("\n[dim]Goodbye.[/dim]")
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
async def _handle_slash_command(self, command: str) -> bool:
|
|
287
|
+
logger.debug(f"Entered into _handle_slash_command: {command}")
|
|
288
|
+
parts = command.split(maxsplit=1)
|
|
289
|
+
cmd = parts[0].lower().lstrip("/")
|
|
290
|
+
arg = parts[1].strip() if len(parts) > 1 else ""
|
|
291
|
+
|
|
292
|
+
if cmd == "help":
|
|
293
|
+
return self._cmd_help(arg)
|
|
294
|
+
if cmd == "model":
|
|
295
|
+
return self._cmd_model(arg)
|
|
296
|
+
if cmd == "mode":
|
|
297
|
+
return self._cmd_mode(arg)
|
|
298
|
+
if cmd == "cost":
|
|
299
|
+
return self._cmd_cost()
|
|
300
|
+
if cmd == "new":
|
|
301
|
+
return self._cmd_new()
|
|
302
|
+
if cmd == "sessions":
|
|
303
|
+
return self._cmd_sessions()
|
|
304
|
+
if cmd == "clear":
|
|
305
|
+
return self._cmd_clear()
|
|
306
|
+
if cmd == "tools":
|
|
307
|
+
return self._cmd_tools(arg)
|
|
308
|
+
if cmd == "mcp":
|
|
309
|
+
return await self._cmd_mcp(arg)
|
|
310
|
+
if cmd == "persona":
|
|
311
|
+
return self._cmd_persona(arg)
|
|
312
|
+
if cmd == "trust":
|
|
313
|
+
return self._cmd_trust()
|
|
314
|
+
if cmd == "balance":
|
|
315
|
+
return await self._cmd_balance()
|
|
316
|
+
if cmd == "memory":
|
|
317
|
+
return self._cmd_memory(arg)
|
|
318
|
+
if cmd == "history":
|
|
319
|
+
return self._cmd_history(arg)
|
|
320
|
+
if cmd == "rules":
|
|
321
|
+
return self._cmd_rules()
|
|
322
|
+
if cmd == "think":
|
|
323
|
+
return self._cmd_think(arg)
|
|
324
|
+
if cmd == "budget":
|
|
325
|
+
return self._cmd_budget()
|
|
326
|
+
if cmd == "research":
|
|
327
|
+
return await self._cmd_research(arg)
|
|
328
|
+
if cmd == "create":
|
|
329
|
+
return await self._cmd_create(arg)
|
|
330
|
+
if cmd == "workflow":
|
|
331
|
+
return await self._cmd_workflow(arg)
|
|
332
|
+
if cmd == "daemon":
|
|
333
|
+
return await self._cmd_daemon(arg)
|
|
334
|
+
if cmd == "theme":
|
|
335
|
+
return self._cmd_theme(arg)
|
|
336
|
+
if cmd == "cache":
|
|
337
|
+
return self._cmd_cache(arg)
|
|
338
|
+
if cmd == "pager":
|
|
339
|
+
return self._cmd_pager(arg)
|
|
340
|
+
if cmd in ("quit", "exit"):
|
|
341
|
+
raise EOFError()
|
|
342
|
+
|
|
343
|
+
self._console.print(f"[yellow]Unknown command: /{cmd}[/yellow] -- type /help")
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
def _cmd_help(self, arg: str) -> bool:
|
|
347
|
+
if arg and self._command_registry:
|
|
348
|
+
cmd_def = self._command_registry.get(arg)
|
|
349
|
+
if cmd_def:
|
|
350
|
+
self._console.print(f"[bold]/{cmd_def.name}[/bold] -- {cmd_def.description}")
|
|
351
|
+
if cmd_def.usage:
|
|
352
|
+
self._console.print(f" Usage: {cmd_def.usage}")
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
cats = self._command_registry.list_by_category() if self._command_registry else {}
|
|
356
|
+
lines: list[str] = []
|
|
357
|
+
for cat, cmds in cats.items():
|
|
358
|
+
lines.append(f"\n[bold]{cat.title()}[/bold]")
|
|
359
|
+
for c in cmds:
|
|
360
|
+
lines.append(f" [bold]/{c.name:12s}[/bold] {c.description}")
|
|
361
|
+
self._console.print(Panel("\n".join(lines), title="Commands", border_style="blue"))
|
|
362
|
+
return True
|
|
363
|
+
|
|
364
|
+
def _cmd_model(self, arg: str) -> bool:
|
|
365
|
+
if arg:
|
|
366
|
+
if arg == "auto":
|
|
367
|
+
self._router.force_model(None)
|
|
368
|
+
self._forced_model = None
|
|
369
|
+
self._console.print("[green]v[/green] Switched to auto model routing.")
|
|
370
|
+
else:
|
|
371
|
+
self._router.force_model(arg)
|
|
372
|
+
self._forced_model = arg
|
|
373
|
+
self._console.print(f"[green]v[/green] Model set to: [cyan]{arg}[/cyan]")
|
|
374
|
+
else:
|
|
375
|
+
current = self._forced_model or "auto"
|
|
376
|
+
self._console.print(f"Current model: [cyan]{current}[/cyan]")
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
def _cmd_mode(self, arg: str) -> bool:
|
|
380
|
+
valid_modes = ("chat", "code", "research", "think", "create")
|
|
381
|
+
if arg and arg in valid_modes:
|
|
382
|
+
self._mode = arg
|
|
383
|
+
self._console.print(f"[green]v[/green] Mode: [cyan]{arg}[/cyan]")
|
|
384
|
+
elif arg:
|
|
385
|
+
self._console.print(f"[yellow]Invalid mode.[/yellow] Choose: {', '.join(valid_modes)}")
|
|
386
|
+
else:
|
|
387
|
+
self._console.print(f"Current mode: [cyan]{self._mode}[/cyan]")
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
def _cmd_cost(self) -> bool:
|
|
391
|
+
self._console.print(
|
|
392
|
+
f"Session cost: [cyan]{self._total_cost_dt:.1f} DT[/cyan] "
|
|
393
|
+
f"(${self._total_cost_dt / 1000:.4f})\n"
|
|
394
|
+
f"Tokens: {self._total_tokens_in:,} in / {self._total_tokens_out:,} out"
|
|
395
|
+
)
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
def _cmd_new(self) -> bool:
|
|
399
|
+
self._messages.clear()
|
|
400
|
+
if self._session_mgr:
|
|
401
|
+
self._session_id = self._session_mgr.create_session(mode=self._mode)
|
|
402
|
+
if self._permission_mgr:
|
|
403
|
+
self._permission_mgr.reset_session()
|
|
404
|
+
self._console.print("[green]v[/green] New session started.")
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
def _cmd_sessions(self) -> bool:
|
|
408
|
+
if not self._session_mgr:
|
|
409
|
+
return True
|
|
410
|
+
sessions = self._session_mgr.list_sessions()
|
|
411
|
+
for s in sessions:
|
|
412
|
+
marker = "->" if s["id"] == self._session_id else " "
|
|
413
|
+
self._console.print(
|
|
414
|
+
f" {marker} [dim]{s['id'][:8]}[/dim] {s['title']} [dim]{s['updated_at']}[/dim]"
|
|
415
|
+
)
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
def _cmd_clear(self) -> bool:
|
|
419
|
+
self._messages.clear()
|
|
420
|
+
self._console.print("[green]v[/green] Conversation cleared.")
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
def _cmd_tools(self, arg: str) -> bool:
|
|
424
|
+
if not self._tool_registry:
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
tools = self._tool_registry.list_tools()
|
|
428
|
+
if arg:
|
|
429
|
+
tools = [t for t in tools if t.category == arg]
|
|
430
|
+
|
|
431
|
+
categories: dict[str, list[str]] = {}
|
|
432
|
+
for t in tools:
|
|
433
|
+
categories.setdefault(t.category, []).append(f" {t.name:20s} {t.description}")
|
|
434
|
+
|
|
435
|
+
lines: list[str] = []
|
|
436
|
+
for cat, items in sorted(categories.items()):
|
|
437
|
+
lines.append(f"\n[bold]{cat}[/bold]")
|
|
438
|
+
lines.extend(items)
|
|
439
|
+
|
|
440
|
+
if self._mcp_registry:
|
|
441
|
+
mcp_tools = self._mcp_registry.list_all_tools()
|
|
442
|
+
if mcp_tools:
|
|
443
|
+
lines.append("\n[bold]mcp[/bold]")
|
|
444
|
+
for t in mcp_tools:
|
|
445
|
+
desc = t.description[:50] if t.description else ""
|
|
446
|
+
lines.append(f" {t.server_name}__{t.name:20s} {desc}")
|
|
447
|
+
|
|
448
|
+
total = len(tools) + (len(self._mcp_registry.list_all_tools()) if self._mcp_registry else 0)
|
|
449
|
+
self._console.print(Panel("\n".join(lines), title=f"Tools ({total})", border_style="green"))
|
|
450
|
+
return True
|
|
451
|
+
|
|
452
|
+
async def _cmd_mcp(self, arg: str) -> bool:
|
|
453
|
+
if not self._mcp_registry:
|
|
454
|
+
self._console.print("[dim]MCP not initialized.[/dim]")
|
|
455
|
+
return True
|
|
456
|
+
|
|
457
|
+
if not arg:
|
|
458
|
+
servers = self._mcp_registry.get_connected_servers()
|
|
459
|
+
if servers:
|
|
460
|
+
for name, tool_count in servers.items():
|
|
461
|
+
self._console.print(f" [green]connected[/green] {name} ({tool_count} tools)")
|
|
462
|
+
else:
|
|
463
|
+
self._console.print("[dim]No MCP servers connected.[/dim]")
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
parts = arg.split(maxsplit=1)
|
|
467
|
+
action = parts[0]
|
|
468
|
+
name = parts[1] if len(parts) > 1 else ""
|
|
469
|
+
|
|
470
|
+
if action == "disconnect" and name:
|
|
471
|
+
await self._mcp_registry.disconnect_server(name)
|
|
472
|
+
self._console.print(f"[green]v[/green] Disconnected MCP server: {name}")
|
|
473
|
+
else:
|
|
474
|
+
self._console.print("[yellow]Usage: /mcp [disconnect <name>][/yellow]")
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
def _cmd_persona(self, arg: str) -> bool:
|
|
478
|
+
if not self._persona_engine:
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
if not arg or arg == "list":
|
|
482
|
+
personas = self._persona_engine.list_personas()
|
|
483
|
+
active = self._persona_engine.active
|
|
484
|
+
for p in personas:
|
|
485
|
+
marker = "->" if active and active.slug == p.slug else " "
|
|
486
|
+
self._console.print(f" {marker} [bold]{p.slug:12s}[/bold] {p.name} -- {p.system_prompt[:60]}...")
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
if arg == "off":
|
|
490
|
+
self._persona_engine.deactivate()
|
|
491
|
+
self._console.print("[green]v[/green] Persona deactivated.")
|
|
492
|
+
return True
|
|
493
|
+
|
|
494
|
+
persona = self._persona_engine.activate(arg)
|
|
495
|
+
if persona:
|
|
496
|
+
self._console.print(f"[green]v[/green] Persona: [cyan]{persona.name}[/cyan]")
|
|
497
|
+
if persona.greeting:
|
|
498
|
+
self._console.print(f"[dim]{persona.greeting}[/dim]")
|
|
499
|
+
else:
|
|
500
|
+
self._console.print(f"[yellow]Unknown persona: {arg}[/yellow] -- type /persona list")
|
|
501
|
+
return True
|
|
502
|
+
|
|
503
|
+
def _cmd_trust(self) -> bool:
|
|
504
|
+
if not self._trust_engine:
|
|
505
|
+
return True
|
|
506
|
+
stats = self._trust_engine.get_stats()
|
|
507
|
+
if not stats:
|
|
508
|
+
self._console.print("[dim]No trust data yet.[/dim]")
|
|
509
|
+
return True
|
|
510
|
+
for action, s in stats.items():
|
|
511
|
+
status = "[green]promoted[/green]" if s.auto_promoted else "[dim]tracking[/dim]"
|
|
512
|
+
self._console.print(f" {action:25s} approved={s.approved_count} denied={s.denied_count} {status}")
|
|
513
|
+
return True
|
|
514
|
+
|
|
515
|
+
async def _cmd_balance(self) -> bool:
|
|
516
|
+
if not self._client:
|
|
517
|
+
return True
|
|
518
|
+
balance = await self._client.get_balance()
|
|
519
|
+
if balance.get("balance_dt", -1) >= 0:
|
|
520
|
+
self._console.print(f"Balance: [cyan]{balance['balance_dt']:,.0f} DT[/cyan] (${balance['balance_dt'] / 1000:.2f})")
|
|
521
|
+
else:
|
|
522
|
+
self._console.print(f"[yellow]Could not fetch balance: {balance.get('error', 'unknown')}[/yellow]")
|
|
523
|
+
return True
|
|
524
|
+
|
|
525
|
+
def _cmd_memory(self, arg: str) -> bool:
|
|
526
|
+
if not self._memory_store:
|
|
527
|
+
self._console.print("[dim]Memory not initialized.[/dim]")
|
|
528
|
+
return True
|
|
529
|
+
|
|
530
|
+
parts = arg.split(maxsplit=1)
|
|
531
|
+
action = parts[0] if parts else "list"
|
|
532
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
533
|
+
|
|
534
|
+
if action == "search" and rest:
|
|
535
|
+
results = self._memory_store.search_text(rest, limit=10)
|
|
536
|
+
if not results:
|
|
537
|
+
self._console.print(f"[dim]No memories matching '{rest}'[/dim]")
|
|
538
|
+
for m in results:
|
|
539
|
+
tier = MemoryTier(m.tier).name
|
|
540
|
+
self._console.print(f" [{tier}] [bold]{m.key}[/bold]: {m.content[:80]}")
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
if action == "save" and rest:
|
|
544
|
+
sep_idx = rest.find("=")
|
|
545
|
+
if sep_idx > 0:
|
|
546
|
+
key = rest[:sep_idx].strip()
|
|
547
|
+
content = rest[sep_idx + 1:].strip()
|
|
548
|
+
from elidia.memory.store import MemoryEntry
|
|
549
|
+
mid = self._memory_store.save(MemoryEntry(
|
|
550
|
+
tier=MemoryTier.USER, key=key, content=content,
|
|
551
|
+
session_id=self._session_id or "", source="user_command",
|
|
552
|
+
))
|
|
553
|
+
self._console.print(f"[green]v[/green] Saved memory: {key} ({mid[:8]})")
|
|
554
|
+
else:
|
|
555
|
+
self._console.print("[yellow]Usage: /memory save key=value[/yellow]")
|
|
556
|
+
return True
|
|
557
|
+
|
|
558
|
+
if action == "forget" and rest:
|
|
559
|
+
count = self._memory_store.delete_by_key(rest)
|
|
560
|
+
self._console.print(f"[green]v[/green] Deleted {count} memories with key '{rest}'")
|
|
561
|
+
return True
|
|
562
|
+
|
|
563
|
+
if action == "list" or not arg:
|
|
564
|
+
tier_filter = None
|
|
565
|
+
if rest:
|
|
566
|
+
try:
|
|
567
|
+
tier_filter = MemoryTier[rest.upper()]
|
|
568
|
+
except KeyError:
|
|
569
|
+
pass
|
|
570
|
+
memories = self._memory_store.list_memories(tier=tier_filter, limit=20)
|
|
571
|
+
if not memories:
|
|
572
|
+
self._console.print("[dim]No memories stored.[/dim]")
|
|
573
|
+
for m in memories:
|
|
574
|
+
tier = MemoryTier(m.tier).name
|
|
575
|
+
self._console.print(f" [{tier}] [bold]{m.key}[/bold]: {m.content[:80]}")
|
|
576
|
+
total = self._memory_store.count()
|
|
577
|
+
self._console.print(f"\n[dim]Total: {total} memories[/dim]")
|
|
578
|
+
return True
|
|
579
|
+
|
|
580
|
+
self._console.print("[yellow]Usage: /memory [list|search <query>|save key=value|forget <key>][/yellow]")
|
|
581
|
+
return True
|
|
582
|
+
|
|
583
|
+
def _cmd_history(self, arg: str) -> bool:
|
|
584
|
+
if not self._history_search:
|
|
585
|
+
self._console.print("[dim]History not available.[/dim]")
|
|
586
|
+
return True
|
|
587
|
+
|
|
588
|
+
if not arg:
|
|
589
|
+
stats = self._history_search.get_session_stats()
|
|
590
|
+
for s in stats[:15]:
|
|
591
|
+
self._console.print(
|
|
592
|
+
f" [dim]{s['id'][:8]}[/dim] {s['title'][:40]:40s} "
|
|
593
|
+
f"[dim]{s['message_count']} msgs | {s['updated_at']}[/dim]"
|
|
594
|
+
)
|
|
595
|
+
return True
|
|
596
|
+
|
|
597
|
+
results = self._history_search.search(arg, limit=10)
|
|
598
|
+
if not results:
|
|
599
|
+
self._console.print(f"[dim]No messages matching '{arg}'[/dim]")
|
|
600
|
+
return True
|
|
601
|
+
|
|
602
|
+
for r in results:
|
|
603
|
+
role = r["role"].upper()
|
|
604
|
+
preview = r["content"][:100].replace("\n", " ")
|
|
605
|
+
self._console.print(
|
|
606
|
+
f" [dim]{r['session_id'][:8]}[/dim] [{role}] {preview}"
|
|
607
|
+
)
|
|
608
|
+
return True
|
|
609
|
+
|
|
610
|
+
def _cmd_rules(self) -> bool:
|
|
611
|
+
if self._project_rules:
|
|
612
|
+
self._console.print(Panel(self._project_rules, title="Project Rules", border_style="blue"))
|
|
613
|
+
else:
|
|
614
|
+
self._console.print("[dim]No project rules found (.elidia/rules.md)[/dim]")
|
|
615
|
+
return True
|
|
616
|
+
|
|
617
|
+
def _cmd_think(self, arg: str) -> bool:
|
|
618
|
+
logger.debug(f"Entered into _cmd_think: arg={arg}")
|
|
619
|
+
if not arg:
|
|
620
|
+
desc = describe_level(self._thinking_level)
|
|
621
|
+
self._console.print(f"Thinking level: [cyan]{desc}[/cyan]")
|
|
622
|
+
return True
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
level = parse_thinking_level(arg)
|
|
626
|
+
self._thinking_level = level
|
|
627
|
+
if self._agent_loop:
|
|
628
|
+
self._agent_loop._thinking_level = level
|
|
629
|
+
desc = describe_level(level)
|
|
630
|
+
self._console.print(f"[green]v[/green] Thinking level: [cyan]{desc}[/cyan]")
|
|
631
|
+
except ValueError:
|
|
632
|
+
self._console.print(
|
|
633
|
+
"[yellow]Invalid level.[/yellow] Choose: minimal, low, medium, high, max (or 1-5)"
|
|
634
|
+
)
|
|
635
|
+
return True
|
|
636
|
+
|
|
637
|
+
def _cmd_budget(self) -> bool:
|
|
638
|
+
logger.debug("Entered into _cmd_budget")
|
|
639
|
+
if not self._budget:
|
|
640
|
+
self._console.print("[dim]Budget not initialized.[/dim]")
|
|
641
|
+
return True
|
|
642
|
+
|
|
643
|
+
summary = self._budget.get_summary()
|
|
644
|
+
self._console.print(Panel(
|
|
645
|
+
f"Session: [cyan]{summary['session_dt_used']:.1f}[/cyan] / "
|
|
646
|
+
f"{summary['session_limit_dt']:.0f} DT ({summary['session_pct']:.1f}%)\n"
|
|
647
|
+
f"Tokens: {summary['session_tokens_in']:,} in / {summary['session_tokens_out']:,} out\n"
|
|
648
|
+
f"Calls: {summary['call_count']}",
|
|
649
|
+
title="Budget",
|
|
650
|
+
border_style="yellow",
|
|
651
|
+
))
|
|
652
|
+
return True
|
|
653
|
+
|
|
654
|
+
async def _cmd_research(self, arg: str) -> bool:
|
|
655
|
+
logger.debug(f"Entered into _cmd_research: arg={arg}")
|
|
656
|
+
if not arg:
|
|
657
|
+
self._console.print("[yellow]Usage: /research <query> [--export md|html][/yellow]")
|
|
658
|
+
return True
|
|
659
|
+
|
|
660
|
+
export_format = None
|
|
661
|
+
query = arg
|
|
662
|
+
if " --export " in arg:
|
|
663
|
+
parts = arg.split(" --export ", 1)
|
|
664
|
+
query = parts[0].strip()
|
|
665
|
+
export_format = parts[1].strip().lower()
|
|
666
|
+
|
|
667
|
+
if not self._client:
|
|
668
|
+
self._console.print("[red]No client initialized.[/red]")
|
|
669
|
+
return True
|
|
670
|
+
|
|
671
|
+
self._console.print(f"[cyan]Researching:[/cyan] {query}")
|
|
672
|
+
|
|
673
|
+
async def search_fn(q: str):
|
|
674
|
+
if self._research_sources:
|
|
675
|
+
return await self._research_sources.search(q)
|
|
676
|
+
return []
|
|
677
|
+
|
|
678
|
+
orchestrator = ResearchOrchestrator(client=self._client, search_fn=search_fn)
|
|
679
|
+
|
|
680
|
+
try:
|
|
681
|
+
async for event in orchestrator.run(query):
|
|
682
|
+
if event.kind == "stage":
|
|
683
|
+
self._console.print(f" [dim]{event.data}[/dim]")
|
|
684
|
+
elif event.kind == "result":
|
|
685
|
+
report = event.data.get("report", "")
|
|
686
|
+
sources = event.data.get("sources", [])
|
|
687
|
+
|
|
688
|
+
if export_format == "html":
|
|
689
|
+
result = export_html(report, sources=sources)
|
|
690
|
+
output_path = ELIDIA_HOME / "research" / f"report_{int(time.time())}.html"
|
|
691
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
692
|
+
output_path.write_text(result.content, encoding="utf-8")
|
|
693
|
+
self._console.print(f"\n[green]v[/green] Saved: {output_path}")
|
|
694
|
+
elif export_format == "md":
|
|
695
|
+
result = export_markdown(report, sources=sources)
|
|
696
|
+
output_path = ELIDIA_HOME / "research" / f"report_{int(time.time())}.md"
|
|
697
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
698
|
+
output_path.write_text(result.content, encoding="utf-8")
|
|
699
|
+
self._console.print(f"\n[green]v[/green] Saved: {output_path}")
|
|
700
|
+
else:
|
|
701
|
+
self._console.print()
|
|
702
|
+
self._console.print(Markdown(report))
|
|
703
|
+
if sources:
|
|
704
|
+
self._console.print(f"\n[dim]{len(sources)} source(s) cited[/dim]")
|
|
705
|
+
elif event.kind == "error":
|
|
706
|
+
self._console.print(f"[red]Research error: {event.data}[/red]")
|
|
707
|
+
except Exception as e:
|
|
708
|
+
self._console.print(f"[red]Research failed: {e}[/red]")
|
|
709
|
+
|
|
710
|
+
return True
|
|
711
|
+
|
|
712
|
+
async def _cmd_create(self, arg: str) -> bool:
|
|
713
|
+
logger.debug(f"Entered into _cmd_create: arg={arg}")
|
|
714
|
+
if not arg:
|
|
715
|
+
self._console.print(
|
|
716
|
+
"[yellow]Usage:[/yellow]\n"
|
|
717
|
+
" /create image <prompt> [--model <model>]\n"
|
|
718
|
+
" /create video <prompt> [--model <model>]\n"
|
|
719
|
+
" /create speech <text> [--voice <voice>]\n"
|
|
720
|
+
" /create music <prompt>\n"
|
|
721
|
+
" /create models"
|
|
722
|
+
)
|
|
723
|
+
return True
|
|
724
|
+
|
|
725
|
+
parts = arg.split(maxsplit=1)
|
|
726
|
+
sub_cmd = parts[0].lower()
|
|
727
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
728
|
+
|
|
729
|
+
if not self._client:
|
|
730
|
+
self._console.print("[red]No client initialized.[/red]")
|
|
731
|
+
return True
|
|
732
|
+
|
|
733
|
+
if sub_cmd == "models":
|
|
734
|
+
self._console.print("[bold]Image models:[/bold]")
|
|
735
|
+
for m in list_image_models():
|
|
736
|
+
self._console.print(f" {m['model']:25s} {m.get('vendor', '')}")
|
|
737
|
+
self._console.print("[bold]Video models:[/bold]")
|
|
738
|
+
for m in list_video_models():
|
|
739
|
+
self._console.print(f" {m['model']:25s} {m.get('vendor', '')}")
|
|
740
|
+
self._console.print("[bold]Audio models:[/bold]")
|
|
741
|
+
for m in list_audio_models():
|
|
742
|
+
self._console.print(f" {m['model']:25s} {m.get('vendor', '')}")
|
|
743
|
+
return True
|
|
744
|
+
|
|
745
|
+
model = None
|
|
746
|
+
voice = None
|
|
747
|
+
prompt = rest
|
|
748
|
+
if " --model " in rest:
|
|
749
|
+
prompt, model = rest.rsplit(" --model ", 1)
|
|
750
|
+
prompt = prompt.strip()
|
|
751
|
+
model = model.strip()
|
|
752
|
+
if " --voice " in rest:
|
|
753
|
+
prompt, voice = rest.rsplit(" --voice ", 1)
|
|
754
|
+
prompt = prompt.strip()
|
|
755
|
+
voice = voice.strip()
|
|
756
|
+
|
|
757
|
+
if not prompt:
|
|
758
|
+
self._console.print("[yellow]Prompt required.[/yellow]")
|
|
759
|
+
return True
|
|
760
|
+
|
|
761
|
+
try:
|
|
762
|
+
if sub_cmd == "image":
|
|
763
|
+
self._console.print("[cyan]Generating image...[/cyan]")
|
|
764
|
+
result = await generate_image(self._client, prompt, model=model)
|
|
765
|
+
self._console.print(f"[green]v[/green] Saved: {result.file_path}")
|
|
766
|
+
display_image(result.file_path)
|
|
767
|
+
|
|
768
|
+
elif sub_cmd == "video":
|
|
769
|
+
self._console.print("[cyan]Generating video (this may take a few minutes)...[/cyan]")
|
|
770
|
+
result = await generate_video(self._client, prompt, model=model)
|
|
771
|
+
self._console.print(f"[green]v[/green] Saved: {result.file_path}")
|
|
772
|
+
|
|
773
|
+
elif sub_cmd == "speech":
|
|
774
|
+
self._console.print("[cyan]Generating speech...[/cyan]")
|
|
775
|
+
result = await generate_speech(self._client, prompt, voice=voice)
|
|
776
|
+
self._console.print(f"[green]v[/green] Saved: {result.file_path}")
|
|
777
|
+
|
|
778
|
+
elif sub_cmd == "music":
|
|
779
|
+
self._console.print("[cyan]Generating music...[/cyan]")
|
|
780
|
+
result = await generate_music(self._client, prompt)
|
|
781
|
+
self._console.print(f"[green]v[/green] Saved: {result.file_path}")
|
|
782
|
+
|
|
783
|
+
else:
|
|
784
|
+
self._console.print(f"[yellow]Unknown create type: {sub_cmd}[/yellow]")
|
|
785
|
+
|
|
786
|
+
except Exception as e:
|
|
787
|
+
self._console.print(f"[red]Creation failed: {e}[/red]")
|
|
788
|
+
|
|
789
|
+
return True
|
|
790
|
+
|
|
791
|
+
async def _cmd_workflow(self, arg: str) -> bool:
|
|
792
|
+
logger.debug(f"Entered into _cmd_workflow: arg={arg}")
|
|
793
|
+
if not arg:
|
|
794
|
+
self._console.print("[yellow]Usage: /workflow <path_to_yaml>[/yellow]")
|
|
795
|
+
return True
|
|
796
|
+
|
|
797
|
+
workflow_path = Path(arg)
|
|
798
|
+
if not workflow_path.exists():
|
|
799
|
+
self._console.print(f"[red]Workflow file not found: {arg}[/red]")
|
|
800
|
+
return True
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
workflow = parse_workflow(workflow_path)
|
|
804
|
+
except Exception as e:
|
|
805
|
+
self._console.print(f"[red]Failed to parse workflow: {e}[/red]")
|
|
806
|
+
return True
|
|
807
|
+
|
|
808
|
+
self._console.print(f"[cyan]Running workflow:[/cyan] {workflow.name}")
|
|
809
|
+
executor = WorkflowExecutor(client=self._client)
|
|
810
|
+
|
|
811
|
+
try:
|
|
812
|
+
async for event in executor.run(workflow):
|
|
813
|
+
if event.kind == "start":
|
|
814
|
+
self._console.print(f" [dim]Steps: {event.data.get('step_count', 0)}[/dim]")
|
|
815
|
+
elif event.kind == "step_start":
|
|
816
|
+
self._console.print(f" [cyan]>{event.data['name']}[/cyan] ({event.data['type']})")
|
|
817
|
+
elif event.kind == "step_done":
|
|
818
|
+
status = event.data.get("status", "?")
|
|
819
|
+
elapsed = event.data.get("elapsed_ms", 0)
|
|
820
|
+
style = "green" if status == "completed" else "yellow" if status == "skipped" else "red"
|
|
821
|
+
self._console.print(f" [{style}]{status}[/{style}] {event.data['name']} ({elapsed}ms)")
|
|
822
|
+
elif event.kind == "done":
|
|
823
|
+
total = event.data.get("total_steps", 0)
|
|
824
|
+
completed = event.data.get("completed", 0)
|
|
825
|
+
failed = event.data.get("failed", 0)
|
|
826
|
+
elapsed = event.data.get("elapsed_ms", 0)
|
|
827
|
+
self._console.print(
|
|
828
|
+
f"\n[bold]Done:[/bold] {completed}/{total} completed, "
|
|
829
|
+
f"{failed} failed ({elapsed}ms)"
|
|
830
|
+
)
|
|
831
|
+
elif event.kind == "error":
|
|
832
|
+
self._console.print(f" [red]Error: {event.data}[/red]")
|
|
833
|
+
except Exception as e:
|
|
834
|
+
self._console.print(f"[red]Workflow error: {e}[/red]")
|
|
835
|
+
|
|
836
|
+
return True
|
|
837
|
+
|
|
838
|
+
async def _cmd_daemon(self, arg: str) -> bool:
|
|
839
|
+
logger.debug(f"Entered into _cmd_daemon: arg={arg}")
|
|
840
|
+
if not self._daemon:
|
|
841
|
+
self._console.print("[dim]Daemon not initialized.[/dim]")
|
|
842
|
+
return True
|
|
843
|
+
|
|
844
|
+
parts = arg.split(maxsplit=2) if arg else []
|
|
845
|
+
action = parts[0] if parts else "status"
|
|
846
|
+
|
|
847
|
+
if action == "status":
|
|
848
|
+
status = self._daemon.get_status()
|
|
849
|
+
self._console.print(Panel(
|
|
850
|
+
f"Running: {'yes' if status['running'] else 'no'}\n"
|
|
851
|
+
f"Tasks: {status['task_count']} ({status['active']} active)",
|
|
852
|
+
title="Daemon",
|
|
853
|
+
border_style="magenta",
|
|
854
|
+
))
|
|
855
|
+
for t in status.get("tasks", []):
|
|
856
|
+
style = "green" if t["status"] == "running" else "dim"
|
|
857
|
+
self._console.print(
|
|
858
|
+
f" [{style}]{t['status']:8s}[/{style}] {t['name']} ({t['type']}) "
|
|
859
|
+
f"runs={t['run_count']}"
|
|
860
|
+
)
|
|
861
|
+
return True
|
|
862
|
+
|
|
863
|
+
if action == "start":
|
|
864
|
+
await self._daemon.start()
|
|
865
|
+
self._console.print("[green]v[/green] Daemon started.")
|
|
866
|
+
return True
|
|
867
|
+
|
|
868
|
+
if action == "stop":
|
|
869
|
+
await self._daemon.stop()
|
|
870
|
+
self._console.print("[green]v[/green] Daemon stopped.")
|
|
871
|
+
return True
|
|
872
|
+
|
|
873
|
+
if action == "watch" and len(parts) >= 2:
|
|
874
|
+
watch_path = parts[1]
|
|
875
|
+
name = parts[2] if len(parts) > 2 else Path(watch_path).name
|
|
876
|
+
task_id = self._daemon.add_watcher(name, watch_path)
|
|
877
|
+
self._console.print(f"[green]v[/green] Watcher added: {name} → {watch_path} ({task_id[:16]})")
|
|
878
|
+
return True
|
|
879
|
+
|
|
880
|
+
if action == "schedule" and len(parts) >= 2:
|
|
881
|
+
interval = int(parts[1])
|
|
882
|
+
command = parts[2] if len(parts) > 2 else ""
|
|
883
|
+
name = f"sched_{interval}s"
|
|
884
|
+
task_id = self._daemon.add_schedule(name, interval, command)
|
|
885
|
+
self._console.print(f"[green]v[/green] Schedule added: every {interval}s ({task_id[:16]})")
|
|
886
|
+
return True
|
|
887
|
+
|
|
888
|
+
self._console.print(
|
|
889
|
+
"[yellow]Usage:[/yellow]\n"
|
|
890
|
+
" /daemon status\n"
|
|
891
|
+
" /daemon start\n"
|
|
892
|
+
" /daemon stop\n"
|
|
893
|
+
" /daemon watch <path> [name]\n"
|
|
894
|
+
" /daemon schedule <seconds> [command]"
|
|
895
|
+
)
|
|
896
|
+
return True
|
|
897
|
+
|
|
898
|
+
def _cmd_theme(self, arg: str) -> bool:
|
|
899
|
+
logger.debug(f"Entered into _cmd_theme: arg={arg}")
|
|
900
|
+
if not self._theme_manager:
|
|
901
|
+
self._console.print("[dim]Theme manager not initialized.[/dim]")
|
|
902
|
+
return True
|
|
903
|
+
|
|
904
|
+
if not arg or arg == "list":
|
|
905
|
+
themes = self._theme_manager.list_themes()
|
|
906
|
+
current = self._theme_manager.current_name
|
|
907
|
+
for t in themes:
|
|
908
|
+
marker = "->" if t["name"] == current else " "
|
|
909
|
+
self._console.print(f" {marker} {t['name']:12s} {t['description']}")
|
|
910
|
+
return True
|
|
911
|
+
|
|
912
|
+
result = self._theme_manager.set_theme(arg)
|
|
913
|
+
if result:
|
|
914
|
+
self._console = self._theme_manager.create_console()
|
|
915
|
+
if self._pager:
|
|
916
|
+
self._pager = AutoPager(console=self._console)
|
|
917
|
+
self._console.print(f"[green]v[/green] Theme: [cyan]{arg}[/cyan]")
|
|
918
|
+
else:
|
|
919
|
+
self._console.print(f"[yellow]Unknown theme: {arg}[/yellow] -- type /theme list")
|
|
920
|
+
return True
|
|
921
|
+
|
|
922
|
+
def _cmd_cache(self, arg: str) -> bool:
|
|
923
|
+
logger.debug(f"Entered into _cmd_cache: arg={arg}")
|
|
924
|
+
if not self._cache:
|
|
925
|
+
self._console.print("[dim]Cache not initialized.[/dim]")
|
|
926
|
+
return True
|
|
927
|
+
|
|
928
|
+
if arg == "clear":
|
|
929
|
+
self._cache.clear()
|
|
930
|
+
self._console.print("[green]v[/green] Cache cleared.")
|
|
931
|
+
return True
|
|
932
|
+
|
|
933
|
+
if arg == "off":
|
|
934
|
+
self._cache.enabled = False
|
|
935
|
+
self._console.print("[green]v[/green] Cache disabled.")
|
|
936
|
+
return True
|
|
937
|
+
|
|
938
|
+
if arg == "on":
|
|
939
|
+
self._cache.enabled = True
|
|
940
|
+
self._console.print("[green]v[/green] Cache enabled.")
|
|
941
|
+
return True
|
|
942
|
+
|
|
943
|
+
stats = self._cache.get_stats()
|
|
944
|
+
self._console.print(Panel(
|
|
945
|
+
f"Enabled: {'yes' if stats['enabled'] else 'no'}\n"
|
|
946
|
+
f"Size: {stats['size']} / {stats['max_size']}\n"
|
|
947
|
+
f"Hits: {stats['hits']} | Misses: {stats['misses']}\n"
|
|
948
|
+
f"Hit rate: {stats['hit_rate_pct']:.1f}%",
|
|
949
|
+
title="Cache",
|
|
950
|
+
border_style="blue",
|
|
951
|
+
))
|
|
952
|
+
return True
|
|
953
|
+
|
|
954
|
+
def _cmd_pager(self, arg: str) -> bool:
|
|
955
|
+
logger.debug(f"Entered into _cmd_pager: arg={arg}")
|
|
956
|
+
if not self._pager:
|
|
957
|
+
self._console.print("[dim]Pager not initialized.[/dim]")
|
|
958
|
+
return True
|
|
959
|
+
|
|
960
|
+
if arg == "off":
|
|
961
|
+
self._pager.enabled = False
|
|
962
|
+
self._console.print("[green]v[/green] Auto-pager disabled.")
|
|
963
|
+
return True
|
|
964
|
+
|
|
965
|
+
if arg == "on":
|
|
966
|
+
self._pager.enabled = True
|
|
967
|
+
self._console.print("[green]v[/green] Auto-pager enabled.")
|
|
968
|
+
return True
|
|
969
|
+
|
|
970
|
+
status = "enabled" if self._pager.enabled else "disabled"
|
|
971
|
+
self._console.print(f"Auto-pager: [cyan]{status}[/cyan] (threshold: {self._pager.threshold:.0%})")
|
|
972
|
+
return True
|
|
973
|
+
|
|
974
|
+
async def send_message(self, user_input: str, interactive: bool = True) -> None:
|
|
975
|
+
logger.debug(f"Entered into send_message: msg_len={len(user_input)}, interactive={interactive}")
|
|
976
|
+
|
|
977
|
+
self._messages.append(ChatMessage(role="user", content=user_input))
|
|
978
|
+
|
|
979
|
+
if self._session_mgr and self._session_id:
|
|
980
|
+
self._session_mgr.add_message(self._session_id, "user", user_input)
|
|
981
|
+
|
|
982
|
+
if self._auto_memory:
|
|
983
|
+
saved = self._auto_memory.analyze_user_message(
|
|
984
|
+
user_input,
|
|
985
|
+
session_id=self._session_id or "",
|
|
986
|
+
project_path=str(Path.cwd()),
|
|
987
|
+
)
|
|
988
|
+
if saved and interactive:
|
|
989
|
+
for entry in saved:
|
|
990
|
+
self._console.print(f"[dim] (remembered: {entry.key})[/dim]")
|
|
991
|
+
|
|
992
|
+
if self._agent_loop:
|
|
993
|
+
await self._send_with_agent_loop(user_input, interactive)
|
|
994
|
+
else:
|
|
995
|
+
await self._send_direct(user_input, interactive)
|
|
996
|
+
|
|
997
|
+
async def _send_with_agent_loop(self, user_input: str, interactive: bool) -> None:
|
|
998
|
+
logger.debug(f"Entered into _send_with_agent_loop: interactive={interactive}")
|
|
999
|
+
start_time = time.monotonic()
|
|
1000
|
+
full_response = ""
|
|
1001
|
+
total_tokens_in = 0
|
|
1002
|
+
total_tokens_out = 0
|
|
1003
|
+
total_cost = 0.0
|
|
1004
|
+
|
|
1005
|
+
try:
|
|
1006
|
+
async for event in self._agent_loop.run(
|
|
1007
|
+
messages=self._messages,
|
|
1008
|
+
mode=self._mode,
|
|
1009
|
+
forced_model=self._forced_model,
|
|
1010
|
+
session_id=self._session_id or "",
|
|
1011
|
+
):
|
|
1012
|
+
if event.kind == "mode_info":
|
|
1013
|
+
if interactive:
|
|
1014
|
+
exec_mode = event.data.get("exec_mode", "direct")
|
|
1015
|
+
confidence = event.data.get("confidence", 0.0)
|
|
1016
|
+
reason = event.data.get("reason", "")
|
|
1017
|
+
if exec_mode != "direct":
|
|
1018
|
+
self._console.print(f"[dim]Mode: {exec_mode} ({confidence:.0%}) — {reason}[/dim]")
|
|
1019
|
+
|
|
1020
|
+
elif event.kind == "budget_warning":
|
|
1021
|
+
if interactive:
|
|
1022
|
+
self._console.print(f"[yellow]Budget: {event.data.get('message', '')}[/yellow]")
|
|
1023
|
+
|
|
1024
|
+
elif event.kind == "thinking":
|
|
1025
|
+
if interactive:
|
|
1026
|
+
model = event.data.get("model", "?")
|
|
1027
|
+
reason = event.data.get("reason", "")
|
|
1028
|
+
self._console.print(f"\n[dim]Model: {model} ({reason})[/dim]")
|
|
1029
|
+
|
|
1030
|
+
elif event.kind == "content":
|
|
1031
|
+
full_response = event.data
|
|
1032
|
+
if interactive:
|
|
1033
|
+
self._console.print()
|
|
1034
|
+
try:
|
|
1035
|
+
if self._pager and self._pager.should_page(full_response):
|
|
1036
|
+
self._pager.print_or_page(full_response, as_markdown=True)
|
|
1037
|
+
else:
|
|
1038
|
+
self._console.print(Markdown(full_response))
|
|
1039
|
+
except Exception:
|
|
1040
|
+
self._console.print(full_response)
|
|
1041
|
+
else:
|
|
1042
|
+
sys.stdout.write(full_response)
|
|
1043
|
+
sys.stdout.flush()
|
|
1044
|
+
|
|
1045
|
+
elif event.kind == "tool_call":
|
|
1046
|
+
name = event.data.get("name", "?")
|
|
1047
|
+
args = event.data.get("arguments", {})
|
|
1048
|
+
args_preview = str(args)[:100]
|
|
1049
|
+
if interactive:
|
|
1050
|
+
self._console.print(f" [cyan]> {name}[/cyan]({args_preview})")
|
|
1051
|
+
|
|
1052
|
+
elif event.kind == "tool_result":
|
|
1053
|
+
name = event.data.get("name", "?")
|
|
1054
|
+
content = event.data.get("content", "")
|
|
1055
|
+
is_error = event.data.get("is_error", False)
|
|
1056
|
+
if interactive:
|
|
1057
|
+
style = "red" if is_error else "dim"
|
|
1058
|
+
preview = content[:200].replace("\n", " ")
|
|
1059
|
+
self._console.print(f" [{style}]< {name}: {preview}[/{style}]")
|
|
1060
|
+
|
|
1061
|
+
elif event.kind == "usage":
|
|
1062
|
+
total_tokens_in += event.data.get("tokens_in", 0)
|
|
1063
|
+
total_tokens_out += event.data.get("tokens_out", 0)
|
|
1064
|
+
total_cost += event.data.get("cost_dt", 0.0)
|
|
1065
|
+
|
|
1066
|
+
elif event.kind == "error":
|
|
1067
|
+
err_msg = str(event.data)
|
|
1068
|
+
if interactive:
|
|
1069
|
+
self._console.print(f"\n[red]Error: {err_msg}[/red]")
|
|
1070
|
+
else:
|
|
1071
|
+
sys.stderr.write(f"\nError: {err_msg}\n")
|
|
1072
|
+
self._messages.pop()
|
|
1073
|
+
return
|
|
1074
|
+
|
|
1075
|
+
elif event.kind == "done":
|
|
1076
|
+
loops = event.data.get("loops", 0)
|
|
1077
|
+
tools_used = event.data.get("tools_called", [])
|
|
1078
|
+
if interactive and tools_used:
|
|
1079
|
+
self._console.print(f"[dim] ({loops} loop(s), {len(tools_used)} tool call(s))[/dim]")
|
|
1080
|
+
|
|
1081
|
+
except KeyboardInterrupt:
|
|
1082
|
+
self._console.print("\n[dim]Generation cancelled.[/dim]")
|
|
1083
|
+
self._messages.pop()
|
|
1084
|
+
return
|
|
1085
|
+
|
|
1086
|
+
elapsed_ms = int((time.monotonic() - start_time) * 1000)
|
|
1087
|
+
|
|
1088
|
+
if full_response:
|
|
1089
|
+
self._messages.append(ChatMessage(role="assistant", content=full_response))
|
|
1090
|
+
|
|
1091
|
+
self._total_cost_dt += total_cost
|
|
1092
|
+
self._total_tokens_in += total_tokens_in
|
|
1093
|
+
self._total_tokens_out += total_tokens_out
|
|
1094
|
+
|
|
1095
|
+
if self._session_mgr and self._session_id:
|
|
1096
|
+
model_used = self._forced_model or "auto"
|
|
1097
|
+
self._session_mgr.add_message(
|
|
1098
|
+
self._session_id,
|
|
1099
|
+
"assistant",
|
|
1100
|
+
full_response,
|
|
1101
|
+
model=model_used,
|
|
1102
|
+
tokens_in=total_tokens_in,
|
|
1103
|
+
tokens_out=total_tokens_out,
|
|
1104
|
+
cost_dt=total_cost,
|
|
1105
|
+
)
|
|
1106
|
+
if len(self._messages) == 2:
|
|
1107
|
+
title = self._messages[0].content[:60]
|
|
1108
|
+
if len(self._messages[0].content) > 60:
|
|
1109
|
+
title += "..."
|
|
1110
|
+
self._session_mgr.update_title(self._session_id, title)
|
|
1111
|
+
|
|
1112
|
+
if self._audit:
|
|
1113
|
+
self._audit.log_model_call(
|
|
1114
|
+
model=self._forced_model or "auto",
|
|
1115
|
+
tokens_in=total_tokens_in,
|
|
1116
|
+
tokens_out=total_tokens_out,
|
|
1117
|
+
cost_dt=total_cost,
|
|
1118
|
+
session_id=self._session_id or "",
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
if interactive:
|
|
1122
|
+
if not full_response.endswith("\n"):
|
|
1123
|
+
self._console.print()
|
|
1124
|
+
self._console.print(
|
|
1125
|
+
f"[dim]{total_tokens_in + total_tokens_out:,} tokens | {elapsed_ms}ms | "
|
|
1126
|
+
f"{total_cost:.1f} DT[/dim]\n"
|
|
1127
|
+
)
|
|
1128
|
+
elif not full_response.endswith("\n"):
|
|
1129
|
+
sys.stdout.write("\n")
|
|
1130
|
+
|
|
1131
|
+
async def _send_direct(self, user_input: str, interactive: bool) -> None:
|
|
1132
|
+
logger.debug(f"Entered into _send_direct: interactive={interactive}")
|
|
1133
|
+
decision = self._router.route(user_input, mode=self._mode)
|
|
1134
|
+
|
|
1135
|
+
if interactive:
|
|
1136
|
+
self._console.print(f"\n[dim]Model: {decision.model} ({decision.reason})[/dim]")
|
|
1137
|
+
|
|
1138
|
+
full_response = ""
|
|
1139
|
+
tokens_in = 0
|
|
1140
|
+
tokens_out = 0
|
|
1141
|
+
start_time = time.monotonic()
|
|
1142
|
+
|
|
1143
|
+
try:
|
|
1144
|
+
if interactive:
|
|
1145
|
+
self._console.print()
|
|
1146
|
+
with Live(
|
|
1147
|
+
Text("...", style="cyan"),
|
|
1148
|
+
console=self._console,
|
|
1149
|
+
refresh_per_second=15,
|
|
1150
|
+
transient=True,
|
|
1151
|
+
) as live:
|
|
1152
|
+
async for event in self._client.chat_completion_stream(
|
|
1153
|
+
messages=self._messages,
|
|
1154
|
+
model=decision.model,
|
|
1155
|
+
):
|
|
1156
|
+
if event.event_type == "content":
|
|
1157
|
+
full_response += event.data
|
|
1158
|
+
try:
|
|
1159
|
+
live.update(Markdown(full_response + " |"))
|
|
1160
|
+
except Exception:
|
|
1161
|
+
live.update(Text(full_response + " |"))
|
|
1162
|
+
elif event.event_type == "usage" and isinstance(event.data, dict):
|
|
1163
|
+
tokens_in = event.data.get("prompt_tokens", 0)
|
|
1164
|
+
tokens_out = event.data.get("completion_tokens", 0)
|
|
1165
|
+
elif event.event_type == "error":
|
|
1166
|
+
msg = (
|
|
1167
|
+
event.data.get("message", str(event.data))
|
|
1168
|
+
if isinstance(event.data, dict)
|
|
1169
|
+
else str(event.data)
|
|
1170
|
+
)
|
|
1171
|
+
live.update(Text(f"Error: {msg}", style="red"))
|
|
1172
|
+
self._messages.pop()
|
|
1173
|
+
return
|
|
1174
|
+
|
|
1175
|
+
if full_response:
|
|
1176
|
+
self._console.print(Markdown(full_response))
|
|
1177
|
+
else:
|
|
1178
|
+
async for event in self._client.chat_completion_stream(
|
|
1179
|
+
messages=self._messages,
|
|
1180
|
+
model=decision.model,
|
|
1181
|
+
):
|
|
1182
|
+
if event.event_type == "content":
|
|
1183
|
+
full_response += event.data
|
|
1184
|
+
sys.stdout.write(event.data)
|
|
1185
|
+
sys.stdout.flush()
|
|
1186
|
+
elif event.event_type == "usage" and isinstance(event.data, dict):
|
|
1187
|
+
tokens_in = event.data.get("prompt_tokens", 0)
|
|
1188
|
+
tokens_out = event.data.get("completion_tokens", 0)
|
|
1189
|
+
elif event.event_type == "error":
|
|
1190
|
+
msg = (
|
|
1191
|
+
event.data.get("message", str(event.data))
|
|
1192
|
+
if isinstance(event.data, dict)
|
|
1193
|
+
else str(event.data)
|
|
1194
|
+
)
|
|
1195
|
+
sys.stderr.write(f"\nError: {msg}\n")
|
|
1196
|
+
self._messages.pop()
|
|
1197
|
+
return
|
|
1198
|
+
if full_response:
|
|
1199
|
+
sys.stdout.write("\n")
|
|
1200
|
+
|
|
1201
|
+
except KeyboardInterrupt:
|
|
1202
|
+
self._console.print("\n[dim]Generation cancelled.[/dim]")
|
|
1203
|
+
self._messages.pop()
|
|
1204
|
+
return
|
|
1205
|
+
|
|
1206
|
+
elapsed_ms = int((time.monotonic() - start_time) * 1000)
|
|
1207
|
+
|
|
1208
|
+
if full_response:
|
|
1209
|
+
self._messages.append(ChatMessage(role="assistant", content=full_response))
|
|
1210
|
+
|
|
1211
|
+
cost_dt = tokens_in * 0.001 + tokens_out * 0.002
|
|
1212
|
+
self._total_cost_dt += cost_dt
|
|
1213
|
+
self._total_tokens_in += tokens_in
|
|
1214
|
+
self._total_tokens_out += tokens_out
|
|
1215
|
+
|
|
1216
|
+
if self._session_mgr and self._session_id:
|
|
1217
|
+
self._session_mgr.add_message(
|
|
1218
|
+
self._session_id,
|
|
1219
|
+
"assistant",
|
|
1220
|
+
full_response,
|
|
1221
|
+
model=decision.model,
|
|
1222
|
+
tokens_in=tokens_in,
|
|
1223
|
+
tokens_out=tokens_out,
|
|
1224
|
+
cost_dt=cost_dt,
|
|
1225
|
+
)
|
|
1226
|
+
if len(self._messages) == 2:
|
|
1227
|
+
title = user_input[:60] + ("..." if len(user_input) > 60 else "")
|
|
1228
|
+
self._session_mgr.update_title(self._session_id, title)
|
|
1229
|
+
|
|
1230
|
+
if interactive:
|
|
1231
|
+
self._console.print(
|
|
1232
|
+
f"\n[dim]{tokens_in + tokens_out:,} tokens | {elapsed_ms}ms | "
|
|
1233
|
+
f"{cost_dt:.1f} DT[/dim]\n"
|
|
1234
|
+
)
|