glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +44 -4
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1413 -0
- glaip_sdk/branding.py +126 -2
- glaip_sdk/cli/account_store.py +555 -0
- glaip_sdk/cli/auth.py +260 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +728 -113
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +12 -8
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/transcripts_original.py +756 -0
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +49 -4
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +41 -20
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +340 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +12 -13
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +580 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
- glaip_sdk/cli/slash/session.py +1105 -153
- glaip_sdk/cli/slash/tui/__init__.py +36 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/clipboard.py +195 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +80 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
- glaip_sdk/cli/slash/tui/terminal.py +407 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +66 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -463
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +227 -10
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +3 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +576 -44
- glaip_sdk/client/base.py +26 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -14
- glaip_sdk/client/mcps.py +165 -24
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +546 -92
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +206 -32
- glaip_sdk/config/constants.py +33 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +89 -0
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +136 -0
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +48 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +445 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +1055 -0
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +488 -0
- glaip_sdk/utils/__init__.py +59 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +403 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +524 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +299 -1434
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -33
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +25 -13
- glaip_sdk/utils/runtime_config.py +426 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +162 -0
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
- glaip_sdk-0.7.17.dist-info/RECORD +224 -0
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1369
- glaip_sdk/cli/commands/mcps.py +0 -1187
- glaip_sdk/cli/commands/tools.py +0 -584
- glaip_sdk/cli/utils.py +0 -1278
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.2.dist-info/RECORD +0 -82
- glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"""Transcript commands for inspecting cached agent transcripts.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Iterable, Sequence
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
from io import StringIO
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from glaip_sdk.branding import (
|
|
21
|
+
INFO_STYLE,
|
|
22
|
+
SUCCESS_STYLE,
|
|
23
|
+
WARNING_STYLE,
|
|
24
|
+
)
|
|
25
|
+
from glaip_sdk.cli.transcript.cache import (
|
|
26
|
+
export_transcript as export_cached_transcript,
|
|
27
|
+
)
|
|
28
|
+
from glaip_sdk.cli.transcript.history import (
|
|
29
|
+
ClearResult,
|
|
30
|
+
HistoryEntry,
|
|
31
|
+
HistorySnapshot,
|
|
32
|
+
clear_cached_runs,
|
|
33
|
+
coerce_sortable_datetime,
|
|
34
|
+
load_history_snapshot,
|
|
35
|
+
)
|
|
36
|
+
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
37
|
+
from glaip_sdk.cli.context import get_ctx_value
|
|
38
|
+
from glaip_sdk.cli.core.output import format_size, parse_json_line
|
|
39
|
+
from glaip_sdk.rich_components import AIPTable
|
|
40
|
+
from glaip_sdk.utils.rendering.layout.panels import create_final_panel
|
|
41
|
+
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
42
|
+
|
|
43
|
+
console = Console()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _format_duration(seconds: int | None) -> str:
|
|
47
|
+
"""Format elapsed seconds as HH:MM:SS."""
|
|
48
|
+
if seconds is None:
|
|
49
|
+
return "—"
|
|
50
|
+
seconds = int(max(0, seconds))
|
|
51
|
+
delta = timedelta(seconds=seconds)
|
|
52
|
+
total = int(delta.total_seconds())
|
|
53
|
+
hours, remainder = divmod(total, 3600)
|
|
54
|
+
minutes, secs = divmod(remainder, 60)
|
|
55
|
+
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _format_timestamp(value: datetime | None) -> str:
|
|
59
|
+
"""Render datetimes in UTC display format."""
|
|
60
|
+
if value is None:
|
|
61
|
+
return "—"
|
|
62
|
+
try:
|
|
63
|
+
dt = value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
64
|
+
return dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
65
|
+
except Exception:
|
|
66
|
+
return str(value)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _row_label(entry: HistoryEntry) -> str:
|
|
70
|
+
"""Build a run id label with warning markers."""
|
|
71
|
+
suffix = ""
|
|
72
|
+
if entry.warning:
|
|
73
|
+
suffix += " !"
|
|
74
|
+
return f"{entry.run_id}{suffix}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _should_use_transcript_viewer(ctx: click.Context | None, target_console: Console, *, force: bool = False) -> bool:
|
|
78
|
+
"""Return True if the interactive transcript viewer should be launched."""
|
|
79
|
+
if not target_console.is_terminal:
|
|
80
|
+
return False
|
|
81
|
+
if force:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
selected_view = get_ctx_value(ctx, "view", "rich") if ctx else "rich"
|
|
85
|
+
if selected_view != "rich":
|
|
86
|
+
return False
|
|
87
|
+
if ctx is not None and not bool(get_ctx_value(ctx, "tty", True)):
|
|
88
|
+
return False
|
|
89
|
+
try:
|
|
90
|
+
return bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
91
|
+
except Exception:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _coerce_timestamp_to_float(value: Any) -> float | None:
|
|
96
|
+
"""Convert assorted timestamp formats to epoch seconds."""
|
|
97
|
+
if value is None:
|
|
98
|
+
return None
|
|
99
|
+
if isinstance(value, (int, float)):
|
|
100
|
+
try:
|
|
101
|
+
return float(value)
|
|
102
|
+
except Exception:
|
|
103
|
+
return None
|
|
104
|
+
if isinstance(value, datetime):
|
|
105
|
+
try:
|
|
106
|
+
return value.timestamp()
|
|
107
|
+
except Exception:
|
|
108
|
+
return None
|
|
109
|
+
if isinstance(value, str):
|
|
110
|
+
try:
|
|
111
|
+
text = value.replace("Z", "+00:00")
|
|
112
|
+
parsed = datetime.fromisoformat(text)
|
|
113
|
+
return parsed.timestamp()
|
|
114
|
+
except ValueError:
|
|
115
|
+
return None
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _build_viewer_context(
|
|
120
|
+
entry: HistoryEntry, meta: dict[str, Any] | None, events: list[dict[str, Any]]
|
|
121
|
+
) -> ViewerContext:
|
|
122
|
+
"""Create a ViewerContext payload for the cached transcript."""
|
|
123
|
+
manifest_entry = dict(entry.manifest or {})
|
|
124
|
+
if entry.run_id and not manifest_entry.get("run_id"):
|
|
125
|
+
manifest_entry["run_id"] = entry.run_id
|
|
126
|
+
|
|
127
|
+
meta_payload = dict(meta or {})
|
|
128
|
+
default_output = str(meta_payload.get("default_output") or meta_payload.get("renderer_output") or "")
|
|
129
|
+
final_output = str(meta_payload.get("final_output") or "")
|
|
130
|
+
|
|
131
|
+
started_hint = meta_payload.get("started_at") or entry.started_at or entry.started_at_iso
|
|
132
|
+
stream_started_at = _coerce_timestamp_to_float(started_hint)
|
|
133
|
+
|
|
134
|
+
return ViewerContext(
|
|
135
|
+
manifest_entry=manifest_entry,
|
|
136
|
+
events=list(events),
|
|
137
|
+
default_output=default_output,
|
|
138
|
+
final_output=final_output,
|
|
139
|
+
stream_started_at=stream_started_at,
|
|
140
|
+
meta=meta_payload,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _launch_transcript_viewer(
|
|
145
|
+
entry: HistoryEntry,
|
|
146
|
+
meta: dict[str, Any] | None,
|
|
147
|
+
events: list[dict[str, Any]],
|
|
148
|
+
*,
|
|
149
|
+
console_override: Console | None = None,
|
|
150
|
+
initial_view: str = "default",
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""Launch the transcript viewer for a cached run."""
|
|
153
|
+
if not entry.run_id:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
target_console = console_override or console
|
|
157
|
+
viewer_ctx = _build_viewer_context(entry, meta, events)
|
|
158
|
+
|
|
159
|
+
def _export(destination: Path) -> Path:
|
|
160
|
+
"""Export cached transcript to destination.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
destination: Path to export transcript to.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Path to exported transcript file.
|
|
167
|
+
"""
|
|
168
|
+
return export_cached_transcript(destination=destination, run_id=entry.run_id)
|
|
169
|
+
|
|
170
|
+
run_viewer_session(target_console, viewer_ctx, _export, initial_view=initial_view)
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _maybe_launch_transcript_viewer(
|
|
175
|
+
ctx: click.Context | None,
|
|
176
|
+
entry: HistoryEntry,
|
|
177
|
+
meta: dict[str, Any] | None,
|
|
178
|
+
events: list[dict[str, Any]],
|
|
179
|
+
*,
|
|
180
|
+
console_override: Console | None = None,
|
|
181
|
+
force: bool = False,
|
|
182
|
+
initial_view: str = "default",
|
|
183
|
+
) -> bool:
|
|
184
|
+
"""Launch the transcript viewer when the environment supports it."""
|
|
185
|
+
target_console = console_override or console
|
|
186
|
+
if not _should_use_transcript_viewer(ctx, target_console, force=force):
|
|
187
|
+
return False
|
|
188
|
+
try:
|
|
189
|
+
_launch_transcript_viewer(
|
|
190
|
+
entry,
|
|
191
|
+
meta,
|
|
192
|
+
events,
|
|
193
|
+
console_override=target_console,
|
|
194
|
+
initial_view=initial_view,
|
|
195
|
+
)
|
|
196
|
+
return True
|
|
197
|
+
except Exception:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _build_table(entries: Iterable[HistoryEntry]) -> AIPTable:
|
|
202
|
+
"""Create the Rich table used by both CLI and slash history commands."""
|
|
203
|
+
table = AIPTable(title="Agent run cache", expand=True)
|
|
204
|
+
table.add_column("Run ID", style="bold")
|
|
205
|
+
table.add_column("Agent")
|
|
206
|
+
table.add_column("Agent ID")
|
|
207
|
+
table.add_column("API URL")
|
|
208
|
+
table.add_column("Started (UTC)")
|
|
209
|
+
table.add_column("Duration")
|
|
210
|
+
table.add_column("Size")
|
|
211
|
+
|
|
212
|
+
for entry in entries:
|
|
213
|
+
row_style = WARNING_STYLE if entry.warning else None
|
|
214
|
+
if entry.status == "cached":
|
|
215
|
+
size_value = entry.size_bytes or 0
|
|
216
|
+
size_text = format_size(size_value)
|
|
217
|
+
else:
|
|
218
|
+
size_text = "—"
|
|
219
|
+
table.add_row(
|
|
220
|
+
_row_label(entry),
|
|
221
|
+
entry.agent_name or "—",
|
|
222
|
+
entry.agent_id or "—",
|
|
223
|
+
entry.api_url or "—",
|
|
224
|
+
_format_timestamp(entry.started_at),
|
|
225
|
+
_format_duration(entry.duration_seconds),
|
|
226
|
+
size_text,
|
|
227
|
+
style=row_style,
|
|
228
|
+
)
|
|
229
|
+
return table
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _emit_warnings(snapshot: HistorySnapshot) -> None:
|
|
233
|
+
"""Print warning strings associated with a snapshot."""
|
|
234
|
+
for warning in snapshot.warnings:
|
|
235
|
+
console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _abbreviate_path(path: Path | None) -> str:
|
|
239
|
+
"""Return a cache path with the home directory abbreviated to `~`."""
|
|
240
|
+
if path is None:
|
|
241
|
+
return "—"
|
|
242
|
+
|
|
243
|
+
raw = str(path)
|
|
244
|
+
try:
|
|
245
|
+
home = Path.home()
|
|
246
|
+
home_str = str(home)
|
|
247
|
+
except Exception:
|
|
248
|
+
return raw
|
|
249
|
+
|
|
250
|
+
if home_str and raw.startswith(home_str):
|
|
251
|
+
suffix = raw[len(home_str) :]
|
|
252
|
+
if suffix.startswith("/"):
|
|
253
|
+
suffix = suffix[1:]
|
|
254
|
+
return f"~/{suffix}" if suffix else "~"
|
|
255
|
+
return raw
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _parse_transcript_line(raw_line: str) -> dict[str, Any] | None:
|
|
259
|
+
"""Parse a JSONL transcript line into a dictionary payload."""
|
|
260
|
+
return parse_json_line(raw_line)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _decode_transcript(contents: str) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
|
|
264
|
+
"""Decode transcript JSONL contents into meta and event payloads."""
|
|
265
|
+
meta: dict[str, Any] | None = None
|
|
266
|
+
events: list[dict[str, Any]] = []
|
|
267
|
+
|
|
268
|
+
for payload in filter(None, (_parse_transcript_line(line) for line in contents.splitlines())):
|
|
269
|
+
kind = payload.get("type")
|
|
270
|
+
if kind == "meta" and meta is None:
|
|
271
|
+
meta = payload
|
|
272
|
+
continue
|
|
273
|
+
if kind == "event":
|
|
274
|
+
event = payload.get("event")
|
|
275
|
+
if isinstance(event, dict):
|
|
276
|
+
events.append(event)
|
|
277
|
+
|
|
278
|
+
return meta, events
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _render_transcript_display(
|
|
282
|
+
entry: HistoryEntry,
|
|
283
|
+
manifest_path: Path,
|
|
284
|
+
transcript_path: Path,
|
|
285
|
+
meta: dict[str, Any] | None,
|
|
286
|
+
events: list[dict[str, Any]],
|
|
287
|
+
) -> str:
|
|
288
|
+
"""Return a Rich-formatted transcript stream similar to transcript mode."""
|
|
289
|
+
buffer = StringIO()
|
|
290
|
+
width = console.width or 120
|
|
291
|
+
view_console = Console(
|
|
292
|
+
file=buffer,
|
|
293
|
+
force_terminal=True,
|
|
294
|
+
color_system=console.color_system,
|
|
295
|
+
width=width,
|
|
296
|
+
soft_wrap=True,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
header = (
|
|
300
|
+
f"[dim]Manifest: {manifest_path} · {entry.run_id or '—'} · "
|
|
301
|
+
f"{_abbreviate_path(transcript_path)} · {len(events)} events[/]"
|
|
302
|
+
)
|
|
303
|
+
view_console.print(header)
|
|
304
|
+
view_console.print()
|
|
305
|
+
|
|
306
|
+
final_text = None
|
|
307
|
+
if meta:
|
|
308
|
+
final_text = meta.get("final_output") or meta.get("default_output")
|
|
309
|
+
if final_text:
|
|
310
|
+
view_console.print(create_final_panel(final_text, title="Final Result", theme="dark"))
|
|
311
|
+
view_console.print()
|
|
312
|
+
|
|
313
|
+
view_console.print("[bold]Transcript Events[/bold]")
|
|
314
|
+
if not events:
|
|
315
|
+
view_console.print("[dim]No SSE events were captured for this run.[/dim]")
|
|
316
|
+
else:
|
|
317
|
+
view_console.print("[dim]────────────────────────────────────────────────────────[/dim]")
|
|
318
|
+
baseline: datetime | None = None
|
|
319
|
+
for event in events:
|
|
320
|
+
received = _parse_event_received_timestamp(event)
|
|
321
|
+
if baseline is None and received is not None:
|
|
322
|
+
baseline = received
|
|
323
|
+
render_debug_event(event, view_console, received_ts=received, baseline_ts=baseline)
|
|
324
|
+
view_console.print()
|
|
325
|
+
|
|
326
|
+
return buffer.getvalue()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _render_transcript_jsonl(
|
|
330
|
+
entry: HistoryEntry,
|
|
331
|
+
manifest_path: Path,
|
|
332
|
+
transcript_path: Path,
|
|
333
|
+
contents: str,
|
|
334
|
+
) -> str:
|
|
335
|
+
"""Return a plain-text transcript stream that mirrors the cached JSONL payload."""
|
|
336
|
+
header = f"Manifest: {manifest_path} · {entry.run_id or '—'} · {_abbreviate_path(transcript_path)}"
|
|
337
|
+
normalized = contents if contents.endswith("\n") else contents + "\n"
|
|
338
|
+
return f"{header}\n{normalized}"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _parse_event_received_timestamp(event: dict[str, Any]) -> datetime | None:
|
|
342
|
+
"""Extract received timestamp metadata from an SSE event."""
|
|
343
|
+
metadata = event.get("metadata") or {}
|
|
344
|
+
ts_value = metadata.get("received_at") or event.get("received_at")
|
|
345
|
+
if not ts_value:
|
|
346
|
+
return None
|
|
347
|
+
if isinstance(ts_value, datetime):
|
|
348
|
+
return ts_value if ts_value.tzinfo else ts_value.replace(tzinfo=timezone.utc)
|
|
349
|
+
if isinstance(ts_value, str):
|
|
350
|
+
try:
|
|
351
|
+
text = ts_value.replace("Z", "+00:00")
|
|
352
|
+
parsed = datetime.fromisoformat(text)
|
|
353
|
+
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
|
354
|
+
except ValueError:
|
|
355
|
+
return None
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _resolve_transcript_path(entry: HistoryEntry) -> Path:
|
|
360
|
+
"""Locate the cached transcript for a manifest entry or raise a helpful error."""
|
|
361
|
+
target = entry.resolved_path or entry.expected_path
|
|
362
|
+
if target is None:
|
|
363
|
+
raise click.ClickException(
|
|
364
|
+
f"Manifest entry for run {entry.run_id or '?'} does not include a transcript filename."
|
|
365
|
+
)
|
|
366
|
+
if not target.exists():
|
|
367
|
+
run_label = entry.run_id or "?"
|
|
368
|
+
hint = entry.run_id or "<RUN_ID>"
|
|
369
|
+
location = _abbreviate_path(target)
|
|
370
|
+
raise click.ClickException(
|
|
371
|
+
f"Transcript file missing for run {run_label} (expected {location}). "
|
|
372
|
+
f"Run `aip transcripts clear --id {hint}` to reconcile the manifest."
|
|
373
|
+
)
|
|
374
|
+
return target
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _load_transcript_text(entry: HistoryEntry) -> tuple[Path, str]:
|
|
378
|
+
"""Read the cached transcript file into memory."""
|
|
379
|
+
path = _resolve_transcript_path(entry)
|
|
380
|
+
try:
|
|
381
|
+
contents = path.read_text(encoding="utf-8")
|
|
382
|
+
except FileNotFoundError:
|
|
383
|
+
raise click.ClickException(
|
|
384
|
+
f"Transcript file missing for run {entry.run_id or '?'} (expected {path})."
|
|
385
|
+
) from None
|
|
386
|
+
except OSError as exc: # Permission problems, etc.
|
|
387
|
+
raise click.ClickException(f"Failed to read cached transcript {path}: {exc}") from exc
|
|
388
|
+
return path, contents
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _transcripts_payload(snapshot: HistorySnapshot) -> dict:
|
|
392
|
+
"""Convert a snapshot into the JSON payload returned by `aip transcripts --json`."""
|
|
393
|
+
rows = []
|
|
394
|
+
for entry in snapshot.entries:
|
|
395
|
+
row = {
|
|
396
|
+
"run_id": entry.run_id,
|
|
397
|
+
"started_at": entry.started_at_iso,
|
|
398
|
+
"finished_at": entry.finished_at_iso,
|
|
399
|
+
"agent_name": entry.agent_name,
|
|
400
|
+
"agent_id": entry.agent_id,
|
|
401
|
+
"api_url": entry.api_url,
|
|
402
|
+
"duration_seconds": entry.duration_seconds,
|
|
403
|
+
"size_bytes": entry.size_bytes,
|
|
404
|
+
"status": entry.status,
|
|
405
|
+
"warning": entry.warning,
|
|
406
|
+
}
|
|
407
|
+
rows.append(row)
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
"manifest_path": str(snapshot.manifest_path),
|
|
411
|
+
"limit_requested": snapshot.limit_requested,
|
|
412
|
+
"limit_applied": snapshot.limit_applied,
|
|
413
|
+
"limit_clamped": snapshot.limit_clamped,
|
|
414
|
+
"total_entries": snapshot.total_entries,
|
|
415
|
+
"cached_entries": snapshot.cached_entries,
|
|
416
|
+
"total_size_bytes": snapshot.total_size_bytes,
|
|
417
|
+
"warnings": list(snapshot.warnings),
|
|
418
|
+
"migration_summary": snapshot.migration_summary,
|
|
419
|
+
"rows": rows,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _print_snapshot(snapshot: HistorySnapshot) -> None:
|
|
424
|
+
"""Render the textual history view for the standard CLI."""
|
|
425
|
+
if snapshot.cached_entries == 0:
|
|
426
|
+
console.print(f"[{WARNING_STYLE}]No cached transcripts found. Try running an agent first.[/]")
|
|
427
|
+
console.print(f"[dim]Manifest: {snapshot.manifest_path}[/]")
|
|
428
|
+
if snapshot.total_entries and snapshot.warnings:
|
|
429
|
+
_emit_warnings(snapshot)
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
header = (
|
|
433
|
+
f"[dim]Manifest: {snapshot.manifest_path} · {snapshot.total_entries} runs · "
|
|
434
|
+
f"{format_size(snapshot.total_size_bytes)} used"
|
|
435
|
+
)
|
|
436
|
+
if snapshot.limit_applied and snapshot.total_entries > snapshot.limit_applied:
|
|
437
|
+
header += (
|
|
438
|
+
f" · showing {len(snapshot.entries)} of {snapshot.total_entries} runs (limit={snapshot.limit_applied})"
|
|
439
|
+
)
|
|
440
|
+
console.print(header)
|
|
441
|
+
|
|
442
|
+
if snapshot.limit_clamped:
|
|
443
|
+
console.print(
|
|
444
|
+
f"[{WARNING_STYLE}]Requested limit exceeded maximum. Showing first {snapshot.limit_applied} runs.[/]"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if snapshot.migration_summary:
|
|
448
|
+
console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
449
|
+
|
|
450
|
+
_emit_warnings(snapshot)
|
|
451
|
+
|
|
452
|
+
table = _build_table(snapshot.entries)
|
|
453
|
+
console.print(table)
|
|
454
|
+
console.print("[dim]! Missing transcript[/]")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _render_detail_view(ctx: click.Context | None, snapshot: HistorySnapshot, run_id: str) -> None:
|
|
458
|
+
"""Render the cached transcript for a specific run."""
|
|
459
|
+
entry = snapshot.index.get(run_id)
|
|
460
|
+
if entry is None:
|
|
461
|
+
raise click.ClickException(f"Run id {run_id} was not found in {snapshot.manifest_path}.")
|
|
462
|
+
|
|
463
|
+
path, contents = _load_transcript_text(entry)
|
|
464
|
+
|
|
465
|
+
meta, events = _decode_transcript(contents)
|
|
466
|
+
if _maybe_launch_transcript_viewer(ctx, entry, meta, events, force=True, initial_view="transcript"):
|
|
467
|
+
if snapshot.migration_summary:
|
|
468
|
+
console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
469
|
+
_emit_warnings(snapshot)
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
if snapshot.migration_summary:
|
|
473
|
+
console.print(f"[{INFO_STYLE}]{snapshot.migration_summary}[/]")
|
|
474
|
+
_emit_warnings(snapshot)
|
|
475
|
+
transcript_view = _render_transcript_jsonl(entry, snapshot.manifest_path, path, contents)
|
|
476
|
+
click.echo_via_pager(transcript_view)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _render_history_overview(snapshot: HistorySnapshot, emit_json: bool) -> None:
|
|
480
|
+
"""Render the standard history table or its JSON payload."""
|
|
481
|
+
if emit_json:
|
|
482
|
+
payload = _transcripts_payload(snapshot)
|
|
483
|
+
click.echo(json.dumps(payload, indent=2, default=str))
|
|
484
|
+
for warning in snapshot.warnings:
|
|
485
|
+
click.echo(warning, err=True)
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
_print_snapshot(snapshot)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@click.group("transcripts", invoke_without_command=True)
|
|
492
|
+
@click.option("--limit", type=int, help="Maximum runs to display (default 10).")
|
|
493
|
+
@click.option("--json", "as_json", is_flag=True, help="Return machine-friendly JSON output.")
|
|
494
|
+
@click.option("--detail", "detail_run_id", metavar="RUN_ID", help="Show cached transcript details for a run id.")
|
|
495
|
+
@click.pass_context
|
|
496
|
+
def transcripts_group(ctx: click.Context, limit: int | None, as_json: bool, detail_run_id: str | None) -> None:
|
|
497
|
+
"""Inspect and manage cached agent transcripts."""
|
|
498
|
+
if ctx.invoked_subcommand or ctx.resilient_parsing:
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
snapshot = load_history_snapshot(limit=limit, ctx=ctx)
|
|
502
|
+
|
|
503
|
+
view = None
|
|
504
|
+
ctx_obj = ctx.obj if isinstance(ctx.obj, dict) else {}
|
|
505
|
+
if ctx_obj:
|
|
506
|
+
view = ctx_obj.get("view")
|
|
507
|
+
|
|
508
|
+
emit_json = as_json or view == "json"
|
|
509
|
+
|
|
510
|
+
if detail_run_id:
|
|
511
|
+
if emit_json:
|
|
512
|
+
raise click.UsageError("--json output is only available for the history table view.")
|
|
513
|
+
_render_detail_view(ctx, snapshot, detail_run_id)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
_render_history_overview(snapshot, emit_json)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@transcripts_group.command("detail")
|
|
520
|
+
@click.argument("run_id")
|
|
521
|
+
@click.pass_context
|
|
522
|
+
def transcripts_detail(ctx: click.Context, run_id: str) -> None:
|
|
523
|
+
"""Show cached transcript details for a specific run id."""
|
|
524
|
+
snapshot = load_history_snapshot(ctx=ctx)
|
|
525
|
+
view = ctx.obj.get("view") if isinstance(ctx.obj, dict) else None
|
|
526
|
+
if view == "json":
|
|
527
|
+
raise click.UsageError("`aip transcripts detail` only supports the default view.")
|
|
528
|
+
_render_detail_view(ctx, snapshot, run_id)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _collect_targets(
|
|
532
|
+
snapshot: HistorySnapshot,
|
|
533
|
+
run_ids: Sequence[str] | None,
|
|
534
|
+
delete_all: bool,
|
|
535
|
+
) -> tuple[list[HistoryEntry], list[str]]:
|
|
536
|
+
"""Return the HistoryEntry objects that should be deleted plus any missing ids."""
|
|
537
|
+
if delete_all:
|
|
538
|
+
runs = sorted(
|
|
539
|
+
snapshot.index.values(),
|
|
540
|
+
key=lambda entry: coerce_sortable_datetime(entry.started_at),
|
|
541
|
+
reverse=False,
|
|
542
|
+
)
|
|
543
|
+
return runs, []
|
|
544
|
+
|
|
545
|
+
ordered: list[str] = []
|
|
546
|
+
seen: set[str] = set()
|
|
547
|
+
for run_id in run_ids or ():
|
|
548
|
+
if run_id in seen:
|
|
549
|
+
continue
|
|
550
|
+
seen.add(run_id)
|
|
551
|
+
ordered.append(run_id)
|
|
552
|
+
|
|
553
|
+
found: list[HistoryEntry] = []
|
|
554
|
+
missing: list[str] = []
|
|
555
|
+
for run_id in ordered:
|
|
556
|
+
entry = snapshot.index.get(run_id)
|
|
557
|
+
if entry is None:
|
|
558
|
+
missing.append(run_id)
|
|
559
|
+
else:
|
|
560
|
+
found.append(entry)
|
|
561
|
+
return found, missing
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _build_deletion_preview_payload(entries: Iterable[HistoryEntry]) -> list[dict[str, Any]]:
|
|
565
|
+
"""Build the payload list for deletion preview."""
|
|
566
|
+
payload = []
|
|
567
|
+
for entry in entries:
|
|
568
|
+
size_text = format_size(entry.size_bytes or 0) if entry.status == "cached" else "—"
|
|
569
|
+
status_text = "Missing file" if entry.warning else "Cached"
|
|
570
|
+
payload.append(
|
|
571
|
+
{
|
|
572
|
+
"run_id": entry.run_id,
|
|
573
|
+
"agent_name": entry.agent_name or "—",
|
|
574
|
+
"agent_id": entry.agent_id or "—",
|
|
575
|
+
"started_at": _format_timestamp(entry.started_at),
|
|
576
|
+
"size": size_text,
|
|
577
|
+
"status": status_text,
|
|
578
|
+
}
|
|
579
|
+
)
|
|
580
|
+
return payload
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _format_timestamp_display(timestamp_raw: Any) -> str:
|
|
584
|
+
"""Format timestamp for display in deletion preview."""
|
|
585
|
+
if timestamp_raw in (None, "—"):
|
|
586
|
+
return "—"
|
|
587
|
+
try:
|
|
588
|
+
timestamp_value = str(timestamp_raw).strip()
|
|
589
|
+
except Exception:
|
|
590
|
+
timestamp_value = str(timestamp_raw)
|
|
591
|
+
return f"{timestamp_value} UTC" if timestamp_value else "—"
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _render_deletion_preview_rich(payload: list[dict[str, Any]], manifest_path: Path) -> None:
|
|
595
|
+
"""Render deletion preview in rich format."""
|
|
596
|
+
console.print("Transcripts slated for deletion:")
|
|
597
|
+
console.print(f"[dim]Manifest: {_abbreviate_path(manifest_path)}[/]")
|
|
598
|
+
for row in payload:
|
|
599
|
+
timestamp_display = _format_timestamp_display(row["started_at"])
|
|
600
|
+
status_suffix = " (file missing)" if row["status"] == "Missing file" else ""
|
|
601
|
+
console.print(
|
|
602
|
+
f" • {row['run_id'] or '—'} {row['agent_name'] or '—'} {timestamp_display} {row['size']}{status_suffix}"
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _render_deletion_preview(
|
|
607
|
+
ctx: click.Context,
|
|
608
|
+
entries: Iterable[HistoryEntry],
|
|
609
|
+
manifest_path: Path,
|
|
610
|
+
*,
|
|
611
|
+
delete_all: bool,
|
|
612
|
+
reclaimed_hint: int,
|
|
613
|
+
) -> None:
|
|
614
|
+
"""Display a preview of the transcripts that are about to be purged."""
|
|
615
|
+
entry_list = list(entries)
|
|
616
|
+
view = get_ctx_value(ctx, "view", "rich")
|
|
617
|
+
|
|
618
|
+
if delete_all:
|
|
619
|
+
summary = {
|
|
620
|
+
"manifest_path": str(manifest_path),
|
|
621
|
+
"delete_all": True,
|
|
622
|
+
"entry_count": len(entry_list),
|
|
623
|
+
"estimated_reclaimed_bytes": reclaimed_hint,
|
|
624
|
+
}
|
|
625
|
+
if view == "json":
|
|
626
|
+
click.echo(json.dumps(summary, indent=2, default=str))
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
console.print("Transcripts slated for deletion:")
|
|
630
|
+
console.print(f"[dim]Manifest: {_abbreviate_path(manifest_path)}[/]")
|
|
631
|
+
console.print(
|
|
632
|
+
f"[{WARNING_STYLE}]This will remove ALL cached transcripts ({len(entry_list)} entries, "
|
|
633
|
+
f"{format_size(reclaimed_hint)} reclaimed).[/]"
|
|
634
|
+
)
|
|
635
|
+
console.print("[dim]Use `aip transcripts clear --id <run_id>` to delete specific runs.[/]")
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
payload = _build_deletion_preview_payload(entry_list)
|
|
639
|
+
preview = {"manifest_path": str(manifest_path), "transcripts": payload}
|
|
640
|
+
if view == "json":
|
|
641
|
+
click.echo(json.dumps(preview, indent=2, default=str))
|
|
642
|
+
else:
|
|
643
|
+
_render_deletion_preview_rich(payload, manifest_path)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _confirm_deletion(
|
|
647
|
+
ctx: click.Context,
|
|
648
|
+
entries: list[HistoryEntry],
|
|
649
|
+
reclaimed_hint: int,
|
|
650
|
+
delete_all: bool,
|
|
651
|
+
skip_prompt: bool,
|
|
652
|
+
manifest_path: Path,
|
|
653
|
+
) -> bool:
|
|
654
|
+
"""Prompt the user for confirmation before deleting transcripts."""
|
|
655
|
+
if skip_prompt:
|
|
656
|
+
return True
|
|
657
|
+
|
|
658
|
+
size_text = format_size(reclaimed_hint)
|
|
659
|
+
if delete_all:
|
|
660
|
+
console.print(
|
|
661
|
+
f"[{WARNING_STYLE}]Deleting ALL cached transcripts ({len(entries)} entries, {size_text} reclaimed).[/]"
|
|
662
|
+
)
|
|
663
|
+
else:
|
|
664
|
+
console.print(
|
|
665
|
+
f"[{WARNING_STYLE}]You are about to delete {len(entries)} cached transcript(s) ({size_text} reclaimed).[/]"
|
|
666
|
+
)
|
|
667
|
+
_render_deletion_preview(
|
|
668
|
+
ctx,
|
|
669
|
+
entries,
|
|
670
|
+
manifest_path,
|
|
671
|
+
delete_all=delete_all,
|
|
672
|
+
reclaimed_hint=reclaimed_hint,
|
|
673
|
+
)
|
|
674
|
+
return click.confirm("Proceed?", default=False)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _handle_clear_result(result: ClearResult) -> None:
|
|
678
|
+
"""Summarise the result of a cache sweep."""
|
|
679
|
+
removed_count = len(result.removed_entries)
|
|
680
|
+
reclaimed_text = format_size(result.reclaimed_bytes)
|
|
681
|
+
console.print(f"[{SUCCESS_STYLE}]Deleted {removed_count} transcript(s), reclaimed {reclaimed_text}.[/]")
|
|
682
|
+
if result.not_found:
|
|
683
|
+
console.print(f"[{WARNING_STYLE}]The following run id(s) were not found: {', '.join(result.not_found)}[/]")
|
|
684
|
+
for warning in result.warnings:
|
|
685
|
+
console.print(f"[{WARNING_STYLE}]{warning}[/]")
|
|
686
|
+
if result.cache_empty:
|
|
687
|
+
console.print(f"[{SUCCESS_STYLE}]Cache folder now clean. Future runs will repopulate history.[/]")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _validate_clear_options(run_ids: tuple[str, ...], delete_all: bool) -> None:
|
|
691
|
+
"""Ensure --all/--id input combinations are valid."""
|
|
692
|
+
if delete_all and run_ids:
|
|
693
|
+
raise click.UsageError("Use either --all or --id, not both.")
|
|
694
|
+
if not delete_all and not run_ids:
|
|
695
|
+
raise click.UsageError("Specify --all to delete everything or provide at least one --id.")
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _should_exit_for_targets(
|
|
699
|
+
*,
|
|
700
|
+
delete_all: bool,
|
|
701
|
+
targets: list[HistoryEntry],
|
|
702
|
+
missing: list[str],
|
|
703
|
+
) -> bool:
|
|
704
|
+
"""Return True when deletion should stop due to empty or invalid selections."""
|
|
705
|
+
if delete_all and not targets:
|
|
706
|
+
console.print(f"[{WARNING_STYLE}]Cache is already empty.[/]")
|
|
707
|
+
return True
|
|
708
|
+
if not delete_all and not targets:
|
|
709
|
+
console.print(f"[{WARNING_STYLE}]No matching transcript ids were found.[/]")
|
|
710
|
+
if missing:
|
|
711
|
+
console.print(f"[{WARNING_STYLE}]Unknown run ids: {', '.join(missing)}[/]")
|
|
712
|
+
return True
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@transcripts_group.command("clear")
|
|
717
|
+
@click.argument("run_ids_args", nargs=-1)
|
|
718
|
+
@click.option("--id", "run_ids", multiple=True, help="Run ID to delete (repeatable).")
|
|
719
|
+
@click.option("--all", "delete_all", is_flag=True, help="Delete all cached transcripts.")
|
|
720
|
+
@click.option("--yes", "assume_yes", is_flag=True, help="Skip confirmation prompt.")
|
|
721
|
+
@click.pass_context
|
|
722
|
+
def transcripts_clear(
|
|
723
|
+
ctx: click.Context,
|
|
724
|
+
run_ids_args: tuple[str, ...],
|
|
725
|
+
run_ids: tuple[str, ...],
|
|
726
|
+
delete_all: bool,
|
|
727
|
+
assume_yes: bool,
|
|
728
|
+
) -> None:
|
|
729
|
+
"""Delete cached transcript files by run id or sweep the entire cache."""
|
|
730
|
+
identifiers = tuple(list(run_ids) + list(run_ids_args))
|
|
731
|
+
|
|
732
|
+
_validate_clear_options(identifiers, delete_all)
|
|
733
|
+
snapshot = load_history_snapshot(ctx=ctx)
|
|
734
|
+
|
|
735
|
+
targets, missing = _collect_targets(snapshot, identifiers, delete_all)
|
|
736
|
+
if _should_exit_for_targets(delete_all=delete_all, targets=targets, missing=missing):
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
total_estimated_bytes = sum(entry.size_bytes or 0 for entry in targets)
|
|
740
|
+
|
|
741
|
+
if missing:
|
|
742
|
+
console.print(f"[{WARNING_STYLE}]Unknown run ids: {', '.join(missing)}[/]")
|
|
743
|
+
|
|
744
|
+
if not _confirm_deletion(
|
|
745
|
+
ctx,
|
|
746
|
+
targets,
|
|
747
|
+
total_estimated_bytes,
|
|
748
|
+
delete_all,
|
|
749
|
+
assume_yes,
|
|
750
|
+
snapshot.manifest_path,
|
|
751
|
+
):
|
|
752
|
+
console.print("[dim]Aborted. Cache unchanged.[/]")
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
result = clear_cached_runs(None if delete_all else list(identifiers))
|
|
756
|
+
_handle_clear_result(result)
|