glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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 +1413 -0
- glaip_sdk/branding.py +126 -2
- glaip_sdk/cli/account_store.py +555 -0
- glaip_sdk/cli/auth.py +260 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -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 +104 -0
- glaip_sdk/cli/commands/configure.py +728 -113
- 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 +12 -8
- 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_original.py +756 -0
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +49 -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 +41 -20
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +340 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +12 -13
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +580 -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 +568 -0
- glaip_sdk/cli/slash/session.py +1105 -153
- glaip_sdk/cli/slash/tui/__init__.py +36 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/clipboard.py +195 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +80 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
- glaip_sdk/cli/slash/tui/terminal.py +407 -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 +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +66 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -463
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +227 -10
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +3 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +576 -44
- glaip_sdk/client/base.py +26 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -14
- glaip_sdk/client/mcps.py +165 -24
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +546 -92
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +206 -32
- glaip_sdk/config/constants.py +33 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +89 -0
- 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/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +136 -0
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +48 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- 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 +445 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +1055 -0
- glaip_sdk/runner/logging_config.py +77 -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 +116 -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 +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +488 -0
- glaip_sdk/utils/__init__.py +59 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +403 -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 +524 -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 +299 -1434
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- 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 -33
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- 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 +426 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +162 -0
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
- glaip_sdk-0.7.17.dist-info/RECORD +224 -0
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1369
- glaip_sdk/cli/commands/mcps.py +0 -1187
- glaip_sdk/cli/commands/tools.py +0 -584
- glaip_sdk/cli/utils.py +0 -1278
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.2.dist-info/RECORD +0 -82
- glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
|
@@ -7,11 +7,14 @@ Authors:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import io
|
|
11
10
|
import json
|
|
12
11
|
import logging
|
|
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
|
|
@@ -19,8 +22,18 @@ from rich.console import Console as _Console
|
|
|
19
22
|
from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
|
|
20
23
|
from glaip_sdk.utils.client_utils import iter_sse_events
|
|
21
24
|
from glaip_sdk.utils.rendering.models import RunStats
|
|
22
|
-
from glaip_sdk.utils.rendering.renderer import
|
|
23
|
-
|
|
25
|
+
from glaip_sdk.utils.rendering.renderer import (
|
|
26
|
+
RendererFactoryOptions,
|
|
27
|
+
RichStreamRenderer,
|
|
28
|
+
make_default_renderer,
|
|
29
|
+
make_minimal_renderer,
|
|
30
|
+
make_silent_renderer,
|
|
31
|
+
make_verbose_renderer,
|
|
32
|
+
)
|
|
33
|
+
from glaip_sdk.utils.rendering.state import TranscriptBuffer
|
|
34
|
+
|
|
35
|
+
NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
|
|
36
|
+
_FINAL_EVENT_TYPES = {"final_response", "error", "step_limit_exceeded"}
|
|
24
37
|
|
|
25
38
|
|
|
26
39
|
def _coerce_to_string(value: Any) -> str:
|
|
@@ -36,41 +49,6 @@ def _has_visible_text(value: Any) -> bool:
|
|
|
36
49
|
return isinstance(value, str) and bool(value.strip())
|
|
37
50
|
|
|
38
51
|
|
|
39
|
-
def _update_state_transcript(state: Any, text_value: str) -> bool:
|
|
40
|
-
"""Inject transcript text into renderer state if possible."""
|
|
41
|
-
if state is None:
|
|
42
|
-
return False
|
|
43
|
-
|
|
44
|
-
updated = False
|
|
45
|
-
|
|
46
|
-
if hasattr(state, "final_text") and not _has_visible_text(getattr(state, "final_text", "")):
|
|
47
|
-
try:
|
|
48
|
-
state.final_text = text_value
|
|
49
|
-
updated = True
|
|
50
|
-
except Exception:
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
buffer = getattr(state, "buffer", None)
|
|
54
|
-
if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
|
|
55
|
-
buffer.append(text_value)
|
|
56
|
-
updated = True
|
|
57
|
-
|
|
58
|
-
return updated
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
|
|
62
|
-
"""Populate the renderer (or its state) with the supplied text."""
|
|
63
|
-
state = getattr(renderer, "state", None)
|
|
64
|
-
if _update_state_transcript(state, text_value):
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
if hasattr(renderer, "final_text") and not _has_visible_text(getattr(renderer, "final_text", "")):
|
|
68
|
-
try:
|
|
69
|
-
renderer.final_text = text_value
|
|
70
|
-
except Exception:
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
|
|
74
52
|
class AgentRunRenderingManager:
|
|
75
53
|
"""Coordinate renderer creation and streaming event handling."""
|
|
76
54
|
|
|
@@ -81,6 +59,7 @@ class AgentRunRenderingManager:
|
|
|
81
59
|
logger: Optional logger instance, creates default if None
|
|
82
60
|
"""
|
|
83
61
|
self._logger = logger or logging.getLogger(__name__)
|
|
62
|
+
self._buffer_factory = TranscriptBuffer
|
|
84
63
|
|
|
85
64
|
# --------------------------------------------------------------------- #
|
|
86
65
|
# Renderer setup helpers
|
|
@@ -92,17 +71,38 @@ class AgentRunRenderingManager:
|
|
|
92
71
|
verbose: bool = False,
|
|
93
72
|
) -> RichStreamRenderer:
|
|
94
73
|
"""Create an appropriate renderer based on the supplied spec."""
|
|
74
|
+
transcript_buffer = self._buffer_factory()
|
|
75
|
+
base_options = RendererFactoryOptions(console=_Console(), transcript_buffer=transcript_buffer)
|
|
95
76
|
if isinstance(renderer_spec, RichStreamRenderer):
|
|
96
77
|
return renderer_spec
|
|
97
78
|
|
|
98
79
|
if isinstance(renderer_spec, str):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
lowered = renderer_spec.lower()
|
|
81
|
+
if lowered == "silent":
|
|
82
|
+
return self._attach_buffer(base_options.build(make_silent_renderer), transcript_buffer)
|
|
83
|
+
if lowered == "minimal":
|
|
84
|
+
return self._attach_buffer(base_options.build(make_minimal_renderer), transcript_buffer)
|
|
85
|
+
if lowered == "verbose":
|
|
86
|
+
return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
|
|
87
|
+
|
|
88
|
+
if verbose:
|
|
89
|
+
return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
|
|
90
|
+
|
|
91
|
+
default_options = RendererFactoryOptions(
|
|
92
|
+
console=_Console(),
|
|
93
|
+
transcript_buffer=transcript_buffer,
|
|
94
|
+
verbose=verbose,
|
|
95
|
+
)
|
|
96
|
+
return self._attach_buffer(default_options.build(make_default_renderer), transcript_buffer)
|
|
104
97
|
|
|
105
|
-
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _attach_buffer(renderer: RichStreamRenderer, buffer: TranscriptBuffer) -> RichStreamRenderer:
|
|
100
|
+
"""Attach a captured transcript buffer to a renderer for later inspection."""
|
|
101
|
+
try:
|
|
102
|
+
renderer._captured_transcript_buffer = buffer # type: ignore[attr-defined]
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return renderer
|
|
106
106
|
|
|
107
107
|
def build_initial_metadata(
|
|
108
108
|
self,
|
|
@@ -123,49 +123,6 @@ class AgentRunRenderingManager:
|
|
|
123
123
|
"""Notify renderer that streaming is starting."""
|
|
124
124
|
renderer.on_start(meta)
|
|
125
125
|
|
|
126
|
-
def _create_silent_renderer(self) -> RichStreamRenderer:
|
|
127
|
-
silent_config = RendererConfig(
|
|
128
|
-
live=False,
|
|
129
|
-
persist_live=False,
|
|
130
|
-
render_thinking=False,
|
|
131
|
-
)
|
|
132
|
-
return RichStreamRenderer(
|
|
133
|
-
console=_Console(file=io.StringIO(), force_terminal=False),
|
|
134
|
-
cfg=silent_config,
|
|
135
|
-
verbose=False,
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
def _create_minimal_renderer(self) -> RichStreamRenderer:
|
|
139
|
-
minimal_config = RendererConfig(
|
|
140
|
-
live=False,
|
|
141
|
-
persist_live=False,
|
|
142
|
-
render_thinking=False,
|
|
143
|
-
)
|
|
144
|
-
return RichStreamRenderer(
|
|
145
|
-
console=_Console(),
|
|
146
|
-
cfg=minimal_config,
|
|
147
|
-
verbose=False,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
def _create_verbose_renderer(self) -> RichStreamRenderer:
|
|
151
|
-
verbose_config = RendererConfig(
|
|
152
|
-
theme="dark",
|
|
153
|
-
style="debug",
|
|
154
|
-
live=False,
|
|
155
|
-
append_finished_snapshots=False,
|
|
156
|
-
)
|
|
157
|
-
return RichStreamRenderer(
|
|
158
|
-
console=_Console(),
|
|
159
|
-
cfg=verbose_config,
|
|
160
|
-
verbose=True,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
|
|
164
|
-
if verbose:
|
|
165
|
-
return self._create_verbose_renderer()
|
|
166
|
-
default_config = RendererConfig()
|
|
167
|
-
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
168
|
-
|
|
169
126
|
# --------------------------------------------------------------------- #
|
|
170
127
|
# Streaming event handling
|
|
171
128
|
# --------------------------------------------------------------------- #
|
|
@@ -176,6 +133,7 @@ class AgentRunRenderingManager:
|
|
|
176
133
|
timeout_seconds: float,
|
|
177
134
|
agent_name: str | None,
|
|
178
135
|
meta: dict[str, Any],
|
|
136
|
+
hitl_handler: RemoteHITLHandler | None = None,
|
|
179
137
|
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
180
138
|
"""Process streaming events and accumulate response."""
|
|
181
139
|
final_text = ""
|
|
@@ -199,10 +157,14 @@ class AgentRunRenderingManager:
|
|
|
199
157
|
final_text,
|
|
200
158
|
stats_usage,
|
|
201
159
|
meta,
|
|
160
|
+
hitl_handler=hitl_handler,
|
|
202
161
|
)
|
|
203
162
|
|
|
204
163
|
if controller and getattr(controller, "enabled", False):
|
|
205
164
|
controller.poll(renderer)
|
|
165
|
+
parsed_event = self._parse_event(event)
|
|
166
|
+
if parsed_event and self._is_final_event(parsed_event):
|
|
167
|
+
break
|
|
206
168
|
finally:
|
|
207
169
|
if controller and getattr(controller, "enabled", False):
|
|
208
170
|
controller.on_stream_complete()
|
|
@@ -210,18 +172,374 @@ class AgentRunRenderingManager:
|
|
|
210
172
|
finished_monotonic = monotonic()
|
|
211
173
|
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
212
174
|
|
|
175
|
+
async def _consume_event_stream(
|
|
176
|
+
self,
|
|
177
|
+
event_stream: AsyncIterable[dict[str, Any]],
|
|
178
|
+
renderer: RichStreamRenderer,
|
|
179
|
+
final_text: str,
|
|
180
|
+
stats_usage: dict[str, Any],
|
|
181
|
+
meta: dict[str, Any],
|
|
182
|
+
skip_final_render: bool,
|
|
183
|
+
last_rendered_content: str | None,
|
|
184
|
+
controller: Any | None,
|
|
185
|
+
) -> tuple[str, dict[str, Any], float | None]:
|
|
186
|
+
"""Consume event stream and update state.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
event_stream: Async iterable yielding SSE-like event dicts.
|
|
190
|
+
renderer: Renderer to use for displaying events.
|
|
191
|
+
final_text: Current accumulated final text.
|
|
192
|
+
stats_usage: Usage statistics dictionary.
|
|
193
|
+
meta: Metadata dictionary.
|
|
194
|
+
skip_final_render: If True, skip rendering final_response events.
|
|
195
|
+
last_rendered_content: Last rendered content to avoid duplicates.
|
|
196
|
+
controller: Controller instance.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Tuple of (final_text, stats_usage, started_monotonic).
|
|
200
|
+
"""
|
|
201
|
+
started_monotonic: float | None = None
|
|
202
|
+
|
|
203
|
+
async for event in event_stream:
|
|
204
|
+
if started_monotonic is None:
|
|
205
|
+
started_monotonic = monotonic()
|
|
206
|
+
|
|
207
|
+
parsed_event = self._parse_event(event)
|
|
208
|
+
if parsed_event is None:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
final_text, stats_usage = self._handle_parsed_event(
|
|
212
|
+
parsed_event,
|
|
213
|
+
renderer,
|
|
214
|
+
final_text,
|
|
215
|
+
stats_usage,
|
|
216
|
+
meta,
|
|
217
|
+
skip_final_render=skip_final_render,
|
|
218
|
+
last_rendered_content=last_rendered_content,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
content_str = self._extract_content_string(parsed_event)
|
|
222
|
+
if content_str:
|
|
223
|
+
last_rendered_content = content_str
|
|
224
|
+
|
|
225
|
+
if controller and getattr(controller, "enabled", False):
|
|
226
|
+
controller.poll(renderer)
|
|
227
|
+
if parsed_event and self._is_final_event(parsed_event):
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
return final_text, stats_usage, started_monotonic
|
|
231
|
+
|
|
232
|
+
async def async_process_stream_events(
|
|
233
|
+
self,
|
|
234
|
+
event_stream: AsyncIterable[dict[str, Any]],
|
|
235
|
+
renderer: RichStreamRenderer,
|
|
236
|
+
meta: dict[str, Any],
|
|
237
|
+
*,
|
|
238
|
+
skip_final_render: bool = True,
|
|
239
|
+
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
240
|
+
"""Process streaming events from an async event source.
|
|
241
|
+
|
|
242
|
+
This method provides unified stream processing for both remote (HTTP)
|
|
243
|
+
and local (LangGraph) agent execution, ensuring consistent behavior.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
event_stream: Async iterable yielding SSE-like event dicts.
|
|
247
|
+
Each event should have a "data" key with JSON string, or be
|
|
248
|
+
a pre-parsed dict with "content", "metadata", etc.
|
|
249
|
+
renderer: Renderer to use for displaying events.
|
|
250
|
+
meta: Metadata dictionary for renderer context.
|
|
251
|
+
skip_final_render: If True, skip rendering final_response events
|
|
252
|
+
(they are rendered separately via finalize_renderer).
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
|
|
256
|
+
"""
|
|
257
|
+
final_text = ""
|
|
258
|
+
stats_usage: dict[str, Any] = {}
|
|
259
|
+
started_monotonic: float | None = None
|
|
260
|
+
last_rendered_content: str | None = None
|
|
261
|
+
|
|
262
|
+
controller = getattr(renderer, "transcript_controller", None)
|
|
263
|
+
if controller and getattr(controller, "enabled", False):
|
|
264
|
+
controller.on_stream_start(renderer)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
final_text, stats_usage, started_monotonic = await self._consume_event_stream(
|
|
268
|
+
event_stream,
|
|
269
|
+
renderer,
|
|
270
|
+
final_text,
|
|
271
|
+
stats_usage,
|
|
272
|
+
meta,
|
|
273
|
+
skip_final_render,
|
|
274
|
+
last_rendered_content,
|
|
275
|
+
controller,
|
|
276
|
+
)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
err_msg = str(e)
|
|
279
|
+
reason = getattr(getattr(e, "result", None), "reason", None)
|
|
280
|
+
if reason:
|
|
281
|
+
final_text = f"⚠️ Guardrail violation: {reason}"
|
|
282
|
+
elif "⚠️ Guardrail violation" in err_msg or "Content blocked by guardrails" in err_msg:
|
|
283
|
+
final_text = err_msg
|
|
284
|
+
else:
|
|
285
|
+
raise e
|
|
286
|
+
finally:
|
|
287
|
+
if controller and getattr(controller, "enabled", False):
|
|
288
|
+
controller.on_stream_complete()
|
|
289
|
+
|
|
290
|
+
finished_monotonic = monotonic()
|
|
291
|
+
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
292
|
+
|
|
293
|
+
def _parse_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
294
|
+
"""Parse an SSE event dict into a usable format.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
event: Raw event dict, either with "data" key (SSE format) or
|
|
298
|
+
pre-parsed with "content", "metadata", etc.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Parsed event dict, or None if parsing fails.
|
|
302
|
+
"""
|
|
303
|
+
if "data" in event:
|
|
304
|
+
try:
|
|
305
|
+
return json.loads(event["data"])
|
|
306
|
+
except json.JSONDecodeError:
|
|
307
|
+
self._logger.debug("Non-JSON SSE fragment skipped")
|
|
308
|
+
return None
|
|
309
|
+
# Already parsed (e.g., from local runner)
|
|
310
|
+
return event if event else None
|
|
311
|
+
|
|
312
|
+
def _handle_parsed_event(
|
|
313
|
+
self,
|
|
314
|
+
ev: dict[str, Any],
|
|
315
|
+
renderer: RichStreamRenderer,
|
|
316
|
+
final_text: str,
|
|
317
|
+
stats_usage: dict[str, Any],
|
|
318
|
+
meta: dict[str, Any],
|
|
319
|
+
*,
|
|
320
|
+
skip_final_render: bool = True,
|
|
321
|
+
last_rendered_content: str | None = None,
|
|
322
|
+
) -> tuple[str, dict[str, Any]]:
|
|
323
|
+
"""Handle a parsed event and update accumulators.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
ev: Parsed event dictionary.
|
|
327
|
+
renderer: Renderer instance.
|
|
328
|
+
final_text: Current accumulated final text.
|
|
329
|
+
stats_usage: Usage statistics dictionary.
|
|
330
|
+
meta: Metadata dictionary.
|
|
331
|
+
skip_final_render: If True, skip rendering final_response events.
|
|
332
|
+
last_rendered_content: Last rendered content to avoid duplicates.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Tuple of (updated_final_text, updated_stats_usage).
|
|
336
|
+
"""
|
|
337
|
+
kind = self._get_event_kind(ev)
|
|
338
|
+
|
|
339
|
+
# Dispatch to specialized handlers based on event kind
|
|
340
|
+
handler = self._get_event_handler(kind, ev)
|
|
341
|
+
if handler:
|
|
342
|
+
return handler(ev, renderer, final_text, stats_usage, meta, skip_final_render)
|
|
343
|
+
|
|
344
|
+
# Default: handle content events
|
|
345
|
+
return self._handle_content_event_async(ev, renderer, final_text, stats_usage, last_rendered_content)
|
|
346
|
+
|
|
347
|
+
def _get_event_handler(
|
|
348
|
+
self,
|
|
349
|
+
kind: str | None,
|
|
350
|
+
ev: dict[str, Any],
|
|
351
|
+
) -> Callable[..., tuple[str, dict[str, Any]]] | None:
|
|
352
|
+
"""Get the appropriate handler for an event kind.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
kind: Event kind string.
|
|
356
|
+
ev: Event dictionary (for checking is_final flag).
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Handler function or None for default content handling.
|
|
360
|
+
"""
|
|
361
|
+
if kind == "usage":
|
|
362
|
+
return self._handle_usage_event
|
|
363
|
+
if kind == "final_response" or ev.get("is_final"):
|
|
364
|
+
return self._handle_final_response_event
|
|
365
|
+
if kind == "run_info":
|
|
366
|
+
return self._handle_run_info_event_wrapper
|
|
367
|
+
if kind in ("artifact", "status_update"):
|
|
368
|
+
return self._handle_render_only_event
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
def _handle_usage_event(
|
|
372
|
+
self,
|
|
373
|
+
ev: dict[str, Any],
|
|
374
|
+
_renderer: RichStreamRenderer,
|
|
375
|
+
final_text: str,
|
|
376
|
+
stats_usage: dict[str, Any],
|
|
377
|
+
_meta: dict[str, Any],
|
|
378
|
+
_skip_final_render: bool,
|
|
379
|
+
) -> tuple[str, dict[str, Any]]:
|
|
380
|
+
"""Handle usage events."""
|
|
381
|
+
stats_usage.update(ev.get("usage") or {})
|
|
382
|
+
return final_text, stats_usage
|
|
383
|
+
|
|
384
|
+
def _handle_final_response_event(
|
|
385
|
+
self,
|
|
386
|
+
ev: dict[str, Any],
|
|
387
|
+
renderer: RichStreamRenderer,
|
|
388
|
+
final_text: str,
|
|
389
|
+
stats_usage: dict[str, Any],
|
|
390
|
+
_meta: dict[str, Any],
|
|
391
|
+
skip_final_render: bool,
|
|
392
|
+
) -> tuple[str, dict[str, Any]]:
|
|
393
|
+
"""Handle final_response events."""
|
|
394
|
+
content = ev.get("content")
|
|
395
|
+
if content:
|
|
396
|
+
final_text = str(content)
|
|
397
|
+
if not skip_final_render:
|
|
398
|
+
renderer.on_event(ev)
|
|
399
|
+
return final_text, stats_usage
|
|
400
|
+
|
|
401
|
+
def _handle_run_info_event_wrapper(
|
|
402
|
+
self,
|
|
403
|
+
ev: dict[str, Any],
|
|
404
|
+
renderer: RichStreamRenderer,
|
|
405
|
+
final_text: str,
|
|
406
|
+
stats_usage: dict[str, Any],
|
|
407
|
+
meta: dict[str, Any],
|
|
408
|
+
_skip_final_render: bool,
|
|
409
|
+
) -> tuple[str, dict[str, Any]]:
|
|
410
|
+
"""Handle run_info events."""
|
|
411
|
+
self._handle_run_info_event(ev, meta, renderer)
|
|
412
|
+
return final_text, stats_usage
|
|
413
|
+
|
|
414
|
+
def _handle_render_only_event(
|
|
415
|
+
self,
|
|
416
|
+
ev: dict[str, Any],
|
|
417
|
+
renderer: RichStreamRenderer,
|
|
418
|
+
final_text: str,
|
|
419
|
+
stats_usage: dict[str, Any],
|
|
420
|
+
_meta: dict[str, Any],
|
|
421
|
+
_skip_final_render: bool,
|
|
422
|
+
) -> tuple[str, dict[str, Any]]:
|
|
423
|
+
"""Handle events that only need rendering (artifact, status_update)."""
|
|
424
|
+
renderer.on_event(ev)
|
|
425
|
+
return final_text, stats_usage
|
|
426
|
+
|
|
427
|
+
def _handle_content_event_async(
|
|
428
|
+
self,
|
|
429
|
+
ev: dict[str, Any],
|
|
430
|
+
renderer: RichStreamRenderer,
|
|
431
|
+
final_text: str,
|
|
432
|
+
stats_usage: dict[str, Any],
|
|
433
|
+
last_rendered_content: str | None,
|
|
434
|
+
) -> tuple[str, dict[str, Any]]:
|
|
435
|
+
"""Handle content events with deduplication."""
|
|
436
|
+
content = ev.get("content")
|
|
437
|
+
if content:
|
|
438
|
+
content_str = str(content)
|
|
439
|
+
if not content_str.startswith("Artifact received:"):
|
|
440
|
+
kind = self._get_event_kind(ev)
|
|
441
|
+
# Skip accumulating content for status updates and agent steps
|
|
442
|
+
if kind in ("agent_step", "status_update"):
|
|
443
|
+
renderer.on_event(ev)
|
|
444
|
+
return final_text, stats_usage
|
|
445
|
+
|
|
446
|
+
if self._is_token_event(ev):
|
|
447
|
+
renderer.on_event(ev)
|
|
448
|
+
final_text = f"{final_text}{content_str}"
|
|
449
|
+
else:
|
|
450
|
+
if content_str != last_rendered_content:
|
|
451
|
+
renderer.on_event(ev)
|
|
452
|
+
final_text = content_str
|
|
453
|
+
else:
|
|
454
|
+
renderer.on_event(ev)
|
|
455
|
+
return final_text, stats_usage
|
|
456
|
+
|
|
457
|
+
def _get_event_kind(self, ev: dict[str, Any]) -> str | None:
|
|
458
|
+
"""Extract normalized event kind from parsed event.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
ev: Parsed event dictionary.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Event kind string or None.
|
|
465
|
+
"""
|
|
466
|
+
metadata = ev.get("metadata") or {}
|
|
467
|
+
kind = metadata.get("kind")
|
|
468
|
+
if kind:
|
|
469
|
+
return str(kind)
|
|
470
|
+
event_type = ev.get("event_type")
|
|
471
|
+
return str(event_type) if event_type else None
|
|
472
|
+
|
|
473
|
+
def _is_token_event(self, ev: dict[str, Any]) -> bool:
|
|
474
|
+
"""Return True when the event represents token streaming output.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
ev: Parsed event dictionary.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True when the event is a token chunk, otherwise False.
|
|
481
|
+
"""
|
|
482
|
+
metadata = ev.get("metadata") or {}
|
|
483
|
+
kind = metadata.get("kind")
|
|
484
|
+
return str(kind).lower() == "token"
|
|
485
|
+
|
|
486
|
+
def _is_final_event(self, ev: dict[str, Any]) -> bool:
|
|
487
|
+
"""Return True when the event marks stream termination.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
ev: Parsed event dictionary.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
True when the event is terminal, otherwise False.
|
|
494
|
+
"""
|
|
495
|
+
if ev.get("is_final") is True or ev.get("final") is True:
|
|
496
|
+
return True
|
|
497
|
+
kind = self._get_event_kind(ev)
|
|
498
|
+
return kind in _FINAL_EVENT_TYPES
|
|
499
|
+
|
|
500
|
+
def _extract_content_string(self, event: dict[str, Any]) -> str | None:
|
|
501
|
+
"""Extract textual content from a parsed event.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
event: Parsed event dictionary.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Content string or None.
|
|
508
|
+
"""
|
|
509
|
+
if not event:
|
|
510
|
+
return None
|
|
511
|
+
content = event.get("content")
|
|
512
|
+
if content:
|
|
513
|
+
return str(content)
|
|
514
|
+
return None
|
|
515
|
+
|
|
213
516
|
def _capture_request_id(
|
|
214
517
|
self,
|
|
215
518
|
stream_response: httpx.Response,
|
|
216
519
|
meta: dict[str, Any],
|
|
217
520
|
renderer: RichStreamRenderer,
|
|
218
521
|
) -> None:
|
|
522
|
+
"""Capture request ID from response headers and update metadata.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
stream_response: HTTP response stream.
|
|
526
|
+
meta: Metadata dictionary to update.
|
|
527
|
+
renderer: Renderer instance.
|
|
528
|
+
"""
|
|
219
529
|
req_id = stream_response.headers.get("x-request-id") or stream_response.headers.get("x-run-id")
|
|
220
530
|
if req_id:
|
|
221
531
|
meta["run_id"] = req_id
|
|
222
532
|
renderer.on_start(meta)
|
|
223
533
|
|
|
224
534
|
def _maybe_start_timer(self, event: dict[str, Any]) -> float | None:
|
|
535
|
+
"""Start timing if this is a content-bearing event.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
event: Event dictionary.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Monotonic time if timer should start, None otherwise.
|
|
542
|
+
"""
|
|
225
543
|
try:
|
|
226
544
|
ev = json.loads(event["data"])
|
|
227
545
|
except json.JSONDecodeError:
|
|
@@ -238,13 +556,38 @@ class AgentRunRenderingManager:
|
|
|
238
556
|
final_text: str,
|
|
239
557
|
stats_usage: dict[str, Any],
|
|
240
558
|
meta: dict[str, Any],
|
|
559
|
+
hitl_handler: RemoteHITLHandler | None = None,
|
|
241
560
|
) -> tuple[str, dict[str, Any]]:
|
|
561
|
+
"""Process a single streaming event.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
event: Event dictionary.
|
|
565
|
+
renderer: Renderer instance.
|
|
566
|
+
final_text: Accumulated text so far.
|
|
567
|
+
stats_usage: Usage statistics dictionary.
|
|
568
|
+
meta: Metadata dictionary.
|
|
569
|
+
hitl_handler: Optional HITL handler for approval callbacks.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Tuple of (updated_final_text, updated_stats_usage).
|
|
573
|
+
"""
|
|
242
574
|
try:
|
|
243
575
|
ev = json.loads(event["data"])
|
|
244
576
|
except json.JSONDecodeError:
|
|
245
577
|
self._logger.debug("Non-JSON SSE fragment skipped")
|
|
246
578
|
return final_text, stats_usage
|
|
247
579
|
|
|
580
|
+
# Handle HITL event (non-blocking via thread)
|
|
581
|
+
if hitl_handler and self._is_hitl_pending_event(ev):
|
|
582
|
+
try:
|
|
583
|
+
hitl_handler.handle_hitl_event(ev)
|
|
584
|
+
except Exception as e:
|
|
585
|
+
# Log but don't crash stream
|
|
586
|
+
self._logger.error(
|
|
587
|
+
f"HITL handler error: {e}",
|
|
588
|
+
exc_info=True,
|
|
589
|
+
)
|
|
590
|
+
|
|
248
591
|
kind = (ev.get("metadata") or {}).get("kind")
|
|
249
592
|
renderer.on_event(ev)
|
|
250
593
|
|
|
@@ -259,7 +602,9 @@ class AgentRunRenderingManager:
|
|
|
259
602
|
if handled is not None:
|
|
260
603
|
return handled
|
|
261
604
|
|
|
262
|
-
|
|
605
|
+
# Only accumulate content for actual content events, not status updates or agent steps
|
|
606
|
+
# Status updates (agent_step) should be rendered but not accumulated in final_text
|
|
607
|
+
if ev.get("content") and kind not in ("agent_step", "status_update"):
|
|
263
608
|
final_text = self._handle_content_event(ev, final_text)
|
|
264
609
|
|
|
265
610
|
return final_text, stats_usage
|
|
@@ -294,17 +639,49 @@ class AgentRunRenderingManager:
|
|
|
294
639
|
return None
|
|
295
640
|
|
|
296
641
|
def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
|
|
642
|
+
"""Handle a content event and update final text.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
ev: Event dictionary.
|
|
646
|
+
final_text: Current accumulated text.
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Updated final text.
|
|
650
|
+
"""
|
|
297
651
|
content = ev.get("content", "")
|
|
298
652
|
if not content.startswith("Artifact received:"):
|
|
653
|
+
if self._is_token_event(ev):
|
|
654
|
+
return f"{final_text}{content}"
|
|
299
655
|
return content
|
|
300
656
|
return final_text
|
|
301
657
|
|
|
658
|
+
@staticmethod
|
|
659
|
+
def _is_hitl_pending_event(event: dict[str, Any]) -> bool:
|
|
660
|
+
"""Check if event is a pending HITL approval request.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
event: Parsed event dictionary.
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
True if event is a pending HITL request.
|
|
667
|
+
"""
|
|
668
|
+
metadata = event.get("metadata", {})
|
|
669
|
+
hitl_meta = metadata.get("hitl", {})
|
|
670
|
+
return hitl_meta.get("required") is True and hitl_meta.get("decision") == "pending"
|
|
671
|
+
|
|
302
672
|
def _handle_run_info_event(
|
|
303
673
|
self,
|
|
304
674
|
ev: dict[str, Any],
|
|
305
675
|
meta: dict[str, Any],
|
|
306
676
|
renderer: RichStreamRenderer,
|
|
307
677
|
) -> None:
|
|
678
|
+
"""Handle a run_info event and update metadata.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
ev: Event dictionary.
|
|
682
|
+
meta: Metadata dictionary to update.
|
|
683
|
+
renderer: Renderer instance.
|
|
684
|
+
"""
|
|
308
685
|
if ev.get("model"):
|
|
309
686
|
meta["model"] = ev["model"]
|
|
310
687
|
renderer.on_start(meta)
|
|
@@ -318,7 +695,52 @@ class AgentRunRenderingManager:
|
|
|
318
695
|
return
|
|
319
696
|
|
|
320
697
|
text_value = _coerce_to_string(text)
|
|
321
|
-
|
|
698
|
+
state = getattr(renderer, "state", None)
|
|
699
|
+
if state is None:
|
|
700
|
+
self._ensure_renderer_text(renderer, text_value)
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
self._ensure_state_final_text(state, text_value)
|
|
704
|
+
self._ensure_state_buffer(state, text_value)
|
|
705
|
+
|
|
706
|
+
def _ensure_renderer_text(self, renderer: RichStreamRenderer, text_value: str) -> None:
|
|
707
|
+
"""Best-effort assignment for renderer.final_text."""
|
|
708
|
+
if not hasattr(renderer, "final_text"):
|
|
709
|
+
return
|
|
710
|
+
current_text = getattr(renderer, "final_text", "")
|
|
711
|
+
if _has_visible_text(current_text):
|
|
712
|
+
return
|
|
713
|
+
self._safe_set_attr(renderer, "final_text", text_value)
|
|
714
|
+
|
|
715
|
+
def _ensure_state_final_text(self, state: Any, text_value: str) -> None:
|
|
716
|
+
"""Best-effort assignment for renderer.state.final_text."""
|
|
717
|
+
current_text = getattr(state, "final_text", "")
|
|
718
|
+
if _has_visible_text(current_text):
|
|
719
|
+
return
|
|
720
|
+
self._safe_set_attr(state, "final_text", text_value)
|
|
721
|
+
|
|
722
|
+
def _ensure_state_buffer(self, state: Any, text_value: str) -> None:
|
|
723
|
+
"""Append fallback text to the state buffer when available."""
|
|
724
|
+
buffer = getattr(state, "buffer", None)
|
|
725
|
+
if not hasattr(buffer, "append"):
|
|
726
|
+
return
|
|
727
|
+
self._safe_append(buffer.append, text_value)
|
|
728
|
+
|
|
729
|
+
@staticmethod
|
|
730
|
+
def _safe_set_attr(target: Any, attr: str, value: str) -> None:
|
|
731
|
+
"""Assign attribute while masking renderer-specific failures."""
|
|
732
|
+
try:
|
|
733
|
+
setattr(target, attr, value)
|
|
734
|
+
except Exception:
|
|
735
|
+
pass
|
|
736
|
+
|
|
737
|
+
@staticmethod
|
|
738
|
+
def _safe_append(appender: Callable[[str], Any], value: str) -> None:
|
|
739
|
+
"""Invoke append-like functions without leaking renderer errors."""
|
|
740
|
+
try:
|
|
741
|
+
appender(value)
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
322
744
|
|
|
323
745
|
# --------------------------------------------------------------------- #
|
|
324
746
|
# Finalisation helpers
|
|
@@ -345,7 +767,9 @@ class AgentRunRenderingManager:
|
|
|
345
767
|
elif hasattr(renderer, "buffer"):
|
|
346
768
|
buffer_values = renderer.buffer
|
|
347
769
|
|
|
348
|
-
if buffer_values
|
|
770
|
+
if isinstance(buffer_values, TranscriptBuffer):
|
|
771
|
+
rendered_text = buffer_values.render()
|
|
772
|
+
elif buffer_values is not None:
|
|
349
773
|
try:
|
|
350
774
|
rendered_text = "".join(buffer_values)
|
|
351
775
|
except TypeError:
|
|
@@ -356,7 +780,7 @@ class AgentRunRenderingManager:
|
|
|
356
780
|
self._ensure_renderer_final_content(renderer, fallback_text)
|
|
357
781
|
|
|
358
782
|
renderer.on_complete(st)
|
|
359
|
-
return final_text or rendered_text or
|
|
783
|
+
return final_text or rendered_text or NO_AGENT_RESPONSE_FALLBACK
|
|
360
784
|
|
|
361
785
|
|
|
362
786
|
def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
|
|
@@ -370,3 +794,33 @@ def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
|
|
|
370
794
|
if not specified in kwargs.
|
|
371
795
|
"""
|
|
372
796
|
return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def finalize_render_manager(
|
|
800
|
+
manager: AgentRunRenderingManager,
|
|
801
|
+
renderer: RichStreamRenderer,
|
|
802
|
+
final_text: str,
|
|
803
|
+
stats_usage: dict[str, Any],
|
|
804
|
+
started_monotonic: float | None,
|
|
805
|
+
finished_monotonic: float | None,
|
|
806
|
+
) -> str:
|
|
807
|
+
"""Helper to finalize renderer via manager and return final text.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
manager: The rendering manager instance.
|
|
811
|
+
renderer: Renderer to finalize.
|
|
812
|
+
final_text: Final text content.
|
|
813
|
+
stats_usage: Usage statistics dictionary.
|
|
814
|
+
started_monotonic: Start time (monotonic).
|
|
815
|
+
finished_monotonic: Finish time (monotonic).
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
Final text string.
|
|
819
|
+
"""
|
|
820
|
+
return manager.finalize_renderer(
|
|
821
|
+
renderer,
|
|
822
|
+
final_text,
|
|
823
|
+
stats_usage,
|
|
824
|
+
started_monotonic,
|
|
825
|
+
finished_monotonic,
|
|
826
|
+
)
|