aloop 0.1.1__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.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/prompts/__init__.py +1 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.1.dist-info/METADATA +252 -0
- aloop-0.1.1.dist-info/RECORD +66 -0
- aloop-0.1.1.dist-info/WHEEL +5 -0
- aloop-0.1.1.dist-info/entry_points.txt +2 -0
- aloop-0.1.1.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.1.dist-info/top_level.txt +9 -0
- cli.py +19 -0
- config.py +146 -0
- interactive.py +865 -0
- llm/__init__.py +51 -0
- llm/base.py +26 -0
- llm/compat.py +226 -0
- llm/content_utils.py +309 -0
- llm/litellm_adapter.py +450 -0
- llm/message_types.py +245 -0
- llm/model_manager.py +265 -0
- llm/retry.py +95 -0
- main.py +246 -0
- memory/__init__.py +20 -0
- memory/compressor.py +554 -0
- memory/manager.py +538 -0
- memory/serialization.py +82 -0
- memory/short_term.py +88 -0
- memory/store/__init__.py +6 -0
- memory/store/memory_store.py +100 -0
- memory/store/yaml_file_memory_store.py +414 -0
- memory/token_tracker.py +203 -0
- memory/types.py +51 -0
- tools/__init__.py +6 -0
- tools/advanced_file_ops.py +557 -0
- tools/base.py +51 -0
- tools/calculator.py +50 -0
- tools/code_navigator.py +975 -0
- tools/explore.py +254 -0
- tools/file_ops.py +150 -0
- tools/git_tools.py +791 -0
- tools/notify.py +69 -0
- tools/parallel_execute.py +420 -0
- tools/session_manager.py +205 -0
- tools/shell.py +147 -0
- tools/shell_background.py +470 -0
- tools/smart_edit.py +491 -0
- tools/todo.py +130 -0
- tools/web_fetch.py +673 -0
- tools/web_search.py +61 -0
- utils/__init__.py +15 -0
- utils/logger.py +105 -0
- utils/model_pricing.py +49 -0
- utils/runtime.py +75 -0
- utils/terminal_ui.py +422 -0
- utils/tui/__init__.py +39 -0
- utils/tui/command_registry.py +49 -0
- utils/tui/components.py +306 -0
- utils/tui/input_handler.py +393 -0
- utils/tui/model_ui.py +204 -0
- utils/tui/progress.py +292 -0
- utils/tui/status_bar.py +178 -0
- utils/tui/theme.py +165 -0
interactive.py
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
"""Interactive multi-turn conversation mode for the agent."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import shlex
|
|
5
|
+
import signal
|
|
6
|
+
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from config import Config
|
|
10
|
+
from llm import ModelManager
|
|
11
|
+
from memory import MemoryManager
|
|
12
|
+
from utils import get_log_file_path, terminal_ui
|
|
13
|
+
from utils.runtime import get_history_file
|
|
14
|
+
from utils.tui.command_registry import CommandRegistry, CommandSpec
|
|
15
|
+
from utils.tui.input_handler import InputHandler
|
|
16
|
+
from utils.tui.model_ui import (
|
|
17
|
+
mask_secret,
|
|
18
|
+
open_config_and_wait_for_save,
|
|
19
|
+
parse_kv_args,
|
|
20
|
+
pick_model_id,
|
|
21
|
+
)
|
|
22
|
+
from utils.tui.status_bar import StatusBar
|
|
23
|
+
from utils.tui.theme import Theme, set_theme
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InteractiveSession:
|
|
27
|
+
"""Manages an interactive conversation session with the agent."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, agent):
|
|
30
|
+
"""Initialize interactive session.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
agent: The agent instance
|
|
34
|
+
"""
|
|
35
|
+
self.agent = agent
|
|
36
|
+
self.conversation_count = 0
|
|
37
|
+
self.show_thinking = Config.TUI_SHOW_THINKING
|
|
38
|
+
|
|
39
|
+
# Use the agent's model manager to avoid divergence
|
|
40
|
+
self.model_manager = getattr(agent, "model_manager", None) or ModelManager()
|
|
41
|
+
|
|
42
|
+
# Track current task for interruption support
|
|
43
|
+
self.current_task = None
|
|
44
|
+
|
|
45
|
+
# Initialize TUI components
|
|
46
|
+
self.command_registry = CommandRegistry(
|
|
47
|
+
commands=[
|
|
48
|
+
CommandSpec("help", "Show this help message"),
|
|
49
|
+
CommandSpec("reset", "Clear conversation memory and start fresh"),
|
|
50
|
+
CommandSpec("stats", "Show memory and token usage statistics"),
|
|
51
|
+
CommandSpec(
|
|
52
|
+
"resume",
|
|
53
|
+
"List and resume a previous session",
|
|
54
|
+
args_hint="[session_id]",
|
|
55
|
+
),
|
|
56
|
+
CommandSpec("theme", "Toggle between dark and light theme"),
|
|
57
|
+
CommandSpec("verbose", "Toggle verbose thinking display"),
|
|
58
|
+
CommandSpec("compact", "Compress conversation memory"),
|
|
59
|
+
CommandSpec(
|
|
60
|
+
"model",
|
|
61
|
+
"Manage models",
|
|
62
|
+
subcommands={
|
|
63
|
+
"edit": CommandSpec(
|
|
64
|
+
"edit",
|
|
65
|
+
"Edit `.aloop/models.yaml` (auto-reload on save)",
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
),
|
|
69
|
+
CommandSpec("exit", "Exit interactive mode"),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
self.input_handler = InputHandler(
|
|
73
|
+
history_file=get_history_file(),
|
|
74
|
+
command_registry=self.command_registry,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Set up keyboard shortcut callbacks
|
|
78
|
+
self.input_handler.set_callbacks(
|
|
79
|
+
on_clear_screen=self._on_clear_screen,
|
|
80
|
+
on_toggle_thinking=self._on_toggle_thinking,
|
|
81
|
+
on_show_stats=self._on_show_stats,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Initialize status bar
|
|
85
|
+
self.status_bar = StatusBar(terminal_ui.console)
|
|
86
|
+
self.status_bar.update(mode="LOOP")
|
|
87
|
+
|
|
88
|
+
# Set up signal handler for graceful interruption
|
|
89
|
+
self._setup_signal_handler()
|
|
90
|
+
|
|
91
|
+
def _setup_signal_handler(self) -> None:
|
|
92
|
+
"""Set up SIGINT handler for graceful task interruption."""
|
|
93
|
+
|
|
94
|
+
def handle_sigint(sig, frame):
|
|
95
|
+
"""Handle SIGINT (Ctrl+C) by canceling the current task."""
|
|
96
|
+
if self.current_task and not self.current_task.done():
|
|
97
|
+
colors = Theme.get_colors()
|
|
98
|
+
terminal_ui.console.print(
|
|
99
|
+
f"\n[bold {colors.warning}]Interrupting...[/bold {colors.warning}]"
|
|
100
|
+
)
|
|
101
|
+
self.current_task.cancel()
|
|
102
|
+
# Don't raise - let the asyncio.CancelledError propagate
|
|
103
|
+
|
|
104
|
+
signal.signal(signal.SIGINT, handle_sigint)
|
|
105
|
+
|
|
106
|
+
def _on_clear_screen(self) -> None:
|
|
107
|
+
"""Handle Ctrl+L - clear screen."""
|
|
108
|
+
terminal_ui.console.clear()
|
|
109
|
+
|
|
110
|
+
def _on_toggle_thinking(self) -> None:
|
|
111
|
+
"""Handle Ctrl+T - toggle thinking display."""
|
|
112
|
+
self.show_thinking = not self.show_thinking
|
|
113
|
+
status = "enabled" if self.show_thinking else "disabled"
|
|
114
|
+
terminal_ui.print_info(f"Thinking display {status}")
|
|
115
|
+
|
|
116
|
+
def _on_show_stats(self) -> None:
|
|
117
|
+
"""Handle Ctrl+S - show quick stats."""
|
|
118
|
+
self._show_stats()
|
|
119
|
+
|
|
120
|
+
def _show_help(self) -> None:
|
|
121
|
+
"""Display help message with available commands."""
|
|
122
|
+
colors = Theme.get_colors()
|
|
123
|
+
terminal_ui.console.print(
|
|
124
|
+
f"\n[bold {colors.primary}]Available Commands:[/bold {colors.primary}]"
|
|
125
|
+
)
|
|
126
|
+
for cmd in self.command_registry.commands:
|
|
127
|
+
terminal_ui.console.print(
|
|
128
|
+
f" [{colors.primary}]{cmd.display}[/{colors.primary}] - {cmd.description}"
|
|
129
|
+
)
|
|
130
|
+
if cmd.subcommands:
|
|
131
|
+
for sub_name, sub in cmd.subcommands.items():
|
|
132
|
+
extra = f" {sub.args_hint}" if sub.args_hint else ""
|
|
133
|
+
terminal_ui.console.print(
|
|
134
|
+
f" [{colors.text_muted}]/{cmd.name} {sub_name}{extra} - {sub.description}[/{colors.text_muted}]"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
terminal_ui.console.print(
|
|
138
|
+
f"\n[bold {colors.primary}]Keyboard Shortcuts:[/bold {colors.primary}]"
|
|
139
|
+
)
|
|
140
|
+
terminal_ui.console.print(
|
|
141
|
+
f" [{colors.secondary}]/[/{colors.secondary}] - Show command suggestions"
|
|
142
|
+
)
|
|
143
|
+
terminal_ui.console.print(
|
|
144
|
+
f" [{colors.secondary}]Ctrl+C[/{colors.secondary}] - Cancel current operation"
|
|
145
|
+
)
|
|
146
|
+
terminal_ui.console.print(
|
|
147
|
+
f" [{colors.secondary}]Ctrl+L[/{colors.secondary}] - Clear screen"
|
|
148
|
+
)
|
|
149
|
+
terminal_ui.console.print(
|
|
150
|
+
f" [{colors.secondary}]Ctrl+T[/{colors.secondary}] - Toggle thinking display"
|
|
151
|
+
)
|
|
152
|
+
terminal_ui.console.print(
|
|
153
|
+
f" [{colors.secondary}]Ctrl+S[/{colors.secondary}] - Show quick stats"
|
|
154
|
+
)
|
|
155
|
+
terminal_ui.console.print(
|
|
156
|
+
f" [{colors.secondary}]Up/Down[/{colors.secondary}] - Navigate command history\n"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _show_stats(self) -> None:
|
|
160
|
+
"""Display current memory and token statistics."""
|
|
161
|
+
terminal_ui.console.print()
|
|
162
|
+
stats = self.agent.memory.get_stats()
|
|
163
|
+
terminal_ui.print_memory_stats(stats)
|
|
164
|
+
terminal_ui.console.print()
|
|
165
|
+
|
|
166
|
+
async def _resume_session(self, session_id: str | None = None) -> None:
|
|
167
|
+
"""Resume a previous session.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
session_id: Optional session ID or prefix. If None, shows recent sessions.
|
|
171
|
+
"""
|
|
172
|
+
colors = Theme.get_colors()
|
|
173
|
+
try:
|
|
174
|
+
if session_id is None:
|
|
175
|
+
# Show recent sessions for user to pick
|
|
176
|
+
sessions = await MemoryManager.list_sessions(limit=10)
|
|
177
|
+
if not sessions:
|
|
178
|
+
terminal_ui.console.print(
|
|
179
|
+
f"\n[{colors.warning}]No saved sessions found.[/{colors.warning}]\n"
|
|
180
|
+
)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
terminal_ui.console.print(
|
|
184
|
+
f"\n[bold {colors.primary}]Recent Sessions:[/bold {colors.primary}]\n"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
table = Table(show_header=True, header_style=f"bold {colors.primary}", box=None)
|
|
188
|
+
table.add_column("#", style=colors.text_muted, width=4)
|
|
189
|
+
table.add_column("ID", style=colors.text_muted, width=38)
|
|
190
|
+
table.add_column("Updated", width=20)
|
|
191
|
+
table.add_column("Msgs", justify="right", width=6)
|
|
192
|
+
table.add_column("Preview", width=50)
|
|
193
|
+
|
|
194
|
+
for i, session in enumerate(sessions, 1):
|
|
195
|
+
sid = session["id"]
|
|
196
|
+
updated = session.get("updated_at", session.get("created_at", ""))[:19]
|
|
197
|
+
msg_count = str(session["message_count"])
|
|
198
|
+
preview = session.get("preview", "")[:50]
|
|
199
|
+
table.add_row(str(i), sid, updated, msg_count, preview)
|
|
200
|
+
|
|
201
|
+
terminal_ui.console.print(table)
|
|
202
|
+
terminal_ui.console.print(
|
|
203
|
+
f"\n[{colors.text_muted}]Usage: /resume <session_id or prefix>[/{colors.text_muted}]\n"
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# Resolve session ID (prefix match)
|
|
208
|
+
resolved_id = await MemoryManager.find_session_by_prefix(session_id)
|
|
209
|
+
if not resolved_id:
|
|
210
|
+
terminal_ui.print_error(f"Session '{session_id}' not found")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Load session via agent (agent owns memory lifecycle)
|
|
214
|
+
await self.agent.load_session(resolved_id)
|
|
215
|
+
|
|
216
|
+
msg_count = self.agent.memory.short_term.count()
|
|
217
|
+
terminal_ui.print_success(
|
|
218
|
+
f"Resumed session {resolved_id} ({msg_count} messages, "
|
|
219
|
+
f"{self.agent.memory.current_tokens} tokens)"
|
|
220
|
+
)
|
|
221
|
+
terminal_ui.console.print()
|
|
222
|
+
|
|
223
|
+
self._print_session_history()
|
|
224
|
+
self._update_status_bar()
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
terminal_ui.print_error(str(e), title="Error resuming session")
|
|
228
|
+
|
|
229
|
+
def _print_session_history(self) -> None:
|
|
230
|
+
"""Print conversation history from a resumed session."""
|
|
231
|
+
messages = self.agent.memory.short_term.get_messages()
|
|
232
|
+
if not messages:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
colors = Theme.get_colors()
|
|
236
|
+
terminal_ui.console.print(
|
|
237
|
+
f"[bold {colors.primary}]Session History:[/bold {colors.primary}]"
|
|
238
|
+
)
|
|
239
|
+
terminal_ui.console.print(f"[{colors.text_muted}]{'─' * 60}[/{colors.text_muted}]")
|
|
240
|
+
|
|
241
|
+
for msg in messages:
|
|
242
|
+
if msg.role == "user":
|
|
243
|
+
content = str(msg.content or "")
|
|
244
|
+
if len(content) > 200:
|
|
245
|
+
content = content[:200] + "..."
|
|
246
|
+
terminal_ui.console.print(
|
|
247
|
+
f"\n[bold {colors.primary}]You:[/bold {colors.primary}] {content}"
|
|
248
|
+
)
|
|
249
|
+
elif msg.role == "assistant" and msg.content:
|
|
250
|
+
content = str(msg.content)
|
|
251
|
+
if len(content) > 300:
|
|
252
|
+
content = content[:300] + "..."
|
|
253
|
+
terminal_ui.console.print(
|
|
254
|
+
f"[bold {colors.secondary}]Assistant:[/bold {colors.secondary}] {content}"
|
|
255
|
+
)
|
|
256
|
+
elif msg.role == "assistant" and msg.tool_calls:
|
|
257
|
+
tool_names = ", ".join(
|
|
258
|
+
(
|
|
259
|
+
tc.get("function", {}).get("name", "?")
|
|
260
|
+
if isinstance(tc, dict)
|
|
261
|
+
else getattr(getattr(tc, "function", None), "name", "?")
|
|
262
|
+
)
|
|
263
|
+
for tc in msg.tool_calls
|
|
264
|
+
)
|
|
265
|
+
terminal_ui.console.print(
|
|
266
|
+
f"[{colors.text_muted}] (used tools: {tool_names})[/{colors.text_muted}]"
|
|
267
|
+
)
|
|
268
|
+
# Skip tool result messages — they are verbose
|
|
269
|
+
|
|
270
|
+
terminal_ui.console.print(f"\n[{colors.text_muted}]{'─' * 60}[/{colors.text_muted}]\n")
|
|
271
|
+
|
|
272
|
+
def _toggle_theme(self) -> None:
|
|
273
|
+
"""Toggle between dark and light theme."""
|
|
274
|
+
current = Theme.get_theme_name()
|
|
275
|
+
new_theme = "light" if current == "dark" else "dark"
|
|
276
|
+
set_theme(new_theme)
|
|
277
|
+
terminal_ui.print_success(f"Switched to {new_theme} theme")
|
|
278
|
+
|
|
279
|
+
def _toggle_verbose(self) -> None:
|
|
280
|
+
"""Toggle verbose thinking display."""
|
|
281
|
+
self.show_thinking = not self.show_thinking
|
|
282
|
+
status = "enabled" if self.show_thinking else "disabled"
|
|
283
|
+
terminal_ui.print_info(f"Verbose thinking display {status}")
|
|
284
|
+
|
|
285
|
+
async def _compact_memory(self) -> None:
|
|
286
|
+
"""Compress conversation memory."""
|
|
287
|
+
result = await self.agent.memory.compress()
|
|
288
|
+
if result is None:
|
|
289
|
+
terminal_ui.print_info("Nothing to compress.")
|
|
290
|
+
else:
|
|
291
|
+
terminal_ui.print_success(
|
|
292
|
+
f"Compressed {result.original_message_count} messages: "
|
|
293
|
+
f"{result.original_tokens} → {result.compressed_tokens} tokens "
|
|
294
|
+
f"({result.savings_percentage:.0f}% saved)"
|
|
295
|
+
)
|
|
296
|
+
self._update_status_bar()
|
|
297
|
+
|
|
298
|
+
def _update_status_bar(self) -> None:
|
|
299
|
+
"""Update status bar with current stats."""
|
|
300
|
+
stats = self.agent.memory.get_stats()
|
|
301
|
+
model_info = self.agent.get_current_model_info()
|
|
302
|
+
model_name = model_info["name"] if model_info else ""
|
|
303
|
+
self.status_bar.update(
|
|
304
|
+
input_tokens=stats.get("total_input_tokens", 0),
|
|
305
|
+
output_tokens=stats.get("total_output_tokens", 0),
|
|
306
|
+
context_tokens=stats.get("current_tokens", 0),
|
|
307
|
+
cost=stats.get("total_cost", 0),
|
|
308
|
+
model_name=model_name,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
async def _handle_command(self, user_input: str) -> bool:
|
|
312
|
+
"""Handle a slash command.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
user_input: User input starting with /
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
True if should continue loop, False if should exit
|
|
319
|
+
"""
|
|
320
|
+
command_parts = user_input.split()
|
|
321
|
+
command = command_parts[0].lower()
|
|
322
|
+
|
|
323
|
+
if command in ("/exit", "/quit"):
|
|
324
|
+
colors = Theme.get_colors()
|
|
325
|
+
terminal_ui.console.print(
|
|
326
|
+
f"\n[bold {colors.warning}]Exiting interactive mode. Goodbye![/bold {colors.warning}]"
|
|
327
|
+
)
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
elif command == "/help":
|
|
331
|
+
self._show_help()
|
|
332
|
+
|
|
333
|
+
elif command == "/reset":
|
|
334
|
+
self.agent.memory.reset()
|
|
335
|
+
self.conversation_count = 0
|
|
336
|
+
self._update_status_bar()
|
|
337
|
+
terminal_ui.print_success("Memory cleared. Starting fresh conversation.")
|
|
338
|
+
terminal_ui.console.print()
|
|
339
|
+
|
|
340
|
+
elif command == "/stats":
|
|
341
|
+
self._show_stats()
|
|
342
|
+
|
|
343
|
+
elif command == "/resume":
|
|
344
|
+
session_id = command_parts[1] if len(command_parts) >= 2 else None
|
|
345
|
+
await self._resume_session(session_id)
|
|
346
|
+
|
|
347
|
+
elif command == "/theme":
|
|
348
|
+
self._toggle_theme()
|
|
349
|
+
|
|
350
|
+
elif command == "/verbose":
|
|
351
|
+
self._toggle_verbose()
|
|
352
|
+
|
|
353
|
+
elif command == "/compact":
|
|
354
|
+
await self._compact_memory()
|
|
355
|
+
|
|
356
|
+
elif command == "/model":
|
|
357
|
+
await self._handle_model_command(user_input)
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
colors = Theme.get_colors()
|
|
361
|
+
terminal_ui.console.print(
|
|
362
|
+
f"[bold {colors.error}]Unknown command: {command}[/bold {colors.error}]"
|
|
363
|
+
)
|
|
364
|
+
terminal_ui.console.print(
|
|
365
|
+
f"[{colors.text_muted}]Type /help to see available commands[/{colors.text_muted}]\n"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return True
|
|
369
|
+
|
|
370
|
+
def _show_models(self) -> None:
|
|
371
|
+
"""Display available models and current selection."""
|
|
372
|
+
colors = Theme.get_colors()
|
|
373
|
+
profiles = self.model_manager.list_models()
|
|
374
|
+
current = self.model_manager.get_current_model()
|
|
375
|
+
default_model_id = self.model_manager.get_default_model_id()
|
|
376
|
+
|
|
377
|
+
terminal_ui.console.print(
|
|
378
|
+
f"\n[bold {colors.primary}]Available Models:[/bold {colors.primary}]\n"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if not profiles:
|
|
382
|
+
terminal_ui.print_error("No models configured.")
|
|
383
|
+
terminal_ui.console.print(
|
|
384
|
+
f"[{colors.text_muted}]Use /model edit (recommended) or edit `.aloop/models.yaml` manually.[/{colors.text_muted}]\n"
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
for i, profile in enumerate(profiles, start=1):
|
|
389
|
+
markers: list[str] = []
|
|
390
|
+
if current and profile.model_id == current.model_id:
|
|
391
|
+
markers.append(f"[{colors.success}]CURRENT[/{colors.success}]")
|
|
392
|
+
if default_model_id and profile.model_id == default_model_id:
|
|
393
|
+
markers.append(f"[{colors.primary}]DEFAULT[/{colors.primary}]")
|
|
394
|
+
marker = (
|
|
395
|
+
" ".join(markers)
|
|
396
|
+
if markers
|
|
397
|
+
else f"[{colors.text_muted}] [/{colors.text_muted}]"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
terminal_ui.console.print(
|
|
401
|
+
f" {marker} [{colors.text_muted}]{i:>2}[/] {profile.model_id}"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
terminal_ui.console.print(
|
|
405
|
+
f"\n[{colors.text_muted}]Tip: run /model to pick; /model edit to change config.[/]\n"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def _switch_model(self, model_id: str) -> None:
|
|
409
|
+
"""Switch to a different model.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
model_id: LiteLLM model ID to switch to
|
|
413
|
+
"""
|
|
414
|
+
colors = Theme.get_colors()
|
|
415
|
+
|
|
416
|
+
# Validate the profile
|
|
417
|
+
profile = self.model_manager.get_model(model_id)
|
|
418
|
+
if profile is None:
|
|
419
|
+
terminal_ui.print_error(f"Model '{model_id}' not found")
|
|
420
|
+
available = ", ".join(self.model_manager.get_model_ids())
|
|
421
|
+
if available:
|
|
422
|
+
terminal_ui.console.print(
|
|
423
|
+
f"[{colors.text_muted}]Available: {available}[/{colors.text_muted}]\n"
|
|
424
|
+
)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
is_valid, error_msg = self.model_manager.validate_model(profile)
|
|
428
|
+
if not is_valid:
|
|
429
|
+
terminal_ui.print_error(error_msg)
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
# Perform the switch
|
|
433
|
+
if self.agent.switch_model(model_id):
|
|
434
|
+
new_profile = self.model_manager.get_current_model()
|
|
435
|
+
if new_profile:
|
|
436
|
+
terminal_ui.print_success(f"Switched to model: {new_profile.model_id}")
|
|
437
|
+
self._update_status_bar()
|
|
438
|
+
else:
|
|
439
|
+
terminal_ui.print_error("Failed to get current model after switch")
|
|
440
|
+
else:
|
|
441
|
+
terminal_ui.print_error(f"Failed to switch to model '{model_id}'")
|
|
442
|
+
|
|
443
|
+
def _parse_kv_args(self, tokens: list[str]) -> tuple[dict[str, str], list[str]]:
|
|
444
|
+
return parse_kv_args(tokens)
|
|
445
|
+
|
|
446
|
+
def _mask_secret(self, value: str | None) -> str:
|
|
447
|
+
return mask_secret(value)
|
|
448
|
+
|
|
449
|
+
async def _handle_model_command(self, user_input: str) -> None:
|
|
450
|
+
"""Handle the /model command.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
user_input: Full user input string
|
|
454
|
+
"""
|
|
455
|
+
colors = Theme.get_colors()
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
parts = shlex.split(user_input)
|
|
459
|
+
except ValueError as e:
|
|
460
|
+
terminal_ui.print_error(str(e), title="Invalid /model command")
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
if len(parts) == 1:
|
|
464
|
+
if not self.model_manager.list_models():
|
|
465
|
+
terminal_ui.print_error("No models configured yet.")
|
|
466
|
+
terminal_ui.console.print(
|
|
467
|
+
f"[{colors.text_muted}]Run /model edit to configure `.aloop/models.yaml`.[/{colors.text_muted}]\n"
|
|
468
|
+
)
|
|
469
|
+
return
|
|
470
|
+
picked = await pick_model_id(self.model_manager, title="Select Model")
|
|
471
|
+
if picked:
|
|
472
|
+
self._switch_model(picked)
|
|
473
|
+
return
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
sub = parts[1]
|
|
477
|
+
|
|
478
|
+
if sub == "edit":
|
|
479
|
+
if len(parts) != 2:
|
|
480
|
+
terminal_ui.print_error("Usage: /model edit")
|
|
481
|
+
terminal_ui.console.print(
|
|
482
|
+
f"[{colors.text_muted}]Edit the YAML directly instead of using subcommands.[/{colors.text_muted}]\n"
|
|
483
|
+
)
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
terminal_ui.console.print(
|
|
487
|
+
f"[{colors.text_muted}]Save the file to auto-reload (Ctrl+C to cancel)...[/]\n"
|
|
488
|
+
)
|
|
489
|
+
ok = await open_config_and_wait_for_save(self.model_manager.config_path)
|
|
490
|
+
if not ok:
|
|
491
|
+
terminal_ui.print_error(
|
|
492
|
+
f"Could not open editor. Please edit `{self.model_manager.config_path}` manually."
|
|
493
|
+
)
|
|
494
|
+
terminal_ui.console.print(
|
|
495
|
+
f"[{colors.text_muted}]Tip: set EDITOR='code' (or similar) for /model edit.[/{colors.text_muted}]\n"
|
|
496
|
+
)
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
self.model_manager.reload()
|
|
500
|
+
terminal_ui.print_success("Reloaded `.aloop/models.yaml`")
|
|
501
|
+
current_after = self.model_manager.get_current_model()
|
|
502
|
+
if not current_after:
|
|
503
|
+
terminal_ui.print_error(
|
|
504
|
+
"No models configured after reload. Edit `.aloop/models.yaml` and set `default`."
|
|
505
|
+
)
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# Reinitialize LLM adapter to pick up updated api_key/api_base/timeout/drop_params.
|
|
509
|
+
self.agent.switch_model(current_after.model_id)
|
|
510
|
+
terminal_ui.print_info(f"Reload applied (current: {current_after.model_id}).")
|
|
511
|
+
return
|
|
512
|
+
terminal_ui.print_error("Unknown /model command.")
|
|
513
|
+
terminal_ui.console.print(
|
|
514
|
+
f"[{colors.text_muted}]Use /model to pick, or /model edit to configure.[/{colors.text_muted}]\n"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
async def run(self) -> None:
|
|
518
|
+
"""Run the interactive session loop."""
|
|
519
|
+
# Print header
|
|
520
|
+
terminal_ui.print_banner()
|
|
521
|
+
|
|
522
|
+
# Display configuration
|
|
523
|
+
current = self.model_manager.get_current_model()
|
|
524
|
+
config_dict = {
|
|
525
|
+
"Model": current.model_id if current else "NOT CONFIGURED",
|
|
526
|
+
"Theme": Theme.get_theme_name(),
|
|
527
|
+
"Commands": "/help for all commands",
|
|
528
|
+
}
|
|
529
|
+
terminal_ui.print_config(config_dict)
|
|
530
|
+
|
|
531
|
+
colors = Theme.get_colors()
|
|
532
|
+
|
|
533
|
+
# If session was loaded via --resume, print history
|
|
534
|
+
if self.agent.memory.short_term.count() > 0:
|
|
535
|
+
terminal_ui.print_info(
|
|
536
|
+
f"Resumed session: {self.agent.memory.session_id} "
|
|
537
|
+
f"({self.agent.memory.short_term.count()} messages)"
|
|
538
|
+
)
|
|
539
|
+
terminal_ui.console.print()
|
|
540
|
+
self._print_session_history()
|
|
541
|
+
|
|
542
|
+
terminal_ui.console.print(
|
|
543
|
+
f"[bold {colors.success}]Interactive mode started. Type your message or use commands.[/bold {colors.success}]"
|
|
544
|
+
)
|
|
545
|
+
terminal_ui.console.print(
|
|
546
|
+
f"[{colors.text_muted}]Tip: Type '/' for command suggestions, Ctrl+T to toggle thinking display[/{colors.text_muted}]\n"
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Show initial status bar
|
|
550
|
+
if Config.TUI_STATUS_BAR:
|
|
551
|
+
self.status_bar.show()
|
|
552
|
+
|
|
553
|
+
while True:
|
|
554
|
+
try:
|
|
555
|
+
# Get user input
|
|
556
|
+
user_input = await self.input_handler.prompt_async("> ")
|
|
557
|
+
|
|
558
|
+
# Handle empty input
|
|
559
|
+
if not user_input:
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
# Handle commands
|
|
563
|
+
if user_input.startswith("/"):
|
|
564
|
+
should_continue = await self._handle_command(user_input)
|
|
565
|
+
if not should_continue:
|
|
566
|
+
break
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
# Process user message
|
|
570
|
+
self.conversation_count += 1
|
|
571
|
+
|
|
572
|
+
# Show turn divider
|
|
573
|
+
terminal_ui.print_turn_divider(self.conversation_count)
|
|
574
|
+
|
|
575
|
+
# Echo user input in Claude Code style
|
|
576
|
+
terminal_ui.print_user_message(user_input)
|
|
577
|
+
|
|
578
|
+
# Update status bar to show processing
|
|
579
|
+
if Config.TUI_STATUS_BAR:
|
|
580
|
+
self.status_bar.update(is_processing=True)
|
|
581
|
+
|
|
582
|
+
try:
|
|
583
|
+
# Create a task for the agent run so it can be cancelled
|
|
584
|
+
self.current_task = asyncio.create_task(
|
|
585
|
+
self.agent.run(user_input, verify=False)
|
|
586
|
+
)
|
|
587
|
+
result = await self.current_task
|
|
588
|
+
self.current_task = None
|
|
589
|
+
|
|
590
|
+
# Display agent response
|
|
591
|
+
terminal_ui.console.print(
|
|
592
|
+
f"[bold {colors.secondary}]Assistant:[/bold {colors.secondary}]"
|
|
593
|
+
)
|
|
594
|
+
terminal_ui.print_assistant_message(result)
|
|
595
|
+
|
|
596
|
+
# Update status bar
|
|
597
|
+
self._update_status_bar()
|
|
598
|
+
if Config.TUI_STATUS_BAR:
|
|
599
|
+
self.status_bar.update(is_processing=False)
|
|
600
|
+
self.status_bar.show()
|
|
601
|
+
|
|
602
|
+
except asyncio.CancelledError:
|
|
603
|
+
terminal_ui.console.print(
|
|
604
|
+
f"\n[bold {colors.warning}]Task interrupted by user.[/bold {colors.warning}]\n"
|
|
605
|
+
)
|
|
606
|
+
if Config.TUI_STATUS_BAR:
|
|
607
|
+
self.status_bar.update(is_processing=False)
|
|
608
|
+
self.current_task = None
|
|
609
|
+
|
|
610
|
+
# Rollback incomplete exchange to prevent API errors on next turn
|
|
611
|
+
self.agent.memory.rollback_incomplete_exchange()
|
|
612
|
+
|
|
613
|
+
continue
|
|
614
|
+
except KeyboardInterrupt:
|
|
615
|
+
terminal_ui.console.print(
|
|
616
|
+
f"\n[bold {colors.warning}]Task interrupted by user.[/bold {colors.warning}]\n"
|
|
617
|
+
)
|
|
618
|
+
if Config.TUI_STATUS_BAR:
|
|
619
|
+
self.status_bar.update(is_processing=False)
|
|
620
|
+
self.current_task = None
|
|
621
|
+
continue
|
|
622
|
+
except Exception as e:
|
|
623
|
+
terminal_ui.print_error(str(e))
|
|
624
|
+
if Config.TUI_STATUS_BAR:
|
|
625
|
+
self.status_bar.update(is_processing=False)
|
|
626
|
+
continue
|
|
627
|
+
|
|
628
|
+
except KeyboardInterrupt:
|
|
629
|
+
terminal_ui.console.print(
|
|
630
|
+
f"\n\n[bold {colors.warning}]Interrupted. Type /exit to quit or continue chatting.[/bold {colors.warning}]\n"
|
|
631
|
+
)
|
|
632
|
+
continue
|
|
633
|
+
except EOFError:
|
|
634
|
+
terminal_ui.console.print(
|
|
635
|
+
f"\n[bold {colors.warning}]Exiting interactive mode. Goodbye![/bold {colors.warning}]"
|
|
636
|
+
)
|
|
637
|
+
break
|
|
638
|
+
|
|
639
|
+
# Show final statistics
|
|
640
|
+
terminal_ui.console.print(
|
|
641
|
+
f"\n[bold {colors.primary}]Final Session Statistics:[/bold {colors.primary}]"
|
|
642
|
+
)
|
|
643
|
+
stats = self.agent.memory.get_stats()
|
|
644
|
+
terminal_ui.print_memory_stats(stats)
|
|
645
|
+
|
|
646
|
+
# Show log file location
|
|
647
|
+
log_file = get_log_file_path()
|
|
648
|
+
if log_file:
|
|
649
|
+
terminal_ui.print_log_location(log_file)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class ModelSetupSession:
|
|
653
|
+
"""Lightweight interactive session for configuring models before the agent can run."""
|
|
654
|
+
|
|
655
|
+
def __init__(self, model_manager: ModelManager | None = None):
|
|
656
|
+
self.model_manager = model_manager or ModelManager()
|
|
657
|
+
self.command_registry = CommandRegistry(
|
|
658
|
+
commands=[
|
|
659
|
+
CommandSpec("help", "Show this help message"),
|
|
660
|
+
CommandSpec(
|
|
661
|
+
"model",
|
|
662
|
+
"Pick a model",
|
|
663
|
+
subcommands={
|
|
664
|
+
"edit": CommandSpec("edit", "Open `.aloop/models.yaml` in editor")
|
|
665
|
+
},
|
|
666
|
+
),
|
|
667
|
+
CommandSpec("exit", "Quit"),
|
|
668
|
+
]
|
|
669
|
+
)
|
|
670
|
+
self.input_handler = InputHandler(
|
|
671
|
+
history_file=get_history_file(),
|
|
672
|
+
command_registry=self.command_registry,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def _show_help(self) -> None:
|
|
676
|
+
colors = Theme.get_colors()
|
|
677
|
+
terminal_ui.console.print(
|
|
678
|
+
f"\n[bold {colors.primary}]Model Setup[/bold {colors.primary}] "
|
|
679
|
+
f"[{colors.text_muted}](edit `.aloop/models.yaml`)[/{colors.text_muted}]\n"
|
|
680
|
+
)
|
|
681
|
+
terminal_ui.console.print(f"[{colors.text_muted}]Commands:[/{colors.text_muted}]\n")
|
|
682
|
+
for cmd in self.command_registry.commands:
|
|
683
|
+
terminal_ui.console.print(
|
|
684
|
+
f" [{colors.primary}]{cmd.display}[/{colors.primary}] - {cmd.description}"
|
|
685
|
+
)
|
|
686
|
+
if cmd.subcommands:
|
|
687
|
+
for sub_name, sub in cmd.subcommands.items():
|
|
688
|
+
extra = f" {sub.args_hint}" if sub.args_hint else ""
|
|
689
|
+
terminal_ui.console.print(
|
|
690
|
+
f" [{colors.text_muted}]/{cmd.name} {sub_name}{extra} - {sub.description}[/{colors.text_muted}]"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
def _show_models(self) -> None:
|
|
694
|
+
colors = Theme.get_colors()
|
|
695
|
+
models = self.model_manager.list_models()
|
|
696
|
+
current = self.model_manager.get_current_model()
|
|
697
|
+
default_model_id = self.model_manager.get_default_model_id()
|
|
698
|
+
|
|
699
|
+
terminal_ui.console.print(
|
|
700
|
+
f"\n[bold {colors.primary}]Configured Models:[/bold {colors.primary}]\n"
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
if not models:
|
|
704
|
+
terminal_ui.print_error("No models configured yet.")
|
|
705
|
+
terminal_ui.console.print(
|
|
706
|
+
f"[{colors.text_muted}]Use /model edit to configure `.aloop/models.yaml`.[/{colors.text_muted}]\n"
|
|
707
|
+
)
|
|
708
|
+
return
|
|
709
|
+
|
|
710
|
+
for i, model in enumerate(models, start=1):
|
|
711
|
+
markers: list[str] = []
|
|
712
|
+
if current and model.model_id == current.model_id:
|
|
713
|
+
markers.append(f"[{colors.success}]CURRENT[/{colors.success}]")
|
|
714
|
+
if default_model_id and model.model_id == default_model_id:
|
|
715
|
+
markers.append(f"[{colors.primary}]DEFAULT[/{colors.primary}]")
|
|
716
|
+
marker = (
|
|
717
|
+
" ".join(markers)
|
|
718
|
+
if markers
|
|
719
|
+
else f"[{colors.text_muted}] [/{colors.text_muted}]"
|
|
720
|
+
)
|
|
721
|
+
terminal_ui.console.print(f" {marker} [{colors.text_muted}]{i:>2}[/] {model.model_id}")
|
|
722
|
+
|
|
723
|
+
terminal_ui.console.print()
|
|
724
|
+
terminal_ui.console.print(
|
|
725
|
+
f"[{colors.text_muted}]Tip: run /model to pick; /model edit to change config.[/]\n"
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def _parse_kv_args(self, tokens: list[str]) -> tuple[dict[str, str], list[str]]:
|
|
729
|
+
return parse_kv_args(tokens)
|
|
730
|
+
|
|
731
|
+
def _mask_secret(self, value: str | None) -> str:
|
|
732
|
+
return mask_secret(value)
|
|
733
|
+
|
|
734
|
+
async def _handle_model_command(self, user_input: str) -> bool:
|
|
735
|
+
colors = Theme.get_colors()
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
parts = shlex.split(user_input)
|
|
739
|
+
except ValueError as e:
|
|
740
|
+
terminal_ui.print_error(str(e), title="Invalid /model command")
|
|
741
|
+
return False
|
|
742
|
+
|
|
743
|
+
if len(parts) == 1:
|
|
744
|
+
if not self.model_manager.list_models():
|
|
745
|
+
terminal_ui.print_error("No models configured yet.")
|
|
746
|
+
terminal_ui.console.print(
|
|
747
|
+
f"[{colors.text_muted}]Run /model edit to configure `.aloop/models.yaml`.[/{colors.text_muted}]\n"
|
|
748
|
+
)
|
|
749
|
+
return False
|
|
750
|
+
picked = await pick_model_id(self.model_manager, title="Select Model")
|
|
751
|
+
if picked:
|
|
752
|
+
self.model_manager.set_default(picked)
|
|
753
|
+
self.model_manager.switch_model(picked)
|
|
754
|
+
terminal_ui.print_success(f"Selected model: {picked}")
|
|
755
|
+
return self._maybe_ready_to_start()
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
sub = parts[1]
|
|
759
|
+
|
|
760
|
+
if sub == "edit":
|
|
761
|
+
if len(parts) != 2:
|
|
762
|
+
terminal_ui.print_error("Usage: /model edit")
|
|
763
|
+
terminal_ui.console.print(
|
|
764
|
+
f"[{colors.text_muted}]Edit the YAML directly instead of using subcommands.[/{colors.text_muted}]\n"
|
|
765
|
+
)
|
|
766
|
+
return False
|
|
767
|
+
|
|
768
|
+
terminal_ui.console.print(
|
|
769
|
+
f"[{colors.text_muted}]Save the file to auto-reload (Ctrl+C to cancel)...[/]\n"
|
|
770
|
+
)
|
|
771
|
+
ok = await open_config_and_wait_for_save(self.model_manager.config_path)
|
|
772
|
+
if not ok:
|
|
773
|
+
terminal_ui.print_error(
|
|
774
|
+
f"Could not open editor. Please edit `{self.model_manager.config_path}` manually."
|
|
775
|
+
)
|
|
776
|
+
terminal_ui.console.print(
|
|
777
|
+
f"[{colors.text_muted}]Tip: set EDITOR='code' (or similar) for /model edit.[/{colors.text_muted}]\n"
|
|
778
|
+
)
|
|
779
|
+
return False
|
|
780
|
+
self.model_manager.reload()
|
|
781
|
+
terminal_ui.print_success("Reloaded `.aloop/models.yaml`")
|
|
782
|
+
self._show_models()
|
|
783
|
+
return self._maybe_ready_to_start()
|
|
784
|
+
|
|
785
|
+
model_id = " ".join(parts[1:]).strip()
|
|
786
|
+
if model_id and self.model_manager.get_model(model_id):
|
|
787
|
+
self.model_manager.set_default(model_id)
|
|
788
|
+
self.model_manager.switch_model(model_id)
|
|
789
|
+
terminal_ui.print_success(f"Selected model: {model_id}")
|
|
790
|
+
return self._maybe_ready_to_start()
|
|
791
|
+
|
|
792
|
+
terminal_ui.print_error("Unknown /model command.")
|
|
793
|
+
terminal_ui.console.print(
|
|
794
|
+
f"[{colors.text_muted}]Use /model to pick, or /model edit to configure.[/{colors.text_muted}]\n"
|
|
795
|
+
)
|
|
796
|
+
return False
|
|
797
|
+
|
|
798
|
+
def _maybe_ready_to_start(self) -> bool:
|
|
799
|
+
current = self.model_manager.get_current_model()
|
|
800
|
+
if not current:
|
|
801
|
+
return False
|
|
802
|
+
is_valid, _ = self.model_manager.validate_model(current)
|
|
803
|
+
return is_valid and self.model_manager.is_configured()
|
|
804
|
+
|
|
805
|
+
async def run(self) -> bool:
|
|
806
|
+
colors = Theme.get_colors()
|
|
807
|
+
terminal_ui.print_header(
|
|
808
|
+
"Agentic Loop - Model Setup", subtitle="Configure `.aloop/models.yaml` to start"
|
|
809
|
+
)
|
|
810
|
+
terminal_ui.console.print(
|
|
811
|
+
f"[{colors.text_muted}]Tip: Use /model edit (recommended) to configure, or /model to pick.[/{colors.text_muted}]\n"
|
|
812
|
+
)
|
|
813
|
+
self._show_help()
|
|
814
|
+
|
|
815
|
+
while True:
|
|
816
|
+
user_input = await self.input_handler.prompt_async("> ")
|
|
817
|
+
if not user_input:
|
|
818
|
+
continue
|
|
819
|
+
|
|
820
|
+
# Allow typing a model_id without the /model prefix, but avoid
|
|
821
|
+
# accidentally interpreting normal text as model selection.
|
|
822
|
+
if not user_input.startswith("/"):
|
|
823
|
+
model_ids = set(self.model_manager.get_model_ids())
|
|
824
|
+
if user_input in model_ids:
|
|
825
|
+
user_input = f"/model {user_input}"
|
|
826
|
+
else:
|
|
827
|
+
terminal_ui.print_error(
|
|
828
|
+
"You're in model setup mode. Pick a model or run /model edit.",
|
|
829
|
+
title="Model Setup",
|
|
830
|
+
)
|
|
831
|
+
continue
|
|
832
|
+
|
|
833
|
+
parts = user_input.split()
|
|
834
|
+
cmd = parts[0].lower()
|
|
835
|
+
|
|
836
|
+
if cmd in ("/exit", "/quit"):
|
|
837
|
+
return False
|
|
838
|
+
|
|
839
|
+
if cmd == "/help":
|
|
840
|
+
self._show_help()
|
|
841
|
+
continue
|
|
842
|
+
|
|
843
|
+
if cmd == "/model":
|
|
844
|
+
ready = await self._handle_model_command(user_input)
|
|
845
|
+
if ready:
|
|
846
|
+
terminal_ui.print_success("Model configuration looks good. Starting agent…")
|
|
847
|
+
return True
|
|
848
|
+
continue
|
|
849
|
+
terminal_ui.print_error(f"Unknown command: {cmd}. Try /help.")
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
async def run_interactive_mode(agent) -> None:
|
|
853
|
+
"""Run agent in interactive multi-turn conversation mode.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
agent: The agent instance
|
|
857
|
+
"""
|
|
858
|
+
session = InteractiveSession(agent)
|
|
859
|
+
await session.run()
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
async def run_model_setup_mode(model_manager: ModelManager | None = None) -> bool:
|
|
863
|
+
"""Run model setup mode; returns True when ready to start agent."""
|
|
864
|
+
session = ModelSetupSession(model_manager=model_manager)
|
|
865
|
+
return await session.run()
|