@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
File without changes
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import asdict
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Dict, List, Optional
7
+ from uuid import uuid4
8
+
9
+ from langgraph.graph import END, StateGraph
10
+
11
+ from kagent.core.normalization import (
12
+ plan_goal as _plan_goal,
13
+ )
14
+ from kagent.core.planning import (
15
+ normalize_fault_plan,
16
+ plan_errors,
17
+ validate_plan_steps,
18
+ )
19
+ from kagent.core.state import AgentConfig, AgentState, AgentStatus
20
+ from kagent.core.tools import execute_step, expected_answer
21
+ from kagent.core.trace import (
22
+ copy_agent_state,
23
+ record_execution_attempt,
24
+ record_node_event,
25
+ )
26
+
27
+
28
+ def build_agent_graph(config: AgentConfig):
29
+ graph = StateGraph(AgentState)
30
+ graph.add_node("planner", _planner)
31
+ graph.add_node("executor", _executor)
32
+ graph.add_node("verifier", _verifier)
33
+ graph.add_node("reflector", _reflector)
34
+ graph.set_entry_point("planner")
35
+ graph.add_conditional_edges(
36
+ "planner",
37
+ _route_after_planner,
38
+ {
39
+ "execute": "executor",
40
+ "end": END,
41
+ },
42
+ )
43
+ graph.add_edge("executor", "verifier")
44
+ graph.add_conditional_edges(
45
+ "verifier",
46
+ _route_after_verifier,
47
+ {
48
+ "execute": "executor",
49
+ "reflect": "reflector",
50
+ "end": END,
51
+ },
52
+ )
53
+ graph.add_edge("reflector", "executor")
54
+ return graph.compile(name=f"kagent-{config.max_steps}-{config.max_retries}")
55
+
56
+
57
+ def agent_topology() -> Dict[str, List[str]]:
58
+ return {
59
+ "nodes": ["planner", "executor", "verifier", "reflector"],
60
+ "edges": [
61
+ "planner -> executor",
62
+ "executor -> verifier",
63
+ "verifier -> reflector",
64
+ "reflector -> executor",
65
+ "verifier -> executor",
66
+ "verifier -> END",
67
+ "planner -> END",
68
+ ],
69
+ }
70
+
71
+
72
+ def run_agent(
73
+ goal: str,
74
+ *,
75
+ config: Optional[AgentConfig] = None,
76
+ fault_plan: Optional[Dict[str, List[str]]] = None,
77
+ ) -> AgentState:
78
+ active_config = config or AgentConfig()
79
+ run_id = str(uuid4())
80
+ started_at = _utc_timestamp()
81
+ started_timer = time.perf_counter()
82
+ initial_state: AgentState = {
83
+ "run_id": run_id,
84
+ "started_at": started_at,
85
+ "completed_at": "",
86
+ "duration_seconds": "0.0000",
87
+ "goal": goal,
88
+ "config": asdict(active_config),
89
+ "plan": [],
90
+ "plan_validations": [],
91
+ "current_step": 0,
92
+ "answer": None,
93
+ "step_results": [],
94
+ "tool_calls": [],
95
+ "execution_attempts": [],
96
+ "verified": False,
97
+ "retry_count": 0,
98
+ "step_retry_count": 0,
99
+ "status": AgentStatus.RUNNING,
100
+ "errors": [],
101
+ "events": [],
102
+ "fault_plan": normalize_fault_plan(fault_plan or {}),
103
+ "fault_counters": {},
104
+ "reflection_notes": [],
105
+ "reflections": [],
106
+ "verification_results": [],
107
+ }
108
+ graph = build_agent_graph(active_config)
109
+ final_state = graph.invoke(initial_state)
110
+ final_state["run_id"] = run_id
111
+ final_state["started_at"] = started_at
112
+ final_state["completed_at"] = _utc_timestamp()
113
+ final_state["duration_seconds"] = f"{time.perf_counter() - started_timer:.4f}"
114
+ return final_state
115
+
116
+
117
+ def preview_plan(
118
+ goal: str,
119
+ *,
120
+ config: Optional[AgentConfig] = None,
121
+ ) -> Dict[str, Any]:
122
+ active_config = config or AgentConfig()
123
+ plan = _plan_goal(goal)
124
+ plan_validations = validate_plan_steps(plan)
125
+ errors = plan_errors(plan, active_config.max_steps)
126
+
127
+ return {
128
+ "status": "failed" if errors else "ready",
129
+ "goal": goal,
130
+ "plan": plan,
131
+ "plan_validations": plan_validations,
132
+ "errors": errors,
133
+ }
134
+
135
+
136
+ def _planner(state: AgentState) -> AgentState:
137
+ next_state = copy_agent_state(state)
138
+ next_state["plan"] = _plan_goal(next_state["goal"])
139
+ next_state["plan_validations"] = validate_plan_steps(next_state["plan"])
140
+ next_state["current_step"] = 0
141
+ errors = plan_errors(next_state["plan"], next_state["config"]["max_steps"])
142
+ if errors:
143
+ next_state["status"] = AgentStatus.FAILED
144
+ next_state["errors"].extend(errors)
145
+ else:
146
+ next_state["status"] = AgentStatus.RUNNING
147
+ record_node_event(next_state, "planner")
148
+ return next_state
149
+
150
+
151
+ def _executor(state: AgentState) -> AgentState:
152
+ next_state = copy_agent_state(state)
153
+ step = _current_step(next_state)
154
+ if step is None:
155
+ next_state["status"] = AgentStatus.FAILED
156
+ next_state["errors"].append("no executable step")
157
+ record_node_event(next_state, "executor")
158
+ return next_state
159
+
160
+ fault = _consume_fault(next_state, step)
161
+ if fault == "wrong-answer":
162
+ next_state["answer"] = "6"
163
+ record_execution_attempt(next_state, step, "", "6", fault)
164
+ elif fault == "empty-answer":
165
+ next_state["answer"] = ""
166
+ record_execution_attempt(next_state, step, "", "", fault)
167
+ elif fault == "tool-error":
168
+ next_state["answer"] = "TOOL_ERROR"
169
+ record_execution_attempt(next_state, step, "", "TOOL_ERROR", fault)
170
+ else:
171
+ execution = execute_step(step)
172
+ if execution is None:
173
+ next_state["answer"] = None
174
+ next_state["errors"].append(f"unsupported step: {step}")
175
+ record_execution_attempt(next_state, step, "", "None", "")
176
+ else:
177
+ next_state["answer"] = execution["output"]
178
+ next_state["tool_calls"].append(execution)
179
+ record_execution_attempt(
180
+ next_state,
181
+ step,
182
+ execution["tool"],
183
+ execution["output"],
184
+ "",
185
+ )
186
+
187
+ record_node_event(next_state, "executor")
188
+ return next_state
189
+
190
+
191
+ def _verifier(state: AgentState) -> AgentState:
192
+ next_state = copy_agent_state(state)
193
+ step = _current_step(next_state)
194
+ expected = expected_answer(step)
195
+ next_state["verified"] = bool(expected is not None and next_state.get("answer") == expected)
196
+ next_state["verification_results"].append(
197
+ {
198
+ "step": step or "",
199
+ "actual": str(next_state.get("answer")),
200
+ "expected": str(expected),
201
+ "passed": str(next_state["verified"]).lower(),
202
+ "retry": str(next_state["step_retry_count"]),
203
+ }
204
+ )
205
+
206
+ if next_state["verified"]:
207
+ if next_state.get("answer") is not None:
208
+ next_state["step_results"].append(next_state["answer"])
209
+ next_state["current_step"] = next_state["current_step"] + 1
210
+ next_state["step_retry_count"] = 0
211
+ if next_state["current_step"] >= len(next_state.get("plan", [])):
212
+ next_state["status"] = AgentStatus.DONE
213
+ else:
214
+ next_state["status"] = AgentStatus.RUNNING
215
+ elif next_state["step_retry_count"] >= next_state["config"]["max_retries"]:
216
+ next_state["status"] = AgentStatus.FAILED
217
+ next_state["errors"].append("retry budget exhausted")
218
+ else:
219
+ next_state["status"] = AgentStatus.RUNNING
220
+
221
+ record_node_event(next_state, "verifier")
222
+ return next_state
223
+
224
+
225
+ def _reflector(state: AgentState) -> AgentState:
226
+ next_state = copy_agent_state(state)
227
+ next_state["retry_count"] = next_state["retry_count"] + 1
228
+ next_state["step_retry_count"] = next_state["step_retry_count"] + 1
229
+ step = _current_step(next_state)
230
+ actual = str(next_state.get("answer"))
231
+ reason = _reflection_reason(actual)
232
+ next_state["reflection_notes"].append(_reflection_note(reason))
233
+ next_state["reflections"].append(
234
+ {
235
+ "step": step or "",
236
+ "actual": actual,
237
+ "expected": str(expected_answer(step)),
238
+ "retry": str(next_state["step_retry_count"]),
239
+ "reason": reason,
240
+ }
241
+ )
242
+ record_node_event(next_state, "reflector")
243
+ return next_state
244
+
245
+
246
+ def _route_after_planner(state: AgentState) -> str:
247
+ if state["status"] == AgentStatus.FAILED:
248
+ return "end"
249
+ return "execute"
250
+
251
+
252
+ def _route_after_verifier(state: AgentState) -> str:
253
+ if state["status"] in {AgentStatus.DONE, AgentStatus.FAILED}:
254
+ return "end"
255
+ if state.get("verified"):
256
+ return "execute"
257
+ return "reflect"
258
+
259
+
260
+ def _current_step(state: AgentState) -> Optional[str]:
261
+ plan = state.get("plan", [])
262
+ current_step = state.get("current_step", 0)
263
+ if current_step < 0 or current_step >= len(plan):
264
+ return None
265
+ return plan[current_step]
266
+
267
+
268
+ def _consume_fault(state: AgentState, step: str) -> Optional[str]:
269
+ fault_plan = state.get("fault_plan", {})
270
+ faults = fault_plan.get(step, [])
271
+ counters = state.setdefault("fault_counters", {})
272
+ index = counters.get(step, 0)
273
+ counters[step] = index + 1
274
+ if index >= len(faults):
275
+ return None
276
+ return faults[index]
277
+
278
+
279
+ def _utc_timestamp() -> str:
280
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
281
+
282
+
283
+ def _reflection_reason(actual: str) -> str:
284
+ if actual == "":
285
+ return "answer was empty"
286
+ if actual == "TOOL_ERROR":
287
+ return "tool execution failed"
288
+ return "answer did not match verifier expectation"
289
+
290
+
291
+ def _reflection_note(reason: str) -> str:
292
+ if reason == "answer was empty":
293
+ return "executor returned an empty answer; retry the same planned step"
294
+ if reason == "tool execution failed":
295
+ return "tool execution failed; retry the same planned step"
296
+ return "executor returned an unverifiable answer; retry with stricter arithmetic"
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable
4
+
5
+ SUPPORTED_FAULTS = {"empty-answer", "tool-error", "wrong-answer"}
6
+
7
+
8
+ def validate_faults(faults: Iterable[str]) -> None:
9
+ for fault in faults:
10
+ if fault not in SUPPORTED_FAULTS:
11
+ raise ValueError(f"unsupported fault: {fault}")
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from enum import Enum
5
+ from typing import Any, Dict, List
6
+
7
+
8
+ def validate_run_invariants(state: Dict[str, Any]) -> List[str]:
9
+ issues = []
10
+ events = state.get("events", [])
11
+ node_counts = Counter(event["node"] for event in events)
12
+ status = _value(state.get("status"))
13
+
14
+ if len(state.get("plan_validations", [])) != len(state.get("plan", [])):
15
+ issues.append("plan_validations count does not match plan")
16
+
17
+ if node_counts["executor"] != len(state.get("execution_attempts", [])):
18
+ issues.append("executor event count does not match execution_attempts")
19
+
20
+ if node_counts["verifier"] != len(state.get("verification_results", [])):
21
+ issues.append("verifier event count does not match verification_results")
22
+
23
+ if node_counts["reflector"] != len(state.get("reflections", [])):
24
+ issues.append("reflector event count does not match reflections")
25
+
26
+ if state.get("retry_count", 0) != len(state.get("reflections", [])):
27
+ issues.append("retry_count does not match reflections")
28
+
29
+ if state.get("current_step", 0) != len(state.get("step_results", [])):
30
+ issues.append("current_step does not match completed step_results")
31
+
32
+ if status == "done" and state.get("current_step", 0) != len(state.get("plan", [])):
33
+ issues.append("done status does not match completed plan")
34
+
35
+ failed_verifications = [
36
+ item for item in state.get("verification_results", []) if item["passed"] == "false"
37
+ ]
38
+ if status == "done" and len(failed_verifications) != len(state.get("reflections", [])):
39
+ issues.append("done run failed verification count does not match reflections")
40
+
41
+ return issues
42
+
43
+
44
+ def _value(value: Any) -> Any:
45
+ if isinstance(value, Enum):
46
+ return value.value
47
+ return value
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+
6
+ def normalize_goal(goal: str) -> str:
7
+ return " then ".join(plan_goal(goal))
8
+
9
+
10
+ def plan_goal(goal: str) -> List[str]:
11
+ return [
12
+ normalized
13
+ for part in _split_goal_steps(goal)
14
+ if (normalized := _normalize_step(part))
15
+ ]
16
+
17
+
18
+ def _normalize_step(step: str) -> str:
19
+ quote = None
20
+ output = []
21
+ previous_space = False
22
+ for char in step.strip():
23
+ if char in {"'", '"'}:
24
+ if quote == char:
25
+ quote = None
26
+ elif quote is None:
27
+ quote = char
28
+ output.append(char)
29
+ previous_space = False
30
+ elif quote is not None:
31
+ output.append(char)
32
+ elif char.isspace():
33
+ if output and not previous_space:
34
+ output.append(" ")
35
+ previous_space = True
36
+ else:
37
+ output.append(char.lower())
38
+ previous_space = False
39
+ return "".join(output).strip()
40
+
41
+
42
+ def _split_goal_steps(goal: str) -> List[str]:
43
+ stripped = goal.strip()
44
+ if not stripped:
45
+ return []
46
+
47
+ parts = []
48
+ buffer = []
49
+ quote = None
50
+ index = 0
51
+ while index < len(stripped):
52
+ char = stripped[index]
53
+ if char in {"'", '"'}:
54
+ if quote == char:
55
+ quote = None
56
+ elif quote is None:
57
+ quote = char
58
+ buffer.append(char)
59
+ index += 1
60
+ elif quote is None and char.isspace():
61
+ next_index = index
62
+ while next_index < len(stripped) and stripped[next_index].isspace():
63
+ next_index += 1
64
+ after_then = next_index + 4
65
+ if (
66
+ stripped[next_index:after_then].lower() == "then"
67
+ and (after_then == len(stripped) or stripped[after_then].isspace())
68
+ ):
69
+ while after_then < len(stripped) and stripped[after_then].isspace():
70
+ after_then += 1
71
+ parts.append("".join(buffer))
72
+ buffer = []
73
+ index = after_then
74
+ else:
75
+ buffer.append(" ")
76
+ index = next_index
77
+ else:
78
+ buffer.append(char)
79
+ index += 1
80
+ parts.append("".join(buffer))
81
+ return parts
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List, Optional
4
+
5
+ from kagent.core.faults import validate_faults
6
+ from kagent.core.normalization import normalize_goal
7
+ from kagent.core.tools import expected_answer, matching_tool_name
8
+
9
+
10
+ def normalize_fault_plan(fault_plan: Dict[str, List[str]]) -> Dict[str, List[str]]:
11
+ for faults in fault_plan.values():
12
+ validate_faults(faults)
13
+ return {
14
+ normalize_goal(step): list(faults)
15
+ for step, faults in fault_plan.items()
16
+ }
17
+
18
+
19
+ def plan_errors(plan: List[str], max_steps: int) -> List[str]:
20
+ unsupported_step = _unsupported_planned_step(plan)
21
+ if not plan:
22
+ return ["empty plan"]
23
+ if len(plan) > max_steps:
24
+ return ["planned steps exceed max_steps"]
25
+ if unsupported_step is not None:
26
+ return [f"unsupported planned step: {unsupported_step}"]
27
+ return []
28
+
29
+
30
+ def validate_plan_steps(plan: List[str]) -> List[Dict[str, str]]:
31
+ validations = []
32
+ for step in plan:
33
+ tool_name = matching_tool_name(step)
34
+ validations.append(
35
+ {
36
+ "step": step,
37
+ "supported": str(tool_name is not None).lower(),
38
+ "tool": tool_name or "",
39
+ }
40
+ )
41
+ return validations
42
+
43
+
44
+ def _unsupported_planned_step(plan: List[str]) -> Optional[str]:
45
+ for step in plan:
46
+ if expected_answer(step) is None:
47
+ return step
48
+ return None
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from os import environ
6
+ from typing import Any, Dict, List, Mapping, Optional, TypedDict
7
+
8
+
9
+ class AgentStatus(str, Enum):
10
+ RUNNING = "running"
11
+ DONE = "done"
12
+ FAILED = "failed"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class AgentConfig:
17
+ max_steps: int = 6
18
+ max_retries: int = 2
19
+
20
+ @classmethod
21
+ def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "AgentConfig":
22
+ source = env if env is not None else environ
23
+ return cls(
24
+ max_steps=_env_int(source, "KAGENT_MAX_STEPS", cls.max_steps),
25
+ max_retries=_env_int(
26
+ source,
27
+ "KAGENT_MAX_RETRIES",
28
+ cls.max_retries,
29
+ ),
30
+ )
31
+
32
+ def __post_init__(self) -> None:
33
+ if self.max_steps < 1:
34
+ raise ValueError("max_steps must be at least 1")
35
+ if self.max_retries < 0:
36
+ raise ValueError("max_retries must be non-negative")
37
+
38
+
39
+ class AgentState(TypedDict, total=False):
40
+ run_id: str
41
+ started_at: str
42
+ completed_at: str
43
+ duration_seconds: str
44
+ goal: str
45
+ config: Dict[str, int]
46
+ plan: List[str]
47
+ plan_validations: List[Dict[str, str]]
48
+ current_step: int
49
+ answer: Optional[str]
50
+ step_results: List[str]
51
+ tool_calls: List[Dict[str, str]]
52
+ execution_attempts: List[Dict[str, str]]
53
+ verified: bool
54
+ retry_count: int
55
+ step_retry_count: int
56
+ status: AgentStatus
57
+ errors: List[str]
58
+ events: List[Dict[str, Any]]
59
+ fault_plan: Dict[str, List[str]]
60
+ fault_counters: Dict[str, int]
61
+ reflection_notes: List[str]
62
+ reflections: List[Dict[str, str]]
63
+ verification_results: List[Dict[str, str]]
64
+
65
+
66
+ def _env_int(env: Mapping[str, str], name: str, default: int) -> int:
67
+ value = env.get(name)
68
+ if value in {None, ""}:
69
+ return default
70
+ try:
71
+ return int(value)
72
+ except ValueError as exc:
73
+ raise ValueError(f"{name} must be an integer") from exc
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from enum import Enum
5
+ from typing import Any, Dict, List
6
+
7
+
8
+ def summarize_run(state: Dict[str, Any]) -> Dict[str, Any]:
9
+ node_counts = Counter(event["node"] for event in state.get("events", []))
10
+ status = _json_value(state.get("status"))
11
+ tool_calls = state.get("tool_calls", [])
12
+ failed_verifications = [
13
+ item for item in state.get("verification_results", []) if item["passed"] == "false"
14
+ ]
15
+ reflection_reasons = [
16
+ item["reason"] for item in state.get("reflections", [])
17
+ ]
18
+ reflection_reason_counts = Counter(reflection_reasons)
19
+ faults = [
20
+ item["fault"]
21
+ for item in state.get("execution_attempts", [])
22
+ if item.get("fault")
23
+ ]
24
+ return {
25
+ "status": status,
26
+ "answer": state.get("answer"),
27
+ "run_id": state.get("run_id", ""),
28
+ "started_at": state.get("started_at", ""),
29
+ "completed_at": state.get("completed_at", ""),
30
+ "duration_seconds": state.get("duration_seconds", "0.0000"),
31
+ "planned_steps": str(len(state.get("plan", []))),
32
+ "completed_steps": str(state.get("current_step", 0)),
33
+ "retry_count": str(state.get("retry_count", 0)),
34
+ "failed_verifications": str(len(failed_verifications)),
35
+ "recovered": str(status == "done" and bool(failed_verifications)).lower(),
36
+ "reflection_reasons": reflection_reasons,
37
+ "reflection_reason_counts": {
38
+ reason: str(count)
39
+ for reason, count in sorted(reflection_reason_counts.items())
40
+ },
41
+ "tool_call_count": str(len(tool_calls)),
42
+ "tool_names": _unique_tool_names(tool_calls),
43
+ "node_counts": {
44
+ node: str(count)
45
+ for node, count in sorted(node_counts.items())
46
+ },
47
+ "faults": faults,
48
+ "errors": list(state.get("errors", [])),
49
+ }
50
+
51
+
52
+ def _unique_tool_names(tool_calls: List[Dict[str, str]]) -> List[str]:
53
+ names = []
54
+ for call in tool_calls:
55
+ name = call["tool"]
56
+ if name not in names:
57
+ names.append(name)
58
+ return names
59
+
60
+
61
+ def _json_value(value: Any) -> Any:
62
+ if isinstance(value, Enum):
63
+ return value.value
64
+ return value