@openlucaskaka/kagent 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +353 -0
  2. package/npm/bin/kagent-serve.js +6 -0
  3. package/npm/bin/kagent.js +6 -0
  4. package/npm/lib/App.js +524 -0
  5. package/npm/lib/app-state.js +224 -0
  6. package/npm/lib/approval-choice.js +25 -0
  7. package/npm/lib/commands.js +59 -0
  8. package/npm/lib/editor.js +188 -0
  9. package/npm/lib/ink-runner.js +41 -0
  10. package/npm/lib/kagent-home.js +39 -0
  11. package/npm/lib/launcher.js +221 -0
  12. package/npm/lib/protocol.js +33 -0
  13. package/npm/lib/provider-setup.js +139 -0
  14. package/npm/lib/python-runner.js +892 -0
  15. package/npm/lib/runtime-client.js +390 -0
  16. package/npm/lib/terminal-input.js +127 -0
  17. package/npm/lib/terminal-text.js +19 -0
  18. package/npm/lib/terminal-width.js +14 -0
  19. package/npm/lib/transcript.js +227 -0
  20. package/npm/lib/ui-components.js +247 -0
  21. package/npm/lib/update-manager.js +334 -0
  22. package/package.json +39 -0
  23. package/pyproject.toml +55 -0
  24. package/src/kagent/__init__.py +90 -0
  25. package/src/kagent/cli/__init__.py +5 -0
  26. package/src/kagent/cli/__main__.py +6 -0
  27. package/src/kagent/cli/commands.py +112 -0
  28. package/src/kagent/cli/conversation.py +127 -0
  29. package/src/kagent/cli/interactive.py +841 -0
  30. package/src/kagent/cli/main.py +685 -0
  31. package/src/kagent/cli/memory.py +460 -0
  32. package/src/kagent/cli/pending_approval.py +169 -0
  33. package/src/kagent/cli/provider.py +27 -0
  34. package/src/kagent/cli/session_commands.py +401 -0
  35. package/src/kagent/cli/stdio_runtime.py +784 -0
  36. package/src/kagent/cli/trace.py +67 -0
  37. package/src/kagent/cli/ui.py +931 -0
  38. package/src/kagent/core/__init__.py +0 -0
  39. package/src/kagent/core/agent.py +296 -0
  40. package/src/kagent/core/faults.py +11 -0
  41. package/src/kagent/core/invariants.py +47 -0
  42. package/src/kagent/core/normalization.py +81 -0
  43. package/src/kagent/core/planning.py +48 -0
  44. package/src/kagent/core/state.py +73 -0
  45. package/src/kagent/core/summary.py +64 -0
  46. package/src/kagent/core/tools.py +196 -0
  47. package/src/kagent/core/trace.py +57 -0
  48. package/src/kagent/eval/__init__.py +11 -0
  49. package/src/kagent/eval/cases.py +229 -0
  50. package/src/kagent/eval/evaluator.py +216 -0
  51. package/src/kagent/integrations/__init__.py +3 -0
  52. package/src/kagent/integrations/audit.py +95 -0
  53. package/src/kagent/integrations/backends.py +131 -0
  54. package/src/kagent/integrations/memory.py +301 -0
  55. package/src/kagent/ops/__init__.py +0 -0
  56. package/src/kagent/ops/batch.py +156 -0
  57. package/src/kagent/ops/doctor.py +255 -0
  58. package/src/kagent/ops/metrics.py +214 -0
  59. package/src/kagent/ops/release_evidence.py +877 -0
  60. package/src/kagent/ops/release_manifest.py +142 -0
  61. package/src/kagent/ops/trace_replay.py +285 -0
  62. package/src/kagent/providers/__init__.py +7 -0
  63. package/src/kagent/providers/embeddings.py +187 -0
  64. package/src/kagent/providers/llm.py +770 -0
  65. package/src/kagent/py.typed +1 -0
  66. package/src/kagent/runtime/__init__.py +28 -0
  67. package/src/kagent/runtime/action_graph.py +543 -0
  68. package/src/kagent/runtime/agent.py +2089 -0
  69. package/src/kagent/runtime/approval.py +64 -0
  70. package/src/kagent/runtime/cancellation.py +64 -0
  71. package/src/kagent/runtime/checkpoint_state.py +146 -0
  72. package/src/kagent/runtime/checkpoint_storage.py +270 -0
  73. package/src/kagent/runtime/context.py +65 -0
  74. package/src/kagent/runtime/file_transaction.py +195 -0
  75. package/src/kagent/runtime/hooks.py +74 -0
  76. package/src/kagent/runtime/metadata.py +116 -0
  77. package/src/kagent/runtime/patch_checkpoints.py +385 -0
  78. package/src/kagent/runtime/policy.py +50 -0
  79. package/src/kagent/runtime/presentation.py +205 -0
  80. package/src/kagent/runtime/redaction.py +130 -0
  81. package/src/kagent/runtime/sandbox.py +331 -0
  82. package/src/kagent/runtime/skills.py +66 -0
  83. package/src/kagent/runtime/steering.py +56 -0
  84. package/src/kagent/runtime/steps.py +255 -0
  85. package/src/kagent/runtime/task_state.py +40 -0
  86. package/src/kagent/runtime/tools.py +3532 -0
  87. package/src/kagent/runtime/types.py +240 -0
  88. package/src/kagent/runtime/workspace.py +597 -0
  89. package/src/kagent/service/__init__.py +26 -0
  90. package/src/kagent/service/__main__.py +6 -0
  91. package/src/kagent/service/active_runs.py +178 -0
  92. package/src/kagent/service/cli.py +737 -0
  93. package/src/kagent/service/contract.py +2571 -0
  94. package/src/kagent/service/errors.py +71 -0
  95. package/src/kagent/service/idempotency.py +584 -0
  96. package/src/kagent/service/router.py +884 -0
  97. package/src/kagent/service/run.py +150 -0
  98. package/src/kagent/service/runtime.py +1915 -0
  99. package/src/kagent/service/runtime_approval.py +20 -0
  100. package/src/kagent/service/runtime_cancel.py +192 -0
  101. package/src/kagent/service/runtime_lifecycle.py +229 -0
  102. package/src/kagent/service/runtime_metadata.py +21 -0
  103. package/src/kagent/service/runtime_policy.py +115 -0
  104. package/src/kagent/service/runtime_recovery.py +573 -0
  105. package/src/kagent/service/runtime_resume.py +382 -0
  106. package/src/kagent/service/runtime_resume_claim.py +116 -0
  107. package/src/kagent/service/runtime_run.py +350 -0
  108. package/src/kagent/service/runtime_status.py +2007 -0
  109. package/src/kagent/service/safety.py +139 -0
  110. package/src/kagent/service/server.py +114 -0
  111. package/src/kagent/service/status.py +233 -0
  112. package/src/kagent/service/trace_store.py +322 -0
  113. package/src/kagent/service/transport.py +24 -0
  114. package/src/kagent/utils/__init__.py +0 -0
  115. package/src/kagent/utils/config_validation.py +21 -0
  116. package/src/kagent/utils/json_output.py +23 -0
  117. package/src/kagent/utils/paths.py +473 -0
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import contextlib
5
+ import io
6
+ import time
7
+ import warnings
8
+ from typing import Any, Callable, Dict, List
9
+
10
+ from kagent.core.invariants import validate_run_invariants
11
+ from kagent.core.summary import summarize_run
12
+ from kagent.eval.cases import (
13
+ CaseCheck,
14
+ EvaluationCase,
15
+ build_evaluation_cases,
16
+ )
17
+ from kagent.utils.json_output import format_and_write_json
18
+
19
+
20
+ def evaluate_agent(category: str = "", case_name: str = "") -> Dict[str, Any]:
21
+ started_at = time.perf_counter()
22
+ warning_sink = io.StringIO()
23
+ with contextlib.redirect_stderr(warning_sink), warnings.catch_warnings():
24
+ warnings.simplefilter("ignore")
25
+ from kagent.core.agent import AgentConfig, AgentStatus, run_agent
26
+
27
+ all_cases = build_evaluation_cases(AgentConfig, AgentStatus, run_agent)
28
+ _validate_case_filters(all_cases, category=category, case_name=case_name)
29
+ selected_cases = _filter_cases(
30
+ all_cases,
31
+ category=category,
32
+ case_name=case_name,
33
+ )
34
+ cases = [
35
+ _run_case(item.name, item.category, item.run, item.check)
36
+ for item in selected_cases
37
+ ]
38
+ passed = sum(1 for case in cases if case["passed"])
39
+ recovered_cases = _recovered_case_count(cases)
40
+ return {
41
+ "passed": passed,
42
+ "failed": len(cases) - passed,
43
+ "filters": {"category": category, "case": case_name},
44
+ "recovered_cases": str(recovered_cases),
45
+ "recovery_rate": f"{recovered_cases / len(cases):.2f}" if cases else "0.00",
46
+ "category_counts": _category_counts(cases),
47
+ "duration_seconds": _duration_since(started_at),
48
+ "slowest_case": _slowest_case_name(cases),
49
+ "cases": cases,
50
+ }
51
+
52
+
53
+ def registered_evaluation_cases() -> List[Dict[str, str]]:
54
+ warning_sink = io.StringIO()
55
+ with contextlib.redirect_stderr(warning_sink), warnings.catch_warnings():
56
+ warnings.simplefilter("ignore")
57
+ from kagent.core.agent import AgentConfig, AgentStatus, run_agent
58
+
59
+ return [
60
+ {"name": case.name, "category": case.category}
61
+ for case in build_evaluation_cases(AgentConfig, AgentStatus, run_agent)
62
+ ]
63
+
64
+
65
+ def _validate_case_filters(
66
+ cases: List[EvaluationCase],
67
+ *,
68
+ category: str,
69
+ case_name: str,
70
+ ) -> None:
71
+ if category and category not in {case.category for case in cases}:
72
+ raise ValueError(f"unknown evaluator category: {category}")
73
+ if case_name and case_name not in {case.name for case in cases}:
74
+ raise ValueError(f"unknown evaluator case: {case_name}")
75
+
76
+
77
+ def _filter_cases(
78
+ cases: List[EvaluationCase],
79
+ *,
80
+ category: str,
81
+ case_name: str,
82
+ ) -> List[EvaluationCase]:
83
+ selected = cases
84
+ if category:
85
+ selected = [case for case in selected if case.category == category]
86
+ if case_name:
87
+ selected = [case for case in selected if case.name == case_name]
88
+ return selected
89
+
90
+
91
+ def _run_case(
92
+ name: str,
93
+ category: str,
94
+ run: Callable[[], Dict[str, Any]],
95
+ check: CaseCheck,
96
+ ) -> Dict[str, Any]:
97
+ started_at = time.perf_counter()
98
+ try:
99
+ result = run()
100
+ except Exception as exc:
101
+ return {
102
+ "name": name,
103
+ "category": category,
104
+ "passed": False,
105
+ "status": "error",
106
+ "answer": None,
107
+ "retry_count": 0,
108
+ "events": [],
109
+ "invariant_errors": [],
110
+ "summary": {},
111
+ "error": str(exc),
112
+ "duration_seconds": _duration_since(started_at),
113
+ }
114
+
115
+ invariant_errors = validate_run_invariants(result)
116
+ return {
117
+ "name": name,
118
+ "category": category,
119
+ "passed": check(result) and not invariant_errors,
120
+ "status": result["status"].value,
121
+ "answer": result.get("answer"),
122
+ "retry_count": result.get("retry_count", 0),
123
+ "events": [event["node"] for event in result.get("events", [])],
124
+ "invariant_errors": invariant_errors,
125
+ "summary": summarize_run(result),
126
+ "error": "",
127
+ "duration_seconds": _duration_since(started_at),
128
+ }
129
+
130
+
131
+ def _duration_since(started_at: float) -> str:
132
+ return f"{time.perf_counter() - started_at:.4f}"
133
+
134
+
135
+ def _slowest_case_name(cases: List[Dict[str, Any]]) -> str:
136
+ if not cases:
137
+ return ""
138
+ return max(cases, key=lambda case: float(case["duration_seconds"]))["name"]
139
+
140
+
141
+ def _recovered_case_count(cases: List[Dict[str, Any]]) -> int:
142
+ return sum(
143
+ 1
144
+ for case in cases
145
+ if case.get("summary", {}).get("recovered") == "true"
146
+ )
147
+
148
+
149
+ def _category_counts(cases: List[Dict[str, Any]]) -> Dict[str, str]:
150
+ counts: Dict[str, int] = {}
151
+ for case in cases:
152
+ category = case.get("category", "")
153
+ counts[category] = counts.get(category, 0) + 1
154
+ return {category: str(counts[category]) for category in sorted(counts)}
155
+
156
+
157
+ def _exit_code_for_report(report: Dict[str, Any], *, fail_on_failure: bool) -> int:
158
+ if fail_on_failure and int(report.get("failed", 0)) > 0:
159
+ return 1
160
+ return 0
161
+
162
+
163
+ def main() -> None:
164
+ parser = argparse.ArgumentParser(description="Evaluate the kagent.")
165
+ parser.add_argument(
166
+ "--category",
167
+ default="",
168
+ help="Run only evaluator cases in this category, such as recovery or tool.",
169
+ )
170
+ parser.add_argument(
171
+ "--case",
172
+ default="",
173
+ help="Run only the evaluator case with this exact name.",
174
+ )
175
+ parser.add_argument(
176
+ "--list-cases",
177
+ action="store_true",
178
+ help="List evaluator case names and categories without running them.",
179
+ )
180
+ parser.add_argument(
181
+ "--output",
182
+ default="",
183
+ metavar="PATH",
184
+ help="Write the JSON payload to PATH as well as stdout.",
185
+ )
186
+ parser.add_argument(
187
+ "--fail-on-failure",
188
+ action="store_true",
189
+ help="Exit with code 1 when the evaluator report contains failed cases.",
190
+ )
191
+ args = parser.parse_args()
192
+ if args.list_cases:
193
+ _emit_json_payload({"cases": registered_evaluation_cases()}, args.output, parser)
194
+ return
195
+ try:
196
+ report = evaluate_agent(category=args.category, case_name=args.case)
197
+ except ValueError as exc:
198
+ parser.error(str(exc))
199
+ _emit_json_payload(report, args.output, parser)
200
+ raise SystemExit(_exit_code_for_report(report, fail_on_failure=args.fail_on_failure))
201
+
202
+
203
+ def _emit_json_payload(
204
+ payload: Dict[str, Any],
205
+ output_path: str,
206
+ parser: argparse.ArgumentParser,
207
+ ) -> None:
208
+ try:
209
+ json_payload = format_and_write_json(payload, output_path)
210
+ except OSError as exc:
211
+ parser.error(f"could not write --output file: {exc}")
212
+ print(json_payload)
213
+
214
+
215
+ if __name__ == "__main__":
216
+ main()
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = []
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.request
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict
7
+
8
+ _AUDIT_EVENT_FIELDS = (
9
+ "run_id",
10
+ "type",
11
+ "node",
12
+ "iteration",
13
+ "action_id",
14
+ "tool",
15
+ "status",
16
+ "error_code",
17
+ "duration_seconds",
18
+ "action_count",
19
+ "iteration_count",
20
+ )
21
+
22
+
23
+ @dataclass
24
+ class KafkaRestAuditHook:
25
+ url: str
26
+ topic: str
27
+ timeout_seconds: float = 2.0
28
+ fail_closed: bool = False
29
+ failure_count: int = 0
30
+
31
+ def on_run_end(self, context: Dict[str, Any]) -> None:
32
+ payload = {
33
+ "topic": self.topic,
34
+ "event": {
35
+ "type": "run_end",
36
+ "run_id": str(context.get("run_id", "")),
37
+ "goal": str(context.get("goal", "")),
38
+ "status": str(context.get("status", "")),
39
+ "duration_seconds": str(context.get("duration_seconds", "")),
40
+ },
41
+ }
42
+ self._post(payload)
43
+
44
+ def _post(self, payload: Dict[str, Any]) -> None:
45
+ request = urllib.request.Request(
46
+ self.url,
47
+ data=json.dumps(payload, sort_keys=True).encode("utf-8"),
48
+ headers={"Content-Type": "application/json"},
49
+ method="POST",
50
+ )
51
+ try:
52
+ with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
53
+ if int(response.status) < 200 or int(response.status) >= 300:
54
+ raise RuntimeError("audit sink rejected event")
55
+ except Exception:
56
+ self.failure_count += 1
57
+ if self.fail_closed:
58
+ raise
59
+
60
+
61
+ @dataclass
62
+ class KafkaRestProgressEventSink:
63
+ url: str
64
+ topic: str
65
+ timeout_seconds: float = 2.0
66
+ fail_closed: bool = False
67
+ failure_count: int = 0
68
+
69
+ def __call__(self, event: Dict[str, Any]) -> None:
70
+ payload = {
71
+ "topic": self.topic,
72
+ "event": _redacted_progress_event(event),
73
+ }
74
+ request = urllib.request.Request(
75
+ self.url,
76
+ data=json.dumps(payload, sort_keys=True).encode("utf-8"),
77
+ headers={"Content-Type": "application/json"},
78
+ method="POST",
79
+ )
80
+ try:
81
+ with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
82
+ if int(response.status) < 200 or int(response.status) >= 300:
83
+ raise RuntimeError("audit sink rejected event")
84
+ except Exception:
85
+ self.failure_count += 1
86
+ if self.fail_closed:
87
+ raise
88
+
89
+
90
+ def _redacted_progress_event(event: Dict[str, Any]) -> Dict[str, str]:
91
+ return {
92
+ field: str(event[field])
93
+ for field in _AUDIT_EVENT_FIELDS
94
+ if field in event and event[field] is not None
95
+ }
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import socket
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+ from dataclasses import dataclass
8
+ from os import environ
9
+ from typing import Dict, Mapping, Optional
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ExternalBackendConfig:
14
+ redis_url: str = ""
15
+ milvus_url: str = ""
16
+ kafka_audit_url: str = ""
17
+ kafka_audit_topic: str = ""
18
+ timeout_seconds: float = 2.0
19
+
20
+ @classmethod
21
+ def from_env(
22
+ cls,
23
+ env: Optional[Mapping[str, str]] = None,
24
+ ) -> "ExternalBackendConfig":
25
+ source = env if env is not None else environ
26
+ return cls(
27
+ redis_url=source.get("KAGENT_REDIS_URL", cls.redis_url),
28
+ milvus_url=source.get("KAGENT_MILVUS_URL", cls.milvus_url),
29
+ kafka_audit_url=source.get(
30
+ "KAGENT_KAFKA_AUDIT_URL",
31
+ cls.kafka_audit_url,
32
+ ),
33
+ kafka_audit_topic=source.get(
34
+ "KAGENT_KAFKA_AUDIT_TOPIC",
35
+ cls.kafka_audit_topic,
36
+ ),
37
+ timeout_seconds=_env_float(
38
+ source,
39
+ "KAGENT_EXTERNAL_BACKEND_TIMEOUT_SECONDS",
40
+ cls.timeout_seconds,
41
+ ),
42
+ )
43
+
44
+ def __post_init__(self) -> None:
45
+ if self.timeout_seconds <= 0:
46
+ raise ValueError("external backend timeout_seconds must be positive")
47
+
48
+ def redacted_snapshot(self) -> Dict[str, str]:
49
+ return {
50
+ "redis_short_term_memory": "enabled" if self.redis_url else "disabled",
51
+ "milvus_long_term_memory": "enabled" if self.milvus_url else "disabled",
52
+ "kafka_audit_sink": "enabled" if self.kafka_audit_url else "disabled",
53
+ "kafka_audit_topic_configured": str(bool(self.kafka_audit_topic)).lower(),
54
+ "external_backend_timeout_seconds": str(self.timeout_seconds),
55
+ }
56
+
57
+
58
+ def check_external_backends(config: ExternalBackendConfig) -> Dict[str, str]:
59
+ checks: Dict[str, str] = {}
60
+ if config.redis_url:
61
+ checks["redis_short_term_memory"] = _check(
62
+ lambda: _redis_ping(config.redis_url, timeout_seconds=config.timeout_seconds),
63
+ "redis_unavailable",
64
+ )
65
+ if config.milvus_url:
66
+ checks["milvus_long_term_memory"] = _check(
67
+ lambda: _http_get_ok(
68
+ config.milvus_url,
69
+ timeout_seconds=config.timeout_seconds,
70
+ ),
71
+ "milvus_unavailable",
72
+ )
73
+ if config.kafka_audit_url:
74
+ checks["kafka_audit_sink"] = _check(
75
+ lambda: _http_get_ok(
76
+ config.kafka_audit_url,
77
+ timeout_seconds=config.timeout_seconds,
78
+ ),
79
+ "kafka_audit_unavailable",
80
+ )
81
+ return checks
82
+
83
+
84
+ def _check(check, failure_code: str) -> str:
85
+ try:
86
+ check()
87
+ except Exception:
88
+ return f"failed: {failure_code}"
89
+ return "ok"
90
+
91
+
92
+ def _redis_ping(redis_url: str, *, timeout_seconds: float) -> None:
93
+ parsed = urllib.parse.urlparse(redis_url)
94
+ if parsed.scheme != "redis":
95
+ raise ValueError("redis url must use redis://")
96
+ if not parsed.hostname:
97
+ raise ValueError("redis url host is required")
98
+ port = parsed.port or 6379
99
+ with socket.create_connection(
100
+ (parsed.hostname, port),
101
+ timeout=timeout_seconds,
102
+ ) as conn:
103
+ conn.settimeout(timeout_seconds)
104
+ conn.sendall(b"*1\r\n$4\r\nPING\r\n")
105
+ response = conn.recv(64)
106
+ if not response.startswith(b"+PONG"):
107
+ raise RuntimeError("redis ping failed")
108
+
109
+
110
+ def _http_get_ok(url: str, *, timeout_seconds: float) -> None:
111
+ parsed = urllib.parse.urlparse(url)
112
+ if parsed.scheme not in {"http", "https"}:
113
+ raise ValueError("backend url must use http:// or https://")
114
+ request = urllib.request.Request(url, method="GET")
115
+ try:
116
+ with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
117
+ status_code = int(response.status)
118
+ except urllib.error.HTTPError as exc:
119
+ status_code = int(exc.code)
120
+ if status_code < 200 or status_code >= 300:
121
+ raise RuntimeError("backend health check failed")
122
+
123
+
124
+ def _env_float(source: Mapping[str, str], name: str, default: float) -> float:
125
+ value = source.get(name, "")
126
+ if value == "":
127
+ return default
128
+ try:
129
+ return float(value)
130
+ except ValueError as exc:
131
+ raise ValueError(f"{name} must be a float") from exc