langchain-agentx-python 2.4.0__py3-none-any.whl → 2.4.1__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.
@@ -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.1"
15
15
 
16
16
  from .mcp.call import McpToolCaller
17
17
  from .mcp.client_manager import McpClientManager, McpConnectedClient
@@ -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,
@@ -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
  ]
@@ -146,12 +146,15 @@ class RuntimeContextFactory:
146
146
  subagent_query_source = (
147
147
  f"agent:{subagent_type}" if subagent_type else "agent:default"
148
148
  )
149
+ # 子 depth 与顶层 agent_depth 必须一致:factory flatten 在 is_subagent 时
150
+ # 优先读 subagent.agent_depth(嵌套 L1→L2 若只写顶层会误成 0)。
151
+ child_depth = int(getattr(parent_ctx, "agent_depth", 0) or 0) + 1
149
152
 
150
153
  return AgentLoopConfig(
151
154
  model=resolved_model,
152
155
  session_id=getattr(parent_ctx, "session_id", "") or "",
153
156
  agent_id=agent_id,
154
- agent_depth=(parent_ctx.agent_depth or 0) + 1,
157
+ agent_depth=child_depth,
155
158
  max_steps=resolved_max_steps,
156
159
  permission_mode=resolved_permission,
157
160
  model_retry=ModelRetrySettings(query_source=subagent_query_source),
@@ -171,6 +174,9 @@ class RuntimeContextFactory:
171
174
  ),
172
175
  subagent=SubagentLinkage(
173
176
  is_subagent=True,
177
+ subagent_type=str(subagent_type) if subagent_type else None,
174
178
  parent_agent_id=parent_agent_id,
179
+ agent_depth=child_depth,
180
+ # fork_from_parent 由 orchestrator mode 决定,derive 不知 mode → False
175
181
  ),
176
182
  )
@@ -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}")
@@ -119,11 +119,13 @@ def create_subagent_graph(
119
119
  parent_tool_call_id=ctx.parent_tool_call_id,
120
120
  parent_conversation_session_id=ctx.parent_conversation_session_id,
121
121
  ),
122
+ agent_depth=ctx.agent_depth,
122
123
  subagent=SubagentLinkage(
123
124
  is_subagent=True,
124
125
  subagent_type=ctx.subagent_type,
125
126
  parent_agent_id=ctx.parent_agent_id,
126
127
  agent_depth=ctx.agent_depth,
128
+ fork_from_parent=bool(ctx.fork_from_parent),
127
129
  ),
128
130
  max_steps=ctx.max_steps,
129
131
  headless=bool(ctx.headless),