glaip-sdk 0.1.3__py3-none-any.whl → 0.6.10__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 (141) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -0
  5. glaip_sdk/branding.py +13 -0
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/auth.py +254 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents.py +213 -73
  11. glaip_sdk/cli/commands/common_config.py +101 -0
  12. glaip_sdk/cli/commands/configure.py +729 -113
  13. glaip_sdk/cli/commands/mcps.py +241 -72
  14. glaip_sdk/cli/commands/models.py +11 -5
  15. glaip_sdk/cli/commands/tools.py +49 -57
  16. glaip_sdk/cli/commands/transcripts.py +755 -0
  17. glaip_sdk/cli/config.py +48 -4
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +8 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +846 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +35 -19
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +6 -3
  28. glaip_sdk/cli/main.py +228 -119
  29. glaip_sdk/cli/masking.py +21 -33
  30. glaip_sdk/cli/pager.py +9 -10
  31. glaip_sdk/cli/parsers/__init__.py +1 -3
  32. glaip_sdk/cli/slash/__init__.py +0 -9
  33. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  34. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  35. glaip_sdk/cli/slash/agent_session.py +62 -21
  36. glaip_sdk/cli/slash/prompt.py +21 -0
  37. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  38. glaip_sdk/cli/slash/session.py +771 -140
  39. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  40. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  41. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  42. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  43. glaip_sdk/cli/slash/tui/loading.py +58 -0
  44. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  45. glaip_sdk/cli/transcript/__init__.py +12 -52
  46. glaip_sdk/cli/transcript/cache.py +255 -44
  47. glaip_sdk/cli/transcript/capture.py +27 -1
  48. glaip_sdk/cli/transcript/history.py +815 -0
  49. glaip_sdk/cli/transcript/viewer.py +72 -499
  50. glaip_sdk/cli/update_notifier.py +14 -5
  51. glaip_sdk/cli/utils.py +243 -1252
  52. glaip_sdk/cli/validators.py +5 -6
  53. glaip_sdk/client/__init__.py +2 -1
  54. glaip_sdk/client/_agent_payloads.py +45 -9
  55. glaip_sdk/client/agent_runs.py +147 -0
  56. glaip_sdk/client/agents.py +287 -29
  57. glaip_sdk/client/base.py +1 -0
  58. glaip_sdk/client/main.py +19 -10
  59. glaip_sdk/client/mcps.py +122 -12
  60. glaip_sdk/client/run_rendering.py +133 -88
  61. glaip_sdk/client/shared.py +21 -0
  62. glaip_sdk/client/tools.py +155 -10
  63. glaip_sdk/config/constants.py +11 -0
  64. glaip_sdk/mcps/__init__.py +21 -0
  65. glaip_sdk/mcps/base.py +345 -0
  66. glaip_sdk/models/__init__.py +90 -0
  67. glaip_sdk/models/agent.py +47 -0
  68. glaip_sdk/models/agent_runs.py +116 -0
  69. glaip_sdk/models/common.py +42 -0
  70. glaip_sdk/models/mcp.py +33 -0
  71. glaip_sdk/models/tool.py +33 -0
  72. glaip_sdk/payload_schemas/__init__.py +1 -13
  73. glaip_sdk/registry/__init__.py +55 -0
  74. glaip_sdk/registry/agent.py +164 -0
  75. glaip_sdk/registry/base.py +139 -0
  76. glaip_sdk/registry/mcp.py +253 -0
  77. glaip_sdk/registry/tool.py +232 -0
  78. glaip_sdk/rich_components.py +58 -2
  79. glaip_sdk/runner/__init__.py +59 -0
  80. glaip_sdk/runner/base.py +84 -0
  81. glaip_sdk/runner/deps.py +115 -0
  82. glaip_sdk/runner/langgraph.py +706 -0
  83. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  84. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  85. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  86. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  87. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  88. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  89. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  90. glaip_sdk/tools/__init__.py +22 -0
  91. glaip_sdk/tools/base.py +435 -0
  92. glaip_sdk/utils/__init__.py +58 -12
  93. glaip_sdk/utils/a2a/__init__.py +34 -0
  94. glaip_sdk/utils/a2a/event_processor.py +188 -0
  95. glaip_sdk/utils/bundler.py +267 -0
  96. glaip_sdk/utils/client.py +111 -0
  97. glaip_sdk/utils/client_utils.py +39 -7
  98. glaip_sdk/utils/datetime_helpers.py +58 -0
  99. glaip_sdk/utils/discovery.py +78 -0
  100. glaip_sdk/utils/display.py +23 -15
  101. glaip_sdk/utils/export.py +143 -0
  102. glaip_sdk/utils/general.py +0 -33
  103. glaip_sdk/utils/import_export.py +12 -7
  104. glaip_sdk/utils/import_resolver.py +492 -0
  105. glaip_sdk/utils/instructions.py +101 -0
  106. glaip_sdk/utils/rendering/__init__.py +115 -1
  107. glaip_sdk/utils/rendering/formatting.py +5 -30
  108. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  109. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  110. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  111. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  112. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  113. glaip_sdk/utils/rendering/models.py +1 -0
  114. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  115. glaip_sdk/utils/rendering/renderer/base.py +217 -1476
  116. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  117. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  118. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  119. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  120. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  121. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  122. glaip_sdk/utils/rendering/state.py +204 -0
  123. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  124. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  125. glaip_sdk/utils/rendering/steps/format.py +176 -0
  126. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  127. glaip_sdk/utils/rendering/timing.py +36 -0
  128. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  129. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  130. glaip_sdk/utils/resource_refs.py +25 -13
  131. glaip_sdk/utils/runtime_config.py +425 -0
  132. glaip_sdk/utils/serialization.py +18 -0
  133. glaip_sdk/utils/sync.py +142 -0
  134. glaip_sdk/utils/tool_detection.py +33 -0
  135. glaip_sdk/utils/validation.py +16 -24
  136. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  137. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  138. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  139. glaip_sdk/models.py +0 -240
  140. glaip_sdk-0.1.3.dist-info/RECORD +0 -83
  141. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,176 @@
1
+ """Presentation helpers for rendering steps.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from glaip_sdk.icons import ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
13
+ from glaip_sdk.utils.rendering.formatting import glyph_for_status, normalise_display_label, pretty_args
14
+ from glaip_sdk.utils.rendering.models import Step
15
+
16
+ if TYPE_CHECKING: # pragma: no cover - typing helpers only
17
+ from glaip_sdk.utils.rendering.layout.transcript import TranscriptGlyphs
18
+
19
+ UNKNOWN_STEP_DETAIL = "Unknown step detail"
20
+ STATUS_ICON_STYLES = {
21
+ "success": "green",
22
+ "failed": "red",
23
+ "warning": "yellow",
24
+ }
25
+ CONNECTOR_VERTICAL = "│ "
26
+ CONNECTOR_EMPTY = " "
27
+ CONNECTOR_BRANCH = "├─ "
28
+ CONNECTOR_LAST = "└─ "
29
+ ROOT_MARKER = ""
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class StepPresentation:
34
+ """Lightweight view model for formatted steps."""
35
+
36
+ step_id: str
37
+ title: str
38
+ glyph: str | None
39
+ status_style: str | None
40
+ args_text: str | None = None
41
+ failure_reason: str | None = None
42
+ duration_ms: int | None = None
43
+ status_text: str | None = None
44
+
45
+
46
+ def humanize_tool_name(raw_name: str | None) -> str:
47
+ """Return a user-facing name for a tool or agent identifier."""
48
+ if not raw_name:
49
+ return UNKNOWN_STEP_DETAIL
50
+ name = raw_name
51
+ if name.startswith("delegate_to_"):
52
+ name = name.removeprefix("delegate_to_")
53
+ elif name.startswith("delegate_"):
54
+ name = name.removeprefix("delegate_")
55
+ cleaned = name.replace("_", " ").replace("-", " ").strip()
56
+ if not cleaned:
57
+ return UNKNOWN_STEP_DETAIL
58
+ lowered = cleaned.lower()
59
+ return lowered[0].upper() + lowered[1:] if lowered else UNKNOWN_STEP_DETAIL
60
+
61
+
62
+ def step_icon_for_kind(step_kind: str) -> str:
63
+ """Return the icon prefix for a step kind."""
64
+ if step_kind == "agent":
65
+ return ICON_AGENT_STEP
66
+ if step_kind == "delegate":
67
+ return ICON_DELEGATE
68
+ if step_kind == "thinking":
69
+ return "💭"
70
+ return ICON_TOOL_STEP
71
+
72
+
73
+ def resolve_label_body(step_kind: str, tool_name: str | None, metadata: dict[str, Any]) -> str:
74
+ """Resolve the textual body for a step label."""
75
+ if step_kind == "thinking":
76
+ thinking_text = metadata.get("thinking_and_activity_info")
77
+ if isinstance(thinking_text, str) and thinking_text.strip():
78
+ return thinking_text.strip()
79
+ return "Thinking…"
80
+
81
+ if step_kind == "delegate":
82
+ return humanize_tool_name(tool_name)
83
+
84
+ if step_kind == "agent":
85
+ agent_name = metadata.get("agent_name")
86
+ if isinstance(agent_name, str) and agent_name.strip():
87
+ return agent_name.strip()
88
+
89
+ return humanize_tool_name(tool_name)
90
+
91
+
92
+ def compose_display_label(
93
+ step_kind: str,
94
+ tool_name: str | None,
95
+ args: dict[str, Any],
96
+ metadata: dict[str, Any],
97
+ ) -> str:
98
+ """Compose the display label for a step using tool metadata."""
99
+ icon = step_icon_for_kind(step_kind)
100
+ body = resolve_label_body(step_kind, tool_name, metadata)
101
+ label = f"{icon} {body}".strip()
102
+ if isinstance(args, dict) and args:
103
+ label = f"{label} —"
104
+ return label or UNKNOWN_STEP_DETAIL
105
+
106
+
107
+ def status_icon_for_step(step: Step) -> str:
108
+ """Return the canonical status icon key for a step."""
109
+ if step.status == "failed":
110
+ return "failed"
111
+ if step.branch_failed:
112
+ return "warning"
113
+ if step.status == "finished":
114
+ return "success"
115
+ if step.status == "stopped":
116
+ return "warning"
117
+ return "spinner"
118
+
119
+
120
+ def format_step_label(step: Step) -> str:
121
+ """Return the normalized display label for a step."""
122
+ label = normalise_display_label(getattr(step, "display_label", None))
123
+ if label and label != UNKNOWN_STEP_DETAIL:
124
+ return label
125
+ metadata = getattr(step, "metadata", {}) or {}
126
+ computed = compose_display_label(step.kind, getattr(step, "name", None), getattr(step, "args", {}), metadata)
127
+ return normalise_display_label(computed)
128
+
129
+
130
+ def format_tool_args(step: Step, max_len: int = 160) -> str | None:
131
+ """Return a pretty-printed args summary for a step."""
132
+ if not step.args:
133
+ return None
134
+ try:
135
+ return pretty_args(step.args, max_len=max_len)
136
+ except Exception:
137
+ return None
138
+
139
+
140
+ def format_step(
141
+ step: Step,
142
+ *,
143
+ glyphs: TranscriptGlyphs | None = None,
144
+ label: str | None = None,
145
+ ) -> StepPresentation:
146
+ """Return a StepPresentation for downstream transcript rendering."""
147
+ del glyphs # Reserved for future glyph customisation hooks
148
+ if label:
149
+ resolved_label = normalise_display_label(label)
150
+ else:
151
+ resolved_label = format_step_label(step)
152
+ glyph_key = status_icon_for_step(step)
153
+ glyph = glyph_for_status(glyph_key)
154
+ style = STATUS_ICON_STYLES.get(glyph_key)
155
+ failure_reason = (step.failure_reason or "").strip() or None
156
+ return StepPresentation(
157
+ step_id=step.step_id,
158
+ title=resolved_label,
159
+ glyph=glyph,
160
+ status_style=style,
161
+ args_text=format_tool_args(step),
162
+ failure_reason=failure_reason,
163
+ duration_ms=getattr(step, "duration_ms", None),
164
+ )
165
+
166
+
167
+ def build_connector_prefix(branch_state: tuple[bool, ...]) -> str:
168
+ """Build connector prefix for a tree line based on ancestry state."""
169
+ if not branch_state:
170
+ return ROOT_MARKER
171
+
172
+ parts: list[str] = []
173
+ for ancestor_is_last in branch_state[:-1]:
174
+ parts.append(CONNECTOR_EMPTY if ancestor_is_last else CONNECTOR_VERTICAL)
175
+ parts.append(CONNECTOR_LAST if branch_state[-1] else CONNECTOR_BRANCH)
176
+ return "".join(parts)
@@ -0,0 +1,387 @@
1
+ """Rendering utilities.
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
+
11
+ from glaip_sdk.utils.rendering.models import Step
12
+ from glaip_sdk.utils.rendering.step_tree_state import StepTreeState
13
+ from glaip_sdk.utils.rendering.steps.event_processor import StepEventMixin
14
+
15
+
16
+ class StepManagerError(Exception):
17
+ """Raised when invalid operations are attempted on the step tree."""
18
+
19
+
20
+ class StepManager(StepEventMixin):
21
+ """Manages the lifecycle and organization of execution steps.
22
+
23
+ Tracks step creation, parent-child relationships, and execution state
24
+ with automatic pruning of old steps when limits are reached.
25
+ """
26
+
27
+ def __init__(self, max_steps: int = 200) -> None:
28
+ """Initialize the step manager.
29
+
30
+ Args:
31
+ max_steps: Maximum number of steps to retain before pruning
32
+ """
33
+ normalised_max = int(max_steps) if isinstance(max_steps, (int, float)) else 0
34
+ self.state = StepTreeState(max_steps=normalised_max)
35
+ self.by_id: dict[str, Step] = self.state.step_index
36
+ self.key_index: dict[tuple, str] = {}
37
+ self.slot_counter: dict[tuple, int] = {}
38
+ self.max_steps = normalised_max
39
+ self._last_running: dict[tuple, str] = {}
40
+ self._step_aliases: dict[str, str] = {}
41
+ self.root_agent_id: str | None = None
42
+ self._scope_anchors: dict[str, list[str]] = {}
43
+ self._step_scope_map: dict[str, str] = {}
44
+
45
+ def set_root_agent(self, agent_id: str | None) -> None:
46
+ """Record the root agent identifier for scope-aware parenting."""
47
+ if isinstance(agent_id, str) and agent_id.strip():
48
+ self.root_agent_id = agent_id.strip()
49
+
50
+ def _alloc_slot(
51
+ self,
52
+ task_id: str | None,
53
+ context_id: str | None,
54
+ kind: str,
55
+ name: str,
56
+ ) -> int:
57
+ k = (task_id, context_id, kind, name)
58
+ self.slot_counter[k] = self.slot_counter.get(k, 0) + 1
59
+ return self.slot_counter[k]
60
+
61
+ def _key(
62
+ self,
63
+ task_id: str | None,
64
+ context_id: str | None,
65
+ kind: str,
66
+ name: str,
67
+ slot: int,
68
+ ) -> tuple[str | None, str | None, str, str, int]:
69
+ return (task_id, context_id, kind, name, slot)
70
+
71
+ def _make_id(
72
+ self,
73
+ task_id: str | None,
74
+ context_id: str | None,
75
+ kind: str,
76
+ name: str,
77
+ slot: int,
78
+ ) -> str:
79
+ return f"{task_id or 't'}::{context_id or 'c'}::{kind}::{name}::{slot}"
80
+
81
+ def start_or_get(
82
+ self,
83
+ *,
84
+ task_id: str | None,
85
+ context_id: str | None,
86
+ kind: str,
87
+ name: str,
88
+ parent_id: str | None = None,
89
+ args: dict[str, object] | None = None,
90
+ ) -> Step:
91
+ """Start a new step or return existing running step with same parameters.
92
+
93
+ Args:
94
+ task_id: Task identifier
95
+ context_id: Context identifier
96
+ kind: Step kind (tool, delegate, agent)
97
+ name: Step name
98
+ parent_id: Parent step ID if this is a child step
99
+ args: Step arguments
100
+
101
+ Returns:
102
+ The Step instance (new or existing)
103
+ """
104
+ existing = self.find_running(task_id=task_id, context_id=context_id, kind=kind, name=name)
105
+ if existing:
106
+ if args and existing.args != args:
107
+ existing.args = args
108
+ return existing
109
+ slot = self._alloc_slot(task_id, context_id, kind, name)
110
+ key = self._key(task_id, context_id, kind, name, slot)
111
+ step_id = self._make_id(task_id, context_id, kind, name, slot)
112
+ st = Step(
113
+ step_id=step_id,
114
+ kind=kind,
115
+ name=name,
116
+ parent_id=parent_id,
117
+ task_id=task_id,
118
+ context_id=context_id,
119
+ args=args or {},
120
+ )
121
+ self.by_id[step_id] = st
122
+ if parent_id:
123
+ self.children.setdefault(parent_id, []).append(step_id)
124
+ else:
125
+ self.order.append(step_id)
126
+ self.key_index[key] = step_id
127
+ self.state.retained_ids.add(step_id)
128
+ self._prune_steps()
129
+ self._last_running[(task_id, context_id, kind, name)] = step_id
130
+ return st
131
+
132
+ def _calculate_total_steps(self) -> int:
133
+ """Calculate total number of steps."""
134
+ return len(self.order) + sum(len(v) for v in self.children.values())
135
+
136
+ def _get_subtree_size(self, root_id: str) -> int:
137
+ """Get the size of a subtree (including root)."""
138
+ subtree = [root_id]
139
+ stack = list(self.children.get(root_id, []))
140
+ while stack:
141
+ x = stack.pop()
142
+ subtree.append(x)
143
+ stack.extend(self.children.get(x, []))
144
+ return len(subtree)
145
+
146
+ def _remove_subtree(self, root_id: str) -> None:
147
+ """Remove a complete subtree from all data structures."""
148
+ for step_id in self._collect_subtree_ids(root_id):
149
+ self._purge_step_references(step_id)
150
+
151
+ def _collect_subtree_ids(self, root_id: str) -> list[str]:
152
+ """Return a flat list of step ids contained within a subtree."""
153
+ stack = [root_id]
154
+ collected: list[str] = []
155
+ while stack:
156
+ sid = stack.pop()
157
+ collected.append(sid)
158
+ stack.extend(self.children.pop(sid, []))
159
+ return collected
160
+
161
+ def _purge_step_references(self, step_id: str) -> None:
162
+ """Remove a single step id from all indexes and helper structures."""
163
+ st = self.by_id.pop(step_id, None)
164
+ if st:
165
+ key = (st.task_id, st.context_id, st.kind, st.name)
166
+ self._last_running.pop(key, None)
167
+ self.state.retained_ids.discard(step_id)
168
+ self.state.discard_running(step_id)
169
+ self._remove_parent_links(step_id)
170
+ if step_id in self.order:
171
+ self.order.remove(step_id)
172
+ self.state.buffered_children.pop(step_id, None)
173
+ self.state.pending_branch_failures.discard(step_id)
174
+
175
+ def _remove_parent_links(self, child_id: str) -> None:
176
+ """Detach a child id from any parent lists."""
177
+ for parent, kids in self.children.copy().items():
178
+ if child_id not in kids:
179
+ continue
180
+ kids.remove(child_id)
181
+ if not kids:
182
+ self.children.pop(parent, None)
183
+
184
+ def _should_prune_steps(self, total: int) -> bool:
185
+ """Check if steps should be pruned."""
186
+ if self.max_steps <= 0:
187
+ return False
188
+ return total > self.max_steps
189
+
190
+ def _get_oldest_step_id(self) -> str | None:
191
+ """Get the oldest step ID for pruning."""
192
+ return self.order[0] if self.order else None
193
+
194
+ def _prune_steps(self) -> None:
195
+ """Prune steps when total exceeds maximum."""
196
+ total = self._calculate_total_steps()
197
+ if not self._should_prune_steps(total):
198
+ return
199
+
200
+ while self._should_prune_steps(total) and self.order:
201
+ sid = self._get_oldest_step_id()
202
+ if not sid:
203
+ break
204
+
205
+ subtree_size = self._get_subtree_size(sid)
206
+ self._remove_subtree(sid)
207
+ total -= subtree_size
208
+
209
+ def remove_step(self, step_id: str) -> None:
210
+ """Remove a single step from the tree and cached indexes."""
211
+ step = self.by_id.pop(step_id, None)
212
+ if not step:
213
+ return
214
+
215
+ if step.parent_id:
216
+ self.state.unlink_child(step.parent_id, step_id)
217
+ else:
218
+ self.state.unlink_root(step_id)
219
+
220
+ self.children.pop(step_id, None)
221
+ self.state.buffered_children.pop(step_id, None)
222
+ self.state.retained_ids.discard(step_id)
223
+ self.state.pending_branch_failures.discard(step_id)
224
+ self.state.discard_running(step_id)
225
+
226
+ self.key_index = {key: sid for key, sid in self.key_index.items() if sid != step_id}
227
+ for key, last_sid in self._last_running.copy().items():
228
+ if last_sid == step_id:
229
+ self._last_running.pop(key, None)
230
+
231
+ aliases = [alias for alias, target in self._step_aliases.items() if alias == step_id or target == step_id]
232
+ for alias in aliases:
233
+ self._step_aliases.pop(alias, None)
234
+
235
+ def get_child_count(self, step_id: str) -> int:
236
+ """Get the number of child steps for a given step.
237
+
238
+ Args:
239
+ step_id: The parent step ID
240
+
241
+ Returns:
242
+ Number of child steps
243
+ """
244
+ return len(self.children.get(step_id, []))
245
+
246
+ def find_running(
247
+ self,
248
+ *,
249
+ task_id: str | None,
250
+ context_id: str | None,
251
+ kind: str,
252
+ name: str,
253
+ ) -> Step | None:
254
+ """Find a currently running step with the given parameters.
255
+
256
+ Args:
257
+ task_id: Task identifier
258
+ context_id: Context identifier
259
+ kind: Step kind (tool, delegate, agent)
260
+ name: Step name
261
+
262
+ Returns:
263
+ The running Step if found, None otherwise
264
+ """
265
+ key = (task_id, context_id, kind, name)
266
+ step_id = self._last_running.get(key)
267
+ if step_id:
268
+ st = self.by_id.get(step_id)
269
+ if st and st.status != "finished":
270
+ return st
271
+ for sid in reversed(list(self._iter_all_steps())):
272
+ st = self.by_id.get(sid)
273
+ if (
274
+ st
275
+ and (st.task_id, st.context_id, st.kind, st.name)
276
+ == (
277
+ task_id,
278
+ context_id,
279
+ kind,
280
+ name,
281
+ )
282
+ and st.status != "finished"
283
+ ):
284
+ return st
285
+ return None
286
+
287
+ def list_ordered(self) -> list[Step]:
288
+ """Return a depth-first list of steps currently tracked."""
289
+ ordered: list[Step] = []
290
+ for step_id, _branch_state in self.iter_tree():
291
+ step = self.by_id.get(step_id)
292
+ if step:
293
+ ordered.append(step)
294
+ return ordered
295
+
296
+ def prune_for_summary(self, limit: int) -> list[tuple[str, tuple[bool, ...]]]:
297
+ """Return the most recent ``limit`` nodes for compact summaries."""
298
+ if limit < 0:
299
+ raise StepManagerError("limit must be non-negative")
300
+ nodes = list(self.iter_tree())
301
+ if limit == 0 or len(nodes) <= limit:
302
+ return nodes
303
+ return nodes[-limit:]
304
+
305
+ def finish(
306
+ self,
307
+ *,
308
+ task_id: str | None,
309
+ context_id: str | None,
310
+ kind: str,
311
+ name: str,
312
+ output: object | None = None,
313
+ duration_raw: float | None = None,
314
+ ) -> Step:
315
+ """Finish a step with the given parameters.
316
+
317
+ Args:
318
+ task_id: Task identifier
319
+ context_id: Context identifier
320
+ kind: Step kind (tool, delegate, agent)
321
+ name: Step name
322
+ output: Step output data
323
+ duration_raw: Raw duration in seconds
324
+
325
+ Returns:
326
+ The finished Step instance
327
+
328
+ Raises:
329
+ RuntimeError: If no matching step is found
330
+ """
331
+ st = self.find_running(task_id=task_id, context_id=context_id, kind=kind, name=name)
332
+ if not st:
333
+ # Try to find any existing step with matching parameters, even if not running
334
+ for sid in reversed(list(self._iter_all_steps())):
335
+ st_check = self.by_id.get(sid)
336
+ if (
337
+ st_check
338
+ and st_check.task_id == task_id
339
+ and st_check.context_id == context_id
340
+ and st_check.kind == kind
341
+ and st_check.name == name
342
+ ):
343
+ st = st_check
344
+ break
345
+
346
+ # If still no step found, create a new one
347
+ if not st:
348
+ st = self.start_or_get(task_id=task_id, context_id=context_id, kind=kind, name=name)
349
+
350
+ if output:
351
+ st.output = output
352
+ st.finish(duration_raw)
353
+ key = (task_id, context_id, kind, name)
354
+ if self._last_running.get(key) == st.step_id:
355
+ self._last_running.pop(key, None)
356
+ return st
357
+
358
+ def _iter_all_steps(self) -> Iterator[str]:
359
+ for root in self.order:
360
+ yield root
361
+ stack = list(self.children.get(root, []))
362
+ while stack:
363
+ sid = stack.pop()
364
+ yield sid
365
+ stack.extend(self.children.get(sid, []))
366
+
367
+ def iter_tree(self) -> Iterator[tuple[str, tuple[bool, ...]]]:
368
+ """Expose depth-first traversal info for rendering."""
369
+ yield from self.state.iter_visible_tree()
370
+
371
+ @property
372
+ def order(self) -> list[str]:
373
+ """Root step ordering accessor backed by StepTreeState."""
374
+ return self.state.root_order
375
+
376
+ @order.setter
377
+ def order(self, value: list[str]) -> None:
378
+ self.state.root_order = list(value)
379
+
380
+ @property
381
+ def children(self) -> dict[str, list[str]]:
382
+ """Child mapping accessor backed by StepTreeState."""
383
+ return self.state.child_map
384
+
385
+ @children.setter
386
+ def children(self, value: dict[str, list[str]]) -> None:
387
+ self.state.child_map = value
@@ -0,0 +1,36 @@
1
+ """Shared timing helpers for renderer components.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+
12
+ def coerce_server_time(value: Any) -> float | None:
13
+ """Convert a raw SSE/server time payload into a float."""
14
+ if isinstance(value, (int, float)):
15
+ return float(value)
16
+ try:
17
+ return float(value)
18
+ except (TypeError, ValueError):
19
+ return None
20
+
21
+
22
+ def calculate_timeline_duration(
23
+ start_server: float | None,
24
+ end_server: float | None,
25
+ start_monotonic: float | None,
26
+ end_monotonic: float | None,
27
+ ) -> float | None:
28
+ """Return best-effort elapsed time using server or monotonic clocks."""
29
+ if start_server is not None and end_server is not None:
30
+ return max(0.0, float(end_server) - float(start_server))
31
+ if start_monotonic is not None and end_monotonic is not None:
32
+ try:
33
+ return max(0.0, float(end_monotonic) - float(start_monotonic))
34
+ except Exception:
35
+ return None
36
+ return None
@@ -0,0 +1,21 @@
1
+ """Shared transcript viewer exports.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.utils.rendering.viewer.presenter import (
8
+ ViewerContext,
9
+ prepare_viewer_snapshot,
10
+ render_post_run_view,
11
+ render_transcript_events,
12
+ render_transcript_view,
13
+ )
14
+
15
+ __all__ = [
16
+ "ViewerContext",
17
+ "prepare_viewer_snapshot",
18
+ "render_post_run_view",
19
+ "render_transcript_events",
20
+ "render_transcript_view",
21
+ ]