@openlucaskaka/kagent 0.1.7
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.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -0,0 +1,2007 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Tuple
|
|
8
|
+
from urllib.parse import parse_qs
|
|
9
|
+
|
|
10
|
+
from kagent.runtime import RUNTIME_TRACE_TYPE
|
|
11
|
+
from kagent.runtime.steps import derive_runtime_steps
|
|
12
|
+
from kagent.service import errors as service_errors
|
|
13
|
+
from kagent.service.errors import failure_payload
|
|
14
|
+
from kagent.service.runtime import ServiceConfig
|
|
15
|
+
from kagent.service.safety import safe_trace_file_stem
|
|
16
|
+
from kagent.service.trace_store import load_trace_by_run_id
|
|
17
|
+
from kagent.utils.json_output import json_ready
|
|
18
|
+
|
|
19
|
+
_TRACE_READ_ERRORS = (OSError, ValueError)
|
|
20
|
+
_RUNTIME_STATUS_FILTER_VALUES = {
|
|
21
|
+
"cancelled",
|
|
22
|
+
"done",
|
|
23
|
+
"failed",
|
|
24
|
+
"requires_approval",
|
|
25
|
+
"resumed",
|
|
26
|
+
"resuming",
|
|
27
|
+
"running",
|
|
28
|
+
}
|
|
29
|
+
_RUNTIME_LIFECYCLE_STATE_FILTER_VALUES = {
|
|
30
|
+
"cancelled",
|
|
31
|
+
"failed",
|
|
32
|
+
"planning",
|
|
33
|
+
"running",
|
|
34
|
+
"succeeded",
|
|
35
|
+
"unknown",
|
|
36
|
+
"waiting_approval",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def execute_runtime_status_request(
|
|
41
|
+
run_id: str,
|
|
42
|
+
service_config: ServiceConfig,
|
|
43
|
+
*,
|
|
44
|
+
request_auth_subject: str = "",
|
|
45
|
+
request_auth_is_admin: bool = False,
|
|
46
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
47
|
+
if not service_config.trace_dir:
|
|
48
|
+
return 400, failure_payload(
|
|
49
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
50
|
+
"trace_dir is required for runtime status",
|
|
51
|
+
)
|
|
52
|
+
if not run_id.strip():
|
|
53
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, "run_id is required")
|
|
54
|
+
try:
|
|
55
|
+
trace = load_trace_by_run_id(run_id, service_config.trace_dir)
|
|
56
|
+
except _TRACE_READ_ERRORS:
|
|
57
|
+
return 500, failure_payload(
|
|
58
|
+
service_errors.TRACE_READ_FAILED,
|
|
59
|
+
"runtime run trace could not be read",
|
|
60
|
+
)
|
|
61
|
+
if trace is None or not is_runtime_trace(trace):
|
|
62
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
63
|
+
if not _runtime_trace_visible(trace, request_auth_subject, request_auth_is_admin):
|
|
64
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
65
|
+
return 200, json_ready(runtime_status_summary(trace, service_config.trace_dir, run_id))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def execute_runtime_list_request(
|
|
69
|
+
query: str,
|
|
70
|
+
service_config: ServiceConfig,
|
|
71
|
+
*,
|
|
72
|
+
request_auth_subject: str = "",
|
|
73
|
+
request_auth_is_admin: bool = False,
|
|
74
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
75
|
+
if not service_config.trace_dir:
|
|
76
|
+
return 400, failure_payload(
|
|
77
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
78
|
+
"trace_dir is required for runtime run listing",
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
limit = _runtime_list_limit(query)
|
|
82
|
+
cursor_key = _runtime_list_cursor_key(query)
|
|
83
|
+
filters = _runtime_list_filters(query)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, str(exc))
|
|
86
|
+
trace_dir = Path(service_config.trace_dir)
|
|
87
|
+
summaries = []
|
|
88
|
+
next_cursor = ""
|
|
89
|
+
has_more = False
|
|
90
|
+
for trace_path in sorted(
|
|
91
|
+
trace_dir.glob("*.json"),
|
|
92
|
+
key=_trace_file_sort_key,
|
|
93
|
+
reverse=True,
|
|
94
|
+
):
|
|
95
|
+
sort_key = _trace_file_sort_key(trace_path)
|
|
96
|
+
if cursor_key is not None and sort_key >= cursor_key:
|
|
97
|
+
continue
|
|
98
|
+
try:
|
|
99
|
+
trace = load_trace_by_run_id(trace_path.stem, service_config.trace_dir)
|
|
100
|
+
except _TRACE_READ_ERRORS:
|
|
101
|
+
continue
|
|
102
|
+
if trace is None or not is_runtime_trace(trace):
|
|
103
|
+
continue
|
|
104
|
+
summary = runtime_status_summary(trace, service_config.trace_dir, trace_path.stem)
|
|
105
|
+
if not _runtime_summary_visible(
|
|
106
|
+
summary,
|
|
107
|
+
request_auth_subject,
|
|
108
|
+
request_auth_is_admin,
|
|
109
|
+
):
|
|
110
|
+
continue
|
|
111
|
+
if not _runtime_summary_matches_filters(summary, filters):
|
|
112
|
+
continue
|
|
113
|
+
if len(summaries) >= limit:
|
|
114
|
+
has_more = True
|
|
115
|
+
break
|
|
116
|
+
summaries.append(_runtime_list_summary(summary))
|
|
117
|
+
next_cursor = _encode_runtime_list_cursor(sort_key)
|
|
118
|
+
if not has_more:
|
|
119
|
+
next_cursor = ""
|
|
120
|
+
return 200, json_ready(
|
|
121
|
+
{
|
|
122
|
+
"runs": summaries,
|
|
123
|
+
"count": str(len(summaries)),
|
|
124
|
+
"next_cursor": next_cursor,
|
|
125
|
+
"has_more": str(has_more).lower(),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def execute_runtime_summary_request(
|
|
131
|
+
query: str,
|
|
132
|
+
service_config: ServiceConfig,
|
|
133
|
+
*,
|
|
134
|
+
request_auth_subject: str = "",
|
|
135
|
+
request_auth_is_admin: bool = False,
|
|
136
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
137
|
+
if not service_config.trace_dir:
|
|
138
|
+
return 400, failure_payload(
|
|
139
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
140
|
+
"trace_dir is required for runtime run summary",
|
|
141
|
+
)
|
|
142
|
+
try:
|
|
143
|
+
filters = _runtime_list_filters(query)
|
|
144
|
+
except ValueError as exc:
|
|
145
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, str(exc))
|
|
146
|
+
aggregate = _empty_runtime_summary_aggregate()
|
|
147
|
+
trace_dir = Path(service_config.trace_dir)
|
|
148
|
+
for trace_path in sorted(
|
|
149
|
+
trace_dir.glob("*.json"),
|
|
150
|
+
key=_trace_file_sort_key,
|
|
151
|
+
reverse=True,
|
|
152
|
+
):
|
|
153
|
+
try:
|
|
154
|
+
trace = load_trace_by_run_id(trace_path.stem, service_config.trace_dir)
|
|
155
|
+
except _TRACE_READ_ERRORS:
|
|
156
|
+
continue
|
|
157
|
+
if trace is None or not is_runtime_trace(trace):
|
|
158
|
+
continue
|
|
159
|
+
summary = runtime_status_summary(trace, service_config.trace_dir, trace_path.stem)
|
|
160
|
+
if not _runtime_summary_visible(
|
|
161
|
+
summary,
|
|
162
|
+
request_auth_subject,
|
|
163
|
+
request_auth_is_admin,
|
|
164
|
+
):
|
|
165
|
+
continue
|
|
166
|
+
if not _runtime_summary_matches_filters(summary, filters):
|
|
167
|
+
continue
|
|
168
|
+
_add_runtime_summary_to_aggregate(aggregate, summary)
|
|
169
|
+
return 200, json_ready(aggregate)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def execute_runtime_approvals_request(
|
|
173
|
+
query: str,
|
|
174
|
+
service_config: ServiceConfig,
|
|
175
|
+
*,
|
|
176
|
+
request_auth_subject: str = "",
|
|
177
|
+
request_auth_is_admin: bool = False,
|
|
178
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
179
|
+
if not service_config.trace_dir:
|
|
180
|
+
return 400, failure_payload(
|
|
181
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
182
|
+
"trace_dir is required for runtime approvals",
|
|
183
|
+
)
|
|
184
|
+
try:
|
|
185
|
+
limit = _runtime_list_limit(query)
|
|
186
|
+
filters = _runtime_list_filters(query)
|
|
187
|
+
except ValueError as exc:
|
|
188
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, str(exc))
|
|
189
|
+
approvals = []
|
|
190
|
+
trace_dir = Path(service_config.trace_dir)
|
|
191
|
+
for trace_path in sorted(
|
|
192
|
+
trace_dir.glob("*.json"),
|
|
193
|
+
key=_trace_file_sort_key,
|
|
194
|
+
reverse=True,
|
|
195
|
+
):
|
|
196
|
+
try:
|
|
197
|
+
trace = load_trace_by_run_id(trace_path.stem, service_config.trace_dir)
|
|
198
|
+
except _TRACE_READ_ERRORS:
|
|
199
|
+
continue
|
|
200
|
+
if trace is None or not is_runtime_trace(trace):
|
|
201
|
+
continue
|
|
202
|
+
summary = runtime_status_summary(trace, service_config.trace_dir, trace_path.stem)
|
|
203
|
+
if not _runtime_summary_visible(
|
|
204
|
+
summary,
|
|
205
|
+
request_auth_subject,
|
|
206
|
+
request_auth_is_admin,
|
|
207
|
+
):
|
|
208
|
+
continue
|
|
209
|
+
if not _runtime_approval_matches_filters(summary, filters):
|
|
210
|
+
continue
|
|
211
|
+
approvals.append(_runtime_approval_summary(summary))
|
|
212
|
+
if len(approvals) >= limit:
|
|
213
|
+
break
|
|
214
|
+
return 200, json_ready(
|
|
215
|
+
{
|
|
216
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
217
|
+
"count": str(len(approvals)),
|
|
218
|
+
"approvals": approvals,
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def execute_runtime_approvals_summary_request(
|
|
224
|
+
query: str,
|
|
225
|
+
service_config: ServiceConfig,
|
|
226
|
+
*,
|
|
227
|
+
request_auth_subject: str = "",
|
|
228
|
+
request_auth_is_admin: bool = False,
|
|
229
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
230
|
+
if not service_config.trace_dir:
|
|
231
|
+
return 400, failure_payload(
|
|
232
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
233
|
+
"trace_dir is required for runtime approvals summary",
|
|
234
|
+
)
|
|
235
|
+
try:
|
|
236
|
+
filters = _runtime_list_filters(query)
|
|
237
|
+
except ValueError as exc:
|
|
238
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, str(exc))
|
|
239
|
+
aggregate = _empty_runtime_approval_summary()
|
|
240
|
+
trace_dir = Path(service_config.trace_dir)
|
|
241
|
+
for trace_path in sorted(
|
|
242
|
+
trace_dir.glob("*.json"),
|
|
243
|
+
key=_trace_file_sort_key,
|
|
244
|
+
reverse=True,
|
|
245
|
+
):
|
|
246
|
+
try:
|
|
247
|
+
trace = load_trace_by_run_id(trace_path.stem, service_config.trace_dir)
|
|
248
|
+
except _TRACE_READ_ERRORS:
|
|
249
|
+
continue
|
|
250
|
+
if trace is None or not is_runtime_trace(trace):
|
|
251
|
+
continue
|
|
252
|
+
summary = runtime_status_summary(trace, service_config.trace_dir, trace_path.stem)
|
|
253
|
+
if not _runtime_summary_visible(
|
|
254
|
+
summary,
|
|
255
|
+
request_auth_subject,
|
|
256
|
+
request_auth_is_admin,
|
|
257
|
+
):
|
|
258
|
+
continue
|
|
259
|
+
if not _runtime_approval_matches_filters(summary, filters):
|
|
260
|
+
continue
|
|
261
|
+
_add_runtime_approval_to_summary(aggregate, summary)
|
|
262
|
+
return 200, json_ready(aggregate)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def execute_runtime_artifact_request(
|
|
266
|
+
run_id: str,
|
|
267
|
+
artifact_id: str,
|
|
268
|
+
service_config: ServiceConfig,
|
|
269
|
+
*,
|
|
270
|
+
request_auth_subject: str = "",
|
|
271
|
+
request_auth_is_admin: bool = False,
|
|
272
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
273
|
+
if not service_config.trace_dir:
|
|
274
|
+
return 400, failure_payload(
|
|
275
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
276
|
+
"trace_dir is required for runtime artifact lookup",
|
|
277
|
+
)
|
|
278
|
+
if not run_id.strip():
|
|
279
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, "run_id is required")
|
|
280
|
+
if not artifact_id.strip():
|
|
281
|
+
return 400, failure_payload(
|
|
282
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
283
|
+
"artifact_id is required",
|
|
284
|
+
)
|
|
285
|
+
try:
|
|
286
|
+
trace = load_trace_by_run_id(run_id, service_config.trace_dir)
|
|
287
|
+
except _TRACE_READ_ERRORS:
|
|
288
|
+
return 500, failure_payload(
|
|
289
|
+
service_errors.TRACE_READ_FAILED,
|
|
290
|
+
"runtime run trace could not be read",
|
|
291
|
+
)
|
|
292
|
+
if trace is None or not is_runtime_trace(trace):
|
|
293
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
294
|
+
if not _runtime_trace_visible(trace, request_auth_subject, request_auth_is_admin):
|
|
295
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime artifact not found")
|
|
296
|
+
artifact = _runtime_trace_artifact(trace, artifact_id)
|
|
297
|
+
if artifact is None:
|
|
298
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime artifact not found")
|
|
299
|
+
return 200, json_ready(
|
|
300
|
+
{
|
|
301
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
302
|
+
"run_id": run_id,
|
|
303
|
+
"trace_path": _runtime_trace_path(trace, service_config.trace_dir, run_id),
|
|
304
|
+
"action_id": artifact["action_id"],
|
|
305
|
+
"tool": artifact["tool"],
|
|
306
|
+
"artifact": artifact["artifact"],
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def execute_runtime_artifacts_request(
|
|
312
|
+
run_id: str,
|
|
313
|
+
service_config: ServiceConfig,
|
|
314
|
+
*,
|
|
315
|
+
request_auth_subject: str = "",
|
|
316
|
+
request_auth_is_admin: bool = False,
|
|
317
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
318
|
+
if not service_config.trace_dir:
|
|
319
|
+
return 400, failure_payload(
|
|
320
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
321
|
+
"trace_dir is required for runtime artifact listing",
|
|
322
|
+
)
|
|
323
|
+
if not run_id.strip():
|
|
324
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, "run_id is required")
|
|
325
|
+
try:
|
|
326
|
+
trace = load_trace_by_run_id(run_id, service_config.trace_dir)
|
|
327
|
+
except _TRACE_READ_ERRORS:
|
|
328
|
+
return 500, failure_payload(
|
|
329
|
+
service_errors.TRACE_READ_FAILED,
|
|
330
|
+
"runtime run trace could not be read",
|
|
331
|
+
)
|
|
332
|
+
if trace is None or not is_runtime_trace(trace):
|
|
333
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
334
|
+
if not _runtime_trace_visible(trace, request_auth_subject, request_auth_is_admin):
|
|
335
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
336
|
+
artifacts = _runtime_trace_artifacts(trace)
|
|
337
|
+
return 200, json_ready(
|
|
338
|
+
{
|
|
339
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
340
|
+
"run_id": run_id,
|
|
341
|
+
"trace_path": _runtime_trace_path(trace, service_config.trace_dir, run_id),
|
|
342
|
+
"count": str(len(artifacts)),
|
|
343
|
+
"artifacts": artifacts,
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def execute_runtime_timeline_request(
|
|
349
|
+
run_id: str,
|
|
350
|
+
service_config: ServiceConfig,
|
|
351
|
+
*,
|
|
352
|
+
request_auth_subject: str = "",
|
|
353
|
+
request_auth_is_admin: bool = False,
|
|
354
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
355
|
+
if not service_config.trace_dir:
|
|
356
|
+
return 400, failure_payload(
|
|
357
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
358
|
+
"trace_dir is required for runtime timeline",
|
|
359
|
+
)
|
|
360
|
+
if not run_id.strip():
|
|
361
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, "run_id is required")
|
|
362
|
+
try:
|
|
363
|
+
trace = load_trace_by_run_id(run_id, service_config.trace_dir)
|
|
364
|
+
except _TRACE_READ_ERRORS:
|
|
365
|
+
return 500, failure_payload(
|
|
366
|
+
service_errors.TRACE_READ_FAILED,
|
|
367
|
+
"runtime run trace could not be read",
|
|
368
|
+
)
|
|
369
|
+
if trace is None or not is_runtime_trace(trace):
|
|
370
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
371
|
+
if not _runtime_trace_visible(trace, request_auth_subject, request_auth_is_admin):
|
|
372
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
373
|
+
events = _runtime_timeline_events(trace.get("events"))
|
|
374
|
+
observations = _runtime_timeline_observations(trace.get("observations"))
|
|
375
|
+
progress_events = _runtime_timeline_progress_events(trace.get("progress_events"))
|
|
376
|
+
graph_phases = _runtime_timeline_graph_phases(trace.get("graph_phases"))
|
|
377
|
+
steps = _runtime_compact_steps(trace)
|
|
378
|
+
return 200, json_ready(
|
|
379
|
+
{
|
|
380
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
381
|
+
"run_id": run_id,
|
|
382
|
+
"trace_path": _runtime_trace_path(trace, service_config.trace_dir, run_id),
|
|
383
|
+
"event_count": str(len(events)),
|
|
384
|
+
"step_count": str(len(steps)),
|
|
385
|
+
"progress_event_count": str(len(progress_events)),
|
|
386
|
+
"graph_phase_count": str(len(graph_phases)),
|
|
387
|
+
"observation_count": str(len(observations)),
|
|
388
|
+
"steps": steps,
|
|
389
|
+
"events": events,
|
|
390
|
+
"progress_events": progress_events,
|
|
391
|
+
"graph_phases": graph_phases,
|
|
392
|
+
"observations": observations,
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def is_runtime_trace(trace: Dict[str, Any]) -> bool:
|
|
398
|
+
return trace.get("trace_type") == RUNTIME_TRACE_TYPE
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _runtime_trace_visible(
|
|
402
|
+
trace: Dict[str, Any],
|
|
403
|
+
request_auth_subject: str,
|
|
404
|
+
request_auth_is_admin: bool,
|
|
405
|
+
) -> bool:
|
|
406
|
+
if request_auth_is_admin or not request_auth_subject:
|
|
407
|
+
return True
|
|
408
|
+
return str(trace.get("auth_subject", "")) == request_auth_subject
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _runtime_summary_visible(
|
|
412
|
+
summary: Dict[str, Any],
|
|
413
|
+
request_auth_subject: str,
|
|
414
|
+
request_auth_is_admin: bool,
|
|
415
|
+
) -> bool:
|
|
416
|
+
if request_auth_is_admin or not request_auth_subject:
|
|
417
|
+
return True
|
|
418
|
+
return str(summary.get("auth_subject", "")) == request_auth_subject
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def runtime_status_summary(
|
|
422
|
+
trace: Dict[str, Any],
|
|
423
|
+
trace_dir: str,
|
|
424
|
+
requested_run_id: str,
|
|
425
|
+
) -> Dict[str, Any]:
|
|
426
|
+
run_id = _runtime_string(trace.get("run_id")) or requested_run_id
|
|
427
|
+
observations = trace.get("observations")
|
|
428
|
+
steps = _runtime_compact_steps(trace)
|
|
429
|
+
artifact_ids = _observation_artifact_ids(observations)
|
|
430
|
+
approved_action_ids = _trace_approved_action_ids(trace)
|
|
431
|
+
pending_approval = trace.get("pending_approval")
|
|
432
|
+
latest_failed_observation = _latest_failed_observation(observations)
|
|
433
|
+
summary = {
|
|
434
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
435
|
+
"run_id": run_id,
|
|
436
|
+
"status": _runtime_scalar(trace.get("status")),
|
|
437
|
+
"lifecycle_state": _runtime_lifecycle_state(trace),
|
|
438
|
+
"goal": _runtime_scalar(trace.get("goal")),
|
|
439
|
+
"auth_subject": _runtime_scalar(trace.get("auth_subject")),
|
|
440
|
+
"metadata": _trace_metadata(trace),
|
|
441
|
+
"metadata_keys": _trace_metadata_keys(trace),
|
|
442
|
+
"tags": _trace_tags(trace),
|
|
443
|
+
"trace_path": _runtime_trace_path(trace, trace_dir, run_id),
|
|
444
|
+
"iteration_count": _runtime_iteration_count(trace),
|
|
445
|
+
"max_iterations": _runtime_scalar(trace.get("max_iterations")),
|
|
446
|
+
"iteration_budget_remaining": _runtime_iteration_budget_remaining(trace),
|
|
447
|
+
"plan_count": str(_list_count(trace.get("plans"))),
|
|
448
|
+
"step_count": str(len(steps)),
|
|
449
|
+
"steps": steps,
|
|
450
|
+
"observation_count": str(_list_count(observations)),
|
|
451
|
+
"event_count": str(_list_count(trace.get("events"))),
|
|
452
|
+
"progress_event_count": str(_list_count(trace.get("progress_events"))),
|
|
453
|
+
"graph_phase_count": str(_list_count(trace.get("graph_phases"))),
|
|
454
|
+
"graph_phase_node_counts": _runtime_graph_phase_node_counts(
|
|
455
|
+
trace.get("graph_phases")
|
|
456
|
+
),
|
|
457
|
+
"progress_event_sink_failure_count": str(
|
|
458
|
+
_parse_non_negative_int(trace.get("progress_event_sink_failure_count"))
|
|
459
|
+
),
|
|
460
|
+
"hook_failure_count": str(
|
|
461
|
+
_parse_non_negative_int(trace.get("hook_failure_count"))
|
|
462
|
+
),
|
|
463
|
+
"failed_observation_count": str(
|
|
464
|
+
_observation_status_count(observations, "failed")
|
|
465
|
+
),
|
|
466
|
+
"planner_failure_count": str(
|
|
467
|
+
_failed_observation_tool_count(observations, "planner")
|
|
468
|
+
),
|
|
469
|
+
"tool_failure_count": str(
|
|
470
|
+
_failed_non_planner_observation_count(observations)
|
|
471
|
+
),
|
|
472
|
+
"approval_required_count": str(
|
|
473
|
+
_observation_status_count(observations, "requires_approval")
|
|
474
|
+
),
|
|
475
|
+
"latest_failed_action_id": _observation_field(
|
|
476
|
+
latest_failed_observation,
|
|
477
|
+
"action_id",
|
|
478
|
+
),
|
|
479
|
+
"latest_failed_tool": _observation_field(latest_failed_observation, "tool"),
|
|
480
|
+
"latest_failed_error_code": _observation_field(
|
|
481
|
+
latest_failed_observation,
|
|
482
|
+
"error_code",
|
|
483
|
+
),
|
|
484
|
+
"pending_approval_action_id": _pending_approval_field(
|
|
485
|
+
pending_approval,
|
|
486
|
+
"id",
|
|
487
|
+
),
|
|
488
|
+
"pending_approval_tool": _pending_approval_field(
|
|
489
|
+
pending_approval,
|
|
490
|
+
"tool",
|
|
491
|
+
),
|
|
492
|
+
"pending_age_seconds": _pending_age_seconds(
|
|
493
|
+
pending_approval,
|
|
494
|
+
trace_dir,
|
|
495
|
+
run_id,
|
|
496
|
+
),
|
|
497
|
+
"approved_action_count": str(len(approved_action_ids)),
|
|
498
|
+
"approved_action_ids": approved_action_ids,
|
|
499
|
+
"approved_tool_counts": _approved_tool_counts(trace.get("events")),
|
|
500
|
+
"error_code_counts": _observation_error_code_counts(observations),
|
|
501
|
+
"latest_plan_action_count": str(_latest_plan_action_count(trace.get("plan"))),
|
|
502
|
+
"latest_plan_action_ids": _latest_plan_action_ids(trace.get("plan")),
|
|
503
|
+
"dependency_edge_count": str(_plan_dependency_edge_count(trace.get("plan"))),
|
|
504
|
+
"tool_names": _observation_tool_names(observations),
|
|
505
|
+
"artifact_count": str(len(artifact_ids)),
|
|
506
|
+
"artifact_ids": artifact_ids,
|
|
507
|
+
"artifact_kinds": _observation_artifact_kinds(observations),
|
|
508
|
+
"artifact_formats": _observation_artifact_formats(observations),
|
|
509
|
+
"artifact_tags": _observation_artifact_tags(observations),
|
|
510
|
+
"artifact_total_bytes": str(_observation_artifact_total_bytes(observations)),
|
|
511
|
+
"artifact_bytes_by_kind": _observation_artifact_bytes_by_kind(observations),
|
|
512
|
+
}
|
|
513
|
+
llm_provider_request = _trace_llm_provider_request(trace)
|
|
514
|
+
if llm_provider_request:
|
|
515
|
+
summary.update(llm_provider_request)
|
|
516
|
+
runtime_engine = _runtime_scalar(trace.get("runtime_engine"))
|
|
517
|
+
if runtime_engine:
|
|
518
|
+
summary["runtime_engine"] = runtime_engine
|
|
519
|
+
for optional_field in [
|
|
520
|
+
"answer",
|
|
521
|
+
"error_code",
|
|
522
|
+
"error",
|
|
523
|
+
"resumed_from_run_id",
|
|
524
|
+
"resumed_to_run_id",
|
|
525
|
+
"resumed_by_auth_subject",
|
|
526
|
+
"approved_by_auth_subject",
|
|
527
|
+
"approved_at",
|
|
528
|
+
"cancelled_at",
|
|
529
|
+
"cancelled_by_auth_subject",
|
|
530
|
+
"cancel_reason",
|
|
531
|
+
"started_at",
|
|
532
|
+
"completed_at",
|
|
533
|
+
"duration_seconds",
|
|
534
|
+
]:
|
|
535
|
+
if optional_field in trace:
|
|
536
|
+
field_value = _runtime_scalar(trace[optional_field])
|
|
537
|
+
if field_value:
|
|
538
|
+
summary[optional_field] = field_value
|
|
539
|
+
pending_approval_detail = _trace_pending_approval(trace)
|
|
540
|
+
if pending_approval_detail:
|
|
541
|
+
summary["pending_approval"] = pending_approval_detail
|
|
542
|
+
final_answer_guardrail = _trace_final_answer_guardrail(trace)
|
|
543
|
+
if final_answer_guardrail:
|
|
544
|
+
summary["final_answer_guardrail"] = final_answer_guardrail
|
|
545
|
+
return summary
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _runtime_list_summary(summary: Dict[str, Any]) -> Dict[str, Any]:
|
|
549
|
+
list_summary = dict(summary)
|
|
550
|
+
list_summary.pop("pending_approval", None)
|
|
551
|
+
list_summary.pop("steps", None)
|
|
552
|
+
return list_summary
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _runtime_approval_summary(summary: Dict[str, Any]) -> Dict[str, Any]:
|
|
556
|
+
approval = {
|
|
557
|
+
"run_id": str(summary.get("run_id", "")),
|
|
558
|
+
"status": str(summary.get("status", "")),
|
|
559
|
+
"goal": str(summary.get("goal", "")),
|
|
560
|
+
"auth_subject": str(summary.get("auth_subject", "")),
|
|
561
|
+
"trace_path": str(summary.get("trace_path", "")),
|
|
562
|
+
"pending_approval_action_id": str(
|
|
563
|
+
summary.get("pending_approval_action_id", "")
|
|
564
|
+
),
|
|
565
|
+
"pending_approval_tool": str(summary.get("pending_approval_tool", "")),
|
|
566
|
+
}
|
|
567
|
+
for optional_field in ["started_at", "duration_seconds", "pending_age_seconds"]:
|
|
568
|
+
if optional_field in summary:
|
|
569
|
+
approval[optional_field] = str(summary[optional_field])
|
|
570
|
+
return approval
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _empty_runtime_approval_summary() -> Dict[str, Any]:
|
|
574
|
+
return {
|
|
575
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
576
|
+
"pending_approval_count": "0",
|
|
577
|
+
"stale_pending_count": "0",
|
|
578
|
+
"max_pending_age_seconds": "0",
|
|
579
|
+
"auth_subject_counts": {},
|
|
580
|
+
"tool_counts": {},
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _add_runtime_approval_to_summary(
|
|
585
|
+
aggregate: Dict[str, Any],
|
|
586
|
+
summary: Dict[str, Any],
|
|
587
|
+
) -> None:
|
|
588
|
+
aggregate["pending_approval_count"] = str(
|
|
589
|
+
int(aggregate["pending_approval_count"]) + 1
|
|
590
|
+
)
|
|
591
|
+
aggregate["stale_pending_count"] = str(int(aggregate["stale_pending_count"]) + 1)
|
|
592
|
+
aggregate["max_pending_age_seconds"] = str(
|
|
593
|
+
max(
|
|
594
|
+
_parse_non_negative_int(aggregate["max_pending_age_seconds"]),
|
|
595
|
+
_parse_non_negative_int(summary.get("pending_age_seconds")),
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
_increment_count(
|
|
599
|
+
aggregate["auth_subject_counts"],
|
|
600
|
+
str(summary.get("auth_subject", "")),
|
|
601
|
+
)
|
|
602
|
+
_increment_count(
|
|
603
|
+
aggregate["tool_counts"],
|
|
604
|
+
str(summary.get("pending_approval_tool", "")),
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _runtime_approval_matches_filters(
|
|
609
|
+
summary: Dict[str, Any],
|
|
610
|
+
filters: Dict[str, Any],
|
|
611
|
+
) -> bool:
|
|
612
|
+
if not str(summary.get("pending_approval_action_id", "")).strip() and not str(
|
|
613
|
+
summary.get("pending_approval_tool", "")
|
|
614
|
+
).strip():
|
|
615
|
+
return False
|
|
616
|
+
if (
|
|
617
|
+
filters.get("tool") is not None
|
|
618
|
+
and summary.get("pending_approval_tool") != filters["tool"]
|
|
619
|
+
):
|
|
620
|
+
return False
|
|
621
|
+
summary_filters = dict(filters)
|
|
622
|
+
summary_filters["tool"] = None
|
|
623
|
+
return _runtime_summary_matches_filters(summary, summary_filters)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _empty_runtime_summary_aggregate() -> Dict[str, Any]:
|
|
627
|
+
return {
|
|
628
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
629
|
+
"run_count": "0",
|
|
630
|
+
"status_counts": {},
|
|
631
|
+
"lifecycle_state_counts": {},
|
|
632
|
+
"runtime_engine_counts": {},
|
|
633
|
+
"auth_subject_counts": {},
|
|
634
|
+
"approved_by_auth_subject_counts": {},
|
|
635
|
+
"tool_counts": {},
|
|
636
|
+
"error_code_counts": {},
|
|
637
|
+
"failed_observation_count": "0",
|
|
638
|
+
"graph_phase_count": "0",
|
|
639
|
+
"graph_phase_node_counts": {},
|
|
640
|
+
"progress_event_sink_failure_count": "0",
|
|
641
|
+
"hook_failure_count": "0",
|
|
642
|
+
"llm_provider_request_count": "0",
|
|
643
|
+
"llm_provider_request_attempt_count": "0",
|
|
644
|
+
"llm_provider_request_retry_count": "0",
|
|
645
|
+
"llm_provider_request_status_counts": {},
|
|
646
|
+
"llm_provider_request_error_type_counts": {},
|
|
647
|
+
"llm_provider_request_http_status_counts": {},
|
|
648
|
+
"llm_provider_request_retryable_reason_counts": {},
|
|
649
|
+
"approval_required_count": "0",
|
|
650
|
+
"approved_tool_counts": {},
|
|
651
|
+
"pending_approval_count": "0",
|
|
652
|
+
"final_answer_guardrail_applied_count": "0",
|
|
653
|
+
"final_answer_guardrail_reason_counts": {},
|
|
654
|
+
"artifact_count": "0",
|
|
655
|
+
"artifact_total_bytes": "0",
|
|
656
|
+
"tag_counts": {},
|
|
657
|
+
"metadata_key_counts": {},
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _add_runtime_summary_to_aggregate(
|
|
662
|
+
aggregate: Dict[str, Any],
|
|
663
|
+
summary: Dict[str, Any],
|
|
664
|
+
) -> None:
|
|
665
|
+
aggregate["run_count"] = str(int(aggregate["run_count"]) + 1)
|
|
666
|
+
_increment_count(aggregate["status_counts"], str(summary.get("status", "")))
|
|
667
|
+
_increment_count(
|
|
668
|
+
aggregate["lifecycle_state_counts"],
|
|
669
|
+
str(summary.get("lifecycle_state", "")),
|
|
670
|
+
)
|
|
671
|
+
_increment_count(
|
|
672
|
+
aggregate["runtime_engine_counts"],
|
|
673
|
+
str(summary.get("runtime_engine", "")),
|
|
674
|
+
)
|
|
675
|
+
_increment_count(
|
|
676
|
+
aggregate["auth_subject_counts"],
|
|
677
|
+
str(summary.get("auth_subject", "")),
|
|
678
|
+
)
|
|
679
|
+
_increment_count(
|
|
680
|
+
aggregate["approved_by_auth_subject_counts"],
|
|
681
|
+
str(summary.get("approved_by_auth_subject", "")),
|
|
682
|
+
)
|
|
683
|
+
for tool_name in summary.get("tool_names", []):
|
|
684
|
+
_increment_count(aggregate["tool_counts"], str(tool_name))
|
|
685
|
+
error_code_counts = summary.get("error_code_counts")
|
|
686
|
+
if isinstance(error_code_counts, dict):
|
|
687
|
+
for error_code, count in error_code_counts.items():
|
|
688
|
+
_increment_count(
|
|
689
|
+
aggregate["error_code_counts"],
|
|
690
|
+
str(error_code),
|
|
691
|
+
_parse_non_negative_int(count),
|
|
692
|
+
)
|
|
693
|
+
aggregate["failed_observation_count"] = str(
|
|
694
|
+
int(aggregate["failed_observation_count"])
|
|
695
|
+
+ _parse_non_negative_int(summary.get("failed_observation_count"))
|
|
696
|
+
)
|
|
697
|
+
aggregate["graph_phase_count"] = str(
|
|
698
|
+
int(aggregate["graph_phase_count"])
|
|
699
|
+
+ _parse_non_negative_int(summary.get("graph_phase_count"))
|
|
700
|
+
)
|
|
701
|
+
graph_phase_node_counts = summary.get("graph_phase_node_counts")
|
|
702
|
+
if isinstance(graph_phase_node_counts, dict):
|
|
703
|
+
for node, count in graph_phase_node_counts.items():
|
|
704
|
+
_increment_count(
|
|
705
|
+
aggregate["graph_phase_node_counts"],
|
|
706
|
+
str(node),
|
|
707
|
+
_parse_non_negative_int(count),
|
|
708
|
+
)
|
|
709
|
+
aggregate["progress_event_sink_failure_count"] = str(
|
|
710
|
+
int(aggregate["progress_event_sink_failure_count"])
|
|
711
|
+
+ _parse_non_negative_int(summary.get("progress_event_sink_failure_count"))
|
|
712
|
+
)
|
|
713
|
+
aggregate["hook_failure_count"] = str(
|
|
714
|
+
int(aggregate["hook_failure_count"])
|
|
715
|
+
+ _parse_non_negative_int(summary.get("hook_failure_count"))
|
|
716
|
+
)
|
|
717
|
+
if str(summary.get("llm_provider_request_status", "")).strip():
|
|
718
|
+
aggregate["llm_provider_request_count"] = str(
|
|
719
|
+
int(aggregate["llm_provider_request_count"]) + 1
|
|
720
|
+
)
|
|
721
|
+
aggregate["llm_provider_request_attempt_count"] = str(
|
|
722
|
+
int(aggregate["llm_provider_request_attempt_count"])
|
|
723
|
+
+ _parse_non_negative_int(
|
|
724
|
+
summary.get("llm_provider_request_attempt_count")
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
aggregate["llm_provider_request_retry_count"] = str(
|
|
728
|
+
int(aggregate["llm_provider_request_retry_count"])
|
|
729
|
+
+ _parse_non_negative_int(summary.get("llm_provider_request_retry_count"))
|
|
730
|
+
)
|
|
731
|
+
_increment_count(
|
|
732
|
+
aggregate["llm_provider_request_status_counts"],
|
|
733
|
+
str(summary.get("llm_provider_request_status", "")),
|
|
734
|
+
)
|
|
735
|
+
_increment_count(
|
|
736
|
+
aggregate["llm_provider_request_error_type_counts"],
|
|
737
|
+
str(summary.get("llm_provider_request_error_type", "")),
|
|
738
|
+
)
|
|
739
|
+
_increment_count(
|
|
740
|
+
aggregate["llm_provider_request_http_status_counts"],
|
|
741
|
+
str(summary.get("llm_provider_request_http_status", "")),
|
|
742
|
+
)
|
|
743
|
+
_increment_count(
|
|
744
|
+
aggregate["llm_provider_request_retryable_reason_counts"],
|
|
745
|
+
str(summary.get("llm_provider_request_retryable_reason", "")),
|
|
746
|
+
)
|
|
747
|
+
aggregate["approval_required_count"] = str(
|
|
748
|
+
int(aggregate["approval_required_count"])
|
|
749
|
+
+ _parse_non_negative_int(summary.get("approval_required_count"))
|
|
750
|
+
)
|
|
751
|
+
approved_tool_counts = summary.get("approved_tool_counts")
|
|
752
|
+
if isinstance(approved_tool_counts, dict):
|
|
753
|
+
for tool_name, count in approved_tool_counts.items():
|
|
754
|
+
_increment_count(
|
|
755
|
+
aggregate["approved_tool_counts"],
|
|
756
|
+
str(tool_name),
|
|
757
|
+
_parse_non_negative_int(count),
|
|
758
|
+
)
|
|
759
|
+
if str(summary.get("pending_approval_action_id", "")).strip() or str(
|
|
760
|
+
summary.get("pending_approval_tool", "")
|
|
761
|
+
).strip():
|
|
762
|
+
aggregate["pending_approval_count"] = str(
|
|
763
|
+
int(aggregate["pending_approval_count"]) + 1
|
|
764
|
+
)
|
|
765
|
+
guardrail = summary.get("final_answer_guardrail")
|
|
766
|
+
if isinstance(guardrail, dict) and guardrail.get("applied") == "true":
|
|
767
|
+
aggregate["final_answer_guardrail_applied_count"] = str(
|
|
768
|
+
int(aggregate["final_answer_guardrail_applied_count"]) + 1
|
|
769
|
+
)
|
|
770
|
+
_increment_count(
|
|
771
|
+
aggregate["final_answer_guardrail_reason_counts"],
|
|
772
|
+
str(guardrail.get("reason", "")),
|
|
773
|
+
)
|
|
774
|
+
aggregate["artifact_count"] = str(
|
|
775
|
+
int(aggregate["artifact_count"])
|
|
776
|
+
+ _parse_non_negative_int(summary.get("artifact_count"))
|
|
777
|
+
)
|
|
778
|
+
aggregate["artifact_total_bytes"] = str(
|
|
779
|
+
int(aggregate["artifact_total_bytes"])
|
|
780
|
+
+ _parse_non_negative_int(summary.get("artifact_total_bytes"))
|
|
781
|
+
)
|
|
782
|
+
for tag in summary.get("tags", []):
|
|
783
|
+
_increment_count(aggregate["tag_counts"], str(tag))
|
|
784
|
+
for metadata_key in summary.get("metadata_keys", []):
|
|
785
|
+
_increment_count(aggregate["metadata_key_counts"], str(metadata_key))
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _increment_count(
|
|
789
|
+
counts: Dict[str, str],
|
|
790
|
+
key: str,
|
|
791
|
+
amount: int = 1,
|
|
792
|
+
) -> None:
|
|
793
|
+
if not key.strip() or amount <= 0:
|
|
794
|
+
return
|
|
795
|
+
counts[key] = str(int(counts.get(key, "0")) + amount)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _parse_non_negative_int(value: Any) -> int:
|
|
799
|
+
try:
|
|
800
|
+
parsed = int(str(value))
|
|
801
|
+
except (TypeError, ValueError):
|
|
802
|
+
return 0
|
|
803
|
+
return max(0, parsed)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _runtime_string(value: Any) -> str:
|
|
807
|
+
if not isinstance(value, str):
|
|
808
|
+
return ""
|
|
809
|
+
return value.strip()
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _runtime_scalar(value: Any) -> str:
|
|
813
|
+
if isinstance(value, bool):
|
|
814
|
+
return ""
|
|
815
|
+
if isinstance(value, str):
|
|
816
|
+
return value.strip()
|
|
817
|
+
if isinstance(value, int):
|
|
818
|
+
return str(value)
|
|
819
|
+
if isinstance(value, float):
|
|
820
|
+
return str(value)
|
|
821
|
+
return ""
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _trace_llm_provider_request(trace: Dict[str, Any]) -> Dict[str, str]:
|
|
825
|
+
raw_request = trace.get("llm_provider_request")
|
|
826
|
+
if not isinstance(raw_request, dict):
|
|
827
|
+
return {}
|
|
828
|
+
fields = {
|
|
829
|
+
"llm_provider_request_status": "status",
|
|
830
|
+
"llm_provider_request_attempt_count": "attempt_count",
|
|
831
|
+
"llm_provider_request_retry_count": "retry_count",
|
|
832
|
+
"llm_provider_request_error_type": "error_type",
|
|
833
|
+
"llm_provider_request_http_status": "http_status",
|
|
834
|
+
"llm_provider_request_retryable_reason": "retryable_reason",
|
|
835
|
+
"llm_provider_request_duration_seconds": "duration_seconds",
|
|
836
|
+
}
|
|
837
|
+
summary = {}
|
|
838
|
+
for output_field, input_field in fields.items():
|
|
839
|
+
value = _runtime_scalar(raw_request.get(input_field))
|
|
840
|
+
if value:
|
|
841
|
+
summary[output_field] = value
|
|
842
|
+
return summary
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _list_count(value: Any) -> int:
|
|
846
|
+
return len(value) if isinstance(value, list) else 0
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _runtime_iteration_count(trace: Dict[str, Any]) -> str:
|
|
850
|
+
value = trace.get("iteration_count")
|
|
851
|
+
if value is not None:
|
|
852
|
+
return _runtime_scalar(value)
|
|
853
|
+
return str(_list_count(trace.get("plans")))
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _runtime_iteration_budget_remaining(trace: Dict[str, Any]) -> str:
|
|
857
|
+
value = trace.get("iteration_budget_remaining")
|
|
858
|
+
if value is not None:
|
|
859
|
+
return _runtime_scalar(value)
|
|
860
|
+
iteration_count = _parse_int_string(_runtime_iteration_count(trace))
|
|
861
|
+
max_iterations = _parse_int_string(trace.get("max_iterations"))
|
|
862
|
+
if iteration_count is None or max_iterations is None:
|
|
863
|
+
return ""
|
|
864
|
+
return str(max(0, max_iterations - iteration_count))
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _parse_int_string(value: Any) -> int | None:
|
|
868
|
+
try:
|
|
869
|
+
return int(str(value))
|
|
870
|
+
except (TypeError, ValueError):
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _trace_approved_action_ids(trace: Dict[str, Any]) -> list[str]:
|
|
875
|
+
value = trace.get("approved_action_ids")
|
|
876
|
+
if not isinstance(value, list):
|
|
877
|
+
return []
|
|
878
|
+
return sorted(
|
|
879
|
+
{
|
|
880
|
+
action_id
|
|
881
|
+
for action_id in value
|
|
882
|
+
if isinstance(action_id, str) and action_id.strip()
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _approved_tool_counts(value: Any) -> Dict[str, str]:
|
|
888
|
+
if not isinstance(value, list):
|
|
889
|
+
return {}
|
|
890
|
+
counts: Dict[str, str] = {}
|
|
891
|
+
for item in value:
|
|
892
|
+
if not isinstance(item, dict):
|
|
893
|
+
continue
|
|
894
|
+
if _runtime_scalar(item.get("node")) != "policy":
|
|
895
|
+
continue
|
|
896
|
+
if _runtime_scalar(item.get("status")) != "approved":
|
|
897
|
+
continue
|
|
898
|
+
_increment_count(counts, _runtime_scalar(item.get("tool")))
|
|
899
|
+
return dict(sorted(counts.items()))
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _runtime_lifecycle_state(trace: Dict[str, Any]) -> str:
|
|
903
|
+
status = _runtime_scalar(trace.get("status"))
|
|
904
|
+
if status == "cancelled":
|
|
905
|
+
return "cancelled"
|
|
906
|
+
if status == "failed":
|
|
907
|
+
return "failed"
|
|
908
|
+
if status == "requires_approval":
|
|
909
|
+
return "waiting_approval"
|
|
910
|
+
if status in {"running", "resuming"}:
|
|
911
|
+
return "running"
|
|
912
|
+
if status == "resumed":
|
|
913
|
+
return "succeeded"
|
|
914
|
+
if status == "done":
|
|
915
|
+
return "succeeded"
|
|
916
|
+
if _trace_pending_approval(trace):
|
|
917
|
+
return "waiting_approval"
|
|
918
|
+
event_state = _latest_event_lifecycle_state(trace.get("progress_events"))
|
|
919
|
+
if event_state != "unknown":
|
|
920
|
+
return event_state
|
|
921
|
+
return _latest_event_lifecycle_state(trace.get("events"))
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def _latest_event_lifecycle_state(value: Any) -> str:
|
|
925
|
+
if not isinstance(value, list):
|
|
926
|
+
return "unknown"
|
|
927
|
+
for item in reversed(value):
|
|
928
|
+
if not isinstance(item, dict):
|
|
929
|
+
continue
|
|
930
|
+
event_type = _runtime_scalar(item.get("type"))
|
|
931
|
+
node = _runtime_scalar(item.get("node"))
|
|
932
|
+
status = _runtime_scalar(item.get("status"))
|
|
933
|
+
if event_type == "run_completed":
|
|
934
|
+
return _lifecycle_state_from_status(status)
|
|
935
|
+
if event_type == "approval_required":
|
|
936
|
+
return "waiting_approval"
|
|
937
|
+
if event_type in {"tool_started", "tool_completed", "policy_completed"}:
|
|
938
|
+
return "running"
|
|
939
|
+
if event_type == "planner_started":
|
|
940
|
+
return "planning"
|
|
941
|
+
if node == "executor":
|
|
942
|
+
return "running"
|
|
943
|
+
if node == "policy" and status == "requires_approval":
|
|
944
|
+
return "waiting_approval"
|
|
945
|
+
if node == "policy":
|
|
946
|
+
return "running"
|
|
947
|
+
if node == "planner" and status == "started":
|
|
948
|
+
return "planning"
|
|
949
|
+
return "unknown"
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _lifecycle_state_from_status(status: str) -> str:
|
|
953
|
+
if status == "cancelled":
|
|
954
|
+
return "cancelled"
|
|
955
|
+
if status == "done":
|
|
956
|
+
return "succeeded"
|
|
957
|
+
if status == "failed":
|
|
958
|
+
return "failed"
|
|
959
|
+
if status == "requires_approval":
|
|
960
|
+
return "waiting_approval"
|
|
961
|
+
if status in {"running", "resuming"}:
|
|
962
|
+
return "running"
|
|
963
|
+
if status == "resumed":
|
|
964
|
+
return "succeeded"
|
|
965
|
+
return "unknown"
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _trace_metadata(trace: Dict[str, Any]) -> Dict[str, str]:
|
|
969
|
+
value = trace.get("metadata")
|
|
970
|
+
if not isinstance(value, dict):
|
|
971
|
+
return {}
|
|
972
|
+
metadata = {}
|
|
973
|
+
for key, metadata_value in value.items():
|
|
974
|
+
key_value = _runtime_string(key)
|
|
975
|
+
field_value = _runtime_scalar(metadata_value)
|
|
976
|
+
if key_value and field_value:
|
|
977
|
+
metadata[key_value] = field_value
|
|
978
|
+
return {key: metadata[key] for key in sorted(metadata)}
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _trace_metadata_keys(trace: Dict[str, Any]) -> list[str]:
|
|
982
|
+
return sorted(_trace_metadata(trace))
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _trace_tags(trace: Dict[str, Any]) -> list[str]:
|
|
986
|
+
value = trace.get("tags")
|
|
987
|
+
if not isinstance(value, list):
|
|
988
|
+
return []
|
|
989
|
+
return sorted(
|
|
990
|
+
{
|
|
991
|
+
tag
|
|
992
|
+
for tag in value
|
|
993
|
+
if isinstance(tag, str) and tag.strip()
|
|
994
|
+
}
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _trace_final_answer_guardrail(trace: Dict[str, Any]) -> Dict[str, str]:
|
|
999
|
+
value = trace.get("final_answer_guardrail")
|
|
1000
|
+
if not isinstance(value, dict):
|
|
1001
|
+
return {}
|
|
1002
|
+
guardrail = {}
|
|
1003
|
+
for field in ["applied", "reason", "original_answer_omitted"]:
|
|
1004
|
+
field_value = _runtime_string(value.get(field))
|
|
1005
|
+
if field_value:
|
|
1006
|
+
guardrail[field] = field_value
|
|
1007
|
+
return guardrail
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def _trace_pending_approval(trace: Dict[str, Any]) -> Dict[str, Any]:
|
|
1011
|
+
value = trace.get("pending_approval")
|
|
1012
|
+
if not isinstance(value, dict):
|
|
1013
|
+
return {}
|
|
1014
|
+
approval: Dict[str, Any] = {
|
|
1015
|
+
"id": _runtime_string(value.get("id")),
|
|
1016
|
+
"tool": _runtime_string(value.get("tool")),
|
|
1017
|
+
}
|
|
1018
|
+
approval_input = value.get("input")
|
|
1019
|
+
if isinstance(approval_input, dict):
|
|
1020
|
+
approval["input"] = json_ready(approval_input)
|
|
1021
|
+
else:
|
|
1022
|
+
approval["input"] = {}
|
|
1023
|
+
reason = _runtime_string(value.get("reason"))
|
|
1024
|
+
if reason:
|
|
1025
|
+
approval["reason"] = reason
|
|
1026
|
+
return approval
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _pending_approval_field(value: Any, field: str) -> str:
|
|
1030
|
+
if not isinstance(value, dict):
|
|
1031
|
+
return ""
|
|
1032
|
+
return _runtime_scalar(value.get(field))
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _pending_age_seconds(
|
|
1036
|
+
pending_approval: Any,
|
|
1037
|
+
trace_dir: str,
|
|
1038
|
+
run_id: str,
|
|
1039
|
+
) -> str:
|
|
1040
|
+
if not isinstance(pending_approval, dict):
|
|
1041
|
+
return ""
|
|
1042
|
+
trace_path = Path(trace_dir) / f"{safe_trace_file_stem(run_id)}.json"
|
|
1043
|
+
try:
|
|
1044
|
+
age_seconds = int(max(0.0, time.time() - trace_path.stat().st_mtime))
|
|
1045
|
+
except OSError:
|
|
1046
|
+
return ""
|
|
1047
|
+
return str(age_seconds)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def _latest_failed_observation(value: Any) -> Dict[str, Any]:
|
|
1051
|
+
if not isinstance(value, list):
|
|
1052
|
+
return {}
|
|
1053
|
+
for item in reversed(value):
|
|
1054
|
+
if isinstance(item, dict) and _runtime_scalar(item.get("status")) == "failed":
|
|
1055
|
+
return item
|
|
1056
|
+
return {}
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _observation_field(value: Dict[str, Any], field: str) -> str:
|
|
1060
|
+
return _runtime_scalar(value.get(field)) if value else ""
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _observation_status_count(value: Any, status: str) -> int:
|
|
1064
|
+
if not isinstance(value, list):
|
|
1065
|
+
return 0
|
|
1066
|
+
return sum(
|
|
1067
|
+
1
|
|
1068
|
+
for item in value
|
|
1069
|
+
if isinstance(item, dict) and _runtime_scalar(item.get("status")) == status
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _failed_observation_tool_count(value: Any, tool: str) -> int:
|
|
1074
|
+
if not isinstance(value, list):
|
|
1075
|
+
return 0
|
|
1076
|
+
return sum(
|
|
1077
|
+
1
|
|
1078
|
+
for item in value
|
|
1079
|
+
if isinstance(item, dict)
|
|
1080
|
+
and _runtime_scalar(item.get("status")) == "failed"
|
|
1081
|
+
and _runtime_scalar(item.get("tool")) == tool
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _failed_non_planner_observation_count(value: Any) -> int:
|
|
1086
|
+
if not isinstance(value, list):
|
|
1087
|
+
return 0
|
|
1088
|
+
return sum(
|
|
1089
|
+
1
|
|
1090
|
+
for item in value
|
|
1091
|
+
if isinstance(item, dict)
|
|
1092
|
+
and _runtime_scalar(item.get("status")) == "failed"
|
|
1093
|
+
and _runtime_scalar(item.get("tool")) != "planner"
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _observation_tool_names(value: Any) -> list[str]:
|
|
1098
|
+
if not isinstance(value, list):
|
|
1099
|
+
return []
|
|
1100
|
+
tool_names = {
|
|
1101
|
+
_runtime_scalar(item.get("tool"))
|
|
1102
|
+
for item in value
|
|
1103
|
+
if isinstance(item, dict) and _runtime_scalar(item.get("tool"))
|
|
1104
|
+
}
|
|
1105
|
+
return sorted(tool_names)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _observation_error_code_counts(value: Any) -> Dict[str, str]:
|
|
1109
|
+
if not isinstance(value, list):
|
|
1110
|
+
return {}
|
|
1111
|
+
counts: Dict[str, int] = {}
|
|
1112
|
+
for item in value:
|
|
1113
|
+
if not isinstance(item, dict):
|
|
1114
|
+
continue
|
|
1115
|
+
error_code = _runtime_scalar(item.get("error_code"))
|
|
1116
|
+
if not error_code:
|
|
1117
|
+
continue
|
|
1118
|
+
counts[error_code] = counts.get(error_code, 0) + 1
|
|
1119
|
+
return {error_code: str(counts[error_code]) for error_code in sorted(counts)}
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _latest_plan_actions(value: Any) -> list[Dict[str, Any]]:
|
|
1123
|
+
if not isinstance(value, dict):
|
|
1124
|
+
return []
|
|
1125
|
+
actions = value.get("actions")
|
|
1126
|
+
if not isinstance(actions, list):
|
|
1127
|
+
return []
|
|
1128
|
+
return [action for action in actions if isinstance(action, dict)]
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _latest_plan_action_count(value: Any) -> int:
|
|
1132
|
+
return len(_latest_plan_actions(value))
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def _latest_plan_action_ids(value: Any) -> list[str]:
|
|
1136
|
+
return [
|
|
1137
|
+
_runtime_scalar(action.get("id"))
|
|
1138
|
+
for action in _latest_plan_actions(value)
|
|
1139
|
+
if _runtime_scalar(action.get("id"))
|
|
1140
|
+
]
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _plan_dependency_edge_count(value: Any) -> int:
|
|
1144
|
+
edge_count = 0
|
|
1145
|
+
for action in _latest_plan_actions(value):
|
|
1146
|
+
depends_on = action.get("depends_on")
|
|
1147
|
+
if isinstance(depends_on, list):
|
|
1148
|
+
edge_count += sum(
|
|
1149
|
+
1 for dependency in depends_on if isinstance(dependency, str) and dependency.strip()
|
|
1150
|
+
)
|
|
1151
|
+
return edge_count
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _observation_artifact_ids(value: Any) -> list[str]:
|
|
1155
|
+
if not isinstance(value, list):
|
|
1156
|
+
return []
|
|
1157
|
+
artifact_ids = {
|
|
1158
|
+
_runtime_scalar(output.get("artifact_id"))
|
|
1159
|
+
for item in value
|
|
1160
|
+
if isinstance(item, dict)
|
|
1161
|
+
for output in [item.get("output")]
|
|
1162
|
+
if isinstance(output, dict) and _runtime_scalar(output.get("artifact_id"))
|
|
1163
|
+
}
|
|
1164
|
+
return sorted(artifact_ids)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _observation_artifact_kinds(value: Any) -> list[str]:
|
|
1168
|
+
if not isinstance(value, list):
|
|
1169
|
+
return []
|
|
1170
|
+
artifact_kinds = {
|
|
1171
|
+
_runtime_scalar(output.get("kind"))
|
|
1172
|
+
for item in value
|
|
1173
|
+
if isinstance(item, dict)
|
|
1174
|
+
for output in [item.get("output")]
|
|
1175
|
+
if isinstance(output, dict)
|
|
1176
|
+
and _runtime_scalar(output.get("artifact_id"))
|
|
1177
|
+
and _runtime_scalar(output.get("kind"))
|
|
1178
|
+
}
|
|
1179
|
+
return sorted(artifact_kinds)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def _observation_artifact_formats(value: Any) -> list[str]:
|
|
1183
|
+
if not isinstance(value, list):
|
|
1184
|
+
return []
|
|
1185
|
+
artifact_formats = {
|
|
1186
|
+
_runtime_scalar(output.get("format"))
|
|
1187
|
+
for item in value
|
|
1188
|
+
if isinstance(item, dict)
|
|
1189
|
+
for output in [item.get("output")]
|
|
1190
|
+
if isinstance(output, dict)
|
|
1191
|
+
and _runtime_scalar(output.get("artifact_id"))
|
|
1192
|
+
and _runtime_scalar(output.get("format"))
|
|
1193
|
+
}
|
|
1194
|
+
return sorted(artifact_formats)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _observation_artifact_tags(value: Any) -> list[str]:
|
|
1198
|
+
if not isinstance(value, list):
|
|
1199
|
+
return []
|
|
1200
|
+
artifact_tags = {
|
|
1201
|
+
tag.strip()
|
|
1202
|
+
for item in value
|
|
1203
|
+
if isinstance(item, dict)
|
|
1204
|
+
for output in [item.get("output")]
|
|
1205
|
+
if isinstance(output, dict)
|
|
1206
|
+
and _runtime_scalar(output.get("artifact_id"))
|
|
1207
|
+
for tags in [output.get("tags")]
|
|
1208
|
+
if isinstance(tags, list)
|
|
1209
|
+
for tag in tags
|
|
1210
|
+
if isinstance(tag, str) and tag.strip()
|
|
1211
|
+
}
|
|
1212
|
+
return sorted(artifact_tags)
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _observation_artifact_total_bytes(value: Any) -> int:
|
|
1216
|
+
return sum(byte_count for _, byte_count in _observation_artifact_byte_records(value))
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def _observation_artifact_bytes_by_kind(value: Any) -> Dict[str, str]:
|
|
1220
|
+
counts: Dict[str, int] = {}
|
|
1221
|
+
for kind, byte_count in _observation_artifact_byte_records(value):
|
|
1222
|
+
if not kind:
|
|
1223
|
+
continue
|
|
1224
|
+
counts[kind] = counts.get(kind, 0) + byte_count
|
|
1225
|
+
return {kind: str(counts[kind]) for kind in sorted(counts)}
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _observation_artifact_byte_records(value: Any) -> list[tuple[str, int]]:
|
|
1229
|
+
if not isinstance(value, list):
|
|
1230
|
+
return []
|
|
1231
|
+
records: Dict[str, tuple[str, int]] = {}
|
|
1232
|
+
for item in value:
|
|
1233
|
+
if not isinstance(item, dict):
|
|
1234
|
+
continue
|
|
1235
|
+
output = item.get("output")
|
|
1236
|
+
if not isinstance(output, dict):
|
|
1237
|
+
continue
|
|
1238
|
+
artifact_id = _runtime_scalar(output.get("artifact_id"))
|
|
1239
|
+
if not artifact_id or artifact_id in records:
|
|
1240
|
+
continue
|
|
1241
|
+
byte_count = _artifact_byte_count(output.get("bytes"))
|
|
1242
|
+
if byte_count is None:
|
|
1243
|
+
continue
|
|
1244
|
+
records[artifact_id] = (_runtime_scalar(output.get("kind")), byte_count)
|
|
1245
|
+
return [records[artifact_id] for artifact_id in sorted(records)]
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _artifact_byte_count(value: Any) -> int | None:
|
|
1249
|
+
if isinstance(value, bool):
|
|
1250
|
+
return None
|
|
1251
|
+
if isinstance(value, int) and value >= 0:
|
|
1252
|
+
return value
|
|
1253
|
+
if isinstance(value, float) and value >= 0 and value.is_integer():
|
|
1254
|
+
return int(value)
|
|
1255
|
+
if isinstance(value, str) and value.strip().isdigit():
|
|
1256
|
+
return int(value.strip())
|
|
1257
|
+
return None
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _runtime_trace_artifact(
|
|
1261
|
+
trace: Dict[str, Any],
|
|
1262
|
+
artifact_id: str,
|
|
1263
|
+
) -> Dict[str, Any] | None:
|
|
1264
|
+
observations = trace.get("observations")
|
|
1265
|
+
if not isinstance(observations, list):
|
|
1266
|
+
return None
|
|
1267
|
+
for observation in observations:
|
|
1268
|
+
if not isinstance(observation, dict):
|
|
1269
|
+
continue
|
|
1270
|
+
output = observation.get("output")
|
|
1271
|
+
if not isinstance(output, dict):
|
|
1272
|
+
continue
|
|
1273
|
+
if output.get("artifact_id") != artifact_id:
|
|
1274
|
+
continue
|
|
1275
|
+
return {
|
|
1276
|
+
"action_id": _artifact_metadata_string(observation.get("action_id")),
|
|
1277
|
+
"tool": _artifact_metadata_string(observation.get("tool")),
|
|
1278
|
+
"artifact": _runtime_trace_artifact_body(output),
|
|
1279
|
+
}
|
|
1280
|
+
return None
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def _runtime_trace_artifact_body(output: Dict[str, Any]) -> Dict[str, Any]:
|
|
1284
|
+
bytes_count = _artifact_byte_count(output.get("bytes"))
|
|
1285
|
+
return {
|
|
1286
|
+
"artifact_id": _artifact_metadata_string(output.get("artifact_id")),
|
|
1287
|
+
"title": _artifact_metadata_string(output.get("title")),
|
|
1288
|
+
"kind": _artifact_metadata_string(output.get("kind")),
|
|
1289
|
+
"format": _artifact_metadata_string(output.get("format")),
|
|
1290
|
+
"content": _artifact_metadata_string(output.get("content")),
|
|
1291
|
+
"tags": _artifact_tag_list(output.get("tags")),
|
|
1292
|
+
"bytes": "" if bytes_count is None else bytes_count,
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def _runtime_trace_artifacts(trace: Dict[str, Any]) -> list[Dict[str, Any]]:
|
|
1297
|
+
observations = trace.get("observations")
|
|
1298
|
+
if not isinstance(observations, list):
|
|
1299
|
+
return []
|
|
1300
|
+
artifacts = []
|
|
1301
|
+
for observation in observations:
|
|
1302
|
+
if not isinstance(observation, dict):
|
|
1303
|
+
continue
|
|
1304
|
+
output = observation.get("output")
|
|
1305
|
+
if not isinstance(output, dict):
|
|
1306
|
+
continue
|
|
1307
|
+
artifact_id = _artifact_metadata_string(output.get("artifact_id"))
|
|
1308
|
+
if not artifact_id.strip():
|
|
1309
|
+
continue
|
|
1310
|
+
bytes_count = _artifact_byte_count(output.get("bytes"))
|
|
1311
|
+
artifacts.append(
|
|
1312
|
+
{
|
|
1313
|
+
"artifact_id": artifact_id,
|
|
1314
|
+
"action_id": _artifact_metadata_string(observation.get("action_id")),
|
|
1315
|
+
"tool": _artifact_metadata_string(observation.get("tool")),
|
|
1316
|
+
"title": _artifact_metadata_string(output.get("title")),
|
|
1317
|
+
"kind": _artifact_metadata_string(output.get("kind")),
|
|
1318
|
+
"format": _artifact_metadata_string(output.get("format")),
|
|
1319
|
+
"tags": _artifact_tag_list(output.get("tags")),
|
|
1320
|
+
"bytes": "" if bytes_count is None else str(bytes_count),
|
|
1321
|
+
}
|
|
1322
|
+
)
|
|
1323
|
+
return artifacts
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def _artifact_metadata_string(value: Any) -> str:
|
|
1327
|
+
if not isinstance(value, str):
|
|
1328
|
+
return ""
|
|
1329
|
+
return value.strip()
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def _artifact_tag_list(value: Any) -> list[str]:
|
|
1333
|
+
if not isinstance(value, list):
|
|
1334
|
+
return []
|
|
1335
|
+
return [tag for tag in value if isinstance(tag, str) and tag.strip()]
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _runtime_timeline_events(value: Any) -> list[Dict[str, Any]]:
|
|
1339
|
+
if not isinstance(value, list):
|
|
1340
|
+
return []
|
|
1341
|
+
fields = [
|
|
1342
|
+
"node",
|
|
1343
|
+
"status",
|
|
1344
|
+
"iteration",
|
|
1345
|
+
"action_id",
|
|
1346
|
+
"tool",
|
|
1347
|
+
"reason",
|
|
1348
|
+
"action_count",
|
|
1349
|
+
"duration_seconds",
|
|
1350
|
+
]
|
|
1351
|
+
events = []
|
|
1352
|
+
for item in value:
|
|
1353
|
+
if not isinstance(item, dict):
|
|
1354
|
+
continue
|
|
1355
|
+
event = {}
|
|
1356
|
+
for field in fields:
|
|
1357
|
+
if field not in item:
|
|
1358
|
+
continue
|
|
1359
|
+
field_value = _runtime_timeline_scalar(item[field])
|
|
1360
|
+
if field_value:
|
|
1361
|
+
event[field] = field_value
|
|
1362
|
+
depends_on = item.get("depends_on")
|
|
1363
|
+
if isinstance(depends_on, list):
|
|
1364
|
+
event["depends_on"] = [
|
|
1365
|
+
dependency
|
|
1366
|
+
for dependency in depends_on
|
|
1367
|
+
if isinstance(dependency, str) and dependency.strip()
|
|
1368
|
+
]
|
|
1369
|
+
dependency_statuses = item.get("dependency_statuses")
|
|
1370
|
+
if isinstance(dependency_statuses, dict):
|
|
1371
|
+
compact_statuses = {}
|
|
1372
|
+
for action_id, status in dependency_statuses.items():
|
|
1373
|
+
action_id_value = _runtime_timeline_scalar(action_id)
|
|
1374
|
+
status_value = _runtime_timeline_scalar(status)
|
|
1375
|
+
if action_id_value and status_value:
|
|
1376
|
+
compact_statuses[action_id_value] = status_value
|
|
1377
|
+
if compact_statuses:
|
|
1378
|
+
event["dependency_statuses"] = compact_statuses
|
|
1379
|
+
if event:
|
|
1380
|
+
events.append(event)
|
|
1381
|
+
return events
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def _runtime_timeline_scalar(value: Any) -> str:
|
|
1385
|
+
if isinstance(value, bool):
|
|
1386
|
+
return ""
|
|
1387
|
+
if isinstance(value, str):
|
|
1388
|
+
return value.strip()
|
|
1389
|
+
if isinstance(value, int):
|
|
1390
|
+
return str(value)
|
|
1391
|
+
if isinstance(value, float):
|
|
1392
|
+
return str(value)
|
|
1393
|
+
return ""
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def _runtime_timeline_observations(value: Any) -> list[Dict[str, str]]:
|
|
1397
|
+
if not isinstance(value, list):
|
|
1398
|
+
return []
|
|
1399
|
+
observations = []
|
|
1400
|
+
for item in value:
|
|
1401
|
+
if not isinstance(item, dict):
|
|
1402
|
+
continue
|
|
1403
|
+
observation = {
|
|
1404
|
+
"action_id": _runtime_timeline_scalar(item.get("action_id")),
|
|
1405
|
+
"tool": _runtime_timeline_scalar(item.get("tool")),
|
|
1406
|
+
"status": _runtime_timeline_scalar(item.get("status")),
|
|
1407
|
+
}
|
|
1408
|
+
error_code = _runtime_timeline_scalar(item.get("error_code"))
|
|
1409
|
+
if error_code:
|
|
1410
|
+
observation["error_code"] = error_code
|
|
1411
|
+
output = item.get("output")
|
|
1412
|
+
artifact_id = (
|
|
1413
|
+
_runtime_timeline_scalar(output.get("artifact_id"))
|
|
1414
|
+
if isinstance(output, dict)
|
|
1415
|
+
else ""
|
|
1416
|
+
)
|
|
1417
|
+
if artifact_id:
|
|
1418
|
+
observation["artifact_id"] = artifact_id
|
|
1419
|
+
observations.append(observation)
|
|
1420
|
+
return observations
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def _runtime_timeline_progress_events(value: Any) -> list[Dict[str, str]]:
|
|
1424
|
+
if not isinstance(value, list):
|
|
1425
|
+
return []
|
|
1426
|
+
fields = [
|
|
1427
|
+
"run_id",
|
|
1428
|
+
"type",
|
|
1429
|
+
"node",
|
|
1430
|
+
"status",
|
|
1431
|
+
"iteration",
|
|
1432
|
+
"action_id",
|
|
1433
|
+
"tool",
|
|
1434
|
+
"reason",
|
|
1435
|
+
"error_code",
|
|
1436
|
+
"action_count",
|
|
1437
|
+
"iteration_count",
|
|
1438
|
+
"duration_seconds",
|
|
1439
|
+
]
|
|
1440
|
+
progress_events = []
|
|
1441
|
+
for item in value:
|
|
1442
|
+
if not isinstance(item, dict):
|
|
1443
|
+
continue
|
|
1444
|
+
event = {}
|
|
1445
|
+
for field in fields:
|
|
1446
|
+
if field not in item:
|
|
1447
|
+
continue
|
|
1448
|
+
field_value = _runtime_timeline_scalar(item[field])
|
|
1449
|
+
if field_value:
|
|
1450
|
+
event[field] = field_value
|
|
1451
|
+
if event:
|
|
1452
|
+
progress_events.append(event)
|
|
1453
|
+
return progress_events
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def _runtime_timeline_graph_phases(value: Any) -> list[Dict[str, str]]:
|
|
1457
|
+
if not isinstance(value, list):
|
|
1458
|
+
return []
|
|
1459
|
+
fields = [
|
|
1460
|
+
"node",
|
|
1461
|
+
"status",
|
|
1462
|
+
"started_at",
|
|
1463
|
+
"completed_at",
|
|
1464
|
+
"duration_seconds",
|
|
1465
|
+
]
|
|
1466
|
+
phases = []
|
|
1467
|
+
for item in value:
|
|
1468
|
+
if not isinstance(item, dict):
|
|
1469
|
+
continue
|
|
1470
|
+
phase = {}
|
|
1471
|
+
for field in fields:
|
|
1472
|
+
if field not in item:
|
|
1473
|
+
continue
|
|
1474
|
+
field_value = _runtime_timeline_scalar(item[field])
|
|
1475
|
+
if field_value:
|
|
1476
|
+
phase[field] = field_value
|
|
1477
|
+
if phase.get("node") and phase.get("status"):
|
|
1478
|
+
phases.append(phase)
|
|
1479
|
+
return phases
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _runtime_graph_phase_node_counts(value: Any) -> Dict[str, str]:
|
|
1483
|
+
counts: Dict[str, str] = {}
|
|
1484
|
+
for phase in _runtime_timeline_graph_phases(value):
|
|
1485
|
+
_increment_count(counts, phase.get("node", ""))
|
|
1486
|
+
return dict(sorted(counts.items()))
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def _runtime_compact_steps(trace: Dict[str, Any]) -> list[Dict[str, str]]:
|
|
1490
|
+
raw_steps = trace.get("steps")
|
|
1491
|
+
if isinstance(raw_steps, list):
|
|
1492
|
+
steps = _runtime_sanitized_steps(raw_steps)
|
|
1493
|
+
if steps:
|
|
1494
|
+
return steps
|
|
1495
|
+
return _runtime_sanitized_steps(derive_runtime_steps(trace))
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def _runtime_sanitized_steps(value: Any) -> list[Dict[str, str]]:
|
|
1499
|
+
if not isinstance(value, list):
|
|
1500
|
+
return []
|
|
1501
|
+
steps = []
|
|
1502
|
+
for item in value:
|
|
1503
|
+
if not isinstance(item, dict):
|
|
1504
|
+
continue
|
|
1505
|
+
index = _runtime_timeline_scalar(item.get("index"))
|
|
1506
|
+
state = _runtime_timeline_scalar(item.get("state"))
|
|
1507
|
+
title = _runtime_timeline_scalar(item.get("title"))
|
|
1508
|
+
if state not in {"done", "failed", "pending", "waiting_approval"}:
|
|
1509
|
+
continue
|
|
1510
|
+
if not index or not title:
|
|
1511
|
+
continue
|
|
1512
|
+
step = {
|
|
1513
|
+
"index": index,
|
|
1514
|
+
"state": state,
|
|
1515
|
+
"title": title,
|
|
1516
|
+
}
|
|
1517
|
+
detail = _runtime_timeline_scalar(item.get("detail"))
|
|
1518
|
+
if detail:
|
|
1519
|
+
step["detail"] = detail
|
|
1520
|
+
steps.append(step)
|
|
1521
|
+
return steps
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
def _runtime_trace_path(
|
|
1525
|
+
trace: Dict[str, Any],
|
|
1526
|
+
trace_dir: str,
|
|
1527
|
+
requested_run_id: str,
|
|
1528
|
+
) -> str:
|
|
1529
|
+
return str(Path(trace_dir) / f"{safe_trace_file_stem(requested_run_id)}.json")
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def _trace_file_sort_key(path: Path) -> tuple[int, str]:
|
|
1533
|
+
try:
|
|
1534
|
+
mtime_ns = path.stat().st_mtime_ns
|
|
1535
|
+
except OSError:
|
|
1536
|
+
mtime_ns = 0
|
|
1537
|
+
return (mtime_ns, path.stem)
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _encode_runtime_list_cursor(sort_key: tuple[int, str]) -> str:
|
|
1541
|
+
raw_cursor = f"{sort_key[0]}:{sort_key[1]}".encode("utf-8")
|
|
1542
|
+
return base64.urlsafe_b64encode(raw_cursor).decode("ascii").rstrip("=")
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def _decode_runtime_list_cursor(value: str) -> tuple[int, str]:
|
|
1546
|
+
padding = "=" * (-len(value) % 4)
|
|
1547
|
+
try:
|
|
1548
|
+
raw_cursor = base64.urlsafe_b64decode(f"{value}{padding}").decode("utf-8")
|
|
1549
|
+
except (binascii.Error, UnicodeDecodeError) as exc:
|
|
1550
|
+
raise ValueError("cursor must be a valid runtime list cursor") from exc
|
|
1551
|
+
mtime_ns_raw, separator, trace_stem = raw_cursor.partition(":")
|
|
1552
|
+
if (
|
|
1553
|
+
separator != ":"
|
|
1554
|
+
or not mtime_ns_raw.isdigit()
|
|
1555
|
+
or not trace_stem
|
|
1556
|
+
):
|
|
1557
|
+
raise ValueError("cursor must be a valid runtime list cursor")
|
|
1558
|
+
return (int(mtime_ns_raw), trace_stem)
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _runtime_list_limit(query: str) -> int:
|
|
1562
|
+
values = parse_qs(query).get("limit", ["50"])
|
|
1563
|
+
try:
|
|
1564
|
+
limit = int(values[0])
|
|
1565
|
+
except ValueError as exc:
|
|
1566
|
+
raise ValueError("limit must be an integer between 1 and 100") from exc
|
|
1567
|
+
if limit < 1 or limit > 100:
|
|
1568
|
+
raise ValueError("limit must be an integer between 1 and 100")
|
|
1569
|
+
return limit
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def _runtime_list_cursor_key(query: str) -> tuple[int, str] | None:
|
|
1573
|
+
values = parse_qs(query, keep_blank_values=True)
|
|
1574
|
+
cursor = _single_query_value(values, "cursor")
|
|
1575
|
+
if cursor is None:
|
|
1576
|
+
return None
|
|
1577
|
+
if not cursor.strip():
|
|
1578
|
+
raise ValueError("cursor must be a non-empty string")
|
|
1579
|
+
return _decode_runtime_list_cursor(cursor)
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _runtime_list_filters(query: str) -> Dict[str, Any]:
|
|
1583
|
+
values = parse_qs(query, keep_blank_values=True)
|
|
1584
|
+
status = _single_query_value(values, "status")
|
|
1585
|
+
if status is not None and status not in _RUNTIME_STATUS_FILTER_VALUES:
|
|
1586
|
+
raise ValueError(
|
|
1587
|
+
"status must be one of: cancelled, done, failed, requires_approval, "
|
|
1588
|
+
"resumed, resuming, running"
|
|
1589
|
+
)
|
|
1590
|
+
lifecycle_state = _single_query_value(values, "lifecycle_state")
|
|
1591
|
+
if (
|
|
1592
|
+
lifecycle_state is not None
|
|
1593
|
+
and lifecycle_state not in _RUNTIME_LIFECYCLE_STATE_FILTER_VALUES
|
|
1594
|
+
):
|
|
1595
|
+
raise ValueError(
|
|
1596
|
+
"lifecycle_state must be one of: cancelled, failed, planning, "
|
|
1597
|
+
"running, succeeded, unknown, waiting_approval"
|
|
1598
|
+
)
|
|
1599
|
+
auth_subject = _single_query_value(values, "auth_subject")
|
|
1600
|
+
if auth_subject is not None and not auth_subject.strip():
|
|
1601
|
+
raise ValueError("auth_subject must be a non-empty string")
|
|
1602
|
+
tool = _single_query_value(values, "tool")
|
|
1603
|
+
if tool is not None and not tool.strip():
|
|
1604
|
+
raise ValueError("tool must be a non-empty string")
|
|
1605
|
+
error_code = _single_query_value(values, "error_code")
|
|
1606
|
+
if error_code is not None and not error_code.strip():
|
|
1607
|
+
raise ValueError("error_code must be a non-empty string")
|
|
1608
|
+
latest_failed_error_code = _single_query_value(values, "latest_failed_error_code")
|
|
1609
|
+
if latest_failed_error_code is not None and not latest_failed_error_code.strip():
|
|
1610
|
+
raise ValueError("latest_failed_error_code must be a non-empty string")
|
|
1611
|
+
latest_failed_action_id = _single_query_value(values, "latest_failed_action_id")
|
|
1612
|
+
if latest_failed_action_id is not None and not latest_failed_action_id.strip():
|
|
1613
|
+
raise ValueError("latest_failed_action_id must be a non-empty string")
|
|
1614
|
+
latest_failed_tool = _single_query_value(values, "latest_failed_tool")
|
|
1615
|
+
if latest_failed_tool is not None and not latest_failed_tool.strip():
|
|
1616
|
+
raise ValueError("latest_failed_tool must be a non-empty string")
|
|
1617
|
+
llm_provider_status = _single_query_value(values, "llm_provider_status")
|
|
1618
|
+
if llm_provider_status is not None and not llm_provider_status.strip():
|
|
1619
|
+
raise ValueError("llm_provider_status must be a non-empty string")
|
|
1620
|
+
llm_provider_error_type = _single_query_value(values, "llm_provider_error_type")
|
|
1621
|
+
if llm_provider_error_type is not None and not llm_provider_error_type.strip():
|
|
1622
|
+
raise ValueError("llm_provider_error_type must be a non-empty string")
|
|
1623
|
+
llm_provider_http_status = _single_query_value(values, "llm_provider_http_status")
|
|
1624
|
+
if llm_provider_http_status is not None and not llm_provider_http_status.strip():
|
|
1625
|
+
raise ValueError("llm_provider_http_status must be a non-empty string")
|
|
1626
|
+
llm_provider_retryable_reason = _single_query_value(
|
|
1627
|
+
values,
|
|
1628
|
+
"llm_provider_retryable_reason",
|
|
1629
|
+
)
|
|
1630
|
+
if (
|
|
1631
|
+
llm_provider_retryable_reason is not None
|
|
1632
|
+
and not llm_provider_retryable_reason.strip()
|
|
1633
|
+
):
|
|
1634
|
+
raise ValueError("llm_provider_retryable_reason must be a non-empty string")
|
|
1635
|
+
iteration_budget_remaining = _single_query_value(
|
|
1636
|
+
values,
|
|
1637
|
+
"iteration_budget_remaining",
|
|
1638
|
+
)
|
|
1639
|
+
if (
|
|
1640
|
+
iteration_budget_remaining is not None
|
|
1641
|
+
and not iteration_budget_remaining.strip()
|
|
1642
|
+
):
|
|
1643
|
+
raise ValueError("iteration_budget_remaining must be a non-negative integer")
|
|
1644
|
+
if (
|
|
1645
|
+
iteration_budget_remaining is not None
|
|
1646
|
+
and not iteration_budget_remaining.isdigit()
|
|
1647
|
+
):
|
|
1648
|
+
raise ValueError("iteration_budget_remaining must be a non-negative integer")
|
|
1649
|
+
artifact_kind = _single_query_value(values, "artifact_kind")
|
|
1650
|
+
if artifact_kind is not None and not artifact_kind.strip():
|
|
1651
|
+
raise ValueError("artifact_kind must be a non-empty string")
|
|
1652
|
+
artifact_format = _single_query_value(values, "artifact_format")
|
|
1653
|
+
if artifact_format is not None and not artifact_format.strip():
|
|
1654
|
+
raise ValueError("artifact_format must be a non-empty string")
|
|
1655
|
+
artifact_tag = _single_query_value(values, "artifact_tag")
|
|
1656
|
+
if artifact_tag is not None and not artifact_tag.strip():
|
|
1657
|
+
raise ValueError("artifact_tag must be a non-empty string")
|
|
1658
|
+
tag = _single_query_value(values, "tag")
|
|
1659
|
+
if tag is not None and not tag.strip():
|
|
1660
|
+
raise ValueError("tag must be a non-empty string")
|
|
1661
|
+
metadata_key = _single_query_value(values, "metadata_key")
|
|
1662
|
+
if metadata_key is not None and not metadata_key.strip():
|
|
1663
|
+
raise ValueError("metadata_key must be a non-empty string")
|
|
1664
|
+
metadata_value = _single_query_value(values, "metadata_value")
|
|
1665
|
+
if metadata_value is not None and not metadata_value.strip():
|
|
1666
|
+
raise ValueError("metadata_value must be a non-empty string")
|
|
1667
|
+
approved_action_id = _single_query_value(values, "approved_action_id")
|
|
1668
|
+
if approved_action_id is not None and not approved_action_id.strip():
|
|
1669
|
+
raise ValueError("approved_action_id must be a non-empty string")
|
|
1670
|
+
approved_by_auth_subject = _single_query_value(values, "approved_by_auth_subject")
|
|
1671
|
+
if approved_by_auth_subject is not None and not approved_by_auth_subject.strip():
|
|
1672
|
+
raise ValueError("approved_by_auth_subject must be a non-empty string")
|
|
1673
|
+
resumed_from_run_id = _single_query_value(values, "resumed_from_run_id")
|
|
1674
|
+
if resumed_from_run_id is not None and not resumed_from_run_id.strip():
|
|
1675
|
+
raise ValueError("resumed_from_run_id must be a non-empty string")
|
|
1676
|
+
resumed_by_auth_subject = _single_query_value(values, "resumed_by_auth_subject")
|
|
1677
|
+
if resumed_by_auth_subject is not None and not resumed_by_auth_subject.strip():
|
|
1678
|
+
raise ValueError("resumed_by_auth_subject must be a non-empty string")
|
|
1679
|
+
pending_approval_tool = _single_query_value(values, "pending_approval_tool")
|
|
1680
|
+
if pending_approval_tool is not None and not pending_approval_tool.strip():
|
|
1681
|
+
raise ValueError("pending_approval_tool must be a non-empty string")
|
|
1682
|
+
pending_approval_action_id = _single_query_value(
|
|
1683
|
+
values,
|
|
1684
|
+
"pending_approval_action_id",
|
|
1685
|
+
)
|
|
1686
|
+
if (
|
|
1687
|
+
pending_approval_action_id is not None
|
|
1688
|
+
and not pending_approval_action_id.strip()
|
|
1689
|
+
):
|
|
1690
|
+
raise ValueError("pending_approval_action_id must be a non-empty string")
|
|
1691
|
+
final_answer_guardrail_reason = _single_query_value(
|
|
1692
|
+
values,
|
|
1693
|
+
"final_answer_guardrail_reason",
|
|
1694
|
+
)
|
|
1695
|
+
if (
|
|
1696
|
+
final_answer_guardrail_reason is not None
|
|
1697
|
+
and not final_answer_guardrail_reason.strip()
|
|
1698
|
+
):
|
|
1699
|
+
raise ValueError("final_answer_guardrail_reason must be a non-empty string")
|
|
1700
|
+
min_pending_age_seconds = _single_query_value(values, "min_pending_age_seconds")
|
|
1701
|
+
if (
|
|
1702
|
+
min_pending_age_seconds is not None
|
|
1703
|
+
and not min_pending_age_seconds.strip()
|
|
1704
|
+
):
|
|
1705
|
+
raise ValueError("min_pending_age_seconds must be a non-negative integer")
|
|
1706
|
+
if (
|
|
1707
|
+
min_pending_age_seconds is not None
|
|
1708
|
+
and not min_pending_age_seconds.isdigit()
|
|
1709
|
+
):
|
|
1710
|
+
raise ValueError("min_pending_age_seconds must be a non-negative integer")
|
|
1711
|
+
has_artifacts_value = _single_query_value(values, "has_artifacts")
|
|
1712
|
+
has_errors_value = _single_query_value(values, "has_errors")
|
|
1713
|
+
has_failures_value = _single_query_value(values, "has_failures")
|
|
1714
|
+
has_approvals_value = _single_query_value(values, "has_approvals")
|
|
1715
|
+
has_pending_approval_value = _single_query_value(values, "has_pending_approval")
|
|
1716
|
+
has_final_answer_guardrail_value = _single_query_value(
|
|
1717
|
+
values,
|
|
1718
|
+
"has_final_answer_guardrail",
|
|
1719
|
+
)
|
|
1720
|
+
has_llm_provider_retries_value = _single_query_value(
|
|
1721
|
+
values,
|
|
1722
|
+
"has_llm_provider_retries",
|
|
1723
|
+
)
|
|
1724
|
+
return {
|
|
1725
|
+
"status": status,
|
|
1726
|
+
"lifecycle_state": lifecycle_state,
|
|
1727
|
+
"auth_subject": auth_subject,
|
|
1728
|
+
"tool": tool,
|
|
1729
|
+
"error_code": error_code,
|
|
1730
|
+
"latest_failed_error_code": latest_failed_error_code,
|
|
1731
|
+
"latest_failed_action_id": latest_failed_action_id,
|
|
1732
|
+
"latest_failed_tool": latest_failed_tool,
|
|
1733
|
+
"llm_provider_status": llm_provider_status,
|
|
1734
|
+
"llm_provider_error_type": llm_provider_error_type,
|
|
1735
|
+
"llm_provider_http_status": llm_provider_http_status,
|
|
1736
|
+
"llm_provider_retryable_reason": llm_provider_retryable_reason,
|
|
1737
|
+
"iteration_budget_remaining": iteration_budget_remaining,
|
|
1738
|
+
"artifact_kind": artifact_kind,
|
|
1739
|
+
"artifact_format": artifact_format,
|
|
1740
|
+
"artifact_tag": artifact_tag,
|
|
1741
|
+
"tag": tag,
|
|
1742
|
+
"metadata_key": metadata_key,
|
|
1743
|
+
"metadata_value": metadata_value,
|
|
1744
|
+
"approved_action_id": approved_action_id,
|
|
1745
|
+
"approved_by_auth_subject": approved_by_auth_subject,
|
|
1746
|
+
"resumed_from_run_id": resumed_from_run_id,
|
|
1747
|
+
"resumed_by_auth_subject": resumed_by_auth_subject,
|
|
1748
|
+
"pending_approval_tool": pending_approval_tool,
|
|
1749
|
+
"pending_approval_action_id": pending_approval_action_id,
|
|
1750
|
+
"final_answer_guardrail_reason": final_answer_guardrail_reason,
|
|
1751
|
+
"min_pending_age_seconds": min_pending_age_seconds,
|
|
1752
|
+
"has_artifacts": _parse_optional_boolean(
|
|
1753
|
+
has_artifacts_value,
|
|
1754
|
+
field_name="has_artifacts",
|
|
1755
|
+
),
|
|
1756
|
+
"has_errors": _parse_optional_boolean(
|
|
1757
|
+
has_errors_value,
|
|
1758
|
+
field_name="has_errors",
|
|
1759
|
+
),
|
|
1760
|
+
"has_failures": _parse_optional_boolean(
|
|
1761
|
+
has_failures_value,
|
|
1762
|
+
field_name="has_failures",
|
|
1763
|
+
),
|
|
1764
|
+
"has_approvals": _parse_optional_boolean(
|
|
1765
|
+
has_approvals_value,
|
|
1766
|
+
field_name="has_approvals",
|
|
1767
|
+
),
|
|
1768
|
+
"has_pending_approval": _parse_optional_boolean(
|
|
1769
|
+
has_pending_approval_value,
|
|
1770
|
+
field_name="has_pending_approval",
|
|
1771
|
+
),
|
|
1772
|
+
"has_final_answer_guardrail": _parse_optional_boolean(
|
|
1773
|
+
has_final_answer_guardrail_value,
|
|
1774
|
+
field_name="has_final_answer_guardrail",
|
|
1775
|
+
),
|
|
1776
|
+
"has_llm_provider_retries": _parse_optional_boolean(
|
|
1777
|
+
has_llm_provider_retries_value,
|
|
1778
|
+
field_name="has_llm_provider_retries",
|
|
1779
|
+
),
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
def _single_query_value(values: Dict[str, list[str]], key: str) -> str | None:
|
|
1784
|
+
if key not in values:
|
|
1785
|
+
return None
|
|
1786
|
+
return values[key][0]
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def _parse_optional_boolean(value: str | None, *, field_name: str) -> bool | None:
|
|
1790
|
+
if value is None:
|
|
1791
|
+
return None
|
|
1792
|
+
if value == "true":
|
|
1793
|
+
return True
|
|
1794
|
+
if value == "false":
|
|
1795
|
+
return False
|
|
1796
|
+
raise ValueError(f"{field_name} must be true or false")
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
def _runtime_summary_matches_filters(
|
|
1800
|
+
summary: Dict[str, Any],
|
|
1801
|
+
filters: Dict[str, Any],
|
|
1802
|
+
) -> bool:
|
|
1803
|
+
status = filters["status"]
|
|
1804
|
+
if status is not None and summary.get("status") != status:
|
|
1805
|
+
return False
|
|
1806
|
+
lifecycle_state = filters["lifecycle_state"]
|
|
1807
|
+
if (
|
|
1808
|
+
lifecycle_state is not None
|
|
1809
|
+
and summary.get("lifecycle_state") != lifecycle_state
|
|
1810
|
+
):
|
|
1811
|
+
return False
|
|
1812
|
+
auth_subject = filters["auth_subject"]
|
|
1813
|
+
if auth_subject is not None and summary.get("auth_subject") != auth_subject:
|
|
1814
|
+
return False
|
|
1815
|
+
tool = filters["tool"]
|
|
1816
|
+
if tool is not None and tool not in summary.get("tool_names", []):
|
|
1817
|
+
return False
|
|
1818
|
+
error_code = filters["error_code"]
|
|
1819
|
+
error_code_counts = summary.get("error_code_counts")
|
|
1820
|
+
if (
|
|
1821
|
+
error_code is not None
|
|
1822
|
+
and (
|
|
1823
|
+
not isinstance(error_code_counts, dict)
|
|
1824
|
+
or error_code not in error_code_counts
|
|
1825
|
+
)
|
|
1826
|
+
):
|
|
1827
|
+
return False
|
|
1828
|
+
latest_failed_error_code = filters["latest_failed_error_code"]
|
|
1829
|
+
if (
|
|
1830
|
+
latest_failed_error_code is not None
|
|
1831
|
+
and summary.get("latest_failed_error_code") != latest_failed_error_code
|
|
1832
|
+
):
|
|
1833
|
+
return False
|
|
1834
|
+
latest_failed_action_id = filters["latest_failed_action_id"]
|
|
1835
|
+
if (
|
|
1836
|
+
latest_failed_action_id is not None
|
|
1837
|
+
and summary.get("latest_failed_action_id") != latest_failed_action_id
|
|
1838
|
+
):
|
|
1839
|
+
return False
|
|
1840
|
+
latest_failed_tool = filters["latest_failed_tool"]
|
|
1841
|
+
if (
|
|
1842
|
+
latest_failed_tool is not None
|
|
1843
|
+
and summary.get("latest_failed_tool") != latest_failed_tool
|
|
1844
|
+
):
|
|
1845
|
+
return False
|
|
1846
|
+
llm_provider_status = filters["llm_provider_status"]
|
|
1847
|
+
if (
|
|
1848
|
+
llm_provider_status is not None
|
|
1849
|
+
and summary.get("llm_provider_request_status") != llm_provider_status
|
|
1850
|
+
):
|
|
1851
|
+
return False
|
|
1852
|
+
llm_provider_error_type = filters["llm_provider_error_type"]
|
|
1853
|
+
if (
|
|
1854
|
+
llm_provider_error_type is not None
|
|
1855
|
+
and summary.get("llm_provider_request_error_type") != llm_provider_error_type
|
|
1856
|
+
):
|
|
1857
|
+
return False
|
|
1858
|
+
llm_provider_http_status = filters["llm_provider_http_status"]
|
|
1859
|
+
if (
|
|
1860
|
+
llm_provider_http_status is not None
|
|
1861
|
+
and summary.get("llm_provider_request_http_status") != llm_provider_http_status
|
|
1862
|
+
):
|
|
1863
|
+
return False
|
|
1864
|
+
llm_provider_retryable_reason = filters["llm_provider_retryable_reason"]
|
|
1865
|
+
if (
|
|
1866
|
+
llm_provider_retryable_reason is not None
|
|
1867
|
+
and summary.get("llm_provider_request_retryable_reason")
|
|
1868
|
+
!= llm_provider_retryable_reason
|
|
1869
|
+
):
|
|
1870
|
+
return False
|
|
1871
|
+
iteration_budget_remaining = filters["iteration_budget_remaining"]
|
|
1872
|
+
if (
|
|
1873
|
+
iteration_budget_remaining is not None
|
|
1874
|
+
and summary.get("iteration_budget_remaining") != iteration_budget_remaining
|
|
1875
|
+
):
|
|
1876
|
+
return False
|
|
1877
|
+
artifact_kind = filters["artifact_kind"]
|
|
1878
|
+
if (
|
|
1879
|
+
artifact_kind is not None
|
|
1880
|
+
and artifact_kind not in summary.get("artifact_kinds", [])
|
|
1881
|
+
):
|
|
1882
|
+
return False
|
|
1883
|
+
artifact_format = filters["artifact_format"]
|
|
1884
|
+
if (
|
|
1885
|
+
artifact_format is not None
|
|
1886
|
+
and artifact_format not in summary.get("artifact_formats", [])
|
|
1887
|
+
):
|
|
1888
|
+
return False
|
|
1889
|
+
artifact_tag = filters["artifact_tag"]
|
|
1890
|
+
if (
|
|
1891
|
+
artifact_tag is not None
|
|
1892
|
+
and artifact_tag not in summary.get("artifact_tags", [])
|
|
1893
|
+
):
|
|
1894
|
+
return False
|
|
1895
|
+
tag = filters["tag"]
|
|
1896
|
+
if tag is not None and tag not in summary.get("tags", []):
|
|
1897
|
+
return False
|
|
1898
|
+
metadata_key = filters["metadata_key"]
|
|
1899
|
+
metadata_value = filters["metadata_value"]
|
|
1900
|
+
metadata = summary.get("metadata")
|
|
1901
|
+
if metadata_key is not None:
|
|
1902
|
+
if not isinstance(metadata, dict) or metadata_key not in metadata:
|
|
1903
|
+
return False
|
|
1904
|
+
if metadata_value is not None and metadata.get(metadata_key) != metadata_value:
|
|
1905
|
+
return False
|
|
1906
|
+
elif metadata_value is not None:
|
|
1907
|
+
if not isinstance(metadata, dict) or metadata_value not in metadata.values():
|
|
1908
|
+
return False
|
|
1909
|
+
approved_action_id = filters["approved_action_id"]
|
|
1910
|
+
if (
|
|
1911
|
+
approved_action_id is not None
|
|
1912
|
+
and approved_action_id not in summary.get("approved_action_ids", [])
|
|
1913
|
+
):
|
|
1914
|
+
return False
|
|
1915
|
+
approved_by_auth_subject = filters["approved_by_auth_subject"]
|
|
1916
|
+
if (
|
|
1917
|
+
approved_by_auth_subject is not None
|
|
1918
|
+
and summary.get("approved_by_auth_subject") != approved_by_auth_subject
|
|
1919
|
+
):
|
|
1920
|
+
return False
|
|
1921
|
+
resumed_from_run_id = filters["resumed_from_run_id"]
|
|
1922
|
+
if (
|
|
1923
|
+
resumed_from_run_id is not None
|
|
1924
|
+
and summary.get("resumed_from_run_id") != resumed_from_run_id
|
|
1925
|
+
):
|
|
1926
|
+
return False
|
|
1927
|
+
resumed_by_auth_subject = filters["resumed_by_auth_subject"]
|
|
1928
|
+
if (
|
|
1929
|
+
resumed_by_auth_subject is not None
|
|
1930
|
+
and summary.get("resumed_by_auth_subject") != resumed_by_auth_subject
|
|
1931
|
+
):
|
|
1932
|
+
return False
|
|
1933
|
+
pending_approval_tool = filters["pending_approval_tool"]
|
|
1934
|
+
if (
|
|
1935
|
+
pending_approval_tool is not None
|
|
1936
|
+
and summary.get("pending_approval_tool") != pending_approval_tool
|
|
1937
|
+
):
|
|
1938
|
+
return False
|
|
1939
|
+
pending_approval_action_id = filters["pending_approval_action_id"]
|
|
1940
|
+
if (
|
|
1941
|
+
pending_approval_action_id is not None
|
|
1942
|
+
and summary.get("pending_approval_action_id") != pending_approval_action_id
|
|
1943
|
+
):
|
|
1944
|
+
return False
|
|
1945
|
+
final_answer_guardrail_reason = filters["final_answer_guardrail_reason"]
|
|
1946
|
+
guardrail = summary.get("final_answer_guardrail")
|
|
1947
|
+
if final_answer_guardrail_reason is not None:
|
|
1948
|
+
if not isinstance(guardrail, dict):
|
|
1949
|
+
return False
|
|
1950
|
+
if guardrail.get("reason") != final_answer_guardrail_reason:
|
|
1951
|
+
return False
|
|
1952
|
+
min_pending_age_seconds = filters["min_pending_age_seconds"]
|
|
1953
|
+
if min_pending_age_seconds is not None and _parse_non_negative_int(
|
|
1954
|
+
summary.get("pending_age_seconds")
|
|
1955
|
+
) < int(min_pending_age_seconds):
|
|
1956
|
+
return False
|
|
1957
|
+
has_artifacts = filters["has_artifacts"]
|
|
1958
|
+
if (
|
|
1959
|
+
has_artifacts is not None
|
|
1960
|
+
and (summary.get("artifact_count") != "0") != has_artifacts
|
|
1961
|
+
):
|
|
1962
|
+
return False
|
|
1963
|
+
has_errors = filters["has_errors"]
|
|
1964
|
+
if has_errors is not None and _runtime_summary_has_errors(summary) != has_errors:
|
|
1965
|
+
return False
|
|
1966
|
+
has_failures = filters["has_failures"]
|
|
1967
|
+
if (
|
|
1968
|
+
has_failures is not None
|
|
1969
|
+
and (summary.get("failed_observation_count") != "0") != has_failures
|
|
1970
|
+
):
|
|
1971
|
+
return False
|
|
1972
|
+
has_approvals = filters["has_approvals"]
|
|
1973
|
+
if (
|
|
1974
|
+
has_approvals is not None
|
|
1975
|
+
and (summary.get("approved_action_count") != "0") != has_approvals
|
|
1976
|
+
):
|
|
1977
|
+
return False
|
|
1978
|
+
has_pending_approval = filters["has_pending_approval"]
|
|
1979
|
+
if has_pending_approval is not None:
|
|
1980
|
+
pending_action_id = str(summary.get("pending_approval_action_id", ""))
|
|
1981
|
+
pending_tool = str(summary.get("pending_approval_tool", ""))
|
|
1982
|
+
if (bool(pending_action_id or pending_tool)) != has_pending_approval:
|
|
1983
|
+
return False
|
|
1984
|
+
has_final_answer_guardrail = filters["has_final_answer_guardrail"]
|
|
1985
|
+
if has_final_answer_guardrail is not None:
|
|
1986
|
+
guardrail_applied = (
|
|
1987
|
+
isinstance(guardrail, dict) and guardrail.get("applied") == "true"
|
|
1988
|
+
)
|
|
1989
|
+
if guardrail_applied != has_final_answer_guardrail:
|
|
1990
|
+
return False
|
|
1991
|
+
has_llm_provider_retries = filters["has_llm_provider_retries"]
|
|
1992
|
+
if has_llm_provider_retries is not None:
|
|
1993
|
+
retry_count = _parse_non_negative_int(
|
|
1994
|
+
summary.get("llm_provider_request_retry_count")
|
|
1995
|
+
)
|
|
1996
|
+
if (retry_count > 0) != has_llm_provider_retries:
|
|
1997
|
+
return False
|
|
1998
|
+
return True
|
|
1999
|
+
|
|
2000
|
+
|
|
2001
|
+
def _runtime_summary_has_errors(summary: Dict[str, Any]) -> bool:
|
|
2002
|
+
error_code_counts = summary.get("error_code_counts")
|
|
2003
|
+
if isinstance(error_code_counts, dict) and len(error_code_counts) > 0:
|
|
2004
|
+
return True
|
|
2005
|
+
if str(summary.get("llm_provider_request_error_type", "")).strip():
|
|
2006
|
+
return True
|
|
2007
|
+
return bool(str(summary.get("error_code", "")).strip())
|