langchain-agentx-python 2.1.6__py3-none-any.whl → 2.1.8__py3-none-any.whl
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.
- langchain_agentx/__init__.py +1 -1
- langchain_agentx/loop/config/loop_config.py +5 -0
- langchain_agentx/loop/config/loop_runtime_overlay.py +6 -1
- langchain_agentx/loop/exit/reason_codes.py +4 -0
- langchain_agentx/loop/graph/factory.py +22 -2
- langchain_agentx/loop/run_context.py +3 -0
- langchain_agentx/loop/runtime/context_factory.py +1 -0
- langchain_agentx/loop/subagent/async_runner.py +77 -15
- langchain_agentx/loop/subagent/context.py +3 -0
- langchain_agentx/loop/subagent/graph.py +1 -0
- langchain_agentx/observability/__init__.py +16 -0
- langchain_agentx/observability/events/__init__.py +18 -0
- langchain_agentx/observability/events/agent_event_projector.py +18 -0
- langchain_agentx/observability/events/finish_event_payload_composer.py +43 -1
- langchain_agentx/observability/events/finish_outcome.py +49 -5
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +9 -1
- langchain_agentx/observability/events/oob_event_sink.py +183 -0
- langchain_agentx/observability/events/workflow_aware_event_adapter.py +9 -0
- langchain_agentx/observability/trace/__init__.py +8 -0
- langchain_agentx/observability/trace/persist_policy.py +40 -0
- langchain_agentx/session/agent_session.py +26 -0
- langchain_agentx/session/conversation_session.py +12 -0
- langchain_agentx/session/factory.py +1 -0
- langchain_agentx/workflow/base.py +20 -2
- langchain_agentx/workspace/capabilities.py +18 -2
- {langchain_agentx_python-2.1.6.dist-info → langchain_agentx_python-2.1.8.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.1.6.dist-info → langchain_agentx_python-2.1.8.dist-info}/RECORD +30 -28
- {langchain_agentx_python-2.1.6.dist-info → langchain_agentx_python-2.1.8.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.1.6.dist-info → langchain_agentx_python-2.1.8.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.1.6.dist-info → langchain_agentx_python-2.1.8.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -81,8 +81,13 @@ class TraceConfig:
|
|
|
81
81
|
"""观测追踪配置。
|
|
82
82
|
|
|
83
83
|
CC 对照:options.debug / queryTracking 等。
|
|
84
|
+
|
|
85
|
+
- ``enabled``:是否挂 TraceCollector 埋点(lifecycle / callback)。
|
|
86
|
+
- ``persist``:是否挂 ``SqliteTraceStore`` 写 traces.db;默认 False,避免盘胀。
|
|
87
|
+
亦可经环境变量 ``LANGCHAIN_AGENTX_TRACE_PERSIST=1`` 打开(见 persist_policy)。
|
|
84
88
|
"""
|
|
85
89
|
enabled: bool = True
|
|
90
|
+
persist: bool = False
|
|
86
91
|
visibility: str | None = None
|
|
87
92
|
parent_run_id: str | None = None
|
|
88
93
|
parent_tool_call_id: str | None = None
|
|
@@ -9,7 +9,8 @@ loop_runtime_overlay.py — L4 graph_factory 运行期 overlay(IF-G6)
|
|
|
9
9
|
→ graph_factory(overlay) → apply_to(AgentLoopConfig) → create_loop_agent
|
|
10
10
|
|
|
11
11
|
当前裁剪范围:
|
|
12
|
-
仅 session_id、trace 父链、subagent 标记、capabilities、enable_trace
|
|
12
|
+
仅 session_id、trace 父链、subagent 标记、capabilities、enable_trace / trace_persist;
|
|
13
|
+
不含 container_type。
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from __future__ import annotations
|
|
@@ -26,6 +27,7 @@ class LoopRuntimeOverlay:
|
|
|
26
27
|
|
|
27
28
|
session_id: str | None = None
|
|
28
29
|
enable_trace: bool | None = None
|
|
30
|
+
trace_persist: bool | None = None
|
|
29
31
|
capabilities: Any = None
|
|
30
32
|
parent_conversation_session_id: str | None = None
|
|
31
33
|
is_subagent: bool | None = None
|
|
@@ -41,6 +43,9 @@ class LoopRuntimeOverlay:
|
|
|
41
43
|
if self.enable_trace is not None:
|
|
42
44
|
trace = replace(trace, enabled=self.enable_trace)
|
|
43
45
|
trace_changed = True
|
|
46
|
+
if self.trace_persist is not None:
|
|
47
|
+
trace = replace(trace, persist=self.trace_persist)
|
|
48
|
+
trace_changed = True
|
|
44
49
|
if self.parent_conversation_session_id is not None:
|
|
45
50
|
trace = replace(trace, parent_conversation_session_id=self.parent_conversation_session_id)
|
|
46
51
|
trace_changed = True
|
|
@@ -48,6 +48,7 @@ TRANSITION_TOKEN_BUDGET_CONTINUATION = "token_budget_continuation"
|
|
|
48
48
|
DEFAULT_TOKEN_BUDGET_MAX_CONTINUATIONS = 3
|
|
49
49
|
|
|
50
50
|
# 错误结束 terminal_reason 集合(finish payload is_error 判据)
|
|
51
|
+
# D-EVT-3:含 aborted_* / hook_stopped,与 build_loop_terminal 对齐
|
|
51
52
|
ERROR_TERMINAL_REASONS = frozenset(
|
|
52
53
|
{
|
|
53
54
|
TERMINAL_REASON_ERROR,
|
|
@@ -60,6 +61,9 @@ ERROR_TERMINAL_REASONS = frozenset(
|
|
|
60
61
|
TERMINAL_REASON_MAX_TOKENS,
|
|
61
62
|
TERMINAL_REASON_BLOCKING_LIMIT,
|
|
62
63
|
TERMINAL_REASON_CLASSIFIER_DENIAL_LIMIT,
|
|
64
|
+
TERMINAL_REASON_ABORTED_STREAMING,
|
|
65
|
+
TERMINAL_REASON_ABORTED_TOOLS,
|
|
66
|
+
TERMINAL_REASON_HOOK_STOPPED,
|
|
63
67
|
}
|
|
64
68
|
)
|
|
65
69
|
|
|
@@ -305,6 +305,7 @@ def _flatten_config_for_runtime(config: AgentLoopConfig) -> _FlatLoopConfig:
|
|
|
305
305
|
agent_depth=config.agent_depth,
|
|
306
306
|
subagent_type=config.subagent.subagent_type,
|
|
307
307
|
trace_enabled=config.trace.enabled,
|
|
308
|
+
trace_persist=config.trace.persist,
|
|
308
309
|
headless=config.headless,
|
|
309
310
|
model_profile=config.model_profile,
|
|
310
311
|
model_profile_registry=config.model_profile_registry,
|
|
@@ -1143,9 +1144,28 @@ class LoopGraphBuilder:
|
|
|
1143
1144
|
if not self._config.trace.enabled:
|
|
1144
1145
|
return None
|
|
1145
1146
|
if self._services.trace_collector is not None:
|
|
1147
|
+
# 显式注入:调用方自管 store 生命周期(测试 / 宿主)
|
|
1146
1148
|
return self._services.trace_collector
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
+
|
|
1150
|
+
from ...observability.trace.persist_policy import resolve_trace_persist
|
|
1151
|
+
|
|
1152
|
+
persist = resolve_trace_persist(configured=bool(self._config.trace.persist))
|
|
1153
|
+
caps = self._services.capabilities
|
|
1154
|
+
caps_collector = (
|
|
1155
|
+
caps.trace_collector
|
|
1156
|
+
if caps is not None and getattr(caps, "trace_collector", None) is not None
|
|
1157
|
+
else None
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
if not persist:
|
|
1161
|
+
# 不落盘:复用 caps 上无 store 的 collector;若 caps 误带 store 则改用内存 collector,避免写库。
|
|
1162
|
+
if caps_collector is not None and getattr(caps_collector, "_store", None) is None:
|
|
1163
|
+
return caps_collector
|
|
1164
|
+
return TraceCollector(store=None)
|
|
1165
|
+
|
|
1166
|
+
# persist=True:必须挂有 store 的 collector;caps 无 store 时兜底 from_env,避免「开了 persist 仍不落盘」。
|
|
1167
|
+
if caps_collector is not None and getattr(caps_collector, "_store", None) is not None:
|
|
1168
|
+
return caps_collector
|
|
1149
1169
|
return TraceCollector(SqliteTraceStore.from_env())
|
|
1150
1170
|
|
|
1151
1171
|
def _resolve_response_format_setup(
|
|
@@ -42,6 +42,9 @@ class RunContext:
|
|
|
42
42
|
child_tasks: set[asyncio.Task] = field(default_factory=set)
|
|
43
43
|
child_threads: set[threading.Thread] = field(default_factory=set)
|
|
44
44
|
cancel_requested: bool = False
|
|
45
|
+
# D-EVT-6:宿主在 cancel_tree / killAll 时写入;finish 投影读取;缺省 unknown。
|
|
46
|
+
# 取值:escape | workflow_stop | kill_agents | unknown;禁止从「是否有后台」猜测。
|
|
47
|
+
cancel_source: str | None = None
|
|
45
48
|
|
|
46
49
|
def register_child(self, task: asyncio.Task) -> None:
|
|
47
50
|
self.child_tasks.add(task)
|
|
@@ -13,6 +13,7 @@ loop/subagent/async_runner.py — 子 Agent 异步生命周期管理
|
|
|
13
13
|
|
|
14
14
|
当前裁剪范围:
|
|
15
15
|
P0:删除 C4;后台仅 C3。P2:killAll 仅遍历 _BACKGROUND_HANDLES(可 Task.cancel)。
|
|
16
|
+
P2 投影:lifecycle / _mark_cancelled_terminal → OOB task-notification(D-EVT-5)。
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
from __future__ import annotations
|
|
@@ -22,7 +23,7 @@ import logging
|
|
|
22
23
|
import os
|
|
23
24
|
import uuid
|
|
24
25
|
from dataclasses import dataclass
|
|
25
|
-
from typing import Any
|
|
26
|
+
from typing import Any, Literal
|
|
26
27
|
|
|
27
28
|
from langchain_agentx.loop.exit.reason_codes import (
|
|
28
29
|
TERMINAL_REASON_ABORTED_STREAMING,
|
|
@@ -106,11 +107,34 @@ def _is_cancel_like_result(result: Any) -> bool:
|
|
|
106
107
|
return getattr(result, "error_kind", None) == "aborted"
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
def _emit_task_notification_bookend(
|
|
111
|
+
*,
|
|
112
|
+
task_id: str,
|
|
113
|
+
status: Literal["completed", "failed", "stopped"],
|
|
114
|
+
output_file: str = "",
|
|
115
|
+
summary: str = "",
|
|
116
|
+
agent_id: str | None = None,
|
|
117
|
+
terminal_reason: str | None = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""D-EVT-5:后台终态 OOB bookend;同 task_id 去重在 sink 内完成。"""
|
|
120
|
+
from langchain_agentx.observability.events.oob_event_sink import get_oob_event_sink
|
|
121
|
+
|
|
122
|
+
get_oob_event_sink().emit_task_notification(
|
|
123
|
+
task_id=task_id,
|
|
124
|
+
status=status,
|
|
125
|
+
output_file=output_file,
|
|
126
|
+
summary=summary,
|
|
127
|
+
agent_id=agent_id,
|
|
128
|
+
terminal_reason=terminal_reason,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
109
132
|
def _mark_cancelled_terminal(
|
|
110
133
|
*,
|
|
111
134
|
task_id: str,
|
|
112
135
|
output_file: str,
|
|
113
136
|
task_runtime: Any | None,
|
|
137
|
+
agent_id: str | None = None,
|
|
114
138
|
) -> None:
|
|
115
139
|
"""写 cancelled 输出 + TaskRuntime STOPPED(对齐 TaskRuntime.cancel / CC killed)。"""
|
|
116
140
|
try:
|
|
@@ -119,18 +143,27 @@ def _mark_cancelled_terminal(
|
|
|
119
143
|
fp.write("cancelled")
|
|
120
144
|
except Exception:
|
|
121
145
|
logger.debug("failed to write cancelled output for task_id=%s", task_id, exc_info=True)
|
|
122
|
-
if task_runtime is None:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
if task_runtime is not None:
|
|
147
|
+
try:
|
|
148
|
+
task_runtime.mark_terminal(
|
|
149
|
+
task_id,
|
|
150
|
+
TaskStatus.STOPPED,
|
|
151
|
+
"subagent cancelled",
|
|
152
|
+
output_file=output_file,
|
|
153
|
+
terminal_reason="cancelled",
|
|
154
|
+
)
|
|
155
|
+
except Exception:
|
|
156
|
+
logger.debug(
|
|
157
|
+
"failed to mark cancelled terminal for task_id=%s", task_id, exc_info=True
|
|
158
|
+
)
|
|
159
|
+
_emit_task_notification_bookend(
|
|
160
|
+
task_id=task_id,
|
|
161
|
+
status="stopped",
|
|
162
|
+
output_file=output_file,
|
|
163
|
+
summary="cancelled",
|
|
164
|
+
agent_id=agent_id,
|
|
165
|
+
terminal_reason="cancelled",
|
|
166
|
+
)
|
|
134
167
|
|
|
135
168
|
|
|
136
169
|
async def _execute_background_lifecycle(
|
|
@@ -146,6 +179,7 @@ async def _execute_background_lifecycle(
|
|
|
146
179
|
"""共享后台生命周期:run_subagent → 写 output → task_runtime 终态。"""
|
|
147
180
|
execution_ctx = ctx
|
|
148
181
|
execution_ctx.is_async = False
|
|
182
|
+
agent_id = getattr(ctx, "agent_id", None)
|
|
149
183
|
try:
|
|
150
184
|
result = await run_subagent(
|
|
151
185
|
ctx=execution_ctx,
|
|
@@ -160,37 +194,64 @@ async def _execute_background_lifecycle(
|
|
|
160
194
|
task_id=task_id,
|
|
161
195
|
output_file=output_file,
|
|
162
196
|
task_runtime=task_runtime,
|
|
197
|
+
agent_id=agent_id,
|
|
163
198
|
)
|
|
164
199
|
return
|
|
165
200
|
with open(output_file, "w", encoding="utf-8") as fp:
|
|
166
201
|
fp.write(result.content)
|
|
202
|
+
summary = (
|
|
203
|
+
result.content[:500]
|
|
204
|
+
if result.content
|
|
205
|
+
else (result.error_message or "subagent completed")
|
|
206
|
+
)
|
|
207
|
+
notif_status: Literal["completed", "failed"] = (
|
|
208
|
+
"completed" if result.status == "completed" else "failed"
|
|
209
|
+
)
|
|
167
210
|
if task_runtime is not None:
|
|
168
211
|
status = TaskStatus.COMPLETED if result.status == "completed" else TaskStatus.FAILED
|
|
169
212
|
task_runtime.mark_terminal(
|
|
170
213
|
task_id,
|
|
171
214
|
status,
|
|
172
|
-
|
|
215
|
+
summary,
|
|
173
216
|
output_file=output_file,
|
|
174
217
|
terminal_reason=result.terminal_reason or None,
|
|
175
218
|
)
|
|
219
|
+
_emit_task_notification_bookend(
|
|
220
|
+
task_id=task_id,
|
|
221
|
+
status=notif_status,
|
|
222
|
+
output_file=output_file,
|
|
223
|
+
summary=summary,
|
|
224
|
+
agent_id=agent_id,
|
|
225
|
+
terminal_reason=result.terminal_reason or None,
|
|
226
|
+
)
|
|
176
227
|
except asyncio.CancelledError:
|
|
177
228
|
_mark_cancelled_terminal(
|
|
178
229
|
task_id=task_id,
|
|
179
230
|
output_file=output_file,
|
|
180
231
|
task_runtime=task_runtime,
|
|
232
|
+
agent_id=agent_id,
|
|
181
233
|
)
|
|
182
234
|
raise
|
|
183
235
|
except Exception as exc:
|
|
184
236
|
with open(output_file, "w", encoding="utf-8") as fp:
|
|
185
237
|
fp.write(str(exc))
|
|
238
|
+
summary = f"Subagent background lifecycle failed: {exc}"
|
|
186
239
|
if task_runtime is not None:
|
|
187
240
|
task_runtime.mark_terminal(
|
|
188
241
|
task_id,
|
|
189
242
|
TaskStatus.FAILED,
|
|
190
|
-
|
|
243
|
+
summary,
|
|
191
244
|
output_file=output_file,
|
|
192
245
|
terminal_reason=TERMINAL_REASON_GRAPH_RUNTIME_ERROR,
|
|
193
246
|
)
|
|
247
|
+
_emit_task_notification_bookend(
|
|
248
|
+
task_id=task_id,
|
|
249
|
+
status="failed",
|
|
250
|
+
output_file=output_file,
|
|
251
|
+
summary=summary,
|
|
252
|
+
agent_id=agent_id,
|
|
253
|
+
terminal_reason=TERMINAL_REASON_GRAPH_RUNTIME_ERROR,
|
|
254
|
+
)
|
|
194
255
|
|
|
195
256
|
|
|
196
257
|
def register_background_agent(
|
|
@@ -309,6 +370,7 @@ def kill_background_agent(task_id: str) -> bool:
|
|
|
309
370
|
task_id=task_id,
|
|
310
371
|
output_file=handle.output_file,
|
|
311
372
|
task_runtime=handle.task_runtime,
|
|
373
|
+
agent_id=handle.agent_id,
|
|
312
374
|
)
|
|
313
375
|
return True
|
|
314
376
|
|
|
@@ -91,6 +91,7 @@ class SubagentContext:
|
|
|
91
91
|
parent_tool_call_id: str | None = None # 触发本子 Agent 的 tool_call_id
|
|
92
92
|
parent_conversation_session_id: str | None = None
|
|
93
93
|
enable_trace: bool = True
|
|
94
|
+
trace_persist: bool = False
|
|
94
95
|
runnable_config: Any = None # 父 runnable_config:仅供 tool_progress custom event 回调链;非子 raw 合并
|
|
95
96
|
isolation: str = "none"
|
|
96
97
|
worktree_dir: str | None = None
|
|
@@ -324,6 +325,7 @@ def create_subagent_context(
|
|
|
324
325
|
parent_tool_call_id=parent_tool_call_id,
|
|
325
326
|
parent_conversation_session_id=parent_conversation_id,
|
|
326
327
|
enable_trace=bool(getattr(parent_ctx, "trace_enabled", True)),
|
|
328
|
+
trace_persist=bool(getattr(parent_ctx, "trace_persist", False)),
|
|
327
329
|
isolation=isolation,
|
|
328
330
|
worktree_dir=worktree_dir,
|
|
329
331
|
workspace_root=str(parent_workspace_root),
|
|
@@ -476,6 +478,7 @@ class SubagentContextFactory:
|
|
|
476
478
|
parent_run_id=parent_runtime.parent_run_id,
|
|
477
479
|
parent_conversation_session_id=parent_runtime.parent_conversation_session_id,
|
|
478
480
|
enable_trace=not skip_transcript,
|
|
481
|
+
trace_persist=bool(getattr(parent_runtime, "trace_persist", False)),
|
|
479
482
|
workspace_root=workspace_root,
|
|
480
483
|
agent_home_segment=agent_home_segment,
|
|
481
484
|
cwd=effective_cwd,
|
|
@@ -10,13 +10,21 @@ from .events import (
|
|
|
10
10
|
LangchainAgentEvent,
|
|
11
11
|
LangchainAgentEventType,
|
|
12
12
|
LangGraphToLangchainAgentEventAdapter,
|
|
13
|
+
OOB_EVENT_QUEUE_CAP,
|
|
14
|
+
OobEventSink,
|
|
13
15
|
RecoveryAction,
|
|
14
16
|
RecoveryActionResolver,
|
|
15
17
|
RecoveryPolicy,
|
|
16
18
|
RecoveryPolicyMode,
|
|
17
19
|
coerce_recovery_policy,
|
|
18
20
|
create_langgraph_event_adapter,
|
|
21
|
+
drain_oob_events,
|
|
19
22
|
enrich_run_result,
|
|
23
|
+
ensure_oob_event_sink,
|
|
24
|
+
get_oob_event_sink,
|
|
25
|
+
install_oob_event_sink,
|
|
26
|
+
reset_oob_event_sink,
|
|
27
|
+
teardown_oob_event_sink,
|
|
20
28
|
)
|
|
21
29
|
from .replay.service import (
|
|
22
30
|
export_call_chain,
|
|
@@ -62,6 +70,8 @@ __all__ = [
|
|
|
62
70
|
"LogContext",
|
|
63
71
|
"LoggingConfigBuilder",
|
|
64
72
|
"OBS_SCHEMA_VERSION",
|
|
73
|
+
"OOB_EVENT_QUEUE_CAP",
|
|
74
|
+
"OobEventSink",
|
|
65
75
|
"ObservabilityLoggerAdapter",
|
|
66
76
|
"build_log_context",
|
|
67
77
|
"configure_logging_once",
|
|
@@ -69,6 +79,12 @@ __all__ = [
|
|
|
69
79
|
"LangchainAgentEventType",
|
|
70
80
|
"LangGraphToLangchainAgentEventAdapter",
|
|
71
81
|
"create_langgraph_event_adapter",
|
|
82
|
+
"drain_oob_events",
|
|
83
|
+
"ensure_oob_event_sink",
|
|
84
|
+
"get_oob_event_sink",
|
|
85
|
+
"install_oob_event_sink",
|
|
86
|
+
"reset_oob_event_sink",
|
|
87
|
+
"teardown_oob_event_sink",
|
|
72
88
|
"RecoveryAction",
|
|
73
89
|
"RecoveryActionResolver",
|
|
74
90
|
"RecoveryPolicy",
|
|
@@ -22,6 +22,16 @@ from .factory import (
|
|
|
22
22
|
ILangGraphEventAdapter,
|
|
23
23
|
create_langgraph_event_adapter,
|
|
24
24
|
)
|
|
25
|
+
from .oob_event_sink import (
|
|
26
|
+
OOB_EVENT_QUEUE_CAP,
|
|
27
|
+
OobEventSink,
|
|
28
|
+
drain_oob_events,
|
|
29
|
+
ensure_oob_event_sink,
|
|
30
|
+
get_oob_event_sink,
|
|
31
|
+
install_oob_event_sink,
|
|
32
|
+
reset_oob_event_sink,
|
|
33
|
+
teardown_oob_event_sink,
|
|
34
|
+
)
|
|
25
35
|
|
|
26
36
|
__all__ = [
|
|
27
37
|
"LangchainAgentEventType",
|
|
@@ -40,4 +50,12 @@ __all__ = [
|
|
|
40
50
|
"enrich_run_result",
|
|
41
51
|
"ILangGraphEventAdapter",
|
|
42
52
|
"create_langgraph_event_adapter",
|
|
53
|
+
"OOB_EVENT_QUEUE_CAP",
|
|
54
|
+
"OobEventSink",
|
|
55
|
+
"drain_oob_events",
|
|
56
|
+
"ensure_oob_event_sink",
|
|
57
|
+
"get_oob_event_sink",
|
|
58
|
+
"install_oob_event_sink",
|
|
59
|
+
"reset_oob_event_sink",
|
|
60
|
+
"teardown_oob_event_sink",
|
|
43
61
|
]
|
|
@@ -139,6 +139,8 @@ class LangchainAgentEventType(Enum):
|
|
|
139
139
|
TASK_NOTIFICATION_ENQUEUED = "task-notification-enqueued"
|
|
140
140
|
TASK_NOTIFICATION_CONSUMED = "task-notification-consumed"
|
|
141
141
|
TASK_TERMINATED = "task-terminated"
|
|
142
|
+
# D-EVT-5:OOB 后台终态书档(对标 CC system/task_notification)
|
|
143
|
+
TASK_NOTIFICATION = "task-notification"
|
|
142
144
|
|
|
143
145
|
|
|
144
146
|
@dataclass
|
|
@@ -1249,6 +1251,22 @@ class AgentEventProjector:
|
|
|
1249
1251
|
tool_name=name,
|
|
1250
1252
|
session_id=str(child_session_id) if child_session_id else None,
|
|
1251
1253
|
)
|
|
1254
|
+
# D-EVT-5′:图内 SUBAGENT_END 覆盖时登记;若 payload 含 task_id 则抑制同 id OOB
|
|
1255
|
+
end_task_id: str | None = None
|
|
1256
|
+
if isinstance(output, dict):
|
|
1257
|
+
raw_tid = output.get("task_id")
|
|
1258
|
+
if not isinstance(raw_tid, str):
|
|
1259
|
+
artifact = output.get("artifact")
|
|
1260
|
+
if isinstance(artifact, dict):
|
|
1261
|
+
raw_tid = artifact.get("task_id")
|
|
1262
|
+
if isinstance(raw_tid, str) and raw_tid.strip():
|
|
1263
|
+
end_task_id = raw_tid.strip()
|
|
1264
|
+
if end_task_id:
|
|
1265
|
+
from langchain_agentx.observability.events.oob_event_sink import (
|
|
1266
|
+
get_oob_event_sink,
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
get_oob_event_sink().note_terminal_projected(end_task_id)
|
|
1252
1270
|
self._on_subagent_end_boundary()
|
|
1253
1271
|
|
|
1254
1272
|
self.state.current_tool = None
|
|
@@ -11,6 +11,7 @@ finish_event_payload_composer.py — finish 事件 data 组装协作者
|
|
|
11
11
|
当前裁剪范围:
|
|
12
12
|
- 不发射事件、不读 LangGraph raw event
|
|
13
13
|
- 不处理 SubAgent 终态;model_retry 进度见 retry_progress.py(Phase 6)
|
|
14
|
+
- INTERRUPT 文案仅经 display_interrupt_text 投影(D-EVT-4),不入 messages
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
@@ -22,8 +23,11 @@ from langchain_core.messages import AIMessage
|
|
|
22
23
|
|
|
23
24
|
from langchain_agentx.loop.exit.reason_codes import (
|
|
24
25
|
ERROR_TERMINAL_REASONS,
|
|
26
|
+
TERMINAL_REASON_ABORTED_STREAMING,
|
|
27
|
+
TERMINAL_REASON_ABORTED_TOOLS,
|
|
25
28
|
TERMINAL_REASON_COMPLETED,
|
|
26
29
|
TERMINAL_REASON_ERROR,
|
|
30
|
+
TERMINAL_REASON_HOOK_STOPPED,
|
|
27
31
|
TERMINAL_REASON_MAX_TOKENS,
|
|
28
32
|
TRANSITION_API_ERROR_TERMINAL,
|
|
29
33
|
TRANSITION_CONTEXT_WINDOW_RECOVERY_EXHAUSTED,
|
|
@@ -32,6 +36,7 @@ from langchain_agentx.loop.exit.reason_codes import (
|
|
|
32
36
|
)
|
|
33
37
|
from langchain_agentx.loop.exit.terminal import read_terminal_reason
|
|
34
38
|
from langchain_agentx.loop.exit.withheld_error import resolve_overflow_kind
|
|
39
|
+
from langchain_agentx.loop.run_context import get_run_context
|
|
35
40
|
|
|
36
41
|
_RECOVERY_EXHAUSTED_TRANSITIONS = frozenset(
|
|
37
42
|
{
|
|
@@ -41,6 +46,17 @@ _RECOVERY_EXHAUSTED_TRANSITIONS = frozenset(
|
|
|
41
46
|
}
|
|
42
47
|
)
|
|
43
48
|
|
|
49
|
+
# D-EVT-2/4:用户取消 display 文案(对齐 CC INTERRUPT;禁止注入 messages)
|
|
50
|
+
DEFAULT_DISPLAY_INTERRUPT_TEXT = "[Request interrupted by user]"
|
|
51
|
+
ERROR_KIND_USER_CANCELLED = "user_cancelled"
|
|
52
|
+
ERROR_KIND_HOOK_STOPPED = "hook_stopped"
|
|
53
|
+
_USER_CANCELLED_TERMINAL_REASONS = frozenset(
|
|
54
|
+
{
|
|
55
|
+
TERMINAL_REASON_ABORTED_STREAMING,
|
|
56
|
+
TERMINAL_REASON_ABORTED_TOOLS,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
44
60
|
_SENSITIVE_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
45
61
|
(re.compile(r"sk-[a-zA-Z0-9_-]{8,}", re.I), "<redacted_api_key>"),
|
|
46
62
|
(re.compile(r"bearer\s+\S+", re.I), "Bearer <redacted>"),
|
|
@@ -157,7 +173,7 @@ class FinishEventPayloadComposer:
|
|
|
157
173
|
messages=messages,
|
|
158
174
|
)
|
|
159
175
|
|
|
160
|
-
|
|
176
|
+
payload: dict[str, Any] = {
|
|
161
177
|
"graph_name": graph_name,
|
|
162
178
|
"answer": answer,
|
|
163
179
|
"terminal_reason": terminal_reason,
|
|
@@ -168,6 +184,10 @@ class FinishEventPayloadComposer:
|
|
|
168
184
|
"error_kind": error_kind,
|
|
169
185
|
"recovery_exhausted": recovery_exhausted,
|
|
170
186
|
}
|
|
187
|
+
if error_kind == ERROR_KIND_USER_CANCELLED:
|
|
188
|
+
payload["display_interrupt_text"] = DEFAULT_DISPLAY_INTERRUPT_TEXT
|
|
189
|
+
payload["cancel_source"] = self._resolve_cancel_source(snap)
|
|
190
|
+
return payload
|
|
171
191
|
|
|
172
192
|
@staticmethod
|
|
173
193
|
def _resolve_terminal_reason(
|
|
@@ -265,6 +285,25 @@ class FinishEventPayloadComposer:
|
|
|
265
285
|
if terminal_reason == TERMINAL_REASON_COMPLETED:
|
|
266
286
|
return "unknown"
|
|
267
287
|
|
|
288
|
+
if terminal_reason in _USER_CANCELLED_TERMINAL_REASONS:
|
|
289
|
+
return ERROR_KIND_USER_CANCELLED
|
|
290
|
+
|
|
291
|
+
if terminal_reason == TERMINAL_REASON_HOOK_STOPPED:
|
|
292
|
+
return ERROR_KIND_HOOK_STOPPED
|
|
293
|
+
|
|
294
|
+
return "unknown"
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _resolve_cancel_source(snap: dict[str, Any]) -> str:
|
|
298
|
+
"""D-EVT-6:宿主写入优先;缺省 unknown;禁止猜测反推。"""
|
|
299
|
+
raw = snap.get("cancel_source")
|
|
300
|
+
if raw is not None and str(raw):
|
|
301
|
+
return str(raw)
|
|
302
|
+
ctx = get_run_context()
|
|
303
|
+
if ctx is not None:
|
|
304
|
+
source = getattr(ctx, "cancel_source", None)
|
|
305
|
+
if source is not None and str(source):
|
|
306
|
+
return str(source)
|
|
268
307
|
return "unknown"
|
|
269
308
|
|
|
270
309
|
@staticmethod
|
|
@@ -323,6 +362,9 @@ class FinishEventPayloadComposer:
|
|
|
323
362
|
|
|
324
363
|
|
|
325
364
|
__all__ = [
|
|
365
|
+
"DEFAULT_DISPLAY_INTERRUPT_TEXT",
|
|
366
|
+
"ERROR_KIND_HOOK_STOPPED",
|
|
367
|
+
"ERROR_KIND_USER_CANCELLED",
|
|
326
368
|
"FinishEventPayloadComposer",
|
|
327
369
|
"sanitize_finish_error_message",
|
|
328
370
|
]
|
|
@@ -20,8 +20,15 @@ from dataclasses import dataclass
|
|
|
20
20
|
from enum import Enum
|
|
21
21
|
from typing import Any, Literal
|
|
22
22
|
|
|
23
|
+
from langchain_agentx.loop.exit.reason_codes import (
|
|
24
|
+
TERMINAL_REASON_ABORTED_STREAMING,
|
|
25
|
+
TERMINAL_REASON_ABORTED_TOOLS,
|
|
26
|
+
TERMINAL_REASON_HOOK_STOPPED,
|
|
27
|
+
)
|
|
23
28
|
from langchain_agentx.loop.exit.terminal import read_terminal_reason
|
|
24
29
|
from langchain_agentx.observability.events.finish_event_payload_composer import (
|
|
30
|
+
ERROR_KIND_HOOK_STOPPED,
|
|
31
|
+
ERROR_KIND_USER_CANCELLED,
|
|
25
32
|
FinishEventPayloadComposer,
|
|
26
33
|
)
|
|
27
34
|
|
|
@@ -53,6 +60,27 @@ FINISH_OUTCOME_KEYS = frozenset(
|
|
|
53
60
|
|
|
54
61
|
RecoveryPolicyMode = Literal["interactive", "auto"]
|
|
55
62
|
|
|
63
|
+
_CANCEL_ERROR_KINDS = frozenset(
|
|
64
|
+
{
|
|
65
|
+
ERROR_KIND_USER_CANCELLED,
|
|
66
|
+
ERROR_KIND_HOOK_STOPPED,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
_CANCEL_TERMINAL_REASONS = frozenset(
|
|
70
|
+
{
|
|
71
|
+
TERMINAL_REASON_ABORTED_STREAMING,
|
|
72
|
+
TERMINAL_REASON_ABORTED_TOOLS,
|
|
73
|
+
TERMINAL_REASON_HOOK_STOPPED,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_cancel_outcome(outcome: "FinishOutcome") -> bool:
|
|
79
|
+
"""用户取消 / hook 终局:禁止建议 compact(D-EVT-2 / CLI 投影)。"""
|
|
80
|
+
if outcome.error_kind in _CANCEL_ERROR_KINDS:
|
|
81
|
+
return True
|
|
82
|
+
return outcome.terminal_reason in _CANCEL_TERMINAL_REASONS
|
|
83
|
+
|
|
56
84
|
|
|
57
85
|
@dataclass(frozen=True)
|
|
58
86
|
class RecoveryPolicy:
|
|
@@ -149,6 +177,7 @@ class FinishOutcomeParser:
|
|
|
149
177
|
snap = dict(state or {})
|
|
150
178
|
terminal = snap.get("terminal") if isinstance(snap.get("terminal"), dict) else None
|
|
151
179
|
if terminal is not None and "is_error" in terminal:
|
|
180
|
+
# from_terminal 已对缺失 error_kind 做 composer 对齐补推(D-EVT-2)
|
|
152
181
|
return self.from_terminal(terminal, answer=answer)
|
|
153
182
|
msgs = messages if messages is not None else snap.get("messages")
|
|
154
183
|
if not isinstance(msgs, list):
|
|
@@ -178,11 +207,23 @@ class FinishOutcomeParser:
|
|
|
178
207
|
*,
|
|
179
208
|
answer: str = "",
|
|
180
209
|
) -> FinishOutcome:
|
|
210
|
+
terminal_reason = _optional_str(read_terminal_reason(terminal))
|
|
211
|
+
is_error = bool(terminal.get("is_error"))
|
|
212
|
+
error_kind = _optional_str(terminal.get("error_kind"))
|
|
213
|
+
if is_error and error_kind is None:
|
|
214
|
+
error_kind = FinishEventPayloadComposer._infer_error_kind(
|
|
215
|
+
terminal_reason=terminal_reason,
|
|
216
|
+
is_error=True,
|
|
217
|
+
withheld_error=None,
|
|
218
|
+
transition_reason=None,
|
|
219
|
+
terminal=terminal,
|
|
220
|
+
messages=None,
|
|
221
|
+
)
|
|
181
222
|
return FinishOutcome(
|
|
182
|
-
terminal_reason=
|
|
183
|
-
is_error=
|
|
223
|
+
terminal_reason=terminal_reason,
|
|
224
|
+
is_error=is_error,
|
|
184
225
|
recoverable=bool(terminal.get("recoverable")),
|
|
185
|
-
error_kind=
|
|
226
|
+
error_kind=error_kind,
|
|
186
227
|
recovery_exhausted=bool(terminal.get("recovery_exhausted")),
|
|
187
228
|
errors=tuple(str(e) for e in (terminal.get("errors") or []) if e),
|
|
188
229
|
answer=answer,
|
|
@@ -220,6 +261,8 @@ class RecoveryActionResolver:
|
|
|
220
261
|
) -> RecoveryAction:
|
|
221
262
|
if not outcome.is_error:
|
|
222
263
|
return RecoveryAction.NONE
|
|
264
|
+
if _is_cancel_outcome(outcome):
|
|
265
|
+
return RecoveryAction.FAIL
|
|
223
266
|
if not outcome.recoverable:
|
|
224
267
|
return RecoveryAction.FAIL
|
|
225
268
|
|
|
@@ -242,12 +285,13 @@ class RecoveryActionResolver:
|
|
|
242
285
|
|
|
243
286
|
@staticmethod
|
|
244
287
|
def blocks_auto_compact(outcome: FinishOutcome) -> bool:
|
|
245
|
-
"""Workflow 门禁:recovery_exhausted 后禁止再次 auto-compact
|
|
288
|
+
"""Workflow 门禁:cancel / recovery_exhausted 后禁止再次 auto-compact。"""
|
|
246
289
|
if not outcome.is_error:
|
|
247
290
|
return False
|
|
291
|
+
if _is_cancel_outcome(outcome):
|
|
292
|
+
return True
|
|
248
293
|
return outcome.recovery_exhausted
|
|
249
294
|
|
|
250
|
-
|
|
251
295
|
def enrich_run_result(
|
|
252
296
|
result: dict[str, Any],
|
|
253
297
|
*,
|
|
@@ -28,6 +28,7 @@ from langchain_agentx.observability.events.agent_event_projector import (
|
|
|
28
28
|
LangchainAgentEvent,
|
|
29
29
|
LangchainAgentEventType,
|
|
30
30
|
)
|
|
31
|
+
from langchain_agentx.observability.events.oob_event_sink import drain_oob_events
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class LangGraphToLangchainAgentEventAdapter:
|
|
@@ -51,12 +52,19 @@ class LangGraphToLangchainAgentEventAdapter:
|
|
|
51
52
|
async def adapt(
|
|
52
53
|
self, events: AsyncIterator[Dict[str, Any]]
|
|
53
54
|
) -> AsyncIterator[LangchainAgentEvent]:
|
|
54
|
-
"""主转换函数:将 LangGraph 事件流转换为 LangChain AgentX 风格事件流。
|
|
55
|
+
"""主转换函数:将 LangGraph 事件流转换为 LangChain AgentX 风格事件流。
|
|
56
|
+
|
|
57
|
+
每批 raw 事件后合并 OOB task-notification(D-EVT-5 / D-EVT-10)。
|
|
58
|
+
"""
|
|
55
59
|
self._projector.reset_stream_closeout_diagnostics()
|
|
56
60
|
try:
|
|
57
61
|
async for event in events:
|
|
58
62
|
async for oc_event in self._projector.consume(event):
|
|
59
63
|
yield oc_event
|
|
64
|
+
for oob in drain_oob_events():
|
|
65
|
+
yield oob
|
|
66
|
+
for oob in drain_oob_events():
|
|
67
|
+
yield oob
|
|
60
68
|
finally:
|
|
61
69
|
self._projector.log_stream_closeout_diagnostics()
|
|
62
70
|
|