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.
Files changed (66) hide show
  1. agent/__init__.py +0 -0
  2. agent/agent.py +182 -0
  3. agent/base.py +406 -0
  4. agent/context.py +126 -0
  5. agent/prompts/__init__.py +1 -0
  6. agent/todo.py +149 -0
  7. agent/tool_executor.py +54 -0
  8. agent/verification.py +135 -0
  9. aloop-0.1.1.dist-info/METADATA +252 -0
  10. aloop-0.1.1.dist-info/RECORD +66 -0
  11. aloop-0.1.1.dist-info/WHEEL +5 -0
  12. aloop-0.1.1.dist-info/entry_points.txt +2 -0
  13. aloop-0.1.1.dist-info/licenses/LICENSE +21 -0
  14. aloop-0.1.1.dist-info/top_level.txt +9 -0
  15. cli.py +19 -0
  16. config.py +146 -0
  17. interactive.py +865 -0
  18. llm/__init__.py +51 -0
  19. llm/base.py +26 -0
  20. llm/compat.py +226 -0
  21. llm/content_utils.py +309 -0
  22. llm/litellm_adapter.py +450 -0
  23. llm/message_types.py +245 -0
  24. llm/model_manager.py +265 -0
  25. llm/retry.py +95 -0
  26. main.py +246 -0
  27. memory/__init__.py +20 -0
  28. memory/compressor.py +554 -0
  29. memory/manager.py +538 -0
  30. memory/serialization.py +82 -0
  31. memory/short_term.py +88 -0
  32. memory/store/__init__.py +6 -0
  33. memory/store/memory_store.py +100 -0
  34. memory/store/yaml_file_memory_store.py +414 -0
  35. memory/token_tracker.py +203 -0
  36. memory/types.py +51 -0
  37. tools/__init__.py +6 -0
  38. tools/advanced_file_ops.py +557 -0
  39. tools/base.py +51 -0
  40. tools/calculator.py +50 -0
  41. tools/code_navigator.py +975 -0
  42. tools/explore.py +254 -0
  43. tools/file_ops.py +150 -0
  44. tools/git_tools.py +791 -0
  45. tools/notify.py +69 -0
  46. tools/parallel_execute.py +420 -0
  47. tools/session_manager.py +205 -0
  48. tools/shell.py +147 -0
  49. tools/shell_background.py +470 -0
  50. tools/smart_edit.py +491 -0
  51. tools/todo.py +130 -0
  52. tools/web_fetch.py +673 -0
  53. tools/web_search.py +61 -0
  54. utils/__init__.py +15 -0
  55. utils/logger.py +105 -0
  56. utils/model_pricing.py +49 -0
  57. utils/runtime.py +75 -0
  58. utils/terminal_ui.py +422 -0
  59. utils/tui/__init__.py +39 -0
  60. utils/tui/command_registry.py +49 -0
  61. utils/tui/components.py +306 -0
  62. utils/tui/input_handler.py +393 -0
  63. utils/tui/model_ui.py +204 -0
  64. utils/tui/progress.py +292 -0
  65. utils/tui/status_bar.py +178 -0
  66. utils/tui/theme.py +165 -0
utils/terminal_ui.py ADDED
@@ -0,0 +1,422 @@
1
+ """Terminal UI utilities using Rich library for beautiful output.
2
+
3
+ This module provides a unified interface for terminal output, integrating
4
+ with the TUI theme system for consistent styling.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional
8
+
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.markdown import Markdown
12
+ from rich.panel import Panel
13
+ from rich.syntax import Syntax
14
+ from rich.table import Table
15
+ from rich.text import Text
16
+
17
+ from config import Config
18
+ from utils.tui.theme import Theme, set_theme
19
+
20
+ # Initialize theme from config
21
+ set_theme(Config.TUI_THEME)
22
+
23
+ # Global console instance with theme support
24
+ console = Console(theme=Theme.get_rich_theme())
25
+
26
+
27
+ def _get_colors():
28
+ """Get current theme colors."""
29
+ return Theme.get_colors()
30
+
31
+
32
+ ALOOP_LOGO = r"""
33
+ █████╗ ██╗ ██████╗ ██████╗ ██████╗
34
+ ██╔══██╗██║ ██╔═══██╗██╔═══██╗██╔══██╗
35
+ ███████║██║ ██║ ██║██║ ██║██████╔╝
36
+ ██╔══██║██║ ██║ ██║██║ ██║██╔═══╝
37
+ ██║ ██║███████╗╚██████╔╝╚██████╔╝██║
38
+ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝"""
39
+
40
+
41
+ TAGLINES = [
42
+ "Think. Act. Observe. Repeat.",
43
+ "Your terminal, now with agency.",
44
+ "Reasoning in loops, so you don't have to.",
45
+ "One loop to rule them all.",
46
+ "Where thoughts become tool calls.",
47
+ "The agent that thinks before it acts. Usually.",
48
+ "Ctrl+C is your safe word.",
49
+ "Turning vibes into function calls since 2025.",
50
+ "I think, therefore I tool_call.",
51
+ "sudo make me a sandwich. Actually, I can do that now.",
52
+ ]
53
+
54
+
55
+ def print_banner(subtitle: Optional[str] = None) -> None:
56
+ """Print the ASCII art banner with a random tagline."""
57
+ import random
58
+
59
+ colors = _get_colors()
60
+ content = f"[bold {colors.primary}]{ALOOP_LOGO.lstrip(chr(10))}[/bold {colors.primary}]"
61
+ tagline = subtitle or random.choice(TAGLINES)
62
+ content += f"\n\n[italic {colors.secondary}]{tagline}[/italic {colors.secondary}]"
63
+
64
+ console.print(Panel(content, border_style=colors.primary, box=box.DOUBLE, padding=(1, 2)))
65
+
66
+
67
+ def print_header(title: str, subtitle: Optional[str] = None) -> None:
68
+ """Print a formatted header panel.
69
+
70
+ Args:
71
+ title: Main title text
72
+ subtitle: Optional subtitle text
73
+ """
74
+ colors = _get_colors()
75
+ content = f"[bold {colors.primary}]{title}[/bold {colors.primary}]"
76
+ if subtitle:
77
+ content += f"\n[{colors.text_secondary}]{subtitle}[/{colors.text_secondary}]"
78
+
79
+ console.print(Panel(content, border_style=colors.primary, box=box.DOUBLE, padding=(1, 2)))
80
+
81
+
82
+ def print_config(config: Dict[str, Any]) -> None:
83
+ """Print configuration in a formatted table.
84
+
85
+ Args:
86
+ config: Dictionary of configuration key-value pairs
87
+ """
88
+ colors = _get_colors()
89
+ table = Table(show_header=False, box=box.SIMPLE, border_style=colors.text_muted, padding=(0, 2))
90
+ table.add_column("Key", style=f"{colors.primary} bold")
91
+ table.add_column("Value", style=colors.success)
92
+
93
+ for key, value in config.items():
94
+ table.add_row(key, str(value))
95
+
96
+ console.print(table)
97
+
98
+
99
+ def print_thinking(thinking: str, max_length: Optional[int] = None) -> None:
100
+ """Print AI thinking/reasoning content.
101
+
102
+ Args:
103
+ thinking: Thinking content string
104
+ max_length: Maximum length to display (uses config default if None)
105
+ """
106
+ if not thinking:
107
+ return
108
+
109
+ if not Config.TUI_SHOW_THINKING:
110
+ return
111
+
112
+ colors = _get_colors()
113
+ max_len = max_length if max_length is not None else Config.TUI_THINKING_MAX_PREVIEW
114
+
115
+ # Truncate if too long
116
+ if len(thinking) > max_len:
117
+ display_text = (
118
+ thinking[:max_len]
119
+ + f"... [{colors.text_muted}]({len(thinking) - max_len} more chars)[/{colors.text_muted}]"
120
+ )
121
+ else:
122
+ display_text = thinking
123
+
124
+ console.print(
125
+ Panel(
126
+ display_text,
127
+ title=f"[bold {colors.thinking_accent}]Thinking[/bold {colors.thinking_accent}]",
128
+ border_style=colors.text_muted,
129
+ box=box.ROUNDED,
130
+ padding=(0, 1),
131
+ )
132
+ )
133
+
134
+
135
+ def print_tool_call(tool_name: str, arguments: Dict[str, Any]) -> None:
136
+ """Print tool call information.
137
+
138
+ Args:
139
+ tool_name: Name of the tool being called
140
+ arguments: Tool arguments
141
+ """
142
+ colors = _get_colors()
143
+
144
+ # Format arguments nicely
145
+ args_lines = []
146
+ for key, value in arguments.items():
147
+ value_str = str(value)
148
+ if len(value_str) > 100:
149
+ value_str = value_str[:97] + "..."
150
+ args_lines.append(
151
+ f" [{colors.text_secondary}]{key}:[/{colors.text_secondary}] {value_str}"
152
+ )
153
+
154
+ content = "\n".join(args_lines) if args_lines else ""
155
+
156
+ console.print(
157
+ Panel(
158
+ content,
159
+ title=f"[{colors.tool_accent}]Tool: {tool_name}[/{colors.tool_accent}]",
160
+ title_align="left",
161
+ border_style=colors.text_muted,
162
+ box=box.ROUNDED,
163
+ padding=(0, 1),
164
+ )
165
+ )
166
+
167
+
168
+ def print_tool_result(
169
+ result: str,
170
+ truncated: bool = False,
171
+ success: bool = True,
172
+ duration: Optional[float] = None,
173
+ ) -> None:
174
+ """Print tool result.
175
+
176
+ Args:
177
+ result: Tool result string
178
+ truncated: Whether the result was truncated
179
+ success: Whether the tool call succeeded
180
+ duration: Optional duration in seconds
181
+ """
182
+ colors = _get_colors()
183
+
184
+ if truncated:
185
+ console.print(f"[{colors.warning}]Result truncated[/{colors.warning}]")
186
+
187
+ # Show status line
188
+ status_icon = "✓" if success else "✗"
189
+ status_color = colors.success if success else colors.error
190
+ status_parts = [f"[{status_color}]{status_icon}[/{status_color}]"]
191
+
192
+ if duration:
193
+ status_parts.append(f"({duration:.1f}s)")
194
+
195
+ if status_parts:
196
+ console.print(" ".join(status_parts))
197
+
198
+
199
+ def print_final_answer(answer: str) -> None:
200
+ """Print final answer in a formatted panel with Markdown rendering.
201
+
202
+ Args:
203
+ answer: Final answer text (supports Markdown)
204
+ """
205
+ colors = _get_colors()
206
+ console.print()
207
+ # Render markdown content
208
+ md = Markdown(answer)
209
+ console.print(
210
+ Panel(
211
+ md,
212
+ title=f"[bold {colors.success}]Final Answer[/bold {colors.success}]",
213
+ border_style=colors.success,
214
+ box=box.DOUBLE,
215
+ padding=(1, 2),
216
+ )
217
+ )
218
+
219
+
220
+ def print_unfinished_answer(answer: str) -> None:
221
+ """Print an intermediate answer that did not pass verification.
222
+
223
+ Args:
224
+ answer: Answer text (supports Markdown)
225
+ """
226
+ colors = _get_colors()
227
+ console.print()
228
+ md = Markdown(answer)
229
+ console.print(
230
+ Panel(
231
+ md,
232
+ title=f"[bold {colors.warning}]Unfinished Answer[/bold {colors.warning}]",
233
+ border_style=colors.warning,
234
+ box=box.ROUNDED,
235
+ padding=(1, 2),
236
+ )
237
+ )
238
+
239
+
240
+ def print_memory_stats(stats: Dict[str, Any]) -> None:
241
+ """Print memory statistics in a formatted table.
242
+
243
+ Args:
244
+ stats: Dictionary of memory statistics
245
+ """
246
+ colors = _get_colors()
247
+ console.print()
248
+ console.print(
249
+ f"[bold {colors.primary}]Memory Statistics[/bold {colors.primary}]", justify="left"
250
+ )
251
+
252
+ table = Table(
253
+ show_header=True,
254
+ header_style=f"bold {colors.primary}",
255
+ box=box.ROUNDED,
256
+ border_style=colors.text_muted,
257
+ padding=(0, 1),
258
+ )
259
+
260
+ table.add_column("Metric", style=colors.primary)
261
+ table.add_column("Value", justify="right", style=colors.success)
262
+
263
+ # Calculate total tokens
264
+ total_used = stats["total_input_tokens"] + stats["total_output_tokens"]
265
+
266
+ # Add rows
267
+ table.add_row("Total Tokens", f"{total_used:,}")
268
+ table.add_row("├─ Input", f"{stats['total_input_tokens']:,}")
269
+ table.add_row("└─ Output", f"{stats['total_output_tokens']:,}")
270
+ table.add_row("Current Context", f"{stats['current_tokens']:,}")
271
+ table.add_row("Compressions", str(stats["compression_count"]))
272
+
273
+ # Net savings with color
274
+ savings = stats["net_savings"]
275
+ savings_str = (
276
+ f"{savings:,}" if savings >= 0 else f"[{colors.error}]{savings:,}[/{colors.error}]"
277
+ )
278
+ table.add_row("Net Savings", savings_str)
279
+
280
+ table.add_row("Total Cost", f"${stats['total_cost']:.4f}")
281
+ table.add_row("Messages", f"{stats['short_term_count']} in memory")
282
+
283
+ console.print(table)
284
+
285
+
286
+ def print_error(message: str, title: str = "Error") -> None:
287
+ """Print an error message.
288
+
289
+ Args:
290
+ message: Error message
291
+ title: Error title (default: "Error")
292
+ """
293
+ colors = _get_colors()
294
+ console.print(
295
+ Panel(
296
+ f"[{colors.error}]{message}[/{colors.error}]",
297
+ title=f"[bold {colors.error}]{title}[/bold {colors.error}]",
298
+ border_style=colors.error,
299
+ box=box.ROUNDED,
300
+ )
301
+ )
302
+
303
+
304
+ def print_warning(message: str) -> None:
305
+ """Print a warning message.
306
+
307
+ Args:
308
+ message: Warning message
309
+ """
310
+ colors = _get_colors()
311
+ console.print(f"[{colors.warning}]{message}[/{colors.warning}]")
312
+
313
+
314
+ def print_success(message: str) -> None:
315
+ """Print a success message.
316
+
317
+ Args:
318
+ message: Success message
319
+ """
320
+ colors = _get_colors()
321
+ console.print(f"[{colors.success}]✓ {message}[/{colors.success}]")
322
+
323
+
324
+ def print_info(message: str) -> None:
325
+ """Print an info message.
326
+
327
+ Args:
328
+ message: Info message
329
+ """
330
+ colors = _get_colors()
331
+ console.print(f"[{colors.primary}]ℹ {message}[/{colors.primary}]")
332
+
333
+
334
+ def print_log_location(log_file: str) -> None:
335
+ """Print log file location.
336
+
337
+ Args:
338
+ log_file: Path to log file
339
+ """
340
+ colors = _get_colors()
341
+ console.print()
342
+ console.print(f"[{colors.text_muted}]Detailed logs: {log_file}[/{colors.text_muted}]")
343
+
344
+
345
+ def print_code(code: str, language: str = "python") -> None:
346
+ """Print syntax-highlighted code.
347
+
348
+ Args:
349
+ code: Code string
350
+ language: Programming language (default: python)
351
+ """
352
+ syntax = Syntax(code, language, theme="monokai", line_numbers=True)
353
+ console.print(syntax)
354
+
355
+
356
+ def print_markdown(markdown_text: str) -> None:
357
+ """Print formatted markdown.
358
+
359
+ Args:
360
+ markdown_text: Markdown text to render
361
+ """
362
+ md = Markdown(markdown_text)
363
+ console.print(md)
364
+
365
+
366
+ def print_divider(width: int = 60) -> None:
367
+ """Print a horizontal divider.
368
+
369
+ Args:
370
+ width: Width of the divider in characters
371
+ """
372
+ colors = _get_colors()
373
+ console.print(Text("─" * width, style=colors.text_muted))
374
+
375
+
376
+ def print_user_message(message: str) -> None:
377
+ """Print a user message in Claude Code style.
378
+
379
+ Args:
380
+ message: User message text
381
+ """
382
+ colors = _get_colors()
383
+ prefix = Text("> ", style=f"bold {colors.user_input}")
384
+ content = Text(message, style=colors.user_input)
385
+ console.print(Text.assemble(prefix, content))
386
+ if not Config.TUI_COMPACT_MODE:
387
+ console.print()
388
+
389
+
390
+ def print_assistant_message(message: str, use_markdown: bool = True) -> None:
391
+ """Print an assistant message.
392
+
393
+ Args:
394
+ message: Assistant message text
395
+ use_markdown: Whether to render as markdown
396
+ """
397
+ colors = _get_colors()
398
+ if use_markdown:
399
+ md = Markdown(message)
400
+ console.print(md)
401
+ else:
402
+ console.print(Text(message, style=colors.assistant_output))
403
+ if not Config.TUI_COMPACT_MODE:
404
+ console.print()
405
+
406
+
407
+ def print_turn_divider(turn_number: Optional[int] = None) -> None:
408
+ """Print a divider between conversation turns.
409
+
410
+ Args:
411
+ turn_number: Optional turn number to display
412
+ """
413
+ colors = _get_colors()
414
+ if turn_number is not None:
415
+ left_line = "─" * 25
416
+ right_line = "─" * 25
417
+ turn_text = f" Turn {turn_number} "
418
+ console.print(Text(f"{left_line}{turn_text}{right_line}", style=colors.text_muted))
419
+ else:
420
+ console.print(Text("─" * 60, style=colors.text_muted))
421
+ if not Config.TUI_COMPACT_MODE:
422
+ console.print()
utils/tui/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ """TUI (Terminal User Interface) package for aloop.
2
+
3
+ This package provides a modern, professional terminal UI with:
4
+ - Theme support (dark/light modes)
5
+ - Clean message display (Claude Code style)
6
+ - Persistent status bar
7
+ - Progress spinners and animations
8
+ - Enhanced input handling with auto-completion
9
+ """
10
+
11
+ from utils.tui.components import (
12
+ Divider,
13
+ MessageDisplay,
14
+ ThinkingDisplay,
15
+ ToolCallDisplay,
16
+ )
17
+ from utils.tui.input_handler import InputHandler
18
+ from utils.tui.progress import ProgressContext, Spinner
19
+ from utils.tui.status_bar import StatusBar
20
+ from utils.tui.theme import Theme, get_theme, set_theme
21
+
22
+ __all__ = [
23
+ # Theme
24
+ "Theme",
25
+ "get_theme",
26
+ "set_theme",
27
+ # Components
28
+ "MessageDisplay",
29
+ "ToolCallDisplay",
30
+ "ThinkingDisplay",
31
+ "Divider",
32
+ # Status Bar
33
+ "StatusBar",
34
+ # Progress
35
+ "Spinner",
36
+ "ProgressContext",
37
+ # Input
38
+ "InputHandler",
39
+ ]
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class CommandSpec:
8
+ name: str
9
+ description: str = ""
10
+ args_hint: str = ""
11
+ group: str = ""
12
+ subcommands: dict[str, CommandSpec] = field(default_factory=dict)
13
+
14
+ @property
15
+ def display(self) -> str:
16
+ if self.args_hint:
17
+ return f"/{self.name} {self.args_hint}"
18
+ return f"/{self.name}"
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class CommandRegistry:
23
+ commands: list[CommandSpec]
24
+
25
+ def to_help_map(self) -> dict[str, str]:
26
+ result: dict[str, str] = {}
27
+ for cmd in self.commands:
28
+ result[cmd.name] = cmd.description
29
+ for sub_name, sub in cmd.subcommands.items():
30
+ key = f"{cmd.name} {sub_name}".strip()
31
+ result[key] = sub.description
32
+ return result
33
+
34
+ def to_subcommand_map(self) -> dict[str, dict[str, str]]:
35
+ result: dict[str, dict[str, str]] = {}
36
+ for cmd in self.commands:
37
+ if cmd.subcommands:
38
+ result[cmd.name] = {k: v.description for k, v in cmd.subcommands.items()}
39
+ return result
40
+
41
+ def to_display_map(self) -> dict[str, str]:
42
+ result: dict[str, str] = {}
43
+ for cmd in self.commands:
44
+ result[cmd.name] = cmd.display
45
+ for sub_name, sub in cmd.subcommands.items():
46
+ key = f"{cmd.name} {sub_name}".strip()
47
+ extra = f" {sub.args_hint}" if sub.args_hint else ""
48
+ result[key] = f"/{cmd.name} {sub_name}{extra}"
49
+ return result