@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 @@
1
+
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from kagent.runtime.agent import (
4
+ MAX_PLANNER_OBSERVATION_STRING_CHARS,
5
+ RUNTIME_TRACE_TYPE,
6
+ build_runtime_graph,
7
+ run_runtime_agent,
8
+ runtime_topology,
9
+ )
10
+ from kagent.runtime.approval import build_resumable_plan
11
+ from kagent.runtime.cancellation import RuntimeCancellationToken
12
+ from kagent.runtime.hooks import RuntimeHookChain, RuntimeHookDecision
13
+ from kagent.runtime.steering import RuntimeSteeringBuffer
14
+ from kagent.runtime.steps import derive_runtime_steps
15
+
16
+ __all__ = [
17
+ "MAX_PLANNER_OBSERVATION_STRING_CHARS",
18
+ "RUNTIME_TRACE_TYPE",
19
+ "build_runtime_graph",
20
+ "build_resumable_plan",
21
+ "derive_runtime_steps",
22
+ "runtime_topology",
23
+ "RuntimeHookChain",
24
+ "RuntimeHookDecision",
25
+ "RuntimeCancellationToken",
26
+ "RuntimeSteeringBuffer",
27
+ "run_runtime_agent",
28
+ ]
@@ -0,0 +1,543 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import time
6
+ from typing import Any, Dict, List
7
+ from uuid import uuid4
8
+
9
+ from kagent.runtime.checkpoint_state import (
10
+ RuntimeGraphContext,
11
+ RuntimeGraphState,
12
+ append_graph_phase,
13
+ checkpoint_plan_projection,
14
+ checkpoint_safe_value,
15
+ timing_fields,
16
+ utc_timestamp,
17
+ )
18
+ from kagent.runtime.policy import RuntimePolicy
19
+ from kagent.runtime.presentation import project_runtime_presentation
20
+ from kagent.runtime.tools import default_runtime_tools, execute_runtime_tool
21
+ from kagent.runtime.types import AgentObservation, parse_agent_plan
22
+
23
+
24
+ def route_after_planner(state: RuntimeGraphState) -> str:
25
+ planner = state.get("initial_planner")
26
+ if not isinstance(planner, dict) or planner.get("status") != "ok":
27
+ return "runtime_loop"
28
+ plan = planner.get("plan")
29
+ actions = plan.get("actions") if isinstance(plan, dict) else None
30
+ return (
31
+ "prepare_action"
32
+ if isinstance(actions, list) and len(actions) == 1
33
+ else "runtime_loop"
34
+ )
35
+
36
+
37
+ def route_after_prepare_action(state: RuntimeGraphState) -> str:
38
+ prepared = state.get("initial_action_prepared")
39
+ if isinstance(prepared, dict) and prepared.get("status") == "prepared":
40
+ return "mark_action_executing"
41
+ return "runtime_loop"
42
+
43
+
44
+ def route_after_mark_action(state: RuntimeGraphState) -> str:
45
+ phase = state.get("initial_action_phase")
46
+ if (
47
+ not state.get("initial_action_outcome")
48
+ and isinstance(phase, dict)
49
+ and phase.get("status") == "executing"
50
+ ):
51
+ return "execute_action"
52
+ return "runtime_loop"
53
+
54
+
55
+ def _planner_plan_for_action(
56
+ state: RuntimeGraphState,
57
+ context: RuntimeGraphContext,
58
+ ) -> tuple[Dict[str, Any] | None, str]:
59
+ planner = state.get("initial_planner")
60
+ if not isinstance(planner, dict) or planner.get("status") != "ok":
61
+ return None, "planner checkpoint is missing plan state"
62
+ run_id = str(state.get("run_id", ""))
63
+ cache_entry = (context.get("planner_plan_cache") or {}).get(run_id)
64
+ if (
65
+ isinstance(cache_entry, dict)
66
+ and cache_entry.get("token") == planner.get("plan_cache_token")
67
+ and isinstance(cache_entry.get("plan"), dict)
68
+ ):
69
+ return copy.deepcopy(cache_entry["plan"]), ""
70
+ plan = planner.get("plan")
71
+ if planner.get("plan_redacted"):
72
+ return None, (
73
+ "planner checkpoint contains sensitive tool input; "
74
+ "the original in-memory plan is unavailable"
75
+ )
76
+ if not isinstance(plan, dict):
77
+ return None, "planner checkpoint is missing plan state"
78
+ return copy.deepcopy(plan), ""
79
+
80
+
81
+ def _ensure_runtime_tools(context: RuntimeGraphContext) -> None:
82
+ if "tools" in context:
83
+ return
84
+ context["tools"] = default_runtime_tools(
85
+ runtime_workspace_dir=context.get("runtime_workspace_dir", ""),
86
+ redis_url=context.get("redis_url", ""),
87
+ milvus_url=context.get("milvus_url", ""),
88
+ embedding_base_url=context.get("embedding_base_url", ""),
89
+ embedding_api_key=context.get("embedding_api_key", ""),
90
+ embedding_model=context.get("embedding_model", ""),
91
+ embedding_timeout_seconds=context.get("embedding_timeout_seconds", 30.0),
92
+ embedding_max_retries=context.get("embedding_max_retries", 2),
93
+ embedding_retry_backoff_seconds=context.get(
94
+ "embedding_retry_backoff_seconds",
95
+ 0.25,
96
+ ),
97
+ external_backend_timeout_seconds=context.get(
98
+ "external_backend_timeout_seconds",
99
+ 2.0,
100
+ ),
101
+ )
102
+
103
+
104
+ def prepare_action_graph_node(
105
+ state: RuntimeGraphState,
106
+ runtime: Any,
107
+ ) -> RuntimeGraphState:
108
+ started_at = utc_timestamp()
109
+ started_timer = time.perf_counter()
110
+ context: RuntimeGraphContext = runtime.context or {}
111
+ _ensure_runtime_tools(context)
112
+ if context.get("hooks"):
113
+ return {
114
+ "initial_action_prepared": {"status": "legacy"},
115
+ "graph_phases": append_graph_phase(
116
+ state.get("graph_phases"),
117
+ "prepare_action",
118
+ started_at,
119
+ started_timer,
120
+ ),
121
+ }
122
+ plan_payload, plan_error = _planner_plan_for_action(state, context)
123
+ if plan_payload is None:
124
+ return {
125
+ "initial_action_outcome": _failed_initial_action_outcome(
126
+ action_id="",
127
+ tool="planner",
128
+ error_code="planner_checkpoint_sensitive_input",
129
+ error=plan_error,
130
+ started_at=started_at,
131
+ started_timer=started_timer,
132
+ ),
133
+ "initial_action_prepared": {"status": "failed"},
134
+ "graph_phases": append_graph_phase(
135
+ state.get("graph_phases"),
136
+ "prepare_action",
137
+ started_at,
138
+ started_timer,
139
+ ),
140
+ }
141
+ plan = parse_agent_plan(json.dumps(plan_payload, ensure_ascii=False))
142
+ if len(plan.actions) != 1 or plan.actions[0].depends_on:
143
+ return {
144
+ "initial_action_prepared": {"status": "legacy"},
145
+ "graph_phases": append_graph_phase(
146
+ state.get("graph_phases"),
147
+ "prepare_action",
148
+ started_at,
149
+ started_timer,
150
+ ),
151
+ }
152
+ action = plan.actions[0]
153
+ cancellation_token = context.get("cancellation_token")
154
+ if cancellation_token is not None and cancellation_token.is_cancelled():
155
+ return {
156
+ "initial_action_prepared": {"status": "legacy"},
157
+ "graph_phases": append_graph_phase(
158
+ state.get("graph_phases"),
159
+ "prepare_action",
160
+ started_at,
161
+ started_timer,
162
+ ),
163
+ }
164
+ steering_buffer = context.get("steering_buffer")
165
+ if steering_buffer is not None and steering_buffer.pending():
166
+ return {
167
+ "initial_action_prepared": {"status": "legacy"},
168
+ "graph_phases": append_graph_phase(
169
+ state.get("graph_phases"),
170
+ "prepare_action",
171
+ started_at,
172
+ started_timer,
173
+ ),
174
+ }
175
+ resolved_input = copy.deepcopy(action.input)
176
+ active_policy = context.get("policy") or RuntimePolicy()
177
+ policy_started_at = utc_timestamp()
178
+ policy_timer = time.perf_counter()
179
+ decision = active_policy.authorize(action.tool, resolved_input)
180
+ if decision.status != "allowed":
181
+ return {
182
+ "initial_action_prepared": {"status": "legacy"},
183
+ "graph_phases": append_graph_phase(
184
+ state.get("graph_phases"),
185
+ "prepare_action",
186
+ started_at,
187
+ started_timer,
188
+ ),
189
+ }
190
+ policy_event = {
191
+ "node": "policy",
192
+ "action_id": action.id,
193
+ "tool": action.tool,
194
+ "status": "allowed",
195
+ "reason": decision.reason,
196
+ "iteration": "1",
197
+ **timing_fields(policy_started_at, policy_timer),
198
+ }
199
+ progress_events, sink_failures = _append_checkpoint_progress(
200
+ state,
201
+ context,
202
+ {
203
+ "type": "policy_completed",
204
+ "iteration": "1",
205
+ "node": "policy",
206
+ "action_id": action.id,
207
+ "tool": action.tool,
208
+ "status": "allowed",
209
+ "reason": decision.reason,
210
+ "duration_seconds": policy_event["duration_seconds"],
211
+ },
212
+ )
213
+ projected_input, input_redacted = checkpoint_plan_projection(resolved_input)
214
+ projected_action, action_redacted = checkpoint_plan_projection(action.to_dict())
215
+ cache_token = str(uuid4())
216
+ prepared_cache = context.setdefault("prepared_action_cache", {})
217
+ prepared_cache[str(state.get("run_id", ""))] = {
218
+ "token": cache_token,
219
+ "action": action.to_dict(),
220
+ "input": resolved_input,
221
+ }
222
+ return {
223
+ "initial_events": [*(state.get("initial_events") or []), policy_event],
224
+ "initial_progress_events": progress_events,
225
+ "initial_progress_event_sink_failure_count": sink_failures,
226
+ "initial_action_prepared": {
227
+ "status": "prepared",
228
+ "action": projected_action,
229
+ "input": projected_input,
230
+ "input_redacted": input_redacted or action_redacted,
231
+ "cache_token": cache_token,
232
+ },
233
+ "graph_phases": append_graph_phase(
234
+ state.get("graph_phases"),
235
+ "prepare_action",
236
+ started_at,
237
+ started_timer,
238
+ ),
239
+ }
240
+
241
+
242
+ def mark_action_graph_node(
243
+ state: RuntimeGraphState,
244
+ runtime: Any,
245
+ ) -> RuntimeGraphState:
246
+ started_at = utc_timestamp()
247
+ started_timer = time.perf_counter()
248
+ context: RuntimeGraphContext = runtime.context or {}
249
+ run_id = str(state.get("run_id", ""))
250
+ prepared = state.get("initial_action_prepared") or {}
251
+ cancellation_token = context.get("cancellation_token")
252
+ steering_buffer = context.get("steering_buffer")
253
+ if (
254
+ cancellation_token is not None and cancellation_token.is_cancelled()
255
+ ) or (steering_buffer is not None and steering_buffer.pending()):
256
+ prepared_cache = context.get("prepared_action_cache")
257
+ if isinstance(prepared_cache, dict):
258
+ prepared_cache.pop(run_id, None)
259
+ return {
260
+ "initial_action_phase": {"status": "legacy"},
261
+ "graph_phases": append_graph_phase(
262
+ state.get("graph_phases"),
263
+ "mark_action_executing",
264
+ started_at,
265
+ started_timer,
266
+ ),
267
+ }
268
+ cache_entry = (context.get("prepared_action_cache") or {}).get(run_id)
269
+ if not (
270
+ isinstance(cache_entry, dict)
271
+ and cache_entry.get("token") == prepared.get("cache_token")
272
+ and isinstance(cache_entry.get("action"), dict)
273
+ and isinstance(cache_entry.get("input"), dict)
274
+ ):
275
+ outcome = _failed_initial_action_outcome(
276
+ action_id=str((prepared.get("action") or {}).get("id", "")),
277
+ tool=str((prepared.get("action") or {}).get("tool", "")),
278
+ error_code="planner_checkpoint_sensitive_input",
279
+ error="prepared action input is unavailable",
280
+ started_at=started_at,
281
+ started_timer=started_timer,
282
+ )
283
+ observation = outcome["observation"]
284
+ checkpoint_event = {
285
+ "node": "checkpoint",
286
+ "action_id": observation["action_id"],
287
+ "tool": observation["tool"],
288
+ "status": "failed",
289
+ "error_code": observation["error_code"],
290
+ "started_at": observation["started_at"],
291
+ "completed_at": observation["completed_at"],
292
+ "duration_seconds": observation["duration_seconds"],
293
+ }
294
+ return {
295
+ "initial_action_outcome": outcome,
296
+ "initial_events": [
297
+ *(state.get("initial_events") or []),
298
+ checkpoint_event,
299
+ ],
300
+ "graph_phases": append_graph_phase(
301
+ state.get("graph_phases"),
302
+ "mark_action_executing",
303
+ started_at,
304
+ started_timer,
305
+ ),
306
+ }
307
+ execution_token = str(uuid4())
308
+ executing_cache = context.setdefault("executing_action_cache", {})
309
+ executing_cache[run_id] = {
310
+ "token": execution_token,
311
+ "action": copy.deepcopy(cache_entry["action"]),
312
+ "input": copy.deepcopy(cache_entry["input"]),
313
+ }
314
+ context["prepared_action_cache"].pop(run_id, None)
315
+ return {
316
+ "initial_action_phase": {
317
+ "status": "executing",
318
+ "action_id": str(cache_entry["action"].get("id", "")),
319
+ "tool": str(cache_entry["action"].get("tool", "")),
320
+ "execution_token": execution_token,
321
+ },
322
+ "graph_phases": append_graph_phase(
323
+ state.get("graph_phases"),
324
+ "mark_action_executing",
325
+ started_at,
326
+ started_timer,
327
+ ),
328
+ }
329
+
330
+
331
+ def execute_action_graph_node(
332
+ state: RuntimeGraphState,
333
+ runtime: Any,
334
+ ) -> RuntimeGraphState:
335
+ started_at = utc_timestamp()
336
+ started_timer = time.perf_counter()
337
+ context: RuntimeGraphContext = runtime.context or {}
338
+ run_id = str(state.get("run_id", ""))
339
+ phase = state.get("initial_action_phase") or {}
340
+ cache_entry = (context.get("executing_action_cache") or {}).get(run_id)
341
+ if not (
342
+ isinstance(cache_entry, dict)
343
+ and cache_entry.get("token") == phase.get("execution_token")
344
+ and isinstance(cache_entry.get("action"), dict)
345
+ and isinstance(cache_entry.get("input"), dict)
346
+ ):
347
+ outcome = _failed_initial_action_outcome(
348
+ action_id=str(phase.get("action_id", "")),
349
+ tool=str(phase.get("tool", "")),
350
+ error_code="approval_execution_interrupted",
351
+ error=(
352
+ "action execution was interrupted; side-effect state is uncertain"
353
+ ),
354
+ started_at=started_at,
355
+ started_timer=started_timer,
356
+ )
357
+ observation = outcome["observation"]
358
+ executor_event = {
359
+ "node": "executor",
360
+ "action_id": observation["action_id"],
361
+ "tool": observation["tool"],
362
+ "status": "failed",
363
+ "iteration": "1",
364
+ "error_code": observation["error_code"],
365
+ "started_at": observation["started_at"],
366
+ "completed_at": observation["completed_at"],
367
+ "duration_seconds": observation["duration_seconds"],
368
+ }
369
+ progress_events, sink_failures = _append_checkpoint_progress(
370
+ state,
371
+ context,
372
+ {
373
+ "type": "tool_completed",
374
+ "iteration": "1",
375
+ "node": "executor",
376
+ "action_id": observation["action_id"],
377
+ "tool": observation["tool"],
378
+ "status": "failed",
379
+ "error_code": observation["error_code"],
380
+ "duration_seconds": observation["duration_seconds"],
381
+ },
382
+ )
383
+ return {
384
+ "initial_action_outcome": outcome,
385
+ "initial_events": [*(state.get("initial_events") or []), executor_event],
386
+ "initial_progress_events": progress_events,
387
+ "initial_progress_event_sink_failure_count": sink_failures,
388
+ "graph_phases": append_graph_phase(
389
+ state.get("graph_phases"),
390
+ "execute_action",
391
+ started_at,
392
+ started_timer,
393
+ ),
394
+ }
395
+ cancellation_token = context.get("cancellation_token")
396
+ steering_buffer = context.get("steering_buffer")
397
+ if (
398
+ cancellation_token is not None and cancellation_token.is_cancelled()
399
+ ) or (steering_buffer is not None and steering_buffer.pending()):
400
+ context["executing_action_cache"].pop(run_id, None)
401
+ return {
402
+ "initial_action_phase": {"status": "legacy"},
403
+ "graph_phases": append_graph_phase(
404
+ state.get("graph_phases"),
405
+ "execute_action",
406
+ started_at,
407
+ started_timer,
408
+ ),
409
+ }
410
+ context["executing_action_cache"].pop(run_id, None)
411
+ planner_cache = context.get("planner_plan_cache")
412
+ if isinstance(planner_cache, dict):
413
+ planner_cache.pop(run_id, None)
414
+ action = cache_entry["action"]
415
+ action_id = str(action.get("id", ""))
416
+ tool_name = str(action.get("tool", ""))
417
+ progress_events, sink_failures = _append_checkpoint_progress(
418
+ state,
419
+ context,
420
+ {
421
+ "type": "tool_started",
422
+ "iteration": "1",
423
+ "node": "executor",
424
+ "action_id": action_id,
425
+ "tool": tool_name,
426
+ "status": "started",
427
+ },
428
+ )
429
+ observation = execute_runtime_tool(
430
+ context.get("tools") or {},
431
+ tool_name,
432
+ cache_entry["input"],
433
+ action_id=action_id,
434
+ )
435
+ executor_event = {
436
+ "node": "executor",
437
+ "action_id": action_id,
438
+ "tool": tool_name,
439
+ "status": observation.status,
440
+ "iteration": "1",
441
+ "started_at": observation.started_at,
442
+ "completed_at": observation.completed_at,
443
+ "duration_seconds": observation.duration_seconds,
444
+ }
445
+ presentation = project_runtime_presentation(
446
+ tool_name,
447
+ observation.status,
448
+ observation.output,
449
+ )
450
+ progress_events, sink_failures = _append_checkpoint_progress_values(
451
+ progress_events,
452
+ sink_failures,
453
+ state,
454
+ context,
455
+ {
456
+ "type": "tool_completed",
457
+ "iteration": "1",
458
+ "node": "executor",
459
+ "action_id": action_id,
460
+ "tool": tool_name,
461
+ "status": observation.status,
462
+ "error_code": observation.error_code,
463
+ "duration_seconds": observation.duration_seconds,
464
+ "presentation": presentation or None,
465
+ },
466
+ )
467
+ return {
468
+ "initial_events": [*(state.get("initial_events") or []), executor_event],
469
+ "initial_progress_events": progress_events,
470
+ "initial_progress_event_sink_failure_count": sink_failures,
471
+ "initial_action_outcome": {
472
+ "status": observation.status,
473
+ "observation": checkpoint_safe_value(observation.to_dict()),
474
+ "should_replan": (
475
+ observation.status != "ok" and state.get("max_iterations", 1) > 1
476
+ ),
477
+ },
478
+ "graph_phases": append_graph_phase(
479
+ state.get("graph_phases"),
480
+ "execute_action",
481
+ started_at,
482
+ started_timer,
483
+ ),
484
+ }
485
+
486
+
487
+ def _failed_initial_action_outcome(
488
+ *,
489
+ action_id: str,
490
+ tool: str,
491
+ error_code: str,
492
+ error: str,
493
+ started_at: str,
494
+ started_timer: float,
495
+ ) -> Dict[str, Any]:
496
+ timing = timing_fields(started_at, started_timer)
497
+ return {
498
+ "status": "failed",
499
+ "observation": AgentObservation(
500
+ action_id=action_id,
501
+ tool=tool,
502
+ status="failed",
503
+ output={},
504
+ error_code=error_code,
505
+ error=error,
506
+ started_at=started_at,
507
+ completed_at=timing["completed_at"],
508
+ duration_seconds=timing["duration_seconds"],
509
+ ).to_dict(),
510
+ "should_replan": False,
511
+ }
512
+
513
+
514
+ def _append_checkpoint_progress(
515
+ state: RuntimeGraphState,
516
+ context: RuntimeGraphContext,
517
+ event: Dict[str, Any],
518
+ ) -> tuple[List[Dict[str, Any]], int]:
519
+ return _append_checkpoint_progress_values(
520
+ list(state.get("initial_progress_events") or []),
521
+ state.get("initial_progress_event_sink_failure_count", 0),
522
+ state,
523
+ context,
524
+ event,
525
+ )
526
+
527
+
528
+ def _append_checkpoint_progress_values(
529
+ progress_events: List[Dict[str, Any]],
530
+ sink_failures: int,
531
+ state: RuntimeGraphState,
532
+ context: RuntimeGraphContext,
533
+ event: Dict[str, Any],
534
+ ) -> tuple[List[Dict[str, Any]], int]:
535
+ event_with_run_id = {"run_id": str(state.get("run_id", "")), **event}
536
+ progress_events.append(checkpoint_safe_value(event_with_run_id))
537
+ event_sink = context.get("event_sink")
538
+ if event_sink is not None:
539
+ try:
540
+ event_sink(event_with_run_id)
541
+ except Exception:
542
+ sink_failures += 1
543
+ return progress_events, sink_failures