@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.
Files changed (117) hide show
  1. package/README.md +353 -0
  2. package/npm/bin/kagent-serve.js +6 -0
  3. package/npm/bin/kagent.js +6 -0
  4. package/npm/lib/App.js +524 -0
  5. package/npm/lib/app-state.js +224 -0
  6. package/npm/lib/approval-choice.js +25 -0
  7. package/npm/lib/commands.js +59 -0
  8. package/npm/lib/editor.js +188 -0
  9. package/npm/lib/ink-runner.js +41 -0
  10. package/npm/lib/kagent-home.js +39 -0
  11. package/npm/lib/launcher.js +221 -0
  12. package/npm/lib/protocol.js +33 -0
  13. package/npm/lib/provider-setup.js +139 -0
  14. package/npm/lib/python-runner.js +892 -0
  15. package/npm/lib/runtime-client.js +390 -0
  16. package/npm/lib/terminal-input.js +127 -0
  17. package/npm/lib/terminal-text.js +19 -0
  18. package/npm/lib/terminal-width.js +14 -0
  19. package/npm/lib/transcript.js +227 -0
  20. package/npm/lib/ui-components.js +247 -0
  21. package/npm/lib/update-manager.js +334 -0
  22. package/package.json +39 -0
  23. package/pyproject.toml +55 -0
  24. package/src/kagent/__init__.py +90 -0
  25. package/src/kagent/cli/__init__.py +5 -0
  26. package/src/kagent/cli/__main__.py +6 -0
  27. package/src/kagent/cli/commands.py +112 -0
  28. package/src/kagent/cli/conversation.py +127 -0
  29. package/src/kagent/cli/interactive.py +841 -0
  30. package/src/kagent/cli/main.py +685 -0
  31. package/src/kagent/cli/memory.py +460 -0
  32. package/src/kagent/cli/pending_approval.py +169 -0
  33. package/src/kagent/cli/provider.py +27 -0
  34. package/src/kagent/cli/session_commands.py +401 -0
  35. package/src/kagent/cli/stdio_runtime.py +784 -0
  36. package/src/kagent/cli/trace.py +67 -0
  37. package/src/kagent/cli/ui.py +931 -0
  38. package/src/kagent/core/__init__.py +0 -0
  39. package/src/kagent/core/agent.py +296 -0
  40. package/src/kagent/core/faults.py +11 -0
  41. package/src/kagent/core/invariants.py +47 -0
  42. package/src/kagent/core/normalization.py +81 -0
  43. package/src/kagent/core/planning.py +48 -0
  44. package/src/kagent/core/state.py +73 -0
  45. package/src/kagent/core/summary.py +64 -0
  46. package/src/kagent/core/tools.py +196 -0
  47. package/src/kagent/core/trace.py +57 -0
  48. package/src/kagent/eval/__init__.py +11 -0
  49. package/src/kagent/eval/cases.py +229 -0
  50. package/src/kagent/eval/evaluator.py +216 -0
  51. package/src/kagent/integrations/__init__.py +3 -0
  52. package/src/kagent/integrations/audit.py +95 -0
  53. package/src/kagent/integrations/backends.py +131 -0
  54. package/src/kagent/integrations/memory.py +301 -0
  55. package/src/kagent/ops/__init__.py +0 -0
  56. package/src/kagent/ops/batch.py +156 -0
  57. package/src/kagent/ops/doctor.py +255 -0
  58. package/src/kagent/ops/metrics.py +214 -0
  59. package/src/kagent/ops/release_evidence.py +877 -0
  60. package/src/kagent/ops/release_manifest.py +142 -0
  61. package/src/kagent/ops/trace_replay.py +285 -0
  62. package/src/kagent/providers/__init__.py +7 -0
  63. package/src/kagent/providers/embeddings.py +187 -0
  64. package/src/kagent/providers/llm.py +770 -0
  65. package/src/kagent/py.typed +1 -0
  66. package/src/kagent/runtime/__init__.py +28 -0
  67. package/src/kagent/runtime/action_graph.py +543 -0
  68. package/src/kagent/runtime/agent.py +2089 -0
  69. package/src/kagent/runtime/approval.py +64 -0
  70. package/src/kagent/runtime/cancellation.py +64 -0
  71. package/src/kagent/runtime/checkpoint_state.py +146 -0
  72. package/src/kagent/runtime/checkpoint_storage.py +270 -0
  73. package/src/kagent/runtime/context.py +65 -0
  74. package/src/kagent/runtime/file_transaction.py +195 -0
  75. package/src/kagent/runtime/hooks.py +74 -0
  76. package/src/kagent/runtime/metadata.py +116 -0
  77. package/src/kagent/runtime/patch_checkpoints.py +385 -0
  78. package/src/kagent/runtime/policy.py +50 -0
  79. package/src/kagent/runtime/presentation.py +205 -0
  80. package/src/kagent/runtime/redaction.py +130 -0
  81. package/src/kagent/runtime/sandbox.py +331 -0
  82. package/src/kagent/runtime/skills.py +66 -0
  83. package/src/kagent/runtime/steering.py +56 -0
  84. package/src/kagent/runtime/steps.py +255 -0
  85. package/src/kagent/runtime/task_state.py +40 -0
  86. package/src/kagent/runtime/tools.py +3532 -0
  87. package/src/kagent/runtime/types.py +240 -0
  88. package/src/kagent/runtime/workspace.py +597 -0
  89. package/src/kagent/service/__init__.py +26 -0
  90. package/src/kagent/service/__main__.py +6 -0
  91. package/src/kagent/service/active_runs.py +178 -0
  92. package/src/kagent/service/cli.py +737 -0
  93. package/src/kagent/service/contract.py +2571 -0
  94. package/src/kagent/service/errors.py +71 -0
  95. package/src/kagent/service/idempotency.py +584 -0
  96. package/src/kagent/service/router.py +884 -0
  97. package/src/kagent/service/run.py +150 -0
  98. package/src/kagent/service/runtime.py +1915 -0
  99. package/src/kagent/service/runtime_approval.py +20 -0
  100. package/src/kagent/service/runtime_cancel.py +192 -0
  101. package/src/kagent/service/runtime_lifecycle.py +229 -0
  102. package/src/kagent/service/runtime_metadata.py +21 -0
  103. package/src/kagent/service/runtime_policy.py +115 -0
  104. package/src/kagent/service/runtime_recovery.py +573 -0
  105. package/src/kagent/service/runtime_resume.py +382 -0
  106. package/src/kagent/service/runtime_resume_claim.py +116 -0
  107. package/src/kagent/service/runtime_run.py +350 -0
  108. package/src/kagent/service/runtime_status.py +2007 -0
  109. package/src/kagent/service/safety.py +139 -0
  110. package/src/kagent/service/server.py +114 -0
  111. package/src/kagent/service/status.py +233 -0
  112. package/src/kagent/service/trace_store.py +322 -0
  113. package/src/kagent/service/transport.py +24 -0
  114. package/src/kagent/utils/__init__.py +0 -0
  115. package/src/kagent/utils/config_validation.py +21 -0
  116. package/src/kagent/utils/json_output.py +23 -0
  117. 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())