@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,2089 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import re
6
+ import time
7
+ import warnings
8
+ from contextlib import nullcontext
9
+ from typing import Any, Callable, Dict, List, Optional, Set
10
+ from uuid import uuid4
11
+
12
+ import kagent.runtime.checkpoint_state as checkpoint_state
13
+ from kagent.runtime.action_graph import (
14
+ execute_action_graph_node,
15
+ mark_action_graph_node,
16
+ prepare_action_graph_node,
17
+ route_after_mark_action,
18
+ route_after_planner,
19
+ route_after_prepare_action,
20
+ )
21
+ from kagent.runtime.cancellation import RuntimeCancellationToken
22
+ from kagent.runtime.context import RuntimeContextManager
23
+ from kagent.runtime.hooks import RuntimeHookChain
24
+ from kagent.runtime.metadata import (
25
+ validate_runtime_metadata,
26
+ validate_runtime_tags,
27
+ )
28
+ from kagent.runtime.policy import RuntimePolicy
29
+ from kagent.runtime.presentation import project_runtime_presentation
30
+ from kagent.runtime.redaction import redact_runtime_payload, redact_runtime_text
31
+ from kagent.runtime.steering import RuntimeSteeringBuffer
32
+ from kagent.runtime.steps import derive_runtime_steps
33
+ from kagent.runtime.tools import (
34
+ RuntimeToolSpec,
35
+ default_runtime_tools,
36
+ execute_runtime_tool,
37
+ runtime_tool_metadata,
38
+ )
39
+ from kagent.runtime.types import (
40
+ MAX_ACTION_REASON_CHARS,
41
+ MAX_PLAN_ACTIONS,
42
+ MAX_PLAN_FINAL_ANSWER_CHARS,
43
+ AgentObservation,
44
+ parse_agent_plan,
45
+ )
46
+
47
+ RuntimeEventSink = checkpoint_state.RuntimeEventSink
48
+ RuntimeGraphContext = checkpoint_state.RuntimeGraphContext
49
+ RuntimeGraphState = checkpoint_state.RuntimeGraphState
50
+ _append_graph_phase = checkpoint_state.append_graph_phase
51
+ _checkpoint_plan_projection = checkpoint_state.checkpoint_plan_projection
52
+ _checkpoint_safe_value = checkpoint_state.checkpoint_safe_value
53
+ _duration_since = checkpoint_state.duration_since
54
+ _timing_fields = checkpoint_state.timing_fields
55
+ _utc_timestamp = checkpoint_state.utc_timestamp
56
+
57
+ _SYSTEM_PROMPT = (
58
+ """You are a production agent planner.
59
+ Your product identity is "kagent", a non-coding automation agent that runs
60
+ inside the user's current CLI or service process.
61
+ Never answer user identity, deployment, ownership, or hosting questions as if
62
+ you are the underlying model provider. Do not claim to be Qwen, ChatGPT,
63
+ Claude, or any other model brand unless the user explicitly asks about the
64
+ configured provider. In user-facing answers, do not expose provider details
65
+ unless the user explicitly asks about provider configuration.
66
+ Do not compare kagent to another assistant, coding tool, or runtime brand in
67
+ user-facing answers. Describe kagent directly.
68
+ Return strict JSON only with this shape:
69
+ {"actions":[{"id":"step-1","tool":"note","input":{"text":"..."},"reason":"..."}],
70
+ "final_answer":"..."}
71
+ Use only tools that are available to you.
72
+ """
73
+ f"Return at most {MAX_PLAN_ACTIONS} actions in one plan.\n"
74
+ f"Keep action reason at most {MAX_ACTION_REASON_CHARS} characters.\n"
75
+ f"Keep final_answer at most {MAX_PLAN_FINAL_ANSWER_CHARS} characters; "
76
+ "use artifact for long-form deliverables.\n"
77
+ "Use optional depends_on with prior action IDs when one action depends on earlier output. "
78
+ "Reference dependency output inside input with "
79
+ '{"$from_action":"step-1","pointer":"/field"}; pointer is a JSON Pointer.\n'
80
+ "Use open_app to open a local macOS application by application name. "
81
+ "Use open_url to open a browser page; use http_request only to fetch URL "
82
+ "content as an observation.\n"
83
+ "Use list_files and read_file to observe workspace state before changing "
84
+ "workspace files with apply_patch.\n"
85
+ "Use patch_history before revert_patch. Revert only when the user requests "
86
+ "undo or rollback, and pass the exact checkpoint ID and paths returned by "
87
+ "patch_history so the reviewed change receives approval.\n"
88
+ "Use delegate_task to hand off a bounded independent subtask to a child "
89
+ "kagent runtime; keep delegated goals specific and self-contained.\n"
90
+ "Use skill_list and skill_get when the task may benefit from installed "
91
+ "runtime skills or reusable operating procedures.\n"
92
+ "Use memory_put and memory_get for configured Redis short-term memory. "
93
+ "Use memory_remember and memory_recall for configured text-based long-term "
94
+ "semantic memory. Use memory_upsert and memory_search only when you have "
95
+ "explicit embedding vectors.\n"
96
+ "Use workspace_history and workspace_diff when reviewing virtual workspace changes, "
97
+ "policy drafts, reports, logs, or persisted working assets.\n"
98
+ "Use workspace_restore only when the user requests rollback, after reading "
99
+ "history or diff, and pass the reviewed current and revision SHA-256 values.\n"
100
+ "Use shell_command for bounded non-interactive local CLI checks; it is "
101
+ "policy-gated and may require explicit approval before execution.\n"
102
+ "If the latest previous observation failed, do not return final_answer with "
103
+ "empty actions; either plan recovery actions or leave the run failed.\n"
104
+ 'If the goal is complete, return {"actions":[]}.'
105
+ )
106
+
107
+ RUNTIME_TRACE_TYPE = "codex_runtime"
108
+ MAX_PLANNER_OBSERVATION_STRING_CHARS = 500
109
+ MAX_STEERING_APPLIED_PER_RUN = 8
110
+
111
+
112
+ def build_runtime_graph(
113
+ *,
114
+ checkpointer: Any = None,
115
+ interrupt_after: List[str] | None = None,
116
+ ):
117
+ with warnings.catch_warnings():
118
+ warnings.simplefilter("ignore")
119
+ try:
120
+ from langchain_core._api.deprecation import (
121
+ suppress_langchain_deprecation_warning,
122
+ )
123
+ except ImportError:
124
+ suppress_langchain_deprecation_warning = nullcontext
125
+ with suppress_langchain_deprecation_warning():
126
+ from langgraph.graph import END, StateGraph
127
+
128
+ graph = StateGraph(RuntimeGraphState, context_schema=RuntimeGraphContext)
129
+ graph.add_node("prepare", _runtime_prepare_graph_node)
130
+ graph.add_node("planner", _runtime_planner_graph_node)
131
+ graph.add_node("prepare_action", prepare_action_graph_node)
132
+ graph.add_node("mark_action_executing", mark_action_graph_node)
133
+ graph.add_node("execute_action", execute_action_graph_node)
134
+ graph.add_node("runtime_loop", _runtime_loop_graph_node)
135
+ graph.add_node("finalize", _runtime_finalize_graph_node)
136
+ graph.set_entry_point("prepare")
137
+ graph.add_edge("prepare", "planner")
138
+ graph.add_conditional_edges(
139
+ "planner",
140
+ route_after_planner,
141
+ {
142
+ "prepare_action": "prepare_action",
143
+ "runtime_loop": "runtime_loop",
144
+ },
145
+ )
146
+ graph.add_conditional_edges(
147
+ "prepare_action",
148
+ route_after_prepare_action,
149
+ {
150
+ "mark_action_executing": "mark_action_executing",
151
+ "runtime_loop": "runtime_loop",
152
+ },
153
+ )
154
+ graph.add_conditional_edges(
155
+ "mark_action_executing",
156
+ route_after_mark_action,
157
+ {
158
+ "execute_action": "execute_action",
159
+ "runtime_loop": "runtime_loop",
160
+ },
161
+ )
162
+ graph.add_edge("execute_action", "runtime_loop")
163
+ graph.add_edge("runtime_loop", "finalize")
164
+ graph.add_edge("finalize", END)
165
+ return graph.compile(
166
+ checkpointer=checkpointer,
167
+ interrupt_after=interrupt_after,
168
+ name="kagent-runtime",
169
+ )
170
+
171
+
172
+ def runtime_topology() -> Dict[str, List[str] | str]:
173
+ return {
174
+ "runtime_engine": "langgraph",
175
+ "entry_point": "prepare",
176
+ "terminal": "END",
177
+ "nodes": [
178
+ "prepare",
179
+ "planner",
180
+ "prepare_action",
181
+ "mark_action_executing",
182
+ "execute_action",
183
+ "runtime_loop",
184
+ "finalize",
185
+ ],
186
+ "edges": [
187
+ "prepare -> planner",
188
+ "planner -> prepare_action | runtime_loop",
189
+ "prepare_action -> mark_action_executing | runtime_loop",
190
+ "mark_action_executing -> execute_action | runtime_loop",
191
+ "execute_action -> runtime_loop",
192
+ "runtime_loop -> finalize",
193
+ "finalize -> END",
194
+ ],
195
+ "loop": (
196
+ "planner checkpoints the first plan; directly allowed single actions "
197
+ "use action checkpoints; runtime_loop handles remaining execution and "
198
+ "replanning"
199
+ ),
200
+ "runtime_loop_nodes": [
201
+ "planner",
202
+ "plan_parser",
203
+ "policy",
204
+ "executor",
205
+ "observation",
206
+ "replan_or_finish",
207
+ ],
208
+ "execution_flow": [
209
+ "cli_goal_input",
210
+ "provider_and_memory_context",
211
+ "langgraph_prepare",
212
+ "langgraph_planner",
213
+ "langgraph_prepare_action",
214
+ "policy",
215
+ "langgraph_mark_action_executing",
216
+ "langgraph_execute_action",
217
+ "executor",
218
+ "observation",
219
+ "replan_or_finish",
220
+ "langgraph_finalize",
221
+ "cli_render",
222
+ ],
223
+ }
224
+
225
+
226
+ def run_runtime_agent(
227
+ goal: str,
228
+ *,
229
+ provider: Any,
230
+ run_id: str = "",
231
+ cancellation_token: Optional[RuntimeCancellationToken] = None,
232
+ steering_buffer: Optional[RuntimeSteeringBuffer] = None,
233
+ policy: Optional[RuntimePolicy] = None,
234
+ tools: Optional[Dict[str, RuntimeToolSpec]] = None,
235
+ max_iterations: int = 1,
236
+ approved_action_ids: Optional[Set[str]] = None,
237
+ metadata: Optional[Dict[str, str]] = None,
238
+ tags: Optional[List[str]] = None,
239
+ event_sink: Optional[RuntimeEventSink] = None,
240
+ hooks: Optional[List[Any]] = None,
241
+ runtime_workspace_dir: str = "",
242
+ redis_url: str = "",
243
+ milvus_url: str = "",
244
+ embedding_base_url: str = "",
245
+ embedding_api_key: str = "",
246
+ embedding_model: str = "",
247
+ embedding_timeout_seconds: float = 30.0,
248
+ embedding_max_retries: int = 2,
249
+ embedding_retry_backoff_seconds: float = 0.25,
250
+ external_backend_timeout_seconds: float = 2.0,
251
+ stream_answers: bool = False,
252
+ checkpointer: Any = None,
253
+ ) -> Dict[str, Any]:
254
+ if max_iterations < 1:
255
+ raise ValueError("max_iterations must be at least 1")
256
+ resolved_run_id = run_id.strip() or str(uuid4())
257
+ normalized_metadata, metadata_error = validate_runtime_metadata(metadata)
258
+ if metadata_error:
259
+ raise ValueError(metadata_error)
260
+ normalized_tags, tags_error = validate_runtime_tags(tags)
261
+ if tags_error:
262
+ raise ValueError(tags_error)
263
+ graph = build_runtime_graph(checkpointer=checkpointer)
264
+ state: RuntimeGraphState = {
265
+ "goal": redact_runtime_text(goal),
266
+ "run_id": resolved_run_id,
267
+ "max_iterations": max_iterations,
268
+ "approved_action_ids": sorted(approved_action_ids or set()),
269
+ "metadata": normalized_metadata,
270
+ "tags": [redact_runtime_text(tag) for tag in normalized_tags],
271
+ }
272
+ context: RuntimeGraphContext = {
273
+ "goal": goal,
274
+ "provider": provider,
275
+ "runtime_workspace_dir": runtime_workspace_dir,
276
+ "redis_url": redis_url,
277
+ "milvus_url": milvus_url,
278
+ "embedding_base_url": embedding_base_url,
279
+ "embedding_api_key": embedding_api_key,
280
+ "embedding_model": embedding_model,
281
+ "embedding_timeout_seconds": embedding_timeout_seconds,
282
+ "embedding_max_retries": embedding_max_retries,
283
+ "embedding_retry_backoff_seconds": embedding_retry_backoff_seconds,
284
+ "external_backend_timeout_seconds": external_backend_timeout_seconds,
285
+ "stream_answers": stream_answers,
286
+ }
287
+ active_policy = policy or RuntimePolicy()
288
+
289
+ def delegate_child(child_goal: str, child_max_iterations: int) -> Dict[str, Any]:
290
+ return run_runtime_agent(
291
+ child_goal,
292
+ provider=provider,
293
+ cancellation_token=cancellation_token,
294
+ policy=active_policy,
295
+ tools=default_runtime_tools(
296
+ runtime_workspace_dir=runtime_workspace_dir,
297
+ redis_url=redis_url,
298
+ milvus_url=milvus_url,
299
+ embedding_base_url=embedding_base_url,
300
+ embedding_api_key=embedding_api_key,
301
+ embedding_model=embedding_model,
302
+ embedding_timeout_seconds=embedding_timeout_seconds,
303
+ embedding_max_retries=embedding_max_retries,
304
+ embedding_retry_backoff_seconds=embedding_retry_backoff_seconds,
305
+ external_backend_timeout_seconds=external_backend_timeout_seconds,
306
+ include_delegate_tool=False,
307
+ ),
308
+ max_iterations=child_max_iterations,
309
+ metadata=normalized_metadata,
310
+ tags=normalized_tags,
311
+ runtime_workspace_dir=runtime_workspace_dir,
312
+ redis_url=redis_url,
313
+ milvus_url=milvus_url,
314
+ embedding_base_url=embedding_base_url,
315
+ embedding_api_key=embedding_api_key,
316
+ embedding_model=embedding_model,
317
+ embedding_timeout_seconds=embedding_timeout_seconds,
318
+ embedding_max_retries=embedding_max_retries,
319
+ embedding_retry_backoff_seconds=embedding_retry_backoff_seconds,
320
+ external_backend_timeout_seconds=external_backend_timeout_seconds,
321
+ )
322
+
323
+ context["policy"] = active_policy
324
+ context["tools"] = tools or default_runtime_tools(
325
+ runtime_workspace_dir=runtime_workspace_dir,
326
+ redis_url=redis_url,
327
+ milvus_url=milvus_url,
328
+ embedding_base_url=embedding_base_url,
329
+ embedding_api_key=embedding_api_key,
330
+ embedding_model=embedding_model,
331
+ embedding_timeout_seconds=embedding_timeout_seconds,
332
+ embedding_max_retries=embedding_max_retries,
333
+ embedding_retry_backoff_seconds=embedding_retry_backoff_seconds,
334
+ external_backend_timeout_seconds=external_backend_timeout_seconds,
335
+ delegate_runner=delegate_child,
336
+ )
337
+ if cancellation_token is not None:
338
+ context["cancellation_token"] = cancellation_token
339
+ if steering_buffer is not None:
340
+ context["steering_buffer"] = steering_buffer
341
+ if event_sink is not None:
342
+ context["event_sink"] = event_sink
343
+ if hooks is not None:
344
+ context["hooks"] = hooks
345
+ config = {"configurable": {"thread_id": resolved_run_id}}
346
+ if checkpointer is not None and checkpointer.get_tuple(config) is not None:
347
+ raise ValueError("checkpoint thread already exists for run_id")
348
+ final_state = graph.invoke(
349
+ state,
350
+ config=config,
351
+ context=context,
352
+ )
353
+ result = final_state.get("result")
354
+ if not isinstance(result, dict):
355
+ raise RuntimeError("runtime graph did not return a result")
356
+ return result
357
+
358
+
359
+ def _runtime_prepare_graph_node(
360
+ state: RuntimeGraphState,
361
+ runtime: Any,
362
+ ) -> RuntimeGraphState:
363
+ started_at = _utc_timestamp()
364
+ started_timer = time.perf_counter()
365
+ if not runtime.context or "provider" not in runtime.context:
366
+ raise ValueError("provider is required")
367
+ context: RuntimeGraphContext = runtime.context
368
+ initial_events: List[Dict[str, Any]] = []
369
+ hook_failure_count = 0
370
+ hooks = context.get("hooks") or []
371
+ if hooks:
372
+ hook_started_at = _utc_timestamp()
373
+ hook_timer = time.perf_counter()
374
+ try:
375
+ RuntimeHookChain(hooks).on_run_start(
376
+ {
377
+ "run_id": state.get("run_id", ""),
378
+ "goal": context.get("goal", state.get("goal", "")),
379
+ "started_at": started_at,
380
+ "metadata": state.get("metadata", {}),
381
+ "tags": state.get("tags", []),
382
+ }
383
+ )
384
+ except Exception as exc:
385
+ hook_failure_count = 1
386
+ initial_events.append(
387
+ {
388
+ "node": "hook",
389
+ "stage": "on_run_start",
390
+ "status": "failed",
391
+ "error_code": "runtime_hook_failed",
392
+ "error": redact_runtime_text(str(exc)),
393
+ **_timing_fields(hook_started_at, hook_timer),
394
+ }
395
+ )
396
+ return {
397
+ "started_at": started_at,
398
+ "initial_events": initial_events,
399
+ "initial_hook_failure_count": hook_failure_count,
400
+ "graph_phases": _append_graph_phase(
401
+ state.get("graph_phases"),
402
+ "prepare",
403
+ started_at,
404
+ started_timer,
405
+ )
406
+ }
407
+
408
+
409
+ def _runtime_planner_graph_node(
410
+ state: RuntimeGraphState,
411
+ runtime: Any,
412
+ ) -> RuntimeGraphState:
413
+ graph_started_at = _utc_timestamp()
414
+ graph_timer = time.perf_counter()
415
+ context: RuntimeGraphContext = runtime.context or {}
416
+ run_id = str(state.get("run_id", ""))
417
+ cancellation_token = context.get("cancellation_token")
418
+ if cancellation_token is not None and cancellation_token.is_cancelled():
419
+ return {
420
+ "initial_planner": {"status": "cancelled"},
421
+ "initial_progress_events": [],
422
+ "initial_progress_event_sink_failure_count": 0,
423
+ "graph_phases": _append_graph_phase(
424
+ state.get("graph_phases"),
425
+ "planner",
426
+ graph_started_at,
427
+ graph_timer,
428
+ ),
429
+ }
430
+
431
+ active_tools = context.get("tools") or default_runtime_tools(
432
+ runtime_workspace_dir=context.get("runtime_workspace_dir", ""),
433
+ redis_url=context.get("redis_url", ""),
434
+ milvus_url=context.get("milvus_url", ""),
435
+ embedding_base_url=context.get("embedding_base_url", ""),
436
+ embedding_api_key=context.get("embedding_api_key", ""),
437
+ embedding_model=context.get("embedding_model", ""),
438
+ embedding_timeout_seconds=context.get("embedding_timeout_seconds", 30.0),
439
+ embedding_max_retries=context.get("embedding_max_retries", 2),
440
+ embedding_retry_backoff_seconds=context.get(
441
+ "embedding_retry_backoff_seconds",
442
+ 0.25,
443
+ ),
444
+ external_backend_timeout_seconds=context.get(
445
+ "external_backend_timeout_seconds",
446
+ 2.0,
447
+ ),
448
+ )
449
+ context["tools"] = active_tools
450
+ context.setdefault("policy", RuntimePolicy())
451
+ progress_events: List[Dict[str, Any]] = []
452
+ sink_failure_count = 0
453
+
454
+ def emit_progress(event: Dict[str, Any]) -> None:
455
+ nonlocal sink_failure_count
456
+ event_with_run_id = {"run_id": run_id, **event}
457
+ progress_events.append(event_with_run_id)
458
+ event_sink = context.get("event_sink")
459
+ if event_sink is not None:
460
+ try:
461
+ event_sink(event_with_run_id)
462
+ except Exception:
463
+ sink_failure_count += 1
464
+
465
+ planner_started_at = _utc_timestamp()
466
+ planner_timer = time.perf_counter()
467
+ _emit_runtime_progress(
468
+ emit_progress,
469
+ "planner_started",
470
+ iteration="1",
471
+ node="planner",
472
+ status="started",
473
+ )
474
+ try:
475
+ plan_text, streamed = _complete_plan_text(
476
+ context["provider"],
477
+ _SYSTEM_PROMPT,
478
+ _runtime_user_prompt(
479
+ str(context.get("goal", state.get("goal", ""))),
480
+ active_tools,
481
+ [],
482
+ context_manager=RuntimeContextManager(
483
+ max_string_chars=MAX_PLANNER_OBSERVATION_STRING_CHARS
484
+ ),
485
+ ),
486
+ emit_progress=emit_progress,
487
+ stream_answer=(
488
+ context.get("stream_answers", False)
489
+ and not _is_runtime_identity_question(
490
+ str(context.get("goal", state.get("goal", ""))).lower()
491
+ )
492
+ and not _is_runtime_deployment_question(
493
+ str(context.get("goal", state.get("goal", ""))).lower()
494
+ )
495
+ ),
496
+ )
497
+ plan = parse_agent_plan(plan_text)
498
+ except Exception as exc:
499
+ error_code = _planner_failure_error_code(exc)
500
+ timing = _timing_fields(planner_started_at, planner_timer)
501
+ event = {
502
+ "node": "planner",
503
+ "status": "failed",
504
+ "iteration": "1",
505
+ **timing,
506
+ }
507
+ _emit_runtime_progress(
508
+ emit_progress,
509
+ "planner_failed",
510
+ iteration="1",
511
+ node="planner",
512
+ status="failed",
513
+ error_code=error_code,
514
+ duration_seconds=timing["duration_seconds"],
515
+ )
516
+ return {
517
+ "initial_events": [*(state.get("initial_events") or []), event],
518
+ "initial_planner": {
519
+ "status": "failed",
520
+ "error_code": error_code,
521
+ "error": redact_runtime_text(str(exc)),
522
+ "started_at": planner_started_at,
523
+ **timing,
524
+ },
525
+ "initial_progress_events": _checkpoint_safe_value(progress_events),
526
+ "initial_progress_event_sink_failure_count": sink_failure_count,
527
+ "graph_phases": _append_graph_phase(
528
+ state.get("graph_phases"),
529
+ "planner",
530
+ graph_started_at,
531
+ graph_timer,
532
+ ),
533
+ }
534
+
535
+ timing = _timing_fields(planner_started_at, planner_timer)
536
+ raw_plan = plan.to_dict()
537
+ checkpoint_plan, plan_redacted = _checkpoint_plan_projection(raw_plan)
538
+ plan_cache_token = str(uuid4())
539
+ planner_plan_cache = context.setdefault("planner_plan_cache", {})
540
+ planner_plan_cache[run_id] = {
541
+ "token": plan_cache_token,
542
+ "plan": copy.deepcopy(raw_plan),
543
+ }
544
+ event = {
545
+ "node": "planner",
546
+ "status": "ok",
547
+ "action_count": str(len(plan.actions)),
548
+ "iteration": "1",
549
+ **timing,
550
+ }
551
+ _emit_runtime_progress(
552
+ emit_progress,
553
+ "planner_completed",
554
+ iteration="1",
555
+ node="planner",
556
+ status="ok",
557
+ action_count=str(len(plan.actions)),
558
+ duration_seconds=timing["duration_seconds"],
559
+ )
560
+ return {
561
+ "initial_events": [*(state.get("initial_events") or []), event],
562
+ "initial_planner": {
563
+ "status": "ok",
564
+ "plan": checkpoint_plan,
565
+ "plan_redacted": plan_redacted,
566
+ "plan_cache_token": plan_cache_token,
567
+ "answer_streamed": bool(streamed),
568
+ "started_at": planner_started_at,
569
+ **timing,
570
+ },
571
+ "initial_progress_events": _checkpoint_safe_value(progress_events),
572
+ "initial_progress_event_sink_failure_count": sink_failure_count,
573
+ "graph_phases": _append_graph_phase(
574
+ state.get("graph_phases"),
575
+ "planner",
576
+ graph_started_at,
577
+ graph_timer,
578
+ ),
579
+ }
580
+
581
+
582
+ def _runtime_loop_graph_node(
583
+ state: RuntimeGraphState,
584
+ runtime: Any,
585
+ ) -> RuntimeGraphState:
586
+ started_at = _utc_timestamp()
587
+ started_timer = time.perf_counter()
588
+ context: RuntimeGraphContext = runtime.context or {}
589
+ initial_planner = copy.deepcopy(state.get("initial_planner"))
590
+ initial_events = list(state.get("initial_events") or [])
591
+ initial_action_outcome = copy.deepcopy(state.get("initial_action_outcome"))
592
+ if (
593
+ initial_action_outcome is None
594
+ and initial_planner
595
+ and initial_planner.get("status") == "ok"
596
+ ):
597
+ run_id = str(state.get("run_id", ""))
598
+ cache_entry = (context.get("planner_plan_cache") or {}).get(run_id)
599
+ expected_cache_token = str(initial_planner.get("plan_cache_token", ""))
600
+ if (
601
+ isinstance(cache_entry, dict)
602
+ and cache_entry.get("token") == expected_cache_token
603
+ and isinstance(cache_entry.get("plan"), dict)
604
+ ):
605
+ initial_planner["plan"] = copy.deepcopy(cache_entry["plan"])
606
+ elif initial_planner.get("plan_redacted"):
607
+ initial_planner = {
608
+ "status": "failed",
609
+ "error_code": "planner_checkpoint_sensitive_input",
610
+ "error": (
611
+ "planner checkpoint contains sensitive tool input; "
612
+ "the original in-memory plan is unavailable"
613
+ ),
614
+ "started_at": initial_planner.get("started_at", started_at),
615
+ "completed_at": _utc_timestamp(),
616
+ "duration_seconds": "0.0000",
617
+ }
618
+ checkpoint_time = _utc_timestamp()
619
+ initial_events.append(
620
+ {
621
+ "node": "checkpoint",
622
+ "status": "failed",
623
+ "error_code": "planner_checkpoint_sensitive_input",
624
+ "started_at": checkpoint_time,
625
+ "completed_at": checkpoint_time,
626
+ "duration_seconds": "0.0000",
627
+ }
628
+ )
629
+ if isinstance(context.get("planner_plan_cache"), dict):
630
+ context["planner_plan_cache"].pop(run_id, None)
631
+ elif initial_action_outcome is not None:
632
+ planner_cache = context.get("planner_plan_cache")
633
+ if isinstance(planner_cache, dict):
634
+ planner_cache.pop(str(state.get("run_id", "")), None)
635
+ result = _checkpoint_safe_value(
636
+ _run_runtime_agent_loop(
637
+ str(context.get("goal", state.get("goal", ""))),
638
+ provider=context["provider"],
639
+ run_id=state.get("run_id", ""),
640
+ cancellation_token=context.get("cancellation_token"),
641
+ steering_buffer=context.get("steering_buffer"),
642
+ policy=context.get("policy"),
643
+ tools=context.get("tools"),
644
+ max_iterations=state.get("max_iterations", 1),
645
+ approved_action_ids=set(state.get("approved_action_ids", [])),
646
+ metadata=state.get("metadata"),
647
+ tags=state.get("tags"),
648
+ event_sink=context.get("event_sink"),
649
+ hooks=context.get("hooks"),
650
+ runtime_workspace_dir=context.get("runtime_workspace_dir", ""),
651
+ redis_url=context.get("redis_url", ""),
652
+ milvus_url=context.get("milvus_url", ""),
653
+ embedding_base_url=context.get("embedding_base_url", ""),
654
+ embedding_api_key=context.get("embedding_api_key", ""),
655
+ embedding_model=context.get("embedding_model", ""),
656
+ embedding_timeout_seconds=context.get(
657
+ "embedding_timeout_seconds",
658
+ 30.0,
659
+ ),
660
+ embedding_max_retries=context.get("embedding_max_retries", 2),
661
+ embedding_retry_backoff_seconds=context.get(
662
+ "embedding_retry_backoff_seconds",
663
+ 0.25,
664
+ ),
665
+ external_backend_timeout_seconds=context.get(
666
+ "external_backend_timeout_seconds",
667
+ 2.0,
668
+ ),
669
+ stream_answers=context.get("stream_answers", False),
670
+ initial_planner=initial_planner,
671
+ initial_events=initial_events,
672
+ initial_progress_events=state.get("initial_progress_events"),
673
+ initial_progress_event_sink_failure_count=state.get(
674
+ "initial_progress_event_sink_failure_count",
675
+ 0,
676
+ ),
677
+ initial_hook_failure_count=state.get("initial_hook_failure_count", 0),
678
+ started_at=state.get("started_at", ""),
679
+ run_start_hook_completed=True,
680
+ initial_action_outcome=initial_action_outcome,
681
+ )
682
+ )
683
+ return {
684
+ "result": result,
685
+ "graph_phases": _append_graph_phase(
686
+ state.get("graph_phases"),
687
+ "runtime_loop",
688
+ started_at,
689
+ started_timer,
690
+ ),
691
+ }
692
+
693
+
694
+ def _observation_from_dict(payload: Dict[str, Any]) -> AgentObservation:
695
+ return AgentObservation(
696
+ action_id=str(payload.get("action_id", "")),
697
+ tool=str(payload.get("tool", "")),
698
+ status=str(payload.get("status", "failed")),
699
+ output=(
700
+ dict(payload["output"])
701
+ if isinstance(payload.get("output"), dict)
702
+ else {}
703
+ ),
704
+ error_code=str(payload.get("error_code", "")),
705
+ error=str(payload.get("error", "")),
706
+ started_at=str(payload.get("started_at", "")),
707
+ completed_at=str(payload.get("completed_at", "")),
708
+ duration_seconds=str(payload.get("duration_seconds", "0.0000")),
709
+ )
710
+
711
+
712
+ def _runtime_finalize_graph_node(state: RuntimeGraphState) -> RuntimeGraphState:
713
+ started_at = _utc_timestamp()
714
+ started_timer = time.perf_counter()
715
+ result = state.get("result")
716
+ if not isinstance(result, dict):
717
+ raise RuntimeError("runtime loop did not return a result")
718
+ result["runtime_engine"] = "langgraph"
719
+ result["graph_phases"] = _append_graph_phase(
720
+ state.get("graph_phases"),
721
+ "finalize",
722
+ started_at,
723
+ started_timer,
724
+ )
725
+ return {"result": result}
726
+
727
+
728
+ def _run_runtime_agent_loop(
729
+ goal: str,
730
+ *,
731
+ provider: Any,
732
+ run_id: str = "",
733
+ cancellation_token: Optional[RuntimeCancellationToken] = None,
734
+ steering_buffer: Optional[RuntimeSteeringBuffer] = None,
735
+ policy: Optional[RuntimePolicy] = None,
736
+ tools: Optional[Dict[str, RuntimeToolSpec]] = None,
737
+ max_iterations: int = 1,
738
+ approved_action_ids: Optional[Set[str]] = None,
739
+ metadata: Optional[Dict[str, str]] = None,
740
+ tags: Optional[List[str]] = None,
741
+ event_sink: Optional[RuntimeEventSink] = None,
742
+ hooks: Optional[List[Any]] = None,
743
+ runtime_workspace_dir: str = "",
744
+ redis_url: str = "",
745
+ milvus_url: str = "",
746
+ embedding_base_url: str = "",
747
+ embedding_api_key: str = "",
748
+ embedding_model: str = "",
749
+ embedding_timeout_seconds: float = 30.0,
750
+ embedding_max_retries: int = 2,
751
+ embedding_retry_backoff_seconds: float = 0.25,
752
+ external_backend_timeout_seconds: float = 2.0,
753
+ stream_answers: bool = False,
754
+ initial_planner: Dict[str, Any] | None = None,
755
+ initial_events: List[Dict[str, Any]] | None = None,
756
+ initial_progress_events: List[Dict[str, Any]] | None = None,
757
+ initial_progress_event_sink_failure_count: int = 0,
758
+ initial_hook_failure_count: int = 0,
759
+ started_at: str = "",
760
+ run_start_hook_completed: bool = False,
761
+ initial_action_outcome: Dict[str, Any] | None = None,
762
+ ) -> Dict[str, Any]:
763
+ if max_iterations < 1:
764
+ raise ValueError("max_iterations must be at least 1")
765
+ normalized_metadata, metadata_error = validate_runtime_metadata(metadata)
766
+ if metadata_error:
767
+ raise ValueError(metadata_error)
768
+ normalized_tags, tags_error = validate_runtime_tags(tags)
769
+ if tags_error:
770
+ raise ValueError(tags_error)
771
+ run_id = run_id.strip() or str(uuid4())
772
+ original_goal = goal
773
+ events = list(initial_events or [])
774
+ started_at = started_at or _utc_timestamp()
775
+ started_timer = time.perf_counter()
776
+ active_policy = policy or RuntimePolicy()
777
+
778
+ def delegate_child(child_goal: str, child_max_iterations: int) -> Dict[str, Any]:
779
+ return run_runtime_agent(
780
+ child_goal,
781
+ provider=provider,
782
+ cancellation_token=cancellation_token,
783
+ policy=active_policy,
784
+ tools=default_runtime_tools(
785
+ runtime_workspace_dir=runtime_workspace_dir,
786
+ redis_url=redis_url,
787
+ milvus_url=milvus_url,
788
+ embedding_base_url=embedding_base_url,
789
+ embedding_api_key=embedding_api_key,
790
+ embedding_model=embedding_model,
791
+ embedding_timeout_seconds=embedding_timeout_seconds,
792
+ embedding_max_retries=embedding_max_retries,
793
+ embedding_retry_backoff_seconds=embedding_retry_backoff_seconds,
794
+ external_backend_timeout_seconds=external_backend_timeout_seconds,
795
+ include_delegate_tool=False,
796
+ ),
797
+ max_iterations=child_max_iterations,
798
+ metadata=normalized_metadata,
799
+ tags=normalized_tags,
800
+ runtime_workspace_dir=runtime_workspace_dir,
801
+ redis_url=redis_url,
802
+ milvus_url=milvus_url,
803
+ embedding_base_url=embedding_base_url,
804
+ embedding_api_key=embedding_api_key,
805
+ embedding_model=embedding_model,
806
+ embedding_timeout_seconds=embedding_timeout_seconds,
807
+ embedding_max_retries=embedding_max_retries,
808
+ embedding_retry_backoff_seconds=embedding_retry_backoff_seconds,
809
+ external_backend_timeout_seconds=external_backend_timeout_seconds,
810
+ )
811
+
812
+ active_tools = tools or default_runtime_tools(
813
+ runtime_workspace_dir=runtime_workspace_dir,
814
+ redis_url=redis_url,
815
+ milvus_url=milvus_url,
816
+ embedding_base_url=embedding_base_url,
817
+ embedding_api_key=embedding_api_key,
818
+ embedding_model=embedding_model,
819
+ embedding_timeout_seconds=embedding_timeout_seconds,
820
+ embedding_max_retries=embedding_max_retries,
821
+ embedding_retry_backoff_seconds=embedding_retry_backoff_seconds,
822
+ external_backend_timeout_seconds=external_backend_timeout_seconds,
823
+ delegate_runner=delegate_child,
824
+ )
825
+ active_approvals = set(approved_action_ids or set())
826
+ consumed_approved_action_ids: Set[str] = set()
827
+ status = "done"
828
+ observations: List[AgentObservation] = []
829
+ plans: List[Dict[str, Any]] = []
830
+ latest_plan = {"actions": []}
831
+ answer = ""
832
+ final_answer_guardrail: Dict[str, str] = {}
833
+ pending_approval: Dict[str, Any] = {}
834
+ terminal_error_code = ""
835
+ terminal_error = ""
836
+ cancelled_at = ""
837
+ cancel_reason = ""
838
+ iteration_count = 0
839
+ progress_events: List[Dict[str, Any]] = list(initial_progress_events or [])
840
+ progress_event_sink_failure_count = initial_progress_event_sink_failure_count
841
+ hook_failure_count = initial_hook_failure_count
842
+ answer_streamed = bool(
843
+ initial_planner and initial_planner.get("answer_streamed")
844
+ )
845
+ steering_applied_count = 0
846
+ steering_iteration_budget_added = 0
847
+ hook_chain = RuntimeHookChain(hooks or [])
848
+ context_manager = RuntimeContextManager(
849
+ max_string_chars=MAX_PLANNER_OBSERVATION_STRING_CHARS
850
+ )
851
+ initial_action_observation: AgentObservation | None = None
852
+ if initial_action_outcome is not None:
853
+ observation_payload = initial_action_outcome.get("observation")
854
+ if isinstance(observation_payload, dict):
855
+ initial_action_observation = _observation_from_dict(observation_payload)
856
+ observations.append(initial_action_observation)
857
+
858
+ def emit_progress(event: Dict[str, Any]) -> None:
859
+ nonlocal progress_event_sink_failure_count
860
+ event_with_run_id = {"run_id": run_id, **event}
861
+ progress_events.append(event_with_run_id)
862
+ if event_sink is not None:
863
+ try:
864
+ event_sink(event_with_run_id)
865
+ except Exception:
866
+ progress_event_sink_failure_count += 1
867
+
868
+ def mark_cancelled() -> bool:
869
+ nonlocal status, terminal_error_code, terminal_error
870
+ nonlocal cancelled_at, cancel_reason
871
+ if cancellation_token is None or not cancellation_token.is_cancelled():
872
+ return False
873
+ if status == "cancelled":
874
+ return True
875
+ token_snapshot = cancellation_token.snapshot()
876
+ cancelled_at = token_snapshot["cancelled_at"] or _utc_timestamp()
877
+ cancel_reason = token_snapshot["reason"]
878
+ status = "cancelled"
879
+ terminal_error_code = "run_cancelled"
880
+ terminal_error = cancel_reason or "runtime run cancelled"
881
+ event: Dict[str, Any] = {
882
+ "node": "control",
883
+ "status": "cancelled",
884
+ "started_at": cancelled_at,
885
+ "completed_at": cancelled_at,
886
+ "duration_seconds": "0.0000",
887
+ }
888
+ if cancel_reason:
889
+ event["reason"] = cancel_reason
890
+ events.append(event)
891
+ _emit_runtime_progress(
892
+ emit_progress,
893
+ "run_cancelled",
894
+ node="control",
895
+ status="cancelled",
896
+ reason=cancel_reason,
897
+ )
898
+ return True
899
+
900
+ def apply_pending_steering(boundary: str, iteration: str) -> bool:
901
+ nonlocal goal, steering_applied_count
902
+ if (
903
+ steering_buffer is None
904
+ or steering_applied_count >= MAX_STEERING_APPLIED_PER_RUN
905
+ ):
906
+ return False
907
+ instruction, revision = steering_buffer.consume()
908
+ if not instruction:
909
+ return False
910
+ goal = f"{goal}\n\nAdditional user instruction:\n{instruction}"
911
+ steering_applied_count += 1
912
+ timestamp = _utc_timestamp()
913
+ events.append(
914
+ {
915
+ "node": "control",
916
+ "status": "applied",
917
+ "boundary": boundary,
918
+ "iteration": iteration,
919
+ "revision": revision,
920
+ "started_at": timestamp,
921
+ "completed_at": timestamp,
922
+ "duration_seconds": "0.0000",
923
+ }
924
+ )
925
+ _emit_runtime_progress(
926
+ emit_progress,
927
+ "steering_applied",
928
+ node="control",
929
+ status="applied",
930
+ boundary=boundary,
931
+ iteration=iteration,
932
+ revision=revision,
933
+ )
934
+ return True
935
+
936
+ def record_hook_failure(
937
+ *,
938
+ stage: str,
939
+ exc: Exception,
940
+ started_at: str,
941
+ timer: float,
942
+ iteration: str = "",
943
+ action_id: str = "",
944
+ tool: str = "",
945
+ dependency_metadata: Dict[str, str] | None = None,
946
+ ) -> None:
947
+ nonlocal hook_failure_count
948
+ hook_failure_count += 1
949
+ event: Dict[str, Any] = {
950
+ "node": "hook",
951
+ "stage": stage,
952
+ "status": "failed",
953
+ "error_code": "runtime_hook_failed",
954
+ "error": str(exc),
955
+ **_timing_fields(started_at, timer),
956
+ }
957
+ if iteration:
958
+ event["iteration"] = iteration
959
+ if action_id:
960
+ event["action_id"] = action_id
961
+ if tool:
962
+ event["tool"] = tool
963
+ if dependency_metadata:
964
+ event.update(dependency_metadata)
965
+ events.append(event)
966
+
967
+ if hook_chain and not run_start_hook_completed:
968
+ hook_started_at = _utc_timestamp()
969
+ hook_timer = time.perf_counter()
970
+ try:
971
+ hook_chain.on_run_start(
972
+ {
973
+ "run_id": run_id,
974
+ "goal": goal,
975
+ "started_at": started_at,
976
+ "metadata": normalized_metadata,
977
+ "tags": normalized_tags,
978
+ }
979
+ )
980
+ except Exception as exc:
981
+ record_hook_failure(
982
+ stage="on_run_start",
983
+ exc=exc,
984
+ started_at=hook_started_at,
985
+ timer=hook_timer,
986
+ )
987
+
988
+ iteration_limit = max_iterations
989
+ iteration = 0
990
+ while iteration < iteration_limit:
991
+ iteration += 1
992
+ if mark_cancelled():
993
+ break
994
+ iteration_count = iteration
995
+ iteration_label = str(iteration)
996
+ use_initial_planner = iteration == 1 and initial_planner is not None
997
+ if use_initial_planner:
998
+ planner_status = str(initial_planner.get("status", ""))
999
+ if planner_status == "cancelled":
1000
+ status = "cancelled"
1001
+ terminal_error_code = "run_cancelled"
1002
+ terminal_error = "runtime run cancelled"
1003
+ cancelled_at = _utc_timestamp()
1004
+ break
1005
+ planner_started_at = str(
1006
+ initial_planner.get("started_at", _utc_timestamp())
1007
+ )
1008
+ timing = {
1009
+ "completed_at": str(
1010
+ initial_planner.get("completed_at", _utc_timestamp())
1011
+ ),
1012
+ "duration_seconds": str(
1013
+ initial_planner.get("duration_seconds", "0.0000")
1014
+ ),
1015
+ }
1016
+ if planner_status == "failed":
1017
+ planner_error_code = str(
1018
+ initial_planner.get("error_code", "invalid_plan")
1019
+ )
1020
+ planner_error = str(initial_planner.get("error", "planner failed"))
1021
+ observations.append(
1022
+ AgentObservation(
1023
+ action_id="",
1024
+ tool="planner",
1025
+ status="failed",
1026
+ output={},
1027
+ error_code=planner_error_code,
1028
+ error=planner_error,
1029
+ started_at=planner_started_at,
1030
+ completed_at=timing["completed_at"],
1031
+ duration_seconds=timing["duration_seconds"],
1032
+ )
1033
+ )
1034
+ if iteration < iteration_limit:
1035
+ continue
1036
+ status = "failed"
1037
+ break
1038
+ plan_payload = initial_planner.get("plan")
1039
+ if not isinstance(plan_payload, dict):
1040
+ raise RuntimeError("planner checkpoint is missing plan state")
1041
+ plan = parse_agent_plan(json.dumps(plan_payload, ensure_ascii=False))
1042
+ else:
1043
+ user_prompt = _runtime_user_prompt(
1044
+ goal,
1045
+ active_tools,
1046
+ observations,
1047
+ context_manager=context_manager,
1048
+ )
1049
+ planner_started_at = _utc_timestamp()
1050
+ planner_timer = time.perf_counter()
1051
+ events.append(
1052
+ {
1053
+ "node": "planner",
1054
+ "status": "started",
1055
+ "iteration": iteration_label,
1056
+ "started_at": planner_started_at,
1057
+ }
1058
+ )
1059
+ _emit_runtime_progress(
1060
+ emit_progress,
1061
+ "planner_started",
1062
+ iteration=iteration_label,
1063
+ node="planner",
1064
+ status="started",
1065
+ )
1066
+ try:
1067
+ plan_text, streamed_this_plan = _complete_plan_text(
1068
+ provider,
1069
+ _SYSTEM_PROMPT,
1070
+ user_prompt,
1071
+ emit_progress=emit_progress,
1072
+ stream_answer=(
1073
+ stream_answers
1074
+ and not observations
1075
+ and not _is_runtime_identity_question(goal.lower())
1076
+ and not _is_runtime_deployment_question(goal.lower())
1077
+ ),
1078
+ )
1079
+ if mark_cancelled():
1080
+ break
1081
+ answer_streamed = answer_streamed or streamed_this_plan
1082
+ plan = parse_agent_plan(plan_text)
1083
+ except Exception as exc:
1084
+ if mark_cancelled():
1085
+ break
1086
+ planner_error_code = _planner_failure_error_code(exc)
1087
+ timing = _timing_fields(planner_started_at, planner_timer)
1088
+ events[-1] = {
1089
+ "node": "planner",
1090
+ "status": "failed",
1091
+ "iteration": iteration_label,
1092
+ **timing,
1093
+ }
1094
+ observations.append(
1095
+ AgentObservation(
1096
+ action_id="",
1097
+ tool="planner",
1098
+ status="failed",
1099
+ output={},
1100
+ error_code=planner_error_code,
1101
+ error=str(exc),
1102
+ started_at=planner_started_at,
1103
+ completed_at=timing["completed_at"],
1104
+ duration_seconds=timing["duration_seconds"],
1105
+ )
1106
+ )
1107
+ _emit_runtime_progress(
1108
+ emit_progress,
1109
+ "planner_failed",
1110
+ iteration=iteration_label,
1111
+ node="planner",
1112
+ status="failed",
1113
+ error_code=planner_error_code,
1114
+ duration_seconds=timing["duration_seconds"],
1115
+ )
1116
+ if iteration < iteration_limit:
1117
+ continue
1118
+ status = "failed"
1119
+ break
1120
+ events[-1] = {
1121
+ "node": "planner",
1122
+ "status": "ok",
1123
+ "action_count": str(len(plan.actions)),
1124
+ "iteration": iteration_label,
1125
+ **_timing_fields(planner_started_at, planner_timer),
1126
+ }
1127
+ _emit_runtime_progress(
1128
+ emit_progress,
1129
+ "planner_completed",
1130
+ iteration=iteration_label,
1131
+ node="planner",
1132
+ status="ok",
1133
+ action_count=str(len(plan.actions)),
1134
+ duration_seconds=events[-1]["duration_seconds"],
1135
+ )
1136
+ latest_plan = plan.to_dict()
1137
+ plans.append(latest_plan)
1138
+ if apply_pending_steering("after_planner", iteration_label):
1139
+ if steering_iteration_budget_added < MAX_STEERING_APPLIED_PER_RUN:
1140
+ iteration_limit += 1
1141
+ steering_iteration_budget_added += 1
1142
+ continue
1143
+ if not plan.actions:
1144
+ if _latest_observation_failed(observations):
1145
+ status = "failed"
1146
+ final_answer_guardrail = _final_answer_guardrail(
1147
+ "unresolved_failure_boundary"
1148
+ )
1149
+ break
1150
+ if plan.final_answer:
1151
+ answer, final_answer_guardrail = _runtime_final_answer(
1152
+ goal,
1153
+ plan.final_answer,
1154
+ )
1155
+ break
1156
+
1157
+ should_replan = False
1158
+ iteration_observations: Dict[str, AgentObservation] = {}
1159
+ actions_to_execute = plan.actions
1160
+ if iteration == 1 and initial_action_outcome is not None:
1161
+ outcome_status = str(initial_action_outcome.get("status", "failed"))
1162
+ if (
1163
+ initial_action_observation is not None
1164
+ and initial_action_observation.action_id
1165
+ ):
1166
+ iteration_observations[initial_action_observation.action_id] = (
1167
+ initial_action_observation
1168
+ )
1169
+ actions_to_execute = []
1170
+ if outcome_status == "cancelled":
1171
+ status = "cancelled"
1172
+ terminal_error_code = "run_cancelled"
1173
+ terminal_error = "runtime run cancelled"
1174
+ cancelled_at = _utc_timestamp()
1175
+ elif outcome_status != "ok":
1176
+ if initial_action_outcome.get("should_replan"):
1177
+ should_replan = True
1178
+ else:
1179
+ status = "failed"
1180
+ for action_index, action in enumerate(actions_to_execute):
1181
+ if mark_cancelled():
1182
+ break
1183
+ dependency_metadata = _action_dependency_event_metadata(
1184
+ action.depends_on,
1185
+ iteration_observations,
1186
+ )
1187
+ resolution_started_at = _utc_timestamp()
1188
+ resolution_timer = time.perf_counter()
1189
+ try:
1190
+ resolved_input = _resolve_dependency_input(
1191
+ action.input,
1192
+ action.depends_on,
1193
+ iteration_observations,
1194
+ )
1195
+ except ValueError as exc:
1196
+ resolution_timing = _timing_fields(
1197
+ resolution_started_at,
1198
+ resolution_timer,
1199
+ )
1200
+ observation = AgentObservation(
1201
+ action_id=action.id,
1202
+ tool=action.tool,
1203
+ status="failed",
1204
+ output={},
1205
+ error_code="dependency_resolution_failed",
1206
+ error=str(exc),
1207
+ started_at=resolution_started_at,
1208
+ completed_at=resolution_timing["completed_at"],
1209
+ duration_seconds=resolution_timing["duration_seconds"],
1210
+ )
1211
+ observations.append(observation)
1212
+ iteration_observations[action.id] = observation
1213
+ events.append(
1214
+ {
1215
+ "node": "executor",
1216
+ "action_id": action.id,
1217
+ "tool": action.tool,
1218
+ "status": "failed",
1219
+ "iteration": iteration_label,
1220
+ **dependency_metadata,
1221
+ **resolution_timing,
1222
+ }
1223
+ )
1224
+ _emit_runtime_progress(
1225
+ emit_progress,
1226
+ "tool_completed",
1227
+ iteration=iteration_label,
1228
+ node="executor",
1229
+ action_id=action.id,
1230
+ tool=action.tool,
1231
+ status="failed",
1232
+ error_code="dependency_resolution_failed",
1233
+ duration_seconds=resolution_timing["duration_seconds"],
1234
+ )
1235
+ status = "failed"
1236
+ break
1237
+ policy_started_at = _utc_timestamp()
1238
+ policy_timer = time.perf_counter()
1239
+ decision = active_policy.authorize(action.tool, resolved_input)
1240
+ approved_by_id = decision.status != "allowed" and action.id in active_approvals
1241
+ if approved_by_id:
1242
+ consumed_approved_action_ids.add(action.id)
1243
+ policy_status = (
1244
+ "approved"
1245
+ if approved_by_id
1246
+ else decision.status
1247
+ )
1248
+ events.append(
1249
+ {
1250
+ "node": "policy",
1251
+ "action_id": action.id,
1252
+ "tool": action.tool,
1253
+ "status": policy_status,
1254
+ "reason": decision.reason,
1255
+ "iteration": iteration_label,
1256
+ **dependency_metadata,
1257
+ **_timing_fields(policy_started_at, policy_timer),
1258
+ }
1259
+ )
1260
+ _emit_runtime_progress(
1261
+ emit_progress,
1262
+ "policy_completed",
1263
+ iteration=iteration_label,
1264
+ node="policy",
1265
+ action_id=action.id,
1266
+ tool=action.tool,
1267
+ status=policy_status,
1268
+ reason=decision.reason,
1269
+ duration_seconds=events[-1]["duration_seconds"],
1270
+ )
1271
+ if decision.status != "allowed" and action.id not in active_approvals:
1272
+ latest_plan["actions"][action_index]["input"] = copy.deepcopy(
1273
+ resolved_input
1274
+ )
1275
+ materialization_failure: tuple[Any, ValueError] | None = None
1276
+ for later_index in range(action_index + 1, len(plan.actions)):
1277
+ later_action = plan.actions[later_index]
1278
+ try:
1279
+ latest_plan["actions"][later_index]["input"] = (
1280
+ _materialize_available_dependency_input(
1281
+ later_action.input,
1282
+ iteration_observations,
1283
+ )
1284
+ )
1285
+ except ValueError as exc:
1286
+ materialization_failure = (later_action, exc)
1287
+ break
1288
+ if materialization_failure is not None:
1289
+ failed_action, failure = materialization_failure
1290
+ failure_started_at = _utc_timestamp()
1291
+ failure_timing = _timing_fields(
1292
+ failure_started_at,
1293
+ time.perf_counter(),
1294
+ )
1295
+ failure_observation = AgentObservation(
1296
+ action_id=failed_action.id,
1297
+ tool=failed_action.tool,
1298
+ status="failed",
1299
+ output={},
1300
+ error_code="dependency_resolution_failed",
1301
+ error=str(failure),
1302
+ started_at=failure_started_at,
1303
+ completed_at=failure_timing["completed_at"],
1304
+ duration_seconds=failure_timing["duration_seconds"],
1305
+ )
1306
+ observations.append(failure_observation)
1307
+ events.append(
1308
+ {
1309
+ "node": "executor",
1310
+ "action_id": failed_action.id,
1311
+ "tool": failed_action.tool,
1312
+ "status": "failed",
1313
+ "iteration": iteration_label,
1314
+ **_action_dependency_event_metadata(
1315
+ failed_action.depends_on,
1316
+ iteration_observations,
1317
+ ),
1318
+ **failure_timing,
1319
+ }
1320
+ )
1321
+ status = "failed"
1322
+ break
1323
+ pending_approval = {**action.to_dict(), "input": resolved_input}
1324
+ approval_started_at = _utc_timestamp()
1325
+ approval_timer = time.perf_counter()
1326
+ observations.append(
1327
+ AgentObservation(
1328
+ action_id=action.id,
1329
+ tool=action.tool,
1330
+ status="requires_approval",
1331
+ output={},
1332
+ error_code=decision.reason or "policy_denied",
1333
+ error="tool execution requires approval",
1334
+ started_at=approval_started_at,
1335
+ completed_at=_utc_timestamp(),
1336
+ duration_seconds=_duration_since(approval_timer),
1337
+ )
1338
+ )
1339
+ _emit_runtime_progress(
1340
+ emit_progress,
1341
+ "approval_required",
1342
+ iteration=iteration_label,
1343
+ node="policy",
1344
+ action_id=action.id,
1345
+ tool=action.tool,
1346
+ status="requires_approval",
1347
+ reason=decision.reason or "policy_denied",
1348
+ )
1349
+ status = "requires_approval"
1350
+ break
1351
+ if hook_chain:
1352
+ hook_started_at = _utc_timestamp()
1353
+ hook_timer = time.perf_counter()
1354
+ try:
1355
+ hook_decision = hook_chain.before_tool(
1356
+ {
1357
+ "run_id": run_id,
1358
+ "goal": goal,
1359
+ "iteration": iteration_label,
1360
+ "action_id": action.id,
1361
+ "tool": action.tool,
1362
+ "input": resolved_input,
1363
+ "reason": action.reason,
1364
+ **dependency_metadata,
1365
+ }
1366
+ )
1367
+ except Exception as exc:
1368
+ hook_timing = _timing_fields(hook_started_at, hook_timer)
1369
+ record_hook_failure(
1370
+ stage="before_tool",
1371
+ exc=exc,
1372
+ started_at=hook_started_at,
1373
+ timer=hook_timer,
1374
+ iteration=iteration_label,
1375
+ action_id=action.id,
1376
+ tool=action.tool,
1377
+ dependency_metadata=dependency_metadata,
1378
+ )
1379
+ observations.append(
1380
+ AgentObservation(
1381
+ action_id=action.id,
1382
+ tool=action.tool,
1383
+ status="failed",
1384
+ output={},
1385
+ error_code="runtime_hook_failed",
1386
+ error=str(exc),
1387
+ started_at=hook_started_at,
1388
+ completed_at=hook_timing["completed_at"],
1389
+ duration_seconds=hook_timing["duration_seconds"],
1390
+ )
1391
+ )
1392
+ status = "failed"
1393
+ break
1394
+ hook_timing = _timing_fields(hook_started_at, hook_timer)
1395
+ events.append(
1396
+ {
1397
+ "node": "hook",
1398
+ "action_id": action.id,
1399
+ "tool": action.tool,
1400
+ "status": hook_decision.status,
1401
+ "reason": hook_decision.reason,
1402
+ "iteration": iteration_label,
1403
+ **dependency_metadata,
1404
+ **hook_timing,
1405
+ }
1406
+ )
1407
+ if hook_decision.status == "denied":
1408
+ observations.append(
1409
+ AgentObservation(
1410
+ action_id=action.id,
1411
+ tool=action.tool,
1412
+ status="failed",
1413
+ output={},
1414
+ error_code="runtime_hook_denied",
1415
+ error=hook_decision.reason,
1416
+ started_at=hook_started_at,
1417
+ completed_at=hook_timing["completed_at"],
1418
+ duration_seconds=hook_timing["duration_seconds"],
1419
+ )
1420
+ )
1421
+ status = "failed"
1422
+ break
1423
+ if mark_cancelled():
1424
+ break
1425
+ _emit_runtime_progress(
1426
+ emit_progress,
1427
+ "tool_started",
1428
+ iteration=iteration_label,
1429
+ node="executor",
1430
+ action_id=action.id,
1431
+ tool=action.tool,
1432
+ status="started",
1433
+ )
1434
+ observation = execute_runtime_tool(
1435
+ active_tools,
1436
+ action.tool,
1437
+ resolved_input,
1438
+ action_id=action.id,
1439
+ )
1440
+ observations.append(observation)
1441
+ iteration_observations[action.id] = observation
1442
+ cancelled_after_tool = mark_cancelled()
1443
+ if hook_chain:
1444
+ hook_started_at = _utc_timestamp()
1445
+ hook_timer = time.perf_counter()
1446
+ try:
1447
+ hook_chain.after_tool(
1448
+ {
1449
+ "run_id": run_id,
1450
+ "goal": goal,
1451
+ "iteration": iteration_label,
1452
+ "action_id": action.id,
1453
+ "tool": action.tool,
1454
+ "input": resolved_input,
1455
+ "observation": observation.to_dict(),
1456
+ **dependency_metadata,
1457
+ }
1458
+ )
1459
+ except Exception as exc:
1460
+ record_hook_failure(
1461
+ stage="after_tool",
1462
+ exc=exc,
1463
+ started_at=hook_started_at,
1464
+ timer=hook_timer,
1465
+ iteration=iteration_label,
1466
+ action_id=action.id,
1467
+ tool=action.tool,
1468
+ dependency_metadata=dependency_metadata,
1469
+ )
1470
+ events.append(
1471
+ {
1472
+ "node": "executor",
1473
+ "action_id": action.id,
1474
+ "tool": action.tool,
1475
+ "status": observation.status,
1476
+ "iteration": iteration_label,
1477
+ **dependency_metadata,
1478
+ "started_at": observation.started_at,
1479
+ "completed_at": observation.completed_at,
1480
+ "duration_seconds": observation.duration_seconds,
1481
+ }
1482
+ )
1483
+ presentation = project_runtime_presentation(
1484
+ action.tool,
1485
+ observation.status,
1486
+ observation.output,
1487
+ )
1488
+ _emit_runtime_progress(
1489
+ emit_progress,
1490
+ "tool_completed",
1491
+ iteration=iteration_label,
1492
+ node="executor",
1493
+ action_id=action.id,
1494
+ tool=action.tool,
1495
+ status=observation.status,
1496
+ error_code=observation.error_code,
1497
+ duration_seconds=observation.duration_seconds,
1498
+ presentation=presentation or None,
1499
+ )
1500
+ if cancelled_after_tool:
1501
+ break
1502
+ if apply_pending_steering("after_tool", iteration_label):
1503
+ if steering_iteration_budget_added < MAX_STEERING_APPLIED_PER_RUN:
1504
+ iteration_limit += 1
1505
+ steering_iteration_budget_added += 1
1506
+ should_replan = True
1507
+ break
1508
+ if observation.status != "ok":
1509
+ if iteration < iteration_limit:
1510
+ should_replan = True
1511
+ else:
1512
+ status = "failed"
1513
+ break
1514
+ if should_replan:
1515
+ continue
1516
+ if status != "done":
1517
+ break
1518
+ if plan.final_answer:
1519
+ answer, final_answer_guardrail = _runtime_final_answer(
1520
+ goal,
1521
+ plan.final_answer,
1522
+ )
1523
+ break
1524
+ if iteration >= iteration_limit:
1525
+ status = "failed"
1526
+ terminal_error_code = "iteration_budget_exhausted"
1527
+ terminal_error = (
1528
+ "iteration budget exhausted before the planner returned a final answer"
1529
+ )
1530
+ break
1531
+
1532
+ result = {
1533
+ "trace_type": RUNTIME_TRACE_TYPE,
1534
+ "run_id": run_id,
1535
+ "status": status,
1536
+ "goal": original_goal,
1537
+ "started_at": started_at,
1538
+ "completed_at": _utc_timestamp(),
1539
+ "duration_seconds": _duration_since(started_timer),
1540
+ "iteration_count": str(iteration_count),
1541
+ "max_iterations": str(max_iterations),
1542
+ "iteration_budget_remaining": str(max(0, max_iterations - iteration_count)),
1543
+ "steering_applied_count": str(steering_applied_count),
1544
+ "steering_iteration_budget_added": str(steering_iteration_budget_added),
1545
+ "prompt_observation_compaction": context_manager.report(),
1546
+ "approved_action_count": str(len(consumed_approved_action_ids)),
1547
+ "approved_action_ids": sorted(consumed_approved_action_ids),
1548
+ "events": events,
1549
+ "progress_events": progress_events,
1550
+ "plan": latest_plan,
1551
+ "plans": plans,
1552
+ "observations": [observation.to_dict() for observation in observations],
1553
+ }
1554
+ result["steps"] = derive_runtime_steps(result)
1555
+ if answer:
1556
+ result["answer"] = answer
1557
+ if answer_streamed and answer:
1558
+ result["answer_streamed"] = "true"
1559
+ if normalized_metadata:
1560
+ result["metadata"] = normalized_metadata
1561
+ if normalized_tags:
1562
+ result["tags"] = normalized_tags
1563
+ provider_request = _llm_provider_request_diagnostics(provider)
1564
+ if provider_request:
1565
+ result["llm_provider_request"] = provider_request
1566
+ if final_answer_guardrail:
1567
+ result["final_answer_guardrail"] = final_answer_guardrail
1568
+ if pending_approval:
1569
+ result["pending_approval"] = pending_approval
1570
+ failed_observation = _last_failed_observation(observations)
1571
+ if status == "failed" and failed_observation is not None:
1572
+ result["error_code"] = failed_observation.error_code
1573
+ result["error"] = failed_observation.error
1574
+ elif status == "failed" and terminal_error_code:
1575
+ result["error_code"] = terminal_error_code
1576
+ result["error"] = terminal_error
1577
+ elif status == "cancelled":
1578
+ result["error_code"] = terminal_error_code
1579
+ result["error"] = terminal_error
1580
+ result["cancelled_at"] = cancelled_at or result["completed_at"]
1581
+ if cancel_reason:
1582
+ result["cancel_reason"] = cancel_reason
1583
+ _emit_runtime_progress(
1584
+ emit_progress,
1585
+ "run_completed",
1586
+ node="run",
1587
+ status=status,
1588
+ iteration_count=str(iteration_count),
1589
+ duration_seconds=result["duration_seconds"],
1590
+ )
1591
+ if progress_event_sink_failure_count:
1592
+ result["progress_event_sink_failure_count"] = str(
1593
+ progress_event_sink_failure_count
1594
+ )
1595
+ if hook_chain:
1596
+ hook_started_at = _utc_timestamp()
1597
+ hook_timer = time.perf_counter()
1598
+ try:
1599
+ hook_chain.on_run_end(
1600
+ {
1601
+ "run_id": run_id,
1602
+ "goal": goal,
1603
+ "status": status,
1604
+ "completed_at": result["completed_at"],
1605
+ "duration_seconds": result["duration_seconds"],
1606
+ "iteration_count": result["iteration_count"],
1607
+ }
1608
+ )
1609
+ except Exception as exc:
1610
+ record_hook_failure(
1611
+ stage="on_run_end",
1612
+ exc=exc,
1613
+ started_at=hook_started_at,
1614
+ timer=hook_timer,
1615
+ )
1616
+ if hook_failure_count:
1617
+ result["hook_failure_count"] = str(hook_failure_count)
1618
+ return redact_runtime_payload(result)
1619
+
1620
+
1621
+ def _complete_plan_text(
1622
+ provider: Any,
1623
+ system_prompt: str,
1624
+ user_prompt: str,
1625
+ *,
1626
+ emit_progress: Callable[..., None],
1627
+ stream_answer: bool,
1628
+ ) -> tuple[str, bool]:
1629
+ stream_complete = getattr(provider, "stream_complete", None)
1630
+ if not stream_answer or not callable(stream_complete):
1631
+ return str(provider.complete(system_prompt, user_prompt)), False
1632
+
1633
+ chunks: List[str] = []
1634
+ streamer = _DirectFinalAnswerStreamer(emit_progress)
1635
+ for chunk in stream_complete(system_prompt, user_prompt):
1636
+ text = str(chunk)
1637
+ chunks.append(text)
1638
+ streamer.feed(text)
1639
+ plan_text = "".join(chunks)
1640
+ return plan_text, streamer.finish()
1641
+
1642
+
1643
+ class _DirectFinalAnswerStreamer:
1644
+ _EMPTY_ACTIONS_RE = re.compile(r'"actions"\s*:\s*\[\s*\]')
1645
+ _FINAL_ANSWER_RE = re.compile(r'"final_answer"\s*:\s*"')
1646
+
1647
+ def __init__(self, emit_progress: Callable[..., None]) -> None:
1648
+ self._emit_progress = emit_progress
1649
+ self._buffer = ""
1650
+ self._answer_start: int | None = None
1651
+ self._scan_index = 0
1652
+ self._started = False
1653
+ self._completed = False
1654
+ self._escape = False
1655
+
1656
+ def feed(self, text: str) -> None:
1657
+ if self._completed:
1658
+ return
1659
+ self._buffer += text
1660
+ if self._answer_start is None:
1661
+ if not self._EMPTY_ACTIONS_RE.search(self._buffer):
1662
+ return
1663
+ answer_match = self._FINAL_ANSWER_RE.search(self._buffer)
1664
+ if answer_match is None:
1665
+ return
1666
+ self._answer_start = answer_match.end()
1667
+ self._scan_index = self._answer_start
1668
+ self._emit_available_answer()
1669
+
1670
+ def finish(self) -> bool:
1671
+ if self._started and not self._completed:
1672
+ self._completed = True
1673
+ _emit_runtime_progress(
1674
+ self._emit_progress,
1675
+ "answer_completed",
1676
+ node="planner",
1677
+ status="done",
1678
+ )
1679
+ return self._started
1680
+
1681
+ def _emit_available_answer(self) -> None:
1682
+ pieces: List[str] = []
1683
+ while self._scan_index < len(self._buffer):
1684
+ char = self._buffer[self._scan_index]
1685
+ self._scan_index += 1
1686
+ if self._escape:
1687
+ decoded = _decode_streamed_json_escape(char)
1688
+ if decoded is None:
1689
+ continue
1690
+ pieces.append(decoded)
1691
+ self._escape = False
1692
+ continue
1693
+ if char == "\\":
1694
+ self._escape = True
1695
+ continue
1696
+ if char == '"':
1697
+ self._completed = True
1698
+ break
1699
+ pieces.append(char)
1700
+ if pieces:
1701
+ self._emit_delta("".join(pieces))
1702
+ if self._completed and self._started:
1703
+ _emit_runtime_progress(
1704
+ self._emit_progress,
1705
+ "answer_completed",
1706
+ node="planner",
1707
+ status="done",
1708
+ )
1709
+
1710
+ def _emit_delta(self, delta: str) -> None:
1711
+ if not self._started:
1712
+ self._started = True
1713
+ _emit_runtime_progress(
1714
+ self._emit_progress,
1715
+ "answer_started",
1716
+ node="planner",
1717
+ status="started",
1718
+ )
1719
+ _emit_runtime_progress(
1720
+ self._emit_progress,
1721
+ "answer_delta",
1722
+ node="planner",
1723
+ status="streaming",
1724
+ delta=delta,
1725
+ )
1726
+
1727
+
1728
+ def _decode_streamed_json_escape(char: str) -> str | None:
1729
+ escapes = {
1730
+ '"': '"',
1731
+ "\\": "\\",
1732
+ "/": "/",
1733
+ "b": "\b",
1734
+ "f": "\f",
1735
+ "n": "\n",
1736
+ "r": "\r",
1737
+ "t": "\t",
1738
+ }
1739
+ if char == "u":
1740
+ return None
1741
+ return escapes.get(char, char)
1742
+
1743
+
1744
+ def _runtime_user_prompt(
1745
+ goal: str,
1746
+ tools: Dict[str, RuntimeToolSpec],
1747
+ observations: List[AgentObservation],
1748
+ *,
1749
+ context_manager: RuntimeContextManager,
1750
+ ) -> str:
1751
+ tool_payload = runtime_tool_metadata(tools)
1752
+ prompt = "Goal:\n" + goal + "\n\nAvailable tools:\n" + json.dumps(
1753
+ tool_payload,
1754
+ sort_keys=True,
1755
+ )
1756
+ if observations:
1757
+ prompt += "\n\nPrevious observations:\n" + json.dumps(
1758
+ [
1759
+ _planner_observation_payload(
1760
+ observation,
1761
+ context_manager=context_manager,
1762
+ )
1763
+ for observation in observations
1764
+ ],
1765
+ sort_keys=True,
1766
+ )
1767
+ return prompt
1768
+
1769
+
1770
+ def _runtime_final_answer(goal: str, final_answer: str) -> tuple[str, Dict[str, str]]:
1771
+ goal_text = goal.lower()
1772
+ answer_text = final_answer.lower()
1773
+ if _is_runtime_identity_question(goal_text) and _looks_like_model_identity(answer_text):
1774
+ return _runtime_identity_answer(), _final_answer_guardrail(
1775
+ "runtime_identity_boundary"
1776
+ )
1777
+ if _is_runtime_deployment_question(goal_text) and _looks_like_model_deployment(answer_text):
1778
+ return _runtime_deployment_answer(), _final_answer_guardrail(
1779
+ "runtime_deployment_boundary"
1780
+ )
1781
+ return final_answer, {}
1782
+
1783
+
1784
+ def _final_answer_guardrail(reason: str) -> Dict[str, str]:
1785
+ return {
1786
+ "applied": "true",
1787
+ "reason": reason,
1788
+ "original_answer_omitted": "true",
1789
+ }
1790
+
1791
+
1792
+ def _is_runtime_identity_question(goal_text: str) -> bool:
1793
+ identity_markers = (
1794
+ "你是谁",
1795
+ "你是什么",
1796
+ "你叫",
1797
+ "who are you",
1798
+ "what are you",
1799
+ )
1800
+ return any(marker in goal_text for marker in identity_markers)
1801
+
1802
+
1803
+ def _is_runtime_deployment_question(goal_text: str) -> bool:
1804
+ deployment_markers = (
1805
+ "部署在哪",
1806
+ "部署在哪里",
1807
+ "运行在哪",
1808
+ "运行在哪里",
1809
+ "在哪里运行",
1810
+ "where are you deployed",
1811
+ "where do you run",
1812
+ "where are you running",
1813
+ )
1814
+ return any(marker in goal_text for marker in deployment_markers)
1815
+
1816
+
1817
+ def _looks_like_model_identity(answer_text: str) -> bool:
1818
+ provider_markers = (
1819
+ "qwen",
1820
+ "通义千问",
1821
+ "阿里云研发",
1822
+ "阿里巴巴",
1823
+ "chatgpt",
1824
+ "openai",
1825
+ "claude",
1826
+ "anthropic",
1827
+ "gemini",
1828
+ )
1829
+ return any(marker in answer_text for marker in provider_markers)
1830
+
1831
+
1832
+ def _looks_like_model_deployment(answer_text: str) -> bool:
1833
+ provider_deployment_markers = (
1834
+ "阿里云服务器",
1835
+ "阿里云",
1836
+ "云服务器",
1837
+ "model provider",
1838
+ "provider server",
1839
+ "openai servers",
1840
+ "anthropic servers",
1841
+ )
1842
+ return any(marker in answer_text for marker in provider_deployment_markers)
1843
+
1844
+
1845
+ def _runtime_identity_answer() -> str:
1846
+ return (
1847
+ "我是 kagent,你的本地或内部自动化助手。"
1848
+ "我可以理解你的目标、规划步骤、调用已允许的工具,并把过程和结果整理给你。"
1849
+ )
1850
+
1851
+
1852
+ def _runtime_deployment_answer() -> str:
1853
+ return (
1854
+ "我运行在你启动的终端或服务进程里。"
1855
+ "具体位置取决于你的运行环境,比如本机、容器、服务器或公司内部平台。"
1856
+ )
1857
+
1858
+
1859
+ def _planner_observation_payload(
1860
+ observation: AgentObservation,
1861
+ *,
1862
+ context_manager: RuntimeContextManager,
1863
+ ) -> Dict[str, Any]:
1864
+ payload = observation.to_dict()
1865
+ payload["output"] = context_manager.compact_observation_output(
1866
+ payload.get("output")
1867
+ )
1868
+ return payload
1869
+
1870
+
1871
+ def _action_dependency_event_metadata(
1872
+ depends_on: List[str],
1873
+ observations_by_action_id: Dict[str, AgentObservation],
1874
+ ) -> Dict[str, Any]:
1875
+ if not depends_on:
1876
+ return {}
1877
+ return {
1878
+ "depends_on": depends_on,
1879
+ "dependency_statuses": {
1880
+ action_id: observations_by_action_id[action_id].status
1881
+ for action_id in depends_on
1882
+ if action_id in observations_by_action_id
1883
+ },
1884
+ }
1885
+
1886
+
1887
+ def _resolve_dependency_input(
1888
+ value: Any,
1889
+ depends_on: List[str],
1890
+ observations_by_action_id: Dict[str, AgentObservation],
1891
+ ) -> Dict[str, Any]:
1892
+ reference_count = [0]
1893
+ resolved = _resolve_dependency_value(
1894
+ value,
1895
+ set(depends_on),
1896
+ observations_by_action_id,
1897
+ reference_count=reference_count,
1898
+ depth=0,
1899
+ )
1900
+ if not isinstance(resolved, dict):
1901
+ raise ValueError("action input must resolve to an object")
1902
+ return resolved
1903
+
1904
+
1905
+ def _materialize_available_dependency_input(
1906
+ value: Any,
1907
+ observations_by_action_id: Dict[str, AgentObservation],
1908
+ ) -> Any:
1909
+ if isinstance(value, list):
1910
+ return [
1911
+ _materialize_available_dependency_input(
1912
+ item,
1913
+ observations_by_action_id,
1914
+ )
1915
+ for item in value
1916
+ ]
1917
+ if not isinstance(value, dict):
1918
+ return copy.deepcopy(value)
1919
+ if "$from_action" in value:
1920
+ action_id = value["$from_action"]
1921
+ observation = observations_by_action_id.get(action_id)
1922
+ if observation is None:
1923
+ return copy.deepcopy(value)
1924
+ if observation.status != "ok":
1925
+ raise ValueError(f"dependency did not complete successfully: {action_id}")
1926
+ return copy.deepcopy(
1927
+ _resolve_json_pointer(
1928
+ observation.output,
1929
+ value["pointer"],
1930
+ action_id=action_id,
1931
+ )
1932
+ )
1933
+ return {
1934
+ key: _materialize_available_dependency_input(
1935
+ item,
1936
+ observations_by_action_id,
1937
+ )
1938
+ for key, item in value.items()
1939
+ }
1940
+
1941
+
1942
+ def _resolve_dependency_value(
1943
+ value: Any,
1944
+ declared_dependencies: Set[str],
1945
+ observations_by_action_id: Dict[str, AgentObservation],
1946
+ *,
1947
+ reference_count: List[int],
1948
+ depth: int,
1949
+ ) -> Any:
1950
+ if depth > 20:
1951
+ raise ValueError("dependency input nesting exceeds 20 levels")
1952
+ if isinstance(value, list):
1953
+ return [
1954
+ _resolve_dependency_value(
1955
+ item,
1956
+ declared_dependencies,
1957
+ observations_by_action_id,
1958
+ reference_count=reference_count,
1959
+ depth=depth + 1,
1960
+ )
1961
+ for item in value
1962
+ ]
1963
+ if not isinstance(value, dict):
1964
+ return copy.deepcopy(value)
1965
+ if "$from_action" in value:
1966
+ if set(value) != {"$from_action", "pointer"}:
1967
+ raise ValueError(
1968
+ "dependency reference must contain only $from_action and pointer"
1969
+ )
1970
+ reference_count[0] += 1
1971
+ if reference_count[0] > 50:
1972
+ raise ValueError("action input contains more than 50 dependency references")
1973
+ action_id = value.get("$from_action")
1974
+ pointer = value.get("pointer")
1975
+ if not isinstance(action_id, str) or action_id not in declared_dependencies:
1976
+ raise ValueError("dependency reference must name a declared dependency")
1977
+ if not isinstance(pointer, str):
1978
+ raise ValueError("dependency reference pointer must be a string")
1979
+ observation = observations_by_action_id.get(action_id)
1980
+ if observation is None or observation.status != "ok":
1981
+ raise ValueError(f"dependency did not complete successfully: {action_id}")
1982
+ return copy.deepcopy(
1983
+ _resolve_json_pointer(observation.output, pointer, action_id=action_id)
1984
+ )
1985
+ return {
1986
+ key: _resolve_dependency_value(
1987
+ item,
1988
+ declared_dependencies,
1989
+ observations_by_action_id,
1990
+ reference_count=reference_count,
1991
+ depth=depth + 1,
1992
+ )
1993
+ for key, item in value.items()
1994
+ }
1995
+
1996
+
1997
+ def _resolve_json_pointer(value: Any, pointer: str, *, action_id: str) -> Any:
1998
+ if pointer == "":
1999
+ return value
2000
+ if not pointer.startswith("/"):
2001
+ raise ValueError("dependency reference pointer must be empty or start with /")
2002
+ current = value
2003
+ for raw_part in pointer[1:].split("/"):
2004
+ part = raw_part.replace("~1", "/").replace("~0", "~")
2005
+ if isinstance(current, dict) and part in current:
2006
+ current = current[part]
2007
+ continue
2008
+ if isinstance(current, list) and part.isdigit():
2009
+ index = int(part)
2010
+ if index < len(current):
2011
+ current = current[index]
2012
+ continue
2013
+ raise ValueError(
2014
+ f"dependency pointer does not exist: {action_id}{pointer}"
2015
+ )
2016
+ return current
2017
+
2018
+
2019
+ def _last_failed_observation(
2020
+ observations: List[AgentObservation],
2021
+ ) -> AgentObservation | None:
2022
+ for observation in reversed(observations):
2023
+ if observation.status == "failed":
2024
+ return observation
2025
+ return None
2026
+
2027
+
2028
+ def _latest_observation_failed(observations: List[AgentObservation]) -> bool:
2029
+ return bool(observations and observations[-1].status == "failed")
2030
+
2031
+
2032
+ def _planner_failure_error_code(exc: BaseException) -> str:
2033
+ if isinstance(exc, TimeoutError):
2034
+ return "llm_provider_error"
2035
+ message = str(exc).lower()
2036
+ if message.startswith("llm provider "):
2037
+ return "llm_provider_error"
2038
+ if "llm provider request failed" in message:
2039
+ return "llm_provider_error"
2040
+ return "invalid_plan"
2041
+
2042
+
2043
+ def _llm_provider_request_diagnostics(provider: Any) -> Dict[str, str]:
2044
+ diagnostics_fn = getattr(provider, "request_diagnostics", None)
2045
+ if not callable(diagnostics_fn):
2046
+ return {}
2047
+ try:
2048
+ diagnostics = diagnostics_fn()
2049
+ except Exception:
2050
+ return {}
2051
+ if not isinstance(diagnostics, dict):
2052
+ return {}
2053
+ allowed_fields = {
2054
+ "attempt_count",
2055
+ "retry_count",
2056
+ "status",
2057
+ "stream",
2058
+ "duration_seconds",
2059
+ "error_type",
2060
+ "http_status",
2061
+ "retryable_reason",
2062
+ }
2063
+ return {
2064
+ key: str(diagnostics[key])
2065
+ for key in sorted(allowed_fields)
2066
+ if str(diagnostics.get(key, "")).strip()
2067
+ }
2068
+
2069
+
2070
+ def _emit_runtime_progress(
2071
+ event_sink: Optional[RuntimeEventSink],
2072
+ event_type: str,
2073
+ **fields: Any,
2074
+ ) -> None:
2075
+ if event_sink is None:
2076
+ return
2077
+ payload = {"type": event_type}
2078
+ payload.update(
2079
+ {
2080
+ key: value
2081
+ for key, value in fields.items()
2082
+ if not _is_empty_progress_field(value)
2083
+ }
2084
+ )
2085
+ event_sink(payload)
2086
+
2087
+
2088
+ def _is_empty_progress_field(value: Any) -> bool:
2089
+ return value is None or value == ""