glaip-sdk 0.6.11__py3-none-any.whl → 0.6.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +42 -5
- {glaip_sdk-0.6.11.dist-info → glaip_sdk-0.6.14.dist-info}/METADATA +31 -37
- glaip_sdk-0.6.14.dist-info/RECORD +12 -0
- {glaip_sdk-0.6.11.dist-info → glaip_sdk-0.6.14.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.14.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.14.dist-info/top_level.txt +1 -0
- glaip_sdk/agents/__init__.py +0 -27
- glaip_sdk/agents/base.py +0 -1191
- glaip_sdk/cli/__init__.py +0 -9
- glaip_sdk/cli/account_store.py +0 -540
- glaip_sdk/cli/agent_config.py +0 -78
- glaip_sdk/cli/auth.py +0 -699
- glaip_sdk/cli/commands/__init__.py +0 -5
- glaip_sdk/cli/commands/accounts.py +0 -746
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/common_config.py +0 -101
- glaip_sdk/cli/commands/configure.py +0 -896
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/models.py +0 -69
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/commands/transcripts.py +0 -755
- glaip_sdk/cli/commands/update.py +0 -61
- glaip_sdk/cli/config.py +0 -95
- glaip_sdk/cli/constants.py +0 -38
- glaip_sdk/cli/context.py +0 -150
- glaip_sdk/cli/core/__init__.py +0 -79
- glaip_sdk/cli/core/context.py +0 -124
- glaip_sdk/cli/core/output.py +0 -846
- glaip_sdk/cli/core/prompting.py +0 -649
- glaip_sdk/cli/core/rendering.py +0 -187
- glaip_sdk/cli/display.py +0 -355
- glaip_sdk/cli/hints.py +0 -57
- glaip_sdk/cli/io.py +0 -112
- glaip_sdk/cli/main.py +0 -604
- glaip_sdk/cli/masking.py +0 -136
- glaip_sdk/cli/mcp_validators.py +0 -287
- glaip_sdk/cli/pager.py +0 -266
- glaip_sdk/cli/parsers/__init__.py +0 -7
- glaip_sdk/cli/parsers/json_input.py +0 -177
- glaip_sdk/cli/resolution.py +0 -67
- glaip_sdk/cli/rich_helpers.py +0 -27
- glaip_sdk/cli/slash/__init__.py +0 -15
- glaip_sdk/cli/slash/accounts_controller.py +0 -578
- glaip_sdk/cli/slash/accounts_shared.py +0 -75
- glaip_sdk/cli/slash/agent_session.py +0 -285
- glaip_sdk/cli/slash/prompt.py +0 -256
- glaip_sdk/cli/slash/remote_runs_controller.py +0 -566
- glaip_sdk/cli/slash/session.py +0 -1708
- glaip_sdk/cli/slash/tui/__init__.py +0 -9
- glaip_sdk/cli/slash/tui/accounts_app.py +0 -876
- glaip_sdk/cli/slash/tui/background_tasks.py +0 -72
- glaip_sdk/cli/slash/tui/loading.py +0 -58
- glaip_sdk/cli/slash/tui/remote_runs_app.py +0 -628
- glaip_sdk/cli/transcript/__init__.py +0 -31
- glaip_sdk/cli/transcript/cache.py +0 -536
- glaip_sdk/cli/transcript/capture.py +0 -329
- glaip_sdk/cli/transcript/export.py +0 -38
- glaip_sdk/cli/transcript/history.py +0 -815
- glaip_sdk/cli/transcript/launcher.py +0 -77
- glaip_sdk/cli/transcript/viewer.py +0 -374
- glaip_sdk/cli/update_notifier.py +0 -290
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk/cli/validators.py +0 -238
- glaip_sdk/client/__init__.py +0 -11
- glaip_sdk/client/_agent_payloads.py +0 -520
- glaip_sdk/client/agent_runs.py +0 -147
- glaip_sdk/client/agents.py +0 -1335
- glaip_sdk/client/base.py +0 -502
- glaip_sdk/client/main.py +0 -249
- glaip_sdk/client/mcps.py +0 -370
- glaip_sdk/client/run_rendering.py +0 -700
- glaip_sdk/client/shared.py +0 -21
- glaip_sdk/client/tools.py +0 -661
- glaip_sdk/client/validators.py +0 -198
- glaip_sdk/config/constants.py +0 -52
- glaip_sdk/mcps/__init__.py +0 -21
- glaip_sdk/mcps/base.py +0 -345
- glaip_sdk/models/__init__.py +0 -90
- glaip_sdk/models/agent.py +0 -47
- glaip_sdk/models/agent_runs.py +0 -116
- glaip_sdk/models/common.py +0 -42
- glaip_sdk/models/mcp.py +0 -33
- glaip_sdk/models/tool.py +0 -33
- glaip_sdk/payload_schemas/__init__.py +0 -7
- glaip_sdk/payload_schemas/agent.py +0 -85
- glaip_sdk/registry/__init__.py +0 -55
- glaip_sdk/registry/agent.py +0 -164
- glaip_sdk/registry/base.py +0 -139
- glaip_sdk/registry/mcp.py +0 -253
- glaip_sdk/registry/tool.py +0 -232
- glaip_sdk/runner/__init__.py +0 -59
- glaip_sdk/runner/base.py +0 -84
- glaip_sdk/runner/deps.py +0 -115
- glaip_sdk/runner/langgraph.py +0 -782
- glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -257
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
- glaip_sdk/runner/tool_adapter/__init__.py +0 -18
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -219
- glaip_sdk/tools/__init__.py +0 -22
- glaip_sdk/tools/base.py +0 -435
- glaip_sdk/utils/__init__.py +0 -86
- glaip_sdk/utils/a2a/__init__.py +0 -34
- glaip_sdk/utils/a2a/event_processor.py +0 -188
- glaip_sdk/utils/agent_config.py +0 -194
- glaip_sdk/utils/bundler.py +0 -267
- glaip_sdk/utils/client.py +0 -111
- glaip_sdk/utils/client_utils.py +0 -486
- glaip_sdk/utils/datetime_helpers.py +0 -58
- glaip_sdk/utils/discovery.py +0 -78
- glaip_sdk/utils/display.py +0 -135
- glaip_sdk/utils/export.py +0 -143
- glaip_sdk/utils/general.py +0 -61
- glaip_sdk/utils/import_export.py +0 -168
- glaip_sdk/utils/import_resolver.py +0 -492
- glaip_sdk/utils/instructions.py +0 -101
- glaip_sdk/utils/rendering/__init__.py +0 -115
- glaip_sdk/utils/rendering/formatting.py +0 -264
- glaip_sdk/utils/rendering/layout/__init__.py +0 -64
- glaip_sdk/utils/rendering/layout/panels.py +0 -156
- glaip_sdk/utils/rendering/layout/progress.py +0 -202
- glaip_sdk/utils/rendering/layout/summary.py +0 -74
- glaip_sdk/utils/rendering/layout/transcript.py +0 -606
- glaip_sdk/utils/rendering/models.py +0 -85
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -55
- glaip_sdk/utils/rendering/renderer/base.py +0 -1024
- glaip_sdk/utils/rendering/renderer/config.py +0 -27
- glaip_sdk/utils/rendering/renderer/console.py +0 -55
- glaip_sdk/utils/rendering/renderer/debug.py +0 -178
- glaip_sdk/utils/rendering/renderer/factory.py +0 -138
- glaip_sdk/utils/rendering/renderer/stream.py +0 -202
- glaip_sdk/utils/rendering/renderer/summary_window.py +0 -79
- glaip_sdk/utils/rendering/renderer/thinking.py +0 -273
- glaip_sdk/utils/rendering/renderer/toggle.py +0 -182
- glaip_sdk/utils/rendering/renderer/tool_panels.py +0 -442
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +0 -162
- glaip_sdk/utils/rendering/state.py +0 -204
- glaip_sdk/utils/rendering/step_tree_state.py +0 -100
- glaip_sdk/utils/rendering/steps/__init__.py +0 -34
- glaip_sdk/utils/rendering/steps/event_processor.py +0 -778
- glaip_sdk/utils/rendering/steps/format.py +0 -176
- glaip_sdk/utils/rendering/steps/manager.py +0 -387
- glaip_sdk/utils/rendering/timing.py +0 -36
- glaip_sdk/utils/rendering/viewer/__init__.py +0 -21
- glaip_sdk/utils/rendering/viewer/presenter.py +0 -184
- glaip_sdk/utils/resource_refs.py +0 -195
- glaip_sdk/utils/run_renderer.py +0 -41
- glaip_sdk/utils/runtime_config.py +0 -425
- glaip_sdk/utils/serialization.py +0 -424
- glaip_sdk/utils/sync.py +0 -142
- glaip_sdk/utils/tool_detection.py +0 -33
- glaip_sdk/utils/validation.py +0 -264
- glaip_sdk-0.6.11.dist-info/RECORD +0 -159
- glaip_sdk-0.6.11.dist-info/entry_points.txt +0 -3
|
@@ -1,566 +0,0 @@
|
|
|
1
|
-
"""Remote runs controller for browsing agent run history.
|
|
2
|
-
|
|
3
|
-
Authors:
|
|
4
|
-
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import math
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import TYPE_CHECKING, Any
|
|
13
|
-
|
|
14
|
-
import click
|
|
15
|
-
from rich.console import Group
|
|
16
|
-
from rich.text import Text
|
|
17
|
-
|
|
18
|
-
try: # pragma: no cover - optional dependency
|
|
19
|
-
import questionary
|
|
20
|
-
from questionary import Choice
|
|
21
|
-
except Exception: # pragma: no cover - optional dependency
|
|
22
|
-
questionary = None # type: ignore[assignment]
|
|
23
|
-
Choice = None # type: ignore[assignment]
|
|
24
|
-
|
|
25
|
-
from glaip_sdk.branding import (
|
|
26
|
-
ERROR_STYLE,
|
|
27
|
-
INFO_STYLE,
|
|
28
|
-
SUCCESS_STYLE,
|
|
29
|
-
WARNING_STYLE,
|
|
30
|
-
)
|
|
31
|
-
from glaip_sdk.cli.constants import DEFAULT_REMOTE_RUNS_PAGE_LIMIT
|
|
32
|
-
from glaip_sdk.cli.slash.tui.remote_runs_app import RemoteRunsTUICallbacks, run_remote_runs_textual
|
|
33
|
-
from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
|
|
34
|
-
from glaip_sdk.exceptions import (
|
|
35
|
-
AuthenticationError,
|
|
36
|
-
ForbiddenError,
|
|
37
|
-
NotFoundError,
|
|
38
|
-
TimeoutError,
|
|
39
|
-
ValidationError,
|
|
40
|
-
)
|
|
41
|
-
from glaip_sdk.rich_components import RemoteRunsTable
|
|
42
|
-
from glaip_sdk.utils.export import export_remote_transcript_jsonl
|
|
43
|
-
from glaip_sdk.utils.rendering import render_remote_sse_transcript
|
|
44
|
-
|
|
45
|
-
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
46
|
-
from glaip_sdk.cli.slash.session import SlashSession
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class RemoteRunsController:
|
|
50
|
-
"""Controller for browsing remote agent run history."""
|
|
51
|
-
|
|
52
|
-
def __init__(self, session: SlashSession) -> None:
|
|
53
|
-
"""Initialize the remote runs controller.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
session: The slash session context.
|
|
57
|
-
"""
|
|
58
|
-
self.session = session
|
|
59
|
-
self.console = session.console
|
|
60
|
-
self.ctx = session.ctx
|
|
61
|
-
|
|
62
|
-
self._snapshot_notice_shown = False
|
|
63
|
-
|
|
64
|
-
def handle_runs_command(self, args: list[str]) -> bool:
|
|
65
|
-
"""Handle the /runs command for browsing remote agent run history.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
args: Command arguments (optional run_id for detail view).
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
True to continue session.
|
|
72
|
-
"""
|
|
73
|
-
current_agent = getattr(self.session, "_current_agent", None)
|
|
74
|
-
if not current_agent:
|
|
75
|
-
self.console.print(
|
|
76
|
-
f"[{WARNING_STYLE}]Open /agents and select an agent first to browse remote run history.[/]"
|
|
77
|
-
)
|
|
78
|
-
return self._continue_session()
|
|
79
|
-
|
|
80
|
-
agent_id = str(getattr(current_agent, "id", ""))
|
|
81
|
-
if not agent_id:
|
|
82
|
-
self.console.print(f"[{ERROR_STYLE}]Invalid agent context.[/]")
|
|
83
|
-
return self._continue_session()
|
|
84
|
-
|
|
85
|
-
if args:
|
|
86
|
-
run_id = args[0]
|
|
87
|
-
self.show_run_detail(agent_id, run_id)
|
|
88
|
-
return self._continue_session()
|
|
89
|
-
|
|
90
|
-
client = self._get_client_or_fail()
|
|
91
|
-
if not client:
|
|
92
|
-
return self._continue_session()
|
|
93
|
-
|
|
94
|
-
agent_name = getattr(current_agent, "name", "") or None
|
|
95
|
-
self.open_remote_runs_browser(client, agent_id, agent_name=agent_name)
|
|
96
|
-
return self._continue_session()
|
|
97
|
-
|
|
98
|
-
def open_remote_runs_browser(self, client: Any, agent_id: str, *, agent_name: str | None = None) -> None:
|
|
99
|
-
"""Fetch and render the remote runs table for the current agent.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
client: API client instance.
|
|
103
|
-
agent_id: UUID of the agent.
|
|
104
|
-
agent_name: Optional display name for the agent.
|
|
105
|
-
"""
|
|
106
|
-
state = self._get_runs_state(agent_id)
|
|
107
|
-
runs_page = self._fetch_remote_runs_page(client, agent_id, state)
|
|
108
|
-
if runs_page is None:
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
state["page"] = runs_page.page
|
|
112
|
-
state["limit"] = runs_page.limit
|
|
113
|
-
cursor = state.get("cursor", 0)
|
|
114
|
-
if runs_page.data:
|
|
115
|
-
cursor = max(0, min(cursor, len(runs_page.data) - 1))
|
|
116
|
-
state["cursor"] = cursor
|
|
117
|
-
|
|
118
|
-
if self._should_use_textual_browser():
|
|
119
|
-
self._run_textual_browser(client, agent_id, runs_page, state, agent_name=agent_name)
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
if not self._snapshot_notice_shown:
|
|
123
|
-
self.console.print(
|
|
124
|
-
f"[{INFO_STYLE}]Interactive remote history requires a TTY. Showing the latest snapshot instead.[/]"
|
|
125
|
-
)
|
|
126
|
-
self._snapshot_notice_shown = True
|
|
127
|
-
self._render_runs_table(runs_page, agent_id, cursor_idx=cursor)
|
|
128
|
-
|
|
129
|
-
def _get_runs_state(self, agent_id: str) -> dict[str, Any]:
|
|
130
|
-
"""Return the persisted pagination state for an agent.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
agent_id: UUID of the agent.
|
|
134
|
-
|
|
135
|
-
Returns:
|
|
136
|
-
Dictionary with page, limit, and cursor state.
|
|
137
|
-
"""
|
|
138
|
-
pagination_state = getattr(self.session, "_runs_pagination_state", {})
|
|
139
|
-
state = pagination_state.setdefault(
|
|
140
|
-
agent_id,
|
|
141
|
-
{"page": 1, "limit": DEFAULT_REMOTE_RUNS_PAGE_LIMIT, "cursor": 0},
|
|
142
|
-
)
|
|
143
|
-
state.setdefault("page", 1)
|
|
144
|
-
state.setdefault("limit", DEFAULT_REMOTE_RUNS_PAGE_LIMIT)
|
|
145
|
-
state.setdefault("cursor", 0)
|
|
146
|
-
return state
|
|
147
|
-
|
|
148
|
-
def _fetch_remote_runs_page(
|
|
149
|
-
self,
|
|
150
|
-
client: Any,
|
|
151
|
-
agent_id: str,
|
|
152
|
-
state: dict[str, Any],
|
|
153
|
-
*,
|
|
154
|
-
allow_reset: bool = True,
|
|
155
|
-
) -> Any | None:
|
|
156
|
-
"""Fetch a RunsPage while handling common error flows.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
client: API client instance.
|
|
160
|
-
agent_id: UUID of the agent.
|
|
161
|
-
state: Pagination state dictionary.
|
|
162
|
-
allow_reset: Whether to reset pagination on validation errors.
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
RunsPage instance or None on error.
|
|
166
|
-
"""
|
|
167
|
-
try:
|
|
168
|
-
return client.agents.runs.list_runs(agent_id, limit=state["limit"], page=state["page"])
|
|
169
|
-
except AuthenticationError:
|
|
170
|
-
self.console.print(f"[{ERROR_STYLE}]Authentication failed. Run /login to refresh credentials.[/]")
|
|
171
|
-
except ForbiddenError as exc:
|
|
172
|
-
self.console.print(f"[{ERROR_STYLE}]Access denied: {exc}[/]")
|
|
173
|
-
except NotFoundError:
|
|
174
|
-
self.console.print(
|
|
175
|
-
f"[{WARNING_STYLE}]Agent not found or access revoked. Re-open /agents to select again.[/]"
|
|
176
|
-
)
|
|
177
|
-
pagination_state = getattr(self.session, "_runs_pagination_state", {})
|
|
178
|
-
pagination_state.pop(agent_id, None)
|
|
179
|
-
except TimeoutError:
|
|
180
|
-
ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else {}
|
|
181
|
-
timeout_seconds = ctx_obj.get("timeout", 30)
|
|
182
|
-
self.console.print(
|
|
183
|
-
f"[{WARNING_STYLE}]Remote history timed out after {timeout_seconds}s. Press Enter to retry.[/]"
|
|
184
|
-
)
|
|
185
|
-
except ValidationError:
|
|
186
|
-
if allow_reset:
|
|
187
|
-
self.console.print(
|
|
188
|
-
f"[{WARNING_STYLE}]Invalid pagination request (page {state['page']}, limit {state['limit']}). "
|
|
189
|
-
"Resetting to defaults.[/]"
|
|
190
|
-
)
|
|
191
|
-
state["page"] = 1
|
|
192
|
-
state["limit"] = DEFAULT_REMOTE_RUNS_PAGE_LIMIT
|
|
193
|
-
return self._fetch_remote_runs_page(client, agent_id, state, allow_reset=False)
|
|
194
|
-
self.console.print(f"[{ERROR_STYLE}]Pagination request rejected by backend.[/]")
|
|
195
|
-
except Exception as exc: # pragma: no cover - unexpected API failure
|
|
196
|
-
self.console.print(f"[{ERROR_STYLE}]Error fetching runs: {exc}[/]")
|
|
197
|
-
return None
|
|
198
|
-
|
|
199
|
-
def _build_runs_table_renderable(
|
|
200
|
-
self,
|
|
201
|
-
runs_page: Any,
|
|
202
|
-
agent_id: str,
|
|
203
|
-
*,
|
|
204
|
-
cursor_idx: int = 0,
|
|
205
|
-
) -> Group:
|
|
206
|
-
"""Build the Rich renderable for the runs table view."""
|
|
207
|
-
current_agent = getattr(self.session, "_current_agent", None)
|
|
208
|
-
agent_label = getattr(current_agent, "name", agent_id) if current_agent else agent_id
|
|
209
|
-
total_pages = 1
|
|
210
|
-
if runs_page.limit:
|
|
211
|
-
total_pages = max(1, math.ceil(runs_page.total / runs_page.limit))
|
|
212
|
-
header = (
|
|
213
|
-
f"[dim]Agent: {agent_label} ({agent_id}) · Limit={runs_page.limit} · "
|
|
214
|
-
f"Page {runs_page.page}/{total_pages} (use ←/→ to paginate)[/]"
|
|
215
|
-
)
|
|
216
|
-
renderables: list[Any] = [Text.from_markup(f"\n{header}")]
|
|
217
|
-
|
|
218
|
-
if runs_page.total == 0:
|
|
219
|
-
renderables.append(
|
|
220
|
-
Text.from_markup(f"[{WARNING_STYLE}]No remote runs yet. Trigger `/agents run` to create a run.[/]")
|
|
221
|
-
)
|
|
222
|
-
return Group(*renderables)
|
|
223
|
-
|
|
224
|
-
table = RemoteRunsTable(title="Remote Runs — ↑/↓ rows · ←/→ pages · q/Esc exit")
|
|
225
|
-
for idx, run in enumerate(runs_page.data):
|
|
226
|
-
run_type_str = run.run_type.title()
|
|
227
|
-
status_str = run.status.upper()
|
|
228
|
-
started_str = run.started_at.strftime("%Y-%m-%d %H:%M:%S") if run.started_at else "—"
|
|
229
|
-
completed_str = run.completed_at.strftime("%Y-%m-%d %H:%M:%S") if run.completed_at else "—"
|
|
230
|
-
duration_str = run.duration_formatted()
|
|
231
|
-
input_preview = run.input_preview()
|
|
232
|
-
table.add_run_row(
|
|
233
|
-
str(run.id),
|
|
234
|
-
run_type_str,
|
|
235
|
-
status_str,
|
|
236
|
-
started_str,
|
|
237
|
-
completed_str,
|
|
238
|
-
duration_str,
|
|
239
|
-
input_preview,
|
|
240
|
-
selected=idx == cursor_idx,
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
renderables.append(table)
|
|
244
|
-
renderables.append(Text.from_markup("[dim]Enter detail · e export JSONL · q/Esc exit[/]"))
|
|
245
|
-
return Group(*renderables)
|
|
246
|
-
|
|
247
|
-
def _render_runs_table(self, runs_page: Any, agent_id: str, *, cursor_idx: int = 0) -> None:
|
|
248
|
-
"""Render runs table with pagination info."""
|
|
249
|
-
renderable = self._build_runs_table_renderable(
|
|
250
|
-
runs_page,
|
|
251
|
-
agent_id,
|
|
252
|
-
cursor_idx=cursor_idx,
|
|
253
|
-
)
|
|
254
|
-
self.console.print(renderable)
|
|
255
|
-
|
|
256
|
-
def _should_use_textual_browser(self) -> bool:
|
|
257
|
-
"""Return True when Textual-based navigation can be used."""
|
|
258
|
-
ctx_obj = getattr(self.session.ctx, "obj", {})
|
|
259
|
-
interactive = bool(getattr(self.session, "_interactive", False))
|
|
260
|
-
if not interactive and isinstance(ctx_obj, dict) and ctx_obj.get("tty"):
|
|
261
|
-
interactive = True
|
|
262
|
-
if not interactive:
|
|
263
|
-
return False
|
|
264
|
-
try:
|
|
265
|
-
stdin_tty = sys.stdin.isatty()
|
|
266
|
-
stdout_tty = sys.stdout.isatty()
|
|
267
|
-
except Exception:
|
|
268
|
-
return False
|
|
269
|
-
return bool(stdin_tty and stdout_tty)
|
|
270
|
-
|
|
271
|
-
def _run_textual_browser(
|
|
272
|
-
self,
|
|
273
|
-
client: Any,
|
|
274
|
-
agent_id: str,
|
|
275
|
-
runs_page: Any,
|
|
276
|
-
state: dict[str, Any],
|
|
277
|
-
*,
|
|
278
|
-
agent_name: str | None = None,
|
|
279
|
-
) -> None:
|
|
280
|
-
"""Launch the Textual UI for browsing runs."""
|
|
281
|
-
|
|
282
|
-
def fetch_page(page: int, limit: int) -> Any | None:
|
|
283
|
-
fetch_state = {"page": page, "limit": limit}
|
|
284
|
-
return self._fetch_remote_runs_page(client, agent_id, fetch_state)
|
|
285
|
-
|
|
286
|
-
def fetch_detail(run_id: str) -> Any | None:
|
|
287
|
-
return self._load_run_detail(client, agent_id, run_id)
|
|
288
|
-
|
|
289
|
-
def export_run(run_id: str, detail: Any | None) -> bool:
|
|
290
|
-
return self.export_remote_run(agent_id, run_id, client, detail)
|
|
291
|
-
|
|
292
|
-
callbacks = RemoteRunsTUICallbacks(
|
|
293
|
-
fetch_page=fetch_page,
|
|
294
|
-
fetch_detail=fetch_detail,
|
|
295
|
-
export_run=export_run,
|
|
296
|
-
)
|
|
297
|
-
page, limit, cursor = run_remote_runs_textual(
|
|
298
|
-
runs_page,
|
|
299
|
-
state.get("cursor", 0),
|
|
300
|
-
callbacks,
|
|
301
|
-
agent_name=agent_name,
|
|
302
|
-
agent_id=agent_id,
|
|
303
|
-
)
|
|
304
|
-
state["page"] = page
|
|
305
|
-
state["limit"] = limit
|
|
306
|
-
state["cursor"] = cursor
|
|
307
|
-
|
|
308
|
-
def _load_run_detail(self, client: Any, agent_id: str, run_id: str) -> Any | None:
|
|
309
|
-
"""Return detailed run payload, handling errors."""
|
|
310
|
-
try:
|
|
311
|
-
return client.agents.runs.get_run(agent_id, run_id)
|
|
312
|
-
except AuthenticationError:
|
|
313
|
-
self.console.print(f"[{ERROR_STYLE}]Authentication failed while loading run detail.[/]")
|
|
314
|
-
except NotFoundError:
|
|
315
|
-
self.console.print(f"[{WARNING_STYLE}]Run no longer exists. It may have been cleaned up.[/]")
|
|
316
|
-
except TimeoutError:
|
|
317
|
-
self.console.print(f"[{WARNING_STYLE}]Fetching remote transcript timed out. Try again.[/]")
|
|
318
|
-
except Exception as exc: # pragma: no cover - unexpected API failure
|
|
319
|
-
self.console.print(f"[{ERROR_STYLE}]Error fetching run detail: {exc}[/]")
|
|
320
|
-
return None
|
|
321
|
-
|
|
322
|
-
def show_run_detail(self, agent_id: str, run_id: str) -> Any | None:
|
|
323
|
-
"""Show detailed run information with SSE events.
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
agent_id: UUID of the agent.
|
|
327
|
-
run_id: UUID of the run.
|
|
328
|
-
|
|
329
|
-
Returns:
|
|
330
|
-
RunWithOutput instance or None on error.
|
|
331
|
-
"""
|
|
332
|
-
client = self._get_client_or_fail()
|
|
333
|
-
if not client:
|
|
334
|
-
return None
|
|
335
|
-
|
|
336
|
-
run_detail = self._load_run_detail(client, agent_id, run_id)
|
|
337
|
-
if run_detail is None:
|
|
338
|
-
return None
|
|
339
|
-
|
|
340
|
-
self.console.print()
|
|
341
|
-
render_remote_sse_transcript(run_detail, self.console, show_metadata=True)
|
|
342
|
-
return run_detail
|
|
343
|
-
|
|
344
|
-
def export_remote_run(
|
|
345
|
-
self,
|
|
346
|
-
agent_id: str,
|
|
347
|
-
run_id: str,
|
|
348
|
-
client: Any,
|
|
349
|
-
detail: Any | None,
|
|
350
|
-
) -> bool:
|
|
351
|
-
"""Export the selected remote run to JSONL.
|
|
352
|
-
|
|
353
|
-
Args:
|
|
354
|
-
agent_id: UUID of the agent.
|
|
355
|
-
run_id: UUID of the run.
|
|
356
|
-
client: API client instance.
|
|
357
|
-
detail: Cached RunWithOutput instance or None.
|
|
358
|
-
"""
|
|
359
|
-
run_detail = detail or self._load_run_detail(client, agent_id, run_id)
|
|
360
|
-
if run_detail is None:
|
|
361
|
-
return False
|
|
362
|
-
|
|
363
|
-
destination = self._prompt_remote_export_path(run_id)
|
|
364
|
-
if destination is None:
|
|
365
|
-
self.console.print("[dim]Export cancelled.[/]")
|
|
366
|
-
return False
|
|
367
|
-
|
|
368
|
-
overwrite = False
|
|
369
|
-
if destination.exists():
|
|
370
|
-
if not click.confirm(f"{destination} already exists. Overwrite?", default=False):
|
|
371
|
-
self.console.print("[dim]Export cancelled.[/]")
|
|
372
|
-
return False
|
|
373
|
-
overwrite = True
|
|
374
|
-
|
|
375
|
-
try:
|
|
376
|
-
agent_label = self._resolve_agent_name()
|
|
377
|
-
exported = export_remote_transcript_jsonl(
|
|
378
|
-
run_detail,
|
|
379
|
-
destination,
|
|
380
|
-
overwrite=overwrite,
|
|
381
|
-
agent_name=agent_label,
|
|
382
|
-
)
|
|
383
|
-
self.console.print(f"[{SUCCESS_STYLE}]Remote transcript exported to {exported}[/]")
|
|
384
|
-
return True
|
|
385
|
-
except FileExistsError:
|
|
386
|
-
self.console.print(f"[{WARNING_STYLE}]File already exists and overwrite was disabled: {destination}[/]")
|
|
387
|
-
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
388
|
-
self.console.print(f"[{ERROR_STYLE}]Failed to export transcript: {exc}[/]")
|
|
389
|
-
return False
|
|
390
|
-
|
|
391
|
-
def _resolve_agent_name(self) -> str | None:
|
|
392
|
-
"""Return the friendly agent name for the active session if available."""
|
|
393
|
-
current_agent = getattr(self.session, "_current_agent", None)
|
|
394
|
-
if current_agent is None:
|
|
395
|
-
return None
|
|
396
|
-
return getattr(current_agent, "name", None) or getattr(current_agent, "display_name", None)
|
|
397
|
-
|
|
398
|
-
def _prompt_remote_export_path(self, run_id: str) -> Path | None:
|
|
399
|
-
"""Prompt the operator for an export destination.
|
|
400
|
-
|
|
401
|
-
Args:
|
|
402
|
-
run_id: UUID of the run.
|
|
403
|
-
|
|
404
|
-
Returns:
|
|
405
|
-
Path object or None if cancelled.
|
|
406
|
-
"""
|
|
407
|
-
# Default to current working directory for exports (user can override via prompt).
|
|
408
|
-
# This is safe as the user explicitly initiates the export operation.
|
|
409
|
-
default_path = Path.cwd() / f"run_{run_id}.jsonl" # noqa: S108
|
|
410
|
-
try:
|
|
411
|
-
result = self._handle_questionary_export_prompt(default_path)
|
|
412
|
-
if result is not None:
|
|
413
|
-
return result
|
|
414
|
-
return None
|
|
415
|
-
except RuntimeError:
|
|
416
|
-
pass
|
|
417
|
-
|
|
418
|
-
is_terminal = bool(getattr(self.console, "is_terminal", False))
|
|
419
|
-
if not is_terminal:
|
|
420
|
-
return default_path
|
|
421
|
-
|
|
422
|
-
return self._prompt_cli_export_choice(default_path)
|
|
423
|
-
|
|
424
|
-
def _handle_questionary_export_prompt(self, default_path: Path) -> Path | None:
|
|
425
|
-
"""Handle questionary-based export prompt with error handling.
|
|
426
|
-
|
|
427
|
-
Args:
|
|
428
|
-
default_path: Default export path.
|
|
429
|
-
|
|
430
|
-
Returns:
|
|
431
|
-
Selected path or None if cancelled.
|
|
432
|
-
|
|
433
|
-
Raises:
|
|
434
|
-
RuntimeError: If questionary prompt is unavailable or fails.
|
|
435
|
-
"""
|
|
436
|
-
selection = self._prompt_questionary_export_choice(default_path)
|
|
437
|
-
if selection is None:
|
|
438
|
-
return None
|
|
439
|
-
|
|
440
|
-
choice, _ = selection
|
|
441
|
-
if choice == "default":
|
|
442
|
-
return default_path
|
|
443
|
-
|
|
444
|
-
if choice == "custom":
|
|
445
|
-
return self._prompt_questionary_custom_destination(default_path)
|
|
446
|
-
|
|
447
|
-
# choice == "cancel" or any other value
|
|
448
|
-
return None
|
|
449
|
-
|
|
450
|
-
def _prompt_questionary_export_choice(self, default_path: Path) -> tuple[str, Path | None] | None:
|
|
451
|
-
"""Render the questionary export menu and return the selected action."""
|
|
452
|
-
display_path = self._format_export_display_path(default_path)
|
|
453
|
-
result = prompt_export_choice_questionary(default_path, display_path)
|
|
454
|
-
if result is None:
|
|
455
|
-
raise RuntimeError("Questionary prompt unavailable")
|
|
456
|
-
return result
|
|
457
|
-
|
|
458
|
-
def _prompt_questionary_custom_destination(self, default_path: Path) -> Path | None:
|
|
459
|
-
"""Prompt for a custom destination using questionary path picker."""
|
|
460
|
-
if questionary is None:
|
|
461
|
-
raise RuntimeError("Questionary prompt unavailable")
|
|
462
|
-
|
|
463
|
-
try:
|
|
464
|
-
prompt = questionary.path(
|
|
465
|
-
"Destination path (Tab to autocomplete):",
|
|
466
|
-
default="",
|
|
467
|
-
only_directories=False,
|
|
468
|
-
)
|
|
469
|
-
response = questionary_safe_ask(prompt)
|
|
470
|
-
except Exception as exc: # pragma: no cover - questionary failure
|
|
471
|
-
raise RuntimeError("Questionary path prompt failed") from exc
|
|
472
|
-
|
|
473
|
-
return self._resolve_export_path(response, default_path, allow_default=False)
|
|
474
|
-
|
|
475
|
-
def _prompt_cli_export_choice(self, default_path: Path) -> Path | None:
|
|
476
|
-
"""Render a click-based export menu when questionary isn't available."""
|
|
477
|
-
display_path = self._format_export_display_path(default_path)
|
|
478
|
-
self.console.print()
|
|
479
|
-
self.console.print("Remote export options:")
|
|
480
|
-
self.console.print(f" 1. Save to default ({display_path})")
|
|
481
|
-
self.console.print(" 2. Choose a different path")
|
|
482
|
-
self.console.print(" 3. Cancel")
|
|
483
|
-
try:
|
|
484
|
-
selection = click.prompt(
|
|
485
|
-
"Select an option",
|
|
486
|
-
type=click.Choice(["1", "2", "3"]),
|
|
487
|
-
default="1",
|
|
488
|
-
show_choices=False,
|
|
489
|
-
)
|
|
490
|
-
except (click.Abort, EOFError, KeyboardInterrupt):
|
|
491
|
-
return None
|
|
492
|
-
|
|
493
|
-
if selection == "1":
|
|
494
|
-
return default_path
|
|
495
|
-
if selection == "2":
|
|
496
|
-
return self._prompt_click_export_path(default_path)
|
|
497
|
-
return None
|
|
498
|
-
|
|
499
|
-
def _prompt_click_export_path(self, default_path: Path) -> Path | None:
|
|
500
|
-
"""Prompt for a custom export destination using click only."""
|
|
501
|
-
default_ref = self._format_export_display_path(default_path)
|
|
502
|
-
self.console.print(
|
|
503
|
-
f"[dim]Enter a custom destination path. Leave blank to cancel. Default reference: {default_ref}[/]"
|
|
504
|
-
)
|
|
505
|
-
try:
|
|
506
|
-
response = click.prompt(
|
|
507
|
-
"Custom path",
|
|
508
|
-
default="",
|
|
509
|
-
show_default=False,
|
|
510
|
-
)
|
|
511
|
-
except (click.Abort, EOFError, KeyboardInterrupt):
|
|
512
|
-
return None
|
|
513
|
-
|
|
514
|
-
return self._resolve_export_path(response, default_path, allow_default=False)
|
|
515
|
-
|
|
516
|
-
def _resolve_export_path(self, response: str | None, default_path: Path, *, allow_default: bool) -> Path | None:
|
|
517
|
-
"""Normalise export path input into a Path instance."""
|
|
518
|
-
value = (response or "").strip()
|
|
519
|
-
if not value:
|
|
520
|
-
return default_path if allow_default else None
|
|
521
|
-
|
|
522
|
-
candidate = Path(value).expanduser()
|
|
523
|
-
if not candidate.is_absolute():
|
|
524
|
-
# Resolve relative paths against current working directory.
|
|
525
|
-
# This is safe as the user explicitly provided this path via prompt.
|
|
526
|
-
candidate = Path.cwd() / candidate # noqa: S108
|
|
527
|
-
return candidate
|
|
528
|
-
|
|
529
|
-
def _format_export_display_path(self, path: Path) -> str:
|
|
530
|
-
"""Return a user-friendly string for default export paths."""
|
|
531
|
-
cwd = Path.cwd()
|
|
532
|
-
try:
|
|
533
|
-
relative = path.relative_to(cwd)
|
|
534
|
-
return str(Path(".") / relative)
|
|
535
|
-
except ValueError:
|
|
536
|
-
pass
|
|
537
|
-
|
|
538
|
-
home = Path.home()
|
|
539
|
-
try:
|
|
540
|
-
relative_home = path.relative_to(home)
|
|
541
|
-
suffix = f"/{relative_home}" if relative_home.parts else ""
|
|
542
|
-
return f"~{suffix}"
|
|
543
|
-
except ValueError:
|
|
544
|
-
pass
|
|
545
|
-
|
|
546
|
-
return str(path)
|
|
547
|
-
|
|
548
|
-
def _get_client_or_fail(self) -> Any:
|
|
549
|
-
"""Get client or handle failure and return None.
|
|
550
|
-
|
|
551
|
-
Returns:
|
|
552
|
-
API client instance or None on error.
|
|
553
|
-
"""
|
|
554
|
-
try:
|
|
555
|
-
return self.session._get_client()
|
|
556
|
-
except click.ClickException as exc:
|
|
557
|
-
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
558
|
-
return None
|
|
559
|
-
|
|
560
|
-
def _continue_session(self) -> bool:
|
|
561
|
-
"""Signal that the slash session should remain active.
|
|
562
|
-
|
|
563
|
-
Returns:
|
|
564
|
-
True to continue session.
|
|
565
|
-
"""
|
|
566
|
-
return not getattr(self.session, "_should_exit", False)
|