langchain-agentx-python 1.3.2__py3-none-any.whl → 1.3.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/command/dispatcher.py +69 -0
- langchain_agentx/loop/graph/factory.py +39 -2
- langchain_agentx/session/agent_session.py +15 -0
- langchain_agentx/session/conversation_session.py +27 -3
- langchain_agentx/task_runtime/skill_prefetch/provider.py +48 -19
- langchain_agentx/tool_runtime/read_permission.py +7 -0
- langchain_agentx/tool_runtime/session_store.py +4 -0
- langchain_agentx/tools/ask_user_question/constants.py +10 -0
- langchain_agentx/tools/ask_user_question/tool.py +14 -3
- langchain_agentx/tools/skill/__init__.py +2 -1
- langchain_agentx/tools/skill/bundled/__init__.py +157 -0
- langchain_agentx/tools/skill/bundled/prompts.py +143 -0
- langchain_agentx/tools/skill/bundled/registry.py +162 -0
- langchain_agentx/tools/skill/catalog.py +463 -0
- langchain_agentx/tools/skill/dynamic_catalog.py +62 -4
- langchain_agentx/tools/skill/file_discovery.py +24 -1
- langchain_agentx/tools/skill/hook_registration.py +110 -0
- langchain_agentx/tools/skill/loader.py +104 -36
- langchain_agentx/tools/skill/models.py +3 -3
- langchain_agentx/tools/skill/multi_source_repository.py +93 -0
- langchain_agentx/tools/skill/paths_matcher.py +119 -0
- langchain_agentx/tools/skill/plugin_loader.py +43 -0
- langchain_agentx/tools/skill/policy.py +2 -37
- langchain_agentx/tools/skill/repository_factory.py +39 -0
- langchain_agentx/tools/skill/source_paths.py +88 -0
- langchain_agentx/tools/skill/tool.py +57 -8
- langchain_agentx/utils/claude_temp_paths.py +34 -1
- langchain_agentx/utils/permissions/filesystem.py +6 -1
- langchain_agentx/utils/settings/__init__.py +5 -0
- langchain_agentx/utils/settings/managed_path.py +38 -0
- langchain_agentx/workspace/config.py +6 -0
- {langchain_agentx_python-1.3.2.dist-info → langchain_agentx_python-1.3.4.dist-info}/METADATA +1 -1
- {langchain_agentx_python-1.3.2.dist-info → langchain_agentx_python-1.3.4.dist-info}/RECORD +37 -25
- {langchain_agentx_python-1.3.2.dist-info → langchain_agentx_python-1.3.4.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-1.3.2.dist-info → langchain_agentx_python-1.3.4.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-1.3.2.dist-info → langchain_agentx_python-1.3.4.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -22,6 +22,12 @@ from .registry import CommandRegistry
|
|
|
22
22
|
from .result import CommandResult
|
|
23
23
|
from .types import CommandDefinition
|
|
24
24
|
from ..plugin.registries import CommandEntry, CommandRegistry as PluginCommandRegistry
|
|
25
|
+
from ..tools.skill.catalog import (
|
|
26
|
+
SkillCatalogService,
|
|
27
|
+
build_skill_catalog_for_context,
|
|
28
|
+
resolve_dynamic_catalog,
|
|
29
|
+
_session_store_from_context,
|
|
30
|
+
)
|
|
25
31
|
|
|
26
32
|
logger = logging.getLogger(__name__)
|
|
27
33
|
|
|
@@ -31,9 +37,11 @@ class CommandDispatcher:
|
|
|
31
37
|
self,
|
|
32
38
|
registry: CommandRegistry,
|
|
33
39
|
plugin_command_registry: PluginCommandRegistry | None = None,
|
|
40
|
+
skill_catalog: SkillCatalogService | None = None,
|
|
34
41
|
) -> None:
|
|
35
42
|
self._registry = registry
|
|
36
43
|
self._plugin_registry = plugin_command_registry
|
|
44
|
+
self._skill_catalog = skill_catalog
|
|
37
45
|
self._plugin_markdown_cache: dict[Path, tuple[tuple[int, int, int], tuple[str, str]]] = {}
|
|
38
46
|
|
|
39
47
|
def is_slash_command(self, user_input: str) -> bool:
|
|
@@ -53,6 +61,9 @@ class CommandDispatcher:
|
|
|
53
61
|
if lookup_error is not None:
|
|
54
62
|
return CommandResult(error=self._format_error("lookup", lookup_error))
|
|
55
63
|
if command is None:
|
|
64
|
+
skill_result = await self._dispatch_disk_skill(name, args, context)
|
|
65
|
+
if skill_result is not None:
|
|
66
|
+
return skill_result
|
|
56
67
|
return CommandResult(error=self._format_error("lookup", f"unknown command: /{name}"))
|
|
57
68
|
if not command.user_invocable:
|
|
58
69
|
return CommandResult(
|
|
@@ -186,6 +197,64 @@ class CommandDispatcher:
|
|
|
186
197
|
return None, f"unknown command: /{name} (ambiguous plugin command: {candidates})"
|
|
187
198
|
return None, None
|
|
188
199
|
|
|
200
|
+
async def _dispatch_disk_skill(
|
|
201
|
+
self,
|
|
202
|
+
name: str,
|
|
203
|
+
args: str,
|
|
204
|
+
context: CommandContext,
|
|
205
|
+
) -> CommandResult | None:
|
|
206
|
+
catalog = self._skill_catalog or build_skill_catalog_for_context(context)
|
|
207
|
+
segment = context.workspace_cfg.agent_home_segment
|
|
208
|
+
session_store = _session_store_from_context(context)
|
|
209
|
+
dynamic = resolve_dynamic_catalog(session_store, agent_home_segment=segment)
|
|
210
|
+
cmd = catalog.find_by_name(
|
|
211
|
+
name,
|
|
212
|
+
dynamic_catalog=dynamic,
|
|
213
|
+
session_store=session_store,
|
|
214
|
+
agent_home_segment=segment,
|
|
215
|
+
)
|
|
216
|
+
if cmd is None:
|
|
217
|
+
return None
|
|
218
|
+
if not catalog.is_user_invocable(cmd):
|
|
219
|
+
return CommandResult(
|
|
220
|
+
error=self._format_error(
|
|
221
|
+
"permission",
|
|
222
|
+
(
|
|
223
|
+
f'/{name} can only be invoked by the model via the Skill tool, '
|
|
224
|
+
f'not directly by users. Ask the agent to use the "{name}" skill.'
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
expanded = catalog.expand_for_slash(
|
|
229
|
+
name,
|
|
230
|
+
args,
|
|
231
|
+
dynamic_catalog=dynamic,
|
|
232
|
+
session_store=session_store,
|
|
233
|
+
agent_home_segment=segment,
|
|
234
|
+
)
|
|
235
|
+
if expanded is None:
|
|
236
|
+
return None
|
|
237
|
+
from langchain_agentx.tools.skill.hook_registration import (
|
|
238
|
+
register_skill_hooks,
|
|
239
|
+
resolve_hooks_snapshot,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
register_skill_hooks(
|
|
243
|
+
resolve_hooks_snapshot(
|
|
244
|
+
session_store=session_store,
|
|
245
|
+
hook_engine=context.hook_engine,
|
|
246
|
+
),
|
|
247
|
+
skill_name=expanded.command.name,
|
|
248
|
+
skill_root=expanded.command.path.parent,
|
|
249
|
+
frontmatter=expanded.command.frontmatter,
|
|
250
|
+
)
|
|
251
|
+
return CommandResult(
|
|
252
|
+
should_query=True,
|
|
253
|
+
injected_prompt=expanded.content,
|
|
254
|
+
allowed_tools=list(expanded.allowed_tools) if expanded.allowed_tools else None,
|
|
255
|
+
execution_context=expanded.execution_context,
|
|
256
|
+
)
|
|
257
|
+
|
|
189
258
|
@staticmethod
|
|
190
259
|
def _format_error(error_type: str, detail: str) -> str:
|
|
191
260
|
return f"[command/{error_type}] {detail}"
|
|
@@ -573,6 +573,11 @@ class LoopGraphBuilder:
|
|
|
573
573
|
if self._services.hooks is not None
|
|
574
574
|
else HooksConfigSnapshot()
|
|
575
575
|
)
|
|
576
|
+
if session_store is not None:
|
|
577
|
+
session_store.hooks_snapshot = self._hook_snapshot
|
|
578
|
+
from langchain_agentx.tools.skill.repository_factory import build_skill_repository
|
|
579
|
+
|
|
580
|
+
session_store.skill_repository = build_skill_repository(self._workspace_cfg)
|
|
576
581
|
self._trace_callback_handler: TraceCallbackHandler | None = None
|
|
577
582
|
self._trace_tool_callback_handler: TraceCallbackHandler | None = None
|
|
578
583
|
hook_event_emitter = None
|
|
@@ -1539,11 +1544,32 @@ class LoopGraphBuilder:
|
|
|
1539
1544
|
# Let providers optionally ingest current loop messages before reserve.
|
|
1540
1545
|
# This enables auto-bridge scenarios like tool_use_summary without external pump.
|
|
1541
1546
|
state_messages = list(state.get("messages") or [])
|
|
1547
|
+
session_store = None
|
|
1548
|
+
try:
|
|
1549
|
+
from langgraph.config import get_config
|
|
1550
|
+
|
|
1551
|
+
configurable = (get_config() or {}).get("configurable") or {}
|
|
1552
|
+
session_store = configurable.get("session_store")
|
|
1553
|
+
except Exception:
|
|
1554
|
+
pass
|
|
1542
1555
|
for provider in provider_chain:
|
|
1543
1556
|
ingest = getattr(provider, "ingest_from_messages", None)
|
|
1544
1557
|
if callable(ingest):
|
|
1545
1558
|
try:
|
|
1546
|
-
ingest(
|
|
1559
|
+
ingest(
|
|
1560
|
+
scope=scope,
|
|
1561
|
+
messages=state_messages,
|
|
1562
|
+
session_store=session_store,
|
|
1563
|
+
)
|
|
1564
|
+
except TypeError:
|
|
1565
|
+
try:
|
|
1566
|
+
ingest(scope=scope, messages=state_messages)
|
|
1567
|
+
except Exception:
|
|
1568
|
+
logger.debug(
|
|
1569
|
+
"provider ingest_from_messages failed; skip provider=%s",
|
|
1570
|
+
provider.__class__.__name__,
|
|
1571
|
+
exc_info=True,
|
|
1572
|
+
)
|
|
1547
1573
|
except Exception:
|
|
1548
1574
|
logger.debug(
|
|
1549
1575
|
"provider ingest_from_messages failed; skip provider=%s",
|
|
@@ -1766,7 +1792,18 @@ class LoopGraphBuilder:
|
|
|
1766
1792
|
content = getattr(msg, "content", "")
|
|
1767
1793
|
if isinstance(content, str) and "[skill_listing]" in content:
|
|
1768
1794
|
return {}
|
|
1769
|
-
|
|
1795
|
+
session_store = None
|
|
1796
|
+
try:
|
|
1797
|
+
from langgraph.config import get_config
|
|
1798
|
+
|
|
1799
|
+
configurable = (get_config() or {}).get("configurable") or {}
|
|
1800
|
+
session_store = configurable.get("session_store")
|
|
1801
|
+
except Exception:
|
|
1802
|
+
pass
|
|
1803
|
+
hint = skill_prefetch_provider._build_listing_hint(
|
|
1804
|
+
scope=skill_init_scope,
|
|
1805
|
+
session_store=session_store,
|
|
1806
|
+
)
|
|
1770
1807
|
if hint is None:
|
|
1771
1808
|
return {}
|
|
1772
1809
|
# skill_listing 以 HumanMessage 注入(非 SystemMessage),与 CC role=user 对齐
|
|
@@ -427,6 +427,21 @@ class AgentSession:
|
|
|
427
427
|
def list_commands(self) -> list[dict[str, object]]:
|
|
428
428
|
return self._runtime_command_registry.list_for_help()
|
|
429
429
|
|
|
430
|
+
def list_skills(self) -> dict[str, object]:
|
|
431
|
+
"""CLI ``/skills`` 菜单:按 source 分组 + token 估算。"""
|
|
432
|
+
from ..tools.skill.catalog import build_skill_catalog, resolve_dynamic_catalog
|
|
433
|
+
|
|
434
|
+
catalog = build_skill_catalog(
|
|
435
|
+
workspace_root=self._workspace_cfg.workspace_root,
|
|
436
|
+
agent_home_segment=self._workspace_cfg.agent_home_segment,
|
|
437
|
+
plugin_registry=self._skill_registry,
|
|
438
|
+
)
|
|
439
|
+
dynamic = resolve_dynamic_catalog(
|
|
440
|
+
self._resolve_session_store(),
|
|
441
|
+
agent_home_segment=self._workspace_cfg.agent_home_segment,
|
|
442
|
+
)
|
|
443
|
+
return catalog.list_skills_menu(dynamic_catalog=dynamic)
|
|
444
|
+
|
|
430
445
|
async def _execute_hook(self, event: HookEvent) -> None:
|
|
431
446
|
ctx = HookContext(event=event, state={}, session_id=self._session_id)
|
|
432
447
|
# HookEngine 当前主接口是 execute;若未来扩展 execute_async,优先兼容新接口。
|
|
@@ -34,6 +34,7 @@ from ..command.builtin import (
|
|
|
34
34
|
make_memory_command,
|
|
35
35
|
make_reload_plugins_command,
|
|
36
36
|
)
|
|
37
|
+
from ..loop.config import merge_run_config
|
|
37
38
|
from ..loop.config.loop_runtime_overlay import LoopRuntimeOverlay
|
|
38
39
|
from ..loop.context.compaction_service import default_loop_context_compaction
|
|
39
40
|
from ..loop.context.message_utils import total_estimated_tokens_for_messages
|
|
@@ -119,12 +120,14 @@ class ConversationSession:
|
|
|
119
120
|
capabilities=self._capabilities,
|
|
120
121
|
),
|
|
121
122
|
)
|
|
123
|
+
run_config = merge_run_config(graph, None)
|
|
122
124
|
try:
|
|
123
125
|
result = await graph.ainvoke(
|
|
124
126
|
{
|
|
125
127
|
"messages": list(self._messages),
|
|
126
128
|
"_session_id": self._conversation_id,
|
|
127
|
-
}
|
|
129
|
+
},
|
|
130
|
+
config=run_config,
|
|
128
131
|
)
|
|
129
132
|
except Exception:
|
|
130
133
|
# 回滚本轮 append,避免异常路径消息重复累积。
|
|
@@ -174,13 +177,15 @@ class ConversationSession:
|
|
|
174
177
|
prompt = result.injected_prompt or user_input
|
|
175
178
|
appended_message = HumanMessage(content=prompt)
|
|
176
179
|
self._messages.append(appended_message)
|
|
180
|
+
run_config = merge_run_config(graph, None)
|
|
177
181
|
try:
|
|
178
182
|
apply_command_tool_scope(store, result.allowed_tools)
|
|
179
183
|
loop_output = await graph.ainvoke(
|
|
180
184
|
{
|
|
181
185
|
"messages": list(self._messages),
|
|
182
186
|
"_session_id": self._conversation_id,
|
|
183
|
-
}
|
|
187
|
+
},
|
|
188
|
+
config=run_config,
|
|
184
189
|
)
|
|
185
190
|
except Exception:
|
|
186
191
|
if self._messages and self._messages[-1] is appended_message:
|
|
@@ -220,6 +225,7 @@ class ConversationSession:
|
|
|
220
225
|
)
|
|
221
226
|
output_messages: list[Any] | None = None
|
|
222
227
|
graph_output: dict[str, Any] | None = None
|
|
228
|
+
run_config = merge_run_config(graph, config)
|
|
223
229
|
driver = LoopStreamDriver()
|
|
224
230
|
try:
|
|
225
231
|
async for event in driver.run(
|
|
@@ -228,7 +234,7 @@ class ConversationSession:
|
|
|
228
234
|
"messages": list(self._messages),
|
|
229
235
|
"_session_id": self._conversation_id,
|
|
230
236
|
},
|
|
231
|
-
config=
|
|
237
|
+
config=run_config,
|
|
232
238
|
version="v2",
|
|
233
239
|
):
|
|
234
240
|
if event.get("event") == "on_chain_end" and event.get("name") == "LangGraph":
|
|
@@ -313,6 +319,24 @@ class ConversationSession:
|
|
|
313
319
|
def list_commands(self) -> list[dict[str, object]]:
|
|
314
320
|
return self._runtime_command_registry.list_for_help()
|
|
315
321
|
|
|
322
|
+
def list_skills(self) -> dict[str, object]:
|
|
323
|
+
"""CLI ``/skills`` 菜单:按 source 分组 + token 估算。"""
|
|
324
|
+
from ..tools.skill.catalog import build_skill_catalog, resolve_dynamic_catalog
|
|
325
|
+
|
|
326
|
+
catalog = build_skill_catalog(
|
|
327
|
+
workspace_root=self._workspace_cfg.workspace_root,
|
|
328
|
+
agent_home_segment=self._workspace_cfg.agent_home_segment,
|
|
329
|
+
plugin_registry=self._skill_registry,
|
|
330
|
+
)
|
|
331
|
+
dynamic = resolve_dynamic_catalog(
|
|
332
|
+
self._resolve_session_store(),
|
|
333
|
+
agent_home_segment=self._workspace_cfg.agent_home_segment,
|
|
334
|
+
)
|
|
335
|
+
return catalog.list_skills_menu(dynamic_catalog=dynamic)
|
|
336
|
+
|
|
337
|
+
def _resolve_session_store(self) -> Any | None:
|
|
338
|
+
return None
|
|
339
|
+
|
|
316
340
|
async def _execute_hook(self, event: HookEvent) -> None:
|
|
317
341
|
ctx = HookContext(event=event, state={}, session_id=self._conversation_id)
|
|
318
342
|
execute_async = getattr(self._hook_engine, "execute_async", None)
|
|
@@ -30,6 +30,8 @@ from .attachments import SkillAttachmentBuilder
|
|
|
30
30
|
from .models import SkillHint
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
|
+
from langchain_agentx.tools.skill.catalog import SkillCatalogService
|
|
34
|
+
from langchain_agentx.tools.skill.dynamic_catalog import DynamicSkillSessionCatalog
|
|
33
35
|
from langchain_agentx.tools.skill.loader import SkillCommandRepository
|
|
34
36
|
|
|
35
37
|
_PRIORITY_ORDER = {
|
|
@@ -56,6 +58,7 @@ class SkillPrefetchProvider:
|
|
|
56
58
|
agent_home_segment: str = ".langchain_agentx",
|
|
57
59
|
skills_root: str | Path | None = None,
|
|
58
60
|
repository: "SkillCommandRepository | None" = None,
|
|
61
|
+
catalog: "SkillCatalogService | None" = None,
|
|
59
62
|
attachment_builder: SkillAttachmentBuilder | None = None,
|
|
60
63
|
max_queue_size: int | None = None,
|
|
61
64
|
ttl_sec: float | None = None,
|
|
@@ -70,12 +73,22 @@ class SkillPrefetchProvider:
|
|
|
70
73
|
if skills_root
|
|
71
74
|
else workspace_cfg.skills_dir
|
|
72
75
|
)
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if catalog is not None:
|
|
77
|
+
self._catalog = catalog
|
|
78
|
+
elif repository is not None:
|
|
79
|
+
from langchain_agentx.tools.skill.catalog import SkillCatalogService
|
|
80
|
+
|
|
81
|
+
self._catalog = SkillCatalogService(repository=repository)
|
|
82
|
+
else:
|
|
83
|
+
from langchain_agentx.tools.skill.catalog import build_skill_catalog
|
|
84
|
+
|
|
85
|
+
self._catalog = build_skill_catalog(
|
|
86
|
+
workspace_root=workspace_root,
|
|
87
|
+
agent_home_segment=agent_home_segment,
|
|
88
|
+
skills_root=skills_root,
|
|
89
|
+
)
|
|
90
|
+
self._repository = self._catalog.repository
|
|
91
|
+
self._agent_home_segment = agent_home_segment
|
|
79
92
|
self._attachment_builder = attachment_builder or SkillAttachmentBuilder()
|
|
80
93
|
self._lock = RLock()
|
|
81
94
|
self._queue: list[SkillHint] = []
|
|
@@ -204,25 +217,29 @@ class SkillPrefetchProvider:
|
|
|
204
217
|
for item in self._queue
|
|
205
218
|
)
|
|
206
219
|
|
|
207
|
-
def ingest_from_messages(
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
def ingest_from_messages(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
scope: TaskScope,
|
|
224
|
+
messages: list,
|
|
225
|
+
session_store: object | None = None,
|
|
226
|
+
) -> int:
|
|
227
|
+
# v1:listing 刷新(幂等);dynamic 来自 session_store 上的 walk-up catalog。
|
|
210
228
|
del messages
|
|
211
|
-
hint = self._build_listing_hint(scope=scope)
|
|
229
|
+
hint = self._build_listing_hint(scope=scope, session_store=session_store)
|
|
212
230
|
if hint is None:
|
|
213
231
|
return 0
|
|
214
232
|
self.enqueue_hint(hint)
|
|
215
233
|
return 1
|
|
216
234
|
|
|
217
|
-
def _build_listing_hint(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
visible = [c for c in ordered if c.visible_in_slash_command_tool_listing()]
|
|
235
|
+
def _build_listing_hint(
|
|
236
|
+
self,
|
|
237
|
+
*,
|
|
238
|
+
scope: TaskScope,
|
|
239
|
+
session_store: object | None = None,
|
|
240
|
+
) -> SkillHint | None:
|
|
241
|
+
dynamic = self._resolve_dynamic_catalog(session_store)
|
|
242
|
+
visible = self._catalog.list_for_listing(dynamic_catalog=dynamic)
|
|
226
243
|
if not visible:
|
|
227
244
|
return None
|
|
228
245
|
signature = tuple(
|
|
@@ -251,6 +268,18 @@ class SkillPrefetchProvider:
|
|
|
251
268
|
dedup_key=f"skill_listing:{scope_id}",
|
|
252
269
|
)
|
|
253
270
|
|
|
271
|
+
def _resolve_dynamic_catalog(
|
|
272
|
+
self, session_store: object | None
|
|
273
|
+
) -> "DynamicSkillSessionCatalog | None":
|
|
274
|
+
if session_store is None:
|
|
275
|
+
return None
|
|
276
|
+
from langchain_agentx.tools.skill.catalog import resolve_dynamic_catalog
|
|
277
|
+
|
|
278
|
+
return resolve_dynamic_catalog(
|
|
279
|
+
session_store,
|
|
280
|
+
agent_home_segment=self._agent_home_segment,
|
|
281
|
+
)
|
|
282
|
+
|
|
254
283
|
@staticmethod
|
|
255
284
|
def _scope_id(scope: TaskScope) -> str:
|
|
256
285
|
return scope.agent_id or "main"
|
|
@@ -196,6 +196,13 @@ class ReadPathAuthorizer:
|
|
|
196
196
|
policy_id="readable_internal_project_temp",
|
|
197
197
|
message="Read allowed for project temp directory path.",
|
|
198
198
|
)
|
|
199
|
+
|
|
200
|
+
if self._internal.is_bundled_skills_path(real_path):
|
|
201
|
+
return AuthorizationDecision(
|
|
202
|
+
behavior="allow",
|
|
203
|
+
policy_id="readable_internal_bundled_skills",
|
|
204
|
+
message="Read allowed for bundled skill reference files.",
|
|
205
|
+
)
|
|
199
206
|
return None
|
|
200
207
|
|
|
201
208
|
def _evaluate_unc(self, real_path: str) -> AuthorizationDecision | None:
|
|
@@ -15,6 +15,8 @@ runtime/session_store.py — 会话级状态存储
|
|
|
15
15
|
granted_roots — 动态扩展的 read/write 边界(Phase 2 grant API)
|
|
16
16
|
denial_tracking — L3 连续拒绝计数(GAP-7;子代理 fork_local 隔离)
|
|
17
17
|
dynamic_skill_catalog — 嵌套 skills walk-up 缓存(D6c;类型为 tools.skill 侧 catalog)
|
|
18
|
+
hooks_snapshot — HookEngine 可变快照引用(skill frontmatter hooks 注册用)
|
|
19
|
+
skill_repository — 会话级 skill 仓库(Read/Write/Edit 条件 skill seed 用)
|
|
18
20
|
|
|
19
21
|
链路位置:
|
|
20
22
|
GraphFactory 注入 configurable["session_store"] → LangChainAdapter.build_tool_execution_context
|
|
@@ -123,6 +125,8 @@ class AgentSessionStore:
|
|
|
123
125
|
self._root_grant_manager: Any | None = None
|
|
124
126
|
self._denial_tracker: Any | None = None
|
|
125
127
|
self.dynamic_skill_catalog: Any = None
|
|
128
|
+
self.hooks_snapshot: Any = None
|
|
129
|
+
self.skill_repository: Any = None
|
|
126
130
|
|
|
127
131
|
def record_file_read(
|
|
128
132
|
self,
|
|
@@ -24,3 +24,13 @@ MAX_PREVIEW_SNAPSHOT_CHARS: int = 8192
|
|
|
24
24
|
|
|
25
25
|
# present():每段 preview / snapshot 写入模型可见文本时的默认截断
|
|
26
26
|
PRESENT_PREVIEW_SNIPPET_MAX: int = 500
|
|
27
|
+
|
|
28
|
+
# 全角问号(中文输入法常见);校验要求 ASCII '?'
|
|
29
|
+
FULLWIDTH_QUESTION_MARK: str = "\uff1f"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize_question_text(text: str) -> str:
|
|
33
|
+
"""将题干末尾全角 ``?`` 规范为 ASCII ``?``(CC / QuestionValidator 一致)。"""
|
|
34
|
+
if text.endswith(FULLWIDTH_QUESTION_MARK):
|
|
35
|
+
return text[:-1] + "?"
|
|
36
|
+
return text
|
|
@@ -29,7 +29,7 @@ from langchain_agentx.tool_runtime.models import (
|
|
|
29
29
|
ValidationResult,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
from .constants import PRESENT_PREVIEW_SNIPPET_MAX
|
|
32
|
+
from .constants import PRESENT_PREVIEW_SNIPPET_MAX, normalize_question_text
|
|
33
33
|
from .html_preview import HtmlPreviewValidator
|
|
34
34
|
from .models import AskUserQuestionInput, AskUserQuestionOutput, QuestionAnnotation
|
|
35
35
|
from .prompt import (
|
|
@@ -116,8 +116,19 @@ class AskUserQuestionTool(RuntimeTool):
|
|
|
116
116
|
questions = data.get("questions")
|
|
117
117
|
if isinstance(questions, list):
|
|
118
118
|
for q in questions:
|
|
119
|
-
if isinstance(q, dict)
|
|
120
|
-
|
|
119
|
+
if isinstance(q, dict):
|
|
120
|
+
if "multiSelect" not in q:
|
|
121
|
+
q["multiSelect"] = False
|
|
122
|
+
qt = q.get("question")
|
|
123
|
+
if isinstance(qt, str):
|
|
124
|
+
q["question"] = normalize_question_text(qt)
|
|
125
|
+
for key in ("answers", "annotations"):
|
|
126
|
+
mapping = data.get(key)
|
|
127
|
+
if isinstance(mapping, dict) and mapping:
|
|
128
|
+
data[key] = {
|
|
129
|
+
normalize_question_text(k) if isinstance(k, str) else k: v
|
|
130
|
+
for k, v in mapping.items()
|
|
131
|
+
}
|
|
121
132
|
if "answers" not in data or data["answers"] is None:
|
|
122
133
|
data["answers"] = {}
|
|
123
134
|
if "annotations" not in data or data["annotations"] is None:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/skill/bundled/__init__.py — Product bundled skill 初始化
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
进程级注册 debug / simplify / batch / update-config bundled skill。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
``SkillCatalogService`` / ``SkillRuntimeTool`` / ``build_skill_catalog`` 启动时调用。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from langchain_agentx.utils.agent_settings import normalize_agent_home_segment
|
|
14
|
+
|
|
15
|
+
from ..models import SkillCommand
|
|
16
|
+
from .prompts import (
|
|
17
|
+
BATCH_MISSING_INSTRUCTION,
|
|
18
|
+
BATCH_NOT_GIT_REPO,
|
|
19
|
+
SIMPLIFY_PROMPT,
|
|
20
|
+
build_batch_prompt,
|
|
21
|
+
build_debug_prompt,
|
|
22
|
+
build_update_config_prompt,
|
|
23
|
+
)
|
|
24
|
+
from .registry import (
|
|
25
|
+
BundledSkillDefinition,
|
|
26
|
+
BundledSkillRegistry,
|
|
27
|
+
bundled_skills_disabled,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_registry: BundledSkillRegistry | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _settings_hint(agent_home_segment: str = ".langchain_agentx") -> str:
|
|
34
|
+
seg = normalize_agent_home_segment(agent_home_segment)
|
|
35
|
+
return (
|
|
36
|
+
f"- Project settings: `{seg}/settings.json`\n"
|
|
37
|
+
f"- Local overrides: `{seg}/settings.local.json` (typically gitignored)"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _register_debug(registry: BundledSkillRegistry) -> None:
|
|
42
|
+
hint = _settings_hint()
|
|
43
|
+
|
|
44
|
+
def _content(args: str) -> str:
|
|
45
|
+
return build_debug_prompt(args=args, settings_hint=hint)
|
|
46
|
+
|
|
47
|
+
registry.register(
|
|
48
|
+
BundledSkillDefinition(
|
|
49
|
+
name="debug",
|
|
50
|
+
description="Enable debug logging for this session and help diagnose issues",
|
|
51
|
+
content=_content,
|
|
52
|
+
allowed_tools=["Read", "Grep", "Glob"],
|
|
53
|
+
disable_model_invocation=True,
|
|
54
|
+
user_invocable=True,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _register_simplify(registry: BundledSkillRegistry) -> None:
|
|
60
|
+
def _content(args: str) -> str:
|
|
61
|
+
prompt = SIMPLIFY_PROMPT
|
|
62
|
+
if args.strip():
|
|
63
|
+
prompt += f"\n\n## Additional Focus\n\n{args.strip()}\n"
|
|
64
|
+
return prompt
|
|
65
|
+
|
|
66
|
+
registry.register(
|
|
67
|
+
BundledSkillDefinition(
|
|
68
|
+
name="simplify",
|
|
69
|
+
description=(
|
|
70
|
+
"Review changed code for reuse, quality, and efficiency, then fix any issues found."
|
|
71
|
+
),
|
|
72
|
+
content=_content,
|
|
73
|
+
user_invocable=True,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _register_batch(registry: BundledSkillRegistry) -> None:
|
|
79
|
+
def _content(args: str) -> str:
|
|
80
|
+
instruction = args.strip()
|
|
81
|
+
if not instruction:
|
|
82
|
+
return BATCH_MISSING_INSTRUCTION
|
|
83
|
+
try:
|
|
84
|
+
from pathlib import Path
|
|
85
|
+
|
|
86
|
+
if not (Path.cwd() / ".git").exists():
|
|
87
|
+
return BATCH_NOT_GIT_REPO
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
return build_batch_prompt(instruction)
|
|
91
|
+
|
|
92
|
+
registry.register(
|
|
93
|
+
BundledSkillDefinition(
|
|
94
|
+
name="batch",
|
|
95
|
+
description=(
|
|
96
|
+
"Research and plan a large-scale change, then execute it in parallel across "
|
|
97
|
+
"isolated worktree agents that each open a PR."
|
|
98
|
+
),
|
|
99
|
+
when_to_use=(
|
|
100
|
+
"Use when the user wants a sweeping mechanical change across many files "
|
|
101
|
+
"that can be decomposed into independent parallel units."
|
|
102
|
+
),
|
|
103
|
+
content=_content,
|
|
104
|
+
user_invocable=True,
|
|
105
|
+
disable_model_invocation=True,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _register_update_config(registry: BundledSkillRegistry) -> None:
|
|
111
|
+
hint = _settings_hint()
|
|
112
|
+
|
|
113
|
+
def _content(args: str) -> str:
|
|
114
|
+
return build_update_config_prompt(args=args, settings_hint=hint)
|
|
115
|
+
|
|
116
|
+
registry.register(
|
|
117
|
+
BundledSkillDefinition(
|
|
118
|
+
name="update-config",
|
|
119
|
+
description="Update agent settings.json / settings.local.json safely",
|
|
120
|
+
content=_content,
|
|
121
|
+
allowed_tools=["Read"],
|
|
122
|
+
user_invocable=True,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def init_bundled_skills() -> BundledSkillRegistry:
|
|
128
|
+
global _registry
|
|
129
|
+
if _registry is not None:
|
|
130
|
+
return _registry
|
|
131
|
+
registry = BundledSkillRegistry()
|
|
132
|
+
if not bundled_skills_disabled():
|
|
133
|
+
_register_debug(registry)
|
|
134
|
+
_register_simplify(registry)
|
|
135
|
+
_register_batch(registry)
|
|
136
|
+
_register_update_config(registry)
|
|
137
|
+
_registry = registry
|
|
138
|
+
return registry
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_bundled_skill_commands() -> list[SkillCommand]:
|
|
142
|
+
return init_bundled_skills().list_commands()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def reset_bundled_skills_for_tests() -> None:
|
|
146
|
+
"""测试专用:清空进程级 registry。"""
|
|
147
|
+
global _registry
|
|
148
|
+
_registry = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = [
|
|
152
|
+
"BundledSkillDefinition",
|
|
153
|
+
"BundledSkillRegistry",
|
|
154
|
+
"get_bundled_skill_commands",
|
|
155
|
+
"init_bundled_skills",
|
|
156
|
+
"reset_bundled_skills_for_tests",
|
|
157
|
+
]
|