glaip-sdk 0.0.3__py3-none-any.whl → 0.0.5__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.
- glaip_sdk/__init__.py +5 -5
- glaip_sdk/branding.py +146 -0
- glaip_sdk/cli/__init__.py +1 -1
- glaip_sdk/cli/agent_config.py +82 -0
- glaip_sdk/cli/commands/__init__.py +3 -3
- glaip_sdk/cli/commands/agents.py +786 -271
- glaip_sdk/cli/commands/configure.py +19 -19
- glaip_sdk/cli/commands/mcps.py +151 -141
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +252 -178
- glaip_sdk/cli/display.py +244 -0
- glaip_sdk/cli/io.py +106 -0
- glaip_sdk/cli/main.py +27 -20
- glaip_sdk/cli/resolution.py +59 -0
- glaip_sdk/cli/utils.py +372 -213
- glaip_sdk/cli/validators.py +235 -0
- glaip_sdk/client/__init__.py +3 -224
- glaip_sdk/client/agents.py +632 -171
- glaip_sdk/client/base.py +66 -4
- glaip_sdk/client/main.py +226 -0
- glaip_sdk/client/mcps.py +143 -18
- glaip_sdk/client/tools.py +327 -104
- glaip_sdk/config/constants.py +10 -1
- glaip_sdk/models.py +43 -3
- glaip_sdk/rich_components.py +29 -0
- glaip_sdk/utils/__init__.py +18 -171
- glaip_sdk/utils/agent_config.py +181 -0
- glaip_sdk/utils/client_utils.py +159 -79
- glaip_sdk/utils/display.py +100 -0
- glaip_sdk/utils/general.py +94 -0
- glaip_sdk/utils/import_export.py +140 -0
- glaip_sdk/utils/rendering/formatting.py +6 -1
- glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
- glaip_sdk/utils/rendering/renderer/base.py +340 -247
- glaip_sdk/utils/rendering/renderer/debug.py +3 -2
- glaip_sdk/utils/rendering/renderer/panels.py +11 -10
- glaip_sdk/utils/rendering/steps.py +1 -1
- glaip_sdk/utils/resource_refs.py +192 -0
- glaip_sdk/utils/rich_utils.py +29 -0
- glaip_sdk/utils/serialization.py +285 -0
- glaip_sdk/utils/validation.py +273 -0
- {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
- glaip_sdk-0.0.5.dist-info/RECORD +55 -0
- glaip_sdk/cli/commands/init.py +0 -177
- glaip_sdk-0.0.3.dist-info/RECORD +0 -40
- {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -16,9 +16,9 @@ from rich.console import Console as RichConsole
|
|
|
16
16
|
from rich.console import Group
|
|
17
17
|
from rich.live import Live
|
|
18
18
|
from rich.markdown import Markdown
|
|
19
|
-
from rich.panel import Panel
|
|
20
19
|
from rich.text import Text
|
|
21
20
|
|
|
21
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
22
22
|
from glaip_sdk.utils.rendering.formatting import (
|
|
23
23
|
format_main_title,
|
|
24
24
|
get_spinner_char,
|
|
@@ -26,6 +26,7 @@ from glaip_sdk.utils.rendering.formatting import (
|
|
|
26
26
|
)
|
|
27
27
|
from glaip_sdk.utils.rendering.models import RunStats
|
|
28
28
|
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
29
|
+
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
29
30
|
from glaip_sdk.utils.rendering.renderer.panels import (
|
|
30
31
|
create_final_panel,
|
|
31
32
|
create_main_panel,
|
|
@@ -41,8 +42,6 @@ from glaip_sdk.utils.rendering.renderer.progress import (
|
|
|
41
42
|
from glaip_sdk.utils.rendering.renderer.stream import StreamProcessor
|
|
42
43
|
from glaip_sdk.utils.rendering.steps import StepManager
|
|
43
44
|
|
|
44
|
-
from .debug import render_debug_event
|
|
45
|
-
|
|
46
45
|
# Configure logger
|
|
47
46
|
logger = logging.getLogger("glaip_sdk.run_renderer")
|
|
48
47
|
|
|
@@ -143,7 +142,7 @@ class RichStreamRenderer:
|
|
|
143
142
|
)
|
|
144
143
|
if query:
|
|
145
144
|
self.console.print(
|
|
146
|
-
|
|
145
|
+
AIPPanel(
|
|
147
146
|
Markdown(f"**Query:** {query}"),
|
|
148
147
|
title="User Request",
|
|
149
148
|
border_style="yellow",
|
|
@@ -225,7 +224,7 @@ class RichStreamRenderer:
|
|
|
225
224
|
# Update live display
|
|
226
225
|
self._ensure_live()
|
|
227
226
|
|
|
228
|
-
def on_complete(self,
|
|
227
|
+
def on_complete(self, _stats: RunStats):
|
|
229
228
|
"""Handle completion event."""
|
|
230
229
|
self.state.finalizing_ui = True
|
|
231
230
|
|
|
@@ -253,6 +252,19 @@ class RichStreamRenderer:
|
|
|
253
252
|
self.live.stop()
|
|
254
253
|
self.live = None
|
|
255
254
|
|
|
255
|
+
# If no explicit final_response was printed, but we have buffered content,
|
|
256
|
+
# print a final result panel so users still see the outcome (especially in --verbose).
|
|
257
|
+
try:
|
|
258
|
+
if self.verbose and not self.state.printed_final_panel:
|
|
259
|
+
body = ("".join(self.state.buffer) or "").strip()
|
|
260
|
+
if body:
|
|
261
|
+
final_panel = create_final_panel(body, theme=self.cfg.theme)
|
|
262
|
+
self.console.print(final_panel)
|
|
263
|
+
self.state.printed_final_panel = True
|
|
264
|
+
except Exception:
|
|
265
|
+
# Non-fatal; renderer best-effort
|
|
266
|
+
pass
|
|
267
|
+
|
|
256
268
|
def _ensure_live(self):
|
|
257
269
|
"""Ensure live display is updated."""
|
|
258
270
|
# Lazily create Live if needed
|
|
@@ -270,7 +282,13 @@ class RichStreamRenderer:
|
|
|
270
282
|
if self.live:
|
|
271
283
|
panels = [self._render_main_panel()]
|
|
272
284
|
steps_renderable = self._render_steps_text()
|
|
273
|
-
panels.append(
|
|
285
|
+
panels.append(
|
|
286
|
+
AIPPanel(
|
|
287
|
+
steps_renderable,
|
|
288
|
+
title="Steps",
|
|
289
|
+
border_style="blue",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
274
292
|
panels.extend(self._render_tool_panels())
|
|
275
293
|
self.live.update(Group(*panels))
|
|
276
294
|
|
|
@@ -286,79 +304,81 @@ class RichStreamRenderer:
|
|
|
286
304
|
# Implementation would track thinking states
|
|
287
305
|
pass
|
|
288
306
|
|
|
289
|
-
def
|
|
307
|
+
def _ensure_tool_panel(
|
|
308
|
+
self, name: str, args: Any, task_id: str, context_id: str
|
|
309
|
+
) -> str:
|
|
310
|
+
"""Ensure a tool panel exists and return its ID."""
|
|
311
|
+
formatted_title = format_tool_title(name)
|
|
312
|
+
is_delegation = is_delegation_tool(name)
|
|
313
|
+
tool_sid = f"tool_{name}_{task_id}_{context_id}"
|
|
314
|
+
|
|
315
|
+
if tool_sid not in self.tool_panels:
|
|
316
|
+
self.tool_panels[tool_sid] = {
|
|
317
|
+
"title": formatted_title,
|
|
318
|
+
"status": "running",
|
|
319
|
+
"started_at": monotonic(),
|
|
320
|
+
"server_started_at": self.stream_processor.server_elapsed_time,
|
|
321
|
+
"chunks": [],
|
|
322
|
+
"args": args or {},
|
|
323
|
+
"output": None,
|
|
324
|
+
"is_delegation": is_delegation,
|
|
325
|
+
}
|
|
326
|
+
# Add Args section once
|
|
327
|
+
if args:
|
|
328
|
+
try:
|
|
329
|
+
args_content = (
|
|
330
|
+
"**Args:**\n```json\n"
|
|
331
|
+
+ json.dumps(args, indent=2)
|
|
332
|
+
+ "\n```\n\n"
|
|
333
|
+
)
|
|
334
|
+
except Exception:
|
|
335
|
+
args_content = f"**Args:**\n{args}\n\n"
|
|
336
|
+
self.tool_panels[tool_sid]["chunks"].append(args_content)
|
|
337
|
+
self.tool_order.append(tool_sid)
|
|
338
|
+
|
|
339
|
+
return tool_sid
|
|
340
|
+
|
|
341
|
+
def _start_tool_step(
|
|
290
342
|
self,
|
|
291
|
-
|
|
292
|
-
|
|
343
|
+
task_id: str,
|
|
344
|
+
context_id: str,
|
|
345
|
+
tool_name: str,
|
|
293
346
|
tool_args: Any,
|
|
294
|
-
|
|
295
|
-
tool_calls_info: list,
|
|
347
|
+
_tool_sid: str,
|
|
296
348
|
):
|
|
297
|
-
"""
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
349
|
+
"""Start or get a step for a tool."""
|
|
350
|
+
if is_delegation_tool(tool_name):
|
|
351
|
+
st = self.steps.start_or_get(
|
|
352
|
+
task_id=task_id,
|
|
353
|
+
context_id=context_id,
|
|
354
|
+
kind="delegate",
|
|
355
|
+
name=tool_name,
|
|
356
|
+
args=tool_args,
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
st = self.steps.start_or_get(
|
|
360
|
+
task_id=task_id,
|
|
361
|
+
context_id=context_id,
|
|
362
|
+
kind="tool",
|
|
363
|
+
name=tool_name,
|
|
364
|
+
args=tool_args,
|
|
365
|
+
)
|
|
302
366
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
self.tool_panels[tool_sid] = {
|
|
309
|
-
"title": formatted_title,
|
|
310
|
-
"status": "running",
|
|
311
|
-
"started_at": monotonic(),
|
|
312
|
-
"server_started_at": self.stream_processor.server_elapsed_time,
|
|
313
|
-
"chunks": [],
|
|
314
|
-
"args": args or {},
|
|
315
|
-
"output": None,
|
|
316
|
-
"is_delegation": is_delegation,
|
|
317
|
-
}
|
|
318
|
-
# Add Args section once
|
|
319
|
-
if args:
|
|
320
|
-
try:
|
|
321
|
-
args_content = (
|
|
322
|
-
"**Args:**\n```json\n"
|
|
323
|
-
+ json.dumps(args, indent=2)
|
|
324
|
-
+ "\n```\n\n"
|
|
325
|
-
)
|
|
326
|
-
except Exception:
|
|
327
|
-
args_content = f"**Args:**\n{args}\n\n"
|
|
328
|
-
self.tool_panels[tool_sid]["chunks"].append(args_content)
|
|
329
|
-
self.tool_order.append(tool_sid)
|
|
330
|
-
return tool_sid
|
|
367
|
+
# Record server start time for this step if available
|
|
368
|
+
if st and self.stream_processor.server_elapsed_time is not None:
|
|
369
|
+
self._step_server_start_times[st.step_id] = (
|
|
370
|
+
self.stream_processor.server_elapsed_time
|
|
371
|
+
)
|
|
331
372
|
|
|
332
|
-
|
|
333
|
-
if tool_name:
|
|
334
|
-
tool_sid = ensure_tool_panel(tool_name, tool_args)
|
|
335
|
-
# Start or get a step for this tool
|
|
336
|
-
if is_delegation_tool(tool_name):
|
|
337
|
-
st = self.steps.start_or_get(
|
|
338
|
-
task_id=task_id,
|
|
339
|
-
context_id=context_id,
|
|
340
|
-
kind="delegate",
|
|
341
|
-
name=tool_name,
|
|
342
|
-
args=tool_args,
|
|
343
|
-
)
|
|
344
|
-
else:
|
|
345
|
-
st = self.steps.start_or_get(
|
|
346
|
-
task_id=task_id,
|
|
347
|
-
context_id=context_id,
|
|
348
|
-
kind="tool",
|
|
349
|
-
name=tool_name,
|
|
350
|
-
args=tool_args,
|
|
351
|
-
)
|
|
352
|
-
# Record server start time for this step if available
|
|
353
|
-
if st and self.stream_processor.server_elapsed_time is not None:
|
|
354
|
-
self._step_server_start_times[st.step_id] = (
|
|
355
|
-
self.stream_processor.server_elapsed_time
|
|
356
|
-
)
|
|
373
|
+
return st
|
|
357
374
|
|
|
358
|
-
|
|
375
|
+
def _process_additional_tool_calls(
|
|
376
|
+
self, tool_calls_info: list, tool_name: str, task_id: str, context_id: str
|
|
377
|
+
):
|
|
378
|
+
"""Process additional tool calls to avoid duplicates."""
|
|
359
379
|
for call_name, call_args, _ in tool_calls_info or []:
|
|
360
380
|
if call_name and call_name != tool_name:
|
|
361
|
-
|
|
381
|
+
self._ensure_tool_panel(call_name, call_args, task_id, context_id)
|
|
362
382
|
if is_delegation_tool(call_name):
|
|
363
383
|
st2 = self.steps.start_or_get(
|
|
364
384
|
task_id=task_id,
|
|
@@ -380,141 +400,205 @@ class RichStreamRenderer:
|
|
|
380
400
|
self.stream_processor.server_elapsed_time
|
|
381
401
|
)
|
|
382
402
|
|
|
383
|
-
|
|
403
|
+
def _detect_tool_completion(
|
|
404
|
+
self, metadata: dict, content: str
|
|
405
|
+
) -> tuple[bool, str | None, Any]:
|
|
406
|
+
"""Detect if a tool has completed and return completion info."""
|
|
384
407
|
tool_info = metadata.get("tool_info", {}) if isinstance(metadata, dict) else {}
|
|
385
|
-
is_tool_finished = False
|
|
386
|
-
finished_tool_name: str | None = None
|
|
387
|
-
finished_tool_output: Any = None
|
|
388
408
|
|
|
389
409
|
if tool_info.get("status") == "finished" and tool_info.get("name"):
|
|
390
|
-
|
|
391
|
-
finished_tool_name = tool_info.get("name")
|
|
392
|
-
finished_tool_output = tool_info.get("output")
|
|
410
|
+
return True, tool_info.get("name"), tool_info.get("output")
|
|
393
411
|
elif content and isinstance(content, str) and content.startswith("Completed "):
|
|
394
412
|
# content like "Completed google_serper"
|
|
395
413
|
tname = content.replace("Completed ", "").strip()
|
|
396
414
|
if tname:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
415
|
+
output = (
|
|
416
|
+
tool_info.get("output") if tool_info.get("name") == tname else None
|
|
417
|
+
)
|
|
418
|
+
return True, tname, output
|
|
401
419
|
elif metadata.get("status") == "finished" and tool_info.get("name"):
|
|
402
|
-
|
|
403
|
-
finished_tool_name = tool_info.get("name")
|
|
404
|
-
finished_tool_output = tool_info.get("output")
|
|
420
|
+
return True, tool_info.get("name"), tool_info.get("output")
|
|
405
421
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
422
|
+
return False, None, None
|
|
423
|
+
|
|
424
|
+
def _finish_tool_panel(
|
|
425
|
+
self,
|
|
426
|
+
finished_tool_name: str,
|
|
427
|
+
finished_tool_output: Any,
|
|
428
|
+
task_id: str,
|
|
429
|
+
context_id: str,
|
|
430
|
+
):
|
|
431
|
+
"""Finish a tool panel and update its status."""
|
|
432
|
+
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
433
|
+
if tool_sid in self.tool_panels:
|
|
434
|
+
meta = self.tool_panels[tool_sid]
|
|
435
|
+
prev_status = meta.get("status")
|
|
436
|
+
|
|
437
|
+
if prev_status != "finished":
|
|
412
438
|
meta["status"] = "finished"
|
|
413
|
-
# Compute and store duration for finished panel
|
|
414
|
-
if prev_status != "finished":
|
|
415
|
-
try:
|
|
416
|
-
server_now = self.stream_processor.server_elapsed_time
|
|
417
|
-
server_start = meta.get("server_started_at")
|
|
418
|
-
dur = None
|
|
419
|
-
if isinstance(server_now, int | float) and isinstance(
|
|
420
|
-
server_start, int | float
|
|
421
|
-
):
|
|
422
|
-
dur = max(0.0, float(server_now) - float(server_start))
|
|
423
|
-
elif meta.get("started_at") is not None:
|
|
424
|
-
dur = max(0.0, float(monotonic() - meta.get("started_at")))
|
|
425
|
-
if dur is not None:
|
|
426
|
-
meta["duration_seconds"] = dur
|
|
427
|
-
meta["server_finished_at"] = (
|
|
428
|
-
server_now
|
|
429
|
-
if isinstance(server_now, int | float)
|
|
430
|
-
else None
|
|
431
|
-
)
|
|
432
|
-
meta["finished_at"] = monotonic()
|
|
433
|
-
except Exception:
|
|
434
|
-
pass
|
|
435
439
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
440
|
+
# Compute and store duration
|
|
441
|
+
try:
|
|
442
|
+
server_now = self.stream_processor.server_elapsed_time
|
|
443
|
+
server_start = meta.get("server_started_at")
|
|
444
|
+
dur = None
|
|
445
|
+
|
|
446
|
+
if isinstance(server_now, int | float) and isinstance(
|
|
447
|
+
server_start, int | float
|
|
448
|
+
):
|
|
449
|
+
dur = max(0.0, float(server_now) - float(server_start))
|
|
450
|
+
elif meta.get("started_at") is not None:
|
|
451
|
+
dur = max(0.0, float(monotonic() - meta.get("started_at")))
|
|
452
|
+
|
|
453
|
+
if dur is not None:
|
|
454
|
+
meta["duration_seconds"] = dur
|
|
455
|
+
meta["server_finished_at"] = (
|
|
456
|
+
server_now if isinstance(server_now, int | float) else None
|
|
441
457
|
)
|
|
442
|
-
meta["
|
|
443
|
-
|
|
444
|
-
|
|
458
|
+
meta["finished_at"] = monotonic()
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
# Add output to panel
|
|
463
|
+
if finished_tool_output is not None:
|
|
464
|
+
meta["chunks"].append(
|
|
465
|
+
self._format_output_block(
|
|
466
|
+
finished_tool_output, finished_tool_name
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
meta["output"] = finished_tool_output
|
|
470
|
+
|
|
471
|
+
# Ensure this finished panel is visible in this frame
|
|
472
|
+
self.stream_processor.current_event_finished_panels.add(tool_sid)
|
|
473
|
+
|
|
474
|
+
def _finish_tool_step(
|
|
475
|
+
self,
|
|
476
|
+
finished_tool_name: str,
|
|
477
|
+
finished_tool_output: Any,
|
|
478
|
+
task_id: str,
|
|
479
|
+
context_id: str,
|
|
480
|
+
):
|
|
481
|
+
"""Finish the corresponding step for a completed tool."""
|
|
482
|
+
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
483
|
+
step_duration = None
|
|
445
484
|
|
|
446
|
-
|
|
485
|
+
try:
|
|
486
|
+
step_duration = self.tool_panels.get(tool_sid, {}).get("duration_seconds")
|
|
487
|
+
except Exception:
|
|
447
488
|
step_duration = None
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
489
|
+
|
|
490
|
+
if is_delegation_tool(finished_tool_name):
|
|
491
|
+
self.steps.finish(
|
|
492
|
+
task_id=task_id,
|
|
493
|
+
context_id=context_id,
|
|
494
|
+
kind="delegate",
|
|
495
|
+
name=finished_tool_name,
|
|
496
|
+
output=finished_tool_output,
|
|
497
|
+
duration_raw=step_duration,
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
self.steps.finish(
|
|
501
|
+
task_id=task_id,
|
|
502
|
+
context_id=context_id,
|
|
503
|
+
kind="tool",
|
|
504
|
+
name=finished_tool_name,
|
|
505
|
+
output=finished_tool_output,
|
|
506
|
+
duration_raw=step_duration,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def _create_tool_snapshot(
|
|
510
|
+
self, finished_tool_name: str, task_id: str, context_id: str
|
|
511
|
+
):
|
|
512
|
+
"""Create and print a snapshot for a finished tool."""
|
|
513
|
+
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
if not (
|
|
517
|
+
self.cfg.append_finished_snapshots
|
|
518
|
+
and not self.tool_panels.get(tool_sid, {}).get("snapshot_printed")
|
|
519
|
+
):
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
meta = self.tool_panels[tool_sid]
|
|
523
|
+
adjusted_title = meta.get("title") or finished_tool_name
|
|
524
|
+
|
|
525
|
+
# Add elapsed time to title
|
|
526
|
+
dur = meta.get("duration_seconds")
|
|
527
|
+
if isinstance(dur, int | float):
|
|
528
|
+
elapsed_str = (
|
|
529
|
+
f"{dur:.2f}s"
|
|
530
|
+
if dur >= 1
|
|
531
|
+
else (f"{int(dur * 1000)}ms" if int(dur * 1000) > 0 else "<1ms")
|
|
471
532
|
)
|
|
533
|
+
adjusted_title = f"{adjusted_title} · {elapsed_str}"
|
|
534
|
+
|
|
535
|
+
# Compose body from chunks and clamp
|
|
536
|
+
body_text = "".join(meta.get("chunks") or [])
|
|
537
|
+
max_lines = int(self.cfg.snapshot_max_lines or 0) or 60
|
|
538
|
+
lines = body_text.splitlines()
|
|
539
|
+
if len(lines) > max_lines:
|
|
540
|
+
lines = lines[:max_lines] + ["… (truncated)"]
|
|
541
|
+
body_text = "\n".join(lines)
|
|
542
|
+
|
|
543
|
+
max_chars = int(self.cfg.snapshot_max_chars or 0) or 4000
|
|
544
|
+
if len(body_text) > max_chars:
|
|
545
|
+
body_text = body_text[: max_chars - 12] + "\n… (truncated)"
|
|
546
|
+
|
|
547
|
+
snapshot_panel = create_tool_panel(
|
|
548
|
+
title=adjusted_title,
|
|
549
|
+
content=body_text or "(no output)",
|
|
550
|
+
status="finished",
|
|
551
|
+
theme=self.cfg.theme,
|
|
552
|
+
is_delegation=is_delegation_tool(finished_tool_name),
|
|
553
|
+
)
|
|
472
554
|
|
|
473
|
-
#
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
555
|
+
# Print as a snapshot entry
|
|
556
|
+
self.console.print(snapshot_panel)
|
|
557
|
+
# Guard so we don't print snapshot twice
|
|
558
|
+
self.tool_panels[tool_sid]["snapshot_printed"] = True
|
|
559
|
+
|
|
560
|
+
except Exception:
|
|
561
|
+
pass
|
|
562
|
+
|
|
563
|
+
def _handle_agent_step(
|
|
564
|
+
self,
|
|
565
|
+
event: dict[str, Any],
|
|
566
|
+
tool_name: str | None,
|
|
567
|
+
tool_args: Any,
|
|
568
|
+
_tool_out: Any,
|
|
569
|
+
tool_calls_info: list,
|
|
570
|
+
):
|
|
571
|
+
"""Handle agent step event."""
|
|
572
|
+
metadata = event.get("metadata", {})
|
|
573
|
+
task_id = event.get("task_id")
|
|
574
|
+
context_id = event.get("context_id")
|
|
575
|
+
content = event.get("content", "")
|
|
576
|
+
|
|
577
|
+
# Create steps and panels for the primary tool
|
|
578
|
+
if tool_name:
|
|
579
|
+
tool_sid = self._ensure_tool_panel(
|
|
580
|
+
tool_name, tool_args, task_id, context_id
|
|
581
|
+
)
|
|
582
|
+
self._start_tool_step(task_id, context_id, tool_name, tool_args, tool_sid)
|
|
583
|
+
|
|
584
|
+
# Handle additional tool calls
|
|
585
|
+
self._process_additional_tool_calls(
|
|
586
|
+
tool_calls_info, tool_name, task_id, context_id
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Check for tool completion
|
|
590
|
+
is_tool_finished, finished_tool_name, finished_tool_output = (
|
|
591
|
+
self._detect_tool_completion(metadata, content)
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
if is_tool_finished and finished_tool_name:
|
|
595
|
+
self._finish_tool_panel(
|
|
596
|
+
finished_tool_name, finished_tool_output, task_id, context_id
|
|
597
|
+
)
|
|
598
|
+
self._finish_tool_step(
|
|
599
|
+
finished_tool_name, finished_tool_output, task_id, context_id
|
|
600
|
+
)
|
|
601
|
+
self._create_tool_snapshot(finished_tool_name, task_id, context_id)
|
|
518
602
|
|
|
519
603
|
def _spinner(self) -> str:
|
|
520
604
|
"""Return spinner character."""
|
|
@@ -600,7 +684,7 @@ class RichStreamRenderer:
|
|
|
600
684
|
|
|
601
685
|
# Modern interface only — no legacy helper shims below
|
|
602
686
|
|
|
603
|
-
def _refresh(self,
|
|
687
|
+
def _refresh(self, _force: bool | None = None) -> None:
|
|
604
688
|
# In the modular renderer, refreshing simply updates the live group
|
|
605
689
|
self._ensure_live()
|
|
606
690
|
|
|
@@ -611,84 +695,93 @@ class RichStreamRenderer:
|
|
|
611
695
|
return True
|
|
612
696
|
return False
|
|
613
697
|
|
|
614
|
-
def
|
|
615
|
-
"""
|
|
616
|
-
if
|
|
617
|
-
return
|
|
698
|
+
def _get_step_icon(self, step_kind: str) -> str:
|
|
699
|
+
"""Get icon for step kind."""
|
|
700
|
+
if step_kind == "tool":
|
|
701
|
+
return "⚙️"
|
|
702
|
+
elif step_kind == "delegate":
|
|
703
|
+
return "🤝"
|
|
704
|
+
elif step_kind == "agent":
|
|
705
|
+
return "🧠"
|
|
706
|
+
return ""
|
|
707
|
+
|
|
708
|
+
def _format_step_status(self, step) -> str:
|
|
709
|
+
"""Format step status with elapsed time or duration."""
|
|
710
|
+
if is_step_finished(step):
|
|
711
|
+
if step.duration_ms is None:
|
|
712
|
+
return "[<1ms]"
|
|
713
|
+
elif step.duration_ms >= 1000:
|
|
714
|
+
return f"[{step.duration_ms/1000:.2f}s]"
|
|
715
|
+
elif step.duration_ms > 0:
|
|
716
|
+
return f"[{step.duration_ms}ms]"
|
|
717
|
+
return "[<1ms]"
|
|
718
|
+
else:
|
|
719
|
+
# Calculate elapsed time for running steps
|
|
720
|
+
elapsed = self._calculate_step_elapsed_time(step)
|
|
721
|
+
if elapsed >= 1:
|
|
722
|
+
return f"[{elapsed:.2f}s]"
|
|
723
|
+
ms = int(elapsed * 1000)
|
|
724
|
+
return f"[{ms}ms]" if ms > 0 else "[<1ms]"
|
|
725
|
+
|
|
726
|
+
def _calculate_step_elapsed_time(self, step) -> float:
|
|
727
|
+
"""Calculate elapsed time for a running step."""
|
|
728
|
+
server_elapsed = self.stream_processor.server_elapsed_time
|
|
729
|
+
server_start = self._step_server_start_times.get(step.step_id)
|
|
730
|
+
|
|
731
|
+
if isinstance(server_elapsed, int | float) and isinstance(
|
|
732
|
+
server_start, int | float
|
|
733
|
+
):
|
|
734
|
+
return max(0.0, float(server_elapsed) - float(server_start))
|
|
618
735
|
|
|
619
|
-
|
|
736
|
+
try:
|
|
737
|
+
return max(0.0, float(monotonic() - step.started_at))
|
|
738
|
+
except Exception:
|
|
739
|
+
return 0.0
|
|
740
|
+
|
|
741
|
+
def _get_step_display_name(self, step) -> str:
|
|
742
|
+
"""Get display name for a step."""
|
|
743
|
+
if step.name and step.name != "step":
|
|
744
|
+
return step.name
|
|
745
|
+
return "thinking..." if step.kind == "agent" else f"{step.kind} step"
|
|
746
|
+
|
|
747
|
+
def _check_parallel_tools(self) -> dict[tuple[str | None, str | None], list]:
|
|
748
|
+
"""Check for parallel running tools."""
|
|
620
749
|
running_by_ctx: dict[tuple[str | None, str | None], list] = {}
|
|
621
750
|
for sid in self.steps.order:
|
|
622
751
|
st = self.steps.by_id[sid]
|
|
623
752
|
if st.kind == "tool" and not is_step_finished(st):
|
|
624
753
|
key = (st.task_id, st.context_id)
|
|
625
754
|
running_by_ctx.setdefault(key, []).append(st)
|
|
755
|
+
return running_by_ctx
|
|
626
756
|
|
|
757
|
+
def _render_steps_text(self) -> Text:
|
|
758
|
+
"""Render the steps panel content."""
|
|
759
|
+
if not (self.steps.order or self.steps.children):
|
|
760
|
+
return Text("No steps yet", style="dim")
|
|
761
|
+
|
|
762
|
+
running_by_ctx = self._check_parallel_tools()
|
|
627
763
|
lines: list[str] = []
|
|
764
|
+
|
|
628
765
|
for sid in self.steps.order:
|
|
629
766
|
st = self.steps.by_id[sid]
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if st.duration_ms is None:
|
|
633
|
-
status_br = "[<1ms]"
|
|
634
|
-
elif st.duration_ms >= 1000:
|
|
635
|
-
status_br = f"[{st.duration_ms/1000:.2f}s]"
|
|
636
|
-
elif st.duration_ms > 0:
|
|
637
|
-
status_br = f"[{st.duration_ms}ms]"
|
|
638
|
-
else:
|
|
639
|
-
status_br = "[<1ms]"
|
|
640
|
-
else:
|
|
641
|
-
# Prefer server timing when we have a server start timestamp
|
|
642
|
-
server_elapsed = self.stream_processor.server_elapsed_time
|
|
643
|
-
server_start = self._step_server_start_times.get(st.step_id)
|
|
644
|
-
if isinstance(server_elapsed, int | float) and isinstance(
|
|
645
|
-
server_start, int | float
|
|
646
|
-
):
|
|
647
|
-
elapsed = max(0.0, float(server_elapsed) - float(server_start))
|
|
648
|
-
else:
|
|
649
|
-
try:
|
|
650
|
-
elapsed = max(0.0, float(monotonic() - st.started_at))
|
|
651
|
-
except Exception:
|
|
652
|
-
elapsed = 0.0
|
|
653
|
-
# Standardized elapsed label without "Working..."
|
|
654
|
-
if elapsed >= 1:
|
|
655
|
-
status_br = f"[{elapsed:.2f}s]"
|
|
656
|
-
else:
|
|
657
|
-
ms = int(elapsed * 1000)
|
|
658
|
-
status_br = f"[{ms}ms]" if ms > 0 else "[<1ms]"
|
|
659
|
-
|
|
660
|
-
display_name = (
|
|
661
|
-
st.name
|
|
662
|
-
if st.name and st.name != "step"
|
|
663
|
-
else ("thinking..." if st.kind == "agent" else f"{st.kind} step")
|
|
664
|
-
)
|
|
767
|
+
status_br = self._format_step_status(st)
|
|
768
|
+
display_name = self._get_step_display_name(st)
|
|
665
769
|
tail = " ✓" if is_step_finished(st) else ""
|
|
666
770
|
|
|
667
|
-
#
|
|
668
|
-
parallel_indicator = ""
|
|
771
|
+
# Add parallel indicator for running tools
|
|
669
772
|
if st.kind == "tool" and not is_step_finished(st):
|
|
670
773
|
key = (st.task_id, st.context_id)
|
|
671
774
|
if len(running_by_ctx.get(key, [])) > 1:
|
|
672
|
-
|
|
673
|
-
status_br = status_br.replace("]", f"{parallel_indicator}]")
|
|
674
|
-
|
|
675
|
-
# Icon prefix (simple mapping)
|
|
676
|
-
if st.kind == "tool":
|
|
677
|
-
icon = "⚙️"
|
|
678
|
-
elif st.kind == "delegate":
|
|
679
|
-
icon = "🤝"
|
|
680
|
-
elif st.kind == "agent":
|
|
681
|
-
icon = "🧠"
|
|
682
|
-
else:
|
|
683
|
-
icon = ""
|
|
775
|
+
status_br = status_br.replace("]", " 🔄]")
|
|
684
776
|
|
|
777
|
+
icon = self._get_step_icon(st.kind)
|
|
685
778
|
lines.append(f"{icon} {display_name} {status_br}{tail}")
|
|
686
779
|
|
|
687
780
|
return Text("\n".join(lines), style="dim")
|
|
688
781
|
|
|
689
|
-
def _render_tool_panels(self) -> list[
|
|
782
|
+
def _render_tool_panels(self) -> list[AIPPanel]:
|
|
690
783
|
"""Render tool execution output panels."""
|
|
691
|
-
panels: list[
|
|
784
|
+
panels: list[AIPPanel] = []
|
|
692
785
|
for sid in self.tool_order:
|
|
693
786
|
meta = self.tool_panels.get(sid) or {}
|
|
694
787
|
title = meta.get("title") or "Tool"
|