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.
- langchain_agentx/__init__.py +1 -1
- langchain_agentx/loop/config/interaction_profile.py +6 -2
- langchain_agentx/loop/config/loop_config.py +2 -0
- langchain_agentx/loop/graph/factory.py +8 -1
- langchain_agentx/loop/run_context.py +48 -0
- langchain_agentx/loop/runtime/context_factory.py +32 -9
- langchain_agentx/loop/subagent/async_runner.py +12 -1
- langchain_agentx/loop/subagent/context.py +20 -0
- langchain_agentx/loop/subagent/definition_permission.py +73 -0
- langchain_agentx/loop/subagent/graph.py +55 -9
- langchain_agentx/loop/subagent/orchestrator.py +543 -366
- langchain_agentx/loop/subagent/runner.py +7 -5
- langchain_agentx/tool_runtime/adapter.py +5 -0
- langchain_agentx/tool_runtime/models.py +4 -1
- langchain_agentx/tool_runtime/prompt/session_mode.py +43 -3
- langchain_agentx/tools/agent/backend.py +33 -11
- langchain_agentx/tools/agent/limits.py +28 -3
- langchain_agentx/tools/agent/loader.py +35 -2
- langchain_agentx/tools/agent/nesting_policy.py +65 -0
- langchain_agentx/tools/agent/registry/config.py +2 -0
- langchain_agentx/tools/agent/scope.py +16 -15
- langchain_agentx/tools/agent/tool.py +14 -1
- {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/RECORD +27 -25
- {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.4.0.dist-info → langchain_agentx_python-2.4.4.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -25,7 +25,10 @@ if TYPE_CHECKING:
|
|
|
25
25
|
_WORKER_CONTAINER_TYPES = frozenset(
|
|
26
26
|
{"background_task", "background", "batch", "worker", "sdk"}
|
|
27
27
|
)
|
|
28
|
-
|
|
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
|
|
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=
|
|
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 继承;
|
|
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 >
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
101
|
+
|
|
102
|
+
parent_permission = (
|
|
93
103
|
overrides.permission_mode
|
|
94
104
|
if overrides.permission_mode is not None
|
|
95
|
-
else
|
|
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=
|
|
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
|
-
|
|
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=
|
|
130
|
-
|
|
131
|
-
|
|
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(
|