glaip-sdk 0.0.1b10__py3-none-any.whl → 0.0.3__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 +2 -2
- glaip_sdk/_version.py +51 -0
- glaip_sdk/cli/commands/agents.py +201 -109
- glaip_sdk/cli/commands/configure.py +29 -87
- glaip_sdk/cli/commands/init.py +16 -7
- glaip_sdk/cli/commands/mcps.py +73 -153
- glaip_sdk/cli/commands/tools.py +185 -49
- glaip_sdk/cli/main.py +30 -27
- glaip_sdk/cli/utils.py +126 -13
- glaip_sdk/client/__init__.py +54 -2
- glaip_sdk/client/agents.py +175 -237
- glaip_sdk/client/base.py +62 -2
- glaip_sdk/client/mcps.py +63 -20
- glaip_sdk/client/tools.py +95 -28
- glaip_sdk/config/constants.py +10 -3
- glaip_sdk/exceptions.py +13 -0
- glaip_sdk/models.py +20 -4
- glaip_sdk/utils/__init__.py +116 -18
- glaip_sdk/utils/client_utils.py +284 -0
- glaip_sdk/utils/rendering/__init__.py +1 -0
- glaip_sdk/utils/rendering/formatting.py +211 -0
- glaip_sdk/utils/rendering/models.py +53 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
- glaip_sdk/utils/rendering/renderer/base.py +827 -0
- glaip_sdk/utils/rendering/renderer/config.py +33 -0
- glaip_sdk/utils/rendering/renderer/console.py +54 -0
- glaip_sdk/utils/rendering/renderer/debug.py +82 -0
- glaip_sdk/utils/rendering/renderer/panels.py +123 -0
- glaip_sdk/utils/rendering/renderer/progress.py +118 -0
- glaip_sdk/utils/rendering/renderer/stream.py +198 -0
- glaip_sdk/utils/rendering/steps.py +168 -0
- glaip_sdk/utils/run_renderer.py +22 -1086
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/METADATA +9 -37
- glaip_sdk-0.0.3.dist-info/RECORD +40 -0
- glaip_sdk/cli/config.py +0 -592
- glaip_sdk/utils.py +0 -167
- glaip_sdk-0.0.1b10.dist-info/RECORD +0 -28
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Configuration types for the renderer package.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RendererConfig:
|
|
14
|
+
"""Configuration for the RichStreamRenderer."""
|
|
15
|
+
|
|
16
|
+
# Style and layout
|
|
17
|
+
theme: str = "dark" # dark|light
|
|
18
|
+
style: str = "pretty" # pretty|debug|minimal
|
|
19
|
+
|
|
20
|
+
# Performance
|
|
21
|
+
think_threshold: float = 0.7
|
|
22
|
+
refresh_debounce: float = 0.25
|
|
23
|
+
render_thinking: bool = True
|
|
24
|
+
live: bool = True
|
|
25
|
+
persist_live: bool = True
|
|
26
|
+
|
|
27
|
+
# Debug visibility toggles
|
|
28
|
+
show_delegate_tool_panels: bool = False
|
|
29
|
+
|
|
30
|
+
# Scrollback/append options
|
|
31
|
+
append_finished_snapshots: bool = False
|
|
32
|
+
snapshot_max_chars: int = 4000
|
|
33
|
+
snapshot_max_lines: int = 60
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Console handling utilities for the renderer package.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
|
|
11
|
+
from rich.console import Console as RichConsole
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CapturingConsole:
|
|
15
|
+
"""Console wrapper that captures all output for saving."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, original_console, capture=False):
|
|
18
|
+
"""Initialize the capturing console.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
original_console: The original Rich console instance
|
|
22
|
+
capture: Whether to capture output in addition to displaying it
|
|
23
|
+
"""
|
|
24
|
+
self.original_console = original_console
|
|
25
|
+
self.capture = capture
|
|
26
|
+
self.captured_output = []
|
|
27
|
+
|
|
28
|
+
def print(self, *args, **kwargs):
|
|
29
|
+
"""Print to both original console and capture buffer if capturing."""
|
|
30
|
+
# Always print to original console
|
|
31
|
+
self.original_console.print(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
if self.capture:
|
|
34
|
+
# Capture the output as text
|
|
35
|
+
# Create a temporary console to capture output
|
|
36
|
+
temp_output = io.StringIO()
|
|
37
|
+
temp_console = RichConsole(
|
|
38
|
+
file=temp_output,
|
|
39
|
+
width=self.original_console.size.width,
|
|
40
|
+
legacy_windows=False,
|
|
41
|
+
force_terminal=False,
|
|
42
|
+
)
|
|
43
|
+
temp_console.print(*args, **kwargs)
|
|
44
|
+
self.captured_output.append(temp_output.getvalue())
|
|
45
|
+
|
|
46
|
+
def get_captured_output(self):
|
|
47
|
+
"""Get the captured output as plain text."""
|
|
48
|
+
if self.capture:
|
|
49
|
+
return "".join(self.captured_output)
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
def __getattr__(self, name):
|
|
53
|
+
"""Delegate all other attributes to the original console."""
|
|
54
|
+
return getattr(self.original_console, name)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Debug rendering utilities for verbose SSE event display.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_debug_event(
|
|
18
|
+
event: dict[str, Any], console: Console, started_ts: float | None = None
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Render a debug panel for an SSE event.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
event: The SSE event data
|
|
24
|
+
console: Rich console to print to
|
|
25
|
+
started_ts: Monotonic timestamp when streaming started
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
# Add relative time since first meaningful event and wall-clock stamp
|
|
29
|
+
now_mono = monotonic()
|
|
30
|
+
rel = 0.0
|
|
31
|
+
if started_ts is not None:
|
|
32
|
+
rel = max(0.0, now_mono - started_ts)
|
|
33
|
+
ts_full = datetime.now().strftime("%H:%M:%S.%f")
|
|
34
|
+
ts_ms = ts_full[:-3] # trim to milliseconds
|
|
35
|
+
|
|
36
|
+
# Compose a descriptive title with kind/status
|
|
37
|
+
sse_kind = (event.get("metadata") or {}).get("kind") or "event"
|
|
38
|
+
status_str = event.get("status") or (event.get("metadata") or {}).get("status")
|
|
39
|
+
title = (
|
|
40
|
+
f"SSE: {sse_kind} — {status_str} @ {ts_ms} (+{rel:.2f}s)"
|
|
41
|
+
if status_str
|
|
42
|
+
else f"SSE: {sse_kind} @ {ts_ms} (+{rel:.2f}s)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Deep-pretty the event by parsing nested JSON strings
|
|
46
|
+
def _dejson(obj):
|
|
47
|
+
if isinstance(obj, dict):
|
|
48
|
+
return {k: _dejson(v) for k, v in obj.items()}
|
|
49
|
+
if isinstance(obj, list):
|
|
50
|
+
return [_dejson(x) for x in obj]
|
|
51
|
+
if isinstance(obj, str):
|
|
52
|
+
s = obj.strip()
|
|
53
|
+
if (s.startswith("{") and s.endswith("}")) or (
|
|
54
|
+
s.startswith("[") and s.endswith("]")
|
|
55
|
+
):
|
|
56
|
+
try:
|
|
57
|
+
return _dejson(json.loads(s))
|
|
58
|
+
except Exception:
|
|
59
|
+
return obj
|
|
60
|
+
return obj
|
|
61
|
+
return obj
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
event_json = json.dumps(_dejson(event), indent=2, ensure_ascii=False)
|
|
65
|
+
except Exception:
|
|
66
|
+
event_json = str(event)
|
|
67
|
+
|
|
68
|
+
# Choose border color by kind for readability
|
|
69
|
+
border = {
|
|
70
|
+
"agent_step": "blue",
|
|
71
|
+
"content": "green",
|
|
72
|
+
"final_response": "green",
|
|
73
|
+
"status": "yellow",
|
|
74
|
+
"artifact": "grey42",
|
|
75
|
+
}.get(sse_kind, "grey42")
|
|
76
|
+
|
|
77
|
+
# Render using Markdown with JSON code block (consistent with tool panels)
|
|
78
|
+
md = Markdown(f"```json\n{event_json}\n```", code_theme="monokai")
|
|
79
|
+
console.print(Panel(md, title=title, border_style=border))
|
|
80
|
+
except Exception as e:
|
|
81
|
+
# Debug helpers must not break streaming
|
|
82
|
+
print(f"Debug error: {e}") # Fallback debug output
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Panel rendering utilities for the renderer package.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_main_panel(content: str, title: str, theme: str = "dark") -> Panel:
|
|
15
|
+
"""Create a main content panel.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
content: The content to display
|
|
19
|
+
title: Panel title
|
|
20
|
+
theme: Color theme ("dark" or "light")
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Rich Panel instance
|
|
24
|
+
"""
|
|
25
|
+
if content.strip():
|
|
26
|
+
return Panel(
|
|
27
|
+
Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
|
|
28
|
+
title=title,
|
|
29
|
+
border_style="green",
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
# Placeholder panel
|
|
33
|
+
placeholder = Text("Processing...", style="dim")
|
|
34
|
+
return Panel(
|
|
35
|
+
placeholder,
|
|
36
|
+
title=title,
|
|
37
|
+
border_style="green",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_tool_panel(
|
|
42
|
+
title: str,
|
|
43
|
+
content: str,
|
|
44
|
+
status: str = "running",
|
|
45
|
+
theme: str = "dark",
|
|
46
|
+
is_delegation: bool = False,
|
|
47
|
+
) -> Panel:
|
|
48
|
+
"""Create a tool execution panel.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
title: Tool name/title
|
|
52
|
+
content: Tool output content
|
|
53
|
+
status: Tool execution status
|
|
54
|
+
theme: Color theme
|
|
55
|
+
is_delegation: Whether this is a delegation tool
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Rich Panel instance
|
|
59
|
+
"""
|
|
60
|
+
mark = "✓" if status == "finished" else "⟳"
|
|
61
|
+
border_style = "magenta" if is_delegation else "blue"
|
|
62
|
+
|
|
63
|
+
return Panel(
|
|
64
|
+
Markdown(
|
|
65
|
+
content or "Processing...",
|
|
66
|
+
code_theme=("monokai" if theme == "dark" else "github"),
|
|
67
|
+
),
|
|
68
|
+
title=f"{title} {mark}",
|
|
69
|
+
border_style=border_style,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_context_panel(
|
|
74
|
+
title: str,
|
|
75
|
+
content: str,
|
|
76
|
+
status: str = "running",
|
|
77
|
+
theme: str = "dark",
|
|
78
|
+
is_delegation: bool = False,
|
|
79
|
+
) -> Panel:
|
|
80
|
+
"""Create a context/sub-agent panel.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
title: Context title
|
|
84
|
+
content: Context content
|
|
85
|
+
status: Execution status
|
|
86
|
+
theme: Color theme
|
|
87
|
+
is_delegation: Whether this is a delegation context
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Rich Panel instance
|
|
91
|
+
"""
|
|
92
|
+
mark = "✓" if status == "finished" else "⟳"
|
|
93
|
+
border_style = "magenta" if is_delegation else "cyan"
|
|
94
|
+
|
|
95
|
+
return Panel(
|
|
96
|
+
Markdown(
|
|
97
|
+
content,
|
|
98
|
+
code_theme=("monokai" if theme == "dark" else "github"),
|
|
99
|
+
),
|
|
100
|
+
title=f"{title} {mark}",
|
|
101
|
+
border_style=border_style,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_final_panel(
|
|
106
|
+
content: str, title: str = "Final Result", theme: str = "dark"
|
|
107
|
+
) -> Panel:
|
|
108
|
+
"""Create a final result panel.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
content: Final result content
|
|
112
|
+
title: Panel title
|
|
113
|
+
theme: Color theme
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Rich Panel instance
|
|
117
|
+
"""
|
|
118
|
+
return Panel(
|
|
119
|
+
Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
|
|
120
|
+
title=title,
|
|
121
|
+
border_style="green",
|
|
122
|
+
padding=(0, 1),
|
|
123
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Progress and timing utilities for the renderer package.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from time import monotonic
|
|
10
|
+
|
|
11
|
+
from glaip_sdk.utils.rendering.formatting import get_spinner_char
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_spinner() -> str:
|
|
15
|
+
"""Return the current animated spinner character for visual feedback."""
|
|
16
|
+
return get_spinner_char()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_working_indicator(
|
|
20
|
+
started_at: float | None,
|
|
21
|
+
server_elapsed_time: float | None = None,
|
|
22
|
+
streaming_started_at: float | None = None,
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Format a working indicator with elapsed time.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
started_at: Timestamp when work started, or None
|
|
28
|
+
server_elapsed_time: Server-reported elapsed time if available
|
|
29
|
+
streaming_started_at: When streaming started
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Formatted working indicator string with elapsed time
|
|
33
|
+
"""
|
|
34
|
+
chip = "Working..."
|
|
35
|
+
|
|
36
|
+
# Use server timing if available (more accurate)
|
|
37
|
+
if server_elapsed_time is not None and streaming_started_at is not None:
|
|
38
|
+
elapsed = server_elapsed_time
|
|
39
|
+
elif started_at:
|
|
40
|
+
try:
|
|
41
|
+
elapsed = monotonic() - started_at
|
|
42
|
+
except Exception:
|
|
43
|
+
return chip
|
|
44
|
+
else:
|
|
45
|
+
return chip
|
|
46
|
+
|
|
47
|
+
if elapsed >= 1:
|
|
48
|
+
chip = f"Working... ({elapsed:.2f}s)"
|
|
49
|
+
else:
|
|
50
|
+
elapsed_ms = int(elapsed * 1000)
|
|
51
|
+
chip = f"Working... ({elapsed_ms}ms)" if elapsed_ms > 0 else "Working... (<1ms)"
|
|
52
|
+
return chip
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_elapsed_time(elapsed_seconds: float) -> str:
|
|
56
|
+
"""Format elapsed time in a human-readable format.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
elapsed_seconds: Time in seconds
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Formatted time string
|
|
63
|
+
"""
|
|
64
|
+
if elapsed_seconds >= 60:
|
|
65
|
+
minutes = int(elapsed_seconds // 60)
|
|
66
|
+
seconds = elapsed_seconds % 60
|
|
67
|
+
return f"{minutes}m {seconds:.1f}s"
|
|
68
|
+
elif elapsed_seconds >= 1:
|
|
69
|
+
return f"{elapsed_seconds:.2f}s"
|
|
70
|
+
else:
|
|
71
|
+
ms = int(elapsed_seconds * 1000)
|
|
72
|
+
return f"{ms}ms" if ms > 0 else "<1ms"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_delegation_tool(tool_name: str) -> bool:
|
|
76
|
+
"""Check if a tool name indicates delegation functionality.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
tool_name: The name of the tool to check
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if this is a delegation tool
|
|
83
|
+
"""
|
|
84
|
+
return (
|
|
85
|
+
tool_name.startswith("delegate_to_")
|
|
86
|
+
or tool_name.startswith("delegate_")
|
|
87
|
+
or "sub_agent" in tool_name.lower()
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def format_tool_title(tool_name: str) -> str:
|
|
92
|
+
"""Format tool name for panel title display.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
tool_name: The full tool name (may include file paths)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Formatted title string suitable for panel display
|
|
99
|
+
"""
|
|
100
|
+
# Check if this is a delegation tool
|
|
101
|
+
if is_delegation_tool(tool_name):
|
|
102
|
+
# Extract the sub-agent name from delegation tool names
|
|
103
|
+
if tool_name.startswith("delegate_to_"):
|
|
104
|
+
sub_agent_name = tool_name.replace("delegate_to_", "")
|
|
105
|
+
return f"Sub-Agent: {sub_agent_name}"
|
|
106
|
+
elif tool_name.startswith("delegate_"):
|
|
107
|
+
sub_agent_name = tool_name.replace("delegate_", "")
|
|
108
|
+
return f"Sub-Agent: {sub_agent_name}"
|
|
109
|
+
|
|
110
|
+
# For regular tools, clean up the name
|
|
111
|
+
# Remove file path prefixes if present
|
|
112
|
+
if "/" in tool_name:
|
|
113
|
+
tool_name = tool_name.split("/")[-1]
|
|
114
|
+
if "." in tool_name:
|
|
115
|
+
tool_name = tool_name.split(".")[0]
|
|
116
|
+
|
|
117
|
+
# Convert snake_case to Title Case
|
|
118
|
+
return tool_name.replace("_", " ").title()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Event routing and parsing utilities for the renderer package.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StreamProcessor:
|
|
14
|
+
"""Handles event routing and parsing for streaming agent execution."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize the stream processor."""
|
|
18
|
+
self.streaming_started_at: float | None = None
|
|
19
|
+
self.server_elapsed_time: float | None = None
|
|
20
|
+
self.current_event_tools: set[str] = set()
|
|
21
|
+
self.current_event_sub_agents: set[str] = set()
|
|
22
|
+
self.current_event_finished_panels: set[str] = set()
|
|
23
|
+
self.last_event_time_by_ctx: dict[str, float] = {}
|
|
24
|
+
|
|
25
|
+
def reset_event_tracking(self):
|
|
26
|
+
"""Reset tracking for the current event."""
|
|
27
|
+
self.current_event_tools.clear()
|
|
28
|
+
self.current_event_sub_agents.clear()
|
|
29
|
+
self.current_event_finished_panels.clear()
|
|
30
|
+
|
|
31
|
+
def extract_event_metadata(self, event: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
"""Extract metadata from an event.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
event: Event dictionary
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dictionary with extracted metadata
|
|
39
|
+
"""
|
|
40
|
+
metadata = event.get("metadata", {})
|
|
41
|
+
# Update server elapsed timing if backend provides it
|
|
42
|
+
try:
|
|
43
|
+
t = metadata.get("time")
|
|
44
|
+
if isinstance(t, int | float):
|
|
45
|
+
self.server_elapsed_time = float(t)
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"kind": metadata.get("kind") if metadata else None,
|
|
51
|
+
"task_id": event.get("task_id"),
|
|
52
|
+
"context_id": event.get("context_id"),
|
|
53
|
+
"content": event.get("content", ""),
|
|
54
|
+
"status": metadata.get("status") if metadata else event.get("status"),
|
|
55
|
+
"metadata": metadata,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def parse_tool_calls(
|
|
59
|
+
self, event: dict[str, Any]
|
|
60
|
+
) -> tuple[str | None, Any, Any, list]:
|
|
61
|
+
"""Parse tool call information from an event.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
event: Event dictionary
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (tool_name, tool_args, tool_output, tool_calls_info)
|
|
68
|
+
"""
|
|
69
|
+
tool_name = None
|
|
70
|
+
tool_args = {}
|
|
71
|
+
tool_out = None
|
|
72
|
+
tool_calls_info = []
|
|
73
|
+
|
|
74
|
+
# Extract tool information from metadata
|
|
75
|
+
metadata = event.get("metadata", {})
|
|
76
|
+
tool_calls = metadata.get("tool_calls", [])
|
|
77
|
+
|
|
78
|
+
if tool_calls:
|
|
79
|
+
# Take the first tool call if multiple exist
|
|
80
|
+
first_call = tool_calls[0] if isinstance(tool_calls, list) else tool_calls
|
|
81
|
+
tool_name = first_call.get("name")
|
|
82
|
+
tool_args = first_call.get("arguments", {})
|
|
83
|
+
tool_out = first_call.get("output")
|
|
84
|
+
|
|
85
|
+
# Collect info for all tool calls
|
|
86
|
+
for call in tool_calls if isinstance(tool_calls, list) else [tool_calls]:
|
|
87
|
+
if isinstance(call, dict) and "name" in call:
|
|
88
|
+
tool_calls_info.append(
|
|
89
|
+
(
|
|
90
|
+
call.get("name", ""),
|
|
91
|
+
call.get("arguments", {}),
|
|
92
|
+
call.get("output"),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Fallback to nested metadata.tool_info (newer schema)
|
|
97
|
+
if not tool_calls_info:
|
|
98
|
+
tool_info = metadata.get("tool_info", {}) or {}
|
|
99
|
+
# Case 1: tool_info.tool_calls
|
|
100
|
+
ti_calls = tool_info.get("tool_calls")
|
|
101
|
+
if isinstance(ti_calls, list) and ti_calls:
|
|
102
|
+
for call in ti_calls:
|
|
103
|
+
if isinstance(call, dict) and call.get("name"):
|
|
104
|
+
tool_calls_info.append(
|
|
105
|
+
(call.get("name"), call.get("args", {}), call.get("output"))
|
|
106
|
+
)
|
|
107
|
+
if tool_calls_info and not tool_name:
|
|
108
|
+
tool_name, tool_args, tool_out = tool_calls_info[0]
|
|
109
|
+
# Case 2: single tool_info name/args/output
|
|
110
|
+
if tool_info.get("name") and not tool_name:
|
|
111
|
+
tool_name = tool_info.get("name")
|
|
112
|
+
tool_args = tool_info.get("args", {})
|
|
113
|
+
tool_out = tool_info.get("output")
|
|
114
|
+
tool_calls_info.append((tool_name, tool_args, tool_out))
|
|
115
|
+
|
|
116
|
+
return tool_name, tool_args, tool_out, tool_calls_info
|
|
117
|
+
|
|
118
|
+
def update_timing(self, context_id: str | None):
|
|
119
|
+
"""Update timing information for the given context.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
context_id: Context identifier
|
|
123
|
+
"""
|
|
124
|
+
if context_id:
|
|
125
|
+
self.last_event_time_by_ctx[context_id] = monotonic()
|
|
126
|
+
|
|
127
|
+
def should_insert_thinking_gap(
|
|
128
|
+
self, task_id: str | None, context_id: str | None, think_threshold: float
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""Determine if a thinking gap should be inserted.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
task_id: Task identifier
|
|
134
|
+
context_id: Context identifier
|
|
135
|
+
think_threshold: Threshold for thinking gap
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if thinking gap should be inserted
|
|
139
|
+
"""
|
|
140
|
+
if not task_id or not context_id:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
last_time = self.last_event_time_by_ctx.get(context_id)
|
|
144
|
+
if last_time is None:
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
elapsed = monotonic() - last_time
|
|
148
|
+
return elapsed >= think_threshold
|
|
149
|
+
|
|
150
|
+
def track_tools_and_agents(
|
|
151
|
+
self, tool_name: str | None, tool_calls_info: list, is_delegation_tool_func
|
|
152
|
+
):
|
|
153
|
+
"""Track tools and sub-agents mentioned in the current event.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
tool_name: Primary tool name
|
|
157
|
+
tool_calls_info: List of tool call information
|
|
158
|
+
is_delegation_tool_func: Function to check if tool is delegation
|
|
159
|
+
"""
|
|
160
|
+
# Track all tools mentioned in this event
|
|
161
|
+
if tool_name:
|
|
162
|
+
self.current_event_tools.add(tool_name)
|
|
163
|
+
# If it's a delegation tool, add the sub-agent name
|
|
164
|
+
if is_delegation_tool_func(tool_name):
|
|
165
|
+
sub_agent_name = self._extract_sub_agent_name(tool_name)
|
|
166
|
+
self.current_event_sub_agents.add(sub_agent_name)
|
|
167
|
+
|
|
168
|
+
if tool_calls_info:
|
|
169
|
+
for tool_call_name, _, _ in tool_calls_info:
|
|
170
|
+
self.current_event_tools.add(tool_call_name)
|
|
171
|
+
# If it's a delegation tool, add the sub-agent name
|
|
172
|
+
if is_delegation_tool_func(tool_call_name):
|
|
173
|
+
sub_agent_name = self._extract_sub_agent_name(tool_call_name)
|
|
174
|
+
self.current_event_sub_agents.add(sub_agent_name)
|
|
175
|
+
|
|
176
|
+
def _extract_sub_agent_name(self, tool_name: str) -> str:
|
|
177
|
+
"""Extract sub-agent name from delegation tool name.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
tool_name: Delegation tool name
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Sub-agent name
|
|
184
|
+
"""
|
|
185
|
+
if tool_name.startswith("delegate_to_"):
|
|
186
|
+
return tool_name.replace("delegate_to_", "")
|
|
187
|
+
elif tool_name.startswith("delegate_"):
|
|
188
|
+
return tool_name.replace("delegate_", "")
|
|
189
|
+
else:
|
|
190
|
+
return tool_name
|
|
191
|
+
|
|
192
|
+
def get_current_event_tools(self) -> set[str]:
|
|
193
|
+
"""Get the set of tools mentioned in the current event."""
|
|
194
|
+
return self.current_event_tools.copy()
|
|
195
|
+
|
|
196
|
+
def get_current_event_sub_agents(self) -> set[str]:
|
|
197
|
+
"""Get the set of sub-agents mentioned in the current event."""
|
|
198
|
+
return self.current_event_sub_agents.copy()
|