glaip-sdk 0.6.5b6__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.
- glaip_sdk/__init__.py +42 -5
- glaip_sdk/agents/base.py +156 -32
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +15 -12
- glaip_sdk/cli/commands/configure.py +2 -3
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +127 -39
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +112 -32
- glaip_sdk/cli/slash/agent_session.py +5 -2
- glaip_sdk/cli/slash/prompt.py +11 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
- glaip_sdk/cli/slash/session.py +58 -13
- glaip_sdk/cli/slash/tui/__init__.py +26 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
- glaip_sdk/cli/slash/tui/accounts_app.py +70 -9
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +59 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +5 -3
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +50 -8
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -1
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +367 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +57 -26
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +17 -0
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/registry/tool.py +273 -59
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +5 -8
- glaip_sdk/runner/langgraph.py +317 -42
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +44 -11
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +43 -11
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/runtime_config.py +15 -12
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +47 -37
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
- glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
|
@@ -9,9 +9,12 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
-
from collections.abc import Callable
|
|
12
|
+
from collections.abc import AsyncIterable, Callable
|
|
13
13
|
from time import monotonic
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from glaip_sdk.hitl.remote import RemoteHITLHandler
|
|
15
18
|
|
|
16
19
|
import httpx
|
|
17
20
|
from rich.console import Console as _Console
|
|
@@ -30,6 +33,7 @@ from glaip_sdk.utils.rendering.renderer import (
|
|
|
30
33
|
from glaip_sdk.utils.rendering.state import TranscriptBuffer
|
|
31
34
|
|
|
32
35
|
NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
|
|
36
|
+
_FINAL_EVENT_TYPES = {"final_response", "error", "step_limit_exceeded"}
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
def _coerce_to_string(value: Any) -> str:
|
|
@@ -129,6 +133,7 @@ class AgentRunRenderingManager:
|
|
|
129
133
|
timeout_seconds: float,
|
|
130
134
|
agent_name: str | None,
|
|
131
135
|
meta: dict[str, Any],
|
|
136
|
+
hitl_handler: RemoteHITLHandler | None = None,
|
|
132
137
|
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
133
138
|
"""Process streaming events and accumulate response."""
|
|
134
139
|
final_text = ""
|
|
@@ -152,10 +157,14 @@ class AgentRunRenderingManager:
|
|
|
152
157
|
final_text,
|
|
153
158
|
stats_usage,
|
|
154
159
|
meta,
|
|
160
|
+
hitl_handler=hitl_handler,
|
|
155
161
|
)
|
|
156
162
|
|
|
157
163
|
if controller and getattr(controller, "enabled", False):
|
|
158
164
|
controller.poll(renderer)
|
|
165
|
+
parsed_event = self._parse_event(event)
|
|
166
|
+
if parsed_event and self._is_final_event(parsed_event):
|
|
167
|
+
break
|
|
159
168
|
finally:
|
|
160
169
|
if controller and getattr(controller, "enabled", False):
|
|
161
170
|
controller.on_stream_complete()
|
|
@@ -163,6 +172,300 @@ class AgentRunRenderingManager:
|
|
|
163
172
|
finished_monotonic = monotonic()
|
|
164
173
|
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
165
174
|
|
|
175
|
+
async def async_process_stream_events(
|
|
176
|
+
self,
|
|
177
|
+
event_stream: AsyncIterable[dict[str, Any]],
|
|
178
|
+
renderer: RichStreamRenderer,
|
|
179
|
+
meta: dict[str, Any],
|
|
180
|
+
*,
|
|
181
|
+
skip_final_render: bool = True,
|
|
182
|
+
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
183
|
+
"""Process streaming events from an async event source.
|
|
184
|
+
|
|
185
|
+
This method provides unified stream processing for both remote (HTTP)
|
|
186
|
+
and local (LangGraph) agent execution, ensuring consistent behavior.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
event_stream: Async iterable yielding SSE-like event dicts.
|
|
190
|
+
Each event should have a "data" key with JSON string, or be
|
|
191
|
+
a pre-parsed dict with "content", "metadata", etc.
|
|
192
|
+
renderer: Renderer to use for displaying events.
|
|
193
|
+
meta: Metadata dictionary for renderer context.
|
|
194
|
+
skip_final_render: If True, skip rendering final_response events
|
|
195
|
+
(they are rendered separately via finalize_renderer).
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
|
|
199
|
+
"""
|
|
200
|
+
final_text = ""
|
|
201
|
+
stats_usage: dict[str, Any] = {}
|
|
202
|
+
started_monotonic: float | None = None
|
|
203
|
+
last_rendered_content: str | None = None
|
|
204
|
+
|
|
205
|
+
controller = getattr(renderer, "transcript_controller", None)
|
|
206
|
+
if controller and getattr(controller, "enabled", False):
|
|
207
|
+
controller.on_stream_start(renderer)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
async for event in event_stream:
|
|
211
|
+
if started_monotonic is None:
|
|
212
|
+
started_monotonic = monotonic()
|
|
213
|
+
|
|
214
|
+
# Parse event if needed (handles both raw SSE and pre-parsed dicts)
|
|
215
|
+
parsed_event = self._parse_event(event)
|
|
216
|
+
if parsed_event is None:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Process the event and update accumulators
|
|
220
|
+
final_text, stats_usage = self._handle_parsed_event(
|
|
221
|
+
parsed_event,
|
|
222
|
+
renderer,
|
|
223
|
+
final_text,
|
|
224
|
+
stats_usage,
|
|
225
|
+
meta,
|
|
226
|
+
skip_final_render=skip_final_render,
|
|
227
|
+
last_rendered_content=last_rendered_content,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Track last rendered content to avoid duplicates
|
|
231
|
+
content_str = self._extract_content_string(parsed_event)
|
|
232
|
+
if content_str:
|
|
233
|
+
last_rendered_content = content_str
|
|
234
|
+
|
|
235
|
+
if controller and getattr(controller, "enabled", False):
|
|
236
|
+
controller.poll(renderer)
|
|
237
|
+
if parsed_event and self._is_final_event(parsed_event):
|
|
238
|
+
break
|
|
239
|
+
finally:
|
|
240
|
+
if controller and getattr(controller, "enabled", False):
|
|
241
|
+
controller.on_stream_complete()
|
|
242
|
+
|
|
243
|
+
finished_monotonic = monotonic()
|
|
244
|
+
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
245
|
+
|
|
246
|
+
def _parse_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
247
|
+
"""Parse an SSE event dict into a usable format.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
event: Raw event dict, either with "data" key (SSE format) or
|
|
251
|
+
pre-parsed with "content", "metadata", etc.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Parsed event dict, or None if parsing fails.
|
|
255
|
+
"""
|
|
256
|
+
if "data" in event:
|
|
257
|
+
try:
|
|
258
|
+
return json.loads(event["data"])
|
|
259
|
+
except json.JSONDecodeError:
|
|
260
|
+
self._logger.debug("Non-JSON SSE fragment skipped")
|
|
261
|
+
return None
|
|
262
|
+
# Already parsed (e.g., from local runner)
|
|
263
|
+
return event if event else None
|
|
264
|
+
|
|
265
|
+
def _handle_parsed_event(
|
|
266
|
+
self,
|
|
267
|
+
ev: dict[str, Any],
|
|
268
|
+
renderer: RichStreamRenderer,
|
|
269
|
+
final_text: str,
|
|
270
|
+
stats_usage: dict[str, Any],
|
|
271
|
+
meta: dict[str, Any],
|
|
272
|
+
*,
|
|
273
|
+
skip_final_render: bool = True,
|
|
274
|
+
last_rendered_content: str | None = None,
|
|
275
|
+
) -> tuple[str, dict[str, Any]]:
|
|
276
|
+
"""Handle a parsed event and update accumulators.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
ev: Parsed event dictionary.
|
|
280
|
+
renderer: Renderer instance.
|
|
281
|
+
final_text: Current accumulated final text.
|
|
282
|
+
stats_usage: Usage statistics dictionary.
|
|
283
|
+
meta: Metadata dictionary.
|
|
284
|
+
skip_final_render: If True, skip rendering final_response events.
|
|
285
|
+
last_rendered_content: Last rendered content to avoid duplicates.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Tuple of (updated_final_text, updated_stats_usage).
|
|
289
|
+
"""
|
|
290
|
+
kind = self._get_event_kind(ev)
|
|
291
|
+
|
|
292
|
+
# Dispatch to specialized handlers based on event kind
|
|
293
|
+
handler = self._get_event_handler(kind, ev)
|
|
294
|
+
if handler:
|
|
295
|
+
return handler(ev, renderer, final_text, stats_usage, meta, skip_final_render)
|
|
296
|
+
|
|
297
|
+
# Default: handle content events
|
|
298
|
+
return self._handle_content_event_async(ev, renderer, final_text, stats_usage, last_rendered_content)
|
|
299
|
+
|
|
300
|
+
def _get_event_handler(
|
|
301
|
+
self,
|
|
302
|
+
kind: str | None,
|
|
303
|
+
ev: dict[str, Any],
|
|
304
|
+
) -> Callable[..., tuple[str, dict[str, Any]]] | None:
|
|
305
|
+
"""Get the appropriate handler for an event kind.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
kind: Event kind string.
|
|
309
|
+
ev: Event dictionary (for checking is_final flag).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Handler function or None for default content handling.
|
|
313
|
+
"""
|
|
314
|
+
if kind == "usage":
|
|
315
|
+
return self._handle_usage_event
|
|
316
|
+
if kind == "final_response" or ev.get("is_final"):
|
|
317
|
+
return self._handle_final_response_event
|
|
318
|
+
if kind == "run_info":
|
|
319
|
+
return self._handle_run_info_event_wrapper
|
|
320
|
+
if kind in ("artifact", "status_update"):
|
|
321
|
+
return self._handle_render_only_event
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
def _handle_usage_event(
|
|
325
|
+
self,
|
|
326
|
+
ev: dict[str, Any],
|
|
327
|
+
_renderer: RichStreamRenderer,
|
|
328
|
+
final_text: str,
|
|
329
|
+
stats_usage: dict[str, Any],
|
|
330
|
+
_meta: dict[str, Any],
|
|
331
|
+
_skip_final_render: bool,
|
|
332
|
+
) -> tuple[str, dict[str, Any]]:
|
|
333
|
+
"""Handle usage events."""
|
|
334
|
+
stats_usage.update(ev.get("usage") or {})
|
|
335
|
+
return final_text, stats_usage
|
|
336
|
+
|
|
337
|
+
def _handle_final_response_event(
|
|
338
|
+
self,
|
|
339
|
+
ev: dict[str, Any],
|
|
340
|
+
renderer: RichStreamRenderer,
|
|
341
|
+
final_text: str,
|
|
342
|
+
stats_usage: dict[str, Any],
|
|
343
|
+
_meta: dict[str, Any],
|
|
344
|
+
skip_final_render: bool,
|
|
345
|
+
) -> tuple[str, dict[str, Any]]:
|
|
346
|
+
"""Handle final_response events."""
|
|
347
|
+
content = ev.get("content")
|
|
348
|
+
if content:
|
|
349
|
+
final_text = str(content)
|
|
350
|
+
if not skip_final_render:
|
|
351
|
+
renderer.on_event(ev)
|
|
352
|
+
return final_text, stats_usage
|
|
353
|
+
|
|
354
|
+
def _handle_run_info_event_wrapper(
|
|
355
|
+
self,
|
|
356
|
+
ev: dict[str, Any],
|
|
357
|
+
renderer: RichStreamRenderer,
|
|
358
|
+
final_text: str,
|
|
359
|
+
stats_usage: dict[str, Any],
|
|
360
|
+
meta: dict[str, Any],
|
|
361
|
+
_skip_final_render: bool,
|
|
362
|
+
) -> tuple[str, dict[str, Any]]:
|
|
363
|
+
"""Handle run_info events."""
|
|
364
|
+
self._handle_run_info_event(ev, meta, renderer)
|
|
365
|
+
return final_text, stats_usage
|
|
366
|
+
|
|
367
|
+
def _handle_render_only_event(
|
|
368
|
+
self,
|
|
369
|
+
ev: dict[str, Any],
|
|
370
|
+
renderer: RichStreamRenderer,
|
|
371
|
+
final_text: str,
|
|
372
|
+
stats_usage: dict[str, Any],
|
|
373
|
+
_meta: dict[str, Any],
|
|
374
|
+
_skip_final_render: bool,
|
|
375
|
+
) -> tuple[str, dict[str, Any]]:
|
|
376
|
+
"""Handle events that only need rendering (artifact, status_update)."""
|
|
377
|
+
renderer.on_event(ev)
|
|
378
|
+
return final_text, stats_usage
|
|
379
|
+
|
|
380
|
+
def _handle_content_event_async(
|
|
381
|
+
self,
|
|
382
|
+
ev: dict[str, Any],
|
|
383
|
+
renderer: RichStreamRenderer,
|
|
384
|
+
final_text: str,
|
|
385
|
+
stats_usage: dict[str, Any],
|
|
386
|
+
last_rendered_content: str | None,
|
|
387
|
+
) -> tuple[str, dict[str, Any]]:
|
|
388
|
+
"""Handle content events with deduplication."""
|
|
389
|
+
content = ev.get("content")
|
|
390
|
+
if content:
|
|
391
|
+
content_str = str(content)
|
|
392
|
+
if not content_str.startswith("Artifact received:"):
|
|
393
|
+
kind = self._get_event_kind(ev)
|
|
394
|
+
# Skip accumulating content for status updates and agent steps
|
|
395
|
+
if kind in ("agent_step", "status_update"):
|
|
396
|
+
renderer.on_event(ev)
|
|
397
|
+
return final_text, stats_usage
|
|
398
|
+
|
|
399
|
+
if self._is_token_event(ev):
|
|
400
|
+
renderer.on_event(ev)
|
|
401
|
+
final_text = f"{final_text}{content_str}"
|
|
402
|
+
else:
|
|
403
|
+
if content_str != last_rendered_content:
|
|
404
|
+
renderer.on_event(ev)
|
|
405
|
+
final_text = content_str
|
|
406
|
+
else:
|
|
407
|
+
renderer.on_event(ev)
|
|
408
|
+
return final_text, stats_usage
|
|
409
|
+
|
|
410
|
+
def _get_event_kind(self, ev: dict[str, Any]) -> str | None:
|
|
411
|
+
"""Extract normalized event kind from parsed event.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
ev: Parsed event dictionary.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Event kind string or None.
|
|
418
|
+
"""
|
|
419
|
+
metadata = ev.get("metadata") or {}
|
|
420
|
+
kind = metadata.get("kind")
|
|
421
|
+
if kind:
|
|
422
|
+
return str(kind)
|
|
423
|
+
event_type = ev.get("event_type")
|
|
424
|
+
return str(event_type) if event_type else None
|
|
425
|
+
|
|
426
|
+
def _is_token_event(self, ev: dict[str, Any]) -> bool:
|
|
427
|
+
"""Return True when the event represents token streaming output.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
ev: Parsed event dictionary.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
True when the event is a token chunk, otherwise False.
|
|
434
|
+
"""
|
|
435
|
+
metadata = ev.get("metadata") or {}
|
|
436
|
+
kind = metadata.get("kind")
|
|
437
|
+
return str(kind).lower() == "token"
|
|
438
|
+
|
|
439
|
+
def _is_final_event(self, ev: dict[str, Any]) -> bool:
|
|
440
|
+
"""Return True when the event marks stream termination.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
ev: Parsed event dictionary.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
True when the event is terminal, otherwise False.
|
|
447
|
+
"""
|
|
448
|
+
if ev.get("is_final") is True or ev.get("final") is True:
|
|
449
|
+
return True
|
|
450
|
+
kind = self._get_event_kind(ev)
|
|
451
|
+
return kind in _FINAL_EVENT_TYPES
|
|
452
|
+
|
|
453
|
+
def _extract_content_string(self, event: dict[str, Any]) -> str | None:
|
|
454
|
+
"""Extract textual content from a parsed event.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
event: Parsed event dictionary.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Content string or None.
|
|
461
|
+
"""
|
|
462
|
+
if not event:
|
|
463
|
+
return None
|
|
464
|
+
content = event.get("content")
|
|
465
|
+
if content:
|
|
466
|
+
return str(content)
|
|
467
|
+
return None
|
|
468
|
+
|
|
166
469
|
def _capture_request_id(
|
|
167
470
|
self,
|
|
168
471
|
stream_response: httpx.Response,
|
|
@@ -206,6 +509,7 @@ class AgentRunRenderingManager:
|
|
|
206
509
|
final_text: str,
|
|
207
510
|
stats_usage: dict[str, Any],
|
|
208
511
|
meta: dict[str, Any],
|
|
512
|
+
hitl_handler: RemoteHITLHandler | None = None,
|
|
209
513
|
) -> tuple[str, dict[str, Any]]:
|
|
210
514
|
"""Process a single streaming event.
|
|
211
515
|
|
|
@@ -215,6 +519,7 @@ class AgentRunRenderingManager:
|
|
|
215
519
|
final_text: Accumulated text so far.
|
|
216
520
|
stats_usage: Usage statistics dictionary.
|
|
217
521
|
meta: Metadata dictionary.
|
|
522
|
+
hitl_handler: Optional HITL handler for approval callbacks.
|
|
218
523
|
|
|
219
524
|
Returns:
|
|
220
525
|
Tuple of (updated_final_text, updated_stats_usage).
|
|
@@ -225,6 +530,17 @@ class AgentRunRenderingManager:
|
|
|
225
530
|
self._logger.debug("Non-JSON SSE fragment skipped")
|
|
226
531
|
return final_text, stats_usage
|
|
227
532
|
|
|
533
|
+
# Handle HITL event (non-blocking via thread)
|
|
534
|
+
if hitl_handler and self._is_hitl_pending_event(ev):
|
|
535
|
+
try:
|
|
536
|
+
hitl_handler.handle_hitl_event(ev)
|
|
537
|
+
except Exception as e:
|
|
538
|
+
# Log but don't crash stream
|
|
539
|
+
self._logger.error(
|
|
540
|
+
f"HITL handler error: {e}",
|
|
541
|
+
exc_info=True,
|
|
542
|
+
)
|
|
543
|
+
|
|
228
544
|
kind = (ev.get("metadata") or {}).get("kind")
|
|
229
545
|
renderer.on_event(ev)
|
|
230
546
|
|
|
@@ -239,7 +555,9 @@ class AgentRunRenderingManager:
|
|
|
239
555
|
if handled is not None:
|
|
240
556
|
return handled
|
|
241
557
|
|
|
242
|
-
|
|
558
|
+
# Only accumulate content for actual content events, not status updates or agent steps
|
|
559
|
+
# Status updates (agent_step) should be rendered but not accumulated in final_text
|
|
560
|
+
if ev.get("content") and kind not in ("agent_step", "status_update"):
|
|
243
561
|
final_text = self._handle_content_event(ev, final_text)
|
|
244
562
|
|
|
245
563
|
return final_text, stats_usage
|
|
@@ -285,9 +603,25 @@ class AgentRunRenderingManager:
|
|
|
285
603
|
"""
|
|
286
604
|
content = ev.get("content", "")
|
|
287
605
|
if not content.startswith("Artifact received:"):
|
|
606
|
+
if self._is_token_event(ev):
|
|
607
|
+
return f"{final_text}{content}"
|
|
288
608
|
return content
|
|
289
609
|
return final_text
|
|
290
610
|
|
|
611
|
+
@staticmethod
|
|
612
|
+
def _is_hitl_pending_event(event: dict[str, Any]) -> bool:
|
|
613
|
+
"""Check if event is a pending HITL approval request.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
event: Parsed event dictionary.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
True if event is a pending HITL request.
|
|
620
|
+
"""
|
|
621
|
+
metadata = event.get("metadata", {})
|
|
622
|
+
hitl_meta = metadata.get("hitl", {})
|
|
623
|
+
return hitl_meta.get("required") is True and hitl_meta.get("decision") == "pending"
|
|
624
|
+
|
|
291
625
|
def _handle_run_info_event(
|
|
292
626
|
self,
|
|
293
627
|
ev: dict[str, Any],
|
|
@@ -413,3 +747,33 @@ def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
|
|
|
413
747
|
if not specified in kwargs.
|
|
414
748
|
"""
|
|
415
749
|
return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def finalize_render_manager(
|
|
753
|
+
manager: AgentRunRenderingManager,
|
|
754
|
+
renderer: RichStreamRenderer,
|
|
755
|
+
final_text: str,
|
|
756
|
+
stats_usage: dict[str, Any],
|
|
757
|
+
started_monotonic: float | None,
|
|
758
|
+
finished_monotonic: float | None,
|
|
759
|
+
) -> str:
|
|
760
|
+
"""Helper to finalize renderer via manager and return final text.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
manager: The rendering manager instance.
|
|
764
|
+
renderer: Renderer to finalize.
|
|
765
|
+
final_text: Final text content.
|
|
766
|
+
stats_usage: Usage statistics dictionary.
|
|
767
|
+
started_monotonic: Start time (monotonic).
|
|
768
|
+
finished_monotonic: Finish time (monotonic).
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
Final text string.
|
|
772
|
+
"""
|
|
773
|
+
return manager.finalize_renderer(
|
|
774
|
+
renderer,
|
|
775
|
+
final_text,
|
|
776
|
+
stats_usage,
|
|
777
|
+
started_monotonic,
|
|
778
|
+
finished_monotonic,
|
|
779
|
+
)
|