langchain-agentx-python 1.2.3__py3-none-any.whl → 1.2.6__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.
Files changed (45) hide show
  1. langchain_agentx/__init__.py +1 -1
  2. langchain_agentx/loop/config/loop_config.py +2 -2
  3. langchain_agentx/loop/config/thinking_config.py +2 -2
  4. langchain_agentx/loop/config/ui_display_context.py +4 -4
  5. langchain_agentx/loop/exit/__init__.py +2 -0
  6. langchain_agentx/loop/graph/factory.py +4 -3
  7. langchain_agentx/loop/message_contract.py +72 -0
  8. langchain_agentx/loop/messages/__init__.py +63 -0
  9. langchain_agentx/loop/messages/contract.py +194 -0
  10. langchain_agentx/loop/messages/engine_protocol.py +29 -0
  11. langchain_agentx/loop/messages/langgraph_adapter.py +116 -0
  12. langchain_agentx/loop/messages/normalizer.py +136 -0
  13. langchain_agentx/loop/messages/transcript_codec.py +73 -0
  14. langchain_agentx/loop/messages/types.py +109 -0
  15. langchain_agentx/loop/messages/validate.py +106 -0
  16. langchain_agentx/loop/model/model_node.py +19 -10
  17. langchain_agentx/loop/subagent/cache_safe_params.py +119 -0
  18. langchain_agentx/loop/subagent/context.py +30 -46
  19. langchain_agentx/loop/subagent/display_projector.py +64 -20
  20. langchain_agentx/loop/subagent/graph.py +1 -1
  21. langchain_agentx/loop/subagent/orchestrator.py +10 -26
  22. langchain_agentx/loop/subagent/progress.py +19 -25
  23. langchain_agentx/loop/subagent/runner.py +55 -39
  24. langchain_agentx/memory/memdir/prefetch.py +3 -2
  25. langchain_agentx/memory/session/session_memory.py +13 -9
  26. langchain_agentx/observability/events/langchain_agentx_event_adapter.py +1 -0
  27. langchain_agentx/observability/events/subagent_event_mapper.py +5 -0
  28. langchain_agentx/provider/compatible_chat_openai.py +1 -0
  29. langchain_agentx/provider/semantics/__init__.py +28 -0
  30. langchain_agentx/provider/semantics/contracts.py +7 -0
  31. langchain_agentx/provider/semantics/profile.py +33 -6
  32. langchain_agentx/provider/semantics/thinking_capability.py +244 -0
  33. langchain_agentx/session/agent_session.py +6 -9
  34. langchain_agentx/session/conversation_recovery.py +48 -39
  35. langchain_agentx/tool_runtime/adapter.py +27 -11
  36. langchain_agentx/tool_runtime/models.py +4 -2
  37. langchain_agentx/tools/agent/tool.py +11 -22
  38. langchain_agentx/tools/bash/backend.py +17 -1
  39. langchain_agentx/tools/bash/tool.py +7 -0
  40. langchain_agentx/workflow/node.py +21 -8
  41. {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/METADATA +1 -1
  42. {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/RECORD +45 -34
  43. {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/LICENSE +0 -0
  44. {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/WHEEL +0 -0
  45. {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ from langchain_agentx import create_loop_agent
11
11
  ```
12
12
  """
13
13
 
14
- __version__ = "1.2.3"
14
+ __version__ = "1.2.6"
15
15
 
16
16
  from .loop import ( # noqa: F401
17
17
  create_loop_agent,
@@ -43,13 +43,13 @@ class PromptConfig:
43
43
  - QueryParams.systemPrompt → system
44
44
  - options.customSystemPrompt → override_system
45
45
  - options.appendSystemPrompt → append_system
46
- - options.thinkingConfig → thinking_enabled
46
+ - options.thinkingConfig → thinking_generation_enabled
47
47
  """
48
48
  system: str | SystemMessage | None = None
49
49
  override_system: str | None = None
50
50
  append_system: str | None = None
51
51
  dynamic_sections: tuple[SystemPromptSection, ...] = ()
52
- thinking_enabled: bool = True # 是否启用 thinking(对齐 CC thinkingConfig)
52
+ thinking_generation_enabled: bool = True # 是否启用 request-side thinking 生成(对齐 CC thinkingConfig)
53
53
 
54
54
 
55
55
  @dataclass(frozen=True)
@@ -6,7 +6,7 @@
6
6
 
7
7
  链路位置:
8
8
  SubagentOrchestrator → ThinkingConfig.from_subagent_mode() →
9
- SubagentContext.thinking_enabled → AgentLoopConfig
9
+ SubagentContext.thinking_generation_enabled → AgentLoopConfig
10
10
 
11
11
  当前裁剪范围:
12
12
  v1 基础实现:mode 驱动启用/禁用;预留父配置覆盖路径。
@@ -63,7 +63,7 @@ class ThinkingConfig:
63
63
  Args:
64
64
  mode: Subagent 模式 ("plain" | "fork" | "coordinate")
65
65
  parent_thinking_config: 父 ThinkingConfig 或 dict(优先于 mode 规则)
66
- parent_thinking_enabled: 父 ToolExecutionContext.thinking_enabled(fork/coordinate 继承)
66
+ parent_thinking_enabled: 父 ToolExecutionContext.thinking_generation_enabled(fork/coordinate 继承)
67
67
 
68
68
  Returns:
69
69
  ThinkingConfig 实例
@@ -23,7 +23,7 @@ class UiDisplayContext:
23
23
 
24
24
  is_transcript_mode: bool = False
25
25
  verbose: bool = False
26
- thinking_enabled: bool | None = None
26
+ show_thinking: bool | None = None
27
27
 
28
28
  @classmethod
29
29
  def from_mapping(cls, raw: Mapping[str, Any] | None) -> UiDisplayContext | None:
@@ -31,12 +31,12 @@ class UiDisplayContext:
31
31
  return None
32
32
  is_transcript = bool(raw.get("is_transcript_mode", False))
33
33
  verbose = bool(raw.get("verbose", False))
34
- thinking_raw = raw.get("thinking_enabled")
35
- thinking_enabled = None if thinking_raw is None else bool(thinking_raw)
34
+ show_thinking_raw = raw.get("show_thinking")
35
+ show_thinking = None if show_thinking_raw is None else bool(show_thinking_raw)
36
36
  return cls(
37
37
  is_transcript_mode=is_transcript,
38
38
  verbose=verbose,
39
- thinking_enabled=thinking_enabled,
39
+ show_thinking=show_thinking,
40
40
  )
41
41
 
42
42
  @classmethod
@@ -1,5 +1,6 @@
1
1
  """LangChain AgentX 退出语义与状态字段。"""
2
2
 
3
+ from .query_source import QuerySource
3
4
  from .resolution import (
4
5
  ContinueResolution,
5
6
  LoopResolution,
@@ -10,6 +11,7 @@ from .resolution import (
10
11
  from .terminal import LoopTerminal, build_loop_terminal
11
12
 
12
13
  __all__ = [
14
+ "QuerySource",
13
15
  "ContinueResolution",
14
16
  "LoopResolution",
15
17
  "TerminalResolution",
@@ -64,6 +64,7 @@ from ..model.model_nodes import make_model_nodes
64
64
  from ..context import default_loop_context_compaction
65
65
  from ..context.settings import CompactionSettings
66
66
  from ..injection.dedup import filter_attachment_dicts_by_task_command_id
67
+ from ..message_contract import to_record
67
68
  from ..model.tool_call_degradation_corrector import (
68
69
  ToolCallDegradationCorrector,
69
70
  _build_allowed_tool_names_for_degradation_correction,
@@ -302,7 +303,7 @@ def _flatten_config_for_runtime(config: AgentLoopConfig) -> _FlatLoopConfig:
302
303
  model_profile_registry=config.model_profile_registry,
303
304
  compact_buffer_tokens=config.compact_buffer_tokens,
304
305
  max_output_tokens_reserved=config.max_output_tokens_reserved,
305
- thinking_enabled=config.prompt.thinking_enabled,
306
+ thinking_generation_enabled=config.prompt.thinking_generation_enabled,
306
307
  )
307
308
 
308
309
 
@@ -955,7 +956,7 @@ class LoopGraphBuilder:
955
956
  if not memory_attachments:
956
957
  return patch
957
958
  messages = list(patch.get("messages") or state.get("messages") or [])
958
- messages.extend(memory_attachments)
959
+ messages.extend(to_record(m) for m in memory_attachments)
959
960
  patch = dict(patch)
960
961
  patch["messages"] = messages
961
962
  return patch
@@ -1545,7 +1546,7 @@ class LoopGraphBuilder:
1545
1546
  # 冻结契约:task 通知附件仅经本节点注入;按 command_id 与已有 messages 去重
1546
1547
  # (见 docs/design-docs/loop/agent-loop/post-tools-injection-contract.md)
1547
1548
  to_attach = filter_attachment_dicts_by_task_command_id(
1548
- messages, list(batch.attachment_messages)
1549
+ messages, [to_record(m) for m in batch.attachment_messages]
1549
1550
  )
1550
1551
  messages.extend(to_attach)
1551
1552
  emit_task_event(
@@ -0,0 +1,72 @@
1
+ """Loop 级消息契约公开入口(re-export façade)。
2
+
3
+ 职责:
4
+ 对外稳定 import 路径;内部实现位于 ``loop.messages`` 包(contract / validate /
5
+ langgraph_adapter / transcript_codec)。
6
+
7
+ 链路位置:
8
+ subagent · session · memory recoverable 边界 → 本模块 → loop.messages.*
9
+
10
+ 当前裁剪范围:
11
+ Phase 4 已拆包;LangGraph 为当前唯一 engine adapter;第二引擎仅预留 Protocol。
12
+ """
13
+
14
+ from langchain_agentx.loop.messages.contract import (
15
+ LoopMessageContract,
16
+ build_engine_input,
17
+ from_transcript_row,
18
+ get_message_contract,
19
+ to_engine_message,
20
+ to_record,
21
+ to_transcript_row,
22
+ to_transcript_tool_row,
23
+ validate_record,
24
+ )
25
+ from langchain_agentx.loop.messages.engine_protocol import EngineMessageAdapter
26
+ from langchain_agentx.loop.messages.langgraph_adapter import LangGraphMessageAdapter
27
+ from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
28
+ from langchain_agentx.loop.messages.transcript_codec import TranscriptCodec
29
+ from langchain_agentx.loop.messages.types import (
30
+ EngineName,
31
+ MessageSurface,
32
+ PROVIDER_EXTENSIONS_KEY,
33
+ REASONING_CONTENT_KEY,
34
+ RECORD_CLASS_KEY,
35
+ RecordClass,
36
+ SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX,
37
+ ValidationResult,
38
+ is_observational_record,
39
+ is_synthetic_transcript_tool_id,
40
+ merge_provider_extensions,
41
+ normalize_role,
42
+ )
43
+ from langchain_agentx.loop.messages.validate import MessageRecordValidator
44
+
45
+ __all__ = [
46
+ "EngineMessageAdapter",
47
+ "EngineName",
48
+ "LangGraphMessageAdapter",
49
+ "LoopMessageContract",
50
+ "MessageRecordNormalizer",
51
+ "MessageRecordValidator",
52
+ "MessageSurface",
53
+ "PROVIDER_EXTENSIONS_KEY",
54
+ "REASONING_CONTENT_KEY",
55
+ "RECORD_CLASS_KEY",
56
+ "RecordClass",
57
+ "SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX",
58
+ "TranscriptCodec",
59
+ "ValidationResult",
60
+ "build_engine_input",
61
+ "from_transcript_row",
62
+ "get_message_contract",
63
+ "is_observational_record",
64
+ "is_synthetic_transcript_tool_id",
65
+ "merge_provider_extensions",
66
+ "normalize_role",
67
+ "to_engine_message",
68
+ "to_record",
69
+ "to_transcript_row",
70
+ "to_transcript_tool_row",
71
+ "validate_record",
72
+ ]
@@ -0,0 +1,63 @@
1
+ """loop.messages — Message Contract 内部分层(contract / validate / adapter / codec)。
2
+
3
+ 职责:
4
+ 实现 loop 级 message contract 内部分层;对外稳定入口为 ``loop.message_contract``。
5
+
6
+ 链路位置:
7
+ subagent · session · memory · workflow recoverable 边界经 façade 间接使用本包。
8
+
9
+ 当前裁剪范围:
10
+ consumer 禁止直接 import adapter/codec;仅 contract 测试与 package 内互引用允许。
11
+ """
12
+
13
+ from langchain_agentx.loop.messages.contract import (
14
+ LoopMessageContract,
15
+ build_engine_input,
16
+ from_transcript_row,
17
+ get_message_contract,
18
+ to_engine_message,
19
+ to_record,
20
+ to_transcript_row,
21
+ to_transcript_tool_row,
22
+ validate_record,
23
+ )
24
+ from langchain_agentx.loop.messages.langgraph_adapter import LangGraphMessageAdapter
25
+ from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
26
+ from langchain_agentx.loop.messages.transcript_codec import TranscriptCodec
27
+ from langchain_agentx.loop.messages.types import (
28
+ EngineName,
29
+ MessageSurface,
30
+ RECORD_CLASS_KEY,
31
+ RecordClass,
32
+ SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX,
33
+ ValidationResult,
34
+ is_observational_record,
35
+ is_synthetic_transcript_tool_id,
36
+ normalize_role,
37
+ )
38
+ from langchain_agentx.loop.messages.validate import MessageRecordValidator
39
+
40
+ __all__ = [
41
+ "EngineName",
42
+ "LangGraphMessageAdapter",
43
+ "LoopMessageContract",
44
+ "MessageRecordNormalizer",
45
+ "MessageRecordValidator",
46
+ "MessageSurface",
47
+ "RECORD_CLASS_KEY",
48
+ "RecordClass",
49
+ "SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX",
50
+ "TranscriptCodec",
51
+ "ValidationResult",
52
+ "build_engine_input",
53
+ "from_transcript_row",
54
+ "get_message_contract",
55
+ "is_observational_record",
56
+ "is_synthetic_transcript_tool_id",
57
+ "normalize_role",
58
+ "to_engine_message",
59
+ "to_record",
60
+ "to_transcript_row",
61
+ "to_transcript_tool_row",
62
+ "validate_record",
63
+ ]
@@ -0,0 +1,194 @@
1
+ """LoopMessageContract façade 与模块级便捷 API。
2
+
3
+ 职责:
4
+ 组合 normalizer / validator / transcript_codec / engine adapter,对外提供 loop 级契约 API。
5
+
6
+ 链路位置:
7
+ ``loop.message_contract`` re-export 本模块;consumer 应经 façade 调用,不直接依赖 adapter。
8
+
9
+ 当前裁剪范围:
10
+ 仅 LangGraph engine;``EngineMessageAdapter`` 协议预留第二引擎,本阶段不实现。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from langchain_core.messages import BaseMessage
18
+
19
+ from langchain_agentx.loop.messages.langgraph_adapter import LangGraphMessageAdapter
20
+ from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
21
+ from langchain_agentx.loop.messages.transcript_codec import TranscriptCodec
22
+ from langchain_agentx.loop.messages.types import EngineName, MessageSurface, ValidationResult
23
+ from langchain_agentx.loop.messages.validate import MessageRecordValidator
24
+
25
+
26
+ class LoopMessageContract:
27
+ """Loop 级消息契约 owner(对外 façade)。"""
28
+
29
+ def __init__(self) -> None:
30
+ self._normalizer = MessageRecordNormalizer()
31
+ self._validator = MessageRecordValidator()
32
+ self._codec = TranscriptCodec(self._normalizer, self._validator)
33
+ self._adapters: dict[EngineName, LangGraphMessageAdapter] = {
34
+ "langgraph": LangGraphMessageAdapter(self._normalizer),
35
+ }
36
+
37
+ def to_record(
38
+ self,
39
+ msg: Any,
40
+ *,
41
+ surface: MessageSurface = MessageSurface.RECOVERABLE,
42
+ ) -> dict[str, Any]:
43
+ """归一化为 canonical record;surface 仅作调用方语义标记,不在此步抛错。"""
44
+ del surface
45
+ return self._normalizer.to_record(msg)
46
+
47
+ def validate(
48
+ self,
49
+ record: dict[str, Any],
50
+ *,
51
+ surface: MessageSurface,
52
+ ) -> ValidationResult:
53
+ normalized = self._normalizer.normalize_dict(record)
54
+ return self._validator.validate(normalized, surface=surface)
55
+
56
+ def to_engine_message(
57
+ self,
58
+ record: dict[str, Any],
59
+ *,
60
+ engine: EngineName = "langgraph",
61
+ ) -> BaseMessage:
62
+ adapter = self._adapters.get(engine)
63
+ if adapter is None:
64
+ raise ValueError(f"unsupported engine adapter: {engine!r}")
65
+ return adapter.to_engine_message(record)
66
+
67
+ def build_engine_input(
68
+ self,
69
+ records: list[Any],
70
+ *,
71
+ surface: MessageSurface = MessageSurface.RECOVERABLE,
72
+ engine: EngineName = "langgraph",
73
+ ) -> list[BaseMessage]:
74
+ messages: list[BaseMessage] = []
75
+ for item in records:
76
+ if isinstance(item, BaseMessage):
77
+ messages.append(item)
78
+ continue
79
+ record = (
80
+ item
81
+ if isinstance(item, dict) and item.get("role")
82
+ else self._normalizer.to_record(item)
83
+ )
84
+ result = self._validator.validate(record, surface=surface)
85
+ if not result.ok or result.record is None:
86
+ raise ValueError(result.error or "invalid message record")
87
+ messages.append(self.to_engine_message(result.record, engine=engine))
88
+ return messages
89
+
90
+ def to_transcript_row(self, record: dict[str, Any]) -> dict[str, Any]:
91
+ return self._codec.to_transcript_row(record)
92
+
93
+ def to_transcript_tool_row(
94
+ self,
95
+ *,
96
+ content: str,
97
+ tool_call_id: str,
98
+ name: str,
99
+ synthetic: bool = False,
100
+ ) -> dict[str, Any]:
101
+ return self._codec.to_transcript_tool_row(
102
+ content=content,
103
+ tool_call_id=tool_call_id,
104
+ name=name,
105
+ synthetic=synthetic,
106
+ )
107
+
108
+ def from_transcript_row(
109
+ self,
110
+ row: dict[str, Any],
111
+ *,
112
+ surface: MessageSurface = MessageSurface.RECOVERABLE,
113
+ ) -> dict[str, Any]:
114
+ return self._codec.from_transcript_row(row, surface=surface)
115
+
116
+
117
+ _default_contract = LoopMessageContract()
118
+
119
+
120
+ def get_message_contract() -> LoopMessageContract:
121
+ return _default_contract
122
+
123
+
124
+ def to_record(
125
+ msg: Any,
126
+ *,
127
+ surface: MessageSurface = MessageSurface.RECOVERABLE,
128
+ ) -> dict[str, Any]:
129
+ return _default_contract.to_record(msg, surface=surface)
130
+
131
+
132
+ def validate_record(
133
+ record: dict[str, Any],
134
+ *,
135
+ surface: MessageSurface,
136
+ ) -> ValidationResult:
137
+ return _default_contract.validate(record, surface=surface)
138
+
139
+
140
+ def to_engine_message(
141
+ record: dict[str, Any],
142
+ *,
143
+ engine: EngineName = "langgraph",
144
+ ) -> BaseMessage:
145
+ return _default_contract.to_engine_message(record, engine=engine)
146
+
147
+
148
+ def build_engine_input(
149
+ records: list[Any],
150
+ *,
151
+ surface: MessageSurface = MessageSurface.RECOVERABLE,
152
+ engine: EngineName = "langgraph",
153
+ ) -> list[BaseMessage]:
154
+ return _default_contract.build_engine_input(records, surface=surface, engine=engine)
155
+
156
+
157
+ def from_transcript_row(
158
+ row: dict[str, Any],
159
+ *,
160
+ surface: MessageSurface = MessageSurface.RECOVERABLE,
161
+ ) -> dict[str, Any]:
162
+ return _default_contract.from_transcript_row(row, surface=surface)
163
+
164
+
165
+ def to_transcript_tool_row(
166
+ *,
167
+ content: str,
168
+ tool_call_id: str,
169
+ name: str,
170
+ synthetic: bool = False,
171
+ ) -> dict[str, Any]:
172
+ return _default_contract.to_transcript_tool_row(
173
+ content=content,
174
+ tool_call_id=tool_call_id,
175
+ name=name,
176
+ synthetic=synthetic,
177
+ )
178
+
179
+
180
+ def to_transcript_row(record: dict[str, Any]) -> dict[str, Any]:
181
+ return _default_contract.to_transcript_row(record)
182
+
183
+
184
+ __all__ = [
185
+ "LoopMessageContract",
186
+ "build_engine_input",
187
+ "from_transcript_row",
188
+ "get_message_contract",
189
+ "to_engine_message",
190
+ "to_record",
191
+ "to_transcript_row",
192
+ "to_transcript_tool_row",
193
+ "validate_record",
194
+ ]
@@ -0,0 +1,29 @@
1
+ """Engine adapter 协议:canonical record ↔ 执行引擎消息(Phase 4 预留第二引擎)。
2
+
3
+ 职责:
4
+ ``EngineMessageAdapter`` Protocol 定义 engine 边界;当前仅 LangGraph 实现。
5
+
6
+ 链路位置:
7
+ contract 通过 ``_adapters`` 字典按 ``EngineName`` 选择 adapter。
8
+
9
+ 当前裁剪范围:
10
+ 协议仅声明 ``to_engine_message``;不绑定 LangChain 具体消息类型。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Protocol
16
+
17
+ from langchain_agentx.loop.messages.types import EngineName
18
+
19
+
20
+ class EngineMessageAdapter(Protocol):
21
+ """执行引擎边界 adapter;当前仅 LangGraph 实现。"""
22
+
23
+ engine: EngineName
24
+
25
+ def to_engine_message(self, record: dict[str, Any]) -> Any:
26
+ """Canonical record → engine-native message object."""
27
+
28
+
29
+ __all__ = ["EngineMessageAdapter"]
@@ -0,0 +1,116 @@
1
+ """LangGraph / LangChain BaseMessage adapter(当前唯一 engine 实现)。
2
+
3
+ 职责:
4
+ canonical record → ``HumanMessage`` / ``AIMessage`` / ``ToolMessage`` 等 BaseMessage。
5
+
6
+ 链路位置:
7
+ contract.to_engine_message / build_engine_input 的 LangGraph 边界 adapter。
8
+
9
+ 当前裁剪范围:
10
+ user/assistant 多模态 content 透传;tool 使用 block-aware string 序列化(非裸 ``str(list)``)。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from typing import Any
17
+
18
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
19
+
20
+ from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
21
+ from langchain_agentx.loop.messages.types import EngineName, PROVIDER_EXTENSIONS_KEY, REASONING_CONTENT_KEY
22
+
23
+
24
+ def _engine_content(content: Any) -> str | list[Any]:
25
+ """LangChain BaseMessage 接受 str 或多模态 block list;其余标量转 str。"""
26
+ if isinstance(content, list):
27
+ return content
28
+ if isinstance(content, str):
29
+ return content
30
+ if content is None:
31
+ return ""
32
+ return str(content)
33
+
34
+
35
+ def _tool_content_for_engine(content: Any) -> str:
36
+ """ToolMessage 需要 string body;list/dict 做 block-aware 序列化,避免 silent str(list) 塌缩。"""
37
+ if isinstance(content, str):
38
+ return content
39
+ if content is None:
40
+ return ""
41
+ if isinstance(content, list):
42
+ parts: list[str] = []
43
+ for block in content:
44
+ if isinstance(block, dict):
45
+ block_type = block.get("type")
46
+ if block_type == "text":
47
+ text = block.get("text")
48
+ if isinstance(text, str) and text:
49
+ parts.append(text)
50
+ elif block_type == "thinking":
51
+ thinking = block.get("thinking")
52
+ if isinstance(thinking, str) and thinking:
53
+ parts.append(thinking)
54
+ else:
55
+ parts.append(json.dumps(block, ensure_ascii=False))
56
+ elif block is not None:
57
+ parts.append(str(block))
58
+ if parts:
59
+ return "\n".join(parts)
60
+ return json.dumps(content, ensure_ascii=False)
61
+ if isinstance(content, dict):
62
+ return json.dumps(content, ensure_ascii=False)
63
+ return str(content)
64
+
65
+
66
+ class LangGraphMessageAdapter:
67
+ """Canonical record → LangChain BaseMessage。"""
68
+
69
+ engine: EngineName = "langgraph"
70
+
71
+ def __init__(self, normalizer: MessageRecordNormalizer) -> None:
72
+ self._normalizer = normalizer
73
+
74
+ def to_engine_message(self, record: dict[str, Any]) -> BaseMessage:
75
+ normalized = self._normalizer.normalize_dict(record)
76
+ role = normalized["role"]
77
+ content = _engine_content(normalized.get("content", ""))
78
+
79
+ if role == "user":
80
+ return HumanMessage(content=content)
81
+ if role == "assistant":
82
+ additional_kwargs: dict[str, Any] = {}
83
+ extensions = normalized.get(PROVIDER_EXTENSIONS_KEY)
84
+ if isinstance(extensions, dict):
85
+ rc = extensions.get(REASONING_CONTENT_KEY)
86
+ if isinstance(rc, str) and rc:
87
+ additional_kwargs[REASONING_CONTENT_KEY] = rc
88
+ return AIMessage(
89
+ content=content,
90
+ tool_calls=list(normalized.get("tool_calls") or []),
91
+ additional_kwargs=additional_kwargs or {},
92
+ )
93
+ if role == "system":
94
+ return SystemMessage(content=content)
95
+ if role == "tool":
96
+ tool_call_id = normalized.get("tool_call_id")
97
+ if not isinstance(tool_call_id, str) or not tool_call_id:
98
+ raise ValueError(
99
+ "tool message record missing tool_call_id; "
100
+ "cannot convert to LangGraph ToolMessage / add_messages"
101
+ )
102
+ name = str(normalized.get("name") or "tool")
103
+ status = normalized.get("status")
104
+ kwargs: dict[str, Any] = {
105
+ "content": _tool_content_for_engine(normalized.get("content", "")),
106
+ "tool_call_id": tool_call_id,
107
+ "name": name,
108
+ }
109
+ if isinstance(status, str) and status:
110
+ kwargs["status"] = status
111
+ return ToolMessage(**kwargs)
112
+
113
+ return HumanMessage(content=content)
114
+
115
+
116
+ __all__ = ["LangGraphMessageAdapter"]