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.
@@ -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 = 12000
33
- snapshot_max_lines: int = 200
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)