@openlucaskaka/kagent 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -0,0 +1,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
|