@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
|
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
|