langchain-agentx-python 1.2.2__py3-none-any.whl → 1.2.5__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 (36) hide show
  1. langchain_agentx/__init__.py +64 -64
  2. langchain_agentx/command/prompt_shell.py +123 -123
  3. langchain_agentx/loop/config/loop_config.py +2 -0
  4. langchain_agentx/loop/config/thinking_config.py +105 -0
  5. langchain_agentx/loop/config/ui_display_context.py +58 -0
  6. langchain_agentx/loop/context/settings.py +109 -109
  7. langchain_agentx/loop/exit/__init__.py +2 -0
  8. langchain_agentx/loop/graph/factory.py +1 -0
  9. langchain_agentx/loop/model/model_node.py +45 -2
  10. langchain_agentx/loop/subagent/cache_safe_params.py +119 -0
  11. langchain_agentx/loop/subagent/context.py +16 -2
  12. langchain_agentx/loop/subagent/display_projector.py +36 -0
  13. langchain_agentx/loop/subagent/graph.py +4 -1
  14. langchain_agentx/loop/subagent/orchestrator.py +33 -1
  15. langchain_agentx/loop/subagent/progress.py +47 -2
  16. langchain_agentx/loop/subagent/reasoning_extractor.py +42 -0
  17. langchain_agentx/loop/subagent/runner.py +42 -12
  18. langchain_agentx/observability/events/langchain_agentx_event_adapter.py +1 -0
  19. langchain_agentx/observability/events/subagent_event_mapper.py +5 -0
  20. langchain_agentx/provider/compatible_chat_openai.py +4 -0
  21. langchain_agentx/provider/semantics/__init__.py +28 -0
  22. langchain_agentx/provider/semantics/contracts.py +7 -0
  23. langchain_agentx/provider/semantics/profile.py +77 -6
  24. langchain_agentx/provider/semantics/thinking_capability.py +244 -0
  25. langchain_agentx/tool_runtime/adapter.py +26 -0
  26. langchain_agentx/tool_runtime/models.py +9 -0
  27. langchain_agentx/tool_runtime/policy_decorator.py +70 -70
  28. langchain_agentx/tools/agent/tool.py +11 -4
  29. langchain_agentx/tools/bash/backend.py +17 -1
  30. langchain_agentx/tools/bash/boundary_check.py +236 -236
  31. langchain_agentx/tools/bash/tool.py +7 -0
  32. {langchain_agentx_python-1.2.2.dist-info → langchain_agentx_python-1.2.5.dist-info}/METADATA +594 -594
  33. {langchain_agentx_python-1.2.2.dist-info → langchain_agentx_python-1.2.5.dist-info}/RECORD +36 -30
  34. {langchain_agentx_python-1.2.2.dist-info → langchain_agentx_python-1.2.5.dist-info}/LICENSE +0 -0
  35. {langchain_agentx_python-1.2.2.dist-info → langchain_agentx_python-1.2.5.dist-info}/WHEEL +0 -0
  36. {langchain_agentx_python-1.2.2.dist-info → langchain_agentx_python-1.2.5.dist-info}/top_level.txt +0 -0
@@ -1,64 +1,64 @@
1
- """
2
- LangChain-based agent utilities for CodeBaseX.
3
-
4
- This package provides:
5
- - LangChain-AgentX LangGraph agent factory (`loop`)
6
-
7
- Typical usage:
8
-
9
- ```python
10
- from langchain_agentx import create_loop_agent
11
- ```
12
- """
13
-
14
- __version__ = "1.2.2"
15
-
16
- from .loop import ( # noqa: F401
17
- create_loop_agent,
18
- )
19
-
20
- from .loop.hook import ( # noqa: F401
21
- ATTACHMENT_HOOK_BLOCKING_ERROR,
22
- ATTACHMENT_HOOK_CANCELLED,
23
- ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
24
- ATTACHMENT_HOOK_STOPPED_CONTINUATION,
25
- ATTACHMENT_HOOK_SUCCESS,
26
- )
27
- from .observability import ( # noqa: F401
28
- EventAdapterState,
29
- LangchainAgentEvent,
30
- LangchainAgentEventType,
31
- LangGraphToLangchainAgentEventAdapter,
32
- TraceCollector,
33
- TraceSession,
34
- TraceSpan,
35
- )
36
- from .provider import CompatibleChatOpenAI # noqa: F401
37
- from .session import AgentSession, create_agent_session # noqa: F401
38
- from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
39
- from .workflow import BaseWorkflow # noqa: F401
40
-
41
- __all__ = [
42
- "ATTACHMENT_HOOK_BLOCKING_ERROR",
43
- "ATTACHMENT_HOOK_CANCELLED",
44
- "ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
45
- "ATTACHMENT_HOOK_STOPPED_CONTINUATION",
46
- "ATTACHMENT_HOOK_SUCCESS",
47
- "create_loop_agent",
48
- "create_agent_session",
49
- "AgentSession",
50
- "BaseWorkflow",
51
- "EventAdapterState",
52
- "LangchainAgentEvent",
53
- "LangchainAgentEventType",
54
- "LangGraphToLangchainAgentEventAdapter",
55
- "TraceCollector",
56
- "TraceSession",
57
- "TraceSpan",
58
- "CompatibleChatOpenAI",
59
- "HealthCheck",
60
- "HealthReport",
61
- "PreflightResult",
62
- "preflight",
63
- "sdk_health",
64
- ]
1
+ """
2
+ LangChain-based agent utilities for CodeBaseX.
3
+
4
+ This package provides:
5
+ - LangChain-AgentX LangGraph agent factory (`loop`)
6
+
7
+ Typical usage:
8
+
9
+ ```python
10
+ from langchain_agentx import create_loop_agent
11
+ ```
12
+ """
13
+
14
+ __version__ = "1.2.5"
15
+
16
+ from .loop import ( # noqa: F401
17
+ create_loop_agent,
18
+ )
19
+
20
+ from .loop.hook import ( # noqa: F401
21
+ ATTACHMENT_HOOK_BLOCKING_ERROR,
22
+ ATTACHMENT_HOOK_CANCELLED,
23
+ ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
24
+ ATTACHMENT_HOOK_STOPPED_CONTINUATION,
25
+ ATTACHMENT_HOOK_SUCCESS,
26
+ )
27
+ from .observability import ( # noqa: F401
28
+ EventAdapterState,
29
+ LangchainAgentEvent,
30
+ LangchainAgentEventType,
31
+ LangGraphToLangchainAgentEventAdapter,
32
+ TraceCollector,
33
+ TraceSession,
34
+ TraceSpan,
35
+ )
36
+ from .provider import CompatibleChatOpenAI # noqa: F401
37
+ from .session import AgentSession, create_agent_session # noqa: F401
38
+ from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
39
+ from .workflow import BaseWorkflow # noqa: F401
40
+
41
+ __all__ = [
42
+ "ATTACHMENT_HOOK_BLOCKING_ERROR",
43
+ "ATTACHMENT_HOOK_CANCELLED",
44
+ "ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
45
+ "ATTACHMENT_HOOK_STOPPED_CONTINUATION",
46
+ "ATTACHMENT_HOOK_SUCCESS",
47
+ "create_loop_agent",
48
+ "create_agent_session",
49
+ "AgentSession",
50
+ "BaseWorkflow",
51
+ "EventAdapterState",
52
+ "LangchainAgentEvent",
53
+ "LangchainAgentEventType",
54
+ "LangGraphToLangchainAgentEventAdapter",
55
+ "TraceCollector",
56
+ "TraceSession",
57
+ "TraceSpan",
58
+ "CompatibleChatOpenAI",
59
+ "HealthCheck",
60
+ "HealthReport",
61
+ "PreflightResult",
62
+ "preflight",
63
+ "sdk_health",
64
+ ]
@@ -1,123 +1,123 @@
1
- """
2
- command/prompt_shell.py — Prompt 内嵌 shell 命令执行(CC executeShellCommandsInPrompt 对齐)。
3
-
4
- 职责:
5
- 解析并执行 prompt 中的 !`command` 占位,将输出内联替换;执行前走路由权限检查。
6
-
7
- 链路位置:
8
- 由 command/builtin/commit* 在展开 prompt 前调用。
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import re
14
- from collections.abc import Callable, Sequence
15
- from pathlib import Path
16
- from typing import Protocol
17
-
18
- from langchain_agentx.tool_runtime.models import ToolExecutionContext
19
- from langchain_agentx.tool_runtime.permission_context import set_command_bash_allow_rules
20
- from langchain_agentx.tool_runtime.session_store import AgentSessionStore
21
- from langchain_agentx.utils.subprocess_text import TextSubprocessRunner, default_text_subprocess_runner
22
-
23
- _INLINE_PATTERN = re.compile(r"!`([^`]+)`")
24
-
25
-
26
- class PromptShellPermissionError(RuntimeError):
27
- """prompt 内嵌 shell 命令未通过 Bash 权限检查。"""
28
-
29
-
30
- class _ShellRunner(Protocol):
31
- def run_text(
32
- self,
33
- argv: list[str],
34
- *,
35
- cwd: str | Path | None = None,
36
- timeout_sec: float | None = None,
37
- ) -> tuple[int, str, str]: ...
38
-
39
-
40
- def _default_runner_call(
41
- runner: TextSubprocessRunner,
42
- command: str,
43
- *,
44
- cwd: str | Path | None,
45
- ) -> tuple[int, str, str]:
46
- completed = runner.run(
47
- ["bash", "-lc", command],
48
- cwd=cwd,
49
- timeout=30.0,
50
- capture_output=True,
51
- )
52
- stdout = completed.stdout or ""
53
- stderr = completed.stderr or ""
54
- if completed.returncode != 0 and not stdout.strip():
55
- return completed.returncode, "", stderr.strip() or f"exit {completed.returncode}"
56
- return completed.returncode, stdout, stderr
57
-
58
-
59
- def _assert_prompt_shell_permission(
60
- command: str,
61
- *,
62
- bash_allow_rules: Sequence[str] | None,
63
- cwd: str | Path | None,
64
- ) -> None:
65
- from langchain_agentx.tools.bash.tool import BashRuntimeTool
66
-
67
- store = AgentSessionStore("prompt-shell")
68
- if bash_allow_rules:
69
- set_command_bash_allow_rules(store, bash_allow_rules)
70
- workspace_root = str(cwd) if cwd is not None else None
71
- ctx = ToolExecutionContext(
72
- tool_name="Bash",
73
- tool_call_id="prompt-shell",
74
- input_args={"command": command},
75
- state={},
76
- session_store=store,
77
- workspace_root=workspace_root,
78
- cwd=workspace_root,
79
- )
80
- l1 = BashRuntimeTool().run_l1_permission_check({"command": command}, ctx)
81
- if l1.outcome == "deny":
82
- message = (l1.decision.message if l1.decision else None) or "Permission denied"
83
- raise PromptShellPermissionError(
84
- f'Shell command permission check failed for "!`{command}`": {message}',
85
- )
86
-
87
-
88
- async def execute_shell_commands_in_prompt(
89
- text: str,
90
- *,
91
- cwd: str | Path | None = None,
92
- runner: TextSubprocessRunner | None = None,
93
- shell_runner: Callable[[str], tuple[int, str, str]] | None = None,
94
- bash_allow_rules: Sequence[str] | None = None,
95
- check_permissions: bool = True,
96
- ) -> str:
97
- """将 text 中所有 !`command` 替换为命令 stdout(失败时内联 stderr/exit)。"""
98
- if "!`" not in text:
99
- return text
100
-
101
- text_runner = runner or default_text_subprocess_runner()
102
- result = text
103
-
104
- for match in list(_INLINE_PATTERN.finditer(text)):
105
- command = match.group(1).strip()
106
- if not command:
107
- continue
108
- if check_permissions and shell_runner is None:
109
- _assert_prompt_shell_permission(
110
- command,
111
- bash_allow_rules=bash_allow_rules,
112
- cwd=cwd,
113
- )
114
- if shell_runner is not None:
115
- _code, stdout, stderr = shell_runner(command)
116
- else:
117
- _code, stdout, stderr = _default_runner_call(text_runner, command, cwd=cwd)
118
- replacement = stdout.strip() if stdout.strip() else stderr.strip()
119
- if not replacement:
120
- replacement = "(no output)"
121
- result = result.replace(match.group(0), replacement, 1)
122
-
123
- return result
1
+ """
2
+ command/prompt_shell.py — Prompt 内嵌 shell 命令执行(CC executeShellCommandsInPrompt 对齐)。
3
+
4
+ 职责:
5
+ 解析并执行 prompt 中的 !`command` 占位,将输出内联替换;执行前走路由权限检查。
6
+
7
+ 链路位置:
8
+ 由 command/builtin/commit* 在展开 prompt 前调用。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from collections.abc import Callable, Sequence
15
+ from pathlib import Path
16
+ from typing import Protocol
17
+
18
+ from langchain_agentx.tool_runtime.models import ToolExecutionContext
19
+ from langchain_agentx.tool_runtime.permission_context import set_command_bash_allow_rules
20
+ from langchain_agentx.tool_runtime.session_store import AgentSessionStore
21
+ from langchain_agentx.utils.subprocess_text import TextSubprocessRunner, default_text_subprocess_runner
22
+
23
+ _INLINE_PATTERN = re.compile(r"!`([^`]+)`")
24
+
25
+
26
+ class PromptShellPermissionError(RuntimeError):
27
+ """prompt 内嵌 shell 命令未通过 Bash 权限检查。"""
28
+
29
+
30
+ class _ShellRunner(Protocol):
31
+ def run_text(
32
+ self,
33
+ argv: list[str],
34
+ *,
35
+ cwd: str | Path | None = None,
36
+ timeout_sec: float | None = None,
37
+ ) -> tuple[int, str, str]: ...
38
+
39
+
40
+ def _default_runner_call(
41
+ runner: TextSubprocessRunner,
42
+ command: str,
43
+ *,
44
+ cwd: str | Path | None,
45
+ ) -> tuple[int, str, str]:
46
+ completed = runner.run(
47
+ ["bash", "-lc", command],
48
+ cwd=cwd,
49
+ timeout=30.0,
50
+ capture_output=True,
51
+ )
52
+ stdout = completed.stdout or ""
53
+ stderr = completed.stderr or ""
54
+ if completed.returncode != 0 and not stdout.strip():
55
+ return completed.returncode, "", stderr.strip() or f"exit {completed.returncode}"
56
+ return completed.returncode, stdout, stderr
57
+
58
+
59
+ def _assert_prompt_shell_permission(
60
+ command: str,
61
+ *,
62
+ bash_allow_rules: Sequence[str] | None,
63
+ cwd: str | Path | None,
64
+ ) -> None:
65
+ from langchain_agentx.tools.bash.tool import BashRuntimeTool
66
+
67
+ store = AgentSessionStore("prompt-shell")
68
+ if bash_allow_rules:
69
+ set_command_bash_allow_rules(store, bash_allow_rules)
70
+ workspace_root = str(cwd) if cwd is not None else None
71
+ ctx = ToolExecutionContext(
72
+ tool_name="Bash",
73
+ tool_call_id="prompt-shell",
74
+ input_args={"command": command},
75
+ state={},
76
+ session_store=store,
77
+ workspace_root=workspace_root,
78
+ cwd=workspace_root,
79
+ )
80
+ l1 = BashRuntimeTool().run_l1_permission_check({"command": command}, ctx)
81
+ if l1.outcome == "deny":
82
+ message = (l1.decision.message if l1.decision else None) or "Permission denied"
83
+ raise PromptShellPermissionError(
84
+ f'Shell command permission check failed for "!`{command}`": {message}',
85
+ )
86
+
87
+
88
+ async def execute_shell_commands_in_prompt(
89
+ text: str,
90
+ *,
91
+ cwd: str | Path | None = None,
92
+ runner: TextSubprocessRunner | None = None,
93
+ shell_runner: Callable[[str], tuple[int, str, str]] | None = None,
94
+ bash_allow_rules: Sequence[str] | None = None,
95
+ check_permissions: bool = True,
96
+ ) -> str:
97
+ """将 text 中所有 !`command` 替换为命令 stdout(失败时内联 stderr/exit)。"""
98
+ if "!`" not in text:
99
+ return text
100
+
101
+ text_runner = runner or default_text_subprocess_runner()
102
+ result = text
103
+
104
+ for match in list(_INLINE_PATTERN.finditer(text)):
105
+ command = match.group(1).strip()
106
+ if not command:
107
+ continue
108
+ if check_permissions and shell_runner is None:
109
+ _assert_prompt_shell_permission(
110
+ command,
111
+ bash_allow_rules=bash_allow_rules,
112
+ cwd=cwd,
113
+ )
114
+ if shell_runner is not None:
115
+ _code, stdout, stderr = shell_runner(command)
116
+ else:
117
+ _code, stdout, stderr = _default_runner_call(text_runner, command, cwd=cwd)
118
+ replacement = stdout.strip() if stdout.strip() else stderr.strip()
119
+ if not replacement:
120
+ replacement = "(no output)"
121
+ result = result.replace(match.group(0), replacement, 1)
122
+
123
+ return result
@@ -43,11 +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_generation_enabled
46
47
  """
47
48
  system: str | SystemMessage | None = None
48
49
  override_system: str | None = None
49
50
  append_system: str | None = None
50
51
  dynamic_sections: tuple[SystemPromptSection, ...] = ()
52
+ thinking_generation_enabled: bool = True # 是否启用 request-side thinking 生成(对齐 CC thinkingConfig)
51
53
 
52
54
 
53
55
  @dataclass(frozen=True)
@@ -0,0 +1,105 @@
1
+ """loop/config/thinking_config.py — Thinking 生成配置
2
+
3
+ 职责:
4
+ 定义 ThinkingConfig 数据类,根据 subagent mode (plain/fork/coordinate)
5
+ 决定是否启用 thinking 功能。
6
+
7
+ 链路位置:
8
+ SubagentOrchestrator → ThinkingConfig.from_subagent_mode() →
9
+ SubagentContext.thinking_generation_enabled → AgentLoopConfig
10
+
11
+ 当前裁剪范围:
12
+ v1 基础实现:mode 驱动启用/禁用;预留父配置覆盖路径。
13
+
14
+ CC 对齐:
15
+ - runAgent.ts:682-683: plain mode 默认 { type: 'disabled' }
16
+ - fork mode 继承父配置或默认启用
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from typing import Any, Literal
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ThinkingConfig:
27
+ """Thinking 生成配置(对齐 CC thinkingConfig)。
28
+
29
+ Attributes:
30
+ enabled: 是否启用 thinking
31
+ type: Thinking 类型(预留,对齐 CC { type: 'enabled' | 'disabled' })
32
+ budget_tokens: Thinking token 预算(预留)
33
+ """
34
+ enabled: bool = True
35
+ type: Literal["enabled", "disabled", "auto"] = "auto"
36
+ budget_tokens: int | None = None
37
+
38
+ @classmethod
39
+ def disabled(cls) -> "ThinkingConfig":
40
+ """返回禁用配置(对齐 CC { type: 'disabled' })。"""
41
+ return cls(enabled=False, type="disabled")
42
+
43
+ @classmethod
44
+ def enabled_config(cls) -> "ThinkingConfig":
45
+ """返回启用配置(对齐 CC { type: 'enabled' })。"""
46
+ return cls(enabled=True, type="enabled")
47
+
48
+ @classmethod
49
+ def from_subagent_mode(
50
+ cls,
51
+ mode: str,
52
+ parent_thinking_config: Any | None = None,
53
+ *,
54
+ parent_thinking_enabled: bool | None = None,
55
+ ) -> "ThinkingConfig":
56
+ """根据 subagent mode 决定是否启用 thinking。
57
+
58
+ CC 对齐规则:
59
+ - plain mode: 禁用 thinking(除非父配置显式启用)
60
+ - fork mode: 继承父 thinking_enabled / thinkingConfig(对齐 runAgent useExactTools)
61
+ - coordinate mode: 同 fork,继承父配置
62
+
63
+ Args:
64
+ mode: Subagent 模式 ("plain" | "fork" | "coordinate")
65
+ parent_thinking_config: 父 ThinkingConfig 或 dict(优先于 mode 规则)
66
+ parent_thinking_enabled: 父 ToolExecutionContext.thinking_generation_enabled(fork/coordinate 继承)
67
+
68
+ Returns:
69
+ ThinkingConfig 实例
70
+ """
71
+ # 优先级 1:父显式 thinkingConfig
72
+ if parent_thinking_config is not None:
73
+ if isinstance(parent_thinking_config, dict):
74
+ parent_enabled = parent_thinking_config.get("enabled", True)
75
+ parent_type = parent_thinking_config.get("type", "auto")
76
+ return cls(
77
+ enabled=parent_enabled,
78
+ type=parent_type if parent_type in ("enabled", "disabled", "auto") else "auto",
79
+ )
80
+ elif isinstance(parent_thinking_config, ThinkingConfig):
81
+ return parent_thinking_config
82
+
83
+ # 优先级 2:mode 规则
84
+ if mode == "plain":
85
+ return cls.disabled()
86
+
87
+ # fork / coordinate:继承父 loop thinking_enabled(CC fork inherit)
88
+ if parent_thinking_enabled is not None:
89
+ enabled = bool(parent_thinking_enabled)
90
+ return cls(
91
+ enabled=enabled,
92
+ type="enabled" if enabled else "disabled",
93
+ )
94
+
95
+ return cls.enabled_config()
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ """转换为字典(传递给 Provider)。"""
99
+ return {
100
+ "enabled": self.enabled,
101
+ "type": self.type,
102
+ }
103
+
104
+
105
+ __all__ = ["ThinkingConfig"]
@@ -0,0 +1,58 @@
1
+ """loop/config/ui_display_context.py — UI 展示意图(CLI → SDK seam)
2
+
3
+ 职责:
4
+ 定义宿主(CLI)注入的 transcript / verbose / thinking 展示意图;
5
+ 供 ToolExecutionContext 与 SubagentOrchestrator 读取。
6
+
7
+ 链路位置:
8
+ RunnableConfig.configurable["ui_display_context"] → LangChainAdapter → ToolExecutionContext
9
+
10
+ 当前裁剪范围:
11
+ 仅数据契约与解析;不做 TUI 渲染(langchain_agentx_cli 职责)。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from typing import Any, Mapping
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class UiDisplayContext:
22
+ """宿主 UI 展示意图(对齐 CC verbose + isTranscriptMode)。"""
23
+
24
+ is_transcript_mode: bool = False
25
+ verbose: bool = False
26
+ show_thinking: bool | None = None
27
+
28
+ @classmethod
29
+ def from_mapping(cls, raw: Mapping[str, Any] | None) -> UiDisplayContext | None:
30
+ if not raw or not isinstance(raw, Mapping):
31
+ return None
32
+ is_transcript = bool(raw.get("is_transcript_mode", False))
33
+ verbose = bool(raw.get("verbose", False))
34
+ show_thinking_raw = raw.get("show_thinking")
35
+ show_thinking = None if show_thinking_raw is None else bool(show_thinking_raw)
36
+ return cls(
37
+ is_transcript_mode=is_transcript,
38
+ verbose=verbose,
39
+ show_thinking=show_thinking,
40
+ )
41
+
42
+ @classmethod
43
+ def from_configurable(cls, configurable: Mapping[str, Any] | None) -> UiDisplayContext | None:
44
+ if not configurable:
45
+ return None
46
+ raw = configurable.get("ui_display_context")
47
+ if isinstance(raw, UiDisplayContext):
48
+ return raw
49
+ if isinstance(raw, Mapping):
50
+ return cls.from_mapping(raw)
51
+ return None
52
+
53
+ def should_show_thinking_full(self) -> bool:
54
+ """对齐 CC Message.tsx:verbose || isTranscriptMode 时展示完整 thinking。"""
55
+ return self.verbose or self.is_transcript_mode
56
+
57
+
58
+ __all__ = ["UiDisplayContext"]