@openlucaskaka/kagent 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -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 == ""
|