glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__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 (216) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1250 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +271 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  12. glaip_sdk/cli/commands/agents/_common.py +561 -0
  13. glaip_sdk/cli/commands/agents/create.py +151 -0
  14. glaip_sdk/cli/commands/agents/delete.py +64 -0
  15. glaip_sdk/cli/commands/agents/get.py +89 -0
  16. glaip_sdk/cli/commands/agents/list.py +129 -0
  17. glaip_sdk/cli/commands/agents/run.py +264 -0
  18. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  19. glaip_sdk/cli/commands/agents/update.py +112 -0
  20. glaip_sdk/cli/commands/common_config.py +104 -0
  21. glaip_sdk/cli/commands/configure.py +734 -143
  22. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  23. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  24. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  25. glaip_sdk/cli/commands/mcps/create.py +152 -0
  26. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  27. glaip_sdk/cli/commands/mcps/get.py +212 -0
  28. glaip_sdk/cli/commands/mcps/list.py +69 -0
  29. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  30. glaip_sdk/cli/commands/mcps/update.py +190 -0
  31. glaip_sdk/cli/commands/models.py +14 -12
  32. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  33. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  34. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  35. glaip_sdk/cli/commands/tools/_common.py +80 -0
  36. glaip_sdk/cli/commands/tools/create.py +228 -0
  37. glaip_sdk/cli/commands/tools/delete.py +61 -0
  38. glaip_sdk/cli/commands/tools/get.py +103 -0
  39. glaip_sdk/cli/commands/tools/list.py +69 -0
  40. glaip_sdk/cli/commands/tools/script.py +49 -0
  41. glaip_sdk/cli/commands/tools/update.py +102 -0
  42. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  43. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  44. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  45. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  46. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  47. glaip_sdk/cli/commands/update.py +164 -23
  48. glaip_sdk/cli/config.py +49 -7
  49. glaip_sdk/cli/constants.py +38 -0
  50. glaip_sdk/cli/context.py +8 -0
  51. glaip_sdk/cli/core/__init__.py +79 -0
  52. glaip_sdk/cli/core/context.py +124 -0
  53. glaip_sdk/cli/core/output.py +851 -0
  54. glaip_sdk/cli/core/prompting.py +649 -0
  55. glaip_sdk/cli/core/rendering.py +187 -0
  56. glaip_sdk/cli/display.py +45 -32
  57. glaip_sdk/cli/entrypoint.py +20 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +14 -17
  60. glaip_sdk/cli/main.py +344 -167
  61. glaip_sdk/cli/masking.py +21 -33
  62. glaip_sdk/cli/mcp_validators.py +5 -15
  63. glaip_sdk/cli/pager.py +15 -22
  64. glaip_sdk/cli/parsers/__init__.py +1 -3
  65. glaip_sdk/cli/parsers/json_input.py +11 -22
  66. glaip_sdk/cli/resolution.py +5 -10
  67. glaip_sdk/cli/rich_helpers.py +1 -3
  68. glaip_sdk/cli/slash/__init__.py +0 -9
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +65 -29
  72. glaip_sdk/cli/slash/prompt.py +24 -10
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +827 -232
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +12 -52
  91. glaip_sdk/cli/transcript/cache.py +258 -60
  92. glaip_sdk/cli/transcript/capture.py +72 -21
  93. glaip_sdk/cli/transcript/history.py +815 -0
  94. glaip_sdk/cli/transcript/launcher.py +1 -3
  95. glaip_sdk/cli/transcript/viewer.py +79 -329
  96. glaip_sdk/cli/update_notifier.py +385 -24
  97. glaip_sdk/cli/validators.py +16 -18
  98. glaip_sdk/client/__init__.py +3 -1
  99. glaip_sdk/client/_schedule_payloads.py +89 -0
  100. glaip_sdk/client/agent_runs.py +147 -0
  101. glaip_sdk/client/agents.py +370 -100
  102. glaip_sdk/client/base.py +78 -35
  103. glaip_sdk/client/hitl.py +136 -0
  104. glaip_sdk/client/main.py +25 -10
  105. glaip_sdk/client/mcps.py +166 -27
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +583 -79
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +214 -56
  113. glaip_sdk/client/validators.py +20 -48
  114. glaip_sdk/config/constants.py +11 -0
  115. glaip_sdk/exceptions.py +1 -3
  116. glaip_sdk/hitl/__init__.py +48 -0
  117. glaip_sdk/hitl/base.py +64 -0
  118. glaip_sdk/hitl/callback.py +43 -0
  119. glaip_sdk/hitl/local.py +121 -0
  120. glaip_sdk/hitl/remote.py +523 -0
  121. glaip_sdk/icons.py +9 -3
  122. glaip_sdk/mcps/__init__.py +21 -0
  123. glaip_sdk/mcps/base.py +345 -0
  124. glaip_sdk/models/__init__.py +107 -0
  125. glaip_sdk/models/agent.py +47 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/mcp.py +33 -0
  129. glaip_sdk/models/schedule.py +224 -0
  130. glaip_sdk/models/tool.py +33 -0
  131. glaip_sdk/payload_schemas/__init__.py +1 -13
  132. glaip_sdk/payload_schemas/agent.py +1 -3
  133. glaip_sdk/registry/__init__.py +55 -0
  134. glaip_sdk/registry/agent.py +164 -0
  135. glaip_sdk/registry/base.py +139 -0
  136. glaip_sdk/registry/mcp.py +253 -0
  137. glaip_sdk/registry/tool.py +445 -0
  138. glaip_sdk/rich_components.py +58 -2
  139. glaip_sdk/runner/__init__.py +76 -0
  140. glaip_sdk/runner/base.py +84 -0
  141. glaip_sdk/runner/deps.py +112 -0
  142. glaip_sdk/runner/langgraph.py +872 -0
  143. glaip_sdk/runner/logging_config.py +77 -0
  144. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  145. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  146. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  147. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  148. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  149. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  150. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  151. glaip_sdk/schedules/__init__.py +22 -0
  152. glaip_sdk/schedules/base.py +291 -0
  153. glaip_sdk/tools/__init__.py +22 -0
  154. glaip_sdk/tools/base.py +468 -0
  155. glaip_sdk/utils/__init__.py +59 -12
  156. glaip_sdk/utils/a2a/__init__.py +34 -0
  157. glaip_sdk/utils/a2a/event_processor.py +188 -0
  158. glaip_sdk/utils/agent_config.py +4 -14
  159. glaip_sdk/utils/bundler.py +403 -0
  160. glaip_sdk/utils/client.py +111 -0
  161. glaip_sdk/utils/client_utils.py +46 -28
  162. glaip_sdk/utils/datetime_helpers.py +58 -0
  163. glaip_sdk/utils/discovery.py +78 -0
  164. glaip_sdk/utils/display.py +25 -21
  165. glaip_sdk/utils/export.py +143 -0
  166. glaip_sdk/utils/general.py +1 -36
  167. glaip_sdk/utils/import_export.py +15 -16
  168. glaip_sdk/utils/import_resolver.py +524 -0
  169. glaip_sdk/utils/instructions.py +101 -0
  170. glaip_sdk/utils/rendering/__init__.py +115 -1
  171. glaip_sdk/utils/rendering/formatting.py +38 -23
  172. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  173. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  174. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  175. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  176. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  177. glaip_sdk/utils/rendering/models.py +18 -8
  178. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  179. glaip_sdk/utils/rendering/renderer/base.py +534 -882
  180. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  181. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  182. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  183. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  184. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  185. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  186. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  187. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  188. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  189. glaip_sdk/utils/rendering/state.py +204 -0
  190. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  191. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  192. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  193. glaip_sdk/utils/rendering/steps/format.py +176 -0
  194. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  195. glaip_sdk/utils/rendering/timing.py +36 -0
  196. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  197. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  198. glaip_sdk/utils/resource_refs.py +29 -26
  199. glaip_sdk/utils/runtime_config.py +425 -0
  200. glaip_sdk/utils/serialization.py +32 -46
  201. glaip_sdk/utils/sync.py +162 -0
  202. glaip_sdk/utils/tool_detection.py +301 -0
  203. glaip_sdk/utils/tool_storage_provider.py +140 -0
  204. glaip_sdk/utils/validation.py +20 -28
  205. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
  206. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  207. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  208. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  209. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  210. glaip_sdk/cli/commands/agents.py +0 -1412
  211. glaip_sdk/cli/commands/mcps.py +0 -1225
  212. glaip_sdk/cli/commands/tools.py +0 -597
  213. glaip_sdk/cli/utils.py +0 -1330
  214. glaip_sdk/models.py +0 -259
  215. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  216. glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,204 @@
1
+ """Renderer state utilities and helpers.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from typing import Any
13
+ from collections.abc import Iterable
14
+
15
+
16
+ def coerce_received_at(value: Any) -> datetime | None:
17
+ """Coerce arbitrary values into timezone-aware datetimes if possible."""
18
+ if value is None:
19
+ return None
20
+
21
+ if isinstance(value, datetime):
22
+ return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
23
+
24
+ if isinstance(value, str):
25
+ try:
26
+ normalised = value.replace("Z", "+00:00")
27
+ dt = datetime.fromisoformat(normalised)
28
+ except ValueError:
29
+ return None
30
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
31
+
32
+ return None
33
+
34
+
35
+ def truncate_display(text: str | None, limit: int = 160) -> str:
36
+ """Return text capped at the given character limit with ellipsis."""
37
+ if not text:
38
+ return ""
39
+ stripped = str(text).strip()
40
+ if len(stripped) <= limit:
41
+ return stripped
42
+ return stripped[: limit - 1] + "…"
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class TranscriptBuffer:
47
+ """Utility container for streaming transcript text."""
48
+
49
+ lines: list[str] = field(default_factory=list)
50
+
51
+ def append(self, value: str | None) -> None:
52
+ """Append a chunk of transcript text."""
53
+ if not value:
54
+ return
55
+ self.lines.append(value)
56
+
57
+ def extend(self, chunks: Iterable[str]) -> None:
58
+ """Append multiple chunks."""
59
+ for chunk in chunks:
60
+ self.append(chunk)
61
+
62
+ def clear(self) -> None:
63
+ """Reset the buffer."""
64
+ self.lines.clear()
65
+
66
+ def render(self) -> str:
67
+ """Return the concatenated transcript text."""
68
+ return "".join(self.lines)
69
+
70
+ def has_visible_text(self) -> bool:
71
+ """Return True when any chunk contains non-whitespace characters."""
72
+ return any(chunk and chunk.strip() for chunk in self.lines)
73
+
74
+ def __bool__(self) -> bool:
75
+ """Allow truthiness checks like a regular list."""
76
+ return bool(self.lines)
77
+
78
+ def __len__(self) -> int:
79
+ """Return buffered chunk count."""
80
+ return len(self.lines)
81
+
82
+ def __iter__(self):
83
+ """Iterate over buffered chunks."""
84
+ return iter(self.lines)
85
+
86
+ def __getitem__(self, index: int) -> str:
87
+ """Return the chunk at the requested index."""
88
+ return self.lines[index]
89
+
90
+ def __contains__(self, item: object) -> bool:
91
+ """Membership test for convenience."""
92
+ return item in self.lines
93
+
94
+
95
+ @dataclass
96
+ class RendererState:
97
+ """Internal state for the renderer."""
98
+
99
+ buffer: TranscriptBuffer = field(default_factory=TranscriptBuffer)
100
+ final_text: str = ""
101
+ streaming_started_at: float | None = None
102
+ printed_final_output: bool = False
103
+ finalizing_ui: bool = False
104
+ final_duration_seconds: float | None = None
105
+ final_duration_text: str | None = None
106
+ events: list[dict[str, Any]] = field(default_factory=list)
107
+ meta: dict[str, Any] = field(default_factory=dict)
108
+ streaming_started_event_ts: datetime | None = None
109
+
110
+ def record_event(self, event: dict[str, Any], *, received_at: datetime | None = None) -> None:
111
+ """Capture an event snapshot for transcript replay."""
112
+ try:
113
+ captured = json.loads(json.dumps(event))
114
+ except Exception:
115
+ captured = dict(event)
116
+
117
+ if received_at is not None:
118
+ try:
119
+ captured["received_at"] = received_at.isoformat()
120
+ except Exception:
121
+ try:
122
+ captured["received_at"] = str(received_at)
123
+ except Exception:
124
+ captured["received_at"] = repr(received_at)
125
+
126
+ self.events.append(captured)
127
+
128
+ def set_final_output(self, value: str) -> None:
129
+ """Record the final assistant output."""
130
+ self.final_text = value
131
+
132
+ def append_transcript_text(self, value: str) -> None:
133
+ """Append streaming text to the transcript buffer."""
134
+ self.buffer.append(value)
135
+
136
+ def start_stream_timer(self, now: float | None) -> None:
137
+ """Record start timestamp when streaming begins."""
138
+ if now is None or self.streaming_started_at is not None:
139
+ return
140
+ self.streaming_started_at = now
141
+
142
+ def stop_stream_timer(self, now: float | None) -> float | None:
143
+ """Record the total elapsed duration."""
144
+ if now is None or self.streaming_started_at is None:
145
+ return None
146
+ duration = max(0.0, now - self.streaming_started_at)
147
+ self.final_duration_seconds = duration
148
+ return duration
149
+
150
+ def mark_final_duration(self, duration: float | None, *, formatted: str | None = None) -> None:
151
+ """Store the final duration metadata."""
152
+ if duration is not None:
153
+ self.final_duration_seconds = duration
154
+ self.final_duration_text = formatted
155
+
156
+ def to_snapshot(self) -> dict[str, Any]:
157
+ """Return a serialisable snapshot for presenters."""
158
+ return prepare_transcript_snapshot(self)
159
+
160
+
161
+ @dataclass
162
+ class ThinkingScopeState:
163
+ """Runtime bookkeeping for deterministic thinking spans."""
164
+
165
+ anchor_id: str
166
+ task_id: str | None
167
+ context_id: str | None
168
+ anchor_started_at: float | None = None
169
+ anchor_finished_at: float | None = None
170
+ idle_started_at: float | None = None
171
+ idle_started_monotonic: float | None = None
172
+ active_thinking_id: str | None = None
173
+ running_children: set[str] = field(default_factory=set)
174
+ closed: bool = False
175
+
176
+
177
+ def accumulate_final_text(*, state: RendererState) -> str:
178
+ """Return the most relevant final text for summary panels."""
179
+ if state.final_text.strip():
180
+ return state.final_text.strip()
181
+ return state.buffer.render().strip()
182
+
183
+
184
+ def prepare_transcript_snapshot(state: RendererState) -> dict[str, Any]:
185
+ """Return a dictionary capturing renderer transcript state."""
186
+ return {
187
+ "final_text": state.final_text,
188
+ "buffer_text": state.buffer.render(),
189
+ "events": list(state.events),
190
+ "meta": dict(state.meta),
191
+ "final_duration_seconds": state.final_duration_seconds,
192
+ "final_duration_text": state.final_duration_text,
193
+ }
194
+
195
+
196
+ __all__ = [
197
+ "RendererState",
198
+ "ThinkingScopeState",
199
+ "TranscriptBuffer",
200
+ "accumulate_final_text",
201
+ "coerce_received_at",
202
+ "prepare_transcript_snapshot",
203
+ "truncate_display",
204
+ ]
@@ -0,0 +1,100 @@
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(default_factory=dict)
24
+ retained_ids: set[str] = field(default_factory=set)
25
+ step_index: dict[str, Step] = field(default_factory=dict)
26
+ pending_branch_failures: set[str] = field(default_factory=set)
27
+
28
+ def link_root(self, step_id: str) -> None:
29
+ """Ensure a step id is present in the root ordering."""
30
+ if step_id not in self.root_order:
31
+ self.root_order.append(step_id)
32
+
33
+ def unlink_root(self, step_id: str) -> None:
34
+ """Remove a step id from the root ordering if present."""
35
+ if step_id in self.root_order:
36
+ self.root_order.remove(step_id)
37
+
38
+ def link_child(self, parent_id: str, child_id: str) -> None:
39
+ """Attach a child step to a parent."""
40
+ children = self.child_map.setdefault(parent_id, [])
41
+ if child_id not in children:
42
+ children.append(child_id)
43
+
44
+ def unlink_child(self, parent_id: str, child_id: str) -> None:
45
+ """Detach a child from a parent."""
46
+ children = self.child_map.get(parent_id)
47
+ if not children:
48
+ return
49
+
50
+ if child_id in children:
51
+ children.remove(child_id)
52
+ # Clean up if the list is now empty
53
+ if len(children) == 0:
54
+ self.child_map.pop(parent_id, None)
55
+
56
+ def buffer_child(self, parent_id: str, child_id: str) -> None:
57
+ """Track a child that is waiting for its parent to appear."""
58
+ queue = self.buffered_children.setdefault(parent_id, [])
59
+ if child_id not in queue:
60
+ queue.append(child_id)
61
+
62
+ def pop_buffered_children(self, parent_id: str) -> list[str]:
63
+ """Return any buffered children for a parent."""
64
+ return self.buffered_children.pop(parent_id, [])
65
+
66
+ def discard_running(self, step_id: str) -> None:
67
+ """Remove a step from running context tracking."""
68
+ for key, running in tuple(self.running_by_context.items()):
69
+ if step_id in running:
70
+ running.discard(step_id)
71
+ if not running:
72
+ self.running_by_context.pop(key, None)
73
+
74
+ def iter_visible_tree(self) -> Iterator[tuple[str, tuple[bool, ...]]]:
75
+ """Yield step ids in depth-first order alongside branch metadata.
76
+
77
+ Returns:
78
+ Iterator of (step_id, branch_state) tuples where branch_state
79
+ captures whether each ancestor was the last child. This data
80
+ is later used by rendering helpers to draw connectors such as
81
+ `│`, `├─`, and `└─` consistently.
82
+ """
83
+ roots = tuple(self.root_order)
84
+ total_roots = len(roots)
85
+ for index, root_id in enumerate(roots):
86
+ yield root_id, ()
87
+ ancestor_state = (index == total_roots - 1,)
88
+ yield from self._walk_children(root_id, ancestor_state)
89
+
90
+ def _walk_children(
91
+ self, parent_id: str, ancestor_state: tuple[bool, ...]
92
+ ) -> Iterator[tuple[str, tuple[bool, ...]]]:
93
+ """Depth-first traversal helper yielding children with ancestry info."""
94
+ children = self.child_map.get(parent_id, [])
95
+ total_children = len(children)
96
+ for idx, child_id in enumerate(children):
97
+ is_last = idx == total_children - 1
98
+ branch_state = ancestor_state + (is_last,)
99
+ yield child_id, branch_state
100
+ yield from self._walk_children(child_id, branch_state)
@@ -0,0 +1,34 @@
1
+ """Step management and presentation helpers."""
2
+
3
+ from glaip_sdk.utils.rendering.steps.format import (
4
+ UNKNOWN_STEP_DETAIL,
5
+ STATUS_ICON_STYLES,
6
+ StepPresentation,
7
+ build_connector_prefix,
8
+ compose_display_label,
9
+ format_step,
10
+ format_step_label,
11
+ format_tool_args,
12
+ humanize_tool_name,
13
+ resolve_label_body,
14
+ status_icon_for_step,
15
+ step_icon_for_kind,
16
+ )
17
+ from glaip_sdk.utils.rendering.steps.manager import StepManager, StepManagerError
18
+
19
+ __all__ = [
20
+ "StepManager",
21
+ "StepManagerError",
22
+ "UNKNOWN_STEP_DETAIL",
23
+ "STATUS_ICON_STYLES",
24
+ "StepPresentation",
25
+ "build_connector_prefix",
26
+ "compose_display_label",
27
+ "format_step",
28
+ "format_step_label",
29
+ "format_tool_args",
30
+ "humanize_tool_name",
31
+ "resolve_label_body",
32
+ "status_icon_for_step",
33
+ "step_icon_for_kind",
34
+ ]