glaip-sdk 0.1.3__py3-none-any.whl → 0.6.19__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 +44 -4
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/auth.py +254 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +213 -73
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +729 -113
- glaip_sdk/cli/commands/mcps.py +241 -72
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +49 -57
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/config.py +48 -4
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +35 -19
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +241 -121
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +771 -140
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +27 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -499
- glaip_sdk/cli/update_notifier.py +14 -5
- glaip_sdk/cli/utils.py +243 -1252
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +45 -9
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +291 -35
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +122 -12
- glaip_sdk/client/run_rendering.py +466 -89
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +155 -10
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/hitl/__init__.py +15 -0
- glaip_sdk/hitl/local.py +151 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +870 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +275 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +25 -13
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/METADATA +56 -21
- glaip_sdk-0.6.19.dist-info/RECORD +163 -0
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.19.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.19.dist-info/top_level.txt +1 -0
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.3.dist-info/RECORD +0 -83
- glaip_sdk-0.1.3.dist-info/entry_points.txt +0 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""SSE event processing mixin for StepManager.
|
|
2
2
|
|
|
3
3
|
Authors:
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
@@ -7,373 +7,27 @@ Authors:
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
-
from collections.abc import
|
|
10
|
+
from collections.abc import Mapping
|
|
11
11
|
from copy import deepcopy
|
|
12
12
|
from time import monotonic
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
-
from glaip_sdk.icons import ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
|
|
16
15
|
from glaip_sdk.utils.rendering.models import Step
|
|
17
|
-
from glaip_sdk.utils.rendering.
|
|
16
|
+
from glaip_sdk.utils.rendering.timing import coerce_server_time
|
|
18
17
|
|
|
19
18
|
logger = logging.getLogger(__name__)
|
|
20
|
-
UNKNOWN_STEP_DETAIL = "Unknown step detail"
|
|
21
19
|
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
"""Manages the lifecycle and organization of execution steps.
|
|
21
|
+
COERCION_FAILED_KEY = "_meta_coercion_failed_"
|
|
25
22
|
|
|
26
|
-
Tracks step creation, parent-child relationships, and execution state
|
|
27
|
-
with automatic pruning of old steps when limits are reached.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(self, max_steps: int = 200) -> None:
|
|
31
|
-
"""Initialize the step manager.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
max_steps: Maximum number of steps to retain before pruning
|
|
35
|
-
"""
|
|
36
|
-
normalised_max = int(max_steps) if isinstance(max_steps, (int, float)) else 0
|
|
37
|
-
self.state = StepTreeState(max_steps=normalised_max)
|
|
38
|
-
self.by_id: dict[str, Step] = self.state.step_index
|
|
39
|
-
self.key_index: dict[tuple, str] = {}
|
|
40
|
-
self.slot_counter: dict[tuple, int] = {}
|
|
41
|
-
self.max_steps = normalised_max
|
|
42
|
-
self._last_running: dict[tuple, str] = {}
|
|
43
|
-
self._step_aliases: dict[str, str] = {}
|
|
44
|
-
self.root_agent_id: str | None = None
|
|
45
|
-
self._scope_anchors: dict[str, list[str]] = {}
|
|
46
|
-
self._step_scope_map: dict[str, str] = {}
|
|
47
|
-
|
|
48
|
-
def set_root_agent(self, agent_id: str | None) -> None:
|
|
49
|
-
"""Record the root agent identifier for scope-aware parenting."""
|
|
50
|
-
if isinstance(agent_id, str) and agent_id.strip():
|
|
51
|
-
self.root_agent_id = agent_id.strip()
|
|
52
|
-
|
|
53
|
-
def _alloc_slot(
|
|
54
|
-
self,
|
|
55
|
-
task_id: str | None,
|
|
56
|
-
context_id: str | None,
|
|
57
|
-
kind: str,
|
|
58
|
-
name: str,
|
|
59
|
-
) -> int:
|
|
60
|
-
k = (task_id, context_id, kind, name)
|
|
61
|
-
self.slot_counter[k] = self.slot_counter.get(k, 0) + 1
|
|
62
|
-
return self.slot_counter[k]
|
|
63
|
-
|
|
64
|
-
def _key(
|
|
65
|
-
self,
|
|
66
|
-
task_id: str | None,
|
|
67
|
-
context_id: str | None,
|
|
68
|
-
kind: str,
|
|
69
|
-
name: str,
|
|
70
|
-
slot: int,
|
|
71
|
-
) -> tuple[str | None, str | None, str, str, int]:
|
|
72
|
-
return (task_id, context_id, kind, name, slot)
|
|
73
|
-
|
|
74
|
-
def _make_id(
|
|
75
|
-
self,
|
|
76
|
-
task_id: str | None,
|
|
77
|
-
context_id: str | None,
|
|
78
|
-
kind: str,
|
|
79
|
-
name: str,
|
|
80
|
-
slot: int,
|
|
81
|
-
) -> str:
|
|
82
|
-
return f"{task_id or 't'}::{context_id or 'c'}::{kind}::{name}::{slot}"
|
|
83
|
-
|
|
84
|
-
def start_or_get(
|
|
85
|
-
self,
|
|
86
|
-
*,
|
|
87
|
-
task_id: str | None,
|
|
88
|
-
context_id: str | None,
|
|
89
|
-
kind: str,
|
|
90
|
-
name: str,
|
|
91
|
-
parent_id: str | None = None,
|
|
92
|
-
args: dict[str, object] | None = None,
|
|
93
|
-
) -> Step:
|
|
94
|
-
"""Start a new step or return existing running step with same parameters.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
task_id: Task identifier
|
|
98
|
-
context_id: Context identifier
|
|
99
|
-
kind: Step kind (tool, delegate, agent)
|
|
100
|
-
name: Step name
|
|
101
|
-
parent_id: Parent step ID if this is a child step
|
|
102
|
-
args: Step arguments
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
The Step instance (new or existing)
|
|
106
|
-
"""
|
|
107
|
-
existing = self.find_running(task_id=task_id, context_id=context_id, kind=kind, name=name)
|
|
108
|
-
if existing:
|
|
109
|
-
if args and existing.args != args:
|
|
110
|
-
existing.args = args
|
|
111
|
-
return existing
|
|
112
|
-
slot = self._alloc_slot(task_id, context_id, kind, name)
|
|
113
|
-
key = self._key(task_id, context_id, kind, name, slot)
|
|
114
|
-
step_id = self._make_id(task_id, context_id, kind, name, slot)
|
|
115
|
-
st = Step(
|
|
116
|
-
step_id=step_id,
|
|
117
|
-
kind=kind,
|
|
118
|
-
name=name,
|
|
119
|
-
parent_id=parent_id,
|
|
120
|
-
task_id=task_id,
|
|
121
|
-
context_id=context_id,
|
|
122
|
-
args=args or {},
|
|
123
|
-
)
|
|
124
|
-
self.by_id[step_id] = st
|
|
125
|
-
if parent_id:
|
|
126
|
-
self.children.setdefault(parent_id, []).append(step_id)
|
|
127
|
-
else:
|
|
128
|
-
self.order.append(step_id)
|
|
129
|
-
self.key_index[key] = step_id
|
|
130
|
-
self.state.retained_ids.add(step_id)
|
|
131
|
-
self._prune_steps()
|
|
132
|
-
self._last_running[(task_id, context_id, kind, name)] = step_id
|
|
133
|
-
return st
|
|
134
|
-
|
|
135
|
-
def _calculate_total_steps(self) -> int:
|
|
136
|
-
"""Calculate total number of steps."""
|
|
137
|
-
return len(self.order) + sum(len(v) for v in self.children.values())
|
|
138
|
-
|
|
139
|
-
def _get_subtree_size(self, root_id: str) -> int:
|
|
140
|
-
"""Get the size of a subtree (including root)."""
|
|
141
|
-
subtree = [root_id]
|
|
142
|
-
stack = list(self.children.get(root_id, []))
|
|
143
|
-
while stack:
|
|
144
|
-
x = stack.pop()
|
|
145
|
-
subtree.append(x)
|
|
146
|
-
stack.extend(self.children.get(x, []))
|
|
147
|
-
return len(subtree)
|
|
148
|
-
|
|
149
|
-
def _remove_subtree(self, root_id: str) -> None:
|
|
150
|
-
"""Remove a complete subtree from all data structures."""
|
|
151
|
-
for step_id in self._collect_subtree_ids(root_id):
|
|
152
|
-
self._purge_step_references(step_id)
|
|
153
|
-
|
|
154
|
-
def _collect_subtree_ids(self, root_id: str) -> list[str]:
|
|
155
|
-
"""Return a flat list of step ids contained within a subtree."""
|
|
156
|
-
stack = [root_id]
|
|
157
|
-
collected: list[str] = []
|
|
158
|
-
while stack:
|
|
159
|
-
sid = stack.pop()
|
|
160
|
-
collected.append(sid)
|
|
161
|
-
stack.extend(self.children.pop(sid, []))
|
|
162
|
-
return collected
|
|
163
|
-
|
|
164
|
-
def _purge_step_references(self, step_id: str) -> None:
|
|
165
|
-
"""Remove a single step id from all indexes and helper structures."""
|
|
166
|
-
st = self.by_id.pop(step_id, None)
|
|
167
|
-
if st:
|
|
168
|
-
key = (st.task_id, st.context_id, st.kind, st.name)
|
|
169
|
-
self._last_running.pop(key, None)
|
|
170
|
-
self.state.retained_ids.discard(step_id)
|
|
171
|
-
self.state.discard_running(step_id)
|
|
172
|
-
self._remove_parent_links(step_id)
|
|
173
|
-
if step_id in self.order:
|
|
174
|
-
self.order.remove(step_id)
|
|
175
|
-
self.state.buffered_children.pop(step_id, None)
|
|
176
|
-
self.state.pending_branch_failures.discard(step_id)
|
|
177
|
-
|
|
178
|
-
def _remove_parent_links(self, child_id: str) -> None:
|
|
179
|
-
"""Detach a child id from any parent lists."""
|
|
180
|
-
for parent, kids in self.children.copy().items():
|
|
181
|
-
if child_id not in kids:
|
|
182
|
-
continue
|
|
183
|
-
kids.remove(child_id)
|
|
184
|
-
if not kids:
|
|
185
|
-
self.children.pop(parent, None)
|
|
186
|
-
|
|
187
|
-
def _should_prune_steps(self, total: int) -> bool:
|
|
188
|
-
"""Check if steps should be pruned."""
|
|
189
|
-
if self.max_steps <= 0:
|
|
190
|
-
return False
|
|
191
|
-
return total > self.max_steps
|
|
192
23
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return self.order[0] if self.order else None
|
|
24
|
+
class StepEventMixin:
|
|
25
|
+
"""Mixin providing SSE event processing capabilities for StepManager.
|
|
196
26
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
while self._should_prune_steps(total) and self.order:
|
|
204
|
-
sid = self._get_oldest_step_id()
|
|
205
|
-
if not sid:
|
|
206
|
-
break
|
|
207
|
-
|
|
208
|
-
subtree_size = self._get_subtree_size(sid)
|
|
209
|
-
self._remove_subtree(sid)
|
|
210
|
-
total -= subtree_size
|
|
211
|
-
|
|
212
|
-
def remove_step(self, step_id: str) -> None:
|
|
213
|
-
"""Remove a single step from the tree and cached indexes."""
|
|
214
|
-
step = self.by_id.pop(step_id, None)
|
|
215
|
-
if not step:
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
if step.parent_id:
|
|
219
|
-
self.state.unlink_child(step.parent_id, step_id)
|
|
220
|
-
else:
|
|
221
|
-
self.state.unlink_root(step_id)
|
|
222
|
-
|
|
223
|
-
self.children.pop(step_id, None)
|
|
224
|
-
self.state.buffered_children.pop(step_id, None)
|
|
225
|
-
self.state.retained_ids.discard(step_id)
|
|
226
|
-
self.state.pending_branch_failures.discard(step_id)
|
|
227
|
-
self.state.discard_running(step_id)
|
|
228
|
-
|
|
229
|
-
self.key_index = {key: sid for key, sid in self.key_index.items() if sid != step_id}
|
|
230
|
-
for key, last_sid in self._last_running.copy().items():
|
|
231
|
-
if last_sid == step_id:
|
|
232
|
-
self._last_running.pop(key, None)
|
|
233
|
-
|
|
234
|
-
aliases = [alias for alias, target in self._step_aliases.items() if alias == step_id or target == step_id]
|
|
235
|
-
for alias in aliases:
|
|
236
|
-
self._step_aliases.pop(alias, None)
|
|
237
|
-
|
|
238
|
-
def get_child_count(self, step_id: str) -> int:
|
|
239
|
-
"""Get the number of child steps for a given step.
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
step_id: The parent step ID
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
Number of child steps
|
|
246
|
-
"""
|
|
247
|
-
return len(self.children.get(step_id, []))
|
|
248
|
-
|
|
249
|
-
def find_running(
|
|
250
|
-
self,
|
|
251
|
-
*,
|
|
252
|
-
task_id: str | None,
|
|
253
|
-
context_id: str | None,
|
|
254
|
-
kind: str,
|
|
255
|
-
name: str,
|
|
256
|
-
) -> Step | None:
|
|
257
|
-
"""Find a currently running step with the given parameters.
|
|
258
|
-
|
|
259
|
-
Args:
|
|
260
|
-
task_id: Task identifier
|
|
261
|
-
context_id: Context identifier
|
|
262
|
-
kind: Step kind (tool, delegate, agent)
|
|
263
|
-
name: Step name
|
|
264
|
-
|
|
265
|
-
Returns:
|
|
266
|
-
The running Step if found, None otherwise
|
|
267
|
-
"""
|
|
268
|
-
key = (task_id, context_id, kind, name)
|
|
269
|
-
step_id = self._last_running.get(key)
|
|
270
|
-
if step_id:
|
|
271
|
-
st = self.by_id.get(step_id)
|
|
272
|
-
if st and st.status != "finished":
|
|
273
|
-
return st
|
|
274
|
-
for sid in reversed(list(self._iter_all_steps())):
|
|
275
|
-
st = self.by_id.get(sid)
|
|
276
|
-
if (
|
|
277
|
-
st
|
|
278
|
-
and (st.task_id, st.context_id, st.kind, st.name)
|
|
279
|
-
== (
|
|
280
|
-
task_id,
|
|
281
|
-
context_id,
|
|
282
|
-
kind,
|
|
283
|
-
name,
|
|
284
|
-
)
|
|
285
|
-
and st.status != "finished"
|
|
286
|
-
):
|
|
287
|
-
return st
|
|
288
|
-
return None
|
|
289
|
-
|
|
290
|
-
def finish(
|
|
291
|
-
self,
|
|
292
|
-
*,
|
|
293
|
-
task_id: str | None,
|
|
294
|
-
context_id: str | None,
|
|
295
|
-
kind: str,
|
|
296
|
-
name: str,
|
|
297
|
-
output: object | None = None,
|
|
298
|
-
duration_raw: float | None = None,
|
|
299
|
-
) -> Step:
|
|
300
|
-
"""Finish a step with the given parameters.
|
|
301
|
-
|
|
302
|
-
Args:
|
|
303
|
-
task_id: Task identifier
|
|
304
|
-
context_id: Context identifier
|
|
305
|
-
kind: Step kind (tool, delegate, agent)
|
|
306
|
-
name: Step name
|
|
307
|
-
output: Step output data
|
|
308
|
-
duration_raw: Raw duration in seconds
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
The finished Step instance
|
|
312
|
-
|
|
313
|
-
Raises:
|
|
314
|
-
RuntimeError: If no matching step is found
|
|
315
|
-
"""
|
|
316
|
-
st = self.find_running(task_id=task_id, context_id=context_id, kind=kind, name=name)
|
|
317
|
-
if not st:
|
|
318
|
-
# Try to find any existing step with matching parameters, even if not running
|
|
319
|
-
for sid in reversed(list(self._iter_all_steps())):
|
|
320
|
-
st_check = self.by_id.get(sid)
|
|
321
|
-
if (
|
|
322
|
-
st_check
|
|
323
|
-
and st_check.task_id == task_id
|
|
324
|
-
and st_check.context_id == context_id
|
|
325
|
-
and st_check.kind == kind
|
|
326
|
-
and st_check.name == name
|
|
327
|
-
):
|
|
328
|
-
st = st_check
|
|
329
|
-
break
|
|
330
|
-
|
|
331
|
-
# If still no step found, create a new one
|
|
332
|
-
if not st:
|
|
333
|
-
st = self.start_or_get(task_id=task_id, context_id=context_id, kind=kind, name=name)
|
|
334
|
-
|
|
335
|
-
if output:
|
|
336
|
-
st.output = output
|
|
337
|
-
st.finish(duration_raw)
|
|
338
|
-
key = (task_id, context_id, kind, name)
|
|
339
|
-
if self._last_running.get(key) == st.step_id:
|
|
340
|
-
self._last_running.pop(key, None)
|
|
341
|
-
return st
|
|
342
|
-
|
|
343
|
-
def _iter_all_steps(self) -> Iterator[str]:
|
|
344
|
-
for root in self.order:
|
|
345
|
-
yield root
|
|
346
|
-
stack = list(self.children.get(root, []))
|
|
347
|
-
while stack:
|
|
348
|
-
sid = stack.pop()
|
|
349
|
-
yield sid
|
|
350
|
-
stack.extend(self.children.get(sid, []))
|
|
351
|
-
|
|
352
|
-
def iter_tree(self) -> Iterator[tuple[str, tuple[bool, ...]]]:
|
|
353
|
-
"""Expose depth-first traversal info for rendering."""
|
|
354
|
-
yield from self.state.iter_visible_tree()
|
|
355
|
-
|
|
356
|
-
@property
|
|
357
|
-
def order(self) -> list[str]:
|
|
358
|
-
"""Root step ordering accessor backed by StepTreeState."""
|
|
359
|
-
return self.state.root_order
|
|
360
|
-
|
|
361
|
-
@order.setter
|
|
362
|
-
def order(self, value: list[str]) -> None:
|
|
363
|
-
self.state.root_order = list(value)
|
|
364
|
-
|
|
365
|
-
@property
|
|
366
|
-
def children(self) -> dict[str, list[str]]:
|
|
367
|
-
"""Child mapping accessor backed by StepTreeState."""
|
|
368
|
-
return self.state.child_map
|
|
369
|
-
|
|
370
|
-
@children.setter
|
|
371
|
-
def children(self, value: dict[str, list[str]]) -> None:
|
|
372
|
-
self.state.child_map = value
|
|
373
|
-
|
|
374
|
-
# ------------------------------------------------------------------
|
|
375
|
-
# SSE-aware helpers
|
|
376
|
-
# ------------------------------------------------------------------
|
|
27
|
+
This mixin adds methods to process server-sent events (SSE) and update
|
|
28
|
+
step state accordingly. It handles event parsing, step creation/updates,
|
|
29
|
+
parent-child relationships, and duration tracking.
|
|
30
|
+
"""
|
|
377
31
|
|
|
378
32
|
def apply_event(self, event: dict[str, Any]) -> Step:
|
|
379
33
|
"""Apply an SSE step event and return the updated step."""
|
|
@@ -463,6 +117,7 @@ class StepManager:
|
|
|
463
117
|
|
|
464
118
|
def _apply_single_event(self, event: dict[str, Any]) -> Step:
|
|
465
119
|
metadata, step_id, tool_info, args = self._parse_event_payload(event)
|
|
120
|
+
metadata_failed = bool(metadata.pop(COERCION_FAILED_KEY, False))
|
|
466
121
|
tool_name = self._resolve_tool_name(tool_info, metadata, step_id)
|
|
467
122
|
kind = self._derive_step_kind(tool_name, metadata)
|
|
468
123
|
parent_hint = self._coerce_parent_id(metadata.get("previous_step_ids"))
|
|
@@ -479,14 +134,17 @@ class StepManager:
|
|
|
479
134
|
self._link_step(step, parent_id)
|
|
480
135
|
|
|
481
136
|
self.state.retained_ids.add(step.step_id)
|
|
482
|
-
|
|
137
|
+
if metadata_failed:
|
|
138
|
+
step.metadata = {}
|
|
139
|
+
else:
|
|
140
|
+
step.metadata = dict(metadata)
|
|
483
141
|
self._flush_buffered_children(step.step_id)
|
|
484
142
|
self._apply_pending_branch_flags(step.step_id)
|
|
485
143
|
|
|
486
144
|
status = self._normalise_status(metadata.get("status"), event.get("status"), event.get("task_state"))
|
|
487
145
|
status = self._apply_failure_state(step, status, event)
|
|
488
146
|
|
|
489
|
-
server_time =
|
|
147
|
+
server_time = coerce_server_time(metadata.get("time"))
|
|
490
148
|
self._update_server_timestamps(step, server_time, status)
|
|
491
149
|
|
|
492
150
|
self._apply_duration(
|
|
@@ -504,16 +162,17 @@ class StepManager:
|
|
|
504
162
|
status=status,
|
|
505
163
|
)
|
|
506
164
|
|
|
507
|
-
step.status_icon = self._status_icon_for_step(step)
|
|
508
165
|
self._update_parallel_tracking(step)
|
|
509
166
|
self._update_running_index(step)
|
|
510
167
|
self._prune_steps()
|
|
511
168
|
return step
|
|
512
169
|
|
|
513
170
|
def _parse_event_payload(self, event: dict[str, Any]) -> tuple[dict[str, Any], str, dict[str, Any], dict[str, Any]]:
|
|
514
|
-
|
|
515
|
-
|
|
171
|
+
metadata_raw = event.get("metadata") or {}
|
|
172
|
+
metadata, metadata_reliable = self._coerce_event_metadata(metadata_raw)
|
|
173
|
+
if not metadata:
|
|
516
174
|
raise ValueError("Step event missing metadata payload")
|
|
175
|
+
metadata[COERCION_FAILED_KEY] = not metadata_reliable
|
|
517
176
|
|
|
518
177
|
step_id = metadata.get("step_id")
|
|
519
178
|
if not isinstance(step_id, str) or not step_id:
|
|
@@ -531,6 +190,39 @@ class StepManager:
|
|
|
531
190
|
|
|
532
191
|
return metadata, step_id, tool_info, args
|
|
533
192
|
|
|
193
|
+
def _coerce_event_metadata(self, metadata: Any) -> tuple[dict[str, Any], bool]:
|
|
194
|
+
"""Return a dict copy of event metadata with graceful fallbacks."""
|
|
195
|
+
if isinstance(metadata, dict):
|
|
196
|
+
return metadata, True
|
|
197
|
+
if isinstance(metadata, Mapping):
|
|
198
|
+
try:
|
|
199
|
+
return dict(metadata), True
|
|
200
|
+
except Exception:
|
|
201
|
+
logger.debug("Failed to coerce mapping metadata; falling back to key-by-key copy", exc_info=True)
|
|
202
|
+
return self._copy_mapping_fields(metadata), False
|
|
203
|
+
# All other payloads are treated as empty
|
|
204
|
+
return {}, False
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _copy_mapping_fields(metadata: Mapping[str, Any]) -> dict[str, Any]:
|
|
208
|
+
"""Copy known fields from a mapping without iteration."""
|
|
209
|
+
copied: dict[str, Any] = {}
|
|
210
|
+
for key in (
|
|
211
|
+
"step_id",
|
|
212
|
+
"tool_info",
|
|
213
|
+
"status",
|
|
214
|
+
"kind",
|
|
215
|
+
"previous_step_ids",
|
|
216
|
+
"time",
|
|
217
|
+
"agent_name",
|
|
218
|
+
"task_id",
|
|
219
|
+
"context_id",
|
|
220
|
+
):
|
|
221
|
+
value = metadata.get(key) # type: ignore[attr-defined]
|
|
222
|
+
if value is not None:
|
|
223
|
+
copied[key] = value
|
|
224
|
+
return copied
|
|
225
|
+
|
|
534
226
|
def _resolve_tool_name(self, tool_info: dict[str, Any], metadata: dict[str, Any], step_id: str) -> str:
|
|
535
227
|
name = tool_info.get("name")
|
|
536
228
|
if not name:
|
|
@@ -738,65 +430,6 @@ class StepManager:
|
|
|
738
430
|
return False
|
|
739
431
|
return all(ch in "0123456789abcdefABCDEF" for ch in stripped)
|
|
740
432
|
|
|
741
|
-
def _step_icon_for_kind(self, step_kind: str) -> str:
|
|
742
|
-
if step_kind == "agent":
|
|
743
|
-
return ICON_AGENT_STEP
|
|
744
|
-
if step_kind == "delegate":
|
|
745
|
-
return ICON_DELEGATE
|
|
746
|
-
if step_kind == "thinking":
|
|
747
|
-
return "💭"
|
|
748
|
-
return ICON_TOOL_STEP
|
|
749
|
-
|
|
750
|
-
def _humanize_tool_name(self, raw_name: str | None) -> str:
|
|
751
|
-
if not raw_name:
|
|
752
|
-
return UNKNOWN_STEP_DETAIL
|
|
753
|
-
name = raw_name
|
|
754
|
-
if name.startswith("delegate_to_"):
|
|
755
|
-
name = name.removeprefix("delegate_to_")
|
|
756
|
-
elif name.startswith("delegate_"):
|
|
757
|
-
name = name.removeprefix("delegate_")
|
|
758
|
-
cleaned = name.replace("_", " ").replace("-", " ").strip()
|
|
759
|
-
if not cleaned:
|
|
760
|
-
return UNKNOWN_STEP_DETAIL
|
|
761
|
-
return cleaned[:1].upper() + cleaned[1:]
|
|
762
|
-
|
|
763
|
-
def _compose_display_label(
|
|
764
|
-
self,
|
|
765
|
-
step_kind: str,
|
|
766
|
-
tool_name: str | None,
|
|
767
|
-
args: dict[str, Any],
|
|
768
|
-
metadata: dict[str, Any],
|
|
769
|
-
) -> str:
|
|
770
|
-
icon = self._step_icon_for_kind(step_kind)
|
|
771
|
-
body = self._resolve_label_body(step_kind, tool_name, metadata)
|
|
772
|
-
label = f"{icon} {body}".strip()
|
|
773
|
-
if isinstance(args, dict) and args:
|
|
774
|
-
label = f"{label} —"
|
|
775
|
-
return label or UNKNOWN_STEP_DETAIL
|
|
776
|
-
|
|
777
|
-
def _resolve_label_body(
|
|
778
|
-
self,
|
|
779
|
-
step_kind: str,
|
|
780
|
-
tool_name: str | None,
|
|
781
|
-
metadata: dict[str, Any],
|
|
782
|
-
) -> str:
|
|
783
|
-
if step_kind == "thinking":
|
|
784
|
-
thinking_text = metadata.get("thinking_and_activity_info")
|
|
785
|
-
if isinstance(thinking_text, str) and thinking_text.strip():
|
|
786
|
-
return thinking_text.strip()
|
|
787
|
-
return "Thinking…"
|
|
788
|
-
|
|
789
|
-
if step_kind == "delegate":
|
|
790
|
-
return self._humanize_tool_name(tool_name)
|
|
791
|
-
|
|
792
|
-
if step_kind == "agent":
|
|
793
|
-
agent_name = metadata.get("agent_name")
|
|
794
|
-
if isinstance(agent_name, str) and agent_name.strip():
|
|
795
|
-
return agent_name.strip()
|
|
796
|
-
|
|
797
|
-
friendly = self._humanize_tool_name(tool_name)
|
|
798
|
-
return friendly
|
|
799
|
-
|
|
800
433
|
def _normalise_status(
|
|
801
434
|
self,
|
|
802
435
|
metadata_status: Any,
|
|
@@ -994,15 +627,6 @@ class StepManager:
|
|
|
994
627
|
slug = slug.replace("-", "_").strip()
|
|
995
628
|
return slug or None
|
|
996
629
|
|
|
997
|
-
@staticmethod
|
|
998
|
-
def _coerce_server_time(value: Any) -> float | None:
|
|
999
|
-
if isinstance(value, (int, float)):
|
|
1000
|
-
return float(value)
|
|
1001
|
-
try:
|
|
1002
|
-
return float(value)
|
|
1003
|
-
except (TypeError, ValueError):
|
|
1004
|
-
return None
|
|
1005
|
-
|
|
1006
630
|
def _update_server_timestamps(self, step: Step, server_time: float | None, status: str) -> None:
|
|
1007
631
|
if server_time is None:
|
|
1008
632
|
return
|
|
@@ -1038,7 +662,6 @@ class StepManager:
|
|
|
1038
662
|
step = self.by_id.get(step_id)
|
|
1039
663
|
if step:
|
|
1040
664
|
step.branch_failed = True
|
|
1041
|
-
step.status_icon = "warning"
|
|
1042
665
|
self.state.pending_branch_failures.discard(step_id)
|
|
1043
666
|
|
|
1044
667
|
def _set_branch_warning(self, parent_id: str | None) -> None:
|
|
@@ -1047,7 +670,6 @@ class StepManager:
|
|
|
1047
670
|
parent = self.by_id.get(parent_id)
|
|
1048
671
|
if parent:
|
|
1049
672
|
parent.branch_failed = True
|
|
1050
|
-
parent.status_icon = "warning"
|
|
1051
673
|
else:
|
|
1052
674
|
self.state.pending_branch_failures.add(parent_id)
|
|
1053
675
|
|
|
@@ -1079,15 +701,6 @@ class StepManager:
|
|
|
1079
701
|
if current:
|
|
1080
702
|
current.is_parallel = is_parallel
|
|
1081
703
|
|
|
1082
|
-
def _status_icon_for_step(self, step: Step) -> str:
|
|
1083
|
-
if step.status == "finished":
|
|
1084
|
-
return "warning" if step.branch_failed else "success"
|
|
1085
|
-
if step.status == "failed":
|
|
1086
|
-
return "failed"
|
|
1087
|
-
if step.status == "stopped":
|
|
1088
|
-
return "warning"
|
|
1089
|
-
return "spinner"
|
|
1090
|
-
|
|
1091
704
|
def _canonicalize_step_id(self, step_id: str, tool_info: dict[str, Any]) -> str:
|
|
1092
705
|
alias = self._lookup_alias(step_id)
|
|
1093
706
|
if alias:
|