aps-agent/server/agent_core/tool_runtime.py

85 lines
4.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 统一工具运行时 v1(moduleId: core-tool-runtime, 可重生 ✅)
# plan.md §3.3 + 矩阵「LLM 只有提议权」:所有 LLM 工具调用必须经本入口执行。
# - LLM 提议的 intent 必须先登记(_POWER_MAP 或只读意图白名单),未登记 → 拒绝 + 审计;
# - 已登记意图委托 handle_intent(P0/P1 直通,P2/P3 经 harness 确认卡,绝不绕过门禁);
# - 每次执行记录 TOOL 审计事件(actor/source/power),溯源 LLM 提议。
# 不能通过未登记 API 或直接 store 写入绕过:本模块是 gateway 聊天的唯一执行入口。
# ============================================================
from __future__ import annotations
import asyncio
from server.agent_core import harness
from server.agent_core.audit import write_audit
from server.contracts import AgentReply, IntentResult
# 只读意图白名单:不写世界状态,但允许 LLM 提议直接执行(分析/查询/生成建议)
_READONLY_INTENTS = frozenset({
"help", "guidance.next", "master.query", "plan.buckets", "plan.rccp",
"plan.feasibility", "plan.inventory", "plan.leveling", "plan.supply",
"plan.trace", "data.analyze", "knowledge.query", "scenario.compare",
"report.generate", "order.pool", "conflict.list", "flex.capacity",
"flex.compare", "flex.simulate_due", "skill.list",
})
def is_registered(intent_name: str) -> bool:
"""intent 是否已登记:写入类必须在 _POWER_MAP;只读类在白名单。"""
if intent_name in _READONLY_INTENTS:
return True
return harness.power_of(intent_name) != "P3" or intent_name in harness._POWER_MAP
def check_tool(store, intent: IntentResult, actor: str = "planner") -> AgentReply | None:
"""统一工具运行时校验:LLM 提议意图执行前的强制登记检查(矩阵 111 行)。
- 未登记 intent → 写 TOOL 审计(tool.denied/P3)并返回拒绝回复(绝不执行);
- 已登记 intent → 写 TOOL 审计(tool.run)并返回 None(gateway 继续调用 handle_intent)。
返回 None 表示放行;返回 AgentReply 表示已拒绝,调用方应直接返回它。
"""
name = intent.intent
source = str(getattr(intent, "source", "") or "unknown")
if not is_registered(name):
write_audit(
store.data, store.next_id, actor=actor, category="TOOL",
action="tool.denied", target={"type": "INTENT", "id": name},
power="P3", rationale={"source": source, "reason": "unregistered-intent"},
result="DENIED",
)
store.save()
return AgentReply(
text=f"「{name}」不是已登记的操作(LLM 只有提议权,未登记意图不执行)。"
"可换一种说法,或先查看帮助。")
power = harness.power_of(name)
write_audit(
store.data, store.next_id, actor=actor, category="TOOL",
action="tool.run", target={"type": "INTENT", "id": name},
power=power, rationale={"source": source, "intent": name},
)
store.save()
return None
async def run_tool_async(store, session_id: str, intent: IntentResult, actor: str = "planner") -> AgentReply:
"""统一工具运行时:LLM 提议意图的异步执行入口(矩阵 111 行,供 gateway 直连端点复用)。
gateway 直连端点(如 /api/actions/scenario/apply)与 chat 共用本入口:
先 check_tool(未登记 → tool.denied 拒绝 + TOOL 审计;已登记 → tool.run 审计),
放行后委托 handle_intent(P0/P1 直通,P2/P3 经 harness 确认卡,绝不绕过门禁)。
"""
denied = check_tool(store, intent, actor=actor)
if denied is not None:
return denied
from server.aps_domain.workflow import handle_intent
return await handle_intent(store, session_id, intent, actor=actor)
def run_tool(store, session_id: str, intent: IntentResult, actor: str = "planner") -> AgentReply:
"""统一工具运行时:LLM 提议意图的执行入口(矩阵 111 行,同步便捷包装)。
委托 run_tool_async(校验放行后 handle_intent);测试/非 async 直连场景使用。
"""
return asyncio.run(run_tool_async(store, session_id, intent, actor=actor))