deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-18 16:07:34 +08:00
parent a34c1919c6
commit b21668b25d
4 changed files with 1588 additions and 21 deletions

View File

@ -6,27 +6,41 @@
项目集成契约apps/pbls/scripts/tests/offline_contract_smoke.py:267写法是
from pbl_agent_runtime import api as AR
AR.pbl_tool_registry_save(tool_key=..., enabled=...)
AR.pbl_tool_adjudicate(agent_code=..., tool_key=..., params_json=...)
AR.pbl_approval_create(approval_type=...)
await AR.pbl_tool_registry_save(tool_key=..., title=..., enabled=1, agent_scope=...)
await AR.pbl_tool_registry_list() # -> enabled_keys[13] / disabled_keys[9]
await AR.pbl_tool_adjudicate(agent_code=..., tool_key=..., params_json=...)
# -> allowed / error_code=AD0x / steps[8]
await AR.pbl_approval_create(approval_type=..., target_type=..., target_id=...)
await AR.pbl_approval_decide(id=..., status='approved', comment_txt=...)
await AR.pbl_agent_critic_run(blueprint_id=...) # -> writable=False + 四要素
await AR.pbl_agent_designer_run(input_txt=..., template_code='TPL-NOT-EXIST')
await AR.pbl_agent_trace_list()
非法审批类型 / G6 禁用工具启用 -> raise pbl_common.PblErrore.code 断言
``api`` 必须是**带契约函数属性的对象**而历史 init.py ``api()`` 是返回字典的工厂函数
两种写法互不兼容本模块给出同时满足两者的实现
``api`` 必须是**带契约函数属性的对象**且函数签名/返回字段/错误码要与集成契约逐字对齐
历史 init.py ``api()`` 是返回字典的工厂函数两种写法互不兼容本模块给出同时满足的实现
api.pbl_tool_adjudicate(...) # 属性直调(冒烟测试写法)
api().pbl_tool_adjudicate(...) # 先调用再属性(工厂写法)
api['pbl_tool_adjudicate'](...) # 下标写法
await api.pbl_tool_adjudicate(...)# async 上下文(.dspy 端点写法
await api.pbl_tool_adjudicate(...)# async 上下文(.dspy 端点写法Result.__await__ 返回自身
契约函数清单以 m4a_contract.CONTRACT_FUNCTIONS 为唯一真源10
wwwroot/api/*.dspyscripts/load_path.pyskill/SKILL.md 四处同步
实现分层避免"改一处坏一处"
------------------------------
* ``m4a_toolset`` 13 启用 / 9 禁用工具四类审批AD0x 错误码的**唯一真源**
* ``m4a_smoke_compat`` 集成契约口径实现对外唯一口径复用 m4a_contract 核心逻辑
* ``m4a_contract`` 内部核心轨迹 7 要素 / LLM 留痕 / append-only 守卫 / self_check
契约函数清单以 ``CONTRACT_FUNCTIONS``10 为唯一真源 wwwroot/api/*.dspy
scripts/load_path.pyskill/SKILL.md 四处同步scripts/check_contract_sync.py 机械核验
"""
from __future__ import annotations
from . import m4a_smoke_compat as COMPAT
from .m4a_contract import (
ADJUDICATION_STEPS,
CONTRACT_FUNCTIONS,
CONTRACT_FUNCTIONS as CORE_CONTRACT_FUNCTIONS,
MANDATORY_APPROVAL_TYPES,
TRACE_ELEMENTS,
build_api,
@ -34,6 +48,14 @@ from .m4a_contract import (
seed_registry,
)
from .m4a_store import APPEND_ONLY_TABLES, PblContractError, Result
from .m4a_toolset import (
DISABLED_TOOLS,
ENABLED_TOOLS,
STEP_ERROR_CODES,
)
#: 对外契约函数清单10 个)—— 与 .dspy / load_path / SKILL.md 同源
CONTRACT_FUNCTIONS = COMPAT.CONTRACT_FUNCTIONS
class ContractApi(dict):
@ -62,23 +84,29 @@ class ContractApi(dict):
return list(CONTRACT_FUNCTIONS)
def describe(self) -> dict:
return {
"module": "pbl_agent_runtime",
"contract_functions": list(CONTRACT_FUNCTIONS),
"contract_count": len(CONTRACT_FUNCTIONS),
"adjudication_steps": [step[0] for step in ADJUDICATION_STEPS],
"mandatory_approval_types": list(MANDATORY_APPROVAL_TYPES),
"trace_elements": list(TRACE_ELEMENTS),
"append_only_tables": list(APPEND_ONLY_TABLES),
"tables": 6,
}
return COMPAT.describe()
def build_contract_api() -> ContractApi:
"""构建契约对象(每次调用返回独立实例,便于测试隔离)。"""
"""构建契约对象(每次调用返回独立实例,便于测试隔离)。
装配顺序内部核心m4a_contract.build_api打底 集成契约实现覆盖同名函数
覆盖后对外口径唯一AD0x 错误码 + ok/success 双布尔 + 冒烟测试字段名
内部核心函数以 ``core_<name>`` 保留供自测与排障对照
"""
base = build_api()
obj = ContractApi(base)
merged = {}
for name, func in base.items():
merged["core_%s" % name] = func
merged[name] = func
merged.update(COMPAT.build_integration_api())
obj = ContractApi(merged)
obj["describe"] = obj.describe
obj["names"] = obj.names
obj["seed_tools"] = COMPAT.seed_tools
obj["seed_registry"] = seed_registry
obj["self_check"] = self_check
obj["adjudication_steps"] = COMPAT.adjudication_steps
return obj
@ -92,9 +120,13 @@ __all__ = [
"self_check",
"seed_registry",
"CONTRACT_FUNCTIONS",
"CORE_CONTRACT_FUNCTIONS",
"ADJUDICATION_STEPS",
"STEP_ERROR_CODES",
"MANDATORY_APPROVAL_TYPES",
"TRACE_ELEMENTS",
"ENABLED_TOOLS",
"DISABLED_TOOLS",
"APPEND_ONLY_TABLES",
"PblContractError",
"Result",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,329 @@
# -*- coding: utf-8 -*-
"""pbl_agent_runtime.m4a_toolset —— 工具注册表**唯一真源**13 启用 / 9 禁用)。
为什么单独一个文件
------------------
QC 退回 #2/#3 的根因之一是「工具键口径分裂」:设计文档用 ``blueprint.create`` 一类键,
需求 ``agent-tool-registry.md`` §2.1/§2.2 ``pbl.get``/``pbl.update``/``pbl.publish``
而项目集成契约 ``apps/pbls/scripts/tests/offline_contract_smoke.py``E1~E11按需求口径
断言且硬断言
len(enabled_keys) == 13 and len(disabled_keys) == 9 and 'pbl.publish' in disabled_keys
本文件把三方口径收敛为**一张表**键名取需求 §2.1/§2.2 tool_key调用方契约
语义/禁用原因锚点取设计文档 §3.1/§3.2 与任务书 ``pbl.publish`` 禁用 = G6/14.2
seed / registry_list / registry_save 白名单 / 裁决 S4~S6 全部只读本文件禁止再各自硬编码
裁决错误码AD0x与集成冒烟测试逐字对齐
------------------------------------------
AD01_TENANT_MISSING S1 租户上下文缺失
AD07_AGENT_UNREGISTERED S2 Agent 未注册
AD08_AGENT_DISABLED S2 Agent 已停用
AD04_PERMISSION_MODE_DENIED S3 Critic 零写read 模式 Agent 请求写类工具
AD02_TOOL_UNREGISTERED S4 工具未注册default-deny
AD03_TOOL_DISABLED S5 工具已禁用pbl.publish 恒拒G6 无绕过
AD09_PERMISSION_DENIED S6 Agent 不在工具 agent_scope
AD05_PARAM_NOT_WHITELISTED S7 入参不在服务端 params_schema 白名单
AD06_APPROVAL_REQUIRED S8 需人工审批且无 approved 审批单
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# 裁决错误码8 步 + 3 个前置/内部码)
# ---------------------------------------------------------------------------
AD01_TENANT_MISSING = "AD01_TENANT_MISSING"
AD02_TOOL_UNREGISTERED = "AD02_TOOL_UNREGISTERED"
AD03_TOOL_DISABLED = "AD03_TOOL_DISABLED"
AD04_PERMISSION_MODE_DENIED = "AD04_PERMISSION_MODE_DENIED"
AD05_PARAM_NOT_WHITELISTED = "AD05_PARAM_NOT_WHITELISTED"
AD06_APPROVAL_REQUIRED = "AD06_APPROVAL_REQUIRED"
AD07_AGENT_UNREGISTERED = "AD07_AGENT_UNREGISTERED"
AD08_AGENT_DISABLED = "AD08_AGENT_DISABLED"
AD09_PERMISSION_DENIED = "AD09_PERMISSION_DENIED"
AD10_INTERNAL = "AD10_INTERNAL"
#: 步骤号 → 该步骤失败时的错误码S1~S8 固定顺序fail-closed
STEP_ERROR_CODES = (
("S1", "tenant_context", AD01_TENANT_MISSING),
("S2", "agent_registered", AD07_AGENT_UNREGISTERED),
("S3", "agent_write_scope", AD04_PERMISSION_MODE_DENIED),
("S4", "tool_registered", AD02_TOOL_UNREGISTERED),
("S5", "tool_enabled", AD03_TOOL_DISABLED),
("S6", "permission_granted", AD09_PERMISSION_DENIED),
("S7", "params_valid", AD05_PARAM_NOT_WHITELISTED),
("S8", "approval_granted", AD06_APPROVAL_REQUIRED),
)
# ---------------------------------------------------------------------------
# 四类强制人工审批14.2 / agent-tool-registry.md §3.3 本迭代开放四类)
# ---------------------------------------------------------------------------
APPROVAL_TYPE_PUBLISH = "publish"
APPROVAL_TYPE_MODIFY_GOAL = "modify_learning_goal"
APPROVAL_TYPE_MODIFY_RUBRIC = "modify_assessment_criteria"
APPROVAL_TYPE_MODIFY_VISIBLE = "modify_student_visible_content"
#: 本迭代开放的四类(第 5 类 experiment 仅注册不开放§3.3 行 5
APPROVAL_TYPES = (
APPROVAL_TYPE_PUBLISH,
APPROVAL_TYPE_MODIFY_GOAL,
APPROVAL_TYPE_MODIFY_RUBRIC,
APPROVAL_TYPE_MODIFY_VISIBLE,
)
#: 注册但本迭代**不开放**(创建即拒 PBL_APPROVAL_TYPE_NOT_OPEN
APPROVAL_TYPES_REGISTERED_NOT_OPEN = ("experiment",)
#: 历史别名 → 四类正名(向后兼容早期 M4a 代码,不新增审批面)
APPROVAL_TYPE_ALIASES = {
"auto_publish": None, # G6不存在「自动发布」审批类型
"compile_execute": APPROVAL_TYPE_PUBLISH,
"blueprint_approve": APPROVAL_TYPE_MODIFY_VISIBLE,
"change_learning_goals": APPROVAL_TYPE_MODIFY_GOAL,
"change_assessment_criteria": APPROVAL_TYPE_MODIFY_RUBRIC,
}
# 审批/工具类错误码(抛 pbl_common.errors.PblError与冒烟测试 e.code 对齐)
PBL_APPROVAL_TYPE_UNKNOWN = "PBL_APPROVAL_TYPE_UNKNOWN"
PBL_APPROVAL_TYPE_NOT_OPEN = "PBL_APPROVAL_TYPE_NOT_OPEN"
PBL_APPROVAL_NO_BYPASS = "PBL_APPROVAL_NO_BYPASS"
PBL_APPROVAL_ALREADY_DECIDED = "PBL_APPROVAL_ALREADY_DECIDED"
PBL_TOOL_FORBIDDEN = "PBL_TOOL_FORBIDDEN"
PBL_TOOL_KEY_REQUIRED = "PBL_TOOL_KEY_REQUIRED"
# ---------------------------------------------------------------------------
# 13 启用工具agent-tool-registry.md §2.1pbl.publish 移入禁用集)
# (tool_key, title, category, write_class, agent_scope, required_perm,
# approval_type, params_schema)
# ---------------------------------------------------------------------------
_S = lambda *names: {"type": "object", "properties": {n: {} for n in names}} # noqa: E731
ENABLED_TOOL_DEFS = (
("pbl.get", "读取蓝图", "blueprint", 0, "both", "pbl_authoring.read", None,
_S("pbl_id", "blueprint_id", "version", "version_no")),
("pbl.create", "创建蓝图草稿", "blueprint", 1, "designer", "pbl_authoring.write", None,
_S("draft_json", "content_json", "generation_source", "title", "subject", "grade")),
("pbl.update", "结构化修改蓝图delta", "blueprint", 1, "designer",
"pbl_authoring.write", APPROVAL_TYPE_MODIFY_GOAL,
_S("pbl_id", "blueprint_id", "delta_json", "content_json", "instruction",
"target_changes", "version_no")),
("pbl.validate", "触发 14 维校验", "validation", 0, "both", "pbl_authoring.write", None,
_S("pbl_id", "blueprint_id", "version", "version_no")),
("pbl.compare", "版本字段级 diff", "validation", 0, "both", "pbl_authoring.read", None,
_S("pbl_id", "blueprint_id", "version_a", "version_b")),
("pbl.compile", "触发编译(教师审批后)", "compile", 1, "designer",
"pbl_authoring.write", APPROVAL_TYPE_PUBLISH,
_S("pbl_id", "blueprint_id", "version", "version_no", "compiler_version")),
("pbl.preview", "教师预览会话", "runtime", 0, "both", "pbl_authoring.read", None,
_S("definition_id", "session_type")),
("template.copy", "模板实例化(离线兜底落点)", "template", 1, "designer",
"pbl_authoring.write", None,
_S("template_id", "template_code", "owner_teacher_id", "class_id", "new_title")),
("kdb.search", "知识检索Q5 桩,空结果不报错)", "kdb", 0, "both", "kdb.read", None,
_S("query", "top_k", "filters")),
("kdb.get", "知识条目读取Q5 桩)", "kdb", 0, "both", "kdb.read", None,
_S("kdb_item_id", "item_id")),
("runtime.get_state", "世界/实体状态快照(只读聚合)", "runtime", 0, "both",
"student_data.read", None, _S("session_id")),
("runtime.playtest", "playtest 会话", "runtime", 1, "designer", "pbl_authoring.write",
None, _S("definition_id", "mode")),
("analytics.query", "聚合指标查询禁返回可识别学生字段Q6", "analytics", 0, "both",
"analytics.read", None, _S("pbl_id", "blueprint_id", "metrics", "window")),
)
# ---------------------------------------------------------------------------
# 9 禁用工具(设计文档 §3.2 out_of_scope键名取需求口径含 pbl.publish
# (tool_key, title, category, disable_reason)
# ---------------------------------------------------------------------------
DISABLED_TOOL_DEFS = (
("pbl.publish", "发布 PBL", "publish",
"G6_no_publish_toolPublish 阶段仅人类(教师/ReviewerAgent 无 pbl.publish 权限"
"14.2/28 章/36 章);发布经 publish 类人工审批单由人类决策"),
("kdb.add_candidate", "写入候选知识", "kdb",
"Q5本迭代不建 KDB知识层只读禁任何写入"),
("kdb.propose_pattern", "提交模式建议", "kdb",
"Q5KDB 只读research 层不得采集学生数据Q6"),
("analytics.compare", "跨组对比分析", "analytics",
"Phase 3Experiment/Research33/35 章)本迭代不做"),
("analytics.aggregate", "自定义聚合", "analytics",
"Phase 3Experiment/Research本迭代不做仅开放 analytics.query 固定聚合"),
("experiment.create", "创建实验", "experiments",
"33 章 Experiment Engine 归 Phase 3影响活跃课程的实验不开放§3.3 行 5"),
("experiment.start", "启动 A/B 实验", "experiments",
"33 章 A/B 引擎归 Phase 3本迭代禁止自动实验"),
("showplace.search", "Marketplace 检索", "marketplace",
"22/23 章 Marketplace 归 Phase 4本迭代发布仅可见范围标记"),
("showplace.copy", "Marketplace 复制", "marketplace",
"22/23 章 Marketplace 归 Phase 4本迭代由 template.copy 替代"),
)
#: 需求 §2.2 其余延后键:**允许登记**(保留契约,后续迭代 is_enabled=1 即可),
#: 但**不预置**(不计入 13/9 断言口径)。
DEFERRED_TOOL_KEYS = (
"experiment.evaluate",
"showplace.get",
"runtime.preview",
"agent.mentor_invoke",
"world.edit_3d",
"billing.charge",
"curriculum.modify_auto",
"research.collect",
)
#: G6 硬禁用键:即使 Platform Admin 也不得 enabled=1registry_save 抛 PBL_TOOL_FORBIDDEN
G6_FORBIDDEN_TO_ENABLE = frozenset(
key for key, _t, _c, _r in DISABLED_TOOL_DEFS
) | frozenset(DEFERRED_TOOL_KEYS)
#: registry_save 可登记的键全集(白名单外一律 PBL_TOOL_FORBIDDEN如 sim.configure
SAVABLE_TOOL_KEYS = frozenset(
[d[0] for d in ENABLED_TOOL_DEFS]
+ [d[0] for d in DISABLED_TOOL_DEFS]
+ list(DEFERRED_TOOL_KEYS)
)
#: 键前缀白名单(第 31 章裁剪子集的命名空间sim.* 等未裁剪命名空间一律拒)
ALLOWED_TOOL_PREFIXES = ("pbl.", "kdb.", "analytics.", "experiment.", "showplace.",
"runtime.", "template.", "agent.", "world.", "billing.",
"curriculum.", "research.")
ENABLED_TOOLS = tuple(d[0] for d in ENABLED_TOOL_DEFS)
DISABLED_TOOLS = tuple(d[0] for d in DISABLED_TOOL_DEFS)
#: Agent 定义13.1 章:本迭代仅 designer + critic
AGENT_DEFS = (
# (code, name, agent_type, permission_mode, write_scope, model_route)
("designer", "Designer Agent", "designer", "write", "blueprint_draft", "pipeline-llm"),
("critic", "Critic Agent", "critic", "read", "none", "pipeline-llm"),
)
#: 轨迹 7 要素28 章 / US-20
TRACE_ELEMENTS = (
"input_context",
"retrieved_knowledge",
"tool_calls",
"proposed_action",
"result",
"approval",
"final_output",
)
def tool_def(tool_key):
"""按 tool_key 取启用工具定义;不存在返回 None。"""
for row in ENABLED_TOOL_DEFS:
if row[0] == tool_key:
return {
"tool_key": row[0], "tool_name": row[1], "category": row[2],
"write_class": row[3], "agent_scope": row[4],
"required_perm": row[5], "approval_type": row[6],
"params_schema": row[7], "status": "enabled", "reason": None,
}
return None
def disabled_def(tool_key):
"""按 tool_key 取禁用工具定义;不存在返回 None。"""
for row in DISABLED_TOOL_DEFS:
if row[0] == tool_key:
return {
"tool_key": row[0], "tool_name": row[1], "category": row[2],
"write_class": 1, "agent_scope": "none", "required_perm": None,
"approval_type": None, "params_schema": {"type": "object"},
"status": "disabled", "reason": row[3],
}
return None
def is_savable(tool_key):
"""registry_save 白名单判定:键全集内,或命名空间前缀在裁剪子集内。"""
if not tool_key:
return False
if tool_key in SAVABLE_TOOL_KEYS:
return True
return tool_key.startswith(ALLOWED_TOOL_PREFIXES)
def seed_rows():
"""13 启用 + 9 禁用的幂等 seed 行sort_no 稳定,便于 registry_list 排序)。"""
rows = []
for idx, row in enumerate(ENABLED_TOOL_DEFS):
rows.append({
"tool_key": row[0], "tool_name": row[1], "category": row[2],
"write_class": row[3], "agent_scope": row[4], "required_perm": row[5],
"approval_type": row[6], "params_schema": row[7],
"status": "enabled", "reason": None, "sort_no": 10 + idx,
})
for idx, row in enumerate(DISABLED_TOOL_DEFS):
rows.append({
"tool_key": row[0], "tool_name": row[1], "category": row[2],
"write_class": 1, "agent_scope": "none", "required_perm": None,
"approval_type": None, "params_schema": {"type": "object"},
"status": "disabled", "reason": row[3], "sort_no": 200 + idx,
})
return rows
def resolve_approval_type(tool_key, params=None):
"""按工具 + delta 命中面动态解析审批类型agent-tool-registry.md §3.3 行 2~4
* pbl.update delta 命中 rubric/assessment modify_assessment_criteria
* 命中 mission/task/entity学生可见内容 modify_student_visible_content
* 其余含默认学习目标 modify_learning_goal
"""
definition = tool_def(tool_key)
default_type = definition["approval_type"] if definition else None
if default_type is None or not isinstance(params, dict):
return default_type
blob = repr(sorted(params.keys())).lower() + repr(params).lower()
if default_type != APPROVAL_TYPE_MODIFY_GOAL:
return default_type
if "rubric" in blob or "assessment" in blob or "criteria" in blob:
return APPROVAL_TYPE_MODIFY_RUBRIC
if "mission" in blob or "task" in blob or "entit" in blob or "artifact" in blob:
return APPROVAL_TYPE_MODIFY_VISIBLE
return APPROVAL_TYPE_MODIFY_GOAL
def normalize_approval_type(raw):
"""审批类型归一化。
返回 ``(type_or_None, error_code_or_None)``
* (None, PBL_APPROVAL_TYPE_UNKNOWN)
* 别名 auto_publish (None, PBL_APPROVAL_TYPE_UNKNOWN) # G6 无自动发布
* 注册未开放 experiment (None, PBL_APPROVAL_TYPE_NOT_OPEN)
* 四类正名/可归一别名 (正名, None)
"""
key = (raw or "").strip()
if not key:
return None, PBL_APPROVAL_TYPE_UNKNOWN
if key in APPROVAL_TYPES:
return key, None
if key in APPROVAL_TYPE_ALIASES:
mapped = APPROVAL_TYPE_ALIASES[key]
if mapped is None:
return None, PBL_APPROVAL_TYPE_UNKNOWN
return mapped, None
if key in APPROVAL_TYPES_REGISTERED_NOT_OPEN:
return None, PBL_APPROVAL_TYPE_NOT_OPEN
return None, PBL_APPROVAL_TYPE_UNKNOWN
__all__ = [
"AD01_TENANT_MISSING", "AD02_TOOL_UNREGISTERED", "AD03_TOOL_DISABLED",
"AD04_PERMISSION_MODE_DENIED", "AD05_PARAM_NOT_WHITELISTED",
"AD06_APPROVAL_REQUIRED", "AD07_AGENT_UNREGISTERED", "AD08_AGENT_DISABLED",
"AD09_PERMISSION_DENIED", "AD10_INTERNAL", "STEP_ERROR_CODES",
"APPROVAL_TYPES", "APPROVAL_TYPES_REGISTERED_NOT_OPEN", "APPROVAL_TYPE_ALIASES",
"APPROVAL_TYPE_PUBLISH", "APPROVAL_TYPE_MODIFY_GOAL",
"APPROVAL_TYPE_MODIFY_RUBRIC", "APPROVAL_TYPE_MODIFY_VISIBLE",
"PBL_APPROVAL_TYPE_UNKNOWN", "PBL_APPROVAL_TYPE_NOT_OPEN",
"PBL_APPROVAL_NO_BYPASS", "PBL_APPROVAL_ALREADY_DECIDED",
"PBL_TOOL_FORBIDDEN", "PBL_TOOL_KEY_REQUIRED",
"ENABLED_TOOL_DEFS", "DISABLED_TOOL_DEFS", "DEFERRED_TOOL_KEYS",
"ENABLED_TOOLS", "DISABLED_TOOLS", "SAVABLE_TOOL_KEYS",
"G6_FORBIDDEN_TO_ENABLE", "ALLOWED_TOOL_PREFIXES",
"AGENT_DEFS", "TRACE_ELEMENTS",
"tool_def", "disabled_def", "is_savable", "seed_rows",
"resolve_approval_type", "normalize_approval_type",
]

BIN
var/m4a_contract.sqlite3 Normal file

Binary file not shown.