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
|
@@ -7,31 +7,14 @@ Authors:
|
|
|
7
7
|
import json
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from typing import Any
|
|
10
|
+
from collections.abc import Callable, Iterable
|
|
10
11
|
|
|
11
12
|
from rich.console import Console
|
|
12
13
|
from rich.markdown import Markdown
|
|
13
14
|
|
|
14
15
|
from glaip_sdk.branding import PRIMARY, SUCCESS, WARNING
|
|
15
16
|
from glaip_sdk.rich_components import AIPPanel
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _coerce_datetime(value: Any) -> datetime | None:
|
|
19
|
-
"""Attempt to coerce an arbitrary value to an aware datetime."""
|
|
20
|
-
if value is None:
|
|
21
|
-
return None
|
|
22
|
-
|
|
23
|
-
if isinstance(value, datetime):
|
|
24
|
-
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
25
|
-
|
|
26
|
-
if isinstance(value, str):
|
|
27
|
-
try:
|
|
28
|
-
normalised = value.replace("Z", "+00:00")
|
|
29
|
-
dt = datetime.fromisoformat(normalised)
|
|
30
|
-
except ValueError:
|
|
31
|
-
return None
|
|
32
|
-
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
33
|
-
|
|
34
|
-
return None
|
|
17
|
+
from glaip_sdk.utils.datetime_helpers import coerce_datetime
|
|
35
18
|
|
|
36
19
|
|
|
37
20
|
def _parse_event_timestamp(event: dict[str, Any], received_ts: datetime | None = None) -> datetime | None:
|
|
@@ -40,7 +23,7 @@ def _parse_event_timestamp(event: dict[str, Any], received_ts: datetime | None =
|
|
|
40
23
|
return received_ts if received_ts.tzinfo else received_ts.replace(tzinfo=timezone.utc)
|
|
41
24
|
|
|
42
25
|
ts_value = event.get("timestamp") or (event.get("metadata") or {}).get("timestamp")
|
|
43
|
-
return
|
|
26
|
+
return coerce_datetime(ts_value)
|
|
44
27
|
|
|
45
28
|
|
|
46
29
|
def _format_timestamp_for_display(dt: datetime) -> str:
|
|
@@ -170,3 +153,26 @@ def render_debug_event(
|
|
|
170
153
|
except Exception as e:
|
|
171
154
|
# Debug helpers must not break streaming
|
|
172
155
|
print(f"Debug error: {e}") # Fallback debug output
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def render_debug_event_stream(
|
|
159
|
+
events: Iterable[dict[str, Any]],
|
|
160
|
+
console: Console,
|
|
161
|
+
*,
|
|
162
|
+
resolve_timestamp: Callable[[dict[str, Any]], datetime | None],
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Render a sequence of SSE events with baseline-aware timestamps."""
|
|
165
|
+
baseline: datetime | None = None
|
|
166
|
+
for event in events:
|
|
167
|
+
try:
|
|
168
|
+
received_ts = resolve_timestamp(event)
|
|
169
|
+
if baseline is None and received_ts is not None:
|
|
170
|
+
baseline = received_ts
|
|
171
|
+
render_debug_event(
|
|
172
|
+
event,
|
|
173
|
+
console,
|
|
174
|
+
received_ts=received_ts,
|
|
175
|
+
baseline_ts=baseline,
|
|
176
|
+
)
|
|
177
|
+
except Exception as exc: # pragma: no cover - debug stream resilience
|
|
178
|
+
console.print(f"[red]Debug stream error: {exc}[/red]")
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Renderer factory helpers for CLI, SDK, and slash sessions.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
from dataclasses import dataclass, is_dataclass, replace
|
|
11
|
+
from inspect import signature
|
|
12
|
+
from typing import Any
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from glaip_sdk.utils.rendering.renderer.base import RichStreamRenderer
|
|
18
|
+
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
19
|
+
from glaip_sdk.utils.rendering.state import TranscriptBuffer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class RendererFactoryOptions:
|
|
24
|
+
"""Shared options for renderer factories."""
|
|
25
|
+
|
|
26
|
+
console: Console | None = None
|
|
27
|
+
cfg_overrides: dict[str, Any] | None = None
|
|
28
|
+
verbose: bool | None = None
|
|
29
|
+
transcript_buffer: TranscriptBuffer | None = None
|
|
30
|
+
callbacks: dict[str, Any] | None = None
|
|
31
|
+
|
|
32
|
+
def build(self, factory: Callable[..., RichStreamRenderer]) -> RichStreamRenderer:
|
|
33
|
+
"""Instantiate a renderer using the provided factory and stored options."""
|
|
34
|
+
params = signature(factory).parameters
|
|
35
|
+
kwargs: dict[str, Any] = {}
|
|
36
|
+
if self.console is not None and "console" in params:
|
|
37
|
+
kwargs["console"] = self.console
|
|
38
|
+
if self.cfg_overrides is not None and "cfg_overrides" in params:
|
|
39
|
+
kwargs["cfg_overrides"] = self.cfg_overrides
|
|
40
|
+
if self.verbose is not None and "verbose" in params:
|
|
41
|
+
kwargs["verbose"] = self.verbose
|
|
42
|
+
if self.transcript_buffer is not None and "transcript_buffer" in params:
|
|
43
|
+
kwargs["transcript_buffer"] = self.transcript_buffer
|
|
44
|
+
if self.callbacks is not None and "callbacks" in params:
|
|
45
|
+
kwargs["callbacks"] = self.callbacks
|
|
46
|
+
return factory(**kwargs)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_config(base: RendererConfig, overrides: dict[str, Any] | None = None) -> RendererConfig:
|
|
50
|
+
cfg = replace(base) if is_dataclass(base) else base
|
|
51
|
+
if overrides:
|
|
52
|
+
for key, value in overrides.items():
|
|
53
|
+
if hasattr(cfg, key):
|
|
54
|
+
setattr(cfg, key, value)
|
|
55
|
+
return cfg
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def make_default_renderer(
|
|
59
|
+
*,
|
|
60
|
+
console: Console | None = None,
|
|
61
|
+
cfg_overrides: dict[str, Any] | None = None,
|
|
62
|
+
verbose: bool = False,
|
|
63
|
+
transcript_buffer: TranscriptBuffer | None = None,
|
|
64
|
+
callbacks: dict[str, Any] | None = None,
|
|
65
|
+
) -> RichStreamRenderer:
|
|
66
|
+
"""Create the default renderer used by SDK and CLI flows."""
|
|
67
|
+
cfg = _build_config(RendererConfig(), cfg_overrides)
|
|
68
|
+
return RichStreamRenderer(
|
|
69
|
+
console=console or Console(),
|
|
70
|
+
cfg=cfg,
|
|
71
|
+
verbose=verbose,
|
|
72
|
+
transcript_buffer=transcript_buffer,
|
|
73
|
+
callbacks=callbacks,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def make_verbose_renderer(
|
|
78
|
+
*,
|
|
79
|
+
console: Console | None = None,
|
|
80
|
+
cfg_overrides: dict[str, Any] | None = None,
|
|
81
|
+
transcript_buffer: TranscriptBuffer | None = None,
|
|
82
|
+
callbacks: dict[str, Any] | None = None,
|
|
83
|
+
) -> RichStreamRenderer:
|
|
84
|
+
"""Create a verbose renderer with snapshot appending disabled."""
|
|
85
|
+
verbose_cfg = RendererConfig(live=True, append_finished_snapshots=False)
|
|
86
|
+
cfg = _build_config(verbose_cfg, cfg_overrides)
|
|
87
|
+
return RichStreamRenderer(
|
|
88
|
+
console=console or Console(),
|
|
89
|
+
cfg=cfg,
|
|
90
|
+
verbose=True,
|
|
91
|
+
transcript_buffer=transcript_buffer,
|
|
92
|
+
callbacks=callbacks,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def make_minimal_renderer(
|
|
97
|
+
*,
|
|
98
|
+
console: Console | None = None,
|
|
99
|
+
cfg_overrides: dict[str, Any] | None = None,
|
|
100
|
+
transcript_buffer: TranscriptBuffer | None = None,
|
|
101
|
+
callbacks: dict[str, Any] | None = None,
|
|
102
|
+
) -> RichStreamRenderer:
|
|
103
|
+
"""Create a renderer that prints only essential output."""
|
|
104
|
+
minimal_cfg = RendererConfig(live=False, persist_live=False, render_thinking=False)
|
|
105
|
+
cfg = _build_config(minimal_cfg, cfg_overrides)
|
|
106
|
+
return RichStreamRenderer(
|
|
107
|
+
console=console or Console(),
|
|
108
|
+
cfg=cfg,
|
|
109
|
+
verbose=False,
|
|
110
|
+
transcript_buffer=transcript_buffer,
|
|
111
|
+
callbacks=callbacks,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def make_silent_renderer(
|
|
116
|
+
*,
|
|
117
|
+
console: Console | None = None,
|
|
118
|
+
cfg_overrides: dict[str, Any] | None = None,
|
|
119
|
+
transcript_buffer: TranscriptBuffer | None = None,
|
|
120
|
+
callbacks: dict[str, Any] | None = None,
|
|
121
|
+
) -> RichStreamRenderer:
|
|
122
|
+
"""Create a renderer that suppresses terminal output for background flows."""
|
|
123
|
+
cfg = _build_config(
|
|
124
|
+
RendererConfig(
|
|
125
|
+
live=False,
|
|
126
|
+
persist_live=False,
|
|
127
|
+
render_thinking=False,
|
|
128
|
+
),
|
|
129
|
+
cfg_overrides,
|
|
130
|
+
)
|
|
131
|
+
silent_console = console or Console(file=io.StringIO(), force_terminal=False)
|
|
132
|
+
return RichStreamRenderer(
|
|
133
|
+
console=silent_console,
|
|
134
|
+
cfg=cfg,
|
|
135
|
+
verbose=False,
|
|
136
|
+
transcript_buffer=transcript_buffer,
|
|
137
|
+
callbacks=callbacks,
|
|
138
|
+
)
|
|
@@ -129,21 +129,13 @@ class StreamProcessor:
|
|
|
129
129
|
metadata = event.get("metadata", {})
|
|
130
130
|
|
|
131
131
|
# Try primary extraction method
|
|
132
|
-
(
|
|
133
|
-
|
|
134
|
-
tool_args,
|
|
135
|
-
tool_out,
|
|
136
|
-
tool_calls_info,
|
|
137
|
-
) = self._extract_metadata_tool_calls(metadata)
|
|
132
|
+
tool_calls_result = self._extract_metadata_tool_calls(metadata)
|
|
133
|
+
tool_name, tool_args, tool_out, tool_calls_info = tool_calls_result
|
|
138
134
|
|
|
139
135
|
# Fallback to nested metadata.tool_info (newer schema)
|
|
140
136
|
if not tool_calls_info:
|
|
141
|
-
(
|
|
142
|
-
|
|
143
|
-
tool_args,
|
|
144
|
-
tool_out,
|
|
145
|
-
tool_calls_info,
|
|
146
|
-
) = self._extract_tool_calls_from_metadata(metadata)
|
|
137
|
+
fallback_result = self._extract_tool_calls_from_metadata(metadata)
|
|
138
|
+
tool_name, tool_args, tool_out, tool_calls_info = fallback_result
|
|
147
139
|
|
|
148
140
|
return tool_name, tool_args, tool_out, tool_calls_info
|
|
149
141
|
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Thinking scope controller used by the renderer.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.utils.rendering.formatting import is_step_finished
|
|
13
|
+
from glaip_sdk.utils.rendering.models import Step
|
|
14
|
+
from glaip_sdk.utils.rendering.state import ThinkingScopeState
|
|
15
|
+
from glaip_sdk.utils.rendering.steps import StepManager
|
|
16
|
+
from glaip_sdk.utils.rendering.timing import calculate_timeline_duration, coerce_server_time
|
|
17
|
+
|
|
18
|
+
FINISHED_STATUS_HINTS = {
|
|
19
|
+
"finished",
|
|
20
|
+
"success",
|
|
21
|
+
"succeeded",
|
|
22
|
+
"completed",
|
|
23
|
+
"failed",
|
|
24
|
+
"stopped",
|
|
25
|
+
"error",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ThinkingScopeController:
|
|
30
|
+
"""Encapsulates deterministic thinking bookkeeping for the renderer."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, steps: StepManager, *, step_server_start_times: dict[str, float]) -> None:
|
|
33
|
+
"""Initialize the thinking scope controller.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
steps: Step manager instance for tracking steps
|
|
37
|
+
step_server_start_times: Dictionary mapping step IDs to server start times
|
|
38
|
+
"""
|
|
39
|
+
self._steps = steps
|
|
40
|
+
self._step_server_start_times = step_server_start_times
|
|
41
|
+
self._scopes: dict[str, ThinkingScopeState] = {}
|
|
42
|
+
|
|
43
|
+
def update_timeline(self, step: Step | None, payload: dict[str, Any], *, enabled: bool) -> None:
|
|
44
|
+
"""Update thinking spans for a streamed step event."""
|
|
45
|
+
if not enabled or not step:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
now_monotonic = monotonic()
|
|
49
|
+
server_time = coerce_server_time(payload.get("time"))
|
|
50
|
+
status_hint = (payload.get("status") or "").lower()
|
|
51
|
+
|
|
52
|
+
if self._is_scope_anchor(step):
|
|
53
|
+
self._update_anchor_thinking(
|
|
54
|
+
step=step,
|
|
55
|
+
server_time=server_time,
|
|
56
|
+
status_hint=status_hint,
|
|
57
|
+
now_monotonic=now_monotonic,
|
|
58
|
+
)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
self._update_child_thinking(
|
|
62
|
+
step=step,
|
|
63
|
+
server_time=server_time,
|
|
64
|
+
status_hint=status_hint,
|
|
65
|
+
now_monotonic=now_monotonic,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def close_active_scopes(self, server_time: float | None) -> None:
|
|
69
|
+
"""Finish any in-flight thinking nodes during finalization."""
|
|
70
|
+
now = monotonic()
|
|
71
|
+
for scope in self._scopes.values():
|
|
72
|
+
if not scope.active_thinking_id:
|
|
73
|
+
continue
|
|
74
|
+
self._finish_scope_thinking(scope, server_time, now)
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# Internal helpers mirroring the previous renderer implementation.
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
def _update_anchor_thinking(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
step: Step,
|
|
83
|
+
server_time: float | None,
|
|
84
|
+
status_hint: str,
|
|
85
|
+
now_monotonic: float,
|
|
86
|
+
) -> None:
|
|
87
|
+
scope = self._get_or_create_scope(step)
|
|
88
|
+
if scope.anchor_started_at is None and server_time is not None:
|
|
89
|
+
scope.anchor_started_at = server_time
|
|
90
|
+
|
|
91
|
+
if not scope.closed and scope.active_thinking_id is None:
|
|
92
|
+
self._start_scope_thinking(
|
|
93
|
+
scope,
|
|
94
|
+
start_server_time=scope.anchor_started_at or server_time,
|
|
95
|
+
start_monotonic=now_monotonic,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
is_anchor_finished = status_hint in FINISHED_STATUS_HINTS or (not status_hint and is_step_finished(step))
|
|
99
|
+
if is_anchor_finished:
|
|
100
|
+
scope.anchor_finished_at = server_time or scope.anchor_finished_at
|
|
101
|
+
self._finish_scope_thinking(scope, server_time, now_monotonic)
|
|
102
|
+
scope.closed = True
|
|
103
|
+
|
|
104
|
+
parent_anchor_id = self._resolve_anchor_id(step)
|
|
105
|
+
if parent_anchor_id:
|
|
106
|
+
self._cascade_anchor_update(
|
|
107
|
+
parent_anchor_id=parent_anchor_id,
|
|
108
|
+
child_step=step,
|
|
109
|
+
server_time=server_time,
|
|
110
|
+
now_monotonic=now_monotonic,
|
|
111
|
+
is_finished=is_anchor_finished,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _cascade_anchor_update(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
parent_anchor_id: str,
|
|
118
|
+
child_step: Step,
|
|
119
|
+
server_time: float | None,
|
|
120
|
+
now_monotonic: float,
|
|
121
|
+
is_finished: bool,
|
|
122
|
+
) -> None:
|
|
123
|
+
parent_scope = self._scopes.get(parent_anchor_id)
|
|
124
|
+
if not parent_scope or parent_scope.closed:
|
|
125
|
+
return
|
|
126
|
+
if is_finished:
|
|
127
|
+
self._mark_child_finished(parent_scope, child_step.step_id, server_time, now_monotonic)
|
|
128
|
+
else:
|
|
129
|
+
self._mark_child_running(parent_scope, child_step, server_time, now_monotonic)
|
|
130
|
+
|
|
131
|
+
def _update_child_thinking(
|
|
132
|
+
self,
|
|
133
|
+
*,
|
|
134
|
+
step: Step,
|
|
135
|
+
server_time: float | None,
|
|
136
|
+
status_hint: str,
|
|
137
|
+
now_monotonic: float,
|
|
138
|
+
) -> None:
|
|
139
|
+
anchor_id = self._resolve_anchor_id(step)
|
|
140
|
+
if not anchor_id:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
scope = self._scopes.get(anchor_id)
|
|
144
|
+
if not scope or scope.closed or step.kind == "thinking":
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
is_finish_event = status_hint in FINISHED_STATUS_HINTS or (not status_hint and is_step_finished(step))
|
|
148
|
+
if is_finish_event:
|
|
149
|
+
self._mark_child_finished(scope, step.step_id, server_time, now_monotonic)
|
|
150
|
+
else:
|
|
151
|
+
self._mark_child_running(scope, step, server_time, now_monotonic)
|
|
152
|
+
|
|
153
|
+
def _resolve_anchor_id(self, step: Step) -> str | None:
|
|
154
|
+
parent_id = step.parent_id
|
|
155
|
+
while parent_id:
|
|
156
|
+
parent = self._steps.by_id.get(parent_id)
|
|
157
|
+
if not parent:
|
|
158
|
+
return None
|
|
159
|
+
if self._is_scope_anchor(parent):
|
|
160
|
+
return parent.step_id
|
|
161
|
+
parent_id = parent.parent_id
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def _get_or_create_scope(self, step: Step) -> ThinkingScopeState:
|
|
165
|
+
scope = self._scopes.get(step.step_id)
|
|
166
|
+
if scope:
|
|
167
|
+
if scope.task_id is None:
|
|
168
|
+
scope.task_id = step.task_id
|
|
169
|
+
if scope.context_id is None:
|
|
170
|
+
scope.context_id = step.context_id
|
|
171
|
+
return scope
|
|
172
|
+
scope = ThinkingScopeState(
|
|
173
|
+
anchor_id=step.step_id,
|
|
174
|
+
task_id=step.task_id,
|
|
175
|
+
context_id=step.context_id,
|
|
176
|
+
)
|
|
177
|
+
self._scopes[step.step_id] = scope
|
|
178
|
+
return scope
|
|
179
|
+
|
|
180
|
+
def _is_scope_anchor(self, step: Step) -> bool:
|
|
181
|
+
if step.kind in {"agent", "delegate"}:
|
|
182
|
+
return True
|
|
183
|
+
name = (step.name or "").lower()
|
|
184
|
+
return name.startswith(("delegate_to_", "delegate_", "delegate "))
|
|
185
|
+
|
|
186
|
+
def _start_scope_thinking(
|
|
187
|
+
self,
|
|
188
|
+
scope: ThinkingScopeState,
|
|
189
|
+
*,
|
|
190
|
+
start_server_time: float | None,
|
|
191
|
+
start_monotonic: float,
|
|
192
|
+
) -> None:
|
|
193
|
+
if scope.closed or scope.active_thinking_id or not scope.anchor_id:
|
|
194
|
+
return
|
|
195
|
+
step = self._steps.start_or_get(
|
|
196
|
+
task_id=scope.task_id,
|
|
197
|
+
context_id=scope.context_id,
|
|
198
|
+
kind="thinking",
|
|
199
|
+
name=f"agent_thinking_step::{scope.anchor_id}",
|
|
200
|
+
parent_id=scope.anchor_id,
|
|
201
|
+
args={"reason": "deterministic_timeline"},
|
|
202
|
+
)
|
|
203
|
+
step.display_label = "💭 Thinking…"
|
|
204
|
+
scope.active_thinking_id = step.step_id
|
|
205
|
+
scope.idle_started_at = start_server_time
|
|
206
|
+
scope.idle_started_monotonic = start_monotonic
|
|
207
|
+
|
|
208
|
+
def _finish_scope_thinking(
|
|
209
|
+
self,
|
|
210
|
+
scope: ThinkingScopeState,
|
|
211
|
+
end_server_time: float | None,
|
|
212
|
+
end_monotonic: float,
|
|
213
|
+
) -> None:
|
|
214
|
+
if not scope.active_thinking_id:
|
|
215
|
+
return
|
|
216
|
+
thinking_step = self._steps.by_id.get(scope.active_thinking_id)
|
|
217
|
+
if not thinking_step:
|
|
218
|
+
scope.active_thinking_id = None
|
|
219
|
+
scope.idle_started_at = None
|
|
220
|
+
scope.idle_started_monotonic = None
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
duration = calculate_timeline_duration(
|
|
224
|
+
scope.idle_started_at,
|
|
225
|
+
end_server_time,
|
|
226
|
+
scope.idle_started_monotonic,
|
|
227
|
+
end_monotonic,
|
|
228
|
+
)
|
|
229
|
+
thinking_step.display_label = thinking_step.display_label or "💭 Thinking…"
|
|
230
|
+
if duration is not None:
|
|
231
|
+
thinking_step.finish(duration, source="timeline")
|
|
232
|
+
else:
|
|
233
|
+
thinking_step.finish(None, source="timeline")
|
|
234
|
+
scope.active_thinking_id = None
|
|
235
|
+
scope.idle_started_at = None
|
|
236
|
+
scope.idle_started_monotonic = None
|
|
237
|
+
|
|
238
|
+
def _mark_child_running(
|
|
239
|
+
self,
|
|
240
|
+
scope: ThinkingScopeState,
|
|
241
|
+
step: Step,
|
|
242
|
+
server_time: float | None,
|
|
243
|
+
now_monotonic: float,
|
|
244
|
+
) -> None:
|
|
245
|
+
if step.step_id in scope.running_children:
|
|
246
|
+
return
|
|
247
|
+
scope.running_children.add(step.step_id)
|
|
248
|
+
if not scope.active_thinking_id:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
start_server = self._step_server_start_times.get(step.step_id)
|
|
252
|
+
if start_server is None:
|
|
253
|
+
start_server = server_time
|
|
254
|
+
self._finish_scope_thinking(scope, start_server, now_monotonic)
|
|
255
|
+
|
|
256
|
+
def _mark_child_finished(
|
|
257
|
+
self,
|
|
258
|
+
scope: ThinkingScopeState,
|
|
259
|
+
step_id: str,
|
|
260
|
+
server_time: float | None,
|
|
261
|
+
now_monotonic: float,
|
|
262
|
+
) -> None:
|
|
263
|
+
scope.running_children.discard(step_id)
|
|
264
|
+
if scope.active_thinking_id or scope.closed or scope.running_children:
|
|
265
|
+
return
|
|
266
|
+
self._start_scope_thinking(
|
|
267
|
+
scope,
|
|
268
|
+
start_server_time=server_time,
|
|
269
|
+
start_monotonic=now_monotonic,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
__all__ = ["ThinkingScopeController", "FINISHED_STATUS_HINTS"]
|