@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,322 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import fcntl
5
+ import json
6
+ import os
7
+ import tempfile
8
+ import time
9
+ from contextlib import contextmanager
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Iterator
12
+
13
+ from kagent.runtime import RUNTIME_TRACE_TYPE
14
+ from kagent.service.safety import (
15
+ safe_trace_file_stem,
16
+ )
17
+ from kagent.utils.json_output import format_and_write_json, json_ready
18
+
19
+ DEFAULT_RUNTIME_RETENTION_STATUSES = ("cancelled", "done", "failed", "resumed")
20
+
21
+
22
+ def trace_path_for_run_id(run_id: Any, trace_dir: str) -> Path:
23
+ return Path(trace_dir) / f"{safe_trace_file_stem(run_id)}.json"
24
+
25
+
26
+ @contextmanager
27
+ def runtime_trace_lock(run_id: Any, trace_dir: str) -> Iterator[None]:
28
+ """Serialize cross-process read/modify/write operations for one runtime trace."""
29
+
30
+ output_dir = Path(trace_dir)
31
+ _ensure_owner_only_trace_dir(output_dir)
32
+ lock_path = output_dir / f".{safe_trace_file_stem(run_id)}.runtime.lock"
33
+ flags = os.O_CREAT | os.O_RDWR
34
+ if hasattr(os, "O_NOFOLLOW"):
35
+ flags |= os.O_NOFOLLOW
36
+ lock_fd = os.open(lock_path, flags, 0o600)
37
+ try:
38
+ os.fchmod(lock_fd, 0o600)
39
+ fcntl.flock(lock_fd, fcntl.LOCK_EX)
40
+ yield
41
+ finally:
42
+ try:
43
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
44
+ finally:
45
+ os.close(lock_fd)
46
+
47
+
48
+ def persist_trace(trace: Dict[str, Any], trace_dir: str) -> str:
49
+ output_dir = Path(trace_dir)
50
+ _ensure_owner_only_trace_dir(output_dir)
51
+ output_path = trace_path_for_run_id(trace.get("run_id"), trace_dir)
52
+ temporary_path = _write_owner_only_temporary_trace(
53
+ output_dir,
54
+ output_path.name,
55
+ json.dumps(json_ready(trace), sort_keys=True) + "\n",
56
+ )
57
+ try:
58
+ temporary_path.replace(output_path)
59
+ finally:
60
+ temporary_path.unlink(missing_ok=True)
61
+ return str(output_path)
62
+
63
+
64
+ def load_trace_by_run_id(run_id: Any, trace_dir: str) -> Dict[str, Any] | None:
65
+ trace_path = trace_path_for_run_id(run_id, trace_dir)
66
+ if trace_path.is_symlink():
67
+ raise OSError("trace file must not be a symlink")
68
+ try:
69
+ payload = json.loads(trace_path.read_text(encoding="utf-8"))
70
+ except FileNotFoundError:
71
+ return None
72
+ if not isinstance(payload, dict):
73
+ raise ValueError("trace payload must be a JSON object")
74
+ return payload
75
+
76
+
77
+ def _ensure_owner_only_trace_dir(output_dir: Path) -> None:
78
+ output_dir.mkdir(parents=True, exist_ok=True)
79
+ output_dir.chmod(0o700)
80
+
81
+
82
+ def _write_owner_only_temporary_trace(output_dir: Path, output_name: str, data: str) -> Path:
83
+ fd, temporary_name = tempfile.mkstemp(
84
+ prefix=f".{output_name}.",
85
+ suffix=".tmp",
86
+ dir=output_dir,
87
+ text=True,
88
+ )
89
+ temporary_path = Path(temporary_name)
90
+ try:
91
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
92
+ fd = -1
93
+ handle.write(data)
94
+ handle.flush()
95
+ os.fsync(handle.fileno())
96
+ except Exception:
97
+ if fd != -1:
98
+ os.close(fd)
99
+ temporary_path.unlink(missing_ok=True)
100
+ raise
101
+ return temporary_path
102
+
103
+
104
+ def prune_traces(
105
+ trace_dir: str | Path,
106
+ *,
107
+ max_age_seconds: float,
108
+ now: float | None = None,
109
+ dry_run: bool = True,
110
+ ) -> Dict[str, Any]:
111
+ if max_age_seconds < 0:
112
+ raise ValueError("max_age_seconds must be non-negative")
113
+ output_dir = Path(trace_dir)
114
+ current_time = time.time() if now is None else now
115
+ cutoff = current_time - max_age_seconds
116
+ scanned = 0
117
+ matched = 0
118
+ deleted = 0
119
+ errors = []
120
+
121
+ for path in sorted(output_dir.glob("*.json")):
122
+ if path.is_symlink():
123
+ continue
124
+ if not path.is_file():
125
+ continue
126
+ scanned += 1
127
+ if path.stat().st_mtime > cutoff:
128
+ continue
129
+ matched += 1
130
+ if dry_run:
131
+ continue
132
+ try:
133
+ path.unlink()
134
+ deleted += 1
135
+ except OSError as exc:
136
+ errors.append({"path": str(path), "error": str(exc)})
137
+
138
+ return {
139
+ "trace_dir": str(output_dir),
140
+ "max_age_seconds": int(max_age_seconds),
141
+ "dry_run": dry_run,
142
+ "scanned": scanned,
143
+ "matched": matched,
144
+ "deleted": deleted,
145
+ "kept": scanned - matched,
146
+ "errors": errors,
147
+ }
148
+
149
+
150
+ def prune_runtime_traces(
151
+ trace_dir: str | Path,
152
+ *,
153
+ max_age_seconds: float,
154
+ statuses: tuple[str, ...] = DEFAULT_RUNTIME_RETENTION_STATUSES,
155
+ now: float | None = None,
156
+ dry_run: bool = True,
157
+ ) -> Dict[str, Any]:
158
+ if max_age_seconds < 0:
159
+ raise ValueError("max_age_seconds must be non-negative")
160
+ normalized_statuses = tuple(sorted({status for status in statuses if status}))
161
+ if not normalized_statuses:
162
+ raise ValueError("statuses must contain at least one runtime status")
163
+ output_dir = Path(trace_dir)
164
+ current_time = time.time() if now is None else now
165
+ cutoff = current_time - max_age_seconds
166
+ scanned = 0
167
+ runtime_scanned = 0
168
+ matched = 0
169
+ deleted = 0
170
+ protected_pending = 0
171
+ skipped_non_runtime = 0
172
+ skipped_fresh = 0
173
+ skipped_status = 0
174
+ unreadable = 0
175
+ matched_by_status: Dict[str, str] = {}
176
+ errors = []
177
+
178
+ for path in sorted(output_dir.glob("*.json")):
179
+ if path.is_symlink():
180
+ continue
181
+ if not path.is_file():
182
+ continue
183
+ scanned += 1
184
+ try:
185
+ trace = json.loads(path.read_text(encoding="utf-8"))
186
+ except (OSError, json.JSONDecodeError) as exc:
187
+ unreadable += 1
188
+ errors.append({"path": str(path), "error": str(exc)})
189
+ continue
190
+ if not isinstance(trace, dict):
191
+ unreadable += 1
192
+ errors.append({"path": str(path), "error": "trace payload must be a JSON object"})
193
+ continue
194
+ if trace.get("trace_type") != RUNTIME_TRACE_TYPE:
195
+ skipped_non_runtime += 1
196
+ continue
197
+ runtime_scanned += 1
198
+ status = str(trace.get("status", ""))
199
+ if status == "requires_approval" and status not in normalized_statuses:
200
+ protected_pending += 1
201
+ skipped_status += 1
202
+ continue
203
+ if status not in normalized_statuses:
204
+ skipped_status += 1
205
+ continue
206
+ if path.stat().st_mtime > cutoff:
207
+ skipped_fresh += 1
208
+ continue
209
+ matched += 1
210
+ matched_by_status[status] = str(int(matched_by_status.get(status, "0")) + 1)
211
+ if dry_run:
212
+ continue
213
+ try:
214
+ path.unlink()
215
+ deleted += 1
216
+ except OSError as exc:
217
+ errors.append({"path": str(path), "error": str(exc)})
218
+
219
+ return {
220
+ "trace_dir": str(output_dir),
221
+ "max_age_seconds": int(max_age_seconds),
222
+ "dry_run": dry_run,
223
+ "statuses": list(normalized_statuses),
224
+ "scanned": scanned,
225
+ "runtime_scanned": runtime_scanned,
226
+ "matched": matched,
227
+ "deleted": deleted,
228
+ "kept": scanned - matched,
229
+ "protected_pending": protected_pending,
230
+ "skipped_non_runtime": skipped_non_runtime,
231
+ "skipped_fresh": skipped_fresh,
232
+ "skipped_status": skipped_status,
233
+ "unreadable": unreadable,
234
+ "matched_by_status": matched_by_status,
235
+ "errors": errors,
236
+ }
237
+
238
+
239
+ def main() -> None:
240
+ parser = argparse.ArgumentParser(
241
+ description="Prune persisted kagent trace JSON files."
242
+ )
243
+ parser.add_argument("trace_dir", help="Trace directory to scan.")
244
+ parser.add_argument(
245
+ "--max-age-days",
246
+ type=float,
247
+ required=True,
248
+ help="Match trace JSON files older than this many days.",
249
+ )
250
+ parser.add_argument(
251
+ "--delete",
252
+ action="store_true",
253
+ help="Delete matched traces. Without this flag the command is a dry run.",
254
+ )
255
+ parser.add_argument(
256
+ "--runtime-only",
257
+ action="store_true",
258
+ help=(
259
+ "Prune only Codex-style runtime traces. By default this matches old "
260
+ "done, failed, and cancelled runs while protecting requires_approval."
261
+ ),
262
+ )
263
+ parser.add_argument(
264
+ "--statuses",
265
+ default=",".join(DEFAULT_RUNTIME_RETENTION_STATUSES),
266
+ help=(
267
+ "Comma-separated runtime statuses matched with --runtime-only. "
268
+ "Defaults to cancelled,done,failed,resumed."
269
+ ),
270
+ )
271
+ parser.add_argument(
272
+ "--output",
273
+ default="",
274
+ metavar="PATH",
275
+ help="Write the JSON summary to PATH as well as stdout.",
276
+ )
277
+ parser.add_argument(
278
+ "--fail-on-errors",
279
+ action="store_true",
280
+ help=(
281
+ "Exit with status 1 after writing the summary when unreadable traces "
282
+ "or delete errors are reported."
283
+ ),
284
+ )
285
+ args = parser.parse_args()
286
+ if args.max_age_days < 0:
287
+ parser.error("--max-age-days must be non-negative")
288
+ if args.runtime_only:
289
+ statuses = tuple(status.strip() for status in args.statuses.split(",") if status.strip())
290
+ summary = prune_runtime_traces(
291
+ args.trace_dir,
292
+ max_age_seconds=args.max_age_days * 24 * 60 * 60,
293
+ statuses=statuses,
294
+ dry_run=not args.delete,
295
+ )
296
+ else:
297
+ summary = prune_traces(
298
+ args.trace_dir,
299
+ max_age_seconds=args.max_age_days * 24 * 60 * 60,
300
+ dry_run=not args.delete,
301
+ )
302
+ print(format_and_write_json(summary, args.output))
303
+ if args.fail_on_errors and _trace_prune_has_errors(summary):
304
+ raise SystemExit(1)
305
+
306
+
307
+ def _trace_prune_has_errors(summary: Dict[str, Any]) -> bool:
308
+ errors = summary.get("errors")
309
+ if isinstance(errors, list) and errors:
310
+ return True
311
+ return _summary_int(summary.get("unreadable")) > 0
312
+
313
+
314
+ def _summary_int(value: Any) -> int:
315
+ try:
316
+ return int(str(value))
317
+ except (TypeError, ValueError):
318
+ return 0
319
+
320
+
321
+ if __name__ == "__main__":
322
+ main()
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Tuple
5
+
6
+ JSON_CONTENT_TYPE = "application/json"
7
+ PROMETHEUS_TEXT_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
8
+ NOSNIFF_HEADER_VALUE = "nosniff"
9
+ CACHE_CONTROL_HEADER_VALUE = "no-store"
10
+ REFERRER_POLICY_HEADER_VALUE = "no-referrer"
11
+ CONTENT_SECURITY_POLICY_HEADER_VALUE = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
12
+ X_FRAME_OPTIONS_HEADER_VALUE = "DENY"
13
+
14
+
15
+ def response_body(payload: Any) -> Tuple[bytes, str]:
16
+ if isinstance(payload, str):
17
+ return payload.encode("utf-8"), PROMETHEUS_TEXT_CONTENT_TYPE
18
+ return json.dumps(payload, sort_keys=True).encode("utf-8"), JSON_CONTENT_TYPE
19
+
20
+
21
+ def error_code_from_payload(payload: Any) -> str:
22
+ if isinstance(payload, dict):
23
+ return str(payload.get("error_code", ""))
24
+ return ""
File without changes
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+
6
+ def optional_json_int(payload: Dict[str, Any], key: str, default: int) -> int:
7
+ value = payload.get(key)
8
+ if value in {None, ""}:
9
+ return default
10
+ if isinstance(value, bool) or not isinstance(value, int):
11
+ raise ValueError(f"{key} must be an integer")
12
+ return value
13
+
14
+
15
+ def optional_json_bool(payload: Dict[str, Any], key: str, default: bool) -> bool:
16
+ if key not in payload:
17
+ return default
18
+ value = payload[key]
19
+ if not isinstance(value, bool):
20
+ raise ValueError(f"{key} must be a boolean")
21
+ return value
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def format_and_write_json(payload: Any, output_path: str) -> str:
10
+ json_payload = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
11
+ if output_path:
12
+ Path(output_path).write_text(json_payload + "\n", encoding="utf-8")
13
+ return json_payload
14
+
15
+
16
+ def json_ready(value: Any) -> Any:
17
+ if isinstance(value, Enum):
18
+ return value.value
19
+ if isinstance(value, dict):
20
+ return {key: json_ready(item) for key, item in value.items()}
21
+ if isinstance(value, list):
22
+ return [json_ready(item) for item in value]
23
+ return value