zero-agent 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.
Files changed (66) hide show
  1. agentz/agent/base.py +262 -0
  2. agentz/artifacts/__init__.py +5 -0
  3. agentz/artifacts/artifact_writer.py +538 -0
  4. agentz/artifacts/reporter.py +235 -0
  5. agentz/artifacts/terminal_writer.py +100 -0
  6. agentz/context/__init__.py +6 -0
  7. agentz/context/context.py +91 -0
  8. agentz/context/conversation.py +205 -0
  9. agentz/context/data_store.py +208 -0
  10. agentz/llm/llm_setup.py +156 -0
  11. agentz/mcp/manager.py +142 -0
  12. agentz/mcp/patches.py +88 -0
  13. agentz/mcp/servers/chrome_devtools/server.py +14 -0
  14. agentz/profiles/base.py +108 -0
  15. agentz/profiles/data/data_analysis.py +38 -0
  16. agentz/profiles/data/data_loader.py +35 -0
  17. agentz/profiles/data/evaluation.py +43 -0
  18. agentz/profiles/data/model_training.py +47 -0
  19. agentz/profiles/data/preprocessing.py +47 -0
  20. agentz/profiles/data/visualization.py +47 -0
  21. agentz/profiles/manager/evaluate.py +51 -0
  22. agentz/profiles/manager/memory.py +62 -0
  23. agentz/profiles/manager/observe.py +48 -0
  24. agentz/profiles/manager/routing.py +66 -0
  25. agentz/profiles/manager/writer.py +51 -0
  26. agentz/profiles/mcp/browser.py +21 -0
  27. agentz/profiles/mcp/chrome.py +21 -0
  28. agentz/profiles/mcp/notion.py +21 -0
  29. agentz/runner/__init__.py +74 -0
  30. agentz/runner/base.py +28 -0
  31. agentz/runner/executor.py +320 -0
  32. agentz/runner/hooks.py +110 -0
  33. agentz/runner/iteration.py +142 -0
  34. agentz/runner/patterns.py +215 -0
  35. agentz/runner/tracker.py +188 -0
  36. agentz/runner/utils.py +45 -0
  37. agentz/runner/workflow.py +250 -0
  38. agentz/tools/__init__.py +20 -0
  39. agentz/tools/data_tools/__init__.py +17 -0
  40. agentz/tools/data_tools/data_analysis.py +152 -0
  41. agentz/tools/data_tools/data_loading.py +92 -0
  42. agentz/tools/data_tools/evaluation.py +175 -0
  43. agentz/tools/data_tools/helpers.py +120 -0
  44. agentz/tools/data_tools/model_training.py +192 -0
  45. agentz/tools/data_tools/preprocessing.py +229 -0
  46. agentz/tools/data_tools/visualization.py +281 -0
  47. agentz/utils/__init__.py +69 -0
  48. agentz/utils/config.py +708 -0
  49. agentz/utils/helpers.py +10 -0
  50. agentz/utils/parsers.py +142 -0
  51. agentz/utils/printer.py +539 -0
  52. pipelines/base.py +972 -0
  53. pipelines/data_scientist.py +97 -0
  54. pipelines/data_scientist_memory.py +151 -0
  55. pipelines/experience_learner.py +0 -0
  56. pipelines/prompt_generator.py +0 -0
  57. pipelines/simple.py +78 -0
  58. pipelines/simple_browser.py +145 -0
  59. pipelines/simple_chrome.py +75 -0
  60. pipelines/simple_notion.py +103 -0
  61. pipelines/tool_builder.py +0 -0
  62. zero_agent-0.1.0.dist-info/METADATA +269 -0
  63. zero_agent-0.1.0.dist-info/RECORD +66 -0
  64. zero_agent-0.1.0.dist-info/WHEEL +5 -0
  65. zero_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  66. zero_agent-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,10 @@
1
+ """
2
+ Miscellaneous helper utilities.
3
+ """
4
+
5
+ import datetime
6
+
7
+
8
+ def get_experiment_timestamp() -> str:
9
+ """Get timestamp for experiment naming."""
10
+ return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -0,0 +1,142 @@
1
+ """
2
+ Output parsing utilities for JSON and structured data extraction.
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Callable, List
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class OutputParserError(Exception):
12
+ """
13
+ Exception raised when the output parser fails to parse the output.
14
+ """
15
+ def __init__(self, message, output=None):
16
+ self.message = message
17
+ self.output = output
18
+ super().__init__(self.message)
19
+
20
+ def __str__(self):
21
+ if self.output:
22
+ return f"{self.message}\nProblematic output: {self.output}"
23
+ return self.message
24
+
25
+
26
+ def _escape_unescaped_quotes(json_text: str) -> str:
27
+ """Escape bare double quotes that appear inside JSON string values."""
28
+ result: List[str] = []
29
+ in_string = False
30
+ escape_next = False
31
+ for index, char in enumerate(json_text):
32
+ if escape_next:
33
+ result.append(char)
34
+ escape_next = False
35
+ continue
36
+ if char == '\\':
37
+ result.append(char)
38
+ escape_next = True
39
+ continue
40
+ if char == '"':
41
+ if in_string:
42
+ lookahead = index + 1
43
+ while lookahead < len(json_text) and json_text[lookahead] in " \t\r\n":
44
+ lookahead += 1
45
+ if lookahead < len(json_text) and json_text[lookahead] not in ",}]":
46
+ result.append('\\"')
47
+ else:
48
+ result.append('"')
49
+ in_string = False
50
+ else:
51
+ in_string = True
52
+ result.append('"')
53
+ else:
54
+ result.append(char)
55
+ return "".join(result)
56
+
57
+
58
+ def find_json_in_string(string: str) -> str:
59
+ """
60
+ Method to extract all text in the left-most brace that appears in a string.
61
+ Used to extract JSON from a string (note that this function does not validate the JSON).
62
+
63
+ Example:
64
+ string = "bla bla bla {this is {some} text{{}and it's sneaky}} because {it's} confusing"
65
+ output = "{this is {some} text{{}and it's sneaky}}"
66
+ """
67
+ stack = 0
68
+ start_index = None
69
+
70
+ for i, c in enumerate(string):
71
+ if c == '{':
72
+ if stack == 0:
73
+ start_index = i # Start index of the first '{'
74
+ stack += 1 # Push to stack
75
+ elif c == '}':
76
+ stack -= 1 # Pop stack
77
+ if stack == 0:
78
+ # Return the substring from the start of the first '{' to the current '}'
79
+ return string[start_index:i + 1] if start_index is not None else ""
80
+
81
+ # If no complete set of braces is found, return an empty string
82
+ return ""
83
+
84
+
85
+ def parse_json_output(output: str) -> Any:
86
+ """Take a string output and parse it as JSON"""
87
+ # First try to load the string as JSON
88
+ try:
89
+ return json.loads(output)
90
+ except json.JSONDecodeError as e:
91
+ escaped_output = _escape_unescaped_quotes(output)
92
+ if escaped_output != output:
93
+ try:
94
+ return json.loads(escaped_output)
95
+ except json.JSONDecodeError:
96
+ pass
97
+ pass
98
+
99
+ # If that fails, assume that the output is in a code block - remove the code block markers and try again
100
+ parsed_output = output
101
+ parsed_output = parsed_output.split("```")[1]
102
+ parsed_output = parsed_output.split("```")[0]
103
+ if parsed_output.startswith("json") or parsed_output.startswith("JSON"):
104
+ parsed_output = parsed_output[4:].strip()
105
+ try:
106
+ return json.loads(parsed_output)
107
+ except json.JSONDecodeError:
108
+ escaped_output = _escape_unescaped_quotes(parsed_output)
109
+ if escaped_output != parsed_output:
110
+ try:
111
+ return json.loads(escaped_output)
112
+ except json.JSONDecodeError:
113
+ pass
114
+ pass
115
+
116
+ # As a last attempt, try to manually find the JSON object in the output and parse it
117
+ parsed_output = find_json_in_string(output)
118
+ if parsed_output:
119
+ try:
120
+ return json.loads(parsed_output)
121
+ except json.JSONDecodeError:
122
+ escaped_output = _escape_unescaped_quotes(parsed_output)
123
+ if escaped_output != parsed_output:
124
+ try:
125
+ return json.loads(escaped_output)
126
+ except json.JSONDecodeError:
127
+ pass
128
+ raise OutputParserError(f"Failed to parse output as JSON", output)
129
+
130
+ # If all fails, raise an error
131
+ raise OutputParserError(f"Failed to parse output as JSON", output)
132
+
133
+
134
+ def create_type_parser(type: BaseModel) -> Callable[[str], BaseModel]:
135
+ """Create a function that takes a string output and parses it as a specified Pydantic model"""
136
+
137
+ def convert_json_string_to_type(output: str) -> BaseModel:
138
+ """Take a string output and parse it as a Pydantic model"""
139
+ output_dict = parse_json_output(output)
140
+ return type.model_validate(output_dict)
141
+
142
+ return convert_json_string_to_type
@@ -0,0 +1,539 @@
1
+ """
2
+ Rich-powered status printer for streaming pipeline progress updates.
3
+ """
4
+
5
+ import json
6
+ import re
7
+ from typing import Any, Dict, List, Optional, Set, Tuple
8
+ from collections import OrderedDict
9
+
10
+ from rich.console import Console, Group
11
+ from rich.live import Live
12
+ from rich.panel import Panel
13
+ from rich.markdown import Markdown
14
+ from rich.spinner import Spinner
15
+ from rich.syntax import Syntax
16
+ from rich.text import Text
17
+
18
+
19
+ class Printer:
20
+ """Rich-powered status printer for streaming pipeline progress updates.
21
+
22
+ - Each item is displayed as a Panel (box) with a title.
23
+ - In-progress items show a spinner above the panel.
24
+ - Completed items show a checkmark in the panel title (unless hidden).
25
+ - Supports nested layout: groups (iterations) contain section panels.
26
+ - Per-section border colors with sensible defaults.
27
+ - Backward compatible with previous `update_item` signature.
28
+ """
29
+
30
+ # Default border colors by section name
31
+ DEFAULT_BORDER_COLORS = {
32
+ "observations": "yellow",
33
+ "observation": "yellow",
34
+ "observe": "yellow",
35
+ "evaluation": "magenta",
36
+ "evaluate": "magenta",
37
+ "routing": "blue",
38
+ "route": "blue",
39
+ "tools": "cyan",
40
+ "tool": "cyan",
41
+ "writer": "green",
42
+ "write": "green",
43
+ }
44
+
45
+ def __init__(self, console: Console) -> None:
46
+ self.console = console
47
+ # You can tweak screen=True to prevent scrollback; kept False to preserve logs
48
+ self.live = Live(
49
+ console=console,
50
+ refresh_per_second=12,
51
+ vertical_overflow="ellipsis", # keep dashboard within viewport
52
+ screen=False, # keep using normal screen buffer
53
+ transient=True, # clear live view when stopping
54
+ )
55
+
56
+ # items: id -> (content, is_done, title, border_style, group_id)
57
+ self.items: Dict[str, Tuple[str, bool, Optional[str], Optional[str], Optional[str]]] = {}
58
+ # Track which items should hide the done checkmark
59
+ self.hide_done_ids: Set[str] = set()
60
+
61
+ # Rich content produced by agents per iteration (title -> panel)
62
+ self.iteration_sections: Dict[int, OrderedDict[str, Panel]] = {}
63
+ # Original content strings for creating previews (title -> content_string)
64
+ self.iteration_content: Dict[int, OrderedDict[str, str]] = {}
65
+ self.iteration_order: List[int] = []
66
+ self.finalized_iterations: Set[int] = set()
67
+
68
+ # Group management
69
+ self.group_order: List[str] = [] # Order of groups
70
+ self.groups: Dict[str, Dict[str, Any]] = {} # group_id -> {title, is_done, border_style, order}
71
+ self.item_order: List[str] = [] # Order of top-level items (no group_id)
72
+
73
+ self.live.start()
74
+
75
+ def end(self) -> None:
76
+ """Stop the live rendering session."""
77
+ self.live.stop()
78
+
79
+ def hide_done_checkmark(self, item_id: str) -> None:
80
+ """Hide the completion checkmark for the given item id."""
81
+ self.hide_done_ids.add(item_id)
82
+
83
+ def start_group(
84
+ self,
85
+ group_id: str,
86
+ *,
87
+ title: Optional[str] = None,
88
+ border_style: Optional[str] = None
89
+ ) -> None:
90
+ """Start a new group (e.g., an iteration panel).
91
+
92
+ Args:
93
+ group_id: Unique identifier for the group
94
+ title: Optional title for the group panel
95
+ border_style: Optional border color (defaults to white)
96
+ """
97
+ if group_id not in self.groups:
98
+ self.group_order.append(group_id)
99
+ self.groups[group_id] = {
100
+ "title": title or group_id,
101
+ "is_done": False,
102
+ "border_style": border_style or "white",
103
+ "order": [] # Track order of items in this group
104
+ }
105
+ self._flush()
106
+
107
+ def end_group(
108
+ self,
109
+ group_id: str,
110
+ *,
111
+ is_done: bool = True,
112
+ title: Optional[str] = None
113
+ ) -> None:
114
+ """Mark a group as complete.
115
+
116
+ Args:
117
+ group_id: Unique identifier for the group
118
+ is_done: Whether the group is complete (default: True)
119
+ title: Optional updated title for the group
120
+ """
121
+ if group_id in self.groups:
122
+ self.groups[group_id]["is_done"] = is_done
123
+ if title:
124
+ self.groups[group_id]["title"] = title
125
+ # Update border style to bright_white when done
126
+ if is_done and self.groups[group_id]["border_style"] == "white":
127
+ self.groups[group_id]["border_style"] = "bright_white"
128
+
129
+ iteration = self._extract_iteration_index(group_id)
130
+ if iteration is not None and is_done:
131
+ self._finalize_iteration(iteration)
132
+ else:
133
+ self._flush()
134
+
135
+ def update_item(
136
+ self,
137
+ item_id: str,
138
+ content: str,
139
+ *,
140
+ is_done: bool = False,
141
+ hide_checkmark: bool = False,
142
+ title: Optional[str] = None,
143
+ border_style: Optional[str] = None,
144
+ group_id: Optional[str] = None,
145
+ ) -> None:
146
+ """Insert or update a status line and refresh the live console.
147
+
148
+ Args:
149
+ item_id: Unique identifier for the item
150
+ content: Content to display
151
+ is_done: Whether the task is complete
152
+ hide_checkmark: Hide completion checkmark
153
+ title: Optional panel title
154
+ border_style: Optional border color (auto-detected from title if not provided)
155
+ group_id: Optional group to nest this item in
156
+ """
157
+ # Auto-detect border style from title if not provided
158
+ if border_style is None and title:
159
+ title_lower = title.lower().strip()
160
+ # Check for exact match first
161
+ if title_lower in self.DEFAULT_BORDER_COLORS:
162
+ border_style = self.DEFAULT_BORDER_COLORS[title_lower]
163
+ else:
164
+ # Check if any key is a substring of the title
165
+ for key, color in self.DEFAULT_BORDER_COLORS.items():
166
+ if key in title_lower:
167
+ border_style = color
168
+ break
169
+
170
+ # Track item in appropriate order list
171
+ if item_id not in self.items:
172
+ if group_id and group_id in self.groups:
173
+ if item_id not in self.groups[group_id]["order"]:
174
+ self.groups[group_id]["order"].append(item_id)
175
+ elif item_id not in self.item_order:
176
+ self.item_order.append(item_id)
177
+
178
+ self.items[item_id] = (content, is_done, title, border_style, group_id)
179
+ if hide_checkmark:
180
+ self.hide_done_ids.add(item_id)
181
+ self._flush()
182
+
183
+ def mark_item_done(
184
+ self,
185
+ item_id: str,
186
+ *,
187
+ title: Optional[str] = None,
188
+ border_style: Optional[str] = None
189
+ ) -> None:
190
+ """Mark an existing status line as completed (optionally update title/border).
191
+
192
+ Args:
193
+ item_id: Unique identifier for the item
194
+ title: Optional updated title
195
+ border_style: Optional updated border color
196
+ """
197
+ if item_id in self.items:
198
+ content, _, old_title, old_border, group_id = self.items[item_id]
199
+ self.items[item_id] = (
200
+ content,
201
+ True,
202
+ title or old_title,
203
+ border_style or old_border,
204
+ group_id
205
+ )
206
+ self._flush()
207
+
208
+ def log_panel(
209
+ self,
210
+ title: str,
211
+ content: str,
212
+ *,
213
+ border_style: Optional[str] = None,
214
+ iteration: Optional[int] = None,
215
+ ) -> None:
216
+ """Render a standalone panel outside the live dashboard.
217
+
218
+ Useful for persisting rich text output to the terminal while keeping the
219
+ live printer focused on lightweight status updates.
220
+ """
221
+ # content = self._truncate_content(content)
222
+
223
+ if border_style is None:
224
+ title_lower = title.lower().strip()
225
+ if title_lower in self.DEFAULT_BORDER_COLORS:
226
+ border_style = self.DEFAULT_BORDER_COLORS[title_lower]
227
+ else:
228
+ for key, color in self.DEFAULT_BORDER_COLORS.items():
229
+ if key in title_lower:
230
+ border_style = color
231
+ break
232
+
233
+ panel = Panel(
234
+ self._detect_and_render_body(content),
235
+ title=Text(title),
236
+ border_style=border_style or "cyan",
237
+ padding=(1, 2),
238
+ expand=True,
239
+ )
240
+
241
+ if iteration is not None:
242
+ sections = self.iteration_sections.setdefault(iteration, OrderedDict())
243
+ content_dict = self.iteration_content.setdefault(iteration, OrderedDict())
244
+ if iteration not in self.iteration_order:
245
+ self.iteration_order.append(iteration)
246
+ sections[title] = panel
247
+ content_dict[title] = content # Store original content for previews
248
+ if iteration in self.finalized_iterations:
249
+ self.finalized_iterations.discard(iteration)
250
+ self._flush()
251
+ else:
252
+ self.live.console.print(panel)
253
+
254
+ # ------------ internals ------------
255
+
256
+ def _truncate_content(self, content: str) -> str:
257
+ """Limit panel body length so it fits comfortably in a terminal window."""
258
+ max_lines = 40
259
+ max_cols = 120
260
+ lines = content.splitlines()
261
+ truncated: List[str] = []
262
+
263
+ for idx, line in enumerate(lines):
264
+ shortened = line
265
+ if len(shortened) > max_cols:
266
+ shortened = shortened[: max_cols - 1].rstrip() + "…"
267
+ truncated.append(shortened)
268
+ if idx + 1 >= max_lines:
269
+ if idx + 1 < len(lines):
270
+ truncated.append("…")
271
+ break
272
+
273
+ result = "\n".join(truncated)
274
+
275
+ # Ensure code fences remain balanced after truncation
276
+ if result.count("```") % 2 == 1:
277
+ result += "\n```"
278
+ return result
279
+
280
+ def _detect_and_render_body(self, content: str) -> Any:
281
+ """Auto-detect content type and render with appropriate Rich object.
282
+
283
+ Detection order:
284
+ 1. ANSI escape codes → Text.from_ansi
285
+ 2. Rich markup (e.g., [bold cyan]...[/]) → Text.from_markup
286
+ 3. JSON → Syntax highlighting
287
+ 4. Code patterns → Syntax highlighting
288
+ 5. Markdown (headers, bold, bullets, code fences) → Markdown
289
+ 6. Plain text → Text
290
+ """
291
+ # Check for ANSI escape codes
292
+ ansi_pattern = r'\x1b\[[0-9;]*m'
293
+ if re.search(ansi_pattern, content):
294
+ return Text.from_ansi(content)
295
+
296
+ # Check for Rich markup patterns
297
+ rich_markup_pattern = r'\[/?[a-z_]+(?:\s+[a-z_]+)*\]'
298
+ if re.search(rich_markup_pattern, content, re.IGNORECASE):
299
+ return Text.from_markup(content, emoji=True)
300
+
301
+ # Check for JSON (starts with { or [, ends with } or ])
302
+ stripped = content.strip()
303
+ if (stripped.startswith('{') and stripped.endswith('}')) or \
304
+ (stripped.startswith('[') and stripped.endswith(']')):
305
+ try:
306
+ json.loads(stripped) # Validate it's valid JSON
307
+ return Syntax(content, "json", theme="monokai", line_numbers=False)
308
+ except (json.JSONDecodeError, ValueError):
309
+ pass
310
+
311
+ # Check for common code patterns (imports, function definitions, etc.)
312
+ code_patterns = [
313
+ r'^\s*import\s+',
314
+ r'^\s*from\s+.+\s+import\s+',
315
+ r'^\s*def\s+\w+\s*\(',
316
+ r'^\s*class\s+\w+',
317
+ r'^\s*async\s+def\s+',
318
+ r'^\s*@\w+',
319
+ r'^\s*if\s+__name__\s*==',
320
+ ]
321
+
322
+ for pattern in code_patterns:
323
+ if re.search(pattern, content, re.MULTILINE):
324
+ # Detect language
325
+ if re.search(r'^\s*(import|from|def|class|async)', content, re.MULTILINE):
326
+ return Syntax(content, "python", theme="monokai", line_numbers=False)
327
+ break
328
+
329
+ # Check for markdown patterns (more aggressive detection)
330
+ markdown_patterns = [
331
+ r'^\s*#{1,6}\s+', # Headers
332
+ r'\*\*[^*]+\*\*', # Bold
333
+ r'\*[^*]+\*', # Italic
334
+ r'^\s*[\*\-\+]\s+', # Unordered lists
335
+ r'^\s*\d+\.\s+', # Ordered lists
336
+ r'```', # Code fences
337
+ r'\[.+\]\(.+\)', # Links
338
+ r'^\s*>\s+', # Blockquotes
339
+ ]
340
+
341
+ for pattern in markdown_patterns:
342
+ if re.search(pattern, content, re.MULTILINE):
343
+ return Markdown(content, code_theme="monokai", inline_code_theme="monokai")
344
+
345
+ # Default to plain text
346
+ return Text(content)
347
+
348
+ def _extract_iteration_index(self, group_id: str) -> Optional[int]:
349
+ match = re.fullmatch(r"iter-(\d+)", group_id)
350
+ if match:
351
+ return int(match.group(1))
352
+ return None
353
+
354
+ def _build_iteration_panel(self, iteration: int) -> Optional[Panel]:
355
+ sections = self.iteration_sections.get(iteration)
356
+ if not sections:
357
+ return None
358
+ section_group = Group(*sections.values())
359
+ return Panel(
360
+ section_group,
361
+ title=Text(f"Iteration {iteration}"),
362
+ border_style="bright_white",
363
+ padding=1,
364
+ expand=True,
365
+ )
366
+
367
+ def _build_activity_preview_panel(self, iteration: int) -> Optional[Panel]:
368
+ """Build a truncated preview panel showing only the current agent output.
369
+
370
+ Args:
371
+ iteration: The iteration number to preview
372
+
373
+ Returns:
374
+ A Panel with truncated preview of the most recent agent output, or None if no content
375
+ """
376
+ content_dict = self.iteration_content.get(iteration)
377
+ if not content_dict:
378
+ return None
379
+
380
+ # Get only the most recent section (last item in OrderedDict)
381
+ last_title = None
382
+ last_content = None
383
+ for title, content in content_dict.items():
384
+ last_title = title
385
+ last_content = content
386
+
387
+ if not last_title or not last_content:
388
+ return None
389
+
390
+ # Truncate to first N lines and max characters to prevent long scrolling
391
+ max_preview_lines = 12
392
+ max_preview_chars = 2000
393
+ lines = last_content.splitlines()
394
+ preview_text = "\n".join(lines[:max_preview_lines])
395
+ if len(lines) > max_preview_lines:
396
+ preview_text += "\n..."
397
+
398
+ # Also truncate by character count
399
+ if len(preview_text) > max_preview_chars:
400
+ preview_text = preview_text[:max_preview_chars] + "\n..."
401
+
402
+ # Render the content using the same detection logic as log_panel
403
+ preview_renderable = self._detect_and_render_body(preview_text)
404
+
405
+ return Panel(
406
+ preview_renderable,
407
+ title=Text(f"Current Activity: {last_title}", style="bold cyan"),
408
+ border_style="bright_black",
409
+ padding=1,
410
+ expand=True,
411
+ )
412
+
413
+ def _finalize_iteration(self, iteration: int) -> None:
414
+ if iteration in self.finalized_iterations:
415
+ return
416
+ panel = self._build_iteration_panel(iteration)
417
+ if panel:
418
+ self.console.print(panel)
419
+ self.finalized_iterations.add(iteration)
420
+ self.iteration_sections.pop(iteration, None)
421
+ self.iteration_content.pop(iteration, None) # Also clean up content storage
422
+ if iteration in self.iteration_order:
423
+ self.iteration_order.remove(iteration)
424
+ self._flush()
425
+
426
+ def _render_item(
427
+ self,
428
+ item_id: str,
429
+ content: str,
430
+ is_done: bool,
431
+ title: Optional[str],
432
+ *,
433
+ indent: int = 0,
434
+ ) -> Any:
435
+ """Render a single status line without surrounding boxes."""
436
+ prefix = "✓" if is_done and item_id not in self.hide_done_ids else ("•" if is_done else "…")
437
+ indent_str = " " * (indent * 2)
438
+ lines = str(content).splitlines() or [""]
439
+
440
+ label = title or item_id
441
+ primary = lines[0].strip()
442
+ if label and primary:
443
+ display_text = f"{label}: {primary}"
444
+ elif label:
445
+ display_text = label
446
+ elif primary:
447
+ display_text = primary
448
+ else:
449
+ display_text = title or item_id or ""
450
+
451
+ if not is_done:
452
+ spinner_text = Text(f"{indent_str}{display_text}")
453
+ spinner = Spinner("dots", text=spinner_text)
454
+ if len(lines) == 1:
455
+ return spinner
456
+ continuation = [Text(f"{indent_str} {line}") for line in lines[1:]]
457
+ return Group(spinner, *continuation)
458
+
459
+ headline = f"{indent_str}{prefix} {display_text}".rstrip()
460
+ if not display_text:
461
+ headline = f"{indent_str}{prefix}"
462
+ first_line = Text(headline)
463
+ if len(lines) == 1:
464
+ return first_line
465
+ continuation = [Text(f"{indent_str} {line}") for line in lines[1:]]
466
+ return Group(first_line, *continuation)
467
+
468
+ def _render_group(self, group_id: str) -> Any:
469
+ """Render a group panel containing its child section panels.
470
+
471
+ Args:
472
+ group_id: The group to render
473
+
474
+ Returns:
475
+ A Panel containing Group of child panels
476
+ """
477
+ group = self.groups[group_id]
478
+ group_items: List[Any] = []
479
+
480
+ # Render items in the order they were added to this group
481
+ for item_id in group["order"]:
482
+ if item_id in self.items:
483
+ content, is_done, title, _border_style, _ = self.items[item_id]
484
+ group_items.append(
485
+ self._render_item(item_id, content, is_done, title, indent=1)
486
+ )
487
+
488
+ status_symbol = "✓" if group["is_done"] else "…"
489
+ header = Text(f"{status_symbol} {group['title']}")
490
+ return Group(header, *(group_items or [Text(" (no activity)")]))
491
+
492
+ def _flush(self) -> None:
493
+ """Re-render the live view with the latest status items."""
494
+ renderables: List[Any] = []
495
+
496
+ # Render top-level items (those without group_id)
497
+ for item_id in self.item_order:
498
+ if item_id in self.items:
499
+ content, is_done, title, _border_style, group_id = self.items[item_id]
500
+ if group_id is None:
501
+ renderables.append(
502
+ self._render_item(item_id, content, is_done, title)
503
+ )
504
+
505
+ # Render groups in order
506
+ for group_id in self.group_order:
507
+ if group_id in self.groups:
508
+ renderables.append(self._render_group(group_id))
509
+
510
+ render_groups: List[Any] = []
511
+
512
+ # Status panel containing live progress lines
513
+ status_body = Group(*renderables) if renderables else Text("No status yet")
514
+ status_panel = Panel(
515
+ status_body,
516
+ title=Text("Status", style="bold"),
517
+ border_style="bright_black",
518
+ padding=(0, 1),
519
+ expand=True,
520
+ )
521
+ render_groups.append(status_panel)
522
+
523
+ # Show only the latest active iteration inside the live view
524
+ active_iterations = [
525
+ iteration
526
+ for iteration in self.iteration_order
527
+ if iteration not in self.finalized_iterations
528
+ and self.iteration_sections.get(iteration)
529
+ ]
530
+ if active_iterations:
531
+ current_iteration = max(active_iterations)
532
+
533
+ # Add truncated preview panel for current activity only
534
+ # (Full iteration panels are shown when finalized via _finalize_iteration)
535
+ preview_panel = self._build_activity_preview_panel(current_iteration)
536
+ if preview_panel:
537
+ render_groups.append(preview_panel)
538
+
539
+ self.live.update(Group(*render_groups))