@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,255 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from kagent import __version__
|
|
7
|
+
from kagent.core.tools import registered_tool_names
|
|
8
|
+
from kagent.providers.llm import LLMProviderConfig
|
|
9
|
+
from kagent.service.runtime import ServiceConfig
|
|
10
|
+
from kagent.service.runtime_policy import (
|
|
11
|
+
execute_runtime_policy_request,
|
|
12
|
+
)
|
|
13
|
+
from kagent.service.safety import safe_header_value
|
|
14
|
+
from kagent.service.status import (
|
|
15
|
+
readiness_payload,
|
|
16
|
+
service_config_snapshot,
|
|
17
|
+
)
|
|
18
|
+
from kagent.utils.json_output import format_and_write_json
|
|
19
|
+
|
|
20
|
+
MIN_PRODUCTION_AUTH_TOKEN_CHARS = 16
|
|
21
|
+
MIN_RUNTIME_PROVIDER_ITERATIONS = 2
|
|
22
|
+
PLACEHOLDER_AUTH_TOKENS = {
|
|
23
|
+
"change-me",
|
|
24
|
+
"changeme",
|
|
25
|
+
"placeholder",
|
|
26
|
+
"replace-me",
|
|
27
|
+
"replace-with-a-long-random-token",
|
|
28
|
+
"secret",
|
|
29
|
+
"token",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def doctor_payload(
|
|
34
|
+
config: Optional[ServiceConfig] = None,
|
|
35
|
+
*,
|
|
36
|
+
require_auth: bool = False,
|
|
37
|
+
require_production_controls: bool = False,
|
|
38
|
+
require_runtime_provider: bool = False,
|
|
39
|
+
llm_config: Optional[LLMProviderConfig] = None,
|
|
40
|
+
) -> Dict[str, Any]:
|
|
41
|
+
active_config = config or ServiceConfig.from_env()
|
|
42
|
+
active_llm_config = llm_config or LLMProviderConfig.from_sources()
|
|
43
|
+
readiness = readiness_payload(active_config)
|
|
44
|
+
policy = _policy_payload(
|
|
45
|
+
active_config,
|
|
46
|
+
active_llm_config,
|
|
47
|
+
require_auth=require_auth,
|
|
48
|
+
require_production_controls=require_production_controls,
|
|
49
|
+
require_runtime_provider=require_runtime_provider,
|
|
50
|
+
)
|
|
51
|
+
status = (
|
|
52
|
+
"ready"
|
|
53
|
+
if readiness["status"] == "ready" and policy["status"] != "failed"
|
|
54
|
+
else "not_ready"
|
|
55
|
+
)
|
|
56
|
+
config_snapshot = service_config_snapshot(active_config)
|
|
57
|
+
config_snapshot.update(active_llm_config.redacted_snapshot())
|
|
58
|
+
runtime_policy = _runtime_policy_summary(active_config)
|
|
59
|
+
return {
|
|
60
|
+
"status": status,
|
|
61
|
+
"version": __version__,
|
|
62
|
+
"readiness": readiness,
|
|
63
|
+
"policy": policy,
|
|
64
|
+
"runtime_policy": runtime_policy,
|
|
65
|
+
"config": config_snapshot,
|
|
66
|
+
"tool_count": str(len(registered_tool_names())),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
71
|
+
parser = argparse.ArgumentParser(
|
|
72
|
+
description="Run deployment self-checks for the kagent."
|
|
73
|
+
)
|
|
74
|
+
try:
|
|
75
|
+
defaults = ServiceConfig.from_env()
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
parser.error(str(exc))
|
|
78
|
+
parser.add_argument("--trace-dir", default=defaults.trace_dir)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--runtime-workspace-dir",
|
|
81
|
+
default=defaults.runtime_workspace_dir,
|
|
82
|
+
help="Optional runtime virtual workspace root to validate in readiness.",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--require-auth",
|
|
86
|
+
action="store_true",
|
|
87
|
+
help="Fail the self-check when POST /run bearer auth is disabled.",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--production",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help=(
|
|
93
|
+
"Fail the self-check unless production controls are configured: "
|
|
94
|
+
"strong auth, diagnostic protection, trace persistence, rate "
|
|
95
|
+
"limiting, and bounded concurrency."
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--require-runtime-provider",
|
|
100
|
+
action="store_true",
|
|
101
|
+
help=(
|
|
102
|
+
"Fail the self-check unless the OpenAI-compatible runtime provider "
|
|
103
|
+
"environment is configured and runtime replanning has at least two "
|
|
104
|
+
"iterations available."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument("--output", default="", help="Optional JSON artifact path.")
|
|
108
|
+
args = parser.parse_args(argv)
|
|
109
|
+
|
|
110
|
+
config = ServiceConfig(
|
|
111
|
+
host=defaults.host,
|
|
112
|
+
port=defaults.port,
|
|
113
|
+
max_request_bytes=defaults.max_request_bytes,
|
|
114
|
+
max_goal_chars=defaults.max_goal_chars,
|
|
115
|
+
auth_token=defaults.auth_token,
|
|
116
|
+
auth_tokens=defaults.auth_tokens,
|
|
117
|
+
rate_limit_per_minute=defaults.rate_limit_per_minute,
|
|
118
|
+
max_concurrent_runs=defaults.max_concurrent_runs,
|
|
119
|
+
idempotency_cache_size=defaults.idempotency_cache_size,
|
|
120
|
+
idempotency_cache_path=defaults.idempotency_cache_path,
|
|
121
|
+
runtime_allowed_tools=defaults.runtime_allowed_tools,
|
|
122
|
+
runtime_allowed_tools_by_subject=defaults.runtime_allowed_tools_by_subject,
|
|
123
|
+
runtime_max_iterations=defaults.runtime_max_iterations,
|
|
124
|
+
allow_full_trace_response=defaults.allow_full_trace_response,
|
|
125
|
+
protect_diagnostics=defaults.protect_diagnostics,
|
|
126
|
+
trust_forwarded_for=defaults.trust_forwarded_for,
|
|
127
|
+
trace_dir=args.trace_dir,
|
|
128
|
+
runtime_workspace_dir=args.runtime_workspace_dir,
|
|
129
|
+
redis_url=defaults.redis_url,
|
|
130
|
+
milvus_url=defaults.milvus_url,
|
|
131
|
+
kafka_audit_url=defaults.kafka_audit_url,
|
|
132
|
+
kafka_audit_topic=defaults.kafka_audit_topic,
|
|
133
|
+
external_backend_timeout_seconds=defaults.external_backend_timeout_seconds,
|
|
134
|
+
run_timeout_seconds=defaults.run_timeout_seconds,
|
|
135
|
+
request_timeout_seconds=defaults.request_timeout_seconds,
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
payload = doctor_payload(
|
|
139
|
+
config,
|
|
140
|
+
require_auth=args.require_auth,
|
|
141
|
+
require_production_controls=args.production,
|
|
142
|
+
require_runtime_provider=args.require_runtime_provider,
|
|
143
|
+
)
|
|
144
|
+
except ValueError as exc:
|
|
145
|
+
parser.error(str(exc))
|
|
146
|
+
try:
|
|
147
|
+
json_payload = format_and_write_json(payload, args.output)
|
|
148
|
+
except OSError as exc:
|
|
149
|
+
parser.error(f"could not write --output file: {exc}")
|
|
150
|
+
print(json_payload)
|
|
151
|
+
return 0 if payload["status"] == "ready" else 1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _policy_payload(
|
|
155
|
+
config: ServiceConfig,
|
|
156
|
+
llm_config: LLMProviderConfig,
|
|
157
|
+
*,
|
|
158
|
+
require_auth: bool,
|
|
159
|
+
require_production_controls: bool,
|
|
160
|
+
require_runtime_provider: bool,
|
|
161
|
+
) -> Dict[str, Any]:
|
|
162
|
+
warnings = []
|
|
163
|
+
failures = []
|
|
164
|
+
configured_auth_tokens = _configured_auth_tokens(config)
|
|
165
|
+
if _is_public_bind(config.host) and not config.auth_required:
|
|
166
|
+
warnings.append("public_bind_without_auth")
|
|
167
|
+
if (require_auth or require_production_controls) and not config.auth_required:
|
|
168
|
+
failures.append("auth_required")
|
|
169
|
+
if require_auth or require_production_controls:
|
|
170
|
+
if any(not safe_header_value(f"Bearer {token}") for token in configured_auth_tokens):
|
|
171
|
+
failures.append("auth_token_unsafe")
|
|
172
|
+
if any(_is_placeholder_auth_token(token) for token in configured_auth_tokens):
|
|
173
|
+
failures.append("auth_token_placeholder")
|
|
174
|
+
if require_production_controls:
|
|
175
|
+
if any(len(token) < MIN_PRODUCTION_AUTH_TOKEN_CHARS for token in configured_auth_tokens):
|
|
176
|
+
failures.append("auth_token_too_short")
|
|
177
|
+
if not config.trace_dir:
|
|
178
|
+
failures.append("trace_dir_required")
|
|
179
|
+
if config.rate_limit_per_minute <= 0:
|
|
180
|
+
failures.append("rate_limit_required")
|
|
181
|
+
if config.max_concurrent_runs <= 0:
|
|
182
|
+
failures.append("concurrency_limit_required")
|
|
183
|
+
if not config.protect_diagnostics:
|
|
184
|
+
failures.append("diagnostics_protection_required")
|
|
185
|
+
if config.allow_full_trace_response:
|
|
186
|
+
failures.append("full_trace_response_must_be_disabled")
|
|
187
|
+
if require_runtime_provider:
|
|
188
|
+
if not llm_config.base_url:
|
|
189
|
+
failures.append("llm_base_url_required")
|
|
190
|
+
if not llm_config.model:
|
|
191
|
+
failures.append("llm_model_required")
|
|
192
|
+
if not llm_config.api_key:
|
|
193
|
+
failures.append("llm_api_key_required")
|
|
194
|
+
if config.runtime_max_iterations < MIN_RUNTIME_PROVIDER_ITERATIONS:
|
|
195
|
+
failures.append("runtime_iterations_too_low")
|
|
196
|
+
if failures:
|
|
197
|
+
status = "failed"
|
|
198
|
+
elif warnings:
|
|
199
|
+
status = "warning"
|
|
200
|
+
else:
|
|
201
|
+
status = "ok"
|
|
202
|
+
return {
|
|
203
|
+
"status": status,
|
|
204
|
+
"warnings": warnings,
|
|
205
|
+
"failures": failures,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _is_public_bind(host: str) -> bool:
|
|
210
|
+
return host in {"0.0.0.0", "::", ""}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _is_placeholder_auth_token(auth_token: str) -> bool:
|
|
214
|
+
normalized = auth_token.strip().lower()
|
|
215
|
+
return normalized in PLACEHOLDER_AUTH_TOKENS
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _configured_auth_tokens(config: ServiceConfig) -> list[str]:
|
|
219
|
+
tokens = []
|
|
220
|
+
if config.auth_token:
|
|
221
|
+
tokens.append(config.auth_token)
|
|
222
|
+
tokens.extend(config.auth_tokens.values())
|
|
223
|
+
return tokens
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _runtime_policy_summary(config: ServiceConfig) -> Dict[str, Any]:
|
|
227
|
+
_status_code, payload = execute_runtime_policy_request(
|
|
228
|
+
config,
|
|
229
|
+
request_auth_subject="default",
|
|
230
|
+
request_auth_is_admin=True,
|
|
231
|
+
)
|
|
232
|
+
effective_tool_policy = payload.get("effective_tool_policy", [])
|
|
233
|
+
approval_required_count = sum(
|
|
234
|
+
1
|
|
235
|
+
for item in effective_tool_policy
|
|
236
|
+
if isinstance(item, dict)
|
|
237
|
+
and item.get("approval_required") == "true"
|
|
238
|
+
)
|
|
239
|
+
return {
|
|
240
|
+
"trace_type": payload.get("trace_type", ""),
|
|
241
|
+
"effective_policy_source": payload.get("effective_policy_source", ""),
|
|
242
|
+
"effective_allowed_tools": payload.get("effective_allowed_tools", []),
|
|
243
|
+
"effective_allowed_tool_count": str(
|
|
244
|
+
len(payload.get("effective_allowed_tools", []))
|
|
245
|
+
),
|
|
246
|
+
"approval_required_tool_count": str(approval_required_count),
|
|
247
|
+
"subject_policy_count": payload.get("subject_policy_count", "0"),
|
|
248
|
+
"effective_tool_policy_sha256": payload.get(
|
|
249
|
+
"effective_tool_policy_sha256", ""
|
|
250
|
+
),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Tuple
|
|
7
|
+
|
|
8
|
+
from kagent.utils.json_output import format_and_write_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def summarize_metrics_file(path: Path) -> Dict[str, Any]:
|
|
12
|
+
metrics_file_found = path.exists()
|
|
13
|
+
records, malformed_lines = _read_jsonl(path)
|
|
14
|
+
total = len(records)
|
|
15
|
+
passed_records = [record for record in records if record.get("status") == "passed"]
|
|
16
|
+
failed_records = [record for record in records if record.get("status") != "passed"]
|
|
17
|
+
durations = [_duration_value(record.get("duration_seconds", 0)) for record in records]
|
|
18
|
+
latest = records[-1] if records else {}
|
|
19
|
+
|
|
20
|
+
pass_rate = len(passed_records) / total if total else 0.0
|
|
21
|
+
average_duration = sum(durations) / total if total else 0.0
|
|
22
|
+
failed_iterations = [str(record.get("iteration")) for record in failed_records]
|
|
23
|
+
latest_evaluator_failed = _string_or_empty(latest.get("evaluator_failed"))
|
|
24
|
+
latest_status = _string_or_empty(latest.get("status"))
|
|
25
|
+
latest_category_counts = _category_counts(latest.get("evaluator_category_counts"))
|
|
26
|
+
recent_statuses = _recent_statuses(records)
|
|
27
|
+
health = _health(total, len(passed_records), latest_evaluator_failed, malformed_lines)
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
"iterations": str(total),
|
|
31
|
+
"passed": str(len(passed_records)),
|
|
32
|
+
"failed": str(len(failed_records)),
|
|
33
|
+
"pass_rate": f"{pass_rate:.2f}",
|
|
34
|
+
"health": health,
|
|
35
|
+
"metrics_file_found": str(metrics_file_found).lower(),
|
|
36
|
+
"average_duration_seconds": f"{average_duration:.2f}",
|
|
37
|
+
"latest_status": latest_status,
|
|
38
|
+
"recent_health": _recent_health(recent_statuses),
|
|
39
|
+
"consecutive_passes": str(_consecutive_pass_count(records)),
|
|
40
|
+
"recent_statuses": recent_statuses,
|
|
41
|
+
"failed_iterations": failed_iterations,
|
|
42
|
+
"malformed_lines": malformed_lines,
|
|
43
|
+
"latest_evaluator_passed": _string_or_empty(latest.get("evaluator_passed")),
|
|
44
|
+
"latest_evaluator_failed": latest_evaluator_failed,
|
|
45
|
+
"latest_slowest_case": _string_or_empty(latest.get("evaluator_slowest_case")),
|
|
46
|
+
"latest_recovered_cases": _string_or_empty(
|
|
47
|
+
latest.get("evaluator_recovered_cases")
|
|
48
|
+
),
|
|
49
|
+
"latest_recovery_rate": _string_or_empty(latest.get("evaluator_recovery_rate")),
|
|
50
|
+
"latest_category_counts": latest_category_counts,
|
|
51
|
+
"recommendations": _recommendations(
|
|
52
|
+
failed_iterations,
|
|
53
|
+
latest_evaluator_failed,
|
|
54
|
+
malformed_lines,
|
|
55
|
+
latest_status,
|
|
56
|
+
_string_or_empty(latest.get("evaluator_passed")),
|
|
57
|
+
latest_category_counts,
|
|
58
|
+
metrics_file_found,
|
|
59
|
+
path,
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
parser = argparse.ArgumentParser(
|
|
66
|
+
description="Summarize continuous iteration JSONL metrics."
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument("metrics_jsonl", help="Path to the metrics JSONL file.")
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--output",
|
|
71
|
+
default="",
|
|
72
|
+
metavar="PATH",
|
|
73
|
+
help="Write the JSON payload to PATH as well as stdout.",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--require-recent-health",
|
|
77
|
+
choices=["healthy", "recovering", "failing", "unknown"],
|
|
78
|
+
default="",
|
|
79
|
+
help="Exit with code 1 unless recent_health matches this value.",
|
|
80
|
+
)
|
|
81
|
+
args = parser.parse_args()
|
|
82
|
+
summary = summarize_metrics_file(Path(args.metrics_jsonl))
|
|
83
|
+
try:
|
|
84
|
+
json_payload = format_and_write_json(summary, args.output)
|
|
85
|
+
except OSError as exc:
|
|
86
|
+
parser.error(f"could not write --output file: {exc}")
|
|
87
|
+
print(json_payload)
|
|
88
|
+
if args.require_recent_health and summary["recent_health"] != args.require_recent_health:
|
|
89
|
+
raise SystemExit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _read_jsonl(path: Path) -> Tuple[List[Dict[str, Any]], List[str]]:
|
|
93
|
+
if not path.exists():
|
|
94
|
+
return [], []
|
|
95
|
+
records = []
|
|
96
|
+
malformed_lines = []
|
|
97
|
+
for line_number, line in enumerate(path.read_text().splitlines(), start=1):
|
|
98
|
+
if not line.strip():
|
|
99
|
+
continue
|
|
100
|
+
try:
|
|
101
|
+
payload = json.loads(line)
|
|
102
|
+
except json.JSONDecodeError:
|
|
103
|
+
malformed_lines.append(str(line_number))
|
|
104
|
+
continue
|
|
105
|
+
if not isinstance(payload, dict):
|
|
106
|
+
malformed_lines.append(str(line_number))
|
|
107
|
+
continue
|
|
108
|
+
records.append(payload)
|
|
109
|
+
return records, malformed_lines
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _string_or_empty(value: Any) -> str:
|
|
113
|
+
if value is None:
|
|
114
|
+
return ""
|
|
115
|
+
return str(value)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _duration_value(value: Any) -> float:
|
|
119
|
+
try:
|
|
120
|
+
return float(value)
|
|
121
|
+
except (TypeError, ValueError):
|
|
122
|
+
return 0.0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _consecutive_pass_count(records: List[Dict[str, Any]]) -> int:
|
|
126
|
+
count = 0
|
|
127
|
+
for record in reversed(records):
|
|
128
|
+
if record.get("status") != "passed":
|
|
129
|
+
break
|
|
130
|
+
count += 1
|
|
131
|
+
return count
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _recent_statuses(records: List[Dict[str, Any]], limit: int = 5) -> List[str]:
|
|
135
|
+
return [
|
|
136
|
+
_string_or_empty(record.get("status"))
|
|
137
|
+
for record in records[-limit:]
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _recent_health(recent_statuses: List[str]) -> str:
|
|
142
|
+
if not recent_statuses:
|
|
143
|
+
return "unknown"
|
|
144
|
+
if recent_statuses[-1] != "passed":
|
|
145
|
+
return "failing"
|
|
146
|
+
if all(status == "passed" for status in recent_statuses):
|
|
147
|
+
return "healthy"
|
|
148
|
+
return "recovering"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _category_counts(value: Any) -> Dict[str, str]:
|
|
152
|
+
if not isinstance(value, dict):
|
|
153
|
+
return {}
|
|
154
|
+
return {str(key): str(value[key]) for key in sorted(value)}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _health(
|
|
158
|
+
total: int,
|
|
159
|
+
passed: int,
|
|
160
|
+
latest_evaluator_failed: str,
|
|
161
|
+
malformed_lines: List[str],
|
|
162
|
+
) -> str:
|
|
163
|
+
if total == 0:
|
|
164
|
+
return "failing" if malformed_lines else "unknown"
|
|
165
|
+
if malformed_lines:
|
|
166
|
+
return "degraded"
|
|
167
|
+
if passed == total and latest_evaluator_failed in {"", "0"}:
|
|
168
|
+
return "healthy"
|
|
169
|
+
if passed == 0:
|
|
170
|
+
return "failing"
|
|
171
|
+
return "degraded"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _recommendations(
|
|
175
|
+
failed_iterations: List[str],
|
|
176
|
+
latest_evaluator_failed: str,
|
|
177
|
+
malformed_lines: List[str],
|
|
178
|
+
latest_status: str,
|
|
179
|
+
latest_evaluator_passed: str,
|
|
180
|
+
latest_category_counts: Dict[str, str],
|
|
181
|
+
metrics_file_found: bool,
|
|
182
|
+
metrics_path: Path,
|
|
183
|
+
) -> List[str]:
|
|
184
|
+
recommendations = []
|
|
185
|
+
if not metrics_file_found:
|
|
186
|
+
recommendations.append(f"metrics file not found: {metrics_path}")
|
|
187
|
+
return recommendations
|
|
188
|
+
if failed_iterations:
|
|
189
|
+
recommendations.append(f"inspect failed iterations: {', '.join(failed_iterations)}")
|
|
190
|
+
if latest_status == "passed":
|
|
191
|
+
recommendations.append("latest run is passing after previous failures")
|
|
192
|
+
elif (
|
|
193
|
+
latest_status == "failed"
|
|
194
|
+
and not latest_evaluator_passed
|
|
195
|
+
and not latest_evaluator_failed
|
|
196
|
+
):
|
|
197
|
+
recommendations.append(
|
|
198
|
+
"inspect check log; latest run failed before a fresh evaluator report"
|
|
199
|
+
)
|
|
200
|
+
if latest_evaluator_failed not in {"", "0"}:
|
|
201
|
+
recommendations.append("review evaluator failures from the latest run")
|
|
202
|
+
if malformed_lines:
|
|
203
|
+
recommendations.append(
|
|
204
|
+
f"inspect malformed metrics lines: {', '.join(malformed_lines)}"
|
|
205
|
+
)
|
|
206
|
+
elif latest_evaluator_passed and not latest_category_counts:
|
|
207
|
+
recommendations.append(
|
|
208
|
+
"review continuous metrics wiring; latest evaluator category counts are missing"
|
|
209
|
+
)
|
|
210
|
+
return recommendations
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
main()
|