@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,1915 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from math import ceil
|
|
7
|
+
from os import environ
|
|
8
|
+
from threading import BoundedSemaphore, Lock
|
|
9
|
+
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from kagent.service.idempotency import (
|
|
12
|
+
ServiceIdempotencyCache as ServiceIdempotencyCache,
|
|
13
|
+
)
|
|
14
|
+
from kagent.service.idempotency import (
|
|
15
|
+
SqliteServiceIdempotencyCache as SqliteServiceIdempotencyCache,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_KNOWN_HTTP_METHODS = {"GET", "HEAD", "OPTIONS", "POST"}
|
|
19
|
+
_UNKNOWN_METRICS_LABEL = "__unknown__"
|
|
20
|
+
_DURATION_BUCKETS = (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
|
|
21
|
+
_RUNTIME_RECONCILIATION_STATUSES = {"ok", "unavailable"}
|
|
22
|
+
_RUNTIME_RECONCILIATION_OUTCOMES = (
|
|
23
|
+
"recovered_running",
|
|
24
|
+
"completed_resumes",
|
|
25
|
+
"reopened_approvals",
|
|
26
|
+
"protected_live",
|
|
27
|
+
"skipped_unowned",
|
|
28
|
+
"skipped_locked",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ServiceConfig:
|
|
34
|
+
host: str = "127.0.0.1"
|
|
35
|
+
port: int = 8000
|
|
36
|
+
max_request_bytes: int = 65536
|
|
37
|
+
max_goal_chars: int = 4096
|
|
38
|
+
auth_token: str = ""
|
|
39
|
+
auth_tokens: Dict[str, str] = field(default_factory=dict)
|
|
40
|
+
rate_limit_per_minute: int = 0
|
|
41
|
+
max_concurrent_runs: int = 4
|
|
42
|
+
idempotency_cache_size: int = 0
|
|
43
|
+
idempotency_cache_path: str = ""
|
|
44
|
+
runtime_allowed_tools: Tuple[str, ...] = ()
|
|
45
|
+
runtime_allowed_tools_by_subject: Dict[str, Tuple[str, ...]] = field(
|
|
46
|
+
default_factory=dict
|
|
47
|
+
)
|
|
48
|
+
runtime_max_iterations: int = 10
|
|
49
|
+
runtime_pending_approval_stale_seconds: int = 3600
|
|
50
|
+
runtime_instance_heartbeat_seconds: float = 10.0
|
|
51
|
+
runtime_orphaned_run_stale_seconds: float = 60.0
|
|
52
|
+
allow_full_trace_response: bool = False
|
|
53
|
+
protect_diagnostics: bool = False
|
|
54
|
+
trust_forwarded_for: bool = False
|
|
55
|
+
trace_dir: str = ""
|
|
56
|
+
runtime_workspace_dir: str = ""
|
|
57
|
+
redis_url: str = ""
|
|
58
|
+
milvus_url: str = ""
|
|
59
|
+
embedding_base_url: str = ""
|
|
60
|
+
embedding_api_key: str = ""
|
|
61
|
+
embedding_model: str = ""
|
|
62
|
+
embedding_timeout_seconds: float = 30.0
|
|
63
|
+
embedding_max_retries: int = 2
|
|
64
|
+
embedding_retry_backoff_seconds: float = 0.25
|
|
65
|
+
kafka_audit_url: str = ""
|
|
66
|
+
kafka_audit_topic: str = ""
|
|
67
|
+
external_backend_timeout_seconds: float = 2.0
|
|
68
|
+
run_timeout_seconds: float = 30.0
|
|
69
|
+
request_timeout_seconds: float = 10.0
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "ServiceConfig":
|
|
73
|
+
source = env if env is not None else environ
|
|
74
|
+
return cls(
|
|
75
|
+
host=source.get("KAGENT_SERVICE_HOST", cls.host),
|
|
76
|
+
port=_env_int(source, "KAGENT_SERVICE_PORT", cls.port),
|
|
77
|
+
max_request_bytes=_env_int(
|
|
78
|
+
source,
|
|
79
|
+
"KAGENT_SERVICE_MAX_REQUEST_BYTES",
|
|
80
|
+
cls.max_request_bytes,
|
|
81
|
+
),
|
|
82
|
+
max_goal_chars=_env_int(
|
|
83
|
+
source,
|
|
84
|
+
"KAGENT_SERVICE_MAX_GOAL_CHARS",
|
|
85
|
+
cls.max_goal_chars,
|
|
86
|
+
),
|
|
87
|
+
auth_token=source.get("KAGENT_SERVICE_AUTH_TOKEN", cls.auth_token),
|
|
88
|
+
auth_tokens=_env_auth_tokens(
|
|
89
|
+
source,
|
|
90
|
+
"KAGENT_SERVICE_AUTH_TOKENS",
|
|
91
|
+
),
|
|
92
|
+
rate_limit_per_minute=_env_int(
|
|
93
|
+
source,
|
|
94
|
+
"KAGENT_SERVICE_RATE_LIMIT_PER_MINUTE",
|
|
95
|
+
cls.rate_limit_per_minute,
|
|
96
|
+
),
|
|
97
|
+
max_concurrent_runs=_env_int(
|
|
98
|
+
source,
|
|
99
|
+
"KAGENT_SERVICE_MAX_CONCURRENT_RUNS",
|
|
100
|
+
cls.max_concurrent_runs,
|
|
101
|
+
),
|
|
102
|
+
idempotency_cache_size=_env_int(
|
|
103
|
+
source,
|
|
104
|
+
"KAGENT_SERVICE_IDEMPOTENCY_CACHE_SIZE",
|
|
105
|
+
cls.idempotency_cache_size,
|
|
106
|
+
),
|
|
107
|
+
idempotency_cache_path=source.get(
|
|
108
|
+
"KAGENT_SERVICE_IDEMPOTENCY_CACHE_PATH",
|
|
109
|
+
cls.idempotency_cache_path,
|
|
110
|
+
),
|
|
111
|
+
runtime_allowed_tools=_env_csv_tuple(
|
|
112
|
+
source,
|
|
113
|
+
"KAGENT_SERVICE_RUNTIME_ALLOWED_TOOLS",
|
|
114
|
+
),
|
|
115
|
+
runtime_allowed_tools_by_subject=_env_subject_csv_tuple_map(
|
|
116
|
+
source,
|
|
117
|
+
"KAGENT_SERVICE_RUNTIME_ALLOWED_TOOLS_BY_SUBJECT",
|
|
118
|
+
),
|
|
119
|
+
runtime_max_iterations=_env_int(
|
|
120
|
+
source,
|
|
121
|
+
"KAGENT_SERVICE_RUNTIME_MAX_ITERATIONS",
|
|
122
|
+
cls.runtime_max_iterations,
|
|
123
|
+
),
|
|
124
|
+
runtime_pending_approval_stale_seconds=_env_int(
|
|
125
|
+
source,
|
|
126
|
+
"KAGENT_SERVICE_RUNTIME_PENDING_APPROVAL_STALE_SECONDS",
|
|
127
|
+
cls.runtime_pending_approval_stale_seconds,
|
|
128
|
+
),
|
|
129
|
+
runtime_instance_heartbeat_seconds=_env_float(
|
|
130
|
+
source,
|
|
131
|
+
"KAGENT_SERVICE_RUNTIME_INSTANCE_HEARTBEAT_SECONDS",
|
|
132
|
+
cls.runtime_instance_heartbeat_seconds,
|
|
133
|
+
),
|
|
134
|
+
runtime_orphaned_run_stale_seconds=_env_float(
|
|
135
|
+
source,
|
|
136
|
+
"KAGENT_SERVICE_RUNTIME_ORPHANED_RUN_STALE_SECONDS",
|
|
137
|
+
cls.runtime_orphaned_run_stale_seconds,
|
|
138
|
+
),
|
|
139
|
+
allow_full_trace_response=_env_bool(
|
|
140
|
+
source,
|
|
141
|
+
"KAGENT_SERVICE_ALLOW_FULL_TRACE_RESPONSE",
|
|
142
|
+
cls.allow_full_trace_response,
|
|
143
|
+
),
|
|
144
|
+
protect_diagnostics=_env_bool(
|
|
145
|
+
source,
|
|
146
|
+
"KAGENT_SERVICE_PROTECT_DIAGNOSTICS",
|
|
147
|
+
cls.protect_diagnostics,
|
|
148
|
+
),
|
|
149
|
+
trust_forwarded_for=_env_bool(
|
|
150
|
+
source,
|
|
151
|
+
"KAGENT_SERVICE_TRUST_FORWARDED_FOR",
|
|
152
|
+
cls.trust_forwarded_for,
|
|
153
|
+
),
|
|
154
|
+
trace_dir=source.get("KAGENT_SERVICE_TRACE_DIR", cls.trace_dir),
|
|
155
|
+
runtime_workspace_dir=source.get(
|
|
156
|
+
"KAGENT_SERVICE_RUNTIME_WORKSPACE_DIR",
|
|
157
|
+
cls.runtime_workspace_dir,
|
|
158
|
+
),
|
|
159
|
+
redis_url=source.get("KAGENT_REDIS_URL", cls.redis_url),
|
|
160
|
+
milvus_url=source.get("KAGENT_MILVUS_URL", cls.milvus_url),
|
|
161
|
+
embedding_base_url=source.get(
|
|
162
|
+
"KAGENT_EMBEDDING_BASE_URL",
|
|
163
|
+
source.get("KAGENT_LLM_BASE_URL", cls.embedding_base_url),
|
|
164
|
+
),
|
|
165
|
+
embedding_api_key=source.get(
|
|
166
|
+
"KAGENT_EMBEDDING_API_KEY",
|
|
167
|
+
source.get("KAGENT_LLM_API_KEY", cls.embedding_api_key),
|
|
168
|
+
),
|
|
169
|
+
embedding_model=source.get(
|
|
170
|
+
"KAGENT_EMBEDDING_MODEL",
|
|
171
|
+
cls.embedding_model,
|
|
172
|
+
),
|
|
173
|
+
embedding_timeout_seconds=_env_float(
|
|
174
|
+
source,
|
|
175
|
+
"KAGENT_EMBEDDING_TIMEOUT_SECONDS",
|
|
176
|
+
cls.embedding_timeout_seconds,
|
|
177
|
+
),
|
|
178
|
+
embedding_max_retries=_env_int(
|
|
179
|
+
source,
|
|
180
|
+
"KAGENT_EMBEDDING_MAX_RETRIES",
|
|
181
|
+
cls.embedding_max_retries,
|
|
182
|
+
),
|
|
183
|
+
embedding_retry_backoff_seconds=_env_float(
|
|
184
|
+
source,
|
|
185
|
+
"KAGENT_EMBEDDING_RETRY_BACKOFF_SECONDS",
|
|
186
|
+
cls.embedding_retry_backoff_seconds,
|
|
187
|
+
),
|
|
188
|
+
kafka_audit_url=source.get(
|
|
189
|
+
"KAGENT_KAFKA_AUDIT_URL",
|
|
190
|
+
cls.kafka_audit_url,
|
|
191
|
+
),
|
|
192
|
+
kafka_audit_topic=source.get(
|
|
193
|
+
"KAGENT_KAFKA_AUDIT_TOPIC",
|
|
194
|
+
cls.kafka_audit_topic,
|
|
195
|
+
),
|
|
196
|
+
external_backend_timeout_seconds=_env_float(
|
|
197
|
+
source,
|
|
198
|
+
"KAGENT_EXTERNAL_BACKEND_TIMEOUT_SECONDS",
|
|
199
|
+
cls.external_backend_timeout_seconds,
|
|
200
|
+
),
|
|
201
|
+
run_timeout_seconds=_env_float(
|
|
202
|
+
source,
|
|
203
|
+
"KAGENT_SERVICE_RUN_TIMEOUT_SECONDS",
|
|
204
|
+
cls.run_timeout_seconds,
|
|
205
|
+
),
|
|
206
|
+
request_timeout_seconds=_env_float(
|
|
207
|
+
source,
|
|
208
|
+
"KAGENT_SERVICE_REQUEST_TIMEOUT_SECONDS",
|
|
209
|
+
cls.request_timeout_seconds,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def __post_init__(self) -> None:
|
|
214
|
+
if self.port < 1 or self.port > 65535:
|
|
215
|
+
raise ValueError("port must be between 1 and 65535")
|
|
216
|
+
if self.max_request_bytes < 1:
|
|
217
|
+
raise ValueError("max_request_bytes must be at least 1")
|
|
218
|
+
if self.max_goal_chars < 1:
|
|
219
|
+
raise ValueError("max_goal_chars must be at least 1")
|
|
220
|
+
if self.rate_limit_per_minute < 0:
|
|
221
|
+
raise ValueError("rate_limit_per_minute must be non-negative")
|
|
222
|
+
if self.max_concurrent_runs < 0:
|
|
223
|
+
raise ValueError("max_concurrent_runs must be non-negative")
|
|
224
|
+
if self.idempotency_cache_size < 0:
|
|
225
|
+
raise ValueError("idempotency_cache_size must be non-negative")
|
|
226
|
+
if self.idempotency_cache_path and self.idempotency_cache_size == 0:
|
|
227
|
+
raise ValueError("idempotency_cache_path requires idempotency_cache_size")
|
|
228
|
+
_validate_runtime_allowed_tools(self.runtime_allowed_tools)
|
|
229
|
+
_validate_runtime_allowed_tools_by_subject(
|
|
230
|
+
self.runtime_allowed_tools_by_subject
|
|
231
|
+
)
|
|
232
|
+
if self.runtime_max_iterations < 1:
|
|
233
|
+
raise ValueError("runtime_max_iterations must be at least 1")
|
|
234
|
+
if self.runtime_pending_approval_stale_seconds < 0:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"runtime_pending_approval_stale_seconds must be non-negative"
|
|
237
|
+
)
|
|
238
|
+
if self.runtime_instance_heartbeat_seconds <= 0:
|
|
239
|
+
raise ValueError("runtime_instance_heartbeat_seconds must be positive")
|
|
240
|
+
if (
|
|
241
|
+
self.runtime_orphaned_run_stale_seconds
|
|
242
|
+
<= self.runtime_instance_heartbeat_seconds
|
|
243
|
+
):
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"runtime_orphaned_run_stale_seconds must be greater than "
|
|
246
|
+
"runtime_instance_heartbeat_seconds"
|
|
247
|
+
)
|
|
248
|
+
if self.protect_diagnostics and not self.auth_required:
|
|
249
|
+
raise ValueError("protect_diagnostics requires auth_token")
|
|
250
|
+
_validate_auth_tokens(self.auth_tokens)
|
|
251
|
+
if self.run_timeout_seconds <= 0:
|
|
252
|
+
raise ValueError("run_timeout_seconds must be positive")
|
|
253
|
+
if self.request_timeout_seconds <= 0:
|
|
254
|
+
raise ValueError("request_timeout_seconds must be positive")
|
|
255
|
+
if self.external_backend_timeout_seconds <= 0:
|
|
256
|
+
raise ValueError("external_backend_timeout_seconds must be positive")
|
|
257
|
+
if self.embedding_timeout_seconds <= 0:
|
|
258
|
+
raise ValueError("embedding_timeout_seconds must be positive")
|
|
259
|
+
if self.embedding_max_retries < 0:
|
|
260
|
+
raise ValueError("embedding_max_retries must be non-negative")
|
|
261
|
+
if self.embedding_retry_backoff_seconds < 0:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
"embedding_retry_backoff_seconds must be non-negative"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def auth_required(self) -> bool:
|
|
268
|
+
return bool(self.auth_token or self.auth_tokens)
|
|
269
|
+
|
|
270
|
+
def runtime_allowed_tools_for_subject(
|
|
271
|
+
self,
|
|
272
|
+
auth_subject: str,
|
|
273
|
+
) -> Optional[Tuple[str, ...]]:
|
|
274
|
+
if auth_subject and auth_subject in self.runtime_allowed_tools_by_subject:
|
|
275
|
+
return self.runtime_allowed_tools_by_subject[auth_subject]
|
|
276
|
+
if self.runtime_allowed_tools:
|
|
277
|
+
return self.runtime_allowed_tools
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ServiceMetrics:
|
|
282
|
+
def __init__(self, *, started_at: Optional[float] = None) -> None:
|
|
283
|
+
self._lock = Lock()
|
|
284
|
+
self._started_at = time.monotonic() if started_at is None else started_at
|
|
285
|
+
self._requests_total = 0
|
|
286
|
+
self._total_duration_seconds = 0.0
|
|
287
|
+
self._max_duration_seconds = 0.0
|
|
288
|
+
self._request_duration_buckets: Dict[str, int] = _empty_duration_buckets()
|
|
289
|
+
self._responses_by_status: Dict[str, int] = {}
|
|
290
|
+
self._requests_by_method: Dict[str, int] = {}
|
|
291
|
+
self._requests_by_path: Dict[str, int] = {}
|
|
292
|
+
self._requests_by_auth_subject: Dict[str, int] = {}
|
|
293
|
+
self._error_responses_by_code: Dict[str, int] = {}
|
|
294
|
+
self._agent_runs_total = 0
|
|
295
|
+
self._agent_run_duration_seconds = 0.0
|
|
296
|
+
self._max_agent_run_duration_seconds = 0.0
|
|
297
|
+
self._agent_run_duration_buckets: Dict[str, int] = _empty_duration_buckets()
|
|
298
|
+
self._agent_runs_by_status: Dict[str, int] = {}
|
|
299
|
+
self._runtime_runs_total = 0
|
|
300
|
+
self._runtime_runs_by_status: Dict[str, int] = {}
|
|
301
|
+
self._runtime_runs_by_lifecycle_state: Dict[str, int] = {}
|
|
302
|
+
self._runtime_runs_by_auth_subject: Dict[str, int] = {}
|
|
303
|
+
self._runtime_runs_by_auth_subject_status: Dict[str, int] = {}
|
|
304
|
+
self._runtime_runs_by_auth_subject_lifecycle_state: Dict[str, int] = {}
|
|
305
|
+
self._runtime_resumes_by_auth_subject: Dict[str, int] = {}
|
|
306
|
+
self._runtime_approvals_by_auth_subject: Dict[str, int] = {}
|
|
307
|
+
self._runtime_failed_observations_total = 0
|
|
308
|
+
self._runtime_progress_event_sink_failures_total = 0
|
|
309
|
+
self._runtime_hook_failures_total = 0
|
|
310
|
+
self._runtime_reconciliation_runs_total = 0
|
|
311
|
+
self._runtime_reconciliation_runs_by_status: Dict[str, int] = {}
|
|
312
|
+
self._runtime_reconciliation_traces_scanned_total = 0
|
|
313
|
+
self._runtime_reconciliation_outcomes: Dict[str, int] = {}
|
|
314
|
+
self._runtime_reconciliation_errors_total = 0
|
|
315
|
+
self._runtime_observation_errors_by_code: Dict[str, int] = {}
|
|
316
|
+
self._runtime_tool_executions_by_tool_status: Dict[str, int] = {}
|
|
317
|
+
self._runtime_planner_attempts_by_status: Dict[str, int] = {}
|
|
318
|
+
self._runtime_planner_failures_total = 0
|
|
319
|
+
self._runtime_planner_failures_by_error_code: Dict[str, int] = {}
|
|
320
|
+
self._runtime_llm_provider_requests_total = 0
|
|
321
|
+
self._runtime_llm_provider_request_attempts_total = 0
|
|
322
|
+
self._runtime_llm_provider_request_retries_total = 0
|
|
323
|
+
self._runtime_llm_provider_requests_by_status: Dict[str, int] = {}
|
|
324
|
+
self._runtime_llm_provider_request_errors_by_type: Dict[str, int] = {}
|
|
325
|
+
self._runtime_llm_provider_request_http_status: Dict[str, int] = {}
|
|
326
|
+
self._runtime_llm_provider_request_retryable_reason: Dict[str, int] = {}
|
|
327
|
+
self._runtime_llm_provider_request_duration_seconds = 0.0
|
|
328
|
+
self._max_runtime_llm_provider_request_duration_seconds = 0.0
|
|
329
|
+
self._runtime_llm_provider_request_duration_buckets: Dict[str, int] = (
|
|
330
|
+
_empty_duration_buckets()
|
|
331
|
+
)
|
|
332
|
+
self._runtime_approval_required_total = 0
|
|
333
|
+
self._runtime_failed_budget_exhaustions_total = 0
|
|
334
|
+
self._runtime_run_duration_seconds = 0.0
|
|
335
|
+
self._max_runtime_run_duration_seconds = 0.0
|
|
336
|
+
self._runtime_run_duration_buckets: Dict[str, int] = _empty_duration_buckets()
|
|
337
|
+
|
|
338
|
+
def record(
|
|
339
|
+
self,
|
|
340
|
+
*,
|
|
341
|
+
path: str,
|
|
342
|
+
status_code: int,
|
|
343
|
+
method: str = "UNKNOWN",
|
|
344
|
+
duration_seconds: float = 0.0,
|
|
345
|
+
error_code: str = "",
|
|
346
|
+
auth_subject: str = "",
|
|
347
|
+
) -> None:
|
|
348
|
+
with self._lock:
|
|
349
|
+
self._requests_total += 1
|
|
350
|
+
self._total_duration_seconds += duration_seconds
|
|
351
|
+
self._max_duration_seconds = max(self._max_duration_seconds, duration_seconds)
|
|
352
|
+
for bucket, label in _duration_bucket_labels():
|
|
353
|
+
if duration_seconds <= bucket:
|
|
354
|
+
self._request_duration_buckets[label] += 1
|
|
355
|
+
self._request_duration_buckets["+Inf"] += 1
|
|
356
|
+
status_key = str(status_code)
|
|
357
|
+
method_key = _method_metrics_label(method)
|
|
358
|
+
self._responses_by_status[status_key] = self._responses_by_status.get(status_key, 0) + 1
|
|
359
|
+
self._requests_by_method[method_key] = self._requests_by_method.get(method_key, 0) + 1
|
|
360
|
+
self._requests_by_path[path] = self._requests_by_path.get(path, 0) + 1
|
|
361
|
+
if auth_subject:
|
|
362
|
+
self._requests_by_auth_subject[auth_subject] = (
|
|
363
|
+
self._requests_by_auth_subject.get(auth_subject, 0) + 1
|
|
364
|
+
)
|
|
365
|
+
if error_code:
|
|
366
|
+
self._error_responses_by_code[error_code] = (
|
|
367
|
+
self._error_responses_by_code.get(error_code, 0) + 1
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def record_agent_run(self, *, status: str, duration_seconds: float = 0.0) -> None:
|
|
371
|
+
with self._lock:
|
|
372
|
+
self._agent_runs_total += 1
|
|
373
|
+
self._agent_run_duration_seconds += duration_seconds
|
|
374
|
+
self._max_agent_run_duration_seconds = max(
|
|
375
|
+
self._max_agent_run_duration_seconds,
|
|
376
|
+
duration_seconds,
|
|
377
|
+
)
|
|
378
|
+
for bucket, label in _duration_bucket_labels():
|
|
379
|
+
if duration_seconds <= bucket:
|
|
380
|
+
self._agent_run_duration_buckets[label] += 1
|
|
381
|
+
self._agent_run_duration_buckets["+Inf"] += 1
|
|
382
|
+
self._agent_runs_by_status[status] = self._agent_runs_by_status.get(status, 0) + 1
|
|
383
|
+
|
|
384
|
+
def record_runtime_run(
|
|
385
|
+
self,
|
|
386
|
+
*,
|
|
387
|
+
status: str,
|
|
388
|
+
lifecycle_state: str = "",
|
|
389
|
+
failed_observation_count: int = 0,
|
|
390
|
+
approval_required_count: int = 0,
|
|
391
|
+
budget_exhausted: bool = False,
|
|
392
|
+
duration_seconds: float = 0.0,
|
|
393
|
+
error_code_counts: Optional[Mapping[str, int]] = None,
|
|
394
|
+
tool_status_counts: Optional[Mapping[str, int]] = None,
|
|
395
|
+
planner_attempt_status_counts: Optional[Mapping[str, int]] = None,
|
|
396
|
+
planner_failure_count: int = 0,
|
|
397
|
+
planner_error_code_counts: Optional[Mapping[str, int]] = None,
|
|
398
|
+
llm_provider_request: Optional[Mapping[str, Any]] = None,
|
|
399
|
+
auth_subject: str = "",
|
|
400
|
+
resumed_by_auth_subject: str = "",
|
|
401
|
+
approved_by_auth_subject: str = "",
|
|
402
|
+
progress_event_sink_failure_count: int = 0,
|
|
403
|
+
hook_failure_count: int = 0,
|
|
404
|
+
) -> None:
|
|
405
|
+
with self._lock:
|
|
406
|
+
self._runtime_runs_total += 1
|
|
407
|
+
self._runtime_runs_by_status[status] = (
|
|
408
|
+
self._runtime_runs_by_status.get(status, 0) + 1
|
|
409
|
+
)
|
|
410
|
+
normalized_lifecycle_state = lifecycle_state or _runtime_lifecycle_state(
|
|
411
|
+
status
|
|
412
|
+
)
|
|
413
|
+
self._runtime_runs_by_lifecycle_state[normalized_lifecycle_state] = (
|
|
414
|
+
self._runtime_runs_by_lifecycle_state.get(
|
|
415
|
+
normalized_lifecycle_state,
|
|
416
|
+
0,
|
|
417
|
+
)
|
|
418
|
+
+ 1
|
|
419
|
+
)
|
|
420
|
+
if auth_subject:
|
|
421
|
+
self._runtime_runs_by_auth_subject[auth_subject] = (
|
|
422
|
+
self._runtime_runs_by_auth_subject.get(auth_subject, 0) + 1
|
|
423
|
+
)
|
|
424
|
+
subject_status_key = _combined_metrics_key(auth_subject, status)
|
|
425
|
+
self._runtime_runs_by_auth_subject_status[subject_status_key] = (
|
|
426
|
+
self._runtime_runs_by_auth_subject_status.get(subject_status_key, 0)
|
|
427
|
+
+ 1
|
|
428
|
+
)
|
|
429
|
+
subject_lifecycle_key = _combined_metrics_key(
|
|
430
|
+
auth_subject,
|
|
431
|
+
normalized_lifecycle_state,
|
|
432
|
+
)
|
|
433
|
+
self._runtime_runs_by_auth_subject_lifecycle_state[
|
|
434
|
+
subject_lifecycle_key
|
|
435
|
+
] = (
|
|
436
|
+
self._runtime_runs_by_auth_subject_lifecycle_state.get(
|
|
437
|
+
subject_lifecycle_key,
|
|
438
|
+
0,
|
|
439
|
+
)
|
|
440
|
+
+ 1
|
|
441
|
+
)
|
|
442
|
+
if resumed_by_auth_subject:
|
|
443
|
+
self._runtime_resumes_by_auth_subject[resumed_by_auth_subject] = (
|
|
444
|
+
self._runtime_resumes_by_auth_subject.get(
|
|
445
|
+
resumed_by_auth_subject,
|
|
446
|
+
0,
|
|
447
|
+
)
|
|
448
|
+
+ 1
|
|
449
|
+
)
|
|
450
|
+
if approved_by_auth_subject:
|
|
451
|
+
self._runtime_approvals_by_auth_subject[approved_by_auth_subject] = (
|
|
452
|
+
self._runtime_approvals_by_auth_subject.get(
|
|
453
|
+
approved_by_auth_subject,
|
|
454
|
+
0,
|
|
455
|
+
)
|
|
456
|
+
+ 1
|
|
457
|
+
)
|
|
458
|
+
self._runtime_run_duration_seconds += duration_seconds
|
|
459
|
+
self._max_runtime_run_duration_seconds = max(
|
|
460
|
+
self._max_runtime_run_duration_seconds,
|
|
461
|
+
duration_seconds,
|
|
462
|
+
)
|
|
463
|
+
for bucket, label in _duration_bucket_labels():
|
|
464
|
+
if duration_seconds <= bucket:
|
|
465
|
+
self._runtime_run_duration_buckets[label] += 1
|
|
466
|
+
self._runtime_run_duration_buckets["+Inf"] += 1
|
|
467
|
+
self._runtime_failed_observations_total += max(0, failed_observation_count)
|
|
468
|
+
self._runtime_progress_event_sink_failures_total += max(
|
|
469
|
+
0,
|
|
470
|
+
progress_event_sink_failure_count,
|
|
471
|
+
)
|
|
472
|
+
self._runtime_hook_failures_total += max(0, hook_failure_count)
|
|
473
|
+
for error_code, count in (error_code_counts or {}).items():
|
|
474
|
+
normalized_error_code = str(error_code)
|
|
475
|
+
if not normalized_error_code:
|
|
476
|
+
continue
|
|
477
|
+
self._runtime_observation_errors_by_code[normalized_error_code] = (
|
|
478
|
+
self._runtime_observation_errors_by_code.get(
|
|
479
|
+
normalized_error_code,
|
|
480
|
+
0,
|
|
481
|
+
)
|
|
482
|
+
+ max(0, int(count))
|
|
483
|
+
)
|
|
484
|
+
for tool_status, count in (tool_status_counts or {}).items():
|
|
485
|
+
tool, status_label = _split_combined_metrics_key(str(tool_status))
|
|
486
|
+
normalized_tool = _runtime_tool_metrics_label(tool)
|
|
487
|
+
normalized_status = _runtime_observation_status_metrics_label(
|
|
488
|
+
status_label
|
|
489
|
+
)
|
|
490
|
+
key = _combined_metrics_key(normalized_tool, normalized_status)
|
|
491
|
+
self._runtime_tool_executions_by_tool_status[key] = (
|
|
492
|
+
self._runtime_tool_executions_by_tool_status.get(key, 0)
|
|
493
|
+
+ max(0, int(count))
|
|
494
|
+
)
|
|
495
|
+
for status_label, count in (planner_attempt_status_counts or {}).items():
|
|
496
|
+
normalized_status = _runtime_planner_attempt_status_metrics_label(
|
|
497
|
+
str(status_label)
|
|
498
|
+
)
|
|
499
|
+
self._runtime_planner_attempts_by_status[normalized_status] = (
|
|
500
|
+
self._runtime_planner_attempts_by_status.get(
|
|
501
|
+
normalized_status,
|
|
502
|
+
0,
|
|
503
|
+
)
|
|
504
|
+
+ max(0, int(count))
|
|
505
|
+
)
|
|
506
|
+
self._runtime_planner_failures_total += max(0, planner_failure_count)
|
|
507
|
+
for error_code, count in (planner_error_code_counts or {}).items():
|
|
508
|
+
normalized_error_code = str(error_code)
|
|
509
|
+
if not normalized_error_code:
|
|
510
|
+
continue
|
|
511
|
+
self._runtime_planner_failures_by_error_code[
|
|
512
|
+
normalized_error_code
|
|
513
|
+
] = (
|
|
514
|
+
self._runtime_planner_failures_by_error_code.get(
|
|
515
|
+
normalized_error_code,
|
|
516
|
+
0,
|
|
517
|
+
)
|
|
518
|
+
+ max(0, int(count))
|
|
519
|
+
)
|
|
520
|
+
if llm_provider_request:
|
|
521
|
+
attempt_count = _non_negative_int_value(
|
|
522
|
+
llm_provider_request.get("attempt_count", 0)
|
|
523
|
+
)
|
|
524
|
+
retry_count = _non_negative_int_value(
|
|
525
|
+
llm_provider_request.get("retry_count", 0)
|
|
526
|
+
)
|
|
527
|
+
provider_duration = _non_negative_float_value(
|
|
528
|
+
llm_provider_request.get("duration_seconds", 0.0)
|
|
529
|
+
)
|
|
530
|
+
provider_status = _runtime_llm_provider_status_metrics_label(
|
|
531
|
+
str(llm_provider_request.get("status", ""))
|
|
532
|
+
)
|
|
533
|
+
provider_error_type = _runtime_llm_provider_error_type_metrics_label(
|
|
534
|
+
str(llm_provider_request.get("error_type", ""))
|
|
535
|
+
)
|
|
536
|
+
provider_http_status = _runtime_llm_provider_http_status_metrics_label(
|
|
537
|
+
str(llm_provider_request.get("http_status", ""))
|
|
538
|
+
)
|
|
539
|
+
provider_retryable_reason = (
|
|
540
|
+
_runtime_llm_provider_retryable_reason_metrics_label(
|
|
541
|
+
str(llm_provider_request.get("retryable_reason", ""))
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
self._runtime_llm_provider_requests_total += 1
|
|
545
|
+
self._runtime_llm_provider_request_attempts_total += attempt_count
|
|
546
|
+
self._runtime_llm_provider_request_retries_total += retry_count
|
|
547
|
+
self._runtime_llm_provider_requests_by_status[provider_status] = (
|
|
548
|
+
self._runtime_llm_provider_requests_by_status.get(
|
|
549
|
+
provider_status,
|
|
550
|
+
0,
|
|
551
|
+
)
|
|
552
|
+
+ 1
|
|
553
|
+
)
|
|
554
|
+
if provider_error_type:
|
|
555
|
+
self._runtime_llm_provider_request_errors_by_type[
|
|
556
|
+
provider_error_type
|
|
557
|
+
] = (
|
|
558
|
+
self._runtime_llm_provider_request_errors_by_type.get(
|
|
559
|
+
provider_error_type,
|
|
560
|
+
0,
|
|
561
|
+
)
|
|
562
|
+
+ 1
|
|
563
|
+
)
|
|
564
|
+
if provider_http_status:
|
|
565
|
+
self._runtime_llm_provider_request_http_status[
|
|
566
|
+
provider_http_status
|
|
567
|
+
] = (
|
|
568
|
+
self._runtime_llm_provider_request_http_status.get(
|
|
569
|
+
provider_http_status,
|
|
570
|
+
0,
|
|
571
|
+
)
|
|
572
|
+
+ 1
|
|
573
|
+
)
|
|
574
|
+
if provider_retryable_reason:
|
|
575
|
+
self._runtime_llm_provider_request_retryable_reason[
|
|
576
|
+
provider_retryable_reason
|
|
577
|
+
] = (
|
|
578
|
+
self._runtime_llm_provider_request_retryable_reason.get(
|
|
579
|
+
provider_retryable_reason,
|
|
580
|
+
0,
|
|
581
|
+
)
|
|
582
|
+
+ 1
|
|
583
|
+
)
|
|
584
|
+
self._runtime_llm_provider_request_duration_seconds += provider_duration
|
|
585
|
+
self._max_runtime_llm_provider_request_duration_seconds = max(
|
|
586
|
+
self._max_runtime_llm_provider_request_duration_seconds,
|
|
587
|
+
provider_duration,
|
|
588
|
+
)
|
|
589
|
+
for bucket, label in _duration_bucket_labels():
|
|
590
|
+
if provider_duration <= bucket:
|
|
591
|
+
self._runtime_llm_provider_request_duration_buckets[label] += 1
|
|
592
|
+
self._runtime_llm_provider_request_duration_buckets["+Inf"] += 1
|
|
593
|
+
self._runtime_approval_required_total += max(0, approval_required_count)
|
|
594
|
+
if budget_exhausted:
|
|
595
|
+
self._runtime_failed_budget_exhaustions_total += 1
|
|
596
|
+
|
|
597
|
+
def record_runtime_reconciliation(
|
|
598
|
+
self,
|
|
599
|
+
summary: Mapping[str, Any],
|
|
600
|
+
*,
|
|
601
|
+
status: str = "ok",
|
|
602
|
+
) -> None:
|
|
603
|
+
status_key = (
|
|
604
|
+
status
|
|
605
|
+
if status in _RUNTIME_RECONCILIATION_STATUSES
|
|
606
|
+
else _UNKNOWN_METRICS_LABEL
|
|
607
|
+
)
|
|
608
|
+
errors = summary.get("errors", ())
|
|
609
|
+
error_count = (
|
|
610
|
+
len(errors)
|
|
611
|
+
if isinstance(errors, (list, tuple))
|
|
612
|
+
else _non_negative_int_value(errors)
|
|
613
|
+
)
|
|
614
|
+
with self._lock:
|
|
615
|
+
self._runtime_reconciliation_runs_total += 1
|
|
616
|
+
self._runtime_reconciliation_runs_by_status[status_key] = (
|
|
617
|
+
self._runtime_reconciliation_runs_by_status.get(status_key, 0) + 1
|
|
618
|
+
)
|
|
619
|
+
self._runtime_reconciliation_traces_scanned_total += (
|
|
620
|
+
_non_negative_int_value(summary.get("scanned", 0))
|
|
621
|
+
)
|
|
622
|
+
for outcome in _RUNTIME_RECONCILIATION_OUTCOMES:
|
|
623
|
+
count = _non_negative_int_value(summary.get(outcome, 0))
|
|
624
|
+
if count:
|
|
625
|
+
self._runtime_reconciliation_outcomes[outcome] = (
|
|
626
|
+
self._runtime_reconciliation_outcomes.get(outcome, 0) + count
|
|
627
|
+
)
|
|
628
|
+
self._runtime_reconciliation_errors_total += error_count
|
|
629
|
+
|
|
630
|
+
def snapshot(self, *, now: Optional[float] = None) -> Dict[str, Any]:
|
|
631
|
+
with self._lock:
|
|
632
|
+
current_time = time.monotonic() if now is None else now
|
|
633
|
+
average_duration = (
|
|
634
|
+
self._total_duration_seconds / self._requests_total
|
|
635
|
+
if self._requests_total
|
|
636
|
+
else 0.0
|
|
637
|
+
)
|
|
638
|
+
average_agent_run_duration = (
|
|
639
|
+
self._agent_run_duration_seconds / self._agent_runs_total
|
|
640
|
+
if self._agent_runs_total
|
|
641
|
+
else 0.0
|
|
642
|
+
)
|
|
643
|
+
average_runtime_run_duration = (
|
|
644
|
+
self._runtime_run_duration_seconds / self._runtime_runs_total
|
|
645
|
+
if self._runtime_runs_total
|
|
646
|
+
else 0.0
|
|
647
|
+
)
|
|
648
|
+
average_runtime_llm_provider_request_duration = (
|
|
649
|
+
self._runtime_llm_provider_request_duration_seconds
|
|
650
|
+
/ self._runtime_llm_provider_requests_total
|
|
651
|
+
if self._runtime_llm_provider_requests_total
|
|
652
|
+
else 0.0
|
|
653
|
+
)
|
|
654
|
+
return {
|
|
655
|
+
"requests_total": str(self._requests_total),
|
|
656
|
+
"responses_by_status": _string_counts(self._responses_by_status),
|
|
657
|
+
"requests_by_method": _string_counts(self._requests_by_method),
|
|
658
|
+
"requests_by_path": _string_counts(self._requests_by_path),
|
|
659
|
+
"requests_by_auth_subject": _string_counts(
|
|
660
|
+
self._requests_by_auth_subject
|
|
661
|
+
),
|
|
662
|
+
"error_responses_by_code": _string_counts(self._error_responses_by_code),
|
|
663
|
+
"request_duration_seconds_bucket": _string_counts(
|
|
664
|
+
self._request_duration_buckets
|
|
665
|
+
),
|
|
666
|
+
"request_duration_seconds_count": str(self._requests_total),
|
|
667
|
+
"request_duration_seconds_sum": f"{self._total_duration_seconds:.4f}",
|
|
668
|
+
"average_duration_seconds": f"{average_duration:.4f}",
|
|
669
|
+
"max_duration_seconds": f"{self._max_duration_seconds:.4f}",
|
|
670
|
+
"agent_runs_total": str(self._agent_runs_total),
|
|
671
|
+
"agent_runs_by_status": _string_counts(self._agent_runs_by_status),
|
|
672
|
+
"agent_run_duration_seconds_bucket": _string_counts(
|
|
673
|
+
self._agent_run_duration_buckets
|
|
674
|
+
),
|
|
675
|
+
"agent_run_duration_seconds_count": str(self._agent_runs_total),
|
|
676
|
+
"agent_run_duration_seconds_sum": f"{self._agent_run_duration_seconds:.4f}",
|
|
677
|
+
"average_agent_run_duration_seconds": f"{average_agent_run_duration:.4f}",
|
|
678
|
+
"max_agent_run_duration_seconds": f"{self._max_agent_run_duration_seconds:.4f}",
|
|
679
|
+
"runtime_runs_total": str(self._runtime_runs_total),
|
|
680
|
+
"runtime_runs_by_status": _string_counts(self._runtime_runs_by_status),
|
|
681
|
+
"runtime_runs_by_lifecycle_state": _string_counts(
|
|
682
|
+
self._runtime_runs_by_lifecycle_state
|
|
683
|
+
),
|
|
684
|
+
"runtime_runs_by_auth_subject": _string_counts(
|
|
685
|
+
self._runtime_runs_by_auth_subject
|
|
686
|
+
),
|
|
687
|
+
"runtime_runs_by_auth_subject_status": _string_counts(
|
|
688
|
+
self._runtime_runs_by_auth_subject_status
|
|
689
|
+
),
|
|
690
|
+
"runtime_runs_by_auth_subject_lifecycle_state": _string_counts(
|
|
691
|
+
self._runtime_runs_by_auth_subject_lifecycle_state
|
|
692
|
+
),
|
|
693
|
+
"runtime_resumes_by_auth_subject": _string_counts(
|
|
694
|
+
self._runtime_resumes_by_auth_subject
|
|
695
|
+
),
|
|
696
|
+
"runtime_approvals_by_auth_subject": _string_counts(
|
|
697
|
+
self._runtime_approvals_by_auth_subject
|
|
698
|
+
),
|
|
699
|
+
"runtime_failed_observations_total": str(
|
|
700
|
+
self._runtime_failed_observations_total
|
|
701
|
+
),
|
|
702
|
+
"runtime_progress_event_sink_failures_total": str(
|
|
703
|
+
self._runtime_progress_event_sink_failures_total
|
|
704
|
+
),
|
|
705
|
+
"runtime_hook_failures_total": str(
|
|
706
|
+
self._runtime_hook_failures_total
|
|
707
|
+
),
|
|
708
|
+
"runtime_reconciliation_runs_total": str(
|
|
709
|
+
self._runtime_reconciliation_runs_total
|
|
710
|
+
),
|
|
711
|
+
"runtime_reconciliation_runs_by_status": _string_counts(
|
|
712
|
+
self._runtime_reconciliation_runs_by_status
|
|
713
|
+
),
|
|
714
|
+
"runtime_reconciliation_traces_scanned_total": str(
|
|
715
|
+
self._runtime_reconciliation_traces_scanned_total
|
|
716
|
+
),
|
|
717
|
+
"runtime_reconciliation_outcomes": _string_counts(
|
|
718
|
+
self._runtime_reconciliation_outcomes
|
|
719
|
+
),
|
|
720
|
+
"runtime_reconciliation_errors_total": str(
|
|
721
|
+
self._runtime_reconciliation_errors_total
|
|
722
|
+
),
|
|
723
|
+
"runtime_observation_errors_by_code": _string_counts(
|
|
724
|
+
self._runtime_observation_errors_by_code
|
|
725
|
+
),
|
|
726
|
+
"runtime_tool_executions_by_tool_status": _string_counts(
|
|
727
|
+
self._runtime_tool_executions_by_tool_status
|
|
728
|
+
),
|
|
729
|
+
"runtime_planner_attempts_by_status": _string_counts(
|
|
730
|
+
self._runtime_planner_attempts_by_status
|
|
731
|
+
),
|
|
732
|
+
"runtime_planner_failures_total": str(
|
|
733
|
+
self._runtime_planner_failures_total
|
|
734
|
+
),
|
|
735
|
+
"runtime_planner_failures_by_error_code": _string_counts(
|
|
736
|
+
self._runtime_planner_failures_by_error_code
|
|
737
|
+
),
|
|
738
|
+
"runtime_llm_provider_requests_total": str(
|
|
739
|
+
self._runtime_llm_provider_requests_total
|
|
740
|
+
),
|
|
741
|
+
"runtime_llm_provider_request_attempts_total": str(
|
|
742
|
+
self._runtime_llm_provider_request_attempts_total
|
|
743
|
+
),
|
|
744
|
+
"runtime_llm_provider_request_retries_total": str(
|
|
745
|
+
self._runtime_llm_provider_request_retries_total
|
|
746
|
+
),
|
|
747
|
+
"runtime_llm_provider_requests_by_status": _string_counts(
|
|
748
|
+
self._runtime_llm_provider_requests_by_status
|
|
749
|
+
),
|
|
750
|
+
"runtime_llm_provider_request_errors_by_type": _string_counts(
|
|
751
|
+
self._runtime_llm_provider_request_errors_by_type
|
|
752
|
+
),
|
|
753
|
+
"runtime_llm_provider_request_http_status": _string_counts(
|
|
754
|
+
self._runtime_llm_provider_request_http_status
|
|
755
|
+
),
|
|
756
|
+
"runtime_llm_provider_request_retryable_reason": _string_counts(
|
|
757
|
+
self._runtime_llm_provider_request_retryable_reason
|
|
758
|
+
),
|
|
759
|
+
"runtime_llm_provider_request_duration_seconds_bucket": _string_counts(
|
|
760
|
+
self._runtime_llm_provider_request_duration_buckets
|
|
761
|
+
),
|
|
762
|
+
"runtime_llm_provider_request_duration_seconds_count": str(
|
|
763
|
+
self._runtime_llm_provider_requests_total
|
|
764
|
+
),
|
|
765
|
+
"runtime_llm_provider_request_duration_seconds_sum": (
|
|
766
|
+
f"{self._runtime_llm_provider_request_duration_seconds:.4f}"
|
|
767
|
+
),
|
|
768
|
+
"average_runtime_llm_provider_request_duration_seconds": (
|
|
769
|
+
f"{average_runtime_llm_provider_request_duration:.4f}"
|
|
770
|
+
),
|
|
771
|
+
"max_runtime_llm_provider_request_duration_seconds": (
|
|
772
|
+
f"{self._max_runtime_llm_provider_request_duration_seconds:.4f}"
|
|
773
|
+
),
|
|
774
|
+
"runtime_approval_required_total": str(
|
|
775
|
+
self._runtime_approval_required_total
|
|
776
|
+
),
|
|
777
|
+
"runtime_failed_budget_exhaustions_total": str(
|
|
778
|
+
self._runtime_failed_budget_exhaustions_total
|
|
779
|
+
),
|
|
780
|
+
"runtime_run_duration_seconds_bucket": _string_counts(
|
|
781
|
+
self._runtime_run_duration_buckets
|
|
782
|
+
),
|
|
783
|
+
"runtime_run_duration_seconds_count": str(self._runtime_runs_total),
|
|
784
|
+
"runtime_run_duration_seconds_sum": (
|
|
785
|
+
f"{self._runtime_run_duration_seconds:.4f}"
|
|
786
|
+
),
|
|
787
|
+
"average_runtime_run_duration_seconds": (
|
|
788
|
+
f"{average_runtime_run_duration:.4f}"
|
|
789
|
+
),
|
|
790
|
+
"max_runtime_run_duration_seconds": (
|
|
791
|
+
f"{self._max_runtime_run_duration_seconds:.4f}"
|
|
792
|
+
),
|
|
793
|
+
"uptime_seconds": f"{max(0.0, current_time - self._started_at):.4f}",
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
class ServiceRateLimiter:
|
|
798
|
+
def __init__(self, *, limit_per_minute: int) -> None:
|
|
799
|
+
if limit_per_minute < 0:
|
|
800
|
+
raise ValueError("limit_per_minute must be non-negative")
|
|
801
|
+
self._limit_per_minute = limit_per_minute
|
|
802
|
+
self._lock = Lock()
|
|
803
|
+
self._windows: Dict[str, Tuple[float, int]] = {}
|
|
804
|
+
|
|
805
|
+
def allow(self, key: str, *, now: Optional[float] = None) -> bool:
|
|
806
|
+
if self._limit_per_minute == 0:
|
|
807
|
+
return True
|
|
808
|
+
current_time = time.monotonic() if now is None else now
|
|
809
|
+
with self._lock:
|
|
810
|
+
self._prune_expired_windows(current_time)
|
|
811
|
+
window_start, count = self._windows.get(key, (current_time, 0))
|
|
812
|
+
if current_time - window_start >= 60:
|
|
813
|
+
self._windows[key] = (current_time, 1)
|
|
814
|
+
return True
|
|
815
|
+
if count >= self._limit_per_minute:
|
|
816
|
+
return False
|
|
817
|
+
self._windows[key] = (window_start, count + 1)
|
|
818
|
+
return True
|
|
819
|
+
|
|
820
|
+
def snapshot(self, *, now: Optional[float] = None) -> Dict[str, str]:
|
|
821
|
+
current_time = time.monotonic() if now is None else now
|
|
822
|
+
with self._lock:
|
|
823
|
+
self._prune_expired_windows(current_time)
|
|
824
|
+
return {
|
|
825
|
+
"active_rate_limit_windows": str(len(self._windows)),
|
|
826
|
+
"rate_limit_per_minute": str(self._limit_per_minute),
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
def retry_after_seconds(self, key: str, *, now: Optional[float] = None) -> int:
|
|
830
|
+
if self._limit_per_minute == 0:
|
|
831
|
+
return 0
|
|
832
|
+
current_time = time.monotonic() if now is None else now
|
|
833
|
+
with self._lock:
|
|
834
|
+
self._prune_expired_windows(current_time)
|
|
835
|
+
window = self._windows.get(key)
|
|
836
|
+
if window is None:
|
|
837
|
+
return 0
|
|
838
|
+
window_start, _count = window
|
|
839
|
+
remaining = 60 - (current_time - window_start)
|
|
840
|
+
return max(1, int(ceil(remaining)))
|
|
841
|
+
|
|
842
|
+
def _prune_expired_windows(self, current_time: float) -> None:
|
|
843
|
+
expired_keys = [
|
|
844
|
+
key
|
|
845
|
+
for key, (window_start, _count) in self._windows.items()
|
|
846
|
+
if current_time - window_start >= 60
|
|
847
|
+
]
|
|
848
|
+
for key in expired_keys:
|
|
849
|
+
del self._windows[key]
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
class ServiceConcurrencyLimiter:
|
|
853
|
+
def __init__(self, *, max_concurrent_runs: int) -> None:
|
|
854
|
+
if max_concurrent_runs < 0:
|
|
855
|
+
raise ValueError("max_concurrent_runs must be non-negative")
|
|
856
|
+
self._lock = Lock()
|
|
857
|
+
self._max_concurrent_runs = max_concurrent_runs
|
|
858
|
+
self._active_runs = 0
|
|
859
|
+
self._disabled = max_concurrent_runs == 0
|
|
860
|
+
self._semaphore = BoundedSemaphore(max_concurrent_runs or 1)
|
|
861
|
+
|
|
862
|
+
def try_acquire(self) -> Optional[Callable[[], None]]:
|
|
863
|
+
if self._disabled:
|
|
864
|
+
return _noop_release
|
|
865
|
+
if not self._semaphore.acquire(blocking=False):
|
|
866
|
+
return None
|
|
867
|
+
with self._lock:
|
|
868
|
+
self._active_runs += 1
|
|
869
|
+
|
|
870
|
+
released = False
|
|
871
|
+
|
|
872
|
+
def release() -> None:
|
|
873
|
+
nonlocal released
|
|
874
|
+
if released:
|
|
875
|
+
return
|
|
876
|
+
released = True
|
|
877
|
+
with self._lock:
|
|
878
|
+
self._active_runs -= 1
|
|
879
|
+
self._semaphore.release()
|
|
880
|
+
|
|
881
|
+
return release
|
|
882
|
+
|
|
883
|
+
def snapshot(self) -> Dict[str, str]:
|
|
884
|
+
with self._lock:
|
|
885
|
+
return {
|
|
886
|
+
"active_concurrent_runs": str(self._active_runs),
|
|
887
|
+
"max_concurrent_runs": str(self._max_concurrent_runs),
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def access_log_record(
|
|
892
|
+
*,
|
|
893
|
+
method: str,
|
|
894
|
+
path: str,
|
|
895
|
+
status_code: int,
|
|
896
|
+
duration_seconds: float,
|
|
897
|
+
request_id: str,
|
|
898
|
+
remote_addr: str,
|
|
899
|
+
error_code: str = "",
|
|
900
|
+
run_id: str = "",
|
|
901
|
+
trace_path: str = "",
|
|
902
|
+
idempotency_key_present: Optional[bool] = None,
|
|
903
|
+
request_body_bytes: Optional[int] = None,
|
|
904
|
+
auth_subject: str = "",
|
|
905
|
+
runtime_owner_auth_subject: str = "",
|
|
906
|
+
resumed_by_auth_subject: str = "",
|
|
907
|
+
approved_by_auth_subject: str = "",
|
|
908
|
+
) -> Dict[str, Any]:
|
|
909
|
+
record = {
|
|
910
|
+
"event": "http_request",
|
|
911
|
+
"method": method,
|
|
912
|
+
"path": path,
|
|
913
|
+
"status_code": status_code,
|
|
914
|
+
"duration_seconds": f"{duration_seconds:.4f}",
|
|
915
|
+
"request_id": request_id,
|
|
916
|
+
"remote_addr": remote_addr,
|
|
917
|
+
}
|
|
918
|
+
if error_code:
|
|
919
|
+
record["error_code"] = error_code
|
|
920
|
+
if run_id:
|
|
921
|
+
record["run_id"] = run_id
|
|
922
|
+
if trace_path:
|
|
923
|
+
record["trace_path"] = trace_path
|
|
924
|
+
if idempotency_key_present is not None:
|
|
925
|
+
record["idempotency_key_present"] = idempotency_key_present
|
|
926
|
+
if request_body_bytes is not None:
|
|
927
|
+
record["request_body_bytes"] = request_body_bytes
|
|
928
|
+
if auth_subject:
|
|
929
|
+
record["auth_subject"] = auth_subject
|
|
930
|
+
if runtime_owner_auth_subject:
|
|
931
|
+
record["runtime_owner_auth_subject"] = runtime_owner_auth_subject
|
|
932
|
+
if resumed_by_auth_subject:
|
|
933
|
+
record["resumed_by_auth_subject"] = resumed_by_auth_subject
|
|
934
|
+
if approved_by_auth_subject:
|
|
935
|
+
record["approved_by_auth_subject"] = approved_by_auth_subject
|
|
936
|
+
return record
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def access_log_schema() -> Dict[str, Any]:
|
|
940
|
+
return {
|
|
941
|
+
"type": "object",
|
|
942
|
+
"required": [
|
|
943
|
+
"event",
|
|
944
|
+
"method",
|
|
945
|
+
"path",
|
|
946
|
+
"status_code",
|
|
947
|
+
"duration_seconds",
|
|
948
|
+
"request_id",
|
|
949
|
+
"remote_addr",
|
|
950
|
+
],
|
|
951
|
+
"properties": {
|
|
952
|
+
"event": {"type": "string", "const": "http_request"},
|
|
953
|
+
"method": {"type": "string"},
|
|
954
|
+
"path": {"type": "string"},
|
|
955
|
+
"status_code": {"type": "integer"},
|
|
956
|
+
"duration_seconds": {"type": "string", "pattern": r"^\d+\.\d{4}$"},
|
|
957
|
+
"request_id": {"type": "string"},
|
|
958
|
+
"remote_addr": {"type": "string"},
|
|
959
|
+
"error_code": {"type": "string"},
|
|
960
|
+
"run_id": {"type": "string"},
|
|
961
|
+
"trace_path": {"type": "string"},
|
|
962
|
+
"idempotency_key_present": {"type": "boolean"},
|
|
963
|
+
"request_body_bytes": {"type": "integer"},
|
|
964
|
+
"auth_subject": {"type": "string"},
|
|
965
|
+
"runtime_owner_auth_subject": {"type": "string"},
|
|
966
|
+
"resumed_by_auth_subject": {"type": "string"},
|
|
967
|
+
"approved_by_auth_subject": {"type": "string"},
|
|
968
|
+
},
|
|
969
|
+
"additionalProperties": False,
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def prometheus_metrics_text(snapshot: Mapping[str, Any]) -> str:
|
|
974
|
+
lines = [
|
|
975
|
+
"# HELP kagent_requests_total Total HTTP requests handled.",
|
|
976
|
+
"# TYPE kagent_requests_total counter",
|
|
977
|
+
f"kagent_requests_total {snapshot.get('requests_total', '0')}",
|
|
978
|
+
"# HELP kagent_responses_total HTTP responses by status code.",
|
|
979
|
+
"# TYPE kagent_responses_total counter",
|
|
980
|
+
]
|
|
981
|
+
for status, count in _mapping_value(snapshot, "responses_by_status").items():
|
|
982
|
+
lines.append(
|
|
983
|
+
f'kagent_responses_total{{status="{_prometheus_label(status)}"}} {count}'
|
|
984
|
+
)
|
|
985
|
+
lines.extend(
|
|
986
|
+
[
|
|
987
|
+
"# HELP kagent_requests_by_method_total HTTP requests by method.",
|
|
988
|
+
"# TYPE kagent_requests_by_method_total counter",
|
|
989
|
+
]
|
|
990
|
+
)
|
|
991
|
+
for method, count in _mapping_value(snapshot, "requests_by_method").items():
|
|
992
|
+
lines.append(
|
|
993
|
+
"kagent_requests_by_method_total"
|
|
994
|
+
f'{{method="{_prometheus_label(method)}"}} {count}'
|
|
995
|
+
)
|
|
996
|
+
lines.extend(
|
|
997
|
+
[
|
|
998
|
+
"# HELP kagent_requests_by_path_total HTTP requests by route.",
|
|
999
|
+
"# TYPE kagent_requests_by_path_total counter",
|
|
1000
|
+
]
|
|
1001
|
+
)
|
|
1002
|
+
for path, count in _mapping_value(snapshot, "requests_by_path").items():
|
|
1003
|
+
lines.append(
|
|
1004
|
+
f'kagent_requests_by_path_total{{path="{_prometheus_label(path)}"}} '
|
|
1005
|
+
f"{count}"
|
|
1006
|
+
)
|
|
1007
|
+
lines.extend(
|
|
1008
|
+
[
|
|
1009
|
+
"# HELP kagent_requests_by_auth_subject_total "
|
|
1010
|
+
"HTTP requests by authenticated internal subject.",
|
|
1011
|
+
"# TYPE kagent_requests_by_auth_subject_total counter",
|
|
1012
|
+
]
|
|
1013
|
+
)
|
|
1014
|
+
for auth_subject, count in _mapping_value(snapshot, "requests_by_auth_subject").items():
|
|
1015
|
+
lines.append(
|
|
1016
|
+
"kagent_requests_by_auth_subject_total"
|
|
1017
|
+
f'{{auth_subject="{_prometheus_label(auth_subject)}"}} {count}'
|
|
1018
|
+
)
|
|
1019
|
+
lines.extend(
|
|
1020
|
+
[
|
|
1021
|
+
"# HELP kagent_error_responses_total Errors by stable code.",
|
|
1022
|
+
"# TYPE kagent_error_responses_total counter",
|
|
1023
|
+
]
|
|
1024
|
+
)
|
|
1025
|
+
for error_code, count in _mapping_value(snapshot, "error_responses_by_code").items():
|
|
1026
|
+
lines.append(
|
|
1027
|
+
"kagent_error_responses_total"
|
|
1028
|
+
f'{{error_code="{_prometheus_label(error_code)}"}} {count}'
|
|
1029
|
+
)
|
|
1030
|
+
lines.extend(
|
|
1031
|
+
[
|
|
1032
|
+
"# HELP kagent_request_duration_seconds "
|
|
1033
|
+
"HTTP request duration in seconds.",
|
|
1034
|
+
"# TYPE kagent_request_duration_seconds histogram",
|
|
1035
|
+
]
|
|
1036
|
+
)
|
|
1037
|
+
for bucket, count in _mapping_value(snapshot, "request_duration_seconds_bucket").items():
|
|
1038
|
+
lines.append(
|
|
1039
|
+
"kagent_request_duration_seconds_bucket"
|
|
1040
|
+
f'{{le="{_prometheus_label(bucket)}"}} {count}'
|
|
1041
|
+
)
|
|
1042
|
+
lines.append(
|
|
1043
|
+
"kagent_request_duration_seconds_count "
|
|
1044
|
+
f"{snapshot.get('request_duration_seconds_count', '0')}"
|
|
1045
|
+
)
|
|
1046
|
+
lines.append(
|
|
1047
|
+
"kagent_request_duration_seconds_sum "
|
|
1048
|
+
f"{snapshot.get('request_duration_seconds_sum', '0.0000')}"
|
|
1049
|
+
)
|
|
1050
|
+
lines.extend(
|
|
1051
|
+
[
|
|
1052
|
+
"# HELP kagent_runs_total Total agent runs handled by /run.",
|
|
1053
|
+
"# TYPE kagent_runs_total counter",
|
|
1054
|
+
f"kagent_runs_total {snapshot.get('agent_runs_total', '0')}",
|
|
1055
|
+
"# HELP kagent_run_status_total Agent runs by final status.",
|
|
1056
|
+
"# TYPE kagent_run_status_total counter",
|
|
1057
|
+
]
|
|
1058
|
+
)
|
|
1059
|
+
for status, count in _mapping_value(snapshot, "agent_runs_by_status").items():
|
|
1060
|
+
lines.append(
|
|
1061
|
+
f'kagent_run_status_total{{status="{_prometheus_label(status)}"}} '
|
|
1062
|
+
f"{count}"
|
|
1063
|
+
)
|
|
1064
|
+
lines.extend(
|
|
1065
|
+
[
|
|
1066
|
+
"# HELP kagent_runtime_runs_total "
|
|
1067
|
+
"Total Codex-style runtime runs handled.",
|
|
1068
|
+
"# TYPE kagent_runtime_runs_total counter",
|
|
1069
|
+
f"kagent_runtime_runs_total "
|
|
1070
|
+
f"{snapshot.get('runtime_runs_total', '0')}",
|
|
1071
|
+
"# HELP kagent_runtime_run_status_total "
|
|
1072
|
+
"Codex-style runtime runs by final status.",
|
|
1073
|
+
"# TYPE kagent_runtime_run_status_total counter",
|
|
1074
|
+
]
|
|
1075
|
+
)
|
|
1076
|
+
for status, count in _mapping_value(snapshot, "runtime_runs_by_status").items():
|
|
1077
|
+
lines.append(
|
|
1078
|
+
"kagent_runtime_run_status_total"
|
|
1079
|
+
f'{{status="{_prometheus_label(status)}"}} {count}'
|
|
1080
|
+
)
|
|
1081
|
+
lines.extend(
|
|
1082
|
+
[
|
|
1083
|
+
"# HELP kagent_runtime_run_lifecycle_state_total "
|
|
1084
|
+
"Codex-style runtime runs by derived operator lifecycle state.",
|
|
1085
|
+
"# TYPE kagent_runtime_run_lifecycle_state_total counter",
|
|
1086
|
+
]
|
|
1087
|
+
)
|
|
1088
|
+
for lifecycle_state, count in _mapping_value(
|
|
1089
|
+
snapshot,
|
|
1090
|
+
"runtime_runs_by_lifecycle_state",
|
|
1091
|
+
).items():
|
|
1092
|
+
lines.append(
|
|
1093
|
+
"kagent_runtime_run_lifecycle_state_total"
|
|
1094
|
+
f'{{lifecycle_state="{_prometheus_label(lifecycle_state)}"}} {count}'
|
|
1095
|
+
)
|
|
1096
|
+
lines.extend(
|
|
1097
|
+
[
|
|
1098
|
+
"# HELP kagent_runtime_runs_by_auth_subject_total "
|
|
1099
|
+
"Codex-style runtime runs by authenticated internal subject.",
|
|
1100
|
+
"# TYPE kagent_runtime_runs_by_auth_subject_total counter",
|
|
1101
|
+
]
|
|
1102
|
+
)
|
|
1103
|
+
for auth_subject, count in _mapping_value(
|
|
1104
|
+
snapshot,
|
|
1105
|
+
"runtime_runs_by_auth_subject",
|
|
1106
|
+
).items():
|
|
1107
|
+
lines.append(
|
|
1108
|
+
"kagent_runtime_runs_by_auth_subject_total"
|
|
1109
|
+
f'{{auth_subject="{_prometheus_label(auth_subject)}"}} {count}'
|
|
1110
|
+
)
|
|
1111
|
+
lines.extend(
|
|
1112
|
+
[
|
|
1113
|
+
"# HELP kagent_runtime_run_status_by_auth_subject_total "
|
|
1114
|
+
"Codex-style runtime runs by authenticated internal subject and final status.",
|
|
1115
|
+
"# TYPE kagent_runtime_run_status_by_auth_subject_total counter",
|
|
1116
|
+
]
|
|
1117
|
+
)
|
|
1118
|
+
for subject_status, count in _mapping_value(
|
|
1119
|
+
snapshot,
|
|
1120
|
+
"runtime_runs_by_auth_subject_status",
|
|
1121
|
+
).items():
|
|
1122
|
+
auth_subject, status = _split_combined_metrics_key(subject_status)
|
|
1123
|
+
lines.append(
|
|
1124
|
+
"kagent_runtime_run_status_by_auth_subject_total"
|
|
1125
|
+
f'{{auth_subject="{_prometheus_label(auth_subject)}",'
|
|
1126
|
+
f'status="{_prometheus_label(status)}"}} {count}'
|
|
1127
|
+
)
|
|
1128
|
+
lines.extend(
|
|
1129
|
+
[
|
|
1130
|
+
"# HELP kagent_runtime_run_lifecycle_state_by_auth_subject_total "
|
|
1131
|
+
"Codex-style runtime runs by authenticated internal subject and lifecycle state.",
|
|
1132
|
+
"# TYPE kagent_runtime_run_lifecycle_state_by_auth_subject_total counter",
|
|
1133
|
+
]
|
|
1134
|
+
)
|
|
1135
|
+
for subject_lifecycle, count in _mapping_value(
|
|
1136
|
+
snapshot,
|
|
1137
|
+
"runtime_runs_by_auth_subject_lifecycle_state",
|
|
1138
|
+
).items():
|
|
1139
|
+
auth_subject, lifecycle_state = _split_combined_metrics_key(subject_lifecycle)
|
|
1140
|
+
lines.append(
|
|
1141
|
+
"kagent_runtime_run_lifecycle_state_by_auth_subject_total"
|
|
1142
|
+
f'{{auth_subject="{_prometheus_label(auth_subject)}",'
|
|
1143
|
+
f'lifecycle_state="{_prometheus_label(lifecycle_state)}"}} {count}'
|
|
1144
|
+
)
|
|
1145
|
+
lines.extend(
|
|
1146
|
+
[
|
|
1147
|
+
"# HELP kagent_runtime_resumes_by_auth_subject_total "
|
|
1148
|
+
"Codex-style runtime resumes by authenticated resume subject.",
|
|
1149
|
+
"# TYPE kagent_runtime_resumes_by_auth_subject_total counter",
|
|
1150
|
+
]
|
|
1151
|
+
)
|
|
1152
|
+
for auth_subject, count in _mapping_value(
|
|
1153
|
+
snapshot,
|
|
1154
|
+
"runtime_resumes_by_auth_subject",
|
|
1155
|
+
).items():
|
|
1156
|
+
lines.append(
|
|
1157
|
+
"kagent_runtime_resumes_by_auth_subject_total"
|
|
1158
|
+
f'{{auth_subject="{_prometheus_label(auth_subject)}"}} {count}'
|
|
1159
|
+
)
|
|
1160
|
+
lines.extend(
|
|
1161
|
+
[
|
|
1162
|
+
"# HELP kagent_runtime_approvals_by_auth_subject_total "
|
|
1163
|
+
"Codex-style runtime approvals by authenticated approver subject.",
|
|
1164
|
+
"# TYPE kagent_runtime_approvals_by_auth_subject_total counter",
|
|
1165
|
+
]
|
|
1166
|
+
)
|
|
1167
|
+
for auth_subject, count in _mapping_value(
|
|
1168
|
+
snapshot,
|
|
1169
|
+
"runtime_approvals_by_auth_subject",
|
|
1170
|
+
).items():
|
|
1171
|
+
lines.append(
|
|
1172
|
+
"kagent_runtime_approvals_by_auth_subject_total"
|
|
1173
|
+
f'{{auth_subject="{_prometheus_label(auth_subject)}"}} {count}'
|
|
1174
|
+
)
|
|
1175
|
+
lines.extend(
|
|
1176
|
+
[
|
|
1177
|
+
"# HELP kagent_runtime_failed_observations_total "
|
|
1178
|
+
"Failed observations produced by Codex-style runtime runs.",
|
|
1179
|
+
"# TYPE kagent_runtime_failed_observations_total counter",
|
|
1180
|
+
"kagent_runtime_failed_observations_total "
|
|
1181
|
+
f"{snapshot.get('runtime_failed_observations_total', '0')}",
|
|
1182
|
+
"# HELP kagent_runtime_observation_errors_total "
|
|
1183
|
+
"Runtime observation failures by stable error code.",
|
|
1184
|
+
"# TYPE kagent_runtime_observation_errors_total counter",
|
|
1185
|
+
]
|
|
1186
|
+
)
|
|
1187
|
+
for error_code, count in _mapping_value(
|
|
1188
|
+
snapshot,
|
|
1189
|
+
"runtime_observation_errors_by_code",
|
|
1190
|
+
).items():
|
|
1191
|
+
lines.append(
|
|
1192
|
+
"kagent_runtime_observation_errors_total"
|
|
1193
|
+
f'{{error_code="{_prometheus_label(error_code)}"}} {count}'
|
|
1194
|
+
)
|
|
1195
|
+
lines.extend(
|
|
1196
|
+
[
|
|
1197
|
+
"# HELP kagent_runtime_tool_executions_total "
|
|
1198
|
+
"Runtime tool observations by bounded tool and status labels.",
|
|
1199
|
+
"# TYPE kagent_runtime_tool_executions_total counter",
|
|
1200
|
+
]
|
|
1201
|
+
)
|
|
1202
|
+
for tool_status, count in _mapping_value(
|
|
1203
|
+
snapshot,
|
|
1204
|
+
"runtime_tool_executions_by_tool_status",
|
|
1205
|
+
).items():
|
|
1206
|
+
tool, status = _split_combined_metrics_key(tool_status)
|
|
1207
|
+
lines.append(
|
|
1208
|
+
"kagent_runtime_tool_executions_total"
|
|
1209
|
+
f'{{tool="{_prometheus_label(tool)}",'
|
|
1210
|
+
f'status="{_prometheus_label(status)}"}} {count}'
|
|
1211
|
+
)
|
|
1212
|
+
lines.extend(
|
|
1213
|
+
[
|
|
1214
|
+
"# HELP kagent_runtime_planner_attempts_total "
|
|
1215
|
+
"Runtime planner attempts by bounded status.",
|
|
1216
|
+
"# TYPE kagent_runtime_planner_attempts_total counter",
|
|
1217
|
+
]
|
|
1218
|
+
)
|
|
1219
|
+
for status, count in _mapping_value(
|
|
1220
|
+
snapshot,
|
|
1221
|
+
"runtime_planner_attempts_by_status",
|
|
1222
|
+
).items():
|
|
1223
|
+
lines.append(
|
|
1224
|
+
"kagent_runtime_planner_attempts_total"
|
|
1225
|
+
f'{{status="{_prometheus_label(status)}"}} {count}'
|
|
1226
|
+
)
|
|
1227
|
+
lines.extend(
|
|
1228
|
+
[
|
|
1229
|
+
"# HELP kagent_runtime_planner_failures_total "
|
|
1230
|
+
"Runtime planner failures before tool execution.",
|
|
1231
|
+
"# TYPE kagent_runtime_planner_failures_total counter",
|
|
1232
|
+
"kagent_runtime_planner_failures_total "
|
|
1233
|
+
f"{snapshot.get('runtime_planner_failures_total', '0')}",
|
|
1234
|
+
"# HELP kagent_runtime_planner_failures_by_error_code_total "
|
|
1235
|
+
"Runtime planner failures by stable error code.",
|
|
1236
|
+
"# TYPE kagent_runtime_planner_failures_by_error_code_total counter",
|
|
1237
|
+
]
|
|
1238
|
+
)
|
|
1239
|
+
for error_code, count in _mapping_value(
|
|
1240
|
+
snapshot,
|
|
1241
|
+
"runtime_planner_failures_by_error_code",
|
|
1242
|
+
).items():
|
|
1243
|
+
lines.append(
|
|
1244
|
+
"kagent_runtime_planner_failures_by_error_code_total"
|
|
1245
|
+
f'{{error_code="{_prometheus_label(error_code)}"}} {count}'
|
|
1246
|
+
)
|
|
1247
|
+
lines.extend(
|
|
1248
|
+
[
|
|
1249
|
+
"# HELP kagent_runtime_llm_provider_requests_total "
|
|
1250
|
+
"Runtime LLM provider requests with redacted diagnostics.",
|
|
1251
|
+
"# TYPE kagent_runtime_llm_provider_requests_total counter",
|
|
1252
|
+
"kagent_runtime_llm_provider_requests_total "
|
|
1253
|
+
f"{snapshot.get('runtime_llm_provider_requests_total', '0')}",
|
|
1254
|
+
"# HELP kagent_runtime_llm_provider_request_attempts_total "
|
|
1255
|
+
"Runtime LLM provider HTTP attempts including retries.",
|
|
1256
|
+
"# TYPE kagent_runtime_llm_provider_request_attempts_total counter",
|
|
1257
|
+
"kagent_runtime_llm_provider_request_attempts_total "
|
|
1258
|
+
f"{snapshot.get('runtime_llm_provider_request_attempts_total', '0')}",
|
|
1259
|
+
"# HELP kagent_runtime_llm_provider_request_retries_total "
|
|
1260
|
+
"Runtime LLM provider retry attempts.",
|
|
1261
|
+
"# TYPE kagent_runtime_llm_provider_request_retries_total counter",
|
|
1262
|
+
"kagent_runtime_llm_provider_request_retries_total "
|
|
1263
|
+
f"{snapshot.get('runtime_llm_provider_request_retries_total', '0')}",
|
|
1264
|
+
"# HELP kagent_runtime_llm_provider_requests_by_status_total "
|
|
1265
|
+
"Runtime LLM provider requests by bounded status.",
|
|
1266
|
+
"# TYPE kagent_runtime_llm_provider_requests_by_status_total counter",
|
|
1267
|
+
]
|
|
1268
|
+
)
|
|
1269
|
+
for status, count in _mapping_value(
|
|
1270
|
+
snapshot,
|
|
1271
|
+
"runtime_llm_provider_requests_by_status",
|
|
1272
|
+
).items():
|
|
1273
|
+
lines.append(
|
|
1274
|
+
"kagent_runtime_llm_provider_requests_by_status_total"
|
|
1275
|
+
f'{{status="{_prometheus_label(status)}"}} {count}'
|
|
1276
|
+
)
|
|
1277
|
+
lines.extend(
|
|
1278
|
+
[
|
|
1279
|
+
"# HELP kagent_runtime_llm_provider_request_errors_by_type_total "
|
|
1280
|
+
"Runtime LLM provider request failures by bounded error type.",
|
|
1281
|
+
"# TYPE kagent_runtime_llm_provider_request_errors_by_type_total counter",
|
|
1282
|
+
]
|
|
1283
|
+
)
|
|
1284
|
+
for error_type, count in _mapping_value(
|
|
1285
|
+
snapshot,
|
|
1286
|
+
"runtime_llm_provider_request_errors_by_type",
|
|
1287
|
+
).items():
|
|
1288
|
+
lines.append(
|
|
1289
|
+
"kagent_runtime_llm_provider_request_errors_by_type_total"
|
|
1290
|
+
f'{{error_type="{_prometheus_label(error_type)}"}} {count}'
|
|
1291
|
+
)
|
|
1292
|
+
lines.extend(
|
|
1293
|
+
[
|
|
1294
|
+
"# HELP kagent_runtime_llm_provider_request_http_status_total "
|
|
1295
|
+
"Runtime LLM provider HTTP failures by status code.",
|
|
1296
|
+
"# TYPE kagent_runtime_llm_provider_request_http_status_total counter",
|
|
1297
|
+
]
|
|
1298
|
+
)
|
|
1299
|
+
for http_status, count in _mapping_value(
|
|
1300
|
+
snapshot,
|
|
1301
|
+
"runtime_llm_provider_request_http_status",
|
|
1302
|
+
).items():
|
|
1303
|
+
lines.append(
|
|
1304
|
+
"kagent_runtime_llm_provider_request_http_status_total"
|
|
1305
|
+
f'{{http_status="{_prometheus_label(http_status)}"}} {count}'
|
|
1306
|
+
)
|
|
1307
|
+
lines.extend(
|
|
1308
|
+
[
|
|
1309
|
+
"# HELP kagent_runtime_llm_provider_request_retryable_reason_total "
|
|
1310
|
+
"Runtime LLM provider retryable failures by bounded reason.",
|
|
1311
|
+
"# TYPE kagent_runtime_llm_provider_request_retryable_reason_total counter",
|
|
1312
|
+
]
|
|
1313
|
+
)
|
|
1314
|
+
for reason, count in _mapping_value(
|
|
1315
|
+
snapshot,
|
|
1316
|
+
"runtime_llm_provider_request_retryable_reason",
|
|
1317
|
+
).items():
|
|
1318
|
+
lines.append(
|
|
1319
|
+
"kagent_runtime_llm_provider_request_retryable_reason_total"
|
|
1320
|
+
f'{{retryable_reason="{_prometheus_label(reason)}"}} {count}'
|
|
1321
|
+
)
|
|
1322
|
+
lines.extend(
|
|
1323
|
+
[
|
|
1324
|
+
"# HELP kagent_runtime_llm_provider_request_duration_seconds "
|
|
1325
|
+
"Runtime LLM provider request duration in seconds.",
|
|
1326
|
+
"# TYPE kagent_runtime_llm_provider_request_duration_seconds histogram",
|
|
1327
|
+
]
|
|
1328
|
+
)
|
|
1329
|
+
for bucket, count in _mapping_value(
|
|
1330
|
+
snapshot,
|
|
1331
|
+
"runtime_llm_provider_request_duration_seconds_bucket",
|
|
1332
|
+
).items():
|
|
1333
|
+
lines.append(
|
|
1334
|
+
"kagent_runtime_llm_provider_request_duration_seconds_bucket"
|
|
1335
|
+
f'{{le="{_prometheus_label(bucket)}"}} {count}'
|
|
1336
|
+
)
|
|
1337
|
+
lines.append(
|
|
1338
|
+
"kagent_runtime_llm_provider_request_duration_seconds_count "
|
|
1339
|
+
f"{snapshot.get('runtime_llm_provider_request_duration_seconds_count', '0')}"
|
|
1340
|
+
)
|
|
1341
|
+
lines.append(
|
|
1342
|
+
"kagent_runtime_llm_provider_request_duration_seconds_sum "
|
|
1343
|
+
f"{snapshot.get('runtime_llm_provider_request_duration_seconds_sum', '0.0000')}"
|
|
1344
|
+
)
|
|
1345
|
+
lines.extend(
|
|
1346
|
+
[
|
|
1347
|
+
"# HELP kagent_runtime_progress_event_sink_failures_total "
|
|
1348
|
+
"Runtime progress event sink delivery failures.",
|
|
1349
|
+
"# TYPE kagent_runtime_progress_event_sink_failures_total counter",
|
|
1350
|
+
"kagent_runtime_progress_event_sink_failures_total "
|
|
1351
|
+
f"{snapshot.get('runtime_progress_event_sink_failures_total', '0')}",
|
|
1352
|
+
"# HELP kagent_runtime_hook_failures_total "
|
|
1353
|
+
"Runtime hook callback failures.",
|
|
1354
|
+
"# TYPE kagent_runtime_hook_failures_total counter",
|
|
1355
|
+
"kagent_runtime_hook_failures_total "
|
|
1356
|
+
f"{snapshot.get('runtime_hook_failures_total', '0')}",
|
|
1357
|
+
"# HELP kagent_runtime_reconciliation_runs_total "
|
|
1358
|
+
"Runtime startup reconciliation passes by bounded status.",
|
|
1359
|
+
"# TYPE kagent_runtime_reconciliation_runs_total counter",
|
|
1360
|
+
]
|
|
1361
|
+
)
|
|
1362
|
+
for status, count in _mapping_value(
|
|
1363
|
+
snapshot,
|
|
1364
|
+
"runtime_reconciliation_runs_by_status",
|
|
1365
|
+
).items():
|
|
1366
|
+
lines.append(
|
|
1367
|
+
"kagent_runtime_reconciliation_runs_total"
|
|
1368
|
+
f'{{status="{_prometheus_label(status)}"}} {count}'
|
|
1369
|
+
)
|
|
1370
|
+
lines.extend(
|
|
1371
|
+
[
|
|
1372
|
+
"# HELP kagent_runtime_reconciliation_traces_scanned_total "
|
|
1373
|
+
"Persisted runtime traces scanned during startup reconciliation.",
|
|
1374
|
+
"# TYPE kagent_runtime_reconciliation_traces_scanned_total counter",
|
|
1375
|
+
"kagent_runtime_reconciliation_traces_scanned_total "
|
|
1376
|
+
f"{snapshot.get('runtime_reconciliation_traces_scanned_total', '0')}",
|
|
1377
|
+
"# HELP kagent_runtime_reconciliation_outcomes_total "
|
|
1378
|
+
"Startup reconciliation outcomes by bounded outcome.",
|
|
1379
|
+
"# TYPE kagent_runtime_reconciliation_outcomes_total counter",
|
|
1380
|
+
]
|
|
1381
|
+
)
|
|
1382
|
+
for outcome, count in _mapping_value(
|
|
1383
|
+
snapshot,
|
|
1384
|
+
"runtime_reconciliation_outcomes",
|
|
1385
|
+
).items():
|
|
1386
|
+
lines.append(
|
|
1387
|
+
"kagent_runtime_reconciliation_outcomes_total"
|
|
1388
|
+
f'{{outcome="{_prometheus_label(outcome)}"}} {count}'
|
|
1389
|
+
)
|
|
1390
|
+
lines.extend(
|
|
1391
|
+
[
|
|
1392
|
+
"# HELP kagent_runtime_reconciliation_errors_total "
|
|
1393
|
+
"Errors encountered while reconciling persisted runtime traces.",
|
|
1394
|
+
"# TYPE kagent_runtime_reconciliation_errors_total counter",
|
|
1395
|
+
"kagent_runtime_reconciliation_errors_total "
|
|
1396
|
+
f"{snapshot.get('runtime_reconciliation_errors_total', '0')}",
|
|
1397
|
+
"# HELP kagent_runtime_approval_required_total "
|
|
1398
|
+
"Approval-required observations produced by Codex-style runtime runs.",
|
|
1399
|
+
"# TYPE kagent_runtime_approval_required_total counter",
|
|
1400
|
+
"kagent_runtime_approval_required_total "
|
|
1401
|
+
f"{snapshot.get('runtime_approval_required_total', '0')}",
|
|
1402
|
+
"# HELP kagent_runtime_final_answer_guardrails_total "
|
|
1403
|
+
"Final answers corrected by runtime guardrails.",
|
|
1404
|
+
"# TYPE kagent_runtime_final_answer_guardrails_total counter",
|
|
1405
|
+
"kagent_runtime_final_answer_guardrails_total "
|
|
1406
|
+
f"{snapshot.get('runtime_final_answer_guardrails_total', '0')}",
|
|
1407
|
+
"# HELP kagent_runtime_final_answer_guardrails_by_reason_total "
|
|
1408
|
+
"Final answer guardrail corrections by stable reason.",
|
|
1409
|
+
"# TYPE kagent_runtime_final_answer_guardrails_by_reason_total counter",
|
|
1410
|
+
]
|
|
1411
|
+
)
|
|
1412
|
+
for reason, count in _mapping_value(
|
|
1413
|
+
snapshot,
|
|
1414
|
+
"runtime_final_answer_guardrails_by_reason",
|
|
1415
|
+
).items():
|
|
1416
|
+
lines.append(
|
|
1417
|
+
"kagent_runtime_final_answer_guardrails_by_reason_total"
|
|
1418
|
+
f'{{reason="{_prometheus_label(reason)}"}} {count}'
|
|
1419
|
+
)
|
|
1420
|
+
lines.extend(
|
|
1421
|
+
[
|
|
1422
|
+
"# HELP kagent_runtime_pending_approvals_current "
|
|
1423
|
+
"Current persisted pending approval queue size.",
|
|
1424
|
+
"# TYPE kagent_runtime_pending_approvals_current gauge",
|
|
1425
|
+
"kagent_runtime_pending_approvals_current "
|
|
1426
|
+
f"{snapshot.get('runtime_pending_approvals_current', '0')}",
|
|
1427
|
+
"# HELP kagent_runtime_stale_pending_approvals_current "
|
|
1428
|
+
"Current pending approvals older than the configured stale threshold.",
|
|
1429
|
+
"# TYPE kagent_runtime_stale_pending_approvals_current gauge",
|
|
1430
|
+
"kagent_runtime_stale_pending_approvals_current "
|
|
1431
|
+
f"{snapshot.get('runtime_stale_pending_approvals_current', '0')}",
|
|
1432
|
+
"# HELP kagent_runtime_max_pending_approval_age_seconds "
|
|
1433
|
+
"Maximum current pending approval age in seconds.",
|
|
1434
|
+
"# TYPE kagent_runtime_max_pending_approval_age_seconds gauge",
|
|
1435
|
+
"kagent_runtime_max_pending_approval_age_seconds "
|
|
1436
|
+
f"{snapshot.get('runtime_max_pending_approval_age_seconds', '0')}",
|
|
1437
|
+
"# HELP kagent_runtime_pending_approval_stale_seconds "
|
|
1438
|
+
"Configured stale pending approval age threshold in seconds.",
|
|
1439
|
+
"# TYPE kagent_runtime_pending_approval_stale_seconds gauge",
|
|
1440
|
+
"kagent_runtime_pending_approval_stale_seconds "
|
|
1441
|
+
f"{snapshot.get('runtime_pending_approval_stale_seconds', '3600')}",
|
|
1442
|
+
"# HELP kagent_runtime_failed_budget_exhaustions_total "
|
|
1443
|
+
"Failed Codex-style runtime runs that exhausted their iteration budget.",
|
|
1444
|
+
"# TYPE kagent_runtime_failed_budget_exhaustions_total counter",
|
|
1445
|
+
"kagent_runtime_failed_budget_exhaustions_total "
|
|
1446
|
+
f"{snapshot.get('runtime_failed_budget_exhaustions_total', '0')}",
|
|
1447
|
+
"# HELP kagent_runtime_run_duration_seconds "
|
|
1448
|
+
"Codex-style runtime run duration in seconds.",
|
|
1449
|
+
"# TYPE kagent_runtime_run_duration_seconds histogram",
|
|
1450
|
+
]
|
|
1451
|
+
)
|
|
1452
|
+
for bucket, count in _mapping_value(
|
|
1453
|
+
snapshot,
|
|
1454
|
+
"runtime_run_duration_seconds_bucket",
|
|
1455
|
+
).items():
|
|
1456
|
+
lines.append(
|
|
1457
|
+
"kagent_runtime_run_duration_seconds_bucket"
|
|
1458
|
+
f'{{le="{_prometheus_label(bucket)}"}} {count}'
|
|
1459
|
+
)
|
|
1460
|
+
lines.extend(
|
|
1461
|
+
[
|
|
1462
|
+
"kagent_runtime_run_duration_seconds_count "
|
|
1463
|
+
f"{snapshot.get('runtime_run_duration_seconds_count', '0')}",
|
|
1464
|
+
"kagent_runtime_run_duration_seconds_sum "
|
|
1465
|
+
f"{snapshot.get('runtime_run_duration_seconds_sum', '0.0000')}",
|
|
1466
|
+
]
|
|
1467
|
+
)
|
|
1468
|
+
lines.extend(
|
|
1469
|
+
[
|
|
1470
|
+
"# HELP kagent_agent_run_duration_seconds "
|
|
1471
|
+
"Agent run duration in seconds.",
|
|
1472
|
+
"# TYPE kagent_agent_run_duration_seconds histogram",
|
|
1473
|
+
]
|
|
1474
|
+
)
|
|
1475
|
+
for bucket, count in _mapping_value(snapshot, "agent_run_duration_seconds_bucket").items():
|
|
1476
|
+
lines.append(
|
|
1477
|
+
"kagent_agent_run_duration_seconds_bucket"
|
|
1478
|
+
f'{{le="{_prometheus_label(bucket)}"}} {count}'
|
|
1479
|
+
)
|
|
1480
|
+
lines.append(
|
|
1481
|
+
"kagent_agent_run_duration_seconds_count "
|
|
1482
|
+
f"{snapshot.get('agent_run_duration_seconds_count', '0')}"
|
|
1483
|
+
)
|
|
1484
|
+
lines.append(
|
|
1485
|
+
"kagent_agent_run_duration_seconds_sum "
|
|
1486
|
+
f"{snapshot.get('agent_run_duration_seconds_sum', '0.0000')}"
|
|
1487
|
+
)
|
|
1488
|
+
if "service_version" in snapshot:
|
|
1489
|
+
build_labels = {
|
|
1490
|
+
"auth_required": snapshot.get("auth_required", "false"),
|
|
1491
|
+
"auth_subject_count": snapshot.get("auth_subject_count", "0"),
|
|
1492
|
+
"allow_full_trace_response": snapshot.get("allow_full_trace_response", "false"),
|
|
1493
|
+
"bind_host": snapshot.get("bind_host", ""),
|
|
1494
|
+
"bind_port": snapshot.get("bind_port", "0"),
|
|
1495
|
+
"idempotency_cache_backend": snapshot.get(
|
|
1496
|
+
"idempotency_cache_backend",
|
|
1497
|
+
"memory",
|
|
1498
|
+
),
|
|
1499
|
+
"idempotency_cache_path_configured": snapshot.get(
|
|
1500
|
+
"idempotency_cache_path_configured",
|
|
1501
|
+
"false",
|
|
1502
|
+
),
|
|
1503
|
+
"idempotency_cache_size": snapshot.get("idempotency_cache_size", "0"),
|
|
1504
|
+
"runtime_allowed_tools": snapshot.get("runtime_allowed_tools", "default"),
|
|
1505
|
+
"runtime_allowed_tools_by_subject_count": snapshot.get(
|
|
1506
|
+
"runtime_allowed_tools_by_subject_count",
|
|
1507
|
+
"0",
|
|
1508
|
+
),
|
|
1509
|
+
"runtime_max_iterations": snapshot.get("runtime_max_iterations", "0"),
|
|
1510
|
+
"max_concurrent_runs": snapshot.get("max_concurrent_runs", "0"),
|
|
1511
|
+
"max_goal_chars": snapshot.get("max_goal_chars", "0"),
|
|
1512
|
+
"max_request_bytes": snapshot.get("max_request_bytes", "0"),
|
|
1513
|
+
"protect_diagnostics": snapshot.get("protect_diagnostics", "false"),
|
|
1514
|
+
"rate_limit_per_minute": snapshot.get("rate_limit_per_minute", "0"),
|
|
1515
|
+
"request_timeout_seconds": snapshot.get("request_timeout_seconds", "0"),
|
|
1516
|
+
"run_timeout_seconds": snapshot.get("run_timeout_seconds", "0"),
|
|
1517
|
+
"trace_persistence": snapshot.get("trace_persistence", "disabled"),
|
|
1518
|
+
"trust_forwarded_for": snapshot.get("trust_forwarded_for", "false"),
|
|
1519
|
+
"version": snapshot["service_version"],
|
|
1520
|
+
"trace_directory_permissions": snapshot.get(
|
|
1521
|
+
"trace_directory_permissions",
|
|
1522
|
+
"",
|
|
1523
|
+
),
|
|
1524
|
+
"trace_file_permissions": snapshot.get("trace_file_permissions", ""),
|
|
1525
|
+
"trace_probe_file_permissions": snapshot.get(
|
|
1526
|
+
"trace_probe_file_permissions",
|
|
1527
|
+
"",
|
|
1528
|
+
),
|
|
1529
|
+
"embedding_provider": snapshot.get("embedding_provider", "unconfigured"),
|
|
1530
|
+
"embedding_base_url": snapshot.get("embedding_base_url", ""),
|
|
1531
|
+
"embedding_base_url_configured": snapshot.get(
|
|
1532
|
+
"embedding_base_url_configured",
|
|
1533
|
+
"false",
|
|
1534
|
+
),
|
|
1535
|
+
"embedding_model": snapshot.get("embedding_model", ""),
|
|
1536
|
+
"embedding_api_key_configured": snapshot.get(
|
|
1537
|
+
"embedding_api_key_configured",
|
|
1538
|
+
"false",
|
|
1539
|
+
),
|
|
1540
|
+
"embedding_timeout_seconds": snapshot.get("embedding_timeout_seconds", "0"),
|
|
1541
|
+
"embedding_max_retries": snapshot.get("embedding_max_retries", "0"),
|
|
1542
|
+
"embedding_retry_backoff_seconds": snapshot.get(
|
|
1543
|
+
"embedding_retry_backoff_seconds",
|
|
1544
|
+
"0",
|
|
1545
|
+
),
|
|
1546
|
+
"llm_provider": snapshot.get("llm_provider", "unconfigured"),
|
|
1547
|
+
"llm_provider_display_name": snapshot.get(
|
|
1548
|
+
"llm_provider_display_name",
|
|
1549
|
+
"Unconfigured",
|
|
1550
|
+
),
|
|
1551
|
+
"llm_base_url": snapshot.get("llm_base_url", ""),
|
|
1552
|
+
"llm_base_url_configured": snapshot.get(
|
|
1553
|
+
"llm_base_url_configured",
|
|
1554
|
+
"false",
|
|
1555
|
+
),
|
|
1556
|
+
"llm_model": snapshot.get("llm_model", ""),
|
|
1557
|
+
"llm_api_key_configured": snapshot.get("llm_api_key_configured", "false"),
|
|
1558
|
+
"llm_timeout_seconds": snapshot.get("llm_timeout_seconds", "0"),
|
|
1559
|
+
"llm_max_retries": snapshot.get("llm_max_retries", "0"),
|
|
1560
|
+
"llm_retry_backoff_seconds": snapshot.get(
|
|
1561
|
+
"llm_retry_backoff_seconds",
|
|
1562
|
+
"0",
|
|
1563
|
+
),
|
|
1564
|
+
"security_response_headers": snapshot.get("security_response_headers", "unknown"),
|
|
1565
|
+
"cache_control_header": snapshot.get("cache_control_header", ""),
|
|
1566
|
+
"content_security_policy_header": snapshot.get(
|
|
1567
|
+
"content_security_policy_header",
|
|
1568
|
+
"",
|
|
1569
|
+
),
|
|
1570
|
+
"referrer_policy_header": snapshot.get("referrer_policy_header", ""),
|
|
1571
|
+
"x_frame_options_header": snapshot.get("x_frame_options_header", ""),
|
|
1572
|
+
"x_content_type_options_header": snapshot.get(
|
|
1573
|
+
"x_content_type_options_header",
|
|
1574
|
+
"",
|
|
1575
|
+
),
|
|
1576
|
+
}
|
|
1577
|
+
build_label_text = ",".join(
|
|
1578
|
+
f'{name}="{_prometheus_label(value)}"'
|
|
1579
|
+
for name, value in build_labels.items()
|
|
1580
|
+
)
|
|
1581
|
+
lines.extend(
|
|
1582
|
+
[
|
|
1583
|
+
"# HELP kagent_build_info Service build and runtime controls.",
|
|
1584
|
+
"# TYPE kagent_build_info gauge",
|
|
1585
|
+
f"kagent_build_info{{{build_label_text}}} 1",
|
|
1586
|
+
]
|
|
1587
|
+
)
|
|
1588
|
+
scalar_metrics = [
|
|
1589
|
+
(
|
|
1590
|
+
"average_duration_seconds",
|
|
1591
|
+
"gauge",
|
|
1592
|
+
"Average HTTP request duration in seconds.",
|
|
1593
|
+
),
|
|
1594
|
+
("max_duration_seconds", "gauge", "Maximum observed HTTP request duration in seconds."),
|
|
1595
|
+
(
|
|
1596
|
+
"average_agent_run_duration_seconds",
|
|
1597
|
+
"gauge",
|
|
1598
|
+
"Average agent run duration in seconds.",
|
|
1599
|
+
),
|
|
1600
|
+
(
|
|
1601
|
+
"max_agent_run_duration_seconds",
|
|
1602
|
+
"gauge",
|
|
1603
|
+
"Maximum observed agent run duration in seconds.",
|
|
1604
|
+
),
|
|
1605
|
+
("uptime_seconds", "gauge", "Service process uptime in seconds."),
|
|
1606
|
+
("active_concurrent_runs", "gauge", "Currently active agent runs."),
|
|
1607
|
+
("max_concurrent_runs", "gauge", "Configured maximum concurrent agent runs."),
|
|
1608
|
+
("max_goal_chars", "gauge", "Configured maximum accepted goal length in characters."),
|
|
1609
|
+
(
|
|
1610
|
+
"runtime_max_iterations",
|
|
1611
|
+
"gauge",
|
|
1612
|
+
"Configured maximum Codex-style runtime planner iterations per request.",
|
|
1613
|
+
),
|
|
1614
|
+
("max_request_bytes", "gauge", "Configured maximum accepted request body size in bytes."),
|
|
1615
|
+
("active_rate_limit_windows", "gauge", "Currently tracked rate limit windows."),
|
|
1616
|
+
("rate_limit_per_minute", "gauge", "Configured per-client run rate limit per minute."),
|
|
1617
|
+
("idempotency_cache_entries", "gauge", "Current idempotency cache entry count."),
|
|
1618
|
+
("idempotency_cache_size", "gauge", "Configured idempotency cache entry limit."),
|
|
1619
|
+
("idempotency_cache_hits", "counter", "Total idempotency cache hits."),
|
|
1620
|
+
("idempotency_cache_misses", "counter", "Total idempotency cache misses."),
|
|
1621
|
+
("idempotency_cache_conflicts", "counter", "Total idempotency key conflicts."),
|
|
1622
|
+
("idempotency_cache_stores", "counter", "Total idempotency cache stores."),
|
|
1623
|
+
("idempotency_cache_evictions", "counter", "Total idempotency cache evictions."),
|
|
1624
|
+
("idempotency_cache_claims", "counter", "Total idempotency execution claims."),
|
|
1625
|
+
("idempotency_cache_waits", "counter", "Total idempotency claim waits."),
|
|
1626
|
+
(
|
|
1627
|
+
"idempotency_cache_wait_timeouts",
|
|
1628
|
+
"counter",
|
|
1629
|
+
"Total idempotency claim wait timeouts.",
|
|
1630
|
+
),
|
|
1631
|
+
("idempotency_cache_takeovers", "counter", "Total expired claim takeovers."),
|
|
1632
|
+
]
|
|
1633
|
+
for name, metric_type, help_text in scalar_metrics:
|
|
1634
|
+
if name in snapshot:
|
|
1635
|
+
lines.append(f"# HELP kagent_{name} {help_text}")
|
|
1636
|
+
lines.append(f"# TYPE kagent_{name} {metric_type}")
|
|
1637
|
+
lines.append(f"kagent_{name} {snapshot[name]}")
|
|
1638
|
+
return "\n".join(lines) + "\n"
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def _env_int(env: Mapping[str, str], name: str, default: int) -> int:
|
|
1642
|
+
value = env.get(name)
|
|
1643
|
+
if value in {None, ""}:
|
|
1644
|
+
return default
|
|
1645
|
+
try:
|
|
1646
|
+
return int(value)
|
|
1647
|
+
except ValueError as exc:
|
|
1648
|
+
raise ValueError(f"{name} must be an integer") from exc
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
def _env_float(env: Mapping[str, str], name: str, default: float) -> float:
|
|
1652
|
+
value = env.get(name)
|
|
1653
|
+
if value in {None, ""}:
|
|
1654
|
+
return default
|
|
1655
|
+
try:
|
|
1656
|
+
return float(value)
|
|
1657
|
+
except ValueError as exc:
|
|
1658
|
+
raise ValueError(f"{name} must be a number") from exc
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def _env_bool(env: Mapping[str, str], name: str, default: bool) -> bool:
|
|
1662
|
+
value = env.get(name)
|
|
1663
|
+
if value in {None, ""}:
|
|
1664
|
+
return default
|
|
1665
|
+
normalized = value.strip().lower()
|
|
1666
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
1667
|
+
return True
|
|
1668
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
1669
|
+
return False
|
|
1670
|
+
raise ValueError(f"{name} must be a boolean")
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
def _env_auth_tokens(env: Mapping[str, str], name: str) -> Dict[str, str]:
|
|
1674
|
+
value = env.get(name)
|
|
1675
|
+
if value in {None, ""}:
|
|
1676
|
+
return {}
|
|
1677
|
+
try:
|
|
1678
|
+
payload = json.loads(str(value))
|
|
1679
|
+
except json.JSONDecodeError as exc:
|
|
1680
|
+
raise ValueError(f"{name} must be a JSON object") from exc
|
|
1681
|
+
if not isinstance(payload, dict):
|
|
1682
|
+
raise ValueError(f"{name} must be a JSON object")
|
|
1683
|
+
tokens: Dict[str, str] = {}
|
|
1684
|
+
for subject, token in payload.items():
|
|
1685
|
+
if not isinstance(subject, str) or not isinstance(token, str):
|
|
1686
|
+
raise ValueError(f"{name} subjects and tokens must be strings")
|
|
1687
|
+
tokens[subject] = token
|
|
1688
|
+
return tokens
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
def _env_csv_tuple(env: Mapping[str, str], name: str) -> Tuple[str, ...]:
|
|
1692
|
+
value = env.get(name)
|
|
1693
|
+
if value in {None, ""}:
|
|
1694
|
+
return ()
|
|
1695
|
+
items = {item.strip() for item in str(value).split(",") if item.strip()}
|
|
1696
|
+
return tuple(sorted(items))
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
def _env_subject_csv_tuple_map(
|
|
1700
|
+
env: Mapping[str, str],
|
|
1701
|
+
name: str,
|
|
1702
|
+
) -> Dict[str, Tuple[str, ...]]:
|
|
1703
|
+
value = env.get(name)
|
|
1704
|
+
if value in {None, ""}:
|
|
1705
|
+
return {}
|
|
1706
|
+
try:
|
|
1707
|
+
payload = json.loads(str(value))
|
|
1708
|
+
except json.JSONDecodeError as exc:
|
|
1709
|
+
raise ValueError(f"{name} must be a JSON object") from exc
|
|
1710
|
+
if not isinstance(payload, dict):
|
|
1711
|
+
raise ValueError(f"{name} must be a JSON object")
|
|
1712
|
+
result: Dict[str, Tuple[str, ...]] = {}
|
|
1713
|
+
for subject in sorted(payload):
|
|
1714
|
+
tools = payload[subject]
|
|
1715
|
+
if not isinstance(subject, str):
|
|
1716
|
+
raise ValueError(f"{name} subjects must be strings")
|
|
1717
|
+
if isinstance(tools, str):
|
|
1718
|
+
result[subject] = tuple(
|
|
1719
|
+
sorted({item.strip() for item in tools.split(",") if item.strip()})
|
|
1720
|
+
)
|
|
1721
|
+
elif isinstance(tools, list) and all(isinstance(item, str) for item in tools):
|
|
1722
|
+
result[subject] = tuple(
|
|
1723
|
+
sorted({item.strip() for item in tools if item.strip()})
|
|
1724
|
+
)
|
|
1725
|
+
else:
|
|
1726
|
+
raise ValueError(f"{name} values must be strings or arrays of strings")
|
|
1727
|
+
return result
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _validate_auth_tokens(auth_tokens: Mapping[str, str]) -> None:
|
|
1731
|
+
for subject, token in auth_tokens.items():
|
|
1732
|
+
if not subject or not _safe_header_value(subject):
|
|
1733
|
+
raise ValueError("auth token subjects must be printable ASCII")
|
|
1734
|
+
if not token or not _safe_header_value(f"Bearer {token}"):
|
|
1735
|
+
raise ValueError("auth tokens must be printable ASCII")
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def _validate_runtime_allowed_tools(runtime_allowed_tools: Tuple[str, ...]) -> None:
|
|
1739
|
+
if not runtime_allowed_tools:
|
|
1740
|
+
return
|
|
1741
|
+
from kagent.runtime.tools import default_runtime_tools
|
|
1742
|
+
|
|
1743
|
+
known_tools = set(default_runtime_tools())
|
|
1744
|
+
unknown_tools = sorted(set(runtime_allowed_tools) - known_tools)
|
|
1745
|
+
if unknown_tools:
|
|
1746
|
+
raise ValueError(
|
|
1747
|
+
"runtime_allowed_tools contains unknown tools: "
|
|
1748
|
+
+ ", ".join(unknown_tools)
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
def _validate_runtime_allowed_tools_by_subject(
|
|
1753
|
+
runtime_allowed_tools_by_subject: Mapping[str, Tuple[str, ...]],
|
|
1754
|
+
) -> None:
|
|
1755
|
+
if not runtime_allowed_tools_by_subject:
|
|
1756
|
+
return
|
|
1757
|
+
from kagent.runtime.tools import default_runtime_tools
|
|
1758
|
+
|
|
1759
|
+
known_tools = set(default_runtime_tools())
|
|
1760
|
+
for subject, tools in runtime_allowed_tools_by_subject.items():
|
|
1761
|
+
if not subject or not _safe_header_value(subject):
|
|
1762
|
+
raise ValueError(
|
|
1763
|
+
"runtime_allowed_tools_by_subject subjects must be printable ASCII"
|
|
1764
|
+
)
|
|
1765
|
+
unknown_tools = sorted(set(tools) - known_tools)
|
|
1766
|
+
if unknown_tools:
|
|
1767
|
+
raise ValueError(
|
|
1768
|
+
"runtime_allowed_tools_by_subject contains unknown tools for "
|
|
1769
|
+
+ subject
|
|
1770
|
+
+ ": "
|
|
1771
|
+
+ ", ".join(unknown_tools)
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def _safe_header_value(value: str) -> bool:
|
|
1776
|
+
return bool(value) and all(32 <= ord(character) <= 126 for character in value)
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
def _string_counts(counts: Mapping[str, int]) -> Dict[str, str]:
|
|
1780
|
+
return {key: str(counts[key]) for key in sorted(counts)}
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
def _combined_metrics_key(left: str, right: str) -> str:
|
|
1784
|
+
return f"{left}:{right}"
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
def _split_combined_metrics_key(value: str) -> Tuple[str, str]:
|
|
1788
|
+
left, separator, right = value.partition(":")
|
|
1789
|
+
if not separator:
|
|
1790
|
+
return value, ""
|
|
1791
|
+
return left, right
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def _runtime_tool_metrics_label(value: str) -> str:
|
|
1795
|
+
normalized = str(value).strip()
|
|
1796
|
+
if not normalized:
|
|
1797
|
+
return "unknown"
|
|
1798
|
+
from kagent.runtime.tools import default_runtime_tools
|
|
1799
|
+
|
|
1800
|
+
if normalized in default_runtime_tools():
|
|
1801
|
+
return normalized
|
|
1802
|
+
return "unknown"
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
def _runtime_observation_status_metrics_label(value: str) -> str:
|
|
1806
|
+
normalized = str(value).strip()
|
|
1807
|
+
if normalized in {"failed", "ok", "requires_approval"}:
|
|
1808
|
+
return normalized
|
|
1809
|
+
return "other"
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
def _runtime_planner_attempt_status_metrics_label(value: str) -> str:
|
|
1813
|
+
normalized = str(value).strip()
|
|
1814
|
+
if normalized in {"failed", "ok"}:
|
|
1815
|
+
return normalized
|
|
1816
|
+
return "other"
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
def _runtime_llm_provider_status_metrics_label(value: str) -> str:
|
|
1820
|
+
normalized = str(value).strip()
|
|
1821
|
+
if normalized in {"failed", "ok"}:
|
|
1822
|
+
return normalized
|
|
1823
|
+
return "other"
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def _runtime_llm_provider_error_type_metrics_label(value: str) -> str:
|
|
1827
|
+
normalized = str(value).strip()
|
|
1828
|
+
if not normalized:
|
|
1829
|
+
return ""
|
|
1830
|
+
if normalized in {
|
|
1831
|
+
"exhausted",
|
|
1832
|
+
"http_error",
|
|
1833
|
+
"provider_error",
|
|
1834
|
+
"response_error",
|
|
1835
|
+
"timeout",
|
|
1836
|
+
"url_error",
|
|
1837
|
+
}:
|
|
1838
|
+
return normalized
|
|
1839
|
+
return "other"
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
def _runtime_llm_provider_http_status_metrics_label(value: str) -> str:
|
|
1843
|
+
normalized = str(value).strip()
|
|
1844
|
+
if len(normalized) == 3 and normalized.isdigit():
|
|
1845
|
+
return normalized
|
|
1846
|
+
return ""
|
|
1847
|
+
|
|
1848
|
+
|
|
1849
|
+
def _runtime_llm_provider_retryable_reason_metrics_label(value: str) -> str:
|
|
1850
|
+
normalized = str(value).strip()
|
|
1851
|
+
if normalized in {"model_unloaded"}:
|
|
1852
|
+
return normalized
|
|
1853
|
+
if normalized:
|
|
1854
|
+
return "other"
|
|
1855
|
+
return ""
|
|
1856
|
+
|
|
1857
|
+
|
|
1858
|
+
def _runtime_lifecycle_state(status: str) -> str:
|
|
1859
|
+
if status == "cancelled":
|
|
1860
|
+
return "cancelled"
|
|
1861
|
+
if status == "done":
|
|
1862
|
+
return "succeeded"
|
|
1863
|
+
if status == "failed":
|
|
1864
|
+
return "failed"
|
|
1865
|
+
if status == "requires_approval":
|
|
1866
|
+
return "waiting_approval"
|
|
1867
|
+
if status in {"running", "resuming"}:
|
|
1868
|
+
return "running"
|
|
1869
|
+
if status == "resumed":
|
|
1870
|
+
return "succeeded"
|
|
1871
|
+
return "unknown"
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
def _duration_bucket_labels() -> Tuple[Tuple[float, str], ...]:
|
|
1875
|
+
return tuple((bucket, f"{bucket:g}") for bucket in _DURATION_BUCKETS)
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def _empty_duration_buckets() -> Dict[str, int]:
|
|
1879
|
+
buckets = {label: 0 for _bucket, label in _duration_bucket_labels()}
|
|
1880
|
+
buckets["+Inf"] = 0
|
|
1881
|
+
return buckets
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
def _non_negative_int_value(value: Any) -> int:
|
|
1885
|
+
try:
|
|
1886
|
+
return max(0, int(value))
|
|
1887
|
+
except (TypeError, ValueError):
|
|
1888
|
+
return 0
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _non_negative_float_value(value: Any) -> float:
|
|
1892
|
+
try:
|
|
1893
|
+
return max(0.0, float(value))
|
|
1894
|
+
except (TypeError, ValueError):
|
|
1895
|
+
return 0.0
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def _method_metrics_label(method: str) -> str:
|
|
1899
|
+
method_key = method.upper()
|
|
1900
|
+
if method_key in _KNOWN_HTTP_METHODS:
|
|
1901
|
+
return method_key
|
|
1902
|
+
return _UNKNOWN_METRICS_LABEL
|
|
1903
|
+
|
|
1904
|
+
|
|
1905
|
+
def _mapping_value(snapshot: Mapping[str, Any], name: str) -> Mapping[str, Any]:
|
|
1906
|
+
value = snapshot.get(name)
|
|
1907
|
+
return value if isinstance(value, dict) else {}
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
def _prometheus_label(value: Any) -> str:
|
|
1911
|
+
return str(value).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"')
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
def _noop_release() -> None:
|
|
1915
|
+
return None
|