glaip-sdk 0.0.20__py3-none-any.whl → 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.
- glaip_sdk/cli/commands/agents.py +19 -0
- glaip_sdk/cli/commands/mcps.py +1 -2
- glaip_sdk/cli/slash/session.py +0 -3
- glaip_sdk/cli/transcript/viewer.py +176 -6
- glaip_sdk/cli/utils.py +0 -1
- glaip_sdk/client/run_rendering.py +125 -20
- glaip_sdk/icons.py +9 -3
- glaip_sdk/utils/rendering/formatting.py +50 -7
- glaip_sdk/utils/rendering/models.py +15 -2
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -2
- glaip_sdk/utils/rendering/renderer/base.py +1131 -218
- glaip_sdk/utils/rendering/renderer/config.py +3 -5
- glaip_sdk/utils/rendering/renderer/stream.py +3 -3
- glaip_sdk/utils/rendering/renderer/toggle.py +184 -0
- glaip_sdk/utils/rendering/step_tree_state.py +102 -0
- glaip_sdk/utils/rendering/steps.py +944 -16
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/METADATA +12 -1
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/RECORD +20 -18
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -24,10 +24,8 @@ class RendererConfig:
|
|
|
24
24
|
live: bool = True
|
|
25
25
|
persist_live: bool = True
|
|
26
26
|
|
|
27
|
-
# Debug visibility toggles
|
|
28
|
-
show_delegate_tool_panels: bool = False
|
|
29
|
-
|
|
30
27
|
# Scrollback/append options
|
|
28
|
+
summary_max_steps: int = 0
|
|
31
29
|
append_finished_snapshots: bool = False
|
|
32
|
-
snapshot_max_chars: int =
|
|
33
|
-
snapshot_max_lines: int =
|
|
30
|
+
snapshot_max_chars: int = 0
|
|
31
|
+
snapshot_max_lines: int = 0
|
|
@@ -38,7 +38,7 @@ class StreamProcessor:
|
|
|
38
38
|
Returns:
|
|
39
39
|
Dictionary with extracted metadata
|
|
40
40
|
"""
|
|
41
|
-
metadata = event.get("metadata"
|
|
41
|
+
metadata = event.get("metadata") or {}
|
|
42
42
|
# Update server elapsed timing if backend provides it
|
|
43
43
|
try:
|
|
44
44
|
t = metadata.get("time")
|
|
@@ -49,8 +49,8 @@ class StreamProcessor:
|
|
|
49
49
|
|
|
50
50
|
return {
|
|
51
51
|
"kind": metadata.get("kind") if metadata else event.get("kind"),
|
|
52
|
-
"task_id": event.get("task_id"),
|
|
53
|
-
"context_id": event.get("context_id"),
|
|
52
|
+
"task_id": metadata.get("task_id") or event.get("task_id"),
|
|
53
|
+
"context_id": metadata.get("context_id") or event.get("context_id"),
|
|
54
54
|
"content": event.get("content", ""),
|
|
55
55
|
"status": metadata.get("status") if metadata else event.get("status"),
|
|
56
56
|
"metadata": metadata,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Keyboard-driven transcript toggling support for the live renderer.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
try: # pragma: no cover - Windows-specific dependencies
|
|
16
|
+
import msvcrt # type: ignore[import]
|
|
17
|
+
except ImportError: # pragma: no cover - POSIX fallback
|
|
18
|
+
msvcrt = None # type: ignore[assignment]
|
|
19
|
+
|
|
20
|
+
if os.name != "nt": # pragma: no cover - POSIX-only imports
|
|
21
|
+
import select
|
|
22
|
+
import termios
|
|
23
|
+
import tty
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
CTRL_T = "\x14"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TranscriptToggleController:
|
|
30
|
+
"""Manage mid-run transcript toggling for RichStreamRenderer instances."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, enabled: bool) -> None:
|
|
33
|
+
"""Initialise controller.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
enabled: Whether toggling should be active (usually gated by TTY checks).
|
|
37
|
+
"""
|
|
38
|
+
self._enabled = enabled and bool(sys.stdin) and sys.stdin.isatty()
|
|
39
|
+
self._lock = threading.Lock()
|
|
40
|
+
self._posix_fd: int | None = None
|
|
41
|
+
self._posix_attrs: list[int] | None = None
|
|
42
|
+
self._active = False
|
|
43
|
+
self._stop_event = threading.Event()
|
|
44
|
+
self._poll_thread: threading.Thread | None = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def enabled(self) -> bool:
|
|
48
|
+
"""Return True when controller is able to process keypresses."""
|
|
49
|
+
return self._enabled
|
|
50
|
+
|
|
51
|
+
def on_stream_start(self, renderer: Any) -> None:
|
|
52
|
+
"""Prepare terminal state before streaming begins."""
|
|
53
|
+
if not self._enabled:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if os.name == "nt": # pragma: no cover - Windows behaviour not in CI
|
|
57
|
+
self._active = True
|
|
58
|
+
self._start_polling_thread(renderer)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
fd = sys.stdin.fileno()
|
|
62
|
+
try:
|
|
63
|
+
attrs = termios.tcgetattr(fd)
|
|
64
|
+
except Exception:
|
|
65
|
+
self._enabled = False
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
tty.setcbreak(fd)
|
|
70
|
+
except Exception:
|
|
71
|
+
try:
|
|
72
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
self._enabled = False
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
with self._lock:
|
|
79
|
+
self._posix_fd = fd
|
|
80
|
+
self._posix_attrs = attrs
|
|
81
|
+
self._active = True
|
|
82
|
+
|
|
83
|
+
self._start_polling_thread(renderer)
|
|
84
|
+
|
|
85
|
+
def on_stream_complete(self) -> None:
|
|
86
|
+
"""Restore terminal state when streaming ends."""
|
|
87
|
+
if not self._active:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self._stop_polling_thread()
|
|
91
|
+
|
|
92
|
+
if os.name == "nt": # pragma: no cover - Windows behaviour not in CI
|
|
93
|
+
self._active = False
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
with self._lock:
|
|
97
|
+
fd = self._posix_fd
|
|
98
|
+
attrs = self._posix_attrs
|
|
99
|
+
self._posix_fd = None
|
|
100
|
+
self._posix_attrs = None
|
|
101
|
+
self._active = False
|
|
102
|
+
|
|
103
|
+
if fd is None or attrs is None:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def poll(self, renderer: Any) -> None:
|
|
112
|
+
"""Poll for toggle keypresses and update renderer if needed."""
|
|
113
|
+
if not self._active:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if os.name == "nt": # pragma: no cover - Windows behaviour not in CI
|
|
117
|
+
self._poll_windows(renderer)
|
|
118
|
+
else:
|
|
119
|
+
self._poll_posix(renderer)
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# Platform-specific polling
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
def _poll_windows(self, renderer: Any) -> None:
|
|
125
|
+
if not msvcrt: # pragma: no cover - safety guard
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
while msvcrt.kbhit():
|
|
129
|
+
ch = msvcrt.getwch()
|
|
130
|
+
if ch == CTRL_T:
|
|
131
|
+
renderer.toggle_transcript_mode()
|
|
132
|
+
|
|
133
|
+
def _poll_posix(self, renderer: Any) -> None: # pragma: no cover - requires TTY
|
|
134
|
+
fd = self._posix_fd
|
|
135
|
+
if fd is None:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
while True:
|
|
139
|
+
readable, _, _ = select.select([fd], [], [], 0)
|
|
140
|
+
if not readable:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
data = os.read(fd, 1)
|
|
145
|
+
except Exception:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if not data:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
ch = data.decode(errors="ignore")
|
|
152
|
+
if ch == CTRL_T:
|
|
153
|
+
renderer.toggle_transcript_mode()
|
|
154
|
+
|
|
155
|
+
def _start_polling_thread(self, renderer: Any) -> None:
|
|
156
|
+
if self._poll_thread and self._poll_thread.is_alive():
|
|
157
|
+
return
|
|
158
|
+
if not self._active:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
self._stop_event.clear()
|
|
162
|
+
self._poll_thread = threading.Thread(
|
|
163
|
+
target=self._poll_loop, args=(renderer,), daemon=True
|
|
164
|
+
)
|
|
165
|
+
self._poll_thread.start()
|
|
166
|
+
|
|
167
|
+
def _stop_polling_thread(self) -> None:
|
|
168
|
+
self._stop_event.set()
|
|
169
|
+
thread = self._poll_thread
|
|
170
|
+
if thread and thread.is_alive():
|
|
171
|
+
thread.join(timeout=0.2)
|
|
172
|
+
self._poll_thread = None
|
|
173
|
+
|
|
174
|
+
def _poll_loop(self, renderer: Any) -> None:
|
|
175
|
+
while self._active and not self._stop_event.is_set():
|
|
176
|
+
try:
|
|
177
|
+
if os.name == "nt":
|
|
178
|
+
self._poll_windows(renderer)
|
|
179
|
+
else:
|
|
180
|
+
self._poll_posix(renderer)
|
|
181
|
+
except Exception:
|
|
182
|
+
# Never let background polling disrupt the main stream
|
|
183
|
+
pass
|
|
184
|
+
time.sleep(0.05)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""State container for hierarchical renderer steps.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.utils.rendering.models import Step
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class StepTreeState:
|
|
17
|
+
"""Track hierarchical ordering, buffers, and pruning metadata."""
|
|
18
|
+
|
|
19
|
+
max_steps: int = 200
|
|
20
|
+
root_order: list[str] = field(default_factory=list)
|
|
21
|
+
child_map: dict[str, list[str]] = field(default_factory=dict)
|
|
22
|
+
buffered_children: dict[str, list[str]] = field(default_factory=dict)
|
|
23
|
+
running_by_context: dict[tuple[str | None, str | None], set[str]] = field(
|
|
24
|
+
default_factory=dict
|
|
25
|
+
)
|
|
26
|
+
retained_ids: set[str] = field(default_factory=set)
|
|
27
|
+
step_index: dict[str, Step] = field(default_factory=dict)
|
|
28
|
+
pending_branch_failures: set[str] = field(default_factory=set)
|
|
29
|
+
|
|
30
|
+
def link_root(self, step_id: str) -> None:
|
|
31
|
+
"""Ensure a step id is present in the root ordering."""
|
|
32
|
+
if step_id not in self.root_order:
|
|
33
|
+
self.root_order.append(step_id)
|
|
34
|
+
|
|
35
|
+
def unlink_root(self, step_id: str) -> None:
|
|
36
|
+
"""Remove a step id from the root ordering if present."""
|
|
37
|
+
if step_id in self.root_order:
|
|
38
|
+
self.root_order.remove(step_id)
|
|
39
|
+
|
|
40
|
+
def link_child(self, parent_id: str, child_id: str) -> None:
|
|
41
|
+
"""Attach a child step to a parent."""
|
|
42
|
+
children = self.child_map.setdefault(parent_id, [])
|
|
43
|
+
if child_id not in children:
|
|
44
|
+
children.append(child_id)
|
|
45
|
+
|
|
46
|
+
def unlink_child(self, parent_id: str, child_id: str) -> None:
|
|
47
|
+
"""Detach a child from a parent."""
|
|
48
|
+
children = self.child_map.get(parent_id)
|
|
49
|
+
if not children:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if child_id in children:
|
|
53
|
+
children.remove(child_id)
|
|
54
|
+
# Clean up if the list is now empty
|
|
55
|
+
if len(children) == 0:
|
|
56
|
+
self.child_map.pop(parent_id, None)
|
|
57
|
+
|
|
58
|
+
def buffer_child(self, parent_id: str, child_id: str) -> None:
|
|
59
|
+
"""Track a child that is waiting for its parent to appear."""
|
|
60
|
+
queue = self.buffered_children.setdefault(parent_id, [])
|
|
61
|
+
if child_id not in queue:
|
|
62
|
+
queue.append(child_id)
|
|
63
|
+
|
|
64
|
+
def pop_buffered_children(self, parent_id: str) -> list[str]:
|
|
65
|
+
"""Return any buffered children for a parent."""
|
|
66
|
+
return self.buffered_children.pop(parent_id, [])
|
|
67
|
+
|
|
68
|
+
def discard_running(self, step_id: str) -> None:
|
|
69
|
+
"""Remove a step from running context tracking."""
|
|
70
|
+
for key, running in tuple(self.running_by_context.items()):
|
|
71
|
+
if step_id in running:
|
|
72
|
+
running.discard(step_id)
|
|
73
|
+
if not running:
|
|
74
|
+
self.running_by_context.pop(key, None)
|
|
75
|
+
|
|
76
|
+
def iter_visible_tree(self) -> Iterator[tuple[str, tuple[bool, ...]]]:
|
|
77
|
+
"""Yield step ids in depth-first order alongside branch metadata.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Iterator of (step_id, branch_state) tuples where branch_state
|
|
81
|
+
captures whether each ancestor was the last child. This data
|
|
82
|
+
is later used by rendering helpers to draw connectors such as
|
|
83
|
+
`│`, `├─`, and `└─` consistently.
|
|
84
|
+
"""
|
|
85
|
+
roots = tuple(self.root_order)
|
|
86
|
+
total_roots = len(roots)
|
|
87
|
+
for index, root_id in enumerate(roots):
|
|
88
|
+
yield root_id, ()
|
|
89
|
+
ancestor_state = (index == total_roots - 1,)
|
|
90
|
+
yield from self._walk_children(root_id, ancestor_state)
|
|
91
|
+
|
|
92
|
+
def _walk_children(
|
|
93
|
+
self, parent_id: str, ancestor_state: tuple[bool, ...]
|
|
94
|
+
) -> Iterator[tuple[str, tuple[bool, ...]]]:
|
|
95
|
+
"""Depth-first traversal helper yielding children with ancestry info."""
|
|
96
|
+
children = self.child_map.get(parent_id, [])
|
|
97
|
+
total_children = len(children)
|
|
98
|
+
for idx, child_id in enumerate(children):
|
|
99
|
+
is_last = idx == total_children - 1
|
|
100
|
+
branch_state = ancestor_state + (is_last,)
|
|
101
|
+
yield child_id, branch_state
|
|
102
|
+
yield from self._walk_children(child_id, branch_state)
|