langchain-agentx-python 2.3.8__py3-none-any.whl → 2.4.0__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/loop_abort.py +14 -2
- langchain_agentx/loop/run_context.py +196 -25
- langchain_agentx/loop/stream/loop_driver.py +5 -0
- langchain_agentx/loop/subagent/orchestrator.py +6 -1
- langchain_agentx/loop/subagent/runner.py +36 -11
- langchain_agentx/tools/bash/ast_security.py +19 -1
- langchain_agentx/tools/bash/command_allow_rules.py +27 -0
- langchain_agentx/tools/bash/read_only_validation.py +40 -2
- langchain_agentx/tools/skill/fork.py +105 -19
- langchain_agentx/tools/skill/tool.py +52 -0
- langchain_agentx/workflow/execution.py +3 -1
- langchain_agentx/workspace/project_memory_layout.py +2 -1
- {langchain_agentx_python-2.3.8.dist-info → langchain_agentx_python-2.4.0.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.3.8.dist-info → langchain_agentx_python-2.4.0.dist-info}/RECORD +18 -18
- {langchain_agentx_python-2.3.8.dist-info → langchain_agentx_python-2.4.0.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.3.8.dist-info → langchain_agentx_python-2.4.0.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.3.8.dist-info → langchain_agentx_python-2.4.0.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -45,11 +45,23 @@ class LoopAbortSignal(Exception):
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def is_cancel_requested() -> bool:
|
|
48
|
-
"""
|
|
48
|
+
"""当前或祖先 RunContext 是否已请求取消(无 context 则为 False)。
|
|
49
|
+
|
|
50
|
+
祖先链:nested Parallelization ctx 下 leaf 可读到 L0 ``cancel_requested``。
|
|
51
|
+
"""
|
|
49
52
|
from .run_context import get_run_context
|
|
50
53
|
|
|
51
54
|
ctx = get_run_context()
|
|
52
|
-
|
|
55
|
+
seen: set[int] = set()
|
|
56
|
+
while ctx is not None:
|
|
57
|
+
ident = id(ctx)
|
|
58
|
+
if ident in seen:
|
|
59
|
+
break
|
|
60
|
+
seen.add(ident)
|
|
61
|
+
if bool(ctx.cancel_requested):
|
|
62
|
+
return True
|
|
63
|
+
ctx = ctx.parent
|
|
64
|
+
return False
|
|
53
65
|
|
|
54
66
|
|
|
55
67
|
def abort_signal_for_phase(
|
|
@@ -12,6 +12,9 @@ run_context.py — Workflow/Agent 结构化并发上下文(parent → child ca
|
|
|
12
12
|
asyncio 为主;thread 仅 bounded join(无法强制中断 LLM);anyio/trio 不保证 context 传播。
|
|
13
13
|
P1:reset 失败时在仍可见 owner 的 derived logical context 内 force-clear;
|
|
14
14
|
宿主 detached ingest 用 create_task_with_copied_context / run_context_scope。
|
|
15
|
+
nested Parallelization:child_contexts + 递归 cancel_tree;W4 可用 use_cancel_authority。
|
|
16
|
+
agent loop 登记:供 skill fork 同步桥 ``run_coroutine_threadsafe`` 解析父 loop
|
|
17
|
+
(ContextVar 在 ThreadPoolExecutor 上常丢失,故用进程级 stack)。
|
|
15
18
|
"""
|
|
16
19
|
|
|
17
20
|
from __future__ import annotations
|
|
@@ -33,6 +36,81 @@ _current_run_context: ContextVar[Optional["RunContext"]] = ContextVar(
|
|
|
33
36
|
"run_ctx", default=None
|
|
34
37
|
)
|
|
35
38
|
|
|
39
|
+
# 进程级父 agent loop / RunContext 栈:ThreadPoolExecutor worker 看不见 ContextVar,
|
|
40
|
+
# 须 threadsafe 回父 loop(bug4)并恢复 owner 供 cancel 登记(skill-fork cancel orphan)。
|
|
41
|
+
_agent_loop_lock = threading.Lock()
|
|
42
|
+
_agent_loop_stack: list[asyncio.AbstractEventLoop] = []
|
|
43
|
+
_agent_run_ctx_stack: list["RunContext"] = []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def register_agent_loop(loop: asyncio.AbstractEventLoop) -> None:
|
|
47
|
+
"""登记当前存活的 agent/session event loop(可嵌套;同 loop 可重复 push)。"""
|
|
48
|
+
with _agent_loop_lock:
|
|
49
|
+
_agent_loop_stack.append(loop)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def unregister_agent_loop(loop: asyncio.AbstractEventLoop) -> None:
|
|
53
|
+
"""弹出最近一次与 ``loop`` 相同的登记(LIFO;未见则 no-op)。"""
|
|
54
|
+
with _agent_loop_lock:
|
|
55
|
+
for index in range(len(_agent_loop_stack) - 1, -1, -1):
|
|
56
|
+
if _agent_loop_stack[index] is loop:
|
|
57
|
+
del _agent_loop_stack[index]
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_agent_loop(
|
|
62
|
+
*,
|
|
63
|
+
owner: RunContext | None = None,
|
|
64
|
+
) -> asyncio.AbstractEventLoop | None:
|
|
65
|
+
"""解析仍在跑的父 agent loop;优先 ``owner.root_task``,否则栈顶存活项。"""
|
|
66
|
+
if owner is not None:
|
|
67
|
+
try:
|
|
68
|
+
loop = owner.root_task.get_loop()
|
|
69
|
+
if not loop.is_closed() and loop.is_running():
|
|
70
|
+
return loop
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
with _agent_loop_lock:
|
|
74
|
+
for loop in reversed(_agent_loop_stack):
|
|
75
|
+
try:
|
|
76
|
+
if not loop.is_closed() and loop.is_running():
|
|
77
|
+
return loop
|
|
78
|
+
except Exception:
|
|
79
|
+
continue
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_owner_run_context(
|
|
84
|
+
owner: RunContext | None = None,
|
|
85
|
+
) -> RunContext | None:
|
|
86
|
+
"""解析取消挂接用的 owner:显式参数 → ContextVar → 进程级 install 栈顶。"""
|
|
87
|
+
if owner is not None:
|
|
88
|
+
return owner
|
|
89
|
+
current = get_run_context()
|
|
90
|
+
if current is not None:
|
|
91
|
+
return current
|
|
92
|
+
with _agent_loop_lock:
|
|
93
|
+
if _agent_run_ctx_stack:
|
|
94
|
+
return _agent_run_ctx_stack[-1]
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def run_under_owner_run_context(
|
|
99
|
+
coro: Awaitable[T],
|
|
100
|
+
*,
|
|
101
|
+
owner: RunContext | None,
|
|
102
|
+
) -> T:
|
|
103
|
+
"""在指定 owner 的 ContextVar 下 await(threadsafe 桥恢复父 RunContext)。"""
|
|
104
|
+
if owner is None:
|
|
105
|
+
return await coro
|
|
106
|
+
if get_run_context() is owner:
|
|
107
|
+
return await coro
|
|
108
|
+
token = _current_run_context.set(owner)
|
|
109
|
+
try:
|
|
110
|
+
return await coro
|
|
111
|
+
finally:
|
|
112
|
+
_current_run_context.reset(token)
|
|
113
|
+
|
|
36
114
|
|
|
37
115
|
@dataclass
|
|
38
116
|
class RunContext:
|
|
@@ -41,10 +119,20 @@ class RunContext:
|
|
|
41
119
|
root_task: asyncio.Task
|
|
42
120
|
child_tasks: set[asyncio.Task] = field(default_factory=set)
|
|
43
121
|
child_threads: set[threading.Thread] = field(default_factory=set)
|
|
122
|
+
# nested Parallelization/Chaining 再 install 时登记;cancel_tree 先深取消其 child_tasks。
|
|
123
|
+
# 用 list:RunContext 含 set/Task 不可哈希,不能放 set。
|
|
124
|
+
child_contexts: list["RunContext"] = field(default_factory=list, repr=False)
|
|
125
|
+
parent: Optional["RunContext"] = field(default=None, repr=False, compare=False)
|
|
44
126
|
cancel_requested: bool = False
|
|
45
127
|
# D-EVT-6:宿主在 cancel_tree / killAll 时写入;finish 投影读取;缺省 unknown。
|
|
46
128
|
# 取值:escape | workflow_stop | kill_agents | unknown;禁止从「是否有后台」猜测。
|
|
47
129
|
cancel_source: str | None = None
|
|
130
|
+
# install 时若成功 register_agent_loop,记下以便 reset 精确 unregister(勿误弹外层)。
|
|
131
|
+
_registered_agent_loop: asyncio.AbstractEventLoop | None = field(
|
|
132
|
+
default=None, repr=False, compare=False
|
|
133
|
+
)
|
|
134
|
+
# install 时是否 push 进进程级 RunContext 栈(供 worker 解析 owner)。
|
|
135
|
+
_registered_on_ctx_stack: bool = field(default=False, repr=False, compare=False)
|
|
48
136
|
|
|
49
137
|
def register_child(self, task: asyncio.Task) -> None:
|
|
50
138
|
self.child_tasks.add(task)
|
|
@@ -52,10 +140,42 @@ class RunContext:
|
|
|
52
140
|
def register_child_thread(self, thread: threading.Thread) -> None:
|
|
53
141
|
self.child_threads.add(thread)
|
|
54
142
|
|
|
55
|
-
|
|
143
|
+
def register_child_context(self, child: "RunContext") -> None:
|
|
144
|
+
"""登记 nested RunContext(install_run_context(parent=…))。"""
|
|
145
|
+
if all(existing is not child for existing in self.child_contexts):
|
|
146
|
+
self.child_contexts.append(child)
|
|
147
|
+
child.parent = self
|
|
148
|
+
|
|
149
|
+
def cancel_authority(self) -> "RunContext":
|
|
150
|
+
"""取消权威:最外层 RunContext(宿主 Stop / cancel_tree 目标)。"""
|
|
151
|
+
node: RunContext = self
|
|
152
|
+
seen: set[int] = set()
|
|
153
|
+
while node.parent is not None:
|
|
154
|
+
ident = id(node)
|
|
155
|
+
if ident in seen:
|
|
156
|
+
break
|
|
157
|
+
seen.add(ident)
|
|
158
|
+
node = node.parent
|
|
159
|
+
return node
|
|
160
|
+
|
|
161
|
+
def _mark_cancel_requested(self, *, source: str | None) -> None:
|
|
162
|
+
"""本层 + 全部 nested 写入 cancel_requested(协作闸可立即看到)。"""
|
|
56
163
|
self.cancel_requested = True
|
|
164
|
+
if source is not None and self.cancel_source is None:
|
|
165
|
+
self.cancel_source = source
|
|
166
|
+
for nested in list(self.child_contexts):
|
|
167
|
+
nested._mark_cancel_requested(source=source or self.cancel_source)
|
|
168
|
+
|
|
169
|
+
async def cancel_tree(self, timeout: float = 10.0) -> None:
|
|
170
|
+
"""取消本层 child_tasks,并递归 nested child_contexts(W4 / leaf producer)。"""
|
|
171
|
+
self._mark_cancel_requested(source=self.cancel_source)
|
|
57
172
|
deadline = time.monotonic() + timeout
|
|
58
173
|
|
|
174
|
+
# 深取消:先砍 nested 内 W4 producer,再 cancel 本层(含 nested.root_task)。
|
|
175
|
+
for nested in list(self.child_contexts):
|
|
176
|
+
remaining = max(0.0, deadline - time.monotonic())
|
|
177
|
+
await nested.cancel_tree(timeout=remaining)
|
|
178
|
+
|
|
59
179
|
for child in list(self.child_tasks):
|
|
60
180
|
child.cancel()
|
|
61
181
|
if self.child_tasks:
|
|
@@ -128,7 +248,20 @@ def install_run_context(
|
|
|
128
248
|
ctx = RunContext(root_task=current)
|
|
129
249
|
if parent is not None:
|
|
130
250
|
parent.register_child(ctx.root_task)
|
|
251
|
+
parent.register_child_context(ctx)
|
|
131
252
|
token = _current_run_context.set(ctx)
|
|
253
|
+
try:
|
|
254
|
+
loop = ctx.root_task.get_loop()
|
|
255
|
+
register_agent_loop(loop)
|
|
256
|
+
ctx._registered_agent_loop = loop
|
|
257
|
+
except Exception:
|
|
258
|
+
logger.debug(
|
|
259
|
+
"register_agent_loop skipped at install_run_context",
|
|
260
|
+
exc_info=True,
|
|
261
|
+
)
|
|
262
|
+
with _agent_loop_lock:
|
|
263
|
+
_agent_run_ctx_stack.append(ctx)
|
|
264
|
+
ctx._registered_on_ctx_stack = True
|
|
132
265
|
return ctx, token
|
|
133
266
|
|
|
134
267
|
|
|
@@ -139,33 +272,53 @@ def reset_run_context(token: Token, *, owner: RunContext | None = None) -> None:
|
|
|
139
272
|
P1:若当前 logical context 仍可见 ``owner``(derived context 继承值但 token 无效),
|
|
140
273
|
显式 ``set(None)`` 清理本层,避免 workflow finally 后 run_ctx 悬挂。
|
|
141
274
|
"""
|
|
275
|
+
source = owner if owner is not None else get_run_context()
|
|
276
|
+
loop_to_unreg: asyncio.AbstractEventLoop | None = None
|
|
277
|
+
ctx_to_unreg: RunContext | None = None
|
|
278
|
+
if source is not None and source._registered_agent_loop is not None:
|
|
279
|
+
loop_to_unreg = source._registered_agent_loop
|
|
280
|
+
source._registered_agent_loop = None
|
|
281
|
+
if source is not None and source._registered_on_ctx_stack:
|
|
282
|
+
ctx_to_unreg = source
|
|
283
|
+
source._registered_on_ctx_stack = False
|
|
284
|
+
|
|
142
285
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
286
|
+
try:
|
|
287
|
+
_current_run_context.reset(token)
|
|
288
|
+
return
|
|
289
|
+
except ValueError:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
current = get_run_context()
|
|
293
|
+
if owner is not None and current is owner:
|
|
294
|
+
_current_run_context.set(None)
|
|
295
|
+
logger.warning(
|
|
296
|
+
"run_ctx force-cleared: token reset failed in derived logical context "
|
|
297
|
+
"(task=%r)",
|
|
298
|
+
owner.root_task.get_name(),
|
|
299
|
+
)
|
|
300
|
+
return
|
|
147
301
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
owner.root_task.get_name(),
|
|
155
|
-
)
|
|
156
|
-
return
|
|
302
|
+
if owner is not None and owner.root_task.done():
|
|
303
|
+
logger.debug(
|
|
304
|
+
"run_ctx reset skipped: owner task %r already done; context discarded",
|
|
305
|
+
owner.root_task.get_name(),
|
|
306
|
+
)
|
|
307
|
+
return
|
|
157
308
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
owner.root_task.get_name(),
|
|
309
|
+
logger.warning(
|
|
310
|
+
"run_ctx reset skipped: token created in a different context",
|
|
311
|
+
exc_info=True,
|
|
162
312
|
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
313
|
+
finally:
|
|
314
|
+
if loop_to_unreg is not None:
|
|
315
|
+
unregister_agent_loop(loop_to_unreg)
|
|
316
|
+
if ctx_to_unreg is not None:
|
|
317
|
+
with _agent_loop_lock:
|
|
318
|
+
for index in range(len(_agent_run_ctx_stack) - 1, -1, -1):
|
|
319
|
+
if _agent_run_ctx_stack[index] is ctx_to_unreg:
|
|
320
|
+
del _agent_run_ctx_stack[index]
|
|
321
|
+
break
|
|
169
322
|
|
|
170
323
|
|
|
171
324
|
@asynccontextmanager
|
|
@@ -205,15 +358,32 @@ async def drive_async_iterator_in_current_task(
|
|
|
205
358
|
yield item
|
|
206
359
|
|
|
207
360
|
|
|
361
|
+
def resolve_cancel_authority(
|
|
362
|
+
ctx: RunContext | None = None,
|
|
363
|
+
) -> RunContext | None:
|
|
364
|
+
"""解析取消权威 ctx;缺省取当前 ContextVar 的 outermost。"""
|
|
365
|
+
current = ctx if ctx is not None else get_run_context()
|
|
366
|
+
if current is None:
|
|
367
|
+
return None
|
|
368
|
+
return current.cancel_authority()
|
|
369
|
+
|
|
370
|
+
|
|
208
371
|
def register_spawned_task(
|
|
209
372
|
task: asyncio.Task,
|
|
210
373
|
*,
|
|
211
374
|
owner: RunContext | None = None,
|
|
375
|
+
use_cancel_authority: bool = False,
|
|
212
376
|
) -> None:
|
|
213
|
-
"""将 spawn 出的 asyncio task 注册到 RunContext(无 context 时 no-op)。
|
|
377
|
+
"""将 spawn 出的 asyncio task 注册到 RunContext(无 context 时 no-op)。
|
|
378
|
+
|
|
379
|
+
``use_cancel_authority=True``:挂到 outermost(Workflow W4),避免仅挂在
|
|
380
|
+
nested Parallelization ctx 上导致 L0 ``cancel_tree`` 砍不到 producer。
|
|
381
|
+
"""
|
|
214
382
|
ctx = owner if owner is not None else get_run_context()
|
|
215
383
|
if ctx is None:
|
|
216
384
|
return
|
|
385
|
+
if use_cancel_authority:
|
|
386
|
+
ctx = ctx.cancel_authority()
|
|
217
387
|
try:
|
|
218
388
|
owner_loop = ctx.root_task.get_loop()
|
|
219
389
|
task_loop = task.get_loop()
|
|
@@ -249,5 +419,6 @@ __all__ = [
|
|
|
249
419
|
"register_spawned_task",
|
|
250
420
|
"register_spawned_thread",
|
|
251
421
|
"reset_run_context",
|
|
422
|
+
"resolve_cancel_authority",
|
|
252
423
|
"run_context_scope",
|
|
253
424
|
]
|
|
@@ -11,6 +11,7 @@ loop_driver.py — Loop-aware shared stream driver.
|
|
|
11
11
|
root closeout 捕获、synthetic error/finish、orphan tool_result repair(stream surface)。
|
|
12
12
|
正常结束但缺 root on_chain_end(LangGraph) 时打 anomaly WARNING(不伪造 synthetic finish)。
|
|
13
13
|
R1 反应恢复:单次 drain 内 budgeted continue 重入(S2,对齐 CC queryLoop continue)。
|
|
14
|
+
进入/退出时登记 agent loop,供 Ask 路径无 Workflow RunContext 时 fork 同步桥解析。
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
@@ -22,6 +23,7 @@ from typing import Any, AsyncIterator, Literal
|
|
|
22
23
|
from langgraph.errors import GraphRecursionError
|
|
23
24
|
|
|
24
25
|
from langchain_agentx.loop.loop_abort import LoopAbortSignal, is_cancel_requested
|
|
26
|
+
from langchain_agentx.loop.run_context import register_agent_loop, unregister_agent_loop
|
|
25
27
|
from ..exit.reason_codes import (
|
|
26
28
|
TERMINAL_REASON_ABORTED_STREAMING,
|
|
27
29
|
TERMINAL_REASON_ABORTED_TOOLS,
|
|
@@ -84,6 +86,8 @@ class LoopStreamDriver:
|
|
|
84
86
|
self.result.reactive_continue_drains = 0
|
|
85
87
|
current_input = dict(graph_input)
|
|
86
88
|
reactive_continues = 0
|
|
89
|
+
agent_loop = asyncio.get_running_loop()
|
|
90
|
+
register_agent_loop(agent_loop)
|
|
87
91
|
try:
|
|
88
92
|
while True:
|
|
89
93
|
async for event in self._stream_single_drain(
|
|
@@ -113,6 +117,7 @@ class LoopStreamDriver:
|
|
|
113
117
|
(self.result.graph_output or {}).get("transition", {}).get("reason"),
|
|
114
118
|
)
|
|
115
119
|
finally:
|
|
120
|
+
unregister_agent_loop(agent_loop)
|
|
116
121
|
self._log_stream_closeout_summary()
|
|
117
122
|
|
|
118
123
|
async def _stream_single_drain(
|
|
@@ -666,6 +666,7 @@ class SubagentOrchestrator:
|
|
|
666
666
|
loader=child_loader,
|
|
667
667
|
model=runtime_ctx.model,
|
|
668
668
|
allowed_tool_names=allowed_tool_names,
|
|
669
|
+
owner_run_ctx=get_run_context(),
|
|
669
670
|
)
|
|
670
671
|
|
|
671
672
|
return AgentToolOutput(
|
|
@@ -1015,13 +1016,17 @@ class SubagentOrchestrator:
|
|
|
1015
1016
|
model: Any,
|
|
1016
1017
|
allowed_tool_names: set[str],
|
|
1017
1018
|
) -> Any:
|
|
1018
|
-
|
|
1019
|
+
from langchain_agentx.loop.run_context import resolve_owner_run_context
|
|
1020
|
+
|
|
1021
|
+
# worker 上 ContextVar 常空;进程级 install 栈仍可解析父 RunContext(cancel 挂接)。
|
|
1022
|
+
owner_run_ctx = resolve_owner_run_context()
|
|
1019
1023
|
return _run_coroutine_sync(
|
|
1020
1024
|
run_subagent(
|
|
1021
1025
|
ctx=subagent_ctx,
|
|
1022
1026
|
loader=loader,
|
|
1023
1027
|
model=model,
|
|
1024
1028
|
allowed_tool_names=allowed_tool_names,
|
|
1029
|
+
owner_run_ctx=owner_run_ctx,
|
|
1025
1030
|
),
|
|
1026
1031
|
owner_run_ctx=owner_run_ctx,
|
|
1027
1032
|
)
|
|
@@ -119,23 +119,43 @@ def _is_abort_like_terminal_reason(reason: str | None) -> bool:
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
def _run_coroutine_sync(coro: Any, *, owner_run_ctx: Any | None = None) -> Any:
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
"""同步桥:无当前线程 running loop 时跑协程。
|
|
123
|
+
|
|
124
|
+
- 当前线程已有 running loop:禁止阻塞桥接(会死锁)→ 须 ``await`` /
|
|
125
|
+
``ainvoke_subagent``(Doc-35 / C2)。
|
|
126
|
+
- 进程内已有存活的父 agent loop(``resolve_agent_loop``):
|
|
127
|
+
``run_coroutine_threadsafe`` 提交回父 loop(skill fork 对齐 CC 同 loop;
|
|
128
|
+
避免 worker ``asyncio.run`` 新 loop + 共享 httpx 池毒化,bug4);
|
|
129
|
+
并 ``run_under_owner_run_context`` 恢复父 RunContext(cancel 登记,bug cancel-orphan)。
|
|
130
|
+
- 否则:``asyncio.run``(纯同步脚本、无父会话)。
|
|
126
131
|
"""
|
|
127
|
-
del owner_run_ctx # C2 删除后同步路径不再登记 worker thread
|
|
128
132
|
try:
|
|
129
133
|
asyncio.get_running_loop()
|
|
130
134
|
except RuntimeError:
|
|
131
|
-
|
|
135
|
+
pass
|
|
136
|
+
else:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
"cannot run subagent coroutine via thread while an asyncio event loop "
|
|
139
|
+
"is running (C2 removed, Doc-35 P0). Use await run_subagent / "
|
|
140
|
+
"ainvoke_subagent on the same loop instead of sync invoke."
|
|
141
|
+
)
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
from langchain_agentx.loop.run_context import (
|
|
144
|
+
resolve_agent_loop,
|
|
145
|
+
resolve_owner_run_context,
|
|
146
|
+
run_under_owner_run_context,
|
|
137
147
|
)
|
|
138
148
|
|
|
149
|
+
owner = resolve_owner_run_context(owner_run_ctx)
|
|
150
|
+
target = resolve_agent_loop(owner=owner)
|
|
151
|
+
if target is not None and target.is_running():
|
|
152
|
+
return asyncio.run_coroutine_threadsafe(
|
|
153
|
+
run_under_owner_run_context(coro, owner=owner),
|
|
154
|
+
target,
|
|
155
|
+
).result()
|
|
156
|
+
|
|
157
|
+
return asyncio.run(coro)
|
|
158
|
+
|
|
139
159
|
|
|
140
160
|
@dataclass
|
|
141
161
|
class BackgroundLaunchedResult:
|
|
@@ -815,7 +835,12 @@ async def run_subagent(
|
|
|
815
835
|
),
|
|
816
836
|
name=f"agentx-subagent-fg-{ctx.agent_id or 'anon'}",
|
|
817
837
|
)
|
|
818
|
-
|
|
838
|
+
# Skill fork / nested Parallelization:挂 cancel authority(L0),对齐 D-REG-1。
|
|
839
|
+
register_spawned_task(
|
|
840
|
+
task,
|
|
841
|
+
owner=owner_run_ctx,
|
|
842
|
+
use_cancel_authority=True,
|
|
843
|
+
)
|
|
819
844
|
return await task
|
|
820
845
|
return await _run_subagent_body(
|
|
821
846
|
ctx=ctx,
|
|
@@ -219,6 +219,18 @@ class BashPermissionSegmenter:
|
|
|
219
219
|
def __init__(self, node_helper: BashAstNodeHelper) -> None:
|
|
220
220
|
self._node_helper = node_helper
|
|
221
221
|
|
|
222
|
+
# 控制结构整段保留:避免 for/while 的变量名与 in-list 词被拆成伪 command 段
|
|
223
|
+
# (否则 ``for i in 1 2; do echo; done`` → segments ``i``/``1``/``2``…,
|
|
224
|
+
# 触发 bash_passthrough ask 误阻只读循环)。
|
|
225
|
+
_ATOMIC_CONTROL_TYPES = frozenset({
|
|
226
|
+
"for_statement",
|
|
227
|
+
"while_statement",
|
|
228
|
+
"until_statement",
|
|
229
|
+
"if_statement",
|
|
230
|
+
"case_statement",
|
|
231
|
+
"function_definition",
|
|
232
|
+
})
|
|
233
|
+
|
|
222
234
|
def collect_segments(self, node: Node) -> list[str]:
|
|
223
235
|
if node.type == "program":
|
|
224
236
|
return self._collect_from_container(node)
|
|
@@ -226,7 +238,13 @@ class BashPermissionSegmenter:
|
|
|
226
238
|
if node.type in {"pipeline", "list"}:
|
|
227
239
|
return self._collect_from_container(node)
|
|
228
240
|
|
|
229
|
-
if node.type in {
|
|
241
|
+
if node.type in {
|
|
242
|
+
"command",
|
|
243
|
+
"redirected_statement",
|
|
244
|
+
"subshell",
|
|
245
|
+
"compound_statement",
|
|
246
|
+
*self._ATOMIC_CONTROL_TYPES,
|
|
247
|
+
}:
|
|
230
248
|
text = self._node_helper.text(node).strip()
|
|
231
249
|
return [text] if text else []
|
|
232
250
|
|
|
@@ -10,6 +10,8 @@ tools/bash/command_allow_rules.py — Bash 内容级 allow 匹配(slash comman
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import re
|
|
14
|
+
import shlex
|
|
13
15
|
from typing import TYPE_CHECKING
|
|
14
16
|
|
|
15
17
|
from langchain_agentx.tools.bash.shell_rule_matching import match_shell_permission_rule
|
|
@@ -29,6 +31,8 @@ from langchain_agentx.tool_runtime.policy import PolicyEngine, resolve_default_p
|
|
|
29
31
|
if TYPE_CHECKING:
|
|
30
32
|
from langchain_agentx.tool_runtime.policy import DefaultPolicyEngine
|
|
31
33
|
|
|
34
|
+
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
def bash_command_matches_allow_rule(command: str, rule_content: str) -> bool:
|
|
34
38
|
"""CC shellRuleMatching(prefix / wildcard / exact)。"""
|
|
@@ -105,6 +109,27 @@ def registry_has_cliarg_bash_allow(registry: PermissionRuleRegistry) -> bool:
|
|
|
105
109
|
return False
|
|
106
110
|
|
|
107
111
|
|
|
112
|
+
def _is_pure_cd_segment(command: str) -> bool:
|
|
113
|
+
"""``cd`` / ``cd <path>``(可带前导 ``VAR=``)不 promote。
|
|
114
|
+
|
|
115
|
+
G4:``cd`` 本身非只读,但段级静默 allow 是 cwd 语义的必要通路;
|
|
116
|
+
若 promote 为 ask,会抢在 ``compound_cd_write`` / 后续只读段之前,
|
|
117
|
+
导致 ``cd && touch`` / ``cd && pwd`` / session cwd 合并用例被 headless 误阻。
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
tokens = shlex.split(command.strip())
|
|
121
|
+
except ValueError:
|
|
122
|
+
tokens = command.strip().split()
|
|
123
|
+
index = 0
|
|
124
|
+
while index < len(tokens) and _ENV_ASSIGNMENT_RE.match(tokens[index]):
|
|
125
|
+
index += 1
|
|
126
|
+
rest = tokens[index:]
|
|
127
|
+
if not rest or rest[0].rsplit("/", 1)[-1] != "cd":
|
|
128
|
+
return False
|
|
129
|
+
# 段级调用不应含 list/pipe;若整串被误传,拒绝当作 pure cd
|
|
130
|
+
return not any(tok in {"&&", "||", "|", ";"} for tok in rest)
|
|
131
|
+
|
|
132
|
+
|
|
108
133
|
def promote_unscoped_bash_passthrough_to_ask(
|
|
109
134
|
decision: AuthorizationDecision,
|
|
110
135
|
*,
|
|
@@ -126,6 +151,8 @@ def promote_unscoped_bash_passthrough_to_ask(
|
|
|
126
151
|
shell_ops_already_resolved=True,
|
|
127
152
|
).is_read_only:
|
|
128
153
|
return decision
|
|
154
|
+
if _is_pure_cd_segment(command):
|
|
155
|
+
return decision
|
|
129
156
|
# 显式规则 / mode / safety 等已标 policy_id 的 allow 不降级
|
|
130
157
|
if decision.policy_id not in (None, "", "write_roots"):
|
|
131
158
|
return decision
|
|
@@ -36,6 +36,10 @@ class BashReadOnlyClassifier:
|
|
|
36
36
|
def __init__(self, *, host_detector: HostPlatformDetector | None = None) -> None:
|
|
37
37
|
self._host = host_detector or default_host_platform_detector()
|
|
38
38
|
|
|
39
|
+
# 前导 ``VAR=value``(与 bash_hardening.BashCommandCanonicalizer 同形),
|
|
40
|
+
# 避免 ``LANG=C git status`` 被误判为未知二进制 ``LANG=C``。
|
|
41
|
+
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
|
|
42
|
+
|
|
39
43
|
# ------------------------------------------------------------------
|
|
40
44
|
# 大 allowlist:常见只读/观测类命令(不含必然写盘的工具如 tee/dd of=)
|
|
41
45
|
# ------------------------------------------------------------------
|
|
@@ -52,7 +56,8 @@ class BashReadOnlyClassifier:
|
|
|
52
56
|
"zipinfo",
|
|
53
57
|
"nl", "cut", "tr", "uniq", "expand", "unexpand", "fold", "fmt", "pr", "rev", "tac",
|
|
54
58
|
"look", "basename", "dirname", "readlink", "realpath",
|
|
55
|
-
|
|
59
|
+
# exit/sleep:进程控制/等待,无写盘;与 true/false 同属只读观测/控制面
|
|
60
|
+
"true", "false", "exit", "sleep", "test", "expr", "seq",
|
|
56
61
|
"iconv", "locale", "locales", "getconf",
|
|
57
62
|
"sw_vers", "sysctl", "system_profiler", "pbpaste",
|
|
58
63
|
"ver",
|
|
@@ -137,7 +142,7 @@ class BashReadOnlyClassifier:
|
|
|
137
142
|
if not shell_ops_already_resolved and self._shell_compound_or_pipeline(command):
|
|
138
143
|
return BashReadOnlyClassification(False, "shell_compound_or_pipeline")
|
|
139
144
|
|
|
140
|
-
tokens = self._split(command)
|
|
145
|
+
tokens = self._strip_leading_assignments(self._split(command))
|
|
141
146
|
if not tokens:
|
|
142
147
|
return BashReadOnlyClassification(False, "empty_command")
|
|
143
148
|
|
|
@@ -146,6 +151,9 @@ class BashReadOnlyClassifier:
|
|
|
146
151
|
if base in {"cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe"}:
|
|
147
152
|
return BashReadOnlyClassification(False, f"{base}_shell_not_classified")
|
|
148
153
|
|
|
154
|
+
if base in {"for", "while", "until", "if", "case"}:
|
|
155
|
+
return self._classify_control_structure_body(command, base)
|
|
156
|
+
|
|
149
157
|
if base == "git":
|
|
150
158
|
return self._classify_git(tokens)
|
|
151
159
|
|
|
@@ -218,6 +226,30 @@ class BashReadOnlyClassifier:
|
|
|
218
226
|
|
|
219
227
|
return BashReadOnlyClassification(False, f"{base}_unknown")
|
|
220
228
|
|
|
229
|
+
def _classify_control_structure_body(
|
|
230
|
+
self,
|
|
231
|
+
command: str,
|
|
232
|
+
base: str,
|
|
233
|
+
) -> BashReadOnlyClassification:
|
|
234
|
+
"""``for``/``while``/``if``/``case``:体中 simple command 全只读则整段只读。"""
|
|
235
|
+
from .ast_security import BashAstAnalyzer
|
|
236
|
+
|
|
237
|
+
analysis = BashAstAnalyzer().analyze(command)
|
|
238
|
+
body_cmds = analysis.simple_commands
|
|
239
|
+
if not body_cmds:
|
|
240
|
+
return BashReadOnlyClassification(False, f"{base}_empty_body")
|
|
241
|
+
for sc in body_cmds:
|
|
242
|
+
text = (sc.text or "").strip()
|
|
243
|
+
if not text:
|
|
244
|
+
continue
|
|
245
|
+
sub = self.classify(text, shell_ops_already_resolved=True)
|
|
246
|
+
if not sub.is_read_only:
|
|
247
|
+
return BashReadOnlyClassification(
|
|
248
|
+
False,
|
|
249
|
+
f"{base}_body_{sub.reason or 'not_readonly'}",
|
|
250
|
+
)
|
|
251
|
+
return BashReadOnlyClassification(True, f"{base}_readonly_body")
|
|
252
|
+
|
|
221
253
|
@staticmethod
|
|
222
254
|
def _shell_compound_or_pipeline(command: str) -> bool:
|
|
223
255
|
"""单 segment 内的 `|` / `&&` / `||` 保守视为非只读(不含 `;`,避免 awk/sed 脚本误判)。"""
|
|
@@ -551,6 +583,12 @@ class BashReadOnlyClassifier:
|
|
|
551
583
|
except ValueError:
|
|
552
584
|
return command.split()
|
|
553
585
|
|
|
586
|
+
def _strip_leading_assignments(self, tokens: list[str]) -> list[str]:
|
|
587
|
+
index = 0
|
|
588
|
+
while index < len(tokens) and self._ENV_ASSIGNMENT_RE.match(tokens[index]):
|
|
589
|
+
index += 1
|
|
590
|
+
return tokens[index:]
|
|
591
|
+
|
|
554
592
|
@staticmethod
|
|
555
593
|
def _basename(token: str) -> str:
|
|
556
594
|
return token.rsplit("/", 1)[-1]
|
|
@@ -5,8 +5,12 @@ tools/skill/fork.py — Skill fork 执行(对齐 CC executeForkedSkill)
|
|
|
5
5
|
frontmatter ``context: fork`` 时在隔离子 loop 中执行 skill 正文,
|
|
6
6
|
父 loop 仅收到摘要结果(不注入 meta HumanMessage / 父级 contextModifier)。
|
|
7
7
|
|
|
8
|
+
链路位置:
|
|
9
|
+
SkillRuntimeTool.ainvoke(主路径 · C1)→ aexecute → ainvoke_subagent;
|
|
10
|
+
SkillRuntimeTool.invoke(纯 sync)→ execute → invoke_subagent(force_sync)。
|
|
11
|
+
|
|
8
12
|
CC 对照:
|
|
9
|
-
SkillTool.executeForkedSkill
|
|
13
|
+
SkillTool.executeForkedSkill(async)+ runAgent(isAsync=false) 共享父 Abort。
|
|
10
14
|
"""
|
|
11
15
|
|
|
12
16
|
from __future__ import annotations
|
|
@@ -16,7 +20,7 @@ from typing import Any
|
|
|
16
20
|
from langchain_agentx.tool_runtime.loop_runtime_context import resolve_skill_model_override
|
|
17
21
|
from langchain_agentx.tool_runtime.models import ToolExecutionContext
|
|
18
22
|
from langchain_agentx.tools.agent.builtin_subagent_loader import DefaultSubagentRegistryProvider
|
|
19
|
-
from langchain_agentx.tools.agent.models import AgentToolInput
|
|
23
|
+
from langchain_agentx.tools.agent.models import AgentToolInput, AgentToolOutput
|
|
20
24
|
from langchain_agentx.tools.agent.registry.config import SubagentConfig
|
|
21
25
|
from langchain_agentx.tools.agent.registry.registry import SubagentRegistry
|
|
22
26
|
|
|
@@ -31,7 +35,7 @@ def is_fork_execution_context(frontmatter: dict[str, Any]) -> bool:
|
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class SkillForkExecutor:
|
|
34
|
-
"""在子 agent loop
|
|
38
|
+
"""在子 agent loop 中执行 fork skill(首选 async / C1)。"""
|
|
35
39
|
|
|
36
40
|
def __init__(self, registry: SubagentRegistry | None = None) -> None:
|
|
37
41
|
self._registry_override = registry
|
|
@@ -49,17 +53,14 @@ class SkillForkExecutor:
|
|
|
49
53
|
return self._registry_override
|
|
50
54
|
return DefaultSubagentRegistryProvider.get()
|
|
51
55
|
|
|
52
|
-
def
|
|
56
|
+
def _build_agent_input(
|
|
53
57
|
self,
|
|
54
58
|
*,
|
|
55
59
|
cmd: SkillCommand,
|
|
56
60
|
content: str,
|
|
57
61
|
ctx: ToolExecutionContext,
|
|
58
62
|
model_override: str | None,
|
|
59
|
-
|
|
60
|
-
allowed_tools: list[str],
|
|
61
|
-
constraints_applied: dict[str, Any],
|
|
62
|
-
) -> SkillToolOutput:
|
|
63
|
+
) -> tuple[AgentToolInput, SubagentConfig, str]:
|
|
63
64
|
subagent_type = self._resolve_subagent_type(cmd.frontmatter)
|
|
64
65
|
subagent_config = self._resolve_subagent_config(subagent_type)
|
|
65
66
|
resolved_model = (
|
|
@@ -74,17 +75,20 @@ class SkillForkExecutor:
|
|
|
74
75
|
model=resolved_model,
|
|
75
76
|
run_in_background=False,
|
|
76
77
|
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
return inp, subagent_config, subagent_type
|
|
79
|
+
|
|
80
|
+
def _to_skill_output(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
cmd: SkillCommand,
|
|
84
|
+
content: str,
|
|
85
|
+
model_override: str | None,
|
|
86
|
+
effort_override: str | None,
|
|
87
|
+
allowed_tools: list[str],
|
|
88
|
+
constraints_applied: dict[str, Any],
|
|
89
|
+
agent_out: AgentToolOutput,
|
|
90
|
+
subagent_type: str,
|
|
91
|
+
) -> SkillToolOutput:
|
|
88
92
|
result_text = (agent_out.content or "").strip() or "Skill execution completed"
|
|
89
93
|
return SkillToolOutput(
|
|
90
94
|
skill_name=cmd.name,
|
|
@@ -105,6 +109,88 @@ class SkillForkExecutor:
|
|
|
105
109
|
},
|
|
106
110
|
)
|
|
107
111
|
|
|
112
|
+
async def aexecute(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
cmd: SkillCommand,
|
|
116
|
+
content: str,
|
|
117
|
+
ctx: ToolExecutionContext,
|
|
118
|
+
model_override: str | None,
|
|
119
|
+
effort_override: str | None,
|
|
120
|
+
allowed_tools: list[str],
|
|
121
|
+
constraints_applied: dict[str, Any],
|
|
122
|
+
) -> SkillToolOutput:
|
|
123
|
+
"""异步 fork(对齐 CC executeForkedSkill):同 loop await,可被 cancel_tree 停住。"""
|
|
124
|
+
inp, subagent_config, subagent_type = self._build_agent_input(
|
|
125
|
+
cmd=cmd,
|
|
126
|
+
content=content,
|
|
127
|
+
ctx=ctx,
|
|
128
|
+
model_override=model_override,
|
|
129
|
+
)
|
|
130
|
+
# force_sync=True ≡ CC isAsync:false(禁止后台);ainvoke_subagent 仍同 loop await(C1),
|
|
131
|
+
# 不会走 Thread+_run_coroutine_sync。
|
|
132
|
+
agent_out = await self._get_orchestrator().ainvoke_subagent(
|
|
133
|
+
inp,
|
|
134
|
+
ctx,
|
|
135
|
+
subagent_config,
|
|
136
|
+
skill_allowed_tools=allowed_tools or None,
|
|
137
|
+
skill_effort_override=effort_override,
|
|
138
|
+
skill_hook_frontmatter=cmd.frontmatter,
|
|
139
|
+
skill_hook_root=cmd.path.parent,
|
|
140
|
+
skill_hook_name=cmd.name,
|
|
141
|
+
force_sync=True,
|
|
142
|
+
)
|
|
143
|
+
return self._to_skill_output(
|
|
144
|
+
cmd=cmd,
|
|
145
|
+
content=content,
|
|
146
|
+
model_override=model_override,
|
|
147
|
+
effort_override=effort_override,
|
|
148
|
+
allowed_tools=allowed_tools,
|
|
149
|
+
constraints_applied=constraints_applied,
|
|
150
|
+
agent_out=agent_out,
|
|
151
|
+
subagent_type=subagent_type,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def execute(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
cmd: SkillCommand,
|
|
158
|
+
content: str,
|
|
159
|
+
ctx: ToolExecutionContext,
|
|
160
|
+
model_override: str | None,
|
|
161
|
+
effort_override: str | None,
|
|
162
|
+
allowed_tools: list[str],
|
|
163
|
+
constraints_applied: dict[str, Any],
|
|
164
|
+
) -> SkillToolOutput:
|
|
165
|
+
"""同步 fork(无父 loop / 纯 sync 调用方);有父会话时应走 ``aexecute``。"""
|
|
166
|
+
inp, subagent_config, subagent_type = self._build_agent_input(
|
|
167
|
+
cmd=cmd,
|
|
168
|
+
content=content,
|
|
169
|
+
ctx=ctx,
|
|
170
|
+
model_override=model_override,
|
|
171
|
+
)
|
|
172
|
+
agent_out = self._get_orchestrator().invoke_subagent(
|
|
173
|
+
inp,
|
|
174
|
+
ctx,
|
|
175
|
+
subagent_config,
|
|
176
|
+
skill_allowed_tools=allowed_tools or None,
|
|
177
|
+
skill_effort_override=effort_override,
|
|
178
|
+
skill_hook_frontmatter=cmd.frontmatter,
|
|
179
|
+
skill_hook_root=cmd.path.parent,
|
|
180
|
+
skill_hook_name=cmd.name,
|
|
181
|
+
force_sync=True,
|
|
182
|
+
)
|
|
183
|
+
return self._to_skill_output(
|
|
184
|
+
cmd=cmd,
|
|
185
|
+
content=content,
|
|
186
|
+
model_override=model_override,
|
|
187
|
+
effort_override=effort_override,
|
|
188
|
+
allowed_tools=allowed_tools,
|
|
189
|
+
constraints_applied=constraints_applied,
|
|
190
|
+
agent_out=agent_out,
|
|
191
|
+
subagent_type=subagent_type,
|
|
192
|
+
)
|
|
193
|
+
|
|
108
194
|
def _resolve_subagent_type(self, frontmatter: dict[str, Any]) -> str:
|
|
109
195
|
raw = frontmatter.get("agent")
|
|
110
196
|
if isinstance(raw, str) and raw.strip():
|
|
@@ -19,6 +19,7 @@ CC 对照:
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
import asyncio
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import Any
|
|
24
25
|
|
|
@@ -299,6 +300,57 @@ class SkillRuntimeTool(RuntimeTool):
|
|
|
299
300
|
constraints_applied=constraints_applied,
|
|
300
301
|
)
|
|
301
302
|
|
|
303
|
+
async def ainvoke(self, data: dict[str, Any], ctx: ToolExecutionContext) -> SkillToolOutput:
|
|
304
|
+
"""异步执行;``context: fork`` 走 C1(同 loop await),对齐 CC executeForkedSkill。
|
|
305
|
+
|
|
306
|
+
非 fork 仍委托默认同步 ``invoke``(经 executor),避免重复展开逻辑。
|
|
307
|
+
"""
|
|
308
|
+
inp = SkillToolInput.model_validate(data)
|
|
309
|
+
cmd = self._resolve_skill_command(inp.skill_name, ctx)
|
|
310
|
+
if cmd is None:
|
|
311
|
+
raise ValueError(f"Unknown skill: {inp.skill_name}")
|
|
312
|
+
if not is_fork_execution_context(cmd.frontmatter):
|
|
313
|
+
loop = asyncio.get_running_loop()
|
|
314
|
+
return await loop.run_in_executor(None, lambda: self.invoke(data, ctx))
|
|
315
|
+
|
|
316
|
+
dynamic = get_dynamic_skill_catalog(
|
|
317
|
+
ctx.session_store,
|
|
318
|
+
agent_home_segment=self._agent_home_segment,
|
|
319
|
+
)
|
|
320
|
+
expanded = self._catalog.expand_for_invoke(
|
|
321
|
+
inp.skill_name,
|
|
322
|
+
inp.args,
|
|
323
|
+
dynamic_catalog=dynamic,
|
|
324
|
+
session_store=ctx.session_store,
|
|
325
|
+
agent_home_segment=self._agent_home_segment,
|
|
326
|
+
)
|
|
327
|
+
content = expanded.content
|
|
328
|
+
frontmatter = cmd.frontmatter
|
|
329
|
+
allowed_tools = frontmatter.get("allowed-tools")
|
|
330
|
+
parsed_model, parsed_effort = self._parse_frontmatter_overrides(frontmatter)
|
|
331
|
+
constraints_applied = {
|
|
332
|
+
"has_allowed_tools": isinstance(allowed_tools, list) and len(allowed_tools) > 0,
|
|
333
|
+
"has_paths": isinstance(frontmatter.get("paths"), list) and len(frontmatter.get("paths")) > 0,
|
|
334
|
+
"has_model_override": parsed_model is not None,
|
|
335
|
+
"has_effort_override": parsed_effort is not None,
|
|
336
|
+
}
|
|
337
|
+
resolved_scope = self._resolve_tool_scope(data, ctx)
|
|
338
|
+
constraints_applied["tool_scope_resolution"] = {
|
|
339
|
+
"auto_approved": sorted(resolved_scope.auto_approved),
|
|
340
|
+
"final_allowed": sorted(resolved_scope.final_allowed),
|
|
341
|
+
"explain": list(resolved_scope.explain),
|
|
342
|
+
}
|
|
343
|
+
allowed_list = [str(x) for x in allowed_tools] if isinstance(allowed_tools, list) else []
|
|
344
|
+
return await self._fork_executor.aexecute(
|
|
345
|
+
cmd=cmd,
|
|
346
|
+
content=content,
|
|
347
|
+
ctx=ctx,
|
|
348
|
+
model_override=parsed_model,
|
|
349
|
+
effort_override=parsed_effort,
|
|
350
|
+
allowed_tools=allowed_list,
|
|
351
|
+
constraints_applied=constraints_applied,
|
|
352
|
+
)
|
|
353
|
+
|
|
302
354
|
@staticmethod
|
|
303
355
|
def _parse_frontmatter_overrides(
|
|
304
356
|
frontmatter: dict[str, Any],
|
|
@@ -445,7 +445,9 @@ class WorkflowAgentLoopExecutor:
|
|
|
445
445
|
await queue.put(None)
|
|
446
446
|
|
|
447
447
|
task = asyncio.create_task(_producer())
|
|
448
|
-
|
|
448
|
+
# W4:挂到 cancel authority(outermost),避免 Parallelization nested
|
|
449
|
+
# install 后 L0.cancel_tree 只看见 nested.root、砍不到 leaf producer。
|
|
450
|
+
register_spawned_task(task, use_cancel_authority=True)
|
|
449
451
|
try:
|
|
450
452
|
while True:
|
|
451
453
|
event = await queue.get()
|
|
@@ -39,7 +39,8 @@ def _mkdir_p(path: Path) -> None:
|
|
|
39
39
|
"""跨平台 ``mkdir -p``;Windows 近 MAX_PATH 时用 ``\\\\?\\`` 扩展前缀。"""
|
|
40
40
|
target = Path(path)
|
|
41
41
|
if os.name == "nt":
|
|
42
|
-
|
|
42
|
+
# Windows MAX_PATH 探测:仅取绝对长度,不参与 workspace 语义解析
|
|
43
|
+
abs_path = os.path.abspath(str(target)) # noqa: workspace-boundary reason="Windows MAX_PATH length probe only; not workspace path SSOT"
|
|
43
44
|
if len(abs_path) >= _WIN_CREATE_DIR_MAX and not abs_path.startswith("\\\\?\\"):
|
|
44
45
|
Path("\\\\?\\" + abs_path).mkdir(parents=True, exist_ok=True)
|
|
45
46
|
return
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langchain_agentx/__init__.py,sha256=
|
|
1
|
+
langchain_agentx/__init__.py,sha256=nekQFWWhUqeV0DS8CHFYpK9w0n3NvKObBPoWWU2WHmo,2014
|
|
2
2
|
langchain_agentx/command/__init__.py,sha256=Ej260S5uFQcmEtGZQG_NH7BYjHoStrpBLtGUsBTxyZk,631
|
|
3
3
|
langchain_agentx/command/allowed_tools.py,sha256=TVA0VM8rm98H-QbLK_GRiX5RhEAZFxKawliMi4kk96A,3359
|
|
4
4
|
langchain_agentx/command/context.py,sha256=DIGOGPBw5UGDBm0WNNJlKx1h_9rCRmcdROv4DT61Qug,731
|
|
@@ -28,9 +28,9 @@ langchain_agentx/health/models.py,sha256=lHwdvn0w-qs-c8mI6I3nlnoCk8--y7gADj7C9u7
|
|
|
28
28
|
langchain_agentx/health/preflight.py,sha256=wGysAnhT-jR1LfyjdaX9diO573Zzt5Z1M3vxDbpdcK0,5612
|
|
29
29
|
langchain_agentx/loop/__init__.py,sha256=JoFPFrlllvGCYFEk2RUuqq9AfUIdLVSNbrRdTRy6g_I,1428
|
|
30
30
|
langchain_agentx/loop/benchmark_frozen_config.py,sha256=YI4IaDVMBOEucF36F2oNv5mw1TqMG_rm_LJSpKAnFWM,13942
|
|
31
|
-
langchain_agentx/loop/loop_abort.py,sha256=
|
|
31
|
+
langchain_agentx/loop/loop_abort.py,sha256=wFlvCLjE9Avs83_8hqmUq0DlP58LSjtxOge-g1K4oPQ,3547
|
|
32
32
|
langchain_agentx/loop/message_contract.py,sha256=Twl83TneU2PRdxgGRBxs6S9_ovRT6cb5WeCUzd6oJnE,2196
|
|
33
|
-
langchain_agentx/loop/run_context.py,sha256=
|
|
33
|
+
langchain_agentx/loop/run_context.py,sha256=LQ4T8KrBPcjuNPsepFNcKcoS5ZDE5Ek7eI-_6oEmdWg,15410
|
|
34
34
|
langchain_agentx/loop/config/__init__.py,sha256=0pf3Mml7LZpQ1C3_AtROFQClrBQegaGCG4YCTfSr9v8,1137
|
|
35
35
|
langchain_agentx/loop/config/agent_config.py,sha256=tOnAHFor6dkle3FvUOVx0YsmiYVTH11UrWSyTmOLV0c,3100
|
|
36
36
|
langchain_agentx/loop/config/compaction_config.py,sha256=MmXqY47-9nPDhdXvnU92Xp0XeaZ605ErQwX9d2emEkM,1176
|
|
@@ -137,7 +137,7 @@ langchain_agentx/loop/runtime/context_factory.py,sha256=m3ics_dOTqEKNlf6ETIOnVj6
|
|
|
137
137
|
langchain_agentx/loop/runtime/subagent_execution_paths.py,sha256=Bgyiom48uta1FZcnIE7fIxyrD-VFCRsw7s8IxIxmLb4,4033
|
|
138
138
|
langchain_agentx/loop/stream/graph_driver.py,sha256=qMZHEw3wJ19Tfbz31-VPLy8Mt6e90xKougIjAoaEFCE,3935
|
|
139
139
|
langchain_agentx/loop/stream/helpers.py,sha256=b2sqtwrAa5ic5B5AzNqItU8pmDHw13iGs3hBiShSIZk,1390
|
|
140
|
-
langchain_agentx/loop/stream/loop_driver.py,sha256=
|
|
140
|
+
langchain_agentx/loop/stream/loop_driver.py,sha256=IE8IGcgdiLMqLZr32Ag9Q5vhbo1g7UxY1eUYsy7r98M,25357
|
|
141
141
|
langchain_agentx/loop/stream/result.py,sha256=xQb9r1cxGCQpoAZ0gNo71CjhSE3eYOiYp5KnoOBzBF8,1131
|
|
142
142
|
langchain_agentx/loop/stream/synthetic_events.py,sha256=Z3JT3iTuB9UQOoQXUprAYvaFV0vXEAuf5iqXYH9YSyI,2175
|
|
143
143
|
langchain_agentx/loop/stream/transcript_gate.py,sha256=4AH3O0l8OrY_GQdqf-TOKOVS3selPLUC0jYLWQUTfkg,4690
|
|
@@ -157,13 +157,13 @@ langchain_agentx/loop/subagent/hook_attachment_messages.py,sha256=em3VXaUfURou_Y
|
|
|
157
157
|
langchain_agentx/loop/subagent/hooks_compose.py,sha256=enPKRsAbqjB5HqACE-YFzM8K3Job7azi4wpqI1kmgl8,2515
|
|
158
158
|
langchain_agentx/loop/subagent/hooks_pipeline.py,sha256=4OweLS52TRbcaUO56YpE-qX71ZPCjPRS0j9KzIcx5ls,6399
|
|
159
159
|
langchain_agentx/loop/subagent/mcp_child_servers.py,sha256=UJ9FtIZroYvP6KfNLUnHKbb7bqg2qSmo15MkzHcVKME,4395
|
|
160
|
-
langchain_agentx/loop/subagent/orchestrator.py,sha256=
|
|
160
|
+
langchain_agentx/loop/subagent/orchestrator.py,sha256=6VR5yxnUD4q9lEhI7JVPdo_BdlEBD_ObfTxAnrIaVAg,42085
|
|
161
161
|
langchain_agentx/loop/subagent/parent_hook_snapshot.py,sha256=cwBC2eXvx1evc6QPpI42zq-iKThpObyoyWmkDNj9E7Q,2095
|
|
162
162
|
langchain_agentx/loop/subagent/progress.py,sha256=nef3VVT9QmB2ZWCDAO86cmtVwCJIR8lXMr6jczQKqOY,6650
|
|
163
163
|
langchain_agentx/loop/subagent/progress_dispatcher.py,sha256=KD5EdfoGplIaT8xvt5vgfXgSLPbvn9KqvY85wLpOX7A,4603
|
|
164
164
|
langchain_agentx/loop/subagent/prompt.py,sha256=p7RgzibNfEJdfX8nBnZGEwQLa3_EgOn4E56T9MBRIEc,1722
|
|
165
165
|
langchain_agentx/loop/subagent/reasoning_extractor.py,sha256=KrCQtBdKzsjAEfA7H5COuXzBhuGGonT2Su8IEKYlUpA,1334
|
|
166
|
-
langchain_agentx/loop/subagent/runner.py,sha256=
|
|
166
|
+
langchain_agentx/loop/subagent/runner.py,sha256=iwLvLLHlzNluSWx0xTvPpB4yPRsScKxqngX6LGGxAm0,36089
|
|
167
167
|
langchain_agentx/loop/subagent/subagent_start_hooks.py,sha256=HIyW7_ta9JNxiyrgL8scNpZzcJrAyld4qaRehYx36GY,1359
|
|
168
168
|
langchain_agentx/loop/subagent/subagent_termination_classifier.py,sha256=ULArQeAibgy6ETSf4vRR0L6X1rsDM7-ZuLzRWQ_pmeY,3158
|
|
169
169
|
langchain_agentx/loop/subagent/transcript.py,sha256=-S0VjZcvqlJQGkVGu8VDMTYwTwOePdjDHfjYi3uu7BY,7187
|
|
@@ -584,14 +584,14 @@ langchain_agentx/tools/ask_user_question/prompt.py,sha256=0RdVuUeNzQBzPeVrLcbiBq
|
|
|
584
584
|
langchain_agentx/tools/ask_user_question/tool.py,sha256=DgU7OBAqrJBb77ewQ76okoUFJxTThAiN6PRMPa8_FvU,13467
|
|
585
585
|
langchain_agentx/tools/ask_user_question/validators.py,sha256=Uc88cBzqUp0PNG_Jf2yX2WtKP8hzBsnkMb_KUXh9C4M,6088
|
|
586
586
|
langchain_agentx/tools/bash/__init__.py,sha256=molPHFDylE2wWG_aQx7fb0ciAx3wWpdXzIixTZV5ne4,214
|
|
587
|
-
langchain_agentx/tools/bash/ast_security.py,sha256=
|
|
587
|
+
langchain_agentx/tools/bash/ast_security.py,sha256=vMP1lAi2bHrsHojun64eu1-Ym8hGW00dSVe3PexbVxc,22999
|
|
588
588
|
langchain_agentx/tools/bash/auto_mode_adapter.py,sha256=7-yb_kGmpytBYkD2Hn5YWPIceo_YN4PatEKj8O7HKsM,4243
|
|
589
589
|
langchain_agentx/tools/bash/backend.py,sha256=E0DMBAENZLUSLvuEh6RubNJ6iCVnx1d2VGsQzVuLA5k,65361
|
|
590
590
|
langchain_agentx/tools/bash/bash_hardening.py,sha256=KEYKRC7kS_to4mzXuChvCmOIIb4dSUkOb9K1MBDtWDc,29296
|
|
591
591
|
langchain_agentx/tools/bash/bash_registry_content.py,sha256=YHE3PwuLBeFrEux4xNaDZjJpYtUSMFPLYr6A7g-fFxk,3569
|
|
592
592
|
langchain_agentx/tools/bash/bash_runtime_contract.py,sha256=s8vrvMl7405KLGw2hPFbzlzWohm5JX3c9cImMBgqh8I,1511
|
|
593
593
|
langchain_agentx/tools/bash/boundary_check.py,sha256=k9Bv-baIAUDE3-c_Brw3VSXXYMv_g3lTM0mabIadnwc,8822
|
|
594
|
-
langchain_agentx/tools/bash/command_allow_rules.py,sha256=
|
|
594
|
+
langchain_agentx/tools/bash/command_allow_rules.py,sha256=5WohbA_UT57oqEtwVPBCH0IIP6hjM-FYCYLt4hWX3mk,8853
|
|
595
595
|
langchain_agentx/tools/bash/cwd_reporter.py,sha256=mASbKWLb4MiX4zLKY3w1ocIdfhmwj5KRgX4OW_PcAns,2642
|
|
596
596
|
langchain_agentx/tools/bash/events.py,sha256=xDpPA3zhne2cnlYTmiiPTsV423-h-Nw-VKDR_rlnbGE,6065
|
|
597
597
|
langchain_agentx/tools/bash/l1_decision_reason.py,sha256=MOL9HBBpJEgypuTiYdNojQBl8eIsTwRge5CtGeYRJKI,5365
|
|
@@ -603,7 +603,7 @@ langchain_agentx/tools/bash/output_tracker.py,sha256=JJv_PVKEoFJljUrjlIjzlRx6GVo
|
|
|
603
603
|
langchain_agentx/tools/bash/output_utils.py,sha256=WYU2vVOjo9HQKSeQ_z88KxJsrvlFm0wf7USCMvuifZ8,6638
|
|
604
604
|
langchain_agentx/tools/bash/path_security.py,sha256=wUJ4RED77-urSwsIuaaZ0IcjYTHYUYSLpvIyMac7sn0,98788
|
|
605
605
|
langchain_agentx/tools/bash/prompt.py,sha256=28WBRLt5G6e-OUildOFbeEyw7eL3iI8T40pEwmWVYxo,12527
|
|
606
|
-
langchain_agentx/tools/bash/read_only_validation.py,sha256=
|
|
606
|
+
langchain_agentx/tools/bash/read_only_validation.py,sha256=Y_9cn01m2hGrohqAg7_4riV3hDfOsyyE4pIFIbXiPe4,26878
|
|
607
607
|
langchain_agentx/tools/bash/redirect_classifier.py,sha256=Xbmrb1VfZMdvlzvsI1YoOk57MkRLlwhf23GP90UNFqQ,8188
|
|
608
608
|
langchain_agentx/tools/bash/result_presenter.py,sha256=OkJid40AmrAsRxhY7jSaw_PgRMoIfMcj6BNXaqcryTo,14946
|
|
609
609
|
langchain_agentx/tools/bash/sandbox_decision.py,sha256=T_JaTfPADDcH2mRtrpKTBbIVo2Q6CHkzg_aUXCuDhLQ,4612
|
|
@@ -680,7 +680,7 @@ langchain_agentx/tools/skill/catalog.py,sha256=fhIsxJ5qq5RgFoav6OU_sFw0z6lBMYixv
|
|
|
680
680
|
langchain_agentx/tools/skill/dynamic_catalog.py,sha256=kc9tcAJMbvieIaO0SN89b58I0OIjPRsccKv8pLVoyXI,5409
|
|
681
681
|
langchain_agentx/tools/skill/dynamic_discoverer.py,sha256=1zUcWHJn96UFJeDY9NPsjpZAJ3iQFAq_Y7REyUGXfMo,2978
|
|
682
682
|
langchain_agentx/tools/skill/file_discovery.py,sha256=mEMSg5eqGbv0bU7ZWY_i7WdYZsL618pMo0wMrbunbSc,2202
|
|
683
|
-
langchain_agentx/tools/skill/fork.py,sha256=
|
|
683
|
+
langchain_agentx/tools/skill/fork.py,sha256=zOHrO-bgfXA4PH5f1dM5hIxpScSmVQeHAt_eV6u8SCI,7846
|
|
684
684
|
langchain_agentx/tools/skill/guards.py,sha256=tO6hLncKW0eMpWLrdnZLeMzfknL0ufbE8NxqfP91LGc,1484
|
|
685
685
|
langchain_agentx/tools/skill/hook_registration.py,sha256=M8ZTykK7ubN4j-SSbwg9WAzq5OpFaRI-IrnRwyiknSs,6261
|
|
686
686
|
langchain_agentx/tools/skill/loader.py,sha256=E0P90vvja5thi-1nPT4OkWtOniBkISvumA5agV150XU,10086
|
|
@@ -694,7 +694,7 @@ langchain_agentx/tools/skill/prompt.py,sha256=tvSqvCGroVdloveiWIYxIPQmpESqzPAs5Z
|
|
|
694
694
|
langchain_agentx/tools/skill/registry_permission.py,sha256=ns_S6IBOdacrZcgHzCOj9rjlPOIecavF5hW-pHmalcY,3258
|
|
695
695
|
langchain_agentx/tools/skill/repository_factory.py,sha256=aFYIw65ctz1sLfEmn8E3YfpgTvTc1YI9XkueWpeiXtA,1359
|
|
696
696
|
langchain_agentx/tools/skill/source_paths.py,sha256=x-WHorUgt2xyI5hcvALT2e6Ra-HDpKgIv7plQezRkws,5324
|
|
697
|
-
langchain_agentx/tools/skill/tool.py,sha256=
|
|
697
|
+
langchain_agentx/tools/skill/tool.py,sha256=a1-uUtNraWy6stcZ-hourI73hCGawzVIPE_2pFtHvFw,22202
|
|
698
698
|
langchain_agentx/tools/skill/bundled/__init__.py,sha256=gaCH_mROpv0lIgLjin1VIFu-8hbP2nY1PHoF7vZt2Ns,4516
|
|
699
699
|
langchain_agentx/tools/skill/bundled/prompts.py,sha256=gcaTnNRlWfHNPgO3EfZxU9vMYbOapMrAduvgaVc-_Fc,4838
|
|
700
700
|
langchain_agentx/tools/skill/bundled/registry.py,sha256=oxvvFeeUzM-qVPtayVaQp5tfRiMazyln2-rCM86AYa8,6274
|
|
@@ -778,7 +778,7 @@ langchain_agentx/workflow/catalog_registry.py,sha256=tYUny5FkHwQU-RRUxOdxaJhiRV_
|
|
|
778
778
|
langchain_agentx/workflow/dag.py,sha256=Y1IR1IwnM79Xuev6HZnOvAE3Q23MRnA9lu4vg2Cie48,4469
|
|
779
779
|
langchain_agentx/workflow/display_enricher.py,sha256=2HJ4xJI8H4hwBoWHAi6D_LwE-dcbZD_pafnS7DMDYZ4,6147
|
|
780
780
|
langchain_agentx/workflow/display_resolver.py,sha256=hEaljqprjubG82zeqLOEeQ32AMAxaZwvwaLq3v-yt3c,10119
|
|
781
|
-
langchain_agentx/workflow/execution.py,sha256=
|
|
781
|
+
langchain_agentx/workflow/execution.py,sha256=1exCcD1zxZ8vSx4js6ygnJ4xXAnK1UwT8rpIAYAfxxc,19155
|
|
782
782
|
langchain_agentx/workflow/loop_factory.py,sha256=hCShf_gDnesW2A9JspaUY3upp3NCIguII_wRiXnSIUM,3954
|
|
783
783
|
langchain_agentx/workflow/memory_drain_registry.py,sha256=Xm_ytbB-BbRIu3-h-KhptsCsmmueOiNZePUuG1hk8zk,1681
|
|
784
784
|
langchain_agentx/workflow/memory_session_policy.py,sha256=2FI0ZbAA6MnxJexnLYLadQpiqipPQwVnUrTWgl55A74,3333
|
|
@@ -831,14 +831,14 @@ langchain_agentx/workspace/path_canonical.py,sha256=Z_mmDJQMP4WmBhjBpRUGKGg7XCsK
|
|
|
831
831
|
langchain_agentx/workspace/path_key_normalizer.py,sha256=ICk9uGiiNFH-zaXJG3_vx91uOpgWmgOPIjJoSYevKOc,1113
|
|
832
832
|
langchain_agentx/workspace/plans.py,sha256=wo0ogpErnmwBrNwsHHCzARnIfHM7xnhpAMiN39SjCCI,11813
|
|
833
833
|
langchain_agentx/workspace/project_key.py,sha256=iA_C6oLM8ebWX6C4GXpJp-SO_n6O1heNcyNebC513QA,2808
|
|
834
|
-
langchain_agentx/workspace/project_memory_layout.py,sha256=
|
|
834
|
+
langchain_agentx/workspace/project_memory_layout.py,sha256=v7Xva8v8E6zjkCRuUy6gAWMkRaicndPJoM5AvuyIFCA,8847
|
|
835
835
|
langchain_agentx/workspace/resolver.py,sha256=stWBxa-jkNO2nZKEJr89Qb4805FcEFm76NpybWb-LYs,6444
|
|
836
836
|
langchain_agentx/workspace/root_grant_manager.py,sha256=W3gy8HTevbERbXqIgRcjzOXOUBGr1Gjnl8CfCoTIuYc,7558
|
|
837
837
|
langchain_agentx/workspace/tool_boundary.py,sha256=UDwX6swpSLsx9HNkYuuRRiV5o7ZZG_Bru2Yn0c5kNX8,5724
|
|
838
838
|
langchain_agentx/workspace/validators.py,sha256=tQt-6TOcL8Fw7Ig5ebA9S7vGWh1rby920eFW6x8Tk9E,1439
|
|
839
839
|
langchain_agentx/workspace/view.py,sha256=PGasqTaqhlD03SXXazHuw4RHfV681AIdlsYqL70pEjc,4774
|
|
840
|
-
langchain_agentx_python-2.
|
|
841
|
-
langchain_agentx_python-2.
|
|
842
|
-
langchain_agentx_python-2.
|
|
843
|
-
langchain_agentx_python-2.
|
|
844
|
-
langchain_agentx_python-2.
|
|
840
|
+
langchain_agentx_python-2.4.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
841
|
+
langchain_agentx_python-2.4.0.dist-info/METADATA,sha256=8umkqCu3uA5-5h8qh8wIatqM0aMpj_UV_b6j1MKgF6A,23971
|
|
842
|
+
langchain_agentx_python-2.4.0.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
|
|
843
|
+
langchain_agentx_python-2.4.0.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
|
|
844
|
+
langchain_agentx_python-2.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{langchain_agentx_python-2.3.8.dist-info → langchain_agentx_python-2.4.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|