langchain-agentx-python 2.4.0__py3-none-any.whl → 2.4.4__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 (27) hide show
  1. langchain_agentx/__init__.py +1 -1
  2. langchain_agentx/loop/config/interaction_profile.py +6 -2
  3. langchain_agentx/loop/config/loop_config.py +2 -0
  4. langchain_agentx/loop/graph/factory.py +8 -1
  5. langchain_agentx/loop/run_context.py +48 -0
  6. langchain_agentx/loop/runtime/context_factory.py +32 -9
  7. langchain_agentx/loop/subagent/async_runner.py +12 -1
  8. langchain_agentx/loop/subagent/context.py +20 -0
  9. langchain_agentx/loop/subagent/definition_permission.py +73 -0
  10. langchain_agentx/loop/subagent/graph.py +55 -9
  11. langchain_agentx/loop/subagent/orchestrator.py +543 -366
  12. langchain_agentx/loop/subagent/runner.py +7 -5
  13. langchain_agentx/tool_runtime/adapter.py +5 -0
  14. langchain_agentx/tool_runtime/models.py +4 -1
  15. langchain_agentx/tool_runtime/prompt/session_mode.py +43 -3
  16. langchain_agentx/tools/agent/backend.py +33 -11
  17. langchain_agentx/tools/agent/limits.py +28 -3
  18. langchain_agentx/tools/agent/loader.py +35 -2
  19. langchain_agentx/tools/agent/nesting_policy.py +65 -0
  20. langchain_agentx/tools/agent/registry/config.py +2 -0
  21. langchain_agentx/tools/agent/scope.py +16 -15
  22. langchain_agentx/tools/agent/tool.py +14 -1
  23. {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/METADATA +1 -1
  24. {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/RECORD +27 -25
  25. {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/LICENSE +0 -0
  26. {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/WHEEL +0 -0
  27. {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.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__ = "2.4.0"
14
+ __version__ = "2.4.4"
15
15
 
16
16
  from .mcp.call import McpToolCaller
17
17
  from .mcp.client_manager import McpClientManager, McpConnectedClient
@@ -25,7 +25,10 @@ if TYPE_CHECKING:
25
25
  _WORKER_CONTAINER_TYPES = frozenset(
26
26
  {"background_task", "background", "batch", "worker", "sdk"}
27
27
  )
28
- _INTERACTIVE_HEADLESS_MODES = frozenset({"auto", "dontAsk", "bypassPermissions"})
28
+ # acceptEdits:headless 子图继承父写快路径(与 auto 同属写链放行 mode)
29
+ _INTERACTIVE_HEADLESS_MODES = frozenset(
30
+ {"auto", "dontAsk", "bypassPermissions", "acceptEdits"}
31
+ )
29
32
  _HITL_PROFILE_IDS = frozenset({"code-interactive", "code-plan"})
30
33
  _HEADLESS_PROFILE_IDS = frozenset(
31
34
  {"qa-auto", "workflow-auto", "worker-deny", "interactive-headless"}
@@ -120,7 +123,8 @@ class InteractionProfileResolver:
120
123
  if mode in ("default", "plan"):
121
124
  raise ValueError(
122
125
  f"interactive + headless=True cannot use permission_mode={mode!r}; "
123
- "use auto, dontAsk, or bypassPermissions (print/script entry)"
126
+ "use auto, dontAsk, bypassPermissions, or acceptEdits "
127
+ "(interactive-headless / print/script entry)"
124
128
  )
125
129
  if mode not in _INTERACTIVE_HEADLESS_MODES:
126
130
  raise ValueError(
@@ -136,6 +136,8 @@ class SubagentLinkage:
136
136
  subagent_type: str | None = None
137
137
  agent_depth: int = 0
138
138
  parent_agent_id: str | None = None
139
+ fork_from_parent: bool = False
140
+ """True = 当前子 loop 为 ForkA worker(会话继承 fork);供 ToolExecutionContext / L1。"""
139
141
 
140
142
 
141
143
  @dataclass(frozen=True, init=False)
@@ -312,7 +312,12 @@ def _flatten_config_for_runtime(config: AgentLoopConfig) -> _FlatLoopConfig:
312
312
  conversation_session_id=config.conversation_session_id,
313
313
  agent_id=config.agent_id,
314
314
  parent_agent_id=config.subagent.parent_agent_id,
315
- agent_depth=config.agent_depth,
315
+ agent_depth=(
316
+ config.subagent.agent_depth
317
+ if config.subagent.is_subagent
318
+ else config.agent_depth
319
+ ),
320
+ fork_from_parent=bool(config.subagent.fork_from_parent),
316
321
  subagent_type=config.subagent.subagent_type,
317
322
  trace_enabled=config.trace.enabled,
318
323
  trace_persist=config.trace.persist,
@@ -980,6 +985,7 @@ class LoopGraphBuilder:
980
985
  system_prompt=self._base_prompt,
981
986
  capabilities=self._services.capabilities,
982
987
  cwd=runtime_cwd,
988
+ permission_mode=self._config.permission_mode,
983
989
  boundary_snapshot=BoundarySnapshotPropagator.from_workspace_boundary(
984
990
  boundary=self._workspace_boundary,
985
991
  runtime_cwd=runtime_cwd,
@@ -1043,6 +1049,7 @@ class LoopGraphBuilder:
1043
1049
  system_prompt=base_prompt,
1044
1050
  capabilities=self._services.capabilities,
1045
1051
  cwd=runtime_cwd,
1052
+ permission_mode=self._config.permission_mode,
1046
1053
  boundary_snapshot=BoundarySnapshotPropagator.from_workspace_boundary(
1047
1054
  boundary=self._workspace_boundary,
1048
1055
  runtime_cwd=runtime_cwd,
@@ -112,6 +112,36 @@ async def run_under_owner_run_context(
112
112
  _current_run_context.reset(token)
113
113
 
114
114
 
115
+ @dataclass
116
+ class SubagentConcurrencyGate:
117
+ """会话级并发子 Agent 计数(挂在 RunContext.cancel_authority 根上)。
118
+
119
+ Doc-37 C9 / Phase 2:独立于深度;无「总 spawn」帽。
120
+ """
121
+
122
+ _running: int = field(default=0, repr=False)
123
+ _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
124
+
125
+ def try_acquire(self, *, limit: int) -> bool:
126
+ if limit < 1:
127
+ return False
128
+ with self._lock:
129
+ if self._running >= limit:
130
+ return False
131
+ self._running += 1
132
+ return True
133
+
134
+ def release(self) -> None:
135
+ with self._lock:
136
+ if self._running > 0:
137
+ self._running -= 1
138
+
139
+ @property
140
+ def running(self) -> int:
141
+ with self._lock:
142
+ return self._running
143
+
144
+
115
145
  @dataclass
116
146
  class RunContext:
117
147
  """运行上下文(Graph/Workflow/Agent 共享)。"""
@@ -127,6 +157,10 @@ class RunContext:
127
157
  # D-EVT-6:宿主在 cancel_tree / killAll 时写入;finish 投影读取;缺省 unknown。
128
158
  # 取值:escape | workflow_stop | kill_agents | unknown;禁止从「是否有后台」猜测。
129
159
  cancel_source: str | None = None
160
+ # Phase 2:并发子 Agent 槽位;acquire/release 始终走 cancel_authority 根上的 gate。
161
+ subagent_concurrency: SubagentConcurrencyGate = field(
162
+ default_factory=SubagentConcurrencyGate, repr=False
163
+ )
130
164
  # install 时若成功 register_agent_loop,记下以便 reset 精确 unregister(勿误弹外层)。
131
165
  _registered_agent_loop: asyncio.AbstractEventLoop | None = field(
132
166
  default=None, repr=False, compare=False
@@ -134,6 +168,18 @@ class RunContext:
134
168
  # install 时是否 push 进进程级 RunContext 栈(供 worker 解析 owner)。
135
169
  _registered_on_ctx_stack: bool = field(default=False, repr=False, compare=False)
136
170
 
171
+ def try_acquire_subagent_slot(self, *, limit: int) -> bool:
172
+ """在 outermost RunContext 上占用一个并发槽;满则 False。"""
173
+ return self.cancel_authority().subagent_concurrency.try_acquire(limit=limit)
174
+
175
+ def release_subagent_slot(self) -> None:
176
+ """释放 outermost 上的一个并发槽(幂等下限 0)。"""
177
+ self.cancel_authority().subagent_concurrency.release()
178
+
179
+ @property
180
+ def running_subagent_count(self) -> int:
181
+ return self.cancel_authority().subagent_concurrency.running
182
+
137
183
  def register_child(self, task: asyncio.Task) -> None:
138
184
  self.child_tasks.add(task)
139
185
 
@@ -412,6 +458,7 @@ def register_spawned_thread(
412
458
 
413
459
  __all__ = [
414
460
  "RunContext",
461
+ "SubagentConcurrencyGate",
415
462
  "create_task_with_copied_context",
416
463
  "drive_async_iterator_in_current_task",
417
464
  "get_run_context",
@@ -420,5 +467,6 @@ __all__ = [
420
467
  "register_spawned_thread",
421
468
  "reset_run_context",
422
469
  "resolve_cancel_authority",
470
+ "resolve_owner_run_context",
423
471
  "run_context_scope",
424
472
  ]
@@ -5,8 +5,17 @@ loop/runtime/context_factory.py — RuntimeContextFactory 继承规则单点
5
5
  继承优先级(单点定义):显式 override > SubagentConfig 默认 > 父 ctx 字段。
6
6
  在整体链路中的位置:SubagentOrchestrator.invoke_subagent() 调用。
7
7
  CC 对照:src/utils/forkedAgent.ts createSubagentContext() 中的字段合并逻辑。
8
- 当前裁剪范围:v1 model / max_steps / available_tools / agent_depth 继承;permission_mode 直通;
8
+ 当前裁剪范围:v1 model / max_steps / available_tools / agent_depth 继承;
9
+ permission_mode 经 read_parent_permission_mode 写入 AgentLoopConfig(derive 侧);
10
+ orchestrator 再经 resolve_definition_permission_mode 合并 definition/overrides 后
11
+ 注入 SubagentContext.permission_mode(构图 SSOT;见 dual-track / parent-wins bug 单);
9
12
  ``subagent_effective_cwd`` 由 ``SubagentExecutionPaths.prepare`` 注入(不在此 mkdir)。
13
+
14
+ 编排消费说明(防双轨幻觉):
15
+ ``derive`` 返回完整 ``AgentLoopConfig``,但 ``create_subagent_graph`` **不直接吃**该对象;
16
+ orchestrator 当前只抽出并注入子 ctx 的字段:``model`` / ``max_steps`` /
17
+ ``agent_depth`` / ``permission_mode``(及 path/workspace 旁路)。子图权限权威在
18
+ ``SubagentContext.permission_mode``,勿在 graph 再读父 runnable。
10
19
  """
11
20
 
12
21
  from __future__ import annotations
@@ -83,16 +92,22 @@ class RuntimeContextFactory:
83
92
  else list(parent_ctx.available_tools or [])
84
93
  )
85
94
 
86
- # 权限:override > 父 ctx.tool_flags["permission_mode"] > "default"
87
- parent_permission = (
88
- getattr(parent_ctx, "permission_mode", None)
89
- or getattr(getattr(parent_ctx, "tool_flags", None), "get", lambda x: None)("permission_mode")
90
- or "default"
95
+ # 权限:override > read_parent_permission_mode > "default"(AgentLoopConfig 侧)。
96
+ # 子图有效 mode(含 definition 父优先)由 orchestrator._resolve_effective_permission_mode
97
+ # 写入 SubagentContext.permission_mode;勿把本字段当作 definition merge 结果。
98
+ from langchain_agentx.tool_runtime.prompt.session_mode import (
99
+ read_parent_permission_mode,
91
100
  )
92
- resolved_permission = (
101
+
102
+ parent_permission = (
93
103
  overrides.permission_mode
94
104
  if overrides.permission_mode is not None
95
- else parent_permission
105
+ else read_parent_permission_mode(parent_ctx)
106
+ )
107
+ resolved_permission = (
108
+ parent_permission.strip()
109
+ if isinstance(parent_permission, str) and parent_permission.strip()
110
+ else "default"
96
111
  )
97
112
  workspace_root = getattr(parent_ctx, "workspace_root", None)
98
113
  parent_agent_home = getattr(parent_ctx, "agent_home_segment", None)
@@ -146,12 +161,17 @@ class RuntimeContextFactory:
146
161
  subagent_query_source = (
147
162
  f"agent:{subagent_type}" if subagent_type else "agent:default"
148
163
  )
164
+ # 子 depth 与顶层 agent_depth 必须一致:factory flatten 在 is_subagent 时
165
+ # 优先读 subagent.agent_depth(嵌套 L1→L2 若只写顶层会误成 0)。
166
+ child_depth = int(getattr(parent_ctx, "agent_depth", 0) or 0) + 1
149
167
 
168
+ # 完整 AgentLoopConfig 供编排/后台路径复用;构图 SSOT 仍经 SubagentContext
169
+ # (orchestrator 仅透传 model/max_steps/agent_depth/permission_mode 等)。
150
170
  return AgentLoopConfig(
151
171
  model=resolved_model,
152
172
  session_id=getattr(parent_ctx, "session_id", "") or "",
153
173
  agent_id=agent_id,
154
- agent_depth=(parent_ctx.agent_depth or 0) + 1,
174
+ agent_depth=child_depth,
155
175
  max_steps=resolved_max_steps,
156
176
  permission_mode=resolved_permission,
157
177
  model_retry=ModelRetrySettings(query_source=subagent_query_source),
@@ -171,6 +191,9 @@ class RuntimeContextFactory:
171
191
  ),
172
192
  subagent=SubagentLinkage(
173
193
  is_subagent=True,
194
+ subagent_type=str(subagent_type) if subagent_type else None,
174
195
  parent_agent_id=parent_agent_id,
196
+ agent_depth=child_depth,
197
+ # fork_from_parent 由 orchestrator mode 决定,derive 不知 mode → False
175
198
  ),
176
199
  )
@@ -175,8 +175,12 @@ async def _execute_background_lifecycle(
175
175
  task_id: str,
176
176
  task_runtime: Any | None,
177
177
  allowed_tool_names: set[str] | None = None,
178
+ slot_owner: RunContext | None = None,
178
179
  ) -> None:
179
- """共享后台生命周期:run_subagent → 写 output → task_runtime 终态。"""
180
+ """共享后台生命周期:run_subagent → 写 output → task_runtime 终态。
181
+
182
+ ``slot_owner``:Phase 2 并发槽从 orchestrator 移交至此;终态时 release。
183
+ """
180
184
  execution_ctx = ctx
181
185
  execution_ctx.is_async = False
182
186
  agent_id = getattr(ctx, "agent_id", None)
@@ -252,6 +256,9 @@ async def _execute_background_lifecycle(
252
256
  agent_id=agent_id,
253
257
  terminal_reason=TERMINAL_REASON_GRAPH_RUNTIME_ERROR,
254
258
  )
259
+ finally:
260
+ if slot_owner is not None:
261
+ slot_owner.release_subagent_slot()
255
262
 
256
263
 
257
264
  def register_background_agent(
@@ -264,6 +271,7 @@ def register_background_agent(
264
271
  parent_ctx: Any | None = None,
265
272
  owner_run_ctx: RunContext | None = None,
266
273
  allowed_tool_names: set[str] | None = None,
274
+ slot_owner: RunContext | None = None,
267
275
  ) -> BackgroundAgentHandle:
268
276
  """注册并启动后台子 Agent(仅同 loop C3;无/异 loop 拒绝,D-BG-1)。"""
269
277
  task_runtime = _resolve_task_runtime(parent_ctx)
@@ -307,6 +315,7 @@ def register_background_agent(
307
315
  task_runtime=task_runtime,
308
316
  owner_run_ctx=owner_run_ctx,
309
317
  allowed_tool_names=allowed_tool_names,
318
+ slot_owner=slot_owner,
310
319
  )
311
320
 
312
321
 
@@ -321,6 +330,7 @@ def _register_background_agent_task(
321
330
  task_runtime: Any | None,
322
331
  owner_run_ctx: RunContext | None,
323
332
  allowed_tool_names: set[str] | None = None,
333
+ slot_owner: RunContext | None = None,
324
334
  ) -> BackgroundAgentHandle:
325
335
  async def _task_body() -> None:
326
336
  await _execute_background_lifecycle(
@@ -331,6 +341,7 @@ def _register_background_agent_task(
331
341
  task_id=task_id,
332
342
  task_runtime=task_runtime,
333
343
  allowed_tool_names=allowed_tool_names,
344
+ slot_owner=slot_owner,
334
345
  )
335
346
 
336
347
  task = loop.create_task(_task_body(), name=f"agentx-subagent-{task_id}")
@@ -54,6 +54,7 @@ from langchain_agentx.loop.subagent.fork_worktree_notice import build_worktree_n
54
54
  from langchain_agentx.loop.message_contract import to_record
55
55
  from langchain_agentx.tool_runtime.prompt.session_mode import (
56
56
  read_parent_loop_config,
57
+ read_parent_permission_mode,
57
58
  resolve_subagent_headless,
58
59
  )
59
60
  from langchain_agentx.tool_runtime.session_store import AgentSessionStore
@@ -82,6 +83,8 @@ class SubagentContext:
82
83
  fork_from_parent: bool = False # True = fork 模式,initial_messages 含父会话历史
83
84
  is_async: bool = False # True = 走异步路径(coordinate/fork-async)
84
85
  headless: bool = True # CC 对齐:async 强制 True;sync 继承父 loop headless
86
+ permission_mode: str | None = None
87
+ """子图 permission_mode SSOT(derive/ctx 工厂写入;create_subagent_graph 只读本字段)。"""
85
88
 
86
89
  # 共享字段(从父会话继承)
87
90
  session_store: Any = None # SessionStore
@@ -177,6 +180,7 @@ def create_subagent_context(
177
180
  is_builtin_subagent: bool = False,
178
181
  recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
179
182
  inherit_hooks: bool | frozenset[HookEvent] | None = True,
183
+ permission_mode: str | None = None,
180
184
  ) -> SubagentContext:
181
185
  """工厂函数:从父上下文创建子 Agent 上下文
182
186
 
@@ -199,6 +203,7 @@ def create_subagent_context(
199
203
  fork_worktree_parent_cwd: 父 Agent 工作目录(须在 ``run_with_cwd_override`` 子 cwd 生效前由编排层捕获)。
200
204
  fork_worktree_path: 子 Agent 的 worktree 根路径(通常即 ``path_prep.worktree_dir``)。
201
205
  thinking_generation_enabled: 是否启用 request-side thinking(由 SubagentOrchestrator 根据 mode 决定)
206
+ permission_mode: 显式子 mode(orchestrator 传 derive 结果);None 时从父 runnable/tool_flags 读一次
202
207
 
203
208
  Returns:
204
209
  SubagentContext 实例
@@ -301,6 +306,12 @@ def create_subagent_context(
301
306
  is_async=is_async,
302
307
  )
303
308
 
309
+ # permission_mode SSOT:显式(derive)> 父 runnable/tool_flags 读一次;构图不再重读父
310
+ if isinstance(permission_mode, str) and permission_mode.strip():
311
+ resolved_permission_mode: str | None = permission_mode.strip()
312
+ else:
313
+ resolved_permission_mode = read_parent_permission_mode(parent_ctx)
314
+
304
315
  resolved_query_source = (
305
316
  query_source
306
317
  if isinstance(query_source, str) and query_source.strip()
@@ -331,6 +342,7 @@ def create_subagent_context(
331
342
  fork_from_parent=fork_from_parent,
332
343
  is_async=is_async,
333
344
  headless=resolved_headless,
345
+ permission_mode=resolved_permission_mode,
334
346
  session_store=session_store,
335
347
  tool_registry=getattr(parent_ctx, "tool_registry", None),
336
348
  tool_loader=getattr(parent_ctx, "tool_loader", None),
@@ -403,6 +415,8 @@ class ForkedParentRuntime:
403
415
  cwd: str | None = None
404
416
  thinking_generation_enabled: bool | None = None
405
417
  boundary_snapshot: BoundarySnapshot | None = None
418
+ permission_mode: str | None = None
419
+ """父 loop permission_mode;后台 fork 写入 SubagentContext SSOT。"""
406
420
 
407
421
 
408
422
  def _clone_parent_session_store(parent_store: Any) -> Any:
@@ -510,6 +524,12 @@ class SubagentContextFactory:
510
524
  fork_from_parent=True,
511
525
  is_async=False,
512
526
  headless=True,
527
+ permission_mode=(
528
+ parent_runtime.permission_mode.strip()
529
+ if isinstance(parent_runtime.permission_mode, str)
530
+ and parent_runtime.permission_mode.strip()
531
+ else None
532
+ ),
513
533
  session_store=session_store,
514
534
  tool_registry=parent_runtime.tool_registry,
515
535
  tool_loader=parent_runtime.tool_loader,
@@ -0,0 +1,73 @@
1
+ """loop/subagent/definition_permission.py — 子 Agent 定义 permissionMode 父优先合并
2
+
3
+ 职责:官网 Permission modes 规则核(纯函数);不读 AppState、不写 ctx。
4
+ 链路:SubagentConfig / overrides.declared + 父 mode → orchestrator → SubagentContext SSOT。
5
+ 裁剪:auto_available 本期恒 True(LOCK-10);无 plugin 忽略面;非法枚举由 loader 告警后置 None。
6
+
7
+ 规范化枚举见 ``tool_runtime.prompt.session_mode.normalize_definition_permission_mode``
8
+ (避免 tools.loader → loop 反向依赖)。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from langchain_agentx.tool_runtime.prompt.session_mode import (
14
+ normalize_definition_permission_mode,
15
+ )
16
+
17
+ _ELEVATED_PARENT_MODES = frozenset(
18
+ {
19
+ "bypassPermissions",
20
+ "acceptEdits",
21
+ "auto",
22
+ }
23
+ )
24
+
25
+
26
+ def _normalize_parent_mode(parent_mode: str | None) -> str | None:
27
+ """父 mode:blank→None;manual→default;其余保留(含合法枚举)。"""
28
+ if parent_mode is None:
29
+ return None
30
+ if not isinstance(parent_mode, str):
31
+ return None
32
+ stripped = parent_mode.strip()
33
+ if not stripped:
34
+ return None
35
+ if stripped == "manual":
36
+ return "default"
37
+ return stripped
38
+
39
+
40
+ def resolve_definition_permission_mode(
41
+ *,
42
+ parent_mode: str | None,
43
+ declared: str | None,
44
+ auto_available: bool = True,
45
+ ) -> str | None:
46
+ """合并父会话 mode 与子定义声明(官网 Permission modes / Nested 链式)。
47
+
48
+ LOCK-2:父 ∈ elevated → 忽略子声明。
49
+ LOCK-3:子 bypass 且父非 bypass → 保持父。
50
+ LOCK-10:本期 auto_available 默认 True。
51
+ """
52
+ parent = _normalize_parent_mode(parent_mode)
53
+ declared_n = normalize_definition_permission_mode(declared)
54
+
55
+ if declared_n is None:
56
+ return parent
57
+
58
+ if parent in _ELEVATED_PARENT_MODES:
59
+ return parent
60
+
61
+ if declared_n == "bypassPermissions" and parent != "bypassPermissions":
62
+ return parent
63
+
64
+ if declared_n == "auto" and not auto_available:
65
+ return parent
66
+
67
+ return declared_n
68
+
69
+
70
+ __all__ = [
71
+ "normalize_definition_permission_mode",
72
+ "resolve_definition_permission_mode",
73
+ ]
@@ -24,6 +24,41 @@ if TYPE_CHECKING:
24
24
  from .context import SubagentContext
25
25
 
26
26
 
27
+ def resolve_subagent_permission_mode(
28
+ *,
29
+ headless: bool,
30
+ inherited: str | None,
31
+ ) -> str:
32
+ """解析子图 permission_mode。
33
+
34
+ headless 时继承父 mode(父 ``auto`` → 子 ``auto``,写快路径才可通);
35
+ 无父 mode 时回落 ``dontAsk``(无交互面)。非 headless 无父 mode 回落 ``default``。
36
+
37
+ 对照:docs/bug/headless-subagent-dontask-blocks-write-roots-2026-10-22.md
38
+ """
39
+ if isinstance(inherited, str) and inherited.strip():
40
+ return inherited.strip()
41
+ return "dontAsk" if headless else "default"
42
+
43
+
44
+ def resolve_subagent_container_type(
45
+ *,
46
+ headless: bool,
47
+ permission_mode: str,
48
+ ) -> str:
49
+ """解析子图 container_type,与 ``InteractionProfileResolver`` 合法三元组对齐。
50
+
51
+ ``sdk`` 剖面强制 ``permission_mode=dontAsk``;若子图继承了 ``auto`` /
52
+ ``acceptEdits`` / ``bypassPermissions``,须改走 ``interactive`` + headless
53
+ (interactive-headless),否则 normalize 会拒掉。
54
+ """
55
+ if not headless:
56
+ return "interactive"
57
+ if permission_mode == "dontAsk":
58
+ return "sdk"
59
+ return "interactive"
60
+
61
+
27
62
  def create_subagent_graph(
28
63
  ctx: SubagentContext,
29
64
  loader: Any,
@@ -58,9 +93,10 @@ def create_subagent_graph(
58
93
  sync_tool_policy_roots_from_workspace,
59
94
  )
60
95
  from langchain_agentx.tool_runtime.loader import ToolRuntimeLoader
61
- from langchain_agentx.tool_runtime.prompt.session_mode import read_parent_permission_mode
62
96
 
63
- inherited_permission_mode = read_parent_permission_mode(ctx)
97
+ # permission_mode SSOT:只读 SubagentContext(derive/create_subagent_context 写入)
98
+ # 勿再 read_parent_permission_mode — docs/bug/subagent-permission-mode-dual-track-ssot-2026-10-22.md
99
+ inherited_permission_mode = ctx.permission_mode
64
100
 
65
101
  if ctx.boundary_snapshot is not None and isinstance(loader, ToolRuntimeLoader):
66
102
  sync_tool_policy_roots_from_workspace(
@@ -89,6 +125,18 @@ def create_subagent_graph(
89
125
  else:
90
126
  session_id = f"subagent:{ctx.subagent_type}:{ctx.agent_id}"
91
127
 
128
+ # 勿无条件 headless→dontAsk / 一律 sdk:会盖掉父 auto,且与 sdk 剖面冲突
129
+ # (docs/bug/headless-subagent-dontask-blocks-write-roots-2026-10-22.md)
130
+ sub_headless = bool(ctx.headless)
131
+ sub_permission_mode = resolve_subagent_permission_mode(
132
+ headless=sub_headless,
133
+ inherited=inherited_permission_mode,
134
+ )
135
+ sub_container_type = resolve_subagent_container_type(
136
+ headless=sub_headless,
137
+ permission_mode=sub_permission_mode,
138
+ )
139
+
92
140
  # Phase 3: config-first 模式
93
141
  config = AgentLoopConfig(
94
142
  session_id=session_id,
@@ -119,20 +167,18 @@ def create_subagent_graph(
119
167
  parent_tool_call_id=ctx.parent_tool_call_id,
120
168
  parent_conversation_session_id=ctx.parent_conversation_session_id,
121
169
  ),
170
+ agent_depth=ctx.agent_depth,
122
171
  subagent=SubagentLinkage(
123
172
  is_subagent=True,
124
173
  subagent_type=ctx.subagent_type,
125
174
  parent_agent_id=ctx.parent_agent_id,
126
175
  agent_depth=ctx.agent_depth,
176
+ fork_from_parent=bool(ctx.fork_from_parent),
127
177
  ),
128
178
  max_steps=ctx.max_steps,
129
- headless=bool(ctx.headless),
130
- container_type="sdk" if ctx.headless else "interactive",
131
- permission_mode=(
132
- "dontAsk"
133
- if ctx.headless
134
- else (inherited_permission_mode or "default")
135
- ),
179
+ headless=sub_headless,
180
+ permission_mode=sub_permission_mode,
181
+ container_type=sub_container_type,
136
182
  name=f"subagent:{ctx.subagent_type}:{ctx.agent_id[:8]}",
137
183
  )
138
184
  services = LoopServices(