@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,884 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, Callable, Dict, Mapping, Optional, Tuple
5
+ from urllib.parse import urlparse
6
+
7
+ from kagent.service import (
8
+ errors as service_errors,
9
+ )
10
+ from kagent.service import (
11
+ run as service_run,
12
+ )
13
+ from kagent.service import (
14
+ runtime_cancel as service_runtime_cancel,
15
+ )
16
+ from kagent.service import (
17
+ runtime_policy as service_runtime_policy,
18
+ )
19
+ from kagent.service import (
20
+ runtime_resume as service_runtime_resume,
21
+ )
22
+ from kagent.service import (
23
+ runtime_run as service_runtime_run,
24
+ )
25
+ from kagent.service import (
26
+ runtime_status as service_runtime_status,
27
+ )
28
+ from kagent.service import (
29
+ safety as service_safety,
30
+ )
31
+ from kagent.service import (
32
+ status as service_status,
33
+ )
34
+ from kagent.service.active_runs import ActiveRunRegistry, ExecutionSlotLease
35
+ from kagent.service.contract import service_openapi
36
+ from kagent.service.idempotency import IdempotencyCache
37
+ from kagent.service.runtime import (
38
+ ServiceConcurrencyLimiter,
39
+ ServiceConfig,
40
+ ServiceMetrics,
41
+ ServiceRateLimiter,
42
+ prometheus_metrics_text,
43
+ )
44
+
45
+
46
+ def handle_request(
47
+ method: str,
48
+ path: str,
49
+ body: bytes,
50
+ *,
51
+ headers: Optional[Mapping[str, str]] = None,
52
+ config: Optional[ServiceConfig] = None,
53
+ metrics: Optional[ServiceMetrics] = None,
54
+ rate_limiter: Optional[ServiceRateLimiter] = None,
55
+ concurrency_limiter: Optional[ServiceConcurrencyLimiter] = None,
56
+ active_run_registry: Optional[ActiveRunRegistry] = None,
57
+ idempotency_cache: Optional[IdempotencyCache] = None,
58
+ remote_addr: str = "",
59
+ agent_runner: Optional[Callable[[str, Any], Dict[str, Any]]] = None,
60
+ ) -> Tuple[int, Any]:
61
+ active_config = config or ServiceConfig()
62
+ parsed_path = urlparse(path)
63
+ route = parsed_path.path
64
+ active_headers = headers or {}
65
+ if method.upper() == "GET" and route == "/health":
66
+ return 200, {"status": "ok"}
67
+ if method.upper() == "GET" and route == "/ready":
68
+ payload = service_status.readiness_payload(active_config)
69
+ return (200 if payload["status"] == "ready" else 503), payload
70
+ if method.upper() == "GET" and _is_protected_diagnostic_route(route):
71
+ if active_config.protect_diagnostics and not service_safety.authorized(
72
+ active_headers,
73
+ active_config.auth_token,
74
+ active_config.auth_tokens,
75
+ ):
76
+ return 401, service_errors.failure_payload(
77
+ service_errors.UNAUTHORIZED,
78
+ "unauthorized",
79
+ )
80
+ runtime_read_subject = ""
81
+ runtime_read_is_admin = False
82
+ if method.upper() == "GET" and (
83
+ route.startswith("/runtime/approvals")
84
+ or route == "/runtime/policy"
85
+ or route.startswith("/runtime/runs")
86
+ ):
87
+ runtime_read_subject, runtime_read_is_admin = _runtime_read_auth_context(
88
+ active_headers,
89
+ active_config,
90
+ )
91
+ if method.upper() == "GET" and route == "/config":
92
+ return 200, service_status.service_config_snapshot(active_config)
93
+ if method.upper() == "GET" and route == "/version":
94
+ from kagent import __version__
95
+
96
+ return 200, {"version": __version__}
97
+ if method.upper() == "GET" and route == "/tools":
98
+ from kagent.core.tools import registered_tool_metadata
99
+
100
+ return 200, {"tools": registered_tool_metadata()}
101
+ if method.upper() == "GET" and route == "/runtime/graph":
102
+ from kagent.runtime import runtime_topology
103
+
104
+ return 200, runtime_topology()
105
+ if method.upper() == "GET" and route == "/runtime/tools":
106
+ from kagent.runtime.tools import (
107
+ registered_runtime_tool_metadata,
108
+ )
109
+
110
+ return 200, {"tools": registered_runtime_tool_metadata()}
111
+ if method.upper() == "GET" and route == "/runtime/policy":
112
+ return service_runtime_policy.execute_runtime_policy_request(
113
+ active_config,
114
+ request_auth_subject=runtime_read_subject,
115
+ request_auth_is_admin=runtime_read_is_admin,
116
+ )
117
+ if method.upper() == "GET" and route == "/runtime/approvals/summary":
118
+ return service_runtime_status.execute_runtime_approvals_summary_request(
119
+ parsed_path.query,
120
+ active_config,
121
+ request_auth_subject=runtime_read_subject,
122
+ request_auth_is_admin=runtime_read_is_admin,
123
+ )
124
+ if method.upper() == "GET" and route == "/runtime/approvals":
125
+ return service_runtime_status.execute_runtime_approvals_request(
126
+ parsed_path.query,
127
+ active_config,
128
+ request_auth_subject=runtime_read_subject,
129
+ request_auth_is_admin=runtime_read_is_admin,
130
+ )
131
+ if method.upper() == "GET" and route == "/runtime/runs":
132
+ return service_runtime_status.execute_runtime_list_request(
133
+ parsed_path.query,
134
+ active_config,
135
+ request_auth_subject=runtime_read_subject,
136
+ request_auth_is_admin=runtime_read_is_admin,
137
+ )
138
+ if method.upper() == "GET" and route == "/runtime/runs/summary":
139
+ return service_runtime_status.execute_runtime_summary_request(
140
+ parsed_path.query,
141
+ active_config,
142
+ request_auth_subject=runtime_read_subject,
143
+ request_auth_is_admin=runtime_read_is_admin,
144
+ )
145
+ if method.upper() == "GET" and route.startswith("/runtime/runs/"):
146
+ timeline_run_id = _runtime_timeline_route(route)
147
+ if timeline_run_id is not None:
148
+ return service_runtime_status.execute_runtime_timeline_request(
149
+ timeline_run_id,
150
+ active_config,
151
+ request_auth_subject=runtime_read_subject,
152
+ request_auth_is_admin=runtime_read_is_admin,
153
+ )
154
+ if method.upper() == "GET" and route.startswith("/runtime/runs/"):
155
+ artifacts_run_id = _runtime_artifacts_route(route)
156
+ if artifacts_run_id is not None:
157
+ return service_runtime_status.execute_runtime_artifacts_request(
158
+ artifacts_run_id,
159
+ active_config,
160
+ request_auth_subject=runtime_read_subject,
161
+ request_auth_is_admin=runtime_read_is_admin,
162
+ )
163
+ if method.upper() == "GET" and route.startswith("/runtime/runs/"):
164
+ artifact_route = _runtime_artifact_route(route)
165
+ if artifact_route is not None:
166
+ run_id, artifact_id = artifact_route
167
+ return service_runtime_status.execute_runtime_artifact_request(
168
+ run_id,
169
+ artifact_id,
170
+ active_config,
171
+ request_auth_subject=runtime_read_subject,
172
+ request_auth_is_admin=runtime_read_is_admin,
173
+ )
174
+ if method.upper() == "GET" and route.startswith("/runtime/runs/"):
175
+ run_id = route.removeprefix("/runtime/runs/")
176
+ return service_runtime_status.execute_runtime_status_request(
177
+ run_id,
178
+ active_config,
179
+ request_auth_subject=runtime_read_subject,
180
+ request_auth_is_admin=runtime_read_is_admin,
181
+ )
182
+ if method.upper() == "GET" and route == "/openapi.json":
183
+ return 200, service_openapi()
184
+ if method.upper() == "GET" and route == "/metrics":
185
+ return 200, metrics_snapshot(
186
+ metrics,
187
+ concurrency_limiter,
188
+ rate_limiter,
189
+ idempotency_cache,
190
+ active_config,
191
+ )
192
+ if method.upper() == "GET" and route == "/metrics.prom":
193
+ return 200, prometheus_metrics_text(
194
+ metrics_snapshot(
195
+ metrics,
196
+ concurrency_limiter,
197
+ rate_limiter,
198
+ idempotency_cache,
199
+ active_config,
200
+ )
201
+ )
202
+ if method.upper() == "POST" and route == "/run":
203
+ return _handle_run_route(
204
+ body,
205
+ headers=headers or {},
206
+ config=active_config,
207
+ metrics=metrics,
208
+ rate_limiter=rate_limiter,
209
+ concurrency_limiter=concurrency_limiter,
210
+ active_run_registry=active_run_registry,
211
+ idempotency_cache=idempotency_cache,
212
+ remote_addr=remote_addr,
213
+ agent_runner=agent_runner,
214
+ )
215
+ if method.upper() == "POST" and route == "/runtime/run":
216
+ return _handle_execution_route(
217
+ body,
218
+ headers=headers or {},
219
+ config=active_config,
220
+ metrics=metrics,
221
+ rate_limiter=rate_limiter,
222
+ concurrency_limiter=concurrency_limiter,
223
+ idempotency_cache=idempotency_cache,
224
+ remote_addr=remote_addr,
225
+ idempotency_scope="POST /runtime/run",
226
+ include_auth_admin=False,
227
+ execute_request=(
228
+ lambda request_body, service_config, auth_subject, _auth_is_admin, lease: (
229
+ service_runtime_run.execute_runtime_run_request(
230
+ request_body,
231
+ service_config,
232
+ auth_subject,
233
+ active_run_registry=active_run_registry,
234
+ execution_slot_lease=lease,
235
+ )
236
+ )
237
+ ),
238
+ )
239
+ if method.upper() == "POST" and route == "/runtime/resume":
240
+ return _handle_execution_route(
241
+ body,
242
+ headers=headers or {},
243
+ config=active_config,
244
+ metrics=metrics,
245
+ rate_limiter=rate_limiter,
246
+ concurrency_limiter=concurrency_limiter,
247
+ idempotency_cache=idempotency_cache,
248
+ remote_addr=remote_addr,
249
+ idempotency_scope="POST /runtime/resume",
250
+ include_auth_admin=True,
251
+ execute_request=(
252
+ lambda request_body, service_config, auth_subject, auth_is_admin, lease: (
253
+ service_runtime_resume.execute_runtime_resume_request(
254
+ request_body,
255
+ service_config,
256
+ auth_subject,
257
+ request_auth_is_admin=auth_is_admin,
258
+ active_run_registry=active_run_registry,
259
+ execution_slot_lease=lease,
260
+ )
261
+ )
262
+ ),
263
+ )
264
+ if method.upper() == "POST" and route.startswith("/runtime/runs/"):
265
+ cancel_run_id = _runtime_cancel_route(route)
266
+ if cancel_run_id is not None:
267
+ return _handle_execution_route(
268
+ body,
269
+ headers=headers or {},
270
+ config=active_config,
271
+ metrics=metrics,
272
+ rate_limiter=rate_limiter,
273
+ concurrency_limiter=concurrency_limiter,
274
+ idempotency_cache=idempotency_cache,
275
+ remote_addr=remote_addr,
276
+ idempotency_scope=f"POST /runtime/runs/{cancel_run_id}/cancel",
277
+ include_auth_admin=True,
278
+ execute_request=(
279
+ lambda request_body, service_config, auth_subject, auth_is_admin, _lease: (
280
+ service_runtime_cancel.execute_runtime_cancel_request(
281
+ cancel_run_id,
282
+ request_body,
283
+ service_config,
284
+ auth_subject,
285
+ request_auth_is_admin=auth_is_admin,
286
+ active_run_registry=active_run_registry,
287
+ )
288
+ )
289
+ ),
290
+ acquire_run_slot=False,
291
+ )
292
+ return 404, service_errors.failure_payload(service_errors.NOT_FOUND, "not found")
293
+
294
+
295
+ def metrics_snapshot(
296
+ metrics: Optional[ServiceMetrics],
297
+ concurrency_limiter: Optional[ServiceConcurrencyLimiter],
298
+ rate_limiter: Optional[ServiceRateLimiter],
299
+ idempotency_cache: Optional[IdempotencyCache] = None,
300
+ config: Optional[ServiceConfig] = None,
301
+ ) -> Dict[str, Any]:
302
+ payload = (metrics or ServiceMetrics()).snapshot()
303
+ active_config = config or ServiceConfig()
304
+ from kagent import __version__
305
+
306
+ payload.update(
307
+ {
308
+ "service_version": __version__,
309
+ "bind_host": active_config.host,
310
+ "bind_port": str(active_config.port),
311
+ "auth_required": str(active_config.auth_required).lower(),
312
+ "auth_subject_count": str(
313
+ len(active_config.auth_tokens) + (1 if active_config.auth_token else 0)
314
+ ),
315
+ "trace_persistence": "enabled" if active_config.trace_dir else "disabled",
316
+ "max_request_bytes": str(active_config.max_request_bytes),
317
+ "rate_limit_per_minute": str(active_config.rate_limit_per_minute),
318
+ "max_concurrent_runs": str(active_config.max_concurrent_runs),
319
+ "idempotency_cache_size": str(active_config.idempotency_cache_size),
320
+ "idempotency_cache_backend": (
321
+ "sqlite" if active_config.idempotency_cache_path else "memory"
322
+ ),
323
+ "idempotency_cache_path_configured": (
324
+ "true" if active_config.idempotency_cache_path else "false"
325
+ ),
326
+ "max_goal_chars": str(active_config.max_goal_chars),
327
+ "runtime_allowed_tools": (
328
+ ",".join(active_config.runtime_allowed_tools)
329
+ if active_config.runtime_allowed_tools
330
+ else "default"
331
+ ),
332
+ "runtime_allowed_tools_by_subject_count": str(
333
+ len(active_config.runtime_allowed_tools_by_subject)
334
+ ),
335
+ "runtime_max_iterations": str(active_config.runtime_max_iterations),
336
+ "runtime_pending_approval_stale_seconds": str(
337
+ active_config.runtime_pending_approval_stale_seconds
338
+ ),
339
+ "allow_full_trace_response": str(active_config.allow_full_trace_response).lower(),
340
+ "protect_diagnostics": str(active_config.protect_diagnostics).lower(),
341
+ "trust_forwarded_for": str(active_config.trust_forwarded_for).lower(),
342
+ "run_timeout_seconds": str(active_config.run_timeout_seconds),
343
+ "request_timeout_seconds": str(active_config.request_timeout_seconds),
344
+ }
345
+ )
346
+ payload.update(service_status.security_response_header_snapshot())
347
+ payload.update(service_status.trace_permission_policy_snapshot())
348
+ payload.update(service_status.llm_provider_snapshot())
349
+ payload.update(_runtime_pending_approval_metrics_snapshot(active_config))
350
+ payload.update(_runtime_guardrail_metrics_snapshot(active_config))
351
+ if concurrency_limiter is not None:
352
+ payload.update(concurrency_limiter.snapshot())
353
+ if rate_limiter is not None:
354
+ payload.update(rate_limiter.snapshot())
355
+ if idempotency_cache is not None:
356
+ payload.update(idempotency_cache.snapshot())
357
+ return payload
358
+
359
+
360
+ def _runtime_pending_approval_metrics_snapshot(config: ServiceConfig) -> Dict[str, str]:
361
+ if not config.trace_dir:
362
+ return _empty_runtime_pending_approval_metrics(config)
363
+ unfiltered_status, unfiltered = (
364
+ service_runtime_status.execute_runtime_approvals_summary_request(
365
+ "",
366
+ config,
367
+ request_auth_is_admin=True,
368
+ )
369
+ )
370
+ stale_status, stale = service_runtime_status.execute_runtime_approvals_summary_request(
371
+ f"min_pending_age_seconds={config.runtime_pending_approval_stale_seconds}",
372
+ config,
373
+ request_auth_is_admin=True,
374
+ )
375
+ if unfiltered_status != 200 or stale_status != 200:
376
+ return _empty_runtime_pending_approval_metrics(config)
377
+ return {
378
+ "runtime_pending_approvals_current": str(
379
+ unfiltered.get("pending_approval_count", "0")
380
+ ),
381
+ "runtime_stale_pending_approvals_current": str(
382
+ stale.get("pending_approval_count", "0")
383
+ ),
384
+ "runtime_max_pending_approval_age_seconds": str(
385
+ unfiltered.get("max_pending_age_seconds", "0")
386
+ ),
387
+ "runtime_pending_approval_stale_seconds": str(
388
+ config.runtime_pending_approval_stale_seconds
389
+ ),
390
+ }
391
+
392
+
393
+ def _empty_runtime_pending_approval_metrics(config: ServiceConfig) -> Dict[str, str]:
394
+ return {
395
+ "runtime_pending_approvals_current": "0",
396
+ "runtime_stale_pending_approvals_current": "0",
397
+ "runtime_max_pending_approval_age_seconds": "0",
398
+ "runtime_pending_approval_stale_seconds": str(
399
+ config.runtime_pending_approval_stale_seconds
400
+ ),
401
+ }
402
+
403
+
404
+ def _runtime_guardrail_metrics_snapshot(config: ServiceConfig) -> Dict[str, Any]:
405
+ if not config.trace_dir:
406
+ return _empty_runtime_guardrail_metrics()
407
+ status_code, summary = service_runtime_status.execute_runtime_summary_request(
408
+ "",
409
+ config,
410
+ request_auth_is_admin=True,
411
+ )
412
+ if status_code != 200:
413
+ return _empty_runtime_guardrail_metrics()
414
+ reason_counts = summary.get("final_answer_guardrail_reason_counts")
415
+ return {
416
+ "runtime_final_answer_guardrails_total": str(
417
+ summary.get("final_answer_guardrail_applied_count", "0")
418
+ ),
419
+ "runtime_final_answer_guardrails_by_reason": (
420
+ reason_counts if isinstance(reason_counts, dict) else {}
421
+ ),
422
+ }
423
+
424
+
425
+ def _empty_runtime_guardrail_metrics() -> Dict[str, Any]:
426
+ return {
427
+ "runtime_final_answer_guardrails_total": "0",
428
+ "runtime_final_answer_guardrails_by_reason": {},
429
+ }
430
+
431
+
432
+ def agent_run_status(status_code: int, payload: Any) -> str:
433
+ if status_code == 504:
434
+ return "timeout"
435
+ if isinstance(payload, dict):
436
+ status = str(payload.get("status", ""))
437
+ if status:
438
+ return status
439
+ if status_code >= 500:
440
+ return "failed"
441
+ return "rejected"
442
+
443
+
444
+ _PROTECTED_DIAGNOSTIC_ROUTES = frozenset(
445
+ {
446
+ "/config",
447
+ "/tools",
448
+ "/runtime/graph",
449
+ "/runtime/tools",
450
+ "/metrics",
451
+ "/metrics.prom",
452
+ "/openapi.json",
453
+ }
454
+ )
455
+
456
+
457
+ def _is_protected_diagnostic_route(route: str) -> bool:
458
+ return (
459
+ route in _PROTECTED_DIAGNOSTIC_ROUTES
460
+ or route.startswith("/runtime/approvals")
461
+ or route == "/runtime/policy"
462
+ or route.startswith("/runtime/runs")
463
+ )
464
+
465
+
466
+ def _runtime_read_auth_context(
467
+ headers: Mapping[str, str],
468
+ config: ServiceConfig,
469
+ ) -> tuple[str, bool]:
470
+ auth_subject = service_safety.authenticated_subject(
471
+ headers,
472
+ config.auth_token,
473
+ config.auth_tokens,
474
+ )
475
+ is_admin = service_safety.authenticated_with_primary_token(
476
+ headers,
477
+ config.auth_token,
478
+ )
479
+ return auth_subject, is_admin
480
+
481
+
482
+ def _runtime_timeline_route(route: str) -> str | None:
483
+ remainder = route.removeprefix("/runtime/runs/")
484
+ run_id, separator, suffix = remainder.partition("/timeline")
485
+ if separator and run_id and suffix == "":
486
+ return run_id
487
+ return None
488
+
489
+
490
+ def _runtime_artifacts_route(route: str) -> str | None:
491
+ remainder = route.removeprefix("/runtime/runs/")
492
+ run_id, separator, suffix = remainder.partition("/artifacts")
493
+ if separator and run_id and suffix == "":
494
+ return run_id
495
+ return None
496
+
497
+
498
+ def _runtime_artifact_route(route: str) -> tuple[str, str] | None:
499
+ remainder = route.removeprefix("/runtime/runs/")
500
+ run_id, separator, artifact_id = remainder.partition("/artifacts/")
501
+ if separator and run_id and artifact_id:
502
+ return run_id, artifact_id
503
+ return None
504
+
505
+
506
+ def _runtime_cancel_route(route: str) -> str | None:
507
+ remainder = route.removeprefix("/runtime/runs/")
508
+ run_id, separator, suffix = remainder.partition("/cancel")
509
+ if separator and run_id and suffix == "":
510
+ return run_id
511
+ return None
512
+
513
+
514
+ def _handle_run_route(
515
+ body: bytes,
516
+ *,
517
+ headers: Mapping[str, str],
518
+ config: ServiceConfig,
519
+ metrics: Optional[ServiceMetrics],
520
+ rate_limiter: Optional[ServiceRateLimiter],
521
+ concurrency_limiter: Optional[ServiceConcurrencyLimiter],
522
+ idempotency_cache: Optional[IdempotencyCache],
523
+ remote_addr: str,
524
+ agent_runner: Optional[Callable[[str, Any], Dict[str, Any]]],
525
+ active_run_registry: Optional[ActiveRunRegistry],
526
+ ) -> Tuple[int, Any]:
527
+ def execute_request(
528
+ request_body: bytes,
529
+ service_config: ServiceConfig,
530
+ _auth_subject: str,
531
+ _auth_is_admin: bool,
532
+ lease: ExecutionSlotLease,
533
+ ) -> Tuple[int, Any]:
534
+ return service_run.execute_run_request(
535
+ request_body,
536
+ service_config,
537
+ agent_runner,
538
+ execution_slot_lease=lease,
539
+ )
540
+
541
+ return _handle_execution_route(
542
+ body,
543
+ headers=headers,
544
+ config=config,
545
+ metrics=metrics,
546
+ rate_limiter=rate_limiter,
547
+ concurrency_limiter=concurrency_limiter,
548
+ active_run_registry=active_run_registry,
549
+ idempotency_cache=idempotency_cache,
550
+ remote_addr=remote_addr,
551
+ idempotency_scope="POST /run",
552
+ include_auth_admin=False,
553
+ execute_request=execute_request,
554
+ )
555
+
556
+
557
+ def _handle_execution_route(
558
+ body: bytes,
559
+ *,
560
+ headers: Mapping[str, str],
561
+ config: ServiceConfig,
562
+ metrics: Optional[ServiceMetrics],
563
+ rate_limiter: Optional[ServiceRateLimiter],
564
+ concurrency_limiter: Optional[ServiceConcurrencyLimiter],
565
+ idempotency_cache: Optional[IdempotencyCache],
566
+ remote_addr: str,
567
+ idempotency_scope: str,
568
+ include_auth_admin: bool,
569
+ execute_request: Callable[
570
+ [bytes, ServiceConfig, str, bool, ExecutionSlotLease],
571
+ Tuple[int, Any],
572
+ ],
573
+ active_run_registry: Optional[ActiveRunRegistry] = None,
574
+ acquire_run_slot: bool = True,
575
+ ) -> Tuple[int, Any]:
576
+ if not service_safety.json_content_type(headers):
577
+ return 415, service_errors.failure_payload(
578
+ service_errors.UNSUPPORTED_MEDIA_TYPE,
579
+ "content-type must be application/json",
580
+ )
581
+ if len(body) > config.max_request_bytes:
582
+ return 413, service_errors.failure_payload(
583
+ service_errors.REQUEST_TOO_LARGE,
584
+ "request body too large",
585
+ )
586
+ auth_subject = service_safety.authenticated_subject(
587
+ headers,
588
+ config.auth_token,
589
+ config.auth_tokens,
590
+ )
591
+ auth_is_admin = (
592
+ service_safety.authenticated_with_primary_token(headers, config.auth_token)
593
+ if include_auth_admin
594
+ else False
595
+ )
596
+ if config.auth_required and not auth_subject:
597
+ return 401, service_errors.failure_payload(service_errors.UNAUTHORIZED, "unauthorized")
598
+ idempotency_key = service_safety.header_value(headers, "Idempotency-Key")
599
+ if idempotency_key and not service_safety.safe_idempotency_key(idempotency_key):
600
+ return 400, service_errors.failure_payload(
601
+ service_errors.INVALID_IDEMPOTENCY_KEY,
602
+ "idempotency key must be 1-128 printable ASCII characters",
603
+ )
604
+ scoped_idempotency_key = _scoped_idempotency_key(
605
+ idempotency_scope,
606
+ idempotency_key,
607
+ auth_subject,
608
+ )
609
+ claim_token = ""
610
+ if (
611
+ idempotency_key
612
+ and config.idempotency_cache_size > 0
613
+ and idempotency_cache is not None
614
+ ):
615
+ single_flight_seconds = (
616
+ config.run_timeout_seconds + config.request_timeout_seconds
617
+ )
618
+ cache_status, cached_response, claim_token = idempotency_cache.acquire(
619
+ scoped_idempotency_key,
620
+ body,
621
+ lease_seconds=single_flight_seconds,
622
+ wait_timeout_seconds=single_flight_seconds,
623
+ )
624
+ if cache_status == "hit" and cached_response is not None:
625
+ return cached_response
626
+ if cache_status == "conflict":
627
+ return 409, service_errors.failure_payload(
628
+ service_errors.IDEMPOTENCY_KEY_CONFLICT,
629
+ "idempotency key was already used with a different request body",
630
+ )
631
+ if cache_status == "in_progress":
632
+ payload = service_errors.failure_payload(
633
+ service_errors.IDEMPOTENCY_REQUEST_IN_PROGRESS,
634
+ "an identical request with this idempotency key is still running",
635
+ )
636
+ payload["retry_after_seconds"] = "1"
637
+ return 409, payload
638
+ try:
639
+ rate_limit_key = service_safety.rate_limit_key(
640
+ headers,
641
+ remote_addr,
642
+ trust_forwarded_for=config.trust_forwarded_for,
643
+ auth_token=config.auth_token,
644
+ auth_tokens=config.auth_tokens,
645
+ auth_subject=auth_subject,
646
+ )
647
+ if rate_limiter is not None and not rate_limiter.allow(rate_limit_key):
648
+ payload = service_errors.failure_payload(
649
+ service_errors.RATE_LIMIT_EXCEEDED,
650
+ "rate limit exceeded",
651
+ )
652
+ payload["retry_after_seconds"] = str(
653
+ rate_limiter.retry_after_seconds(rate_limit_key)
654
+ )
655
+ return 429, payload
656
+ release_run_slot = (
657
+ concurrency_limiter.try_acquire()
658
+ if concurrency_limiter is not None and acquire_run_slot
659
+ else None
660
+ )
661
+ if acquire_run_slot and concurrency_limiter is not None and release_run_slot is None:
662
+ payload = service_errors.failure_payload(
663
+ service_errors.TOO_MANY_CONCURRENT_RUNS,
664
+ "too many concurrent runs",
665
+ )
666
+ payload["retry_after_seconds"] = "1"
667
+ return 503, payload
668
+ execution_slot_lease = ExecutionSlotLease(release_run_slot)
669
+ try:
670
+ started_at = time.perf_counter()
671
+ status_code, payload = execute_request(
672
+ body,
673
+ config,
674
+ auth_subject,
675
+ auth_is_admin,
676
+ execution_slot_lease,
677
+ )
678
+ if (
679
+ status_code == 200
680
+ and claim_token
681
+ and idempotency_cache is not None
682
+ and idempotency_cache.complete(
683
+ scoped_idempotency_key,
684
+ body,
685
+ claim_token,
686
+ status_code,
687
+ payload,
688
+ )
689
+ ):
690
+ claim_token = ""
691
+ if metrics is not None:
692
+ metrics.record_agent_run(
693
+ status=agent_run_status(status_code, payload),
694
+ duration_seconds=time.perf_counter() - started_at,
695
+ )
696
+ _record_runtime_run_metrics(metrics, status_code, payload)
697
+ return status_code, payload
698
+ finally:
699
+ execution_slot_lease.release_if_owned()
700
+ finally:
701
+ if claim_token and idempotency_cache is not None:
702
+ idempotency_cache.release(scoped_idempotency_key, claim_token)
703
+
704
+
705
+ def _scoped_idempotency_key(scope: str, key: str, auth_subject: str = "") -> str:
706
+ if not key:
707
+ return ""
708
+ subject_scope = auth_subject or "__anonymous__"
709
+ return f"{scope}\x1f{subject_scope}\x1f{key}"
710
+
711
+
712
+ def _record_runtime_run_metrics(
713
+ metrics: ServiceMetrics,
714
+ status_code: int,
715
+ payload: Any,
716
+ ) -> None:
717
+ if status_code != 200 or not isinstance(payload, dict):
718
+ return
719
+ if payload.get("trace_type") != "codex_runtime":
720
+ return
721
+ status = str(payload.get("status", ""))
722
+ observations = payload.get("observations")
723
+ events = payload.get("events")
724
+ failed_observation_count = _runtime_observation_status_count(
725
+ observations,
726
+ "failed",
727
+ )
728
+ approval_required_count = _runtime_observation_status_count(
729
+ observations,
730
+ "requires_approval",
731
+ )
732
+ metrics.record_runtime_run(
733
+ status=status,
734
+ lifecycle_state=str(payload.get("lifecycle_state", "")),
735
+ failed_observation_count=failed_observation_count,
736
+ approval_required_count=approval_required_count,
737
+ budget_exhausted=(
738
+ status == "failed"
739
+ and str(payload.get("iteration_budget_remaining", "")) == "0"
740
+ ),
741
+ duration_seconds=_runtime_duration_seconds(payload),
742
+ error_code_counts=_runtime_observation_error_code_counts(observations),
743
+ tool_status_counts=_runtime_observation_tool_status_counts(observations),
744
+ planner_attempt_status_counts=_runtime_planner_attempt_status_counts(events),
745
+ planner_failure_count=_runtime_planner_failure_count(observations),
746
+ planner_error_code_counts=_runtime_planner_error_code_counts(observations),
747
+ llm_provider_request=_runtime_llm_provider_request(payload),
748
+ auth_subject=str(payload.get("auth_subject", "")),
749
+ resumed_by_auth_subject=str(payload.get("resumed_by_auth_subject", "")),
750
+ approved_by_auth_subject=str(payload.get("approved_by_auth_subject", "")),
751
+ progress_event_sink_failure_count=_runtime_non_negative_int(
752
+ payload.get("progress_event_sink_failure_count", 0)
753
+ ),
754
+ hook_failure_count=_runtime_non_negative_int(
755
+ payload.get("hook_failure_count", 0)
756
+ ),
757
+ )
758
+
759
+
760
+ def _runtime_duration_seconds(payload: Dict[str, Any]) -> float:
761
+ try:
762
+ return max(0.0, float(payload.get("duration_seconds", 0.0)))
763
+ except (TypeError, ValueError):
764
+ return 0.0
765
+
766
+
767
+ def _runtime_non_negative_int(value: Any) -> int:
768
+ try:
769
+ return max(0, int(value))
770
+ except (TypeError, ValueError):
771
+ return 0
772
+
773
+
774
+ def _runtime_llm_provider_request(value: Mapping[str, Any]) -> Dict[str, str]:
775
+ diagnostics = value.get("llm_provider_request")
776
+ if not isinstance(diagnostics, dict):
777
+ return {}
778
+ allowed_fields = {
779
+ "attempt_count",
780
+ "retry_count",
781
+ "status",
782
+ "stream",
783
+ "duration_seconds",
784
+ "error_type",
785
+ "http_status",
786
+ "retryable_reason",
787
+ }
788
+ return {
789
+ key: str(diagnostics[key])
790
+ for key in sorted(allowed_fields)
791
+ if str(diagnostics.get(key, "")).strip()
792
+ }
793
+
794
+
795
+ def _runtime_observation_status_count(value: Any, status: str) -> int:
796
+ if not isinstance(value, list):
797
+ return 0
798
+ return sum(
799
+ 1
800
+ for item in value
801
+ if isinstance(item, dict) and str(item.get("status", "")) == status
802
+ )
803
+
804
+
805
+ def _runtime_observation_error_code_counts(value: Any) -> Dict[str, int]:
806
+ if not isinstance(value, list):
807
+ return {}
808
+ counts: Dict[str, int] = {}
809
+ for item in value:
810
+ if not isinstance(item, dict):
811
+ continue
812
+ if str(item.get("status", "")) not in {"failed", "requires_approval"}:
813
+ continue
814
+ error_code = str(item.get("error_code", ""))
815
+ if not error_code:
816
+ continue
817
+ counts[error_code] = counts.get(error_code, 0) + 1
818
+ return counts
819
+
820
+
821
+ def _runtime_observation_tool_status_counts(value: Any) -> Dict[str, int]:
822
+ if not isinstance(value, list):
823
+ return {}
824
+ counts: Dict[str, int] = {}
825
+ for item in value:
826
+ if not isinstance(item, dict):
827
+ continue
828
+ tool = str(item.get("tool", "")).strip()
829
+ status = str(item.get("status", "")).strip()
830
+ if tool == "planner":
831
+ continue
832
+ if not tool or not status:
833
+ continue
834
+ key = f"{tool}:{status}"
835
+ counts[key] = counts.get(key, 0) + 1
836
+ return counts
837
+
838
+
839
+ def _runtime_planner_failure_count(value: Any) -> int:
840
+ if not isinstance(value, list):
841
+ return 0
842
+ return sum(
843
+ 1
844
+ for item in value
845
+ if (
846
+ isinstance(item, dict)
847
+ and str(item.get("tool", "")) == "planner"
848
+ and str(item.get("status", "")) == "failed"
849
+ )
850
+ )
851
+
852
+
853
+ def _runtime_planner_error_code_counts(value: Any) -> Dict[str, int]:
854
+ if not isinstance(value, list):
855
+ return {}
856
+ counts: Dict[str, int] = {}
857
+ for item in value:
858
+ if not isinstance(item, dict):
859
+ continue
860
+ if str(item.get("tool", "")) != "planner":
861
+ continue
862
+ if str(item.get("status", "")) != "failed":
863
+ continue
864
+ error_code = str(item.get("error_code", ""))
865
+ if not error_code:
866
+ continue
867
+ counts[error_code] = counts.get(error_code, 0) + 1
868
+ return counts
869
+
870
+
871
+ def _runtime_planner_attempt_status_counts(value: Any) -> Dict[str, int]:
872
+ if not isinstance(value, list):
873
+ return {}
874
+ counts: Dict[str, int] = {}
875
+ for item in value:
876
+ if not isinstance(item, dict):
877
+ continue
878
+ if str(item.get("node", "")) != "planner":
879
+ continue
880
+ status = str(item.get("status", "")).strip()
881
+ if not status:
882
+ continue
883
+ counts[status] = counts.get(status, 0) + 1
884
+ return counts