glaip-sdk 0.0.20__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 +44 -4
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1250 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +271 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- 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 +104 -0
- glaip_sdk/cli/commands/configure.py +734 -143
- 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 +14 -12
- 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 +164 -23
- glaip_sdk/cli/config.py +49 -7
- 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 +45 -32
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +344 -167
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +15 -22
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +5 -10
- glaip_sdk/cli/rich_helpers.py +1 -3
- 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 +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +827 -232
- glaip_sdk/cli/slash/tui/__init__.py +34 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- 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/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -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/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -329
- glaip_sdk/cli/update_notifier.py +385 -24
- glaip_sdk/cli/validators.py +16 -18
- 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 +370 -100
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -10
- glaip_sdk/client/mcps.py +166 -27
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +583 -79
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +214 -56
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- 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/icons.py +9 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +107 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -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 -3
- 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 +112 -0
- glaip_sdk/runner/langgraph.py +872 -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 +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 +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 +468 -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 +4 -14
- glaip_sdk/utils/bundler.py +403 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- 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 +38 -23
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +18 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +534 -882
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -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/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
- 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 +29 -26
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- 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 +20 -28
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.0.20.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 -1412
- glaip_sdk/cli/commands/mcps.py +0 -1225
- glaip_sdk/cli/commands/tools.py +0 -597
- glaip_sdk/cli/utils.py +0 -1330
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Presentation helpers for rendering steps.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.icons import ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
|
|
13
|
+
from glaip_sdk.utils.rendering.formatting import glyph_for_status, normalise_display_label, pretty_args
|
|
14
|
+
from glaip_sdk.utils.rendering.models import Step
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING: # pragma: no cover - typing helpers only
|
|
17
|
+
from glaip_sdk.utils.rendering.layout.transcript import TranscriptGlyphs
|
|
18
|
+
|
|
19
|
+
UNKNOWN_STEP_DETAIL = "Unknown step detail"
|
|
20
|
+
STATUS_ICON_STYLES = {
|
|
21
|
+
"success": "green",
|
|
22
|
+
"failed": "red",
|
|
23
|
+
"warning": "yellow",
|
|
24
|
+
}
|
|
25
|
+
CONNECTOR_VERTICAL = "│ "
|
|
26
|
+
CONNECTOR_EMPTY = " "
|
|
27
|
+
CONNECTOR_BRANCH = "├─ "
|
|
28
|
+
CONNECTOR_LAST = "└─ "
|
|
29
|
+
ROOT_MARKER = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class StepPresentation:
|
|
34
|
+
"""Lightweight view model for formatted steps."""
|
|
35
|
+
|
|
36
|
+
step_id: str
|
|
37
|
+
title: str
|
|
38
|
+
glyph: str | None
|
|
39
|
+
status_style: str | None
|
|
40
|
+
args_text: str | None = None
|
|
41
|
+
failure_reason: str | None = None
|
|
42
|
+
duration_ms: int | None = None
|
|
43
|
+
status_text: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def humanize_tool_name(raw_name: str | None) -> str:
|
|
47
|
+
"""Return a user-facing name for a tool or agent identifier."""
|
|
48
|
+
if not raw_name:
|
|
49
|
+
return UNKNOWN_STEP_DETAIL
|
|
50
|
+
name = raw_name
|
|
51
|
+
if name.startswith("delegate_to_"):
|
|
52
|
+
name = name.removeprefix("delegate_to_")
|
|
53
|
+
elif name.startswith("delegate_"):
|
|
54
|
+
name = name.removeprefix("delegate_")
|
|
55
|
+
cleaned = name.replace("_", " ").replace("-", " ").strip()
|
|
56
|
+
if not cleaned:
|
|
57
|
+
return UNKNOWN_STEP_DETAIL
|
|
58
|
+
lowered = cleaned.lower()
|
|
59
|
+
return lowered[0].upper() + lowered[1:] if lowered else UNKNOWN_STEP_DETAIL
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def step_icon_for_kind(step_kind: str) -> str:
|
|
63
|
+
"""Return the icon prefix for a step kind."""
|
|
64
|
+
if step_kind == "agent":
|
|
65
|
+
return ICON_AGENT_STEP
|
|
66
|
+
if step_kind == "delegate":
|
|
67
|
+
return ICON_DELEGATE
|
|
68
|
+
if step_kind == "thinking":
|
|
69
|
+
return "💭"
|
|
70
|
+
return ICON_TOOL_STEP
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_label_body(step_kind: str, tool_name: str | None, metadata: dict[str, Any]) -> str:
|
|
74
|
+
"""Resolve the textual body for a step label."""
|
|
75
|
+
if step_kind == "thinking":
|
|
76
|
+
thinking_text = metadata.get("thinking_and_activity_info")
|
|
77
|
+
if isinstance(thinking_text, str) and thinking_text.strip():
|
|
78
|
+
return thinking_text.strip()
|
|
79
|
+
return "Thinking…"
|
|
80
|
+
|
|
81
|
+
if step_kind == "delegate":
|
|
82
|
+
return humanize_tool_name(tool_name)
|
|
83
|
+
|
|
84
|
+
if step_kind == "agent":
|
|
85
|
+
agent_name = metadata.get("agent_name")
|
|
86
|
+
if isinstance(agent_name, str) and agent_name.strip():
|
|
87
|
+
return agent_name.strip()
|
|
88
|
+
|
|
89
|
+
return humanize_tool_name(tool_name)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def compose_display_label(
|
|
93
|
+
step_kind: str,
|
|
94
|
+
tool_name: str | None,
|
|
95
|
+
args: dict[str, Any],
|
|
96
|
+
metadata: dict[str, Any],
|
|
97
|
+
) -> str:
|
|
98
|
+
"""Compose the display label for a step using tool metadata."""
|
|
99
|
+
icon = step_icon_for_kind(step_kind)
|
|
100
|
+
body = resolve_label_body(step_kind, tool_name, metadata)
|
|
101
|
+
label = f"{icon} {body}".strip()
|
|
102
|
+
if isinstance(args, dict) and args:
|
|
103
|
+
label = f"{label} —"
|
|
104
|
+
return label or UNKNOWN_STEP_DETAIL
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def status_icon_for_step(step: Step) -> str:
|
|
108
|
+
"""Return the canonical status icon key for a step."""
|
|
109
|
+
if step.status == "failed":
|
|
110
|
+
return "failed"
|
|
111
|
+
if step.branch_failed:
|
|
112
|
+
return "warning"
|
|
113
|
+
if step.status == "finished":
|
|
114
|
+
return "success"
|
|
115
|
+
if step.status == "stopped":
|
|
116
|
+
return "warning"
|
|
117
|
+
return "spinner"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def format_step_label(step: Step) -> str:
|
|
121
|
+
"""Return the normalized display label for a step."""
|
|
122
|
+
label = normalise_display_label(getattr(step, "display_label", None))
|
|
123
|
+
if label and label != UNKNOWN_STEP_DETAIL:
|
|
124
|
+
return label
|
|
125
|
+
metadata = getattr(step, "metadata", {}) or {}
|
|
126
|
+
computed = compose_display_label(step.kind, getattr(step, "name", None), getattr(step, "args", {}), metadata)
|
|
127
|
+
return normalise_display_label(computed)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def format_tool_args(step: Step, max_len: int = 160) -> str | None:
|
|
131
|
+
"""Return a pretty-printed args summary for a step."""
|
|
132
|
+
if not step.args:
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
return pretty_args(step.args, max_len=max_len)
|
|
136
|
+
except Exception:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_step(
|
|
141
|
+
step: Step,
|
|
142
|
+
*,
|
|
143
|
+
glyphs: TranscriptGlyphs | None = None,
|
|
144
|
+
label: str | None = None,
|
|
145
|
+
) -> StepPresentation:
|
|
146
|
+
"""Return a StepPresentation for downstream transcript rendering."""
|
|
147
|
+
del glyphs # Reserved for future glyph customisation hooks
|
|
148
|
+
if label:
|
|
149
|
+
resolved_label = normalise_display_label(label)
|
|
150
|
+
else:
|
|
151
|
+
resolved_label = format_step_label(step)
|
|
152
|
+
glyph_key = status_icon_for_step(step)
|
|
153
|
+
glyph = glyph_for_status(glyph_key)
|
|
154
|
+
style = STATUS_ICON_STYLES.get(glyph_key)
|
|
155
|
+
failure_reason = (step.failure_reason or "").strip() or None
|
|
156
|
+
return StepPresentation(
|
|
157
|
+
step_id=step.step_id,
|
|
158
|
+
title=resolved_label,
|
|
159
|
+
glyph=glyph,
|
|
160
|
+
status_style=style,
|
|
161
|
+
args_text=format_tool_args(step),
|
|
162
|
+
failure_reason=failure_reason,
|
|
163
|
+
duration_ms=getattr(step, "duration_ms", None),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_connector_prefix(branch_state: tuple[bool, ...]) -> str:
|
|
168
|
+
"""Build connector prefix for a tree line based on ancestry state."""
|
|
169
|
+
if not branch_state:
|
|
170
|
+
return ROOT_MARKER
|
|
171
|
+
|
|
172
|
+
parts: list[str] = []
|
|
173
|
+
for ancestor_is_last in branch_state[:-1]:
|
|
174
|
+
parts.append(CONNECTOR_EMPTY if ancestor_is_last else CONNECTOR_VERTICAL)
|
|
175
|
+
parts.append(CONNECTOR_LAST if branch_state[-1] else CONNECTOR_BRANCH)
|
|
176
|
+
return "".join(parts)
|
|
@@ -9,9 +9,15 @@ from __future__ import annotations
|
|
|
9
9
|
from collections.abc import Iterator
|
|
10
10
|
|
|
11
11
|
from glaip_sdk.utils.rendering.models import Step
|
|
12
|
+
from glaip_sdk.utils.rendering.step_tree_state import StepTreeState
|
|
13
|
+
from glaip_sdk.utils.rendering.steps.event_processor import StepEventMixin
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class
|
|
16
|
+
class StepManagerError(Exception):
|
|
17
|
+
"""Raised when invalid operations are attempted on the step tree."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StepManager(StepEventMixin):
|
|
15
21
|
"""Manages the lifecycle and organization of execution steps.
|
|
16
22
|
|
|
17
23
|
Tracks step creation, parent-child relationships, and execution state
|
|
@@ -24,13 +30,22 @@ class StepManager:
|
|
|
24
30
|
Args:
|
|
25
31
|
max_steps: Maximum number of steps to retain before pruning
|
|
26
32
|
"""
|
|
27
|
-
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
33
|
+
normalised_max = int(max_steps) if isinstance(max_steps, (int, float)) else 0
|
|
34
|
+
self.state = StepTreeState(max_steps=normalised_max)
|
|
35
|
+
self.by_id: dict[str, Step] = self.state.step_index
|
|
30
36
|
self.key_index: dict[tuple, str] = {}
|
|
31
37
|
self.slot_counter: dict[tuple, int] = {}
|
|
32
|
-
self.max_steps =
|
|
38
|
+
self.max_steps = normalised_max
|
|
33
39
|
self._last_running: dict[tuple, str] = {}
|
|
40
|
+
self._step_aliases: dict[str, str] = {}
|
|
41
|
+
self.root_agent_id: str | None = None
|
|
42
|
+
self._scope_anchors: dict[str, list[str]] = {}
|
|
43
|
+
self._step_scope_map: dict[str, str] = {}
|
|
44
|
+
|
|
45
|
+
def set_root_agent(self, agent_id: str | None) -> None:
|
|
46
|
+
"""Record the root agent identifier for scope-aware parenting."""
|
|
47
|
+
if isinstance(agent_id, str) and agent_id.strip():
|
|
48
|
+
self.root_agent_id = agent_id.strip()
|
|
34
49
|
|
|
35
50
|
def _alloc_slot(
|
|
36
51
|
self,
|
|
@@ -86,9 +101,7 @@ class StepManager:
|
|
|
86
101
|
Returns:
|
|
87
102
|
The Step instance (new or existing)
|
|
88
103
|
"""
|
|
89
|
-
existing = self.find_running(
|
|
90
|
-
task_id=task_id, context_id=context_id, kind=kind, name=name
|
|
91
|
-
)
|
|
104
|
+
existing = self.find_running(task_id=task_id, context_id=context_id, kind=kind, name=name)
|
|
92
105
|
if existing:
|
|
93
106
|
if args and existing.args != args:
|
|
94
107
|
existing.args = args
|
|
@@ -111,6 +124,7 @@ class StepManager:
|
|
|
111
124
|
else:
|
|
112
125
|
self.order.append(step_id)
|
|
113
126
|
self.key_index[key] = step_id
|
|
127
|
+
self.state.retained_ids.add(step_id)
|
|
114
128
|
self._prune_steps()
|
|
115
129
|
self._last_running[(task_id, context_id, kind, name)] = step_id
|
|
116
130
|
return st
|
|
@@ -131,26 +145,46 @@ class StepManager:
|
|
|
131
145
|
|
|
132
146
|
def _remove_subtree(self, root_id: str) -> None:
|
|
133
147
|
"""Remove a complete subtree from all data structures."""
|
|
148
|
+
for step_id in self._collect_subtree_ids(root_id):
|
|
149
|
+
self._purge_step_references(step_id)
|
|
150
|
+
|
|
151
|
+
def _collect_subtree_ids(self, root_id: str) -> list[str]:
|
|
152
|
+
"""Return a flat list of step ids contained within a subtree."""
|
|
134
153
|
stack = [root_id]
|
|
135
|
-
|
|
154
|
+
collected: list[str] = []
|
|
136
155
|
while stack:
|
|
137
156
|
sid = stack.pop()
|
|
138
|
-
|
|
157
|
+
collected.append(sid)
|
|
139
158
|
stack.extend(self.children.pop(sid, []))
|
|
159
|
+
return collected
|
|
140
160
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
def _purge_step_references(self, step_id: str) -> None:
|
|
162
|
+
"""Remove a single step id from all indexes and helper structures."""
|
|
163
|
+
st = self.by_id.pop(step_id, None)
|
|
164
|
+
if st:
|
|
165
|
+
key = (st.task_id, st.context_id, st.kind, st.name)
|
|
166
|
+
self._last_running.pop(key, None)
|
|
167
|
+
self.state.retained_ids.discard(step_id)
|
|
168
|
+
self.state.discard_running(step_id)
|
|
169
|
+
self._remove_parent_links(step_id)
|
|
170
|
+
if step_id in self.order:
|
|
171
|
+
self.order.remove(step_id)
|
|
172
|
+
self.state.buffered_children.pop(step_id, None)
|
|
173
|
+
self.state.pending_branch_failures.discard(step_id)
|
|
174
|
+
|
|
175
|
+
def _remove_parent_links(self, child_id: str) -> None:
|
|
176
|
+
"""Detach a child id from any parent lists."""
|
|
177
|
+
for parent, kids in self.children.copy().items():
|
|
178
|
+
if child_id not in kids:
|
|
179
|
+
continue
|
|
180
|
+
kids.remove(child_id)
|
|
181
|
+
if not kids:
|
|
182
|
+
self.children.pop(parent, None)
|
|
151
183
|
|
|
152
184
|
def _should_prune_steps(self, total: int) -> bool:
|
|
153
185
|
"""Check if steps should be pruned."""
|
|
186
|
+
if self.max_steps <= 0:
|
|
187
|
+
return False
|
|
154
188
|
return total > self.max_steps
|
|
155
189
|
|
|
156
190
|
def _get_oldest_step_id(self) -> str | None:
|
|
@@ -172,6 +206,32 @@ class StepManager:
|
|
|
172
206
|
self._remove_subtree(sid)
|
|
173
207
|
total -= subtree_size
|
|
174
208
|
|
|
209
|
+
def remove_step(self, step_id: str) -> None:
|
|
210
|
+
"""Remove a single step from the tree and cached indexes."""
|
|
211
|
+
step = self.by_id.pop(step_id, None)
|
|
212
|
+
if not step:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if step.parent_id:
|
|
216
|
+
self.state.unlink_child(step.parent_id, step_id)
|
|
217
|
+
else:
|
|
218
|
+
self.state.unlink_root(step_id)
|
|
219
|
+
|
|
220
|
+
self.children.pop(step_id, None)
|
|
221
|
+
self.state.buffered_children.pop(step_id, None)
|
|
222
|
+
self.state.retained_ids.discard(step_id)
|
|
223
|
+
self.state.pending_branch_failures.discard(step_id)
|
|
224
|
+
self.state.discard_running(step_id)
|
|
225
|
+
|
|
226
|
+
self.key_index = {key: sid for key, sid in self.key_index.items() if sid != step_id}
|
|
227
|
+
for key, last_sid in self._last_running.copy().items():
|
|
228
|
+
if last_sid == step_id:
|
|
229
|
+
self._last_running.pop(key, None)
|
|
230
|
+
|
|
231
|
+
aliases = [alias for alias, target in self._step_aliases.items() if alias == step_id or target == step_id]
|
|
232
|
+
for alias in aliases:
|
|
233
|
+
self._step_aliases.pop(alias, None)
|
|
234
|
+
|
|
175
235
|
def get_child_count(self, step_id: str) -> int:
|
|
176
236
|
"""Get the number of child steps for a given step.
|
|
177
237
|
|
|
@@ -224,6 +284,24 @@ class StepManager:
|
|
|
224
284
|
return st
|
|
225
285
|
return None
|
|
226
286
|
|
|
287
|
+
def list_ordered(self) -> list[Step]:
|
|
288
|
+
"""Return a depth-first list of steps currently tracked."""
|
|
289
|
+
ordered: list[Step] = []
|
|
290
|
+
for step_id, _branch_state in self.iter_tree():
|
|
291
|
+
step = self.by_id.get(step_id)
|
|
292
|
+
if step:
|
|
293
|
+
ordered.append(step)
|
|
294
|
+
return ordered
|
|
295
|
+
|
|
296
|
+
def prune_for_summary(self, limit: int) -> list[tuple[str, tuple[bool, ...]]]:
|
|
297
|
+
"""Return the most recent ``limit`` nodes for compact summaries."""
|
|
298
|
+
if limit < 0:
|
|
299
|
+
raise StepManagerError("limit must be non-negative")
|
|
300
|
+
nodes = list(self.iter_tree())
|
|
301
|
+
if limit == 0 or len(nodes) <= limit:
|
|
302
|
+
return nodes
|
|
303
|
+
return nodes[-limit:]
|
|
304
|
+
|
|
227
305
|
def finish(
|
|
228
306
|
self,
|
|
229
307
|
*,
|
|
@@ -250,9 +328,7 @@ class StepManager:
|
|
|
250
328
|
Raises:
|
|
251
329
|
RuntimeError: If no matching step is found
|
|
252
330
|
"""
|
|
253
|
-
st = self.find_running(
|
|
254
|
-
task_id=task_id, context_id=context_id, kind=kind, name=name
|
|
255
|
-
)
|
|
331
|
+
st = self.find_running(task_id=task_id, context_id=context_id, kind=kind, name=name)
|
|
256
332
|
if not st:
|
|
257
333
|
# Try to find any existing step with matching parameters, even if not running
|
|
258
334
|
for sid in reversed(list(self._iter_all_steps())):
|
|
@@ -269,9 +345,7 @@ class StepManager:
|
|
|
269
345
|
|
|
270
346
|
# If still no step found, create a new one
|
|
271
347
|
if not st:
|
|
272
|
-
st = self.start_or_get(
|
|
273
|
-
task_id=task_id, context_id=context_id, kind=kind, name=name
|
|
274
|
-
)
|
|
348
|
+
st = self.start_or_get(task_id=task_id, context_id=context_id, kind=kind, name=name)
|
|
275
349
|
|
|
276
350
|
if output:
|
|
277
351
|
st.output = output
|
|
@@ -289,3 +363,25 @@ class StepManager:
|
|
|
289
363
|
sid = stack.pop()
|
|
290
364
|
yield sid
|
|
291
365
|
stack.extend(self.children.get(sid, []))
|
|
366
|
+
|
|
367
|
+
def iter_tree(self) -> Iterator[tuple[str, tuple[bool, ...]]]:
|
|
368
|
+
"""Expose depth-first traversal info for rendering."""
|
|
369
|
+
yield from self.state.iter_visible_tree()
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def order(self) -> list[str]:
|
|
373
|
+
"""Root step ordering accessor backed by StepTreeState."""
|
|
374
|
+
return self.state.root_order
|
|
375
|
+
|
|
376
|
+
@order.setter
|
|
377
|
+
def order(self, value: list[str]) -> None:
|
|
378
|
+
self.state.root_order = list(value)
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def children(self) -> dict[str, list[str]]:
|
|
382
|
+
"""Child mapping accessor backed by StepTreeState."""
|
|
383
|
+
return self.state.child_map
|
|
384
|
+
|
|
385
|
+
@children.setter
|
|
386
|
+
def children(self, value: dict[str, list[str]]) -> None:
|
|
387
|
+
self.state.child_map = value
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Shared timing helpers for renderer components.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def coerce_server_time(value: Any) -> float | None:
|
|
13
|
+
"""Convert a raw SSE/server time payload into a float."""
|
|
14
|
+
if isinstance(value, (int, float)):
|
|
15
|
+
return float(value)
|
|
16
|
+
try:
|
|
17
|
+
return float(value)
|
|
18
|
+
except (TypeError, ValueError):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def calculate_timeline_duration(
|
|
23
|
+
start_server: float | None,
|
|
24
|
+
end_server: float | None,
|
|
25
|
+
start_monotonic: float | None,
|
|
26
|
+
end_monotonic: float | None,
|
|
27
|
+
) -> float | None:
|
|
28
|
+
"""Return best-effort elapsed time using server or monotonic clocks."""
|
|
29
|
+
if start_server is not None and end_server is not None:
|
|
30
|
+
return max(0.0, float(end_server) - float(start_server))
|
|
31
|
+
if start_monotonic is not None and end_monotonic is not None:
|
|
32
|
+
try:
|
|
33
|
+
return max(0.0, float(end_monotonic) - float(start_monotonic))
|
|
34
|
+
except Exception:
|
|
35
|
+
return None
|
|
36
|
+
return None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared transcript viewer exports.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from glaip_sdk.utils.rendering.viewer.presenter import (
|
|
8
|
+
ViewerContext,
|
|
9
|
+
prepare_viewer_snapshot,
|
|
10
|
+
render_post_run_view,
|
|
11
|
+
render_transcript_events,
|
|
12
|
+
render_transcript_view,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ViewerContext",
|
|
17
|
+
"prepare_viewer_snapshot",
|
|
18
|
+
"render_post_run_view",
|
|
19
|
+
"render_transcript_events",
|
|
20
|
+
"render_transcript_view",
|
|
21
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Shared presenter utilities for CLI/offline transcript viewing.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from glaip_sdk.utils.rendering.layout.transcript import (
|
|
15
|
+
DEFAULT_TRANSCRIPT_THEME,
|
|
16
|
+
TranscriptGlyphs,
|
|
17
|
+
TranscriptSnapshot,
|
|
18
|
+
build_transcript_snapshot,
|
|
19
|
+
build_transcript_view,
|
|
20
|
+
)
|
|
21
|
+
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event_stream
|
|
22
|
+
from glaip_sdk.utils.rendering.state import RendererState, coerce_received_at
|
|
23
|
+
from glaip_sdk.utils.rendering.steps import StepManager
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class ViewerContext:
|
|
28
|
+
"""Runtime context passed to transcript presenters."""
|
|
29
|
+
|
|
30
|
+
manifest_entry: dict[str, Any]
|
|
31
|
+
events: list[dict[str, Any]]
|
|
32
|
+
default_output: str
|
|
33
|
+
final_output: str
|
|
34
|
+
stream_started_at: float | None
|
|
35
|
+
meta: dict[str, Any]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_post_run_view(
|
|
39
|
+
console: Console,
|
|
40
|
+
ctx: ViewerContext,
|
|
41
|
+
*,
|
|
42
|
+
glyphs: TranscriptGlyphs | None = None,
|
|
43
|
+
theme: str = DEFAULT_TRANSCRIPT_THEME,
|
|
44
|
+
) -> TranscriptSnapshot:
|
|
45
|
+
"""Render the default summary view and return the snapshot used."""
|
|
46
|
+
snapshot, _state = prepare_viewer_snapshot(
|
|
47
|
+
ctx,
|
|
48
|
+
glyphs=glyphs,
|
|
49
|
+
theme=theme,
|
|
50
|
+
)
|
|
51
|
+
render_transcript_view(console, snapshot, theme=theme)
|
|
52
|
+
return snapshot
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def render_transcript_view(
|
|
56
|
+
console: Console,
|
|
57
|
+
snapshot: TranscriptSnapshot,
|
|
58
|
+
*,
|
|
59
|
+
theme: str = DEFAULT_TRANSCRIPT_THEME,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Render the transcript summary using a prepared snapshot."""
|
|
62
|
+
header, body = build_transcript_view(snapshot, theme=theme)
|
|
63
|
+
_print_renderables(console, header + body)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def render_transcript_events(console: Console, events: list[dict[str, Any]]) -> None:
|
|
67
|
+
"""Pretty-print transcript events using shared debug presenter."""
|
|
68
|
+
if not events:
|
|
69
|
+
console.print("[dim]No SSE events were captured for this run.[/dim]")
|
|
70
|
+
console.print()
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
console.print("[bold]Transcript Events[/bold]")
|
|
74
|
+
console.print("[dim]────────────────────────────────────────────────────────[/dim]")
|
|
75
|
+
|
|
76
|
+
render_debug_event_stream(
|
|
77
|
+
events,
|
|
78
|
+
console,
|
|
79
|
+
resolve_timestamp=lambda event: coerce_received_at(event.get("received_at")),
|
|
80
|
+
)
|
|
81
|
+
console.print()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def prepare_viewer_snapshot(
|
|
85
|
+
ctx: ViewerContext,
|
|
86
|
+
*,
|
|
87
|
+
glyphs: TranscriptGlyphs | None,
|
|
88
|
+
theme: str,
|
|
89
|
+
) -> tuple[TranscriptSnapshot, RendererState]:
|
|
90
|
+
"""Build a transcript snapshot plus renderer state for reusable viewing."""
|
|
91
|
+
state = _build_renderer_state(ctx)
|
|
92
|
+
manager = _build_steps_from_events(ctx.events)
|
|
93
|
+
query = _extract_query_from_manifest(ctx)
|
|
94
|
+
merged_meta = _merge_meta(ctx)
|
|
95
|
+
snapshot = build_transcript_snapshot(
|
|
96
|
+
state,
|
|
97
|
+
manager,
|
|
98
|
+
glyphs=glyphs,
|
|
99
|
+
query_text=query,
|
|
100
|
+
meta=merged_meta,
|
|
101
|
+
theme=theme,
|
|
102
|
+
)
|
|
103
|
+
return snapshot, state
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_renderer_state(ctx: ViewerContext) -> RendererState:
|
|
107
|
+
state = RendererState()
|
|
108
|
+
state.meta = dict(ctx.meta or {})
|
|
109
|
+
|
|
110
|
+
final_text = (ctx.final_output or "").strip()
|
|
111
|
+
default_text = (ctx.default_output or "").strip()
|
|
112
|
+
if final_text:
|
|
113
|
+
state.final_text = final_text
|
|
114
|
+
elif default_text:
|
|
115
|
+
state.final_text = default_text
|
|
116
|
+
state.buffer.append(default_text)
|
|
117
|
+
|
|
118
|
+
duration = _extract_final_duration(ctx.events)
|
|
119
|
+
if duration:
|
|
120
|
+
state.final_duration_text = duration # pragma: no cover - exercised indirectly via end-to-end tests
|
|
121
|
+
state.events = list(ctx.events or [])
|
|
122
|
+
return state
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_steps_from_events(events: list[dict[str, Any]]) -> StepManager:
|
|
126
|
+
manager = StepManager()
|
|
127
|
+
for event in events or []:
|
|
128
|
+
payload = _coerce_step_event(event)
|
|
129
|
+
if not payload:
|
|
130
|
+
continue
|
|
131
|
+
try:
|
|
132
|
+
manager.apply_event(payload)
|
|
133
|
+
except ValueError:
|
|
134
|
+
continue
|
|
135
|
+
return manager
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _coerce_step_event(event: dict[str, Any]) -> dict[str, Any] | None:
|
|
139
|
+
metadata = event.get("metadata")
|
|
140
|
+
if not isinstance(metadata, dict):
|
|
141
|
+
return None
|
|
142
|
+
if not isinstance(metadata.get("step_id"), str):
|
|
143
|
+
return None
|
|
144
|
+
return {
|
|
145
|
+
"metadata": metadata,
|
|
146
|
+
"status": event.get("status"),
|
|
147
|
+
"task_state": event.get("task_state"),
|
|
148
|
+
"content": event.get("content"),
|
|
149
|
+
"task_id": event.get("task_id"),
|
|
150
|
+
"context_id": event.get("context_id"),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _extract_final_duration(events: list[dict[str, Any]]) -> str | None:
|
|
155
|
+
for event in events or []:
|
|
156
|
+
metadata = event.get("metadata") or {}
|
|
157
|
+
if metadata.get("kind") != "final_response":
|
|
158
|
+
continue
|
|
159
|
+
time_value = metadata.get("time")
|
|
160
|
+
if isinstance(time_value, (int, float)):
|
|
161
|
+
return f"{float(time_value):.2f}s"
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _extract_query_from_manifest(ctx: ViewerContext) -> str | None:
|
|
166
|
+
query = ctx.manifest_entry.get("input_message") or ctx.meta.get("input_message") or ctx.meta.get("query")
|
|
167
|
+
if isinstance(query, str) and query.strip():
|
|
168
|
+
return query.strip()
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _merge_meta(ctx: ViewerContext) -> dict[str, Any]:
|
|
173
|
+
merged = dict(ctx.meta or {})
|
|
174
|
+
manifest = ctx.manifest_entry or {}
|
|
175
|
+
for key in ("agent_name", "agent_id", "model", "run_id", "input_message"):
|
|
176
|
+
if key in manifest and manifest[key] and key not in merged:
|
|
177
|
+
merged[key] = manifest[key]
|
|
178
|
+
return merged
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _print_renderables(console: Console, renderables: list[Any]) -> None:
|
|
182
|
+
for renderable in renderables:
|
|
183
|
+
console.print(renderable)
|
|
184
|
+
console.print()
|