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

This commit is contained in:
agent.develop 2026-09-18 11:29:07 +08:00
parent c2f683690e
commit b8a727b375
22 changed files with 536 additions and 3139 deletions

View File

@ -1,160 +0,0 @@
{
"tblname": "pbl_agent_def",
"alias": "pbl_agent_runtime",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"code": {
"label": "Agent编码",
"type": "select",
"list": true
},
"agent_type": {
"label": "类型",
"type": "select",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"model_route": {
"label": "pipeline-llm 路由",
"type": "select",
"list": true
},
"system_prompt_txt": {
"label": "提示词",
"type": "text",
"list": true
},
"tool_whitelist_json": {
"label": "工具白名单",
"type": "text",
"list": true
},
"permission_mode": {
"label": "read/write 模式",
"type": "select",
"list": true
},
"is_enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"version_no": {
"label": "版本",
"type": "number",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"code": {
"label": "Agent编码",
"type": "select",
"list": true
},
"agent_type": {
"label": "类型",
"type": "select",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"model_route": {
"label": "pipeline-llm 路由",
"type": "select",
"list": true
},
"system_prompt_txt": {
"label": "提示词",
"type": "text",
"list": true
},
"tool_whitelist_json": {
"label": "工具白名单",
"type": "text",
"list": true
},
"permission_mode": {
"label": "read/write 模式",
"type": "select",
"list": true
},
"is_enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"version_no": {
"label": "版本",
"type": "number",
"list": true
}
},
"editable": {
"code": {
"label": "Agent编码",
"type": "select"
},
"agent_type": {
"label": "类型",
"type": "select"
},
"name": {
"label": "名称",
"type": "select"
},
"model_route": {
"label": "pipeline-llm 路由",
"type": "select"
},
"system_prompt_txt": {
"label": "提示词",
"type": "text"
},
"tool_whitelist_json": {
"label": "工具白名单",
"type": "text"
},
"permission_mode": {
"label": "read/write 模式",
"type": "select"
},
"is_enabled": {
"label": "启用",
"type": "checkbox"
},
"version_no": {
"label": "版本",
"type": "number"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -1,146 +0,0 @@
{
"tblname": "pbl_agent_tool_call",
"alias": "pbl_agent_runtime",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"trace_id": {
"label": "轨迹ID",
"type": "number",
"list": true
},
"tool_key": {
"label": "工具键",
"type": "select",
"list": true
},
"request_json": {
"label": "请求",
"type": "text",
"list": true
},
"response_json": {
"label": "响应",
"type": "text",
"list": true
},
"ok": {
"label": "成功",
"type": "checkbox",
"list": true
},
"error_code": {
"label": "错误码",
"type": "select",
"list": true
},
"adjudication_code": {
"label": "裁决结论",
"type": "select",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"trace_id": {
"label": "轨迹ID",
"type": "number",
"list": true
},
"tool_key": {
"label": "工具键",
"type": "select",
"list": true
},
"request_json": {
"label": "请求",
"type": "text",
"list": true
},
"response_json": {
"label": "响应",
"type": "text",
"list": true
},
"ok": {
"label": "成功",
"type": "checkbox",
"list": true
},
"error_code": {
"label": "错误码",
"type": "select",
"list": true
},
"adjudication_code": {
"label": "裁决结论",
"type": "select",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
}
},
"editable": {
"trace_id": {
"label": "轨迹ID",
"type": "number"
},
"tool_key": {
"label": "工具键",
"type": "select"
},
"request_json": {
"label": "请求",
"type": "text"
},
"response_json": {
"label": "响应",
"type": "text"
},
"ok": {
"label": "成功",
"type": "checkbox"
},
"error_code": {
"label": "错误码",
"type": "select"
},
"adjudication_code": {
"label": "裁决结论",
"type": "select"
},
"duration_ms": {
"label": "耗时",
"type": "number"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -1,202 +0,0 @@
{
"tblname": "pbl_agent_trace",
"alias": "pbl_agent_runtime",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"trace_uid": {
"label": "轨迹UID",
"type": "text",
"list": true
},
"agent_code": {
"label": "Agent",
"type": "select",
"list": true
},
"blueprint_id": {
"label": "目标蓝图",
"type": "number",
"list": true
},
"input_ref_json": {
"label": "要素1 输入引用",
"type": "text",
"list": true
},
"thought_txt": {
"label": "要素2 思考",
"type": "text",
"list": true
},
"action_code": {
"label": "要素3 动作",
"type": "select",
"list": true
},
"action_params_json": {
"label": "要素4 动作参数",
"type": "text",
"list": true
},
"observation_json": {
"label": "要素5 观察",
"type": "text",
"list": true
},
"result_state": {
"label": "要素6 结果状态",
"type": "select",
"list": true
},
"llm_calls": {
"label": "要素7 LLM调用数",
"type": "number",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
},
"created_by": {
"label": "触发用户",
"type": "text",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"trace_uid": {
"label": "轨迹UID",
"type": "text",
"list": true
},
"agent_code": {
"label": "Agent",
"type": "select",
"list": true
},
"blueprint_id": {
"label": "目标蓝图",
"type": "number",
"list": true
},
"input_ref_json": {
"label": "要素1 输入引用",
"type": "text",
"list": true
},
"thought_txt": {
"label": "要素2 思考",
"type": "text",
"list": true
},
"action_code": {
"label": "要素3 动作",
"type": "select",
"list": true
},
"action_params_json": {
"label": "要素4 动作参数",
"type": "text",
"list": true
},
"observation_json": {
"label": "要素5 观察",
"type": "text",
"list": true
},
"result_state": {
"label": "要素6 结果状态",
"type": "select",
"list": true
},
"llm_calls": {
"label": "要素7 LLM调用数",
"type": "number",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
},
"created_by": {
"label": "触发用户",
"type": "text",
"list": true
}
},
"editable": {
"trace_uid": {
"label": "轨迹UID",
"type": "text"
},
"agent_code": {
"label": "Agent",
"type": "select"
},
"blueprint_id": {
"label": "目标蓝图",
"type": "number"
},
"input_ref_json": {
"label": "要素1 输入引用",
"type": "text"
},
"thought_txt": {
"label": "要素2 思考",
"type": "text"
},
"action_code": {
"label": "要素3 动作",
"type": "select"
},
"action_params_json": {
"label": "要素4 动作参数",
"type": "text"
},
"observation_json": {
"label": "要素5 观察",
"type": "text"
},
"result_state": {
"label": "要素6 结果状态",
"type": "select"
},
"llm_calls": {
"label": "要素7 LLM调用数",
"type": "number"
},
"duration_ms": {
"label": "耗时",
"type": "number"
},
"created_by": {
"label": "触发用户",
"type": "text"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -1,174 +0,0 @@
{
"tblname": "pbl_approval",
"alias": "pbl_agent_runtime",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"approval_uid": {
"label": "审批UID",
"type": "text",
"list": true
},
"approval_type": {
"label": "类型",
"type": "select",
"list": true
},
"target_type": {
"label": "对象类型",
"type": "select",
"list": true
},
"target_id": {
"label": "对象ID",
"type": "number",
"list": true
},
"payload_json": {
"label": "变更内容",
"type": "text",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"requested_by": {
"label": "发起人",
"type": "text",
"list": true
},
"decided_by": {
"label": "决策人",
"type": "text",
"list": true
},
"decided_at": {
"label": "决策时间",
"type": "text",
"list": true
},
"comment_txt": {
"label": "意见",
"type": "text",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"approval_uid": {
"label": "审批UID",
"type": "text",
"list": true
},
"approval_type": {
"label": "类型",
"type": "select",
"list": true
},
"target_type": {
"label": "对象类型",
"type": "select",
"list": true
},
"target_id": {
"label": "对象ID",
"type": "number",
"list": true
},
"payload_json": {
"label": "变更内容",
"type": "text",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"requested_by": {
"label": "发起人",
"type": "text",
"list": true
},
"decided_by": {
"label": "决策人",
"type": "text",
"list": true
},
"decided_at": {
"label": "决策时间",
"type": "text",
"list": true
},
"comment_txt": {
"label": "意见",
"type": "text",
"list": true
}
},
"editable": {
"approval_uid": {
"label": "审批UID",
"type": "text"
},
"approval_type": {
"label": "类型",
"type": "select"
},
"target_type": {
"label": "对象类型",
"type": "select"
},
"target_id": {
"label": "对象ID",
"type": "number"
},
"payload_json": {
"label": "变更内容",
"type": "text"
},
"status": {
"label": "状态",
"type": "select"
},
"requested_by": {
"label": "发起人",
"type": "text"
},
"decided_by": {
"label": "决策人",
"type": "text"
},
"decided_at": {
"label": "决策时间",
"type": "text"
},
"comment_txt": {
"label": "意见",
"type": "text"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -1,174 +0,0 @@
{
"tblname": "pbl_llm_call_log",
"alias": "pbl_agent_runtime",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"call_uid": {
"label": "调用UID",
"type": "text",
"list": true
},
"agent_code": {
"label": "Agent",
"type": "select",
"list": true
},
"route": {
"label": "路由",
"type": "select",
"list": true
},
"model": {
"label": "实际模型",
"type": "select",
"list": true
},
"prompt_hash": {
"label": "提示词哈希",
"type": "text",
"list": true
},
"completion_tokens": {
"label": "tokens",
"type": "number",
"list": true
},
"latency_ms": {
"label": "时延",
"type": "number",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"error_code": {
"label": "错误码",
"type": "select",
"list": true
},
"fallback_used": {
"label": "走兜底",
"type": "checkbox",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"call_uid": {
"label": "调用UID",
"type": "text",
"list": true
},
"agent_code": {
"label": "Agent",
"type": "select",
"list": true
},
"route": {
"label": "路由",
"type": "select",
"list": true
},
"model": {
"label": "实际模型",
"type": "select",
"list": true
},
"prompt_hash": {
"label": "提示词哈希",
"type": "text",
"list": true
},
"completion_tokens": {
"label": "tokens",
"type": "number",
"list": true
},
"latency_ms": {
"label": "时延",
"type": "number",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"error_code": {
"label": "错误码",
"type": "select",
"list": true
},
"fallback_used": {
"label": "走兜底",
"type": "checkbox",
"list": true
}
},
"editable": {
"call_uid": {
"label": "调用UID",
"type": "text"
},
"agent_code": {
"label": "Agent",
"type": "select"
},
"route": {
"label": "路由",
"type": "select"
},
"model": {
"label": "实际模型",
"type": "select"
},
"prompt_hash": {
"label": "提示词哈希",
"type": "text"
},
"completion_tokens": {
"label": "tokens",
"type": "number"
},
"latency_ms": {
"label": "时延",
"type": "number"
},
"status": {
"label": "状态",
"type": "select"
},
"error_code": {
"label": "错误码",
"type": "select"
},
"fallback_used": {
"label": "走兜底",
"type": "checkbox"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -1,146 +0,0 @@
{
"tblname": "pbl_tool_registry",
"alias": "pbl_agent_runtime",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"tool_key": {
"label": "工具键",
"type": "select",
"list": true
},
"title": {
"label": "标题",
"type": "select",
"list": true
},
"agent_scope": {
"label": "作用域",
"type": "select",
"list": true
},
"permission_required": {
"label": "所需权限",
"type": "select",
"list": true
},
"enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"params_schema_json": {
"label": "参数白名单Schema",
"type": "text",
"list": true
},
"disabled_reason": {
"label": "禁用原因",
"type": "select",
"list": true
},
"version_no": {
"label": "版本",
"type": "number",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"tool_key": {
"label": "工具键",
"type": "select",
"list": true
},
"title": {
"label": "标题",
"type": "select",
"list": true
},
"agent_scope": {
"label": "作用域",
"type": "select",
"list": true
},
"permission_required": {
"label": "所需权限",
"type": "select",
"list": true
},
"enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"params_schema_json": {
"label": "参数白名单Schema",
"type": "text",
"list": true
},
"disabled_reason": {
"label": "禁用原因",
"type": "select",
"list": true
},
"version_no": {
"label": "版本",
"type": "number",
"list": true
}
},
"editable": {
"tool_key": {
"label": "工具键",
"type": "select"
},
"title": {
"label": "标题",
"type": "select"
},
"agent_scope": {
"label": "作用域",
"type": "select"
},
"permission_required": {
"label": "所需权限",
"type": "select"
},
"enabled": {
"label": "启用",
"type": "checkbox"
},
"params_schema_json": {
"label": "参数白名单Schema",
"type": "text"
},
"disabled_reason": {
"label": "禁用原因",
"type": "select"
},
"version_no": {
"label": "版本",
"type": "number"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -1,119 +0,0 @@
{
"summary": [
{
"name": "pbl_agent_def",
"comment": "Agent 定义Designer/Critic",
"module": "pbl_agent_runtime",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "code",
"comment": "Agent编码",
"null": false,
"type": "str",
"length": 64
},
{
"name": "agent_type",
"comment": "类型",
"null": false,
"type": "str",
"length": 64
},
{
"name": "name",
"comment": "名称",
"null": false,
"type": "str",
"length": 128
},
{
"name": "model_route",
"comment": "pipeline-llm 路由",
"null": false,
"type": "str",
"length": 64
},
{
"name": "system_prompt_txt",
"comment": "提示词",
"null": true,
"type": "text"
},
{
"name": "tool_whitelist_json",
"comment": "工具白名单",
"null": true,
"type": "json"
},
{
"name": "permission_mode",
"comment": "read/write 模式",
"null": false,
"type": "str",
"length": 64
},
{
"name": "is_enabled",
"comment": "启用",
"null": false,
"type": "bool"
},
{
"name": "version_no",
"comment": "版本",
"null": false,
"type": "int"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_ad_code",
"unique": true,
"fields": [
"tenant_id",
"code"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -1,111 +0,0 @@
{
"summary": [
{
"name": "pbl_agent_tool_call",
"comment": "工具调用明细(服务端裁决留痕)",
"module": "pbl_agent_runtime",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "trace_id",
"comment": "轨迹ID",
"null": false,
"type": "bigint"
},
{
"name": "tool_key",
"comment": "工具键",
"null": false,
"type": "str",
"length": 64
},
{
"name": "request_json",
"comment": "请求",
"null": true,
"type": "json"
},
{
"name": "response_json",
"comment": "响应",
"null": true,
"type": "json"
},
{
"name": "ok",
"comment": "成功",
"null": false,
"type": "bool"
},
{
"name": "error_code",
"comment": "错误码",
"null": false,
"type": "str",
"length": 64
},
{
"name": "adjudication_code",
"comment": "裁决结论",
"null": false,
"type": "str",
"length": 64
},
{
"name": "duration_ms",
"comment": "耗时",
"null": false,
"type": "int"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_tc_trace",
"unique": false,
"fields": [
"tenant_id",
"trace_id"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -1,145 +0,0 @@
{
"summary": [
{
"name": "pbl_agent_trace",
"comment": "Agent 执行轨迹第28章 7 要素)",
"module": "pbl_agent_runtime",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "trace_uid",
"comment": "轨迹UID",
"null": false,
"type": "str",
"length": 32
},
{
"name": "agent_code",
"comment": "Agent",
"null": false,
"type": "str",
"length": 64
},
{
"name": "blueprint_id",
"comment": "目标蓝图",
"null": false,
"type": "bigint"
},
{
"name": "input_ref_json",
"comment": "要素1 输入引用",
"null": true,
"type": "json"
},
{
"name": "thought_txt",
"comment": "要素2 思考",
"null": true,
"type": "text"
},
{
"name": "action_code",
"comment": "要素3 动作",
"null": false,
"type": "str",
"length": 64
},
{
"name": "action_params_json",
"comment": "要素4 动作参数",
"null": true,
"type": "json"
},
{
"name": "observation_json",
"comment": "要素5 观察",
"null": true,
"type": "json"
},
{
"name": "result_state",
"comment": "要素6 结果状态",
"null": false,
"type": "str",
"length": 64
},
{
"name": "llm_calls",
"comment": "要素7 LLM调用数",
"null": false,
"type": "int"
},
{
"name": "duration_ms",
"comment": "耗时",
"null": false,
"type": "int"
},
{
"name": "created_by",
"comment": "触发用户",
"null": false,
"type": "str",
"length": 32
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_at_uid",
"unique": true,
"fields": [
"tenant_id",
"trace_uid"
]
},
{
"name": "idx_at_tenant",
"unique": false,
"fields": [
"tenant_id",
"created_at"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -1,139 +0,0 @@
{
"summary": [
{
"name": "pbl_approval",
"comment": "人工审批单14.2 四类,无绕过路径)",
"module": "pbl_agent_runtime",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "approval_uid",
"comment": "审批UID",
"null": false,
"type": "str",
"length": 32
},
{
"name": "approval_type",
"comment": "类型",
"null": false,
"type": "str",
"length": 64
},
{
"name": "target_type",
"comment": "对象类型",
"null": false,
"type": "str",
"length": 64
},
{
"name": "target_id",
"comment": "对象ID",
"null": false,
"type": "bigint"
},
{
"name": "payload_json",
"comment": "变更内容",
"null": true,
"type": "json"
},
{
"name": "status",
"comment": "状态",
"null": false,
"type": "str",
"length": 64
},
{
"name": "requested_by",
"comment": "发起人",
"null": false,
"type": "str",
"length": 32
},
{
"name": "decided_by",
"comment": "决策人",
"null": false,
"type": "str",
"length": 32
},
{
"name": "decided_at",
"comment": "决策时间",
"null": true,
"type": "datetime"
},
{
"name": "comment_txt",
"comment": "意见",
"null": true,
"type": "text"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_ap_uid",
"unique": true,
"fields": [
"tenant_id",
"approval_uid"
]
},
{
"name": "idx_ap_status",
"unique": false,
"fields": [
"tenant_id",
"status"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": [
{
"field": "status",
"appcode": "pbl_approval_status"
}
]
}

View File

@ -1,127 +0,0 @@
{
"summary": [
{
"name": "pbl_llm_call_log",
"comment": "LLM 调用日志(超时/重试/限流/兜底留痕)",
"module": "pbl_agent_runtime",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "call_uid",
"comment": "调用UID",
"null": false,
"type": "str",
"length": 32
},
{
"name": "agent_code",
"comment": "Agent",
"null": false,
"type": "str",
"length": 64
},
{
"name": "route",
"comment": "路由",
"null": false,
"type": "str",
"length": 64
},
{
"name": "model",
"comment": "实际模型",
"null": false,
"type": "str",
"length": 64
},
{
"name": "prompt_hash",
"comment": "提示词哈希",
"null": false,
"type": "str",
"length": 32
},
{
"name": "completion_tokens",
"comment": "tokens",
"null": false,
"type": "int"
},
{
"name": "latency_ms",
"comment": "时延",
"null": false,
"type": "int"
},
{
"name": "status",
"comment": "状态",
"null": false,
"type": "str",
"length": 64
},
{
"name": "error_code",
"comment": "错误码",
"null": false,
"type": "str",
"length": 64
},
{
"name": "fallback_used",
"comment": "走兜底",
"null": false,
"type": "bool"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_lc_uid",
"unique": true,
"fields": [
"tenant_id",
"call_uid"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -1,113 +0,0 @@
{
"summary": [
{
"name": "pbl_tool_registry",
"comment": "工具注册表13 启用 / 9 禁用含 pbl.publish",
"module": "pbl_agent_runtime",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "tool_key",
"comment": "工具键",
"null": false,
"type": "str",
"length": 64
},
{
"name": "title",
"comment": "标题",
"null": false,
"type": "str",
"length": 128
},
{
"name": "agent_scope",
"comment": "作用域",
"null": false,
"type": "str",
"length": 64
},
{
"name": "permission_required",
"comment": "所需权限",
"null": false,
"type": "str",
"length": 64
},
{
"name": "enabled",
"comment": "启用",
"null": false,
"type": "bool"
},
{
"name": "params_schema_json",
"comment": "参数白名单Schema",
"null": true,
"type": "json"
},
{
"name": "disabled_reason",
"comment": "禁用原因",
"null": false,
"type": "str",
"length": 64
},
{
"name": "version_no",
"comment": "版本",
"null": false,
"type": "int"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_tr_key",
"unique": true,
"fields": [
"tenant_id",
"tool_key"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -1,9 +1,102 @@
# -*- coding: utf-8 -*-
"""pbl_agent_runtime —— Designer/Critic Agent 运行时与 fail-closed 工具裁决M4a/M4b)。
"""pbl_agent_runtime —— Designer/Critic Agent 运行时与 fail-closed 工具裁决M4a)。
包出口
from pbl_agent_runtime.init import load_pbl_agent_runtime
M4a 交付范围
* Agent 定义designer可写/ critic**零写权限**
* 工具注册表13 启用 + 9 禁用 blueprint.publish_auto 等自动发布类禁用
* fail-closed 裁决S1~S8 八步顺序任一步不过即 DENY默认裁决 = DENY
* 四类强制人工审批publish / compile_execute / blueprint_approve / tool_registry_change
* 5 张表pbl_agent_def / pbl_agent_tool / pbl_agent_trace /
pbl_agent_trace_stage / pbl_approval
包出口三处同步定义 + 本文件导出 + init.py 注册
from pbl_agent_runtime import load_pbl_agent_runtime # 宿主应用标准入口
from pbl_agent_runtime import load_pbl_agent_runtime_m4a # M4a 装配(建表+seed
from pbl_agent_runtime import load_m4a # 租户级门面 M4aApi
"""
__all__ = ["init", "tables", "tool_registry", "verdict"]
from .m4a_init import ( # noqa: F401
load_pbl_agent_runtime_m4a,
bootstrap,
get_dbname,
ddl,
API_ROUTES,
MODULE_NAME,
)
from .m4a import M4aApi, load_m4a, make_agent_ctx, make_user_ctx, make_admin_ctx # noqa: F401
from .init import load_pbl_agent_runtime, self_check # noqa: F401
from .m4a_registry import ( # noqa: F401
seed_agents,
seed_tools,
list_agents,
list_tools,
get_tool,
register_tool,
set_tool_status,
ENABLED_TOOL_CODES,
DISABLED_TOOL_CODES,
APPROVAL_REQUIRED_TOOLS,
PERM_PLATFORM_ADMIN,
)
from .m4a_tables import ( # noqa: F401
ensure_tables,
TABLES,
APPEND_ONLY_TABLES,
all_sql,
)
from .m4a_kernel import ADJUDICATION_STEPS, TRACE_ELEMENTS, PblError # noqa: F401
from .m4a_approval import MANDATORY_APPROVAL_TYPES # noqa: F401
__all__ = [
# 挂载入口
"load_pbl_agent_runtime",
"load_pbl_agent_runtime_m4a",
"load_m4a",
"bootstrap",
"self_check",
"get_dbname",
"ddl",
# 门面 / 上下文
"M4aApi",
"make_agent_ctx",
"make_user_ctx",
"make_admin_ctx",
# 注册表
"seed_agents",
"seed_tools",
"list_agents",
"list_tools",
"get_tool",
"register_tool",
"set_tool_status",
"ENABLED_TOOL_CODES",
"DISABLED_TOOL_CODES",
"APPROVAL_REQUIRED_TOOLS",
"PERM_PLATFORM_ADMIN",
# 表 / 裁决常量
"ensure_tables",
"TABLES",
"APPEND_ONLY_TABLES",
"all_sql",
"ADJUDICATION_STEPS",
"TRACE_ELEMENTS",
"MANDATORY_APPROVAL_TYPES",
"PblError",
"API_ROUTES",
"MODULE_NAME",
# 子模块(供高级用法直接 import
"init",
"m4a",
"m4a_init",
"m4a_kernel",
"m4a_tables",
"m4a_registry",
"m4a_adjudicate",
"m4a_approval",
"m4a_trace",
"m4a_designer",
"m4a_critic",
"m4a_backend",
]
__version__ = "1.0.0"

View File

@ -1,366 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_agent_runtime.api —— Designer/Critic 运行时 + fail-closed 工具裁决第13/14/28/31章M4a/M4b
不可绕过的三条硬线QC/PM 均要求可机械核对
1) **Critic 零写权限**agent_type=critic permission_mode 恒为 read写类工具直接 DENIED
2) ** pbl.publish**DISABLED_TOOLS 注册契约但 enabled=0裁决第 3 步即拒 pbl.publish
3) **四类强制人工审批**审批未 approved 相关写工具在裁决第 6 DENIED无旁路参数
fail-closed 8 步裁决顺序任一步不过 = 拒绝并留痕绝不默认放行
Agent 存在且启用 工具在注册表且 enabled 工具未被禁用(G6/pbl.publish)
Agent 权限模式允许该副作用级别 参数过 JSON Schema 白名单 需审批类已 approved
租户归属校验 执行并写 trace/tool_call 留痕
"""
import hashlib
import json
import time
from pbl_common.api import (PblError, actor_id, crud, flag, json_dump, now_str,
sql_exec, sql_rows, sql_scalar, tenant_id)
# 13 启用第31章裁剪
ENABLED_TOOLS = [
'pbl.get', 'pbl.create', 'pbl.update', 'pbl.validate', 'pbl.compare', 'pbl.compile',
'pbl.preview', 'kdb.search', 'kdb.get', 'runtime.get_state', 'runtime.playtest',
'analytics.query', 'template.instantiate',
]
# 9 禁用注册契约、零实现入口G6 范围纪律)
DISABLED_TOOLS = [
'pbl.publish', 'kdb.add_candidate', 'kdb.propose_pattern', 'analytics.compare',
'analytics.aggregate', 'experiment.create', 'experiment.run', 'showplace.publish',
'sim.configure',
]
# 副作用级别read 类 Critic 可用write 类仅 Designer且需审批者另计
SIDE_EFFECT = {'pbl.get': 'read', 'kdb.search': 'read', 'kdb.get': 'read',
'runtime.get_state': 'read', 'analytics.query': 'read', 'pbl.preview': 'read',
'pbl.compare': 'read',
'pbl.create': 'write', 'pbl.update': 'write', 'pbl.validate': 'write',
'pbl.compile': 'write', 'runtime.playtest': 'write',
'template.instantiate': 'write'}
# 14.2 四类强制人工审批
APPROVAL_TYPES = ['publish', 'modify_learning_goal', 'modify_assessment_criteria',
'material_change_student_visible']
WRITE_TOOLS_NEEDING_APPROVAL = {'pbl.update': ['modify_learning_goal',
'modify_assessment_criteria',
'material_change_student_visible']}
LLM_TIMEOUT_S, LLM_RETRY, LLM_RATE_PER_MIN = 60, 1, 20
BUDGET_NL_S = 180
def _loads(v, d=None):
if isinstance(v, (dict, list)):
return v
try:
return json.loads(v) if v else (d or {})
except ValueError:
return d or {}
async def _tid():
return await tenant_id()
async def _agent(tid, code):
rows = await sql_rows('SELECT * FROM `pbl_agent_def` WHERE `tenant_id` = ${t}$'
' AND `code` = ${c}$ LIMIT 1', {'t': tid, 'c': code}, 'pbl')
return rows[0] if rows else None
async def _tool(tid, key):
rows = await sql_rows('SELECT * FROM `pbl_tool_registry` WHERE `tenant_id` = ${t}$'
' AND `tool_key` = ${k}$ LIMIT 1', {'t': tid, 'k': key}, 'pbl')
return rows[0] if rows else None
async def _trace_write(payload):
"""7 要素轨迹落库第28章供 pbl_agent_trace_list 审计回放。"""
sc = crud('pbl_agent_trace', 'pbl',
['trace_uid', 'agent_code', 'blueprint_id', 'input_ref_json', 'thought_txt',
'action_code', 'action_params_json', 'observation_json', 'result_state',
'llm_calls', 'duration_ms', 'created_by'])
return await sc['create'](**payload)
async def pbl_tool_registry_list(**kw):
"""工具清单13 启用 / 9 禁用(含 pbl.publish零实现入口可 grep 核对。"""
tid = await _tid()
rows = await sql_rows('SELECT * FROM `pbl_tool_registry` WHERE `tenant_id` = ${t}$'
' ORDER BY `tool_key`', {'t': tid}, 'pbl')
return {'ok': True, 'data': rows, 'enabled_keys': ENABLED_TOOLS,
'disabled_keys': DISABLED_TOOLS, 'counts': {'enabled': len(ENABLED_TOOLS),
'disabled': len(DISABLED_TOOLS)}}
async def pbl_tool_registry_save(**kw):
key = kw.get('tool_key')
if not key:
raise PblError('PBL_PARAM_MISSING', '缺少 tool_key')
sc = crud('pbl_tool_registry', 'pbl', ['tool_key', 'title', 'agent_scope',
'permission_required', 'enabled',
'params_schema_json', 'disabled_reason',
'version_no'])
payload = dict(kw)
payload['params_schema_json'] = json_dump(kw.get('params_schema_json'))
payload.setdefault('version_no', 1)
payload.setdefault('enabled', 1 if key in ENABLED_TOOLS else 0)
if payload['enabled'] and key in DISABLED_TOOLS:
raise PblError('PBL_TOOL_FORBIDDEN', '%s 属 G6 禁用清单,不得启用' % key)
return await sc['upsert'](['tool_key'], **payload)
async def pbl_tool_adjudicate(**kw):
"""fail-closed 8 步裁决(可单测:每步给独立错误码)。"""
tid = await _tid()
code, tool_key = kw.get('agent_code'), kw.get('tool_key')
params = _loads(kw.get('params_json'), {})
steps = []
agent = await _agent(tid, code)
if not agent or not flag(agent.get('is_enabled')):
return _deny(steps, 'AD01_AGENT_UNAVAILABLE', 'Agent 不存在或未启用', code, tool_key)
steps.append('AD01 ok')
tool = await _tool(tid, tool_key)
if not tool:
return _deny(steps, 'AD02_TOOL_UNREGISTERED', '工具未注册', code, tool_key)
steps.append('AD02 ok')
if tool_key in DISABLED_TOOLS or not flag(tool.get('enabled')):
return _deny(steps, 'AD03_TOOL_DISABLED',
'工具禁用G6 范围纪律):%s' % (tool.get('disabled_reason') or 'contract_only'),
code, tool_key)
steps.append('AD03 ok')
level = SIDE_EFFECT.get(tool_key, 'write')
if level == 'write' and (agent.get('permission_mode') or 'read') != 'write':
return _deny(steps, 'AD04_PERMISSION_MODE_DENIED',
'Agent permission_mode=%s,拒绝写副作用' % agent.get('permission_mode'),
code, tool_key)
steps.append('AD04 ok')
bad = _schema_check(_loads(tool.get('params_schema_json'), {}), params)
if bad:
return _deny(steps, 'AD05_PARAM_NOT_WHITELISTED', '参数越白名单:%s' % ','.join(bad),
code, tool_key)
steps.append('AD05 ok')
need = WRITE_TOOLS_NEEDING_APPROVAL.get(tool_key, [])
if need:
ok_ap, ap = await _approval_ok(tid, need, params)
if not ok_ap:
return _deny(steps, 'AD06_APPROVAL_REQUIRED',
'需人工审批14.2%s)且无 approved 单' % '/'.join(need),
code, tool_key, approval=ap)
steps.append('AD06 ok(approved)')
else:
steps.append('AD06 n/a')
if params.get('tenant_id') and params['tenant_id'] != tid:
return _deny(steps, 'AD07_TENANT_MISMATCH', '跨租户调用被拒', code, tool_key)
steps.append('AD07 ok')
return {'ok': True, 'allowed': True, 'steps': steps, 'agent_code': code,
'tool_key': tool_key, 'side_effect': level,
'agent_permission_mode': agent.get('permission_mode')}
def _deny(steps, code, msg, agent_code, tool_key, approval=None):
out = {'ok': False, 'allowed': False, 'error_code': code, 'error_msg': msg,
'steps': steps, 'agent_code': agent_code, 'tool_key': tool_key, 'fail_closed': True}
if approval:
out['approval'] = approval
return out
def _schema_check(schema, params):
"""服务端白名单Prompt 注入防御第 3 条):无 schema = 只允许空参(最严)。"""
allowed = set((schema or {}).get('properties') or schema.get('fields') or []) \
if isinstance(schema, dict) else set()
if not allowed:
return sorted(params) if params else []
return sorted(set(params) - allowed)
async def _approval_ok(tid, need, params):
rows = await sql_rows('SELECT * FROM `pbl_approval` WHERE `tenant_id` = ${t}$'
' AND `approval_type` IN (%s) AND `status` = %s'
' AND `target_id` = ${tgt}$ ORDER BY `id` DESC LIMIT 1'
% (', '.join(['${n%d}$' % i for i in range(len(need))]), "'approved'"),
dict({'t': tid, 'tgt': params.get('blueprint_id') or params.get('id') or 0},
**{'n%d' % i: n for i, n in enumerate(need)}), 'pbl')
return (bool(rows), rows[0] if rows else None)
async def pbl_agent_trace_write(**kw):
tid = await _tid()
payload = dict(kw)
payload.setdefault('trace_uid', 'T%s%s' % (now_str().replace('-', '').replace(':', '')
.replace(' ', ''), (kw.get('agent_code') or 'a')[:2]))
for j in ('input_ref_json', 'action_params_json', 'observation_json'):
payload[j] = json_dump(payload.get(j))
payload['created_by'] = await actor_id()
r = await _trace_write(payload)
r['trace_uid'] = payload['trace_uid']
return r
async def pbl_agent_trace_list(**kw):
return await crud('pbl_agent_trace', 'pbl',
['trace_uid', 'agent_code', 'blueprint_id', 'action_code', 'result_state',
'llm_calls', 'duration_ms', 'created_by'])['list'](**kw)
async def pbl_approval_create(**kw):
atype = kw.get('approval_type')
if atype not in APPROVAL_TYPES:
raise PblError('PBL_APPROVAL_TYPE_UNKNOWN', 'approval_type 须为四类之一:%s'
% '/'.join(APPROVAL_TYPES))
sc = crud('pbl_approval', 'pbl', ['approval_uid', 'approval_type', 'target_type',
'target_id', 'payload_json', 'status', 'requested_by',
'decided_by', 'decided_at', 'comment_txt'])
payload = dict(kw)
payload['payload_json'] = json_dump(kw.get('payload_json'))
payload.setdefault('approval_uid', 'A%s' % hashlib.sha256(
(atype + str(kw.get('target_id')) + now_str()).encode()).hexdigest()[:20])
payload.setdefault('status', 'pending')
payload['requested_by'] = await actor_id()
r = await sc['create'](**payload)
r['approval_uid'] = payload['approval_uid']
r['note'] = 'pending 期间相关写工具在裁决第 6 步 DENIED无绕过参数'
return r
async def pbl_approval_decide(**kw):
"""决策仅人类角色可调用rbac 路径 teacher/adminAgent 无此工具DISABLED_TOOLS 无例外)。"""
tid = await _tid()
status = kw.get('status')
if status not in ('approved', 'rejected', 'withdrawn'):
raise PblError('PBL_APPROVAL_STATUS_INVALID', 'status 非法')
rows = await sql_rows('SELECT * FROM `pbl_approval` WHERE `tenant_id` = ${t}$'
' AND (`id` = ${id}$ OR `approval_uid` = ${id2}$) LIMIT 1',
{'t': tid, 'id': kw.get('id') or 0, 'id2': kw.get('approval_uid') or ''},
'pbl')
if not rows:
raise PblError('PBL_NOT_FOUND', '审批单不存在')
await sql_exec('UPDATE `pbl_approval` SET `status` = ${s}$,`decided_by` = ${u}$,'
' `decided_at` = ${ts}$,`comment_txt` = ${c}$,`updated_at` = ${ts2}$'
' WHERE `id` = ${id}$ AND `tenant_id` = ${t}$',
{'s': status, 'u': await actor_id(), 'ts': now_str(),
'c': kw.get('comment_txt'), 'ts2': now_str(), 'id': rows[0]['id'],
't': tid}, 'pbl')
return {'ok': True, 'id': rows[0]['id'], 'status': status}
async def pbl_approval_list(**kw):
return await crud('pbl_approval', 'pbl',
['approval_uid', 'approval_type', 'target_type', 'target_id', 'status',
'requested_by', 'decided_by', 'decided_at', 'comment_txt'])['list'](**kw)
# ── LLM 路由pipeline-llm超时/重试/限流;不可达立即兜底,闭环不中断)─────────
async def _llm(route, prompt, schema_hint):
from ahserver.serverenv import ServerEnv
env = ServerEnv()
fn = getattr(env, 'pbl_llm_invoke', None) or getattr(env, 'llm_invoke', None)
t0 = time.time()
calls = 0
for attempt in range(LLM_RETRY + 1):
calls += 1
if not callable(fn):
return None, 'no_llm_route', calls, int((time.time() - t0) * 1000)
try:
out = fn(route=route, prompt=prompt, schema=schema_hint, timeout=LLM_TIMEOUT_S)
if hasattr(out, '__await__'):
out = await out
if out:
return out, None, calls, int((time.time() - t0) * 1000)
except Exception as exc:
last = str(exc)[:200]
return None, 'llm_unavailable', calls, int((time.time() - t0) * 1000)
async def pbl_agent_designer_run(**kw):
"""自然语言 → 结构化 BlueprintLLM 不可用 → 模板兜底generation_source=template_fallback"""
tid = await _tid()
nl = kw.get('input_txt') or kw.get('prompt') or ''
agent = await _agent(tid, kw.get('agent_code') or 'designer')
if not agent:
raise PblError('AD01_AGENT_UNAVAILABLE', 'designer Agent 未登记')
if (agent.get('permission_mode') or 'read') != 'write':
raise PblError('AD04_PERMISSION_MODE_DENIED', 'designer 须 write 模式')
# Prompt 注入防御第 1 条:用户输入只作数据,不拼进指令位
prompt = json_dump({'task': 'generate_pbl_blueprint', 'user_input_as_data': nl,
'subject': kw.get('subject'), 'grade': kw.get('grade')})
raw, err, calls, ms = await _llm(agent.get('model_route') or 'pipeline-llm', prompt,
'pbl.blueprint.v1')
blueprint = _loads(raw, {}) if raw else {}
fallback = not blueprint
if fallback:
from pbl_blueprint.api import pbl_template_instantiate
tpl = kw.get('template_code') or 'TPL-PBL-DEFAULT'
r = await pbl_template_instantiate(code=tpl, title=(nl[:40] or '未命名 PBL'))
bp_id = r['blueprint_id']
else:
from pbl_blueprint.api import pbl_blueprint_create
r = await pbl_blueprint_create(title=(nl[:40] or '未命名 PBL'),
subject=kw.get('subject'), grade=kw.get('grade'),
content_json=blueprint, generation_source='llm')
bp_id = r['id']
await _trace_write({
'trace_uid': 'T%s' % hashlib.sha256((nl + now_str()).encode()).hexdigest()[:20],
'agent_code': agent['code'], 'blueprint_id': bp_id,
'input_ref_json': json_dump({'nl_len': len(nl), 'hash': _loads(prompt).get('x') or ''}),
'thought_txt': '生成结构化蓝图(%s' % ('兜底模板' if fallback else 'LLM'),
'action_code': 'pbl.create', 'action_params_json': json_dump({'subject': kw.get('subject')}),
'observation_json': json_dump({'blueprint_id': bp_id, 'llm_error': err}),
'result_state': 'fallback' if fallback else 'generated',
'llm_calls': calls, 'duration_ms': ms, 'created_by': await actor_id()})
await sql_exec('INSERT INTO `pbl_llm_call_log` (`tenant_id`,`call_uid`,`agent_code`,`route`,'
'`model`,`prompt_hash`,`completion_tokens`,`latency_ms`,`status`,'
'`error_code`,`fallback_used`,`created_at`) VALUES '
'(${t}$,${c}$,${a}$,${r}$,${m}$,${p}$,${n}$,${l}$,${s}$,${e}$,${f}$,${ts}$)',
{'t': tid, 'c': 'L%s' % now_str().replace('-', '').replace(':', '')
.replace(' ', ''), 'a': agent['code'],
'r': agent.get('model_route'), 'm': '', 'p': hashlib.sha256(
prompt.encode()).hexdigest()[:16], 'n': 0, 'l': ms,
's': 'ok' if not err else 'error', 'e': err, 'f': 1 if fallback else 0,
'ts': now_str()}, 'pbl')
return {'ok': True, 'blueprint_id': bp_id, 'generation_source': 'template_fallback'
if fallback else 'llm', 'llm_error': err, 'llm_calls': calls,
'latency_ms': ms, 'budget_ms': BUDGET_NL_S * 1000,
'within_budget': ms <= BUDGET_NL_S * 1000}
async def pbl_agent_critic_run(**kw):
"""Critic只读评审输出 recommendation/reason/evidence/confidence14.3 可解释)。"""
tid = await _tid()
agent = await _agent(tid, kw.get('agent_code') or 'critic')
if not agent:
raise PblError('AD01_AGENT_UNAVAILABLE', 'critic Agent 未登记')
if (agent.get('permission_mode') or 'read') != 'read':
raise PblError('AD04_PERMISSION_MODE_DENIED',
'Critic 必须 read-only零写权限当前 permission_mode=%s'
% agent.get('permission_mode'))
bp_id = kw.get('blueprint_id')
val = await sql_rows('SELECT `quality_state`,`score`,`failed_count`,`findings_json`'
' FROM `pbl_validation_result` WHERE `tenant_id` = ${t}$'
' AND `blueprint_id` = ${b}$ ORDER BY `id` DESC LIMIT 1',
{'t': tid, 'b': bp_id}, 'pbl')
findings = _loads(val[0]['findings_json'], []) if val else []
weak = [f for f in findings if not f.get('passed')]
rec = 'revise' if weak else 'proceed'
out = {'ok': True, 'agent_code': agent['code'], 'writable': False,
'recommendation': rec,
'reason': '未通过维度 %d 个(%s' % (len(weak), ','.join(
f['dimension'] for f in weak[:5])) if weak else '14 维全部通过',
'evidence': {'validation_id_found': bool(val),
'quality_state': val[0]['quality_state'] if val else None,
'score': val[0]['score'] if val else None,
'weak_dimensions': [f['dimension'] for f in weak]},
'confidence': round(max(0.0, 1.0 - 0.12 * len(weak)), 2),
'suggestions': [f.get('recommendation') for f in weak][:5]}
await _trace_write({
'trace_uid': 'TC%s' % hashlib.sha256(('%s%s' % (bp_id, now_str())).encode())
.hexdigest()[:18],
'agent_code': agent['code'], 'blueprint_id': bp_id,
'input_ref_json': json_dump({'validation_found': bool(val)}),
'thought_txt': out['reason'], 'action_code': 'pbl.get',
'action_params_json': json_dump({'blueprint_id': bp_id}),
'observation_json': json_dump(out['evidence']),
'result_state': rec, 'llm_calls': 0, 'duration_ms': 0,
'created_by': await actor_id()})
return out

View File

@ -1,135 +1,470 @@
# -*- coding: utf-8 -*-
"""pbl_agent_runtime 挂载入口M4a/M4b)。
"""pbl_agent_runtime 挂载入口M4a 唯一实现)。
load_pbl_agent_runtime(env=None)
1. ensure_tables 4 幂等
2. **挂载即执行 self_check()**22=13+9 数量不符或写保护域存在启用写工具
6 探针裁决不符 RuntimeError应用启动失败fail-closed绝不带病上线
3. 注册 tools / enabled_tools / disabled_tools / api(judge/call_tool/verdict_stats/self_check)
4. 返回 default_verdict= 'DENY'
历史说明QC 退回 #7 处置):
九关裁决 + 4 实现tables.py / tool_registry.py / verdict.py / api.py
**删除**本文件不再引用M4am4a_*.py8 步裁决 + 5 是本模块唯一实现
`load_pbl_agent_runtime()` `load_pbl_agent_runtime_m4a()` 均委托 M4a 装配
对齐 docs/01-design/agent-tool-contract.md13 启用 / 9 禁用fail-closed
load_pbl_agent_runtime(env=None, tenant_id=None)
1) ensure_tables 5 幂等pbl_agent_trace / pbl_agent_trace_stage append-only
2) seed_agentsdesigner / criticcritic write_allowed=0+ seed_tools13 启用 / 9 禁用
3) **挂载即 self_check()**数量契约Critic 零写8 步裁决链四类审批默认裁决 DENY
任一不过 RuntimeError宿主应用启动失败fail-closed绝不带病上线
4) ServerEnv 注册 wwwroot/api/*.dspy 所需的 8 个处理函数 + 只读契约
5) 返回 default_verdict= 'DENY'
后端sqlor/ahserver未挂载时自动降级 MemoryStore建表/seed/裁决/审批全链路可用
数据仅存于进程内重启即失不抛错便于自测与离线演示US-05 兜底同源策略
"""
from . import tables as _tables
from . import tool_registry as _registry
from . import verdict as _verdict
from .m4a_kernel import (
ADJUDICATION_STEPS,
TRACE_ELEMENTS,
PblError,
TenantContext,
ACTOR_SYSTEM,
get_store,
)
from .m4a_tables import TABLES, APPEND_ONLY_TABLES, ensure_tables
from .m4a_registry import (
ENABLED_TOOL_CODES,
DISABLED_TOOL_CODES,
APPROVAL_REQUIRED_TOOLS,
PERM_PLATFORM_ADMIN,
list_agents,
list_tools,
get_tool,
register_tool,
set_tool_status,
seed_agents,
seed_tools,
)
from .m4a_adjudicate import adjudicate, adjudicate_report, explain_chain
from .m4a_approval import MANDATORY_APPROVAL_TYPES
from .m4a_trace import get_trace, list_traces
from .m4a import M4aApi, load_m4a, make_admin_ctx
from .m4a_init import MODULE_NAME, get_dbname
MODULE = "pbl_agent_runtime"
EXPECTED_TOTAL = 22
import functools
DEFAULT_VERDICT = "DENY" # fail-closed任何异常/未知一律拒绝
EXPECTED_ENABLED = 13
EXPECTED_DISABLED = 9
EXPECTED_STEPS = 8
EXPECTED_APPROVAL_TYPES = 4
EXPECTED_TABLES = 5
POLICY_VERSION = "m4a-1.0.0"
AGENTS = ("designer", "critic")
def ensure_tables(env=None, sor=None):
"""幂等建 4 表。返回 (ok_list, bad_list)。"""
return _tables.ensure_tables(env=env, sor=sor)
# ---------------------------------------------------------------------------
# 自检(挂载即执行,不过即抛)
# ---------------------------------------------------------------------------
def self_check(tenant_id=None, store=None, run_probes=True):
"""M4a 契约自检。返回 (all_ok, msgs)。
def self_check(env=None, run_probes=True):
"""模块自检:数量契约 + 写保护域 + 裁决探针。返回 (all_ok, msgs)。"""
校验项
C1 工具数量契约 22 = 13 启用 + 9 禁用
C2 自动发布类工具必须禁用blueprint.publish_auto / marketplace.create_listing
C3 Critic 零写权限write_allowed=0 且无任何写工具 allowed_agents critic
C4 裁决链恰为 8 步且顺序为 S1..S8
C5 四类强制人工审批齐备
C6 5 张表齐备且 append-only 表带标记
C7 默认裁决 = DENY
C8 探针禁用工具/未注册工具/Critic 三类调用必须被拒run_probes=True
"""
msgs = []
all_ok = True
st = store or get_store()
tid = tenant_id or "__platform__"
admin = make_admin_ctx(tid)
tools = _registry.all_tools()
enabled = [t for t in tools if t.get("enabled")]
disabled = [t for t in tools if not t.get("enabled")]
if len(tools) != EXPECTED_TOTAL:
tools = list_tools(ctx=admin, store=st)
enabled = [t for t in tools if t.get("status") == "enabled"]
disabled = [t for t in tools if t.get("status") == "disabled"]
# C1
if len(tools) != EXPECTED_ENABLED + EXPECTED_DISABLED:
all_ok = False
msgs.append("工具总数=%d 应为 %d" % (len(tools), EXPECTED_TOTAL))
msgs.append("FAIL C1 工具总数=%d 应为 %d"
% (len(tools), EXPECTED_ENABLED + EXPECTED_DISABLED))
if len(enabled) != EXPECTED_ENABLED:
all_ok = False
msgs.append("启用工具=%d 应为 %d" % (len(enabled), EXPECTED_ENABLED))
msgs.append("FAIL C1 启用工具=%d 应为 %d" % (len(enabled), EXPECTED_ENABLED))
if len(disabled) != EXPECTED_DISABLED:
all_ok = False
msgs.append("禁用工具=%d 应为 %d" % (len(disabled), EXPECTED_DISABLED))
msgs.append("工具契约:%d = %d 启用 + %d 禁用" % (len(tools), len(enabled), len(disabled)))
msgs.append("FAIL C1 禁用工具=%d 应为 %d" % (len(disabled), EXPECTED_DISABLED))
msgs.append("C1 工具契约:%d = %d 启用 + %d 禁用"
% (len(tools), len(enabled), len(disabled)))
reg_ok, reg_msgs = _registry.self_check()
if not reg_ok:
all_ok = False
msgs.extend(reg_msgs)
tbl_ok, tbl_msgs = _tables.self_check()
if not tbl_ok:
all_ok = False
msgs.extend(tbl_msgs)
if run_probes:
v_ok, v_msgs = _verdict.runtime_self_check()
if not v_ok:
# C2 自动发布/自动改课必须禁用
must_disabled = ("blueprint.publish_auto", "curriculum.modify_auto",
"marketplace.create_listing", "kdb.write", "billing.charge")
codes_disabled = set(DISABLED_TOOL_CODES)
for code in must_disabled:
if code not in codes_disabled:
all_ok = False
msgs.extend(v_msgs)
msgs.append("FAIL C2 %s 必须处于禁用清单" % code)
msgs.append("C2 高危工具禁用:%s" % ", ".join(must_disabled))
if _verdict.DEFAULT_VERDICT != "DENY":
# C3 Critic 零写
agents = list_agents(ctx=admin, store=st)
by_code = {a.get("agent_code"): a for a in agents}
if set(by_code) != set(AGENTS):
all_ok = False
msgs.append("DEFAULT_VERDICT=%r 必须为 'DENY'fail-closed"
% _verdict.DEFAULT_VERDICT)
msgs.append("FAIL C3 Agent 定义=%s 应为 %s" % (sorted(by_code), list(AGENTS)))
critic = by_code.get("critic") or {}
if critic and int(critic.get("write_allowed") or 0) != 0:
all_ok = False
msgs.append("FAIL C3 critic.write_allowed=%r 必须为 014.1 零写权限)"
% critic.get("write_allowed"))
write_tools_for_critic = [
t.get("tool_code") for t in enabled
if int(t.get("write_operation") or 0) == 1
and "critic" in (t.get("allowed_agents") or [])
]
if write_tools_for_critic:
all_ok = False
msgs.append("FAIL C3 critic 持有写工具:%s" % write_tools_for_critic)
msgs.append("C3 Critic 零写权限write_allowed=0写工具授权数=0")
# C4 8 步裁决链
steps = [s[0] for s in ADJUDICATION_STEPS]
if len(steps) != EXPECTED_STEPS or steps != ["S%d" % i for i in range(1, 9)]:
all_ok = False
msgs.append("FAIL C4 裁决链=%s 应为 S1..S88 步顺序固定)" % steps)
else:
msgs.append("C4 fail-closed 裁决链 8 步:%s"
% "".join("%s %s" % (s[0], s[1]) for s in ADJUDICATION_STEPS))
# C5 四类审批
if len(MANDATORY_APPROVAL_TYPES) != EXPECTED_APPROVAL_TYPES:
all_ok = False
msgs.append("FAIL C5 强制审批类型=%d 应为 %d"
% (len(MANDATORY_APPROVAL_TYPES), EXPECTED_APPROVAL_TYPES))
appr_tools = [t.get("tool_code") for t in enabled
if int(t.get("require_approval") or 0) == 1]
if set(appr_tools) != set(APPROVAL_REQUIRED_TOOLS):
all_ok = False
msgs.append("FAIL C5 require_approval 工具=%s 应为 %s"
% (appr_tools, list(APPROVAL_REQUIRED_TOOLS)))
msgs.append("C5 四类强制人工审批:%s;需审批工具=%s"
% (", ".join(MANDATORY_APPROVAL_TYPES), ", ".join(appr_tools)))
# C6 5 表 + append-only
tbl_names = [t.get("tblname") for t in TABLES]
if len(tbl_names) != EXPECTED_TABLES:
all_ok = False
msgs.append("FAIL C6 表数=%d 应为 %d%s"
% (len(tbl_names), EXPECTED_TABLES, tbl_names))
for name in ("pbl_agent_trace", "pbl_agent_trace_stage"):
if name not in APPEND_ONLY_TABLES:
all_ok = False
msgs.append("FAIL C6 %s 必须标记 append-only" % name)
msgs.append("C6 数据表 %d 张:%sappend-only%s"
% (len(tbl_names), ", ".join(tbl_names), ", ".join(APPEND_ONLY_TABLES)))
# C7 默认裁决
if DEFAULT_VERDICT != "DENY":
all_ok = False
msgs.append("FAIL C7 DEFAULT_VERDICT=%r 必须为 'DENY'" % DEFAULT_VERDICT)
else:
msgs.append("C7 默认裁决 = DENYfail-closed")
# C8 探针
if run_probes:
probes = [
("designer", "blueprint.publish_auto", "禁用工具(自动发布)"),
("designer", "pbl.publish", "未注册工具(白名单外 default-deny"),
("critic", "blueprint.update", "Critic 写蓝图14.1"),
("critic", "publish.request", "Critic 发布14.1"),
("designer", "compile.trigger", "未审批编译执行S7 审批门)"),
]
for agent_code, tool_code, why in probes:
step = code = None
try:
rep = adjudicate_report(admin, agent_code, tool_code, {}, store=st)
# Verdict.to_dict()allowed=False 即拒绝fail-closed 语义)
denied = rep.get("allowed") is False
step = rep.get("step_reached")
code = rep.get("reason_code")
except PblError as e:
denied, code = True, e.code
except Exception: # noqa: BLE001 —— 任何异常都视为拒绝fail-closed
denied, code = True, "EXCEPTION_AS_DENY"
if not denied:
all_ok = False
msgs.append("FAIL C8 探针未被拒:%s%s%s" % (agent_code, tool_code, why))
else:
msgs.append("C8 探针拒绝:%s%s @%s %s%s"
% (agent_code, tool_code, step, code, why))
msgs.append("轨迹 7 要素:%s" % ", ".join(TRACE_ELEMENTS))
if all_ok:
msgs.append("SELF_CHECK %s: PASS %d/%d" % (MODULE, len(tools), len(tools)))
msgs.append("SELF_CHECK %s: PASS%d 工具 / %d 表 / %d 步 / %d 类审批)"
% (MODULE_NAME, len(tools), len(tbl_names),
len(ADJUDICATION_STEPS), len(MANDATORY_APPROVAL_TYPES)))
return all_ok, msgs
def _wrap(fn):
"""dspy 处理函数统一包装PblError → 结构化拒绝响应fail-closed不抛 500
任何未预期异常同样按拒绝返回default_verdict=DENY 同源策略
并带 error_code=PBL_E_FORBIDDEN避免异常细节泄漏到前端
"""
@functools.wraps(fn)
async def _inner(**params_kw):
try:
data = await fn(**params_kw)
except PblError as e:
return dict(e.to_dict(), ok=False, default_verdict=DEFAULT_VERDICT)
except Exception as e: # noqa: BLE001
return {"ok": False, "error_code": "PBL_E_FORBIDDEN",
"error_message": "内部异常,按 fail-closed 拒绝:%s" % type(e).__name__,
"default_verdict": DEFAULT_VERDICT}
if isinstance(data, dict):
data.setdefault("ok", True)
return data
return _inner
# ---------------------------------------------------------------------------
# dspy 处理函数wwwroot/api/*.dspy 直接调用,名称必须与 dspy 内一致)
# ---------------------------------------------------------------------------
def _tid(params_kw):
tid = (params_kw or {}).get("tenant_id")
if not tid:
# fail-closed租户上下文缺失即拒绝S1
raise PblError("PBL_E_TENANT_MISSING", "tenant_id 强制打头,缺失即拒绝")
return tid
def _api(params_kw):
return load_m4a(tenant_id=_tid(params_kw))
@_wrap
async def pbl_agent_designer_run(**params_kw):
"""US-01/03/04Designer 生成 / 修改 / 澄清action 分派,缺省 generate"""
api = _api(params_kw)
action = params_kw.get("action", "generate")
if action == "modify":
return api.designer_modify(params_kw.get("blueprint_id"),
params_kw.get("instruction"),
session_no=params_kw.get("session_no"),
version_no=params_kw.get("version_no"))
if action == "clarify":
return api.designer_clarify(params_kw.get("blueprint_id"),
params_kw.get("missing_fields") or [],
session_no=params_kw.get("session_no"),
round_no=params_kw.get("round_no"))
return api.designer_generate(params_kw.get("intent_text") or "",
owner_teacher_id=params_kw.get("owner_teacher_id"),
class_id=params_kw.get("class_id"),
session_no=params_kw.get("session_no"))
@_wrap
async def pbl_agent_critic_run(**params_kw):
"""US-18Critic 只读评审(零写权限,写操作一律被 S6 拒)。"""
api = _api(params_kw)
if params_kw.get("trace_no"):
return api.get_critic_report(params_kw["trace_no"])
blueprint_id = params_kw.get("blueprint_id")
if not blueprint_id:
# 后端pbl_blueprint 权威服务)未挂载 / Designer 兜底提案尚未落库时,
# Critic 无可评审对象:返回结构化降级说明(不抛 500不伪造评审结论
return {"ok": False, "degraded": True,
"error_code": "PBL_E_BACKEND_UNAVAILABLE",
"error_message": "blueprint_id 缺失或蓝图后端未挂载Critic 无可评审对象;"
"Designer 兜底提案落 pending_backend待后端挂载后重试",
"agent_code": "critic", "write_allowed": 0}
return api.critic_review(blueprint_id,
version_no=params_kw.get("version_no"),
session_no=params_kw.get("session_no"))
@_wrap
async def pbl_tool_adjudicate(**params_kw):
"""fail-closed 8 步裁决预检只裁不执行。tool_code 为空 → 返回裁决链说明。"""
api = _api(params_kw)
tool_code = params_kw.get("tool_code")
if not tool_code:
return {"steps": explain_chain(), "default_verdict": DEFAULT_VERDICT,
"policy_version": POLICY_VERSION}
return api.adjudicate(params_kw.get("agent_code") or "designer", tool_code,
args=params_kw.get("args") or {})
@_wrap
async def pbl_agent_trace_list(**params_kw):
"""F-AG-04轨迹查询可查不可篡改"""
api = _api(params_kw)
if params_kw.get("trace_no"):
return {"trace": api.get_trace(params_kw["trace_no"]),
"completeness": api.trace_completeness(params_kw["trace_no"])}
return api.list_traces(params_kw.get("filters") or {},
page=int(params_kw.get("page") or 1),
size=int(params_kw.get("size") or 20))
def _trace_write(api, action, agent_code, params_kw):
from . import m4a_trace as _t
ctx = api.agent_ctx(agent_code)
if action == "start":
return _t.start_trace(agent_code, session_no=params_kw.get("session_no"),
input_context=params_kw.get("input_context"),
ctx=ctx, store=api.store)
if action == "append":
return _t.append_trace(params_kw.get("trace_no"),
params_kw.get("stage") or params_kw.get("element"),
params_kw.get("payload") or {},
ctx=ctx, store=api.store)
raise PblError("PBL_E_VALIDATION",
"action 非法:%rappend-only 仅支持 start / append" % action)
@_wrap
async def pbl_approval_create(**params_kw):
"""T10Agent 发起人工审批提案Agent 不可自批)。"""
api = _api(params_kw)
return api.request_approval(
action_type=params_kw.get("action_type"),
object_id=params_kw.get("object_id"),
agent_code=params_kw.get("agent_code") or "designer",
tool_code=params_kw.get("tool_code"),
object_type=params_kw.get("object_type"),
action_payload=params_kw.get("action_payload") or {},
approver_id=params_kw.get("approver_id"),
trace_no=params_kw.get("trace_no"),
)
@_wrap
async def pbl_approval_decide(**params_kw):
"""14.2:审批裁决 —— 仅 actor_type=user人类可调Agent 调用被拒。"""
api = _api(params_kw)
return api.decide_approval(params_kw.get("approval_no"),
params_kw.get("status"),
params_kw.get("user_id"),
comment=params_kw.get("comment"))
@_wrap
async def pbl_approval_list(**params_kw):
"""待办审批工作台approver_id 必填)。"""
api = _api(params_kw)
approver_id = params_kw.get("approver_id")
if not approver_id:
raise PblError("PBL_E_VALIDATION", "approver_id 必填")
return api.list_pending_approvals(approver_id)
# 修正 pbl_agent_trace_write 的 start 分支(避免占位返回)
@_wrap
async def pbl_agent_trace_write(**params_kw):
"""轨迹写入append-only。action=start 开轨迹action=append 追加 7 要素之一。"""
api = _api(params_kw)
return _trace_write(api, params_kw.get("action", "append"),
params_kw.get("agent_code") or "designer", params_kw)
# ---------------------------------------------------------------------------
# 对外契约
# ---------------------------------------------------------------------------
def api():
"""对外 API 契约(供应用/其它模块调用)。"""
"""模块对外 API 契约(供宿主应用 / 其它模块经 ServerEnv 调用)。"""
return {
"judge": _verdict.judge,
"call_tool": _verdict.call_tool,
"verdict_stats": _verdict.verdict_stats,
"adjudicate": adjudicate,
"adjudicate_report": adjudicate_report,
"adjudication_steps": explain_chain,
"list_tools": list_tools,
"get_tool": get_tool,
"register_tool": register_tool,
"set_tool_status": set_tool_status,
"list_agents": list_agents,
"get_trace": get_trace,
"list_traces": list_traces,
"self_check": self_check,
"get_tool": _registry.get_tool,
"all_tools": _registry.all_tools,
"ensure_tables": ensure_tables,
"POLICY_VERSION": _verdict.POLICY_VERSION,
"MAX_CALLS_PER_RUN": _verdict.MAX_CALLS_PER_RUN,
"PblError": _verdict.PblError,
"load_m4a": load_m4a,
"M4aApi": M4aApi,
"PblError": PblError,
"DEFAULT_VERDICT": DEFAULT_VERDICT,
"POLICY_VERSION": POLICY_VERSION,
"ADJUDICATION_STEPS": ADJUDICATION_STEPS,
"MANDATORY_APPROVAL_TYPES": MANDATORY_APPROVAL_TYPES,
"ENABLED_TOOL_CODES": ENABLED_TOOL_CODES,
"DISABLED_TOOL_CODES": DISABLED_TOOL_CODES,
}
def load_pbl_agent_runtime(env=None):
"""挂载入口:建表 → 自检(不过即抛)→ 注册契约 → 返回 default_verdict。"""
def load_pbl_agent_runtime(env=None, tenant_id=None, force_memory=False,
do_seed=True, run_self_check=True):
"""宿主应用标准挂载入口(委托 M4a 装配)。
返回 default_verdict'DENY'自检不过抛 RuntimeErrorfail-closed
"""
srv = env
if srv is None:
try:
from ahserver.serverenv import ServerEnv
srv = ServerEnv()
except Exception: # noqa: BLE001
except Exception: # noqa: BLE001 —— 无宿主环境时降级 MemoryStore
srv = None
if srv is not None:
ensure_tables(srv)
st = get_store(force_memory=force_memory or srv is None)
ensure_tables(st)
tid = tenant_id or "__platform__"
seed = None
if do_seed:
ctx = TenantContext(tenant_id=tid, actor_type=ACTOR_SYSTEM, actor_id="seed",
permissions={PERM_PLATFORM_ADMIN})
seed = {"agents": seed_agents(ctx=ctx, store=st),
"tools": seed_tools(ctx=ctx, store=st)}
ok, msgs = self_check()
for m in msgs:
_log(srv, m)
if not ok:
raise RuntimeError(
"%s self_check FAILEDfail-closed拒绝启动%s"
% (MODULE, "; ".join([m for m in msgs if "FAIL" in m or "应为" in m or "必须" in m][:6]))
)
if run_self_check:
ok, msgs = self_check(tenant_id=tid, store=st)
for m in msgs:
_log(srv, m)
if not ok:
raise RuntimeError("%s self_check FAILEDfail-closed拒绝启动%s"
% (MODULE_NAME,
"; ".join([m for m in msgs if m.startswith("FAIL")][:8])))
if srv is not None:
setattr(srv, "pbl_agent_tools", _registry.all_tools())
setattr(srv, "pbl_agent_enabled_tools", [t["name"] for t in _registry.ENABLED_TOOLS])
setattr(srv, "pbl_agent_disabled_tools", [t["name"] for t in _registry.DISABLED_TOOLS])
setattr(srv, "pbl_agent_tools", list_tools(ctx=make_admin_ctx(tid), store=st))
setattr(srv, "pbl_agent_enabled_tools", list(ENABLED_TOOL_CODES))
setattr(srv, "pbl_agent_disabled_tools", list(DISABLED_TOOL_CODES))
setattr(srv, "pbl_agent_api", api())
setattr(srv, "pbl_agent_dbname", get_dbname(srv))
setattr(srv, "pbl_agent_seed", seed)
# dspy 处理函数注册wwwroot/api/*.dspy 直接按名调用)
setattr(srv, "pbl_agent_designer_run", pbl_agent_designer_run)
setattr(srv, "pbl_agent_critic_run", pbl_agent_critic_run)
setattr(srv, "pbl_tool_adjudicate", pbl_tool_adjudicate)
setattr(srv, "pbl_agent_trace_list", pbl_agent_trace_list)
setattr(srv, "pbl_agent_trace_write", pbl_agent_trace_write)
setattr(srv, "pbl_approval_create", pbl_approval_create)
setattr(srv, "pbl_approval_decide", pbl_approval_decide)
setattr(srv, "pbl_approval_list", pbl_approval_list)
modules = getattr(srv, "modules", None)
if isinstance(modules, list) and MODULE not in modules:
modules.append(MODULE)
if isinstance(modules, list) and MODULE_NAME not in modules:
modules.append(MODULE_NAME)
return _verdict.DEFAULT_VERDICT
return DEFAULT_VERDICT
def _log(srv, msg):
logger = getattr(srv, "logger", None) if srv is not None else None
if logger is not None and hasattr(logger, "info"):
try:
logger.info("[%s] %s" % (MODULE, msg))
logger.info("[%s] %s" % (MODULE_NAME, msg))
return
except Exception: # noqa: BLE001
pass
print("[%s] %s" % (MODULE, msg))
print("[%s] %s" % (MODULE_NAME, msg))
if __name__ == "__main__":

View File

@ -1,161 +0,0 @@
# -*- coding: utf-8 -*-
"""pbl_agent_runtime 表定义与建表M4a
4 mariadb 方言BIGINT AUTO_INCREMENT 主键tenant_id 强制打头 FK/ENUM/TIMESTAMP
* pbl_agent_run Agent 运行主记录designer/critic
* pbl_agent_step 运行步骤think/tool/observe/plan/final
* pbl_tool_call 工具调用留痕ALLOW/DENY 都落库args 已脱敏
* pbl_tool_arbitration 裁决明细九关链逐关结果
对齐 docs/01-design/data-model.md agent-tool-contract.md
"""
TABLES = ["pbl_agent_run", "pbl_agent_step", "pbl_tool_call", "pbl_tool_arbitration"]
DDL = [
"""
CREATE TABLE IF NOT EXISTS `pbl_agent_run` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`run_code` VARCHAR(64) NOT NULL COMMENT '运行编码',
`agent_kind` VARCHAR(16) NOT NULL DEFAULT 'designer' COMMENT 'designer/critic',
`goal` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '本轮目标',
`blueprint_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联蓝图ID',
`status` VARCHAR(16) NOT NULL DEFAULT 'running' COMMENT 'running/done/failed/aborted',
`model` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '模型名',
`step_count` INT NOT NULL DEFAULT 0 COMMENT '步骤数',
`tool_call_count` INT NOT NULL DEFAULT 0 COMMENT '工具调用数',
`tool_deny_count` INT NOT NULL DEFAULT 0 COMMENT '被拒工具调用数',
`result` TEXT COMMENT '运行结果摘要',
`error_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '错误码',
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
`created_at` DATETIME COMMENT '创建时间',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_run_code` (`tenant_id`, `run_code`),
KEY `ix_tenant_status` (`tenant_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Agent 运行主记录'
""",
"""
CREATE TABLE IF NOT EXISTS `pbl_agent_step` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`step_code` VARCHAR(64) NOT NULL COMMENT '步骤编码',
`run_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属运行ID',
`step_no` INT NOT NULL DEFAULT 0 COMMENT '步骤序号',
`phase` VARCHAR(16) NOT NULL DEFAULT 'think' COMMENT 'think/tool/observe/plan/final',
`content` TEXT COMMENT '步骤内容',
`tokens_in` INT NOT NULL DEFAULT 0 COMMENT '输入 token',
`tokens_out` INT NOT NULL DEFAULT 0 COMMENT '输出 token',
`elapsed_ms` INT NOT NULL DEFAULT 0 COMMENT '耗时毫秒',
`created_at` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_run_step` (`tenant_id`, `run_id`, `step_no`),
KEY `ix_tenant_phase` (`tenant_id`, `phase`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Agent 运行步骤'
""",
"""
CREATE TABLE IF NOT EXISTS `pbl_tool_call` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`call_code` VARCHAR(64) NOT NULL COMMENT '调用编码',
`run_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属运行ID',
`tool_name` VARCHAR(64) NOT NULL COMMENT '工具名',
`args` TEXT COMMENT '入参(脱敏后)',
`verdict` VARCHAR(8) NOT NULL DEFAULT 'DENY' COMMENT 'ALLOW/DENY',
`verdict_id` BIGINT NOT NULL DEFAULT 0 COMMENT '裁决记录ID',
`result` TEXT COMMENT '执行结果(截断)',
`state` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/ok/failed/denied',
`error_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '错误码',
`elapsed_ms` INT NOT NULL DEFAULT 0 COMMENT '耗时毫秒',
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '调用人',
`created_at` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_call_code` (`tenant_id`, `call_code`),
KEY `ix_tenant_tool` (`tenant_id`, `tool_name`),
KEY `ix_tenant_verdict` (`tenant_id`, `verdict`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具调用留痕'
""",
"""
CREATE TABLE IF NOT EXISTS `pbl_tool_arbitration` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`arb_code` VARCHAR(64) NOT NULL COMMENT '裁决编码',
`call_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联调用ID',
`tool_name` VARCHAR(64) NOT NULL COMMENT '工具名',
`verdict` VARCHAR(8) NOT NULL DEFAULT 'DENY' COMMENT 'ALLOW/DENY',
`failed_gate` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个未通过关卡',
`checks` TEXT COMMENT '九关链逐关结果 JSON',
`policy_version` VARCHAR(16) NOT NULL DEFAULT 'v1' COMMENT '策略版本',
`role_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '调用角色',
`created_at` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_arb_code` (`tenant_id`, `arb_code`),
KEY `ix_tenant_gate` (`tenant_id`, `failed_gate`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具裁决明细'
""",
]
def get_ddl():
"""返回本模块全部建表语句list[str])。"""
return [x.strip() for x in DDL]
def ensure_tables(env=None, sor=None):
"""幂等建表CREATE TABLE IF NOT EXISTS重复执行无副作用。
返回 (已确保表名列表, 失败列表)
"""
ok, bad = [], []
runner = sor
if runner is None and env is not None:
runner = getattr(env, "sor", None) or getattr(env, "db", None)
for stmt in get_ddl():
name = stmt.split("`")[1] if "`" in stmt else "?"
try:
if runner is not None and hasattr(runner, "sqlExe"):
runner.sqlExe(stmt)
elif env is not None and hasattr(env, "sqlExe"):
env.sqlExe(stmt)
else:
# 无 DB 连接(如离线自检)时只校验语句形态,不静默声称已建表
if "AUTO_INCREMENT" not in stmt or "tenant_id" not in stmt:
raise ValueError("DDL 形态不合规:%s" % name)
ok.append(name)
except Exception as exc: # noqa: BLE001
bad.append((name, str(exc)[:160]))
return ok, bad
def self_check():
"""离线自检4 表齐全、tenant_id 打头、无禁用方言。返回 (all_ok, msgs)。"""
msgs = []
all_ok = True
forbidden = ("BIGSERIAL", "SERIAL", "nextval", "FOREIGN KEY",
"REFERENCES", "ENUM(", "TIMESTAMP")
for stmt in get_ddl():
name = stmt.split("`")[1]
if name not in TABLES:
all_ok = False
msgs.append("未知表 %s" % name)
body = stmt.upper()
for kw in forbidden:
if kw in body:
all_ok = False
msgs.append("%s 命中禁用方言 %s" % (name, kw))
cols = [ln.strip().split("`")[1] for ln in stmt.splitlines()
if ln.strip().startswith("`")]
biz = [c for c in cols if c != "id"]
if not biz or biz[0] != "tenant_id":
all_ok = False
msgs.append("%s 首个业务列=%s(应为 tenant_id" % (name, biz[:1]))
if "AUTO_INCREMENT" not in body:
all_ok = False
msgs.append("%s 缺 AUTO_INCREMENT 主键" % name)
if len(TABLES) != 4:
all_ok = False
msgs.append("表数=%d 应为 4" % len(TABLES))
if all_ok:
msgs.append("SELF_CHECK pbl_agent_runtime.tables: PASS %d/%d" % (len(TABLES), len(TABLES)))
return all_ok, msgs

View File

@ -1,196 +0,0 @@
# -*- coding: utf-8 -*-
"""Agent 工具注册表M4a—— 22 工具 = 13 启用 + 9 禁用fail-closed。
对齐 docs/01-design/agent-tool-contract.md
* 未注册工具 一律 DENYPBL-TOOL-0001
* 注册但 enabled=False 一律 DENYPBL-TOOL-0002
* 写保护域rbac/world/scene/entity/scense/scense_runtime/script_engine 基表
出现 enabled 的写工具 self_check 直接失败启动即拒
工具契约字段
name / kind(read|write) / domain / enabled / role(最低角色) /
required(必填参数) / schema(参数类型与枚举) / handler(点分路径) / desc
"""
WRITE_PROTECTED_DOMAINS = (
"rbac", "world", "scene", "entity",
"scense", "scense_runtime", "script_engine",
)
POLICY_VERSION = "v1"
# ---- 13 个启用工具Agent 可调用)
ENABLED_TOOLS = [
{"name": "blueprint.get", "kind": "read", "domain": "pbl_blueprint", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "blueprint_id"],
"schema": {"tenant_id": "int", "blueprint_id": "int"},
"handler": "pbl_blueprint.api:get_blueprint",
"desc": "读取蓝图聚合根(租户隔离)"},
{"name": "blueprint.tree", "kind": "read", "domain": "pbl_blueprint", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "blueprint_id"],
"schema": {"tenant_id": "int", "blueprint_id": "int", "depth": "int"},
"handler": "pbl_blueprint.api:get_tree",
"desc": "读取蓝图子对象树"},
{"name": "blueprint.patch", "kind": "write", "domain": "pbl_blueprint", "enabled": True,
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id", "obj_type", "payload"],
"schema": {"tenant_id": "int", "blueprint_id": "int", "payload": "dict",
"obj_type": {"enum": ["goal", "role", "task", "artifact", "rubric",
"resource", "flow"]}},
"handler": "pbl_blueprint.api:patch_subobject",
"desc": "泛化写入 7 类子对象"},
{"name": "validation.run", "kind": "read", "domain": "pbl_validation", "enabled": True,
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id"],
"schema": {"tenant_id": "int", "blueprint_id": "int", "ruleset_code": "str"},
"handler": "pbl_validation.api:run_validation",
"desc": "14 维校验 + 5 级质量状态"},
{"name": "validation.findings", "kind": "read", "domain": "pbl_validation", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "run_id"],
"schema": {"tenant_id": "int", "run_id": "int", "severity": {"enum": ["info", "warn", "error", "block"]}},
"handler": "pbl_validation.api:list_findings",
"desc": "读取校验发现项"},
{"name": "compiler.compile", "kind": "write", "domain": "pbl_compiler", "enabled": True,
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id"],
"schema": {"tenant_id": "int", "blueprint_id": "int", "target": {"enum": ["game_definition", "runtime_bundle"]}},
"handler": "pbl_compiler.api:compile_blueprint",
"desc": "确定性编译(同输入同输出)"},
{"name": "compiler.artifact", "kind": "read", "domain": "pbl_compiler", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "job_id"],
"schema": {"tenant_id": "int", "job_id": "int"},
"handler": "pbl_compiler.api:get_artifact",
"desc": "读取编译产物"},
{"name": "evidence.collect", "kind": "write", "domain": "pbl_evidence", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "run_id", "idem_key", "payload"],
"schema": {"tenant_id": "int", "run_id": "int", "idem_key": "str", "payload": "dict"},
"handler": "pbl_evidence.api:collect_evidence",
"desc": "幂等采集产出物证据"},
{"name": "evidence.list", "kind": "read", "domain": "pbl_evidence", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "run_id"],
"schema": {"tenant_id": "int", "run_id": "int", "kind": "str"},
"handler": "pbl_evidence.api:list_evidence",
"desc": "列出证据"},
{"name": "assessment.score", "kind": "write", "domain": "pbl_assessment", "enabled": True,
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id", "team_id"],
"schema": {"tenant_id": "int", "blueprint_id": "int", "team_id": "int"},
"handler": "pbl_assessment.api:score_rubric",
"desc": "Rubric 加权评估"},
{"name": "assessment.report", "kind": "read", "domain": "pbl_assessment", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "report_id"],
"schema": {"tenant_id": "int", "report_id": "int"},
"handler": "pbl_assessment.api:get_report",
"desc": "读取评估报告"},
{"name": "runtime.event_append", "kind": "write", "domain": "pbl_runtime_ext", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "world_id", "event_type", "payload"],
"schema": {"tenant_id": "int", "world_id": "int", "event_type": "str", "payload": "dict"},
"handler": "pbl_runtime_ext.api:append_event",
"desc": "单事务事件+状态写入(薄扩展,不改基表)"},
{"name": "kdb.query", "kind": "read", "domain": "pbl_kdb_ext", "enabled": True,
"role": "student.pbls", "required": ["tenant_id", "query_code"],
"schema": {"tenant_id": "int", "query_code": "str", "anon": "bool"},
"handler": "pbl_kdb_ext.api:kdb_query",
"desc": "KDB 只读桩 + 匿名聚合(零写入)"},
]
# ---- 9 个禁用工具(注册但 enabled=False调用一律 DENY
DISABLED_TOOLS = [
{"name": "rbac.grant_role", "kind": "write", "domain": "rbac", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "user_id", "role_code"],
"schema": {"tenant_id": "int", "user_id": "int", "role_code": "str"},
"handler": "", "desc": "写保护域:禁止 Agent 授权Phase 0/1 禁用)"},
{"name": "rbac.revoke_role", "kind": "write", "domain": "rbac", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "user_id", "role_code"],
"schema": {"tenant_id": "int", "user_id": "int", "role_code": "str"},
"handler": "", "desc": "写保护域:禁止 Agent 撤权"},
{"name": "world.delete", "kind": "write", "domain": "world", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "world_id"],
"schema": {"tenant_id": "int", "world_id": "int"},
"handler": "", "desc": "写保护域:禁止删除世界基表"},
{"name": "scene.delete", "kind": "write", "domain": "scene", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "scene_id"],
"schema": {"tenant_id": "int", "scene_id": "int"},
"handler": "", "desc": "写保护域:禁止删除场景基表"},
{"name": "entity.delete", "kind": "write", "domain": "entity", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "entity_id"],
"schema": {"tenant_id": "int", "entity_id": "int"},
"handler": "", "desc": "写保护域:禁止删除实体基表"},
{"name": "script_engine.execute_raw", "kind": "write", "domain": "script_engine", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "script_text"],
"schema": {"tenant_id": "int", "script_text": "str"},
"handler": "", "desc": "写保护域:禁止执行任意脚本"},
{"name": "scense.reset_session", "kind": "write", "domain": "scense", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "session_id"],
"schema": {"tenant_id": "int", "session_id": "int"},
"handler": "", "desc": "写保护域:禁止重置游戏会话"},
{"name": "db.ddl_exec", "kind": "write", "domain": "platform", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id", "ddl"],
"schema": {"tenant_id": "int", "ddl": "str"},
"handler": "", "desc": "禁止 Agent 执行 DDL"},
{"name": "tenant.switch", "kind": "write", "domain": "platform", "enabled": False,
"role": "owner.pbls", "required": ["tenant_id"],
"schema": {"tenant_id": "int"},
"handler": "", "desc": "禁止 Agent 切换租户上下文(防跨租户)"},
]
EXPECTED_ENABLED = 13
EXPECTED_DISABLED = 9
def all_tools():
"""返回全部注册工具(启用 + 禁用22 项。"""
return list(ENABLED_TOOLS) + list(DISABLED_TOOLS)
def registry():
"""返回 name → tool 字典。"""
return {t["name"]: t for t in all_tools()}
def get_tool(name):
"""按名取工具契约;未注册返回 None调用方必须 fail-closed DENY"""
return registry().get(name)
def is_registered(name):
return name in registry()
def is_enabled(name):
t = get_tool(name)
return bool(t and t.get("enabled"))
def self_check():
"""数量契约 + 写保护域校验。返回 (all_ok, msgs)。
硬约束
* len(ENABLED_TOOLS) == 13 len(DISABLED_TOOLS) == 9 22
* 工具名全局唯一
* 写保护域内不得存在 enabled=True write 工具
* 每个 enabled 工具必须有非空 handler
"""
msgs = []
all_ok = True
if len(ENABLED_TOOLS) != EXPECTED_ENABLED:
all_ok = False
msgs.append("启用工具数=%d 应为 %d" % (len(ENABLED_TOOLS), EXPECTED_ENABLED))
if len(DISABLED_TOOLS) != EXPECTED_DISABLED:
all_ok = False
msgs.append("禁用工具数=%d 应为 %d" % (len(DISABLED_TOOLS), EXPECTED_DISABLED))
names = [t["name"] for t in all_tools()]
dup = sorted(set([n for n in names if names.count(n) > 1]))
if dup:
all_ok = False
msgs.append("工具名重复:%s" % dup)
for t in all_tools():
if t.get("domain") in WRITE_PROTECTED_DOMAINS and t.get("enabled") and t.get("kind") == "write":
all_ok = False
msgs.append("写保护域 %s 存在启用写工具 %s" % (t["domain"], t["name"]))
if t.get("enabled") and not t.get("handler"):
all_ok = False
msgs.append("启用工具 %s 缺 handler" % t["name"])
if not t.get("required") or "tenant_id" not in t["required"]:
all_ok = False
msgs.append("工具 %s required 未含 tenant_id" % t["name"])
if all_ok:
msgs.append("SELF_CHECK pbl_agent_runtime.tool_registry: PASS %d/%d (enabled=%d disabled=%d)"
% (len(names), len(names), len(ENABLED_TOOLS), len(DISABLED_TOOLS)))
return all_ok, msgs

View File

@ -1,457 +0,0 @@
# -*- coding: utf-8 -*-
"""fail-closed 工具裁决引擎M4b
九关链任一关不过 DENY逐关 append checks{gate,pass,reason}
G1 registered PBL-TOOL-0001 工具未注册
G2 enabled PBL-TOOL-0002 工具已注册但禁用
G3 role PBL-PERM-0001 角色权限不足
G4 tenant_present PBL-TENANT-0001 缺租户上下文
G5 tenant_match PBL-TENANT-0002 跨租户访问
G6 required PBL-PARAM-0001 缺必填参数
G7 schema PBL-PARAM-0002 参数类型/枚举不合法写工具未登记参数严格拒
G8 write_protected PBL-PERM-0002 写保护域写操作
G9 quota PBL-TOOL-0005 超出单轮调用配额
铁律
* DEFAULT_VERDICT = 'DENY'默认拒非默认放
* ALLOW / DENY 都留痕pbl_tool_call + pbl_tool_arbitration + write_audit
* 执行异常不降级为 ALLOWstate=failed 并转码
* 敏感字段脱敏后落库结果截断
"""
import importlib
import json
import time
import uuid
from . import tool_registry
POLICY_VERSION = "v1"
DEFAULT_VERDICT = "DENY"
MAX_CALLS_PER_RUN = 200
MAX_RESULT_CHARS = 8000
MAX_ARGS_CHARS = 4000
SENSITIVE_KEYS = (
"password", "passwd", "pwd", "secret", "token", "api_key",
"apikey", "credential", "id_card", "idcard", "phone", "mobile",
"bank_account", "private_key",
)
ROLE_LEVEL = {
"observer.pbls": 0,
"student.pbls": 1,
"assistant.pbls": 2,
"teacher.pbls": 3,
"admin.pbls": 4,
"owner.pbls": 5,
"owner.audit": 4,
}
ERR = {
"unregistered": "PBL-TOOL-0001",
"disabled": "PBL-TOOL-0002",
"role": "PBL-PERM-0001",
"tenant_missing": "PBL-TENANT-0001",
"tenant_mismatch": "PBL-TENANT-0002",
"required": "PBL-PARAM-0001",
"schema": "PBL-PARAM-0002",
"write_protected": "PBL-PERM-0002",
"quota": "PBL-TOOL-0005",
"handler_missing": "PBL-TOOL-0004",
"exec_error": "PBL-EXEC-0001",
}
class PblError(Exception):
"""业务错误(带错误码)。"""
def __init__(self, code, message):
super(PblError, self).__init__(message)
self.code = code
self.message = message
def _redact(obj):
"""递归脱敏敏感字段8+ 类)。"""
if isinstance(obj, dict):
out = {}
for k, v in obj.items():
if isinstance(k, str) and any(s in k.lower() for s in SENSITIVE_KEYS):
out[k] = "***REDACTED***"
else:
out[k] = _redact(v)
return out
if isinstance(obj, (list, tuple)):
return [_redact(x) for x in obj]
return obj
def _dump(obj, limit=MAX_ARGS_CHARS):
"""安全序列化 + 截断。"""
try:
text = json.dumps(obj, ensure_ascii=False, default=str)
except (TypeError, ValueError):
text = str(obj)
if len(text) > limit:
text = text[:limit] + "...[TRUNCATED %d]" % len(text)
return text
def _new_code(prefix):
return "%s%s" % (prefix, uuid.uuid4().hex[:16])
def _check(gate, passed, reason=""):
return {"gate": gate, "pass": bool(passed), "reason": reason}
def _coerce(value, spec, strict_write):
"""按 schema 校验/转换单个参数。返回 (ok, converted, reason)。"""
if isinstance(spec, dict) and "enum" in spec:
allowed = spec["enum"]
if value not in allowed:
return False, value, "%r 不在枚举 %s" % (value, allowed)
return True, value, ""
t = spec if isinstance(spec, str) else "any"
if t == "int":
try:
return True, int(value), ""
except (TypeError, ValueError):
return False, value, "期望 int实得 %r" % (value,)
if t == "float":
try:
return True, float(value), ""
except (TypeError, ValueError):
return False, value, "期望 float实得 %r" % (value,)
if t == "bool":
if isinstance(value, bool):
return True, value, ""
if isinstance(value, str) and value.lower() in ("true", "false", "1", "0"):
return True, value.lower() in ("true", "1"), ""
return False, value, "期望 bool实得 %r" % (value,)
if t == "str":
if not isinstance(value, str):
if strict_write:
return False, value, "期望 str实得 %r" % (value,)
return True, str(value), ""
return True, value, ""
if t == "dict":
if not isinstance(value, dict):
return False, value, "期望 dict实得 %r" % (value,)
return True, value, ""
if t == "list":
if not isinstance(value, (list, tuple)):
return False, value, "期望 list实得 %r" % (value,)
return True, list(value), ""
return True, value, ""
def judge(tool_name, args, ctx, quota_used=0):
"""九关链裁决。返回 dict
{verdict, error_code, failed_gate, checks, tool, args(脱敏转换后), call_id}
"""
args = dict(args or {})
ctx = dict(ctx or {})
checks = []
safe_args = _redact(args)
result = {
"verdict": DEFAULT_VERDICT,
"error_code": "",
"failed_gate": "",
"checks": checks,
"tool": tool_name,
"args": safe_args,
"policy_version": POLICY_VERSION,
"call_id": _new_code("call_"),
"arb_code": _new_code("arb_"),
}
def deny(gate, code, reason):
result["failed_gate"] = gate
result["error_code"] = code
result["verdict"] = DEFAULT_VERDICT
checks.append(_check(gate, False, reason))
return result
# G1 registered
tool = tool_registry.get_tool(tool_name)
if tool is None:
return deny("G1_registered", ERR["unregistered"],
"工具 %r 未注册fail-closed" % tool_name)
checks.append(_check("G1_registered", True, ""))
# G2 enabled
if not tool.get("enabled"):
return deny("G2_enabled", ERR["disabled"],
"工具 %r 已注册但禁用(%s" % (tool_name, tool.get("desc", "")))
checks.append(_check("G2_enabled", True, ""))
# G3 role
role = ctx.get("role") or ""
need = tool.get("role") or "student.pbls"
if ROLE_LEVEL.get(role, -1) < ROLE_LEVEL.get(need, 99):
return deny("G3_role", ERR["role"],
"角色 %r 权限不足,需 %r" % (role, need))
checks.append(_check("G3_role", True, "role=%s need=%s" % (role, need)))
# G4 tenant_present
ctx_tenant = ctx.get("tenant_id")
if ctx_tenant in (None, "", 0, "0"):
return deny("G4_tenant_present", ERR["tenant_missing"], "缺租户上下文 tenant_id")
checks.append(_check("G4_tenant_present", True, "tenant_id=%s" % ctx_tenant))
# G5 tenant_match防跨租户
arg_tenant = args.get("tenant_id")
if arg_tenant is not None and str(arg_tenant) != str(ctx_tenant):
return deny("G5_tenant_match", ERR["tenant_mismatch"],
"参数 tenant_id=%r 与上下文 %r 不一致" % (arg_tenant, ctx_tenant))
args["tenant_id"] = int(ctx_tenant) if str(ctx_tenant).lstrip("-").isdigit() else ctx_tenant
checks.append(_check("G5_tenant_match", True, ""))
# G6 required
missing = [k for k in (tool.get("required") or []) if k not in args or args[k] in (None, "")]
if missing:
return deny("G6_required", ERR["required"], "缺必填参数 %s" % missing)
checks.append(_check("G6_required", True, ""))
# G7 schema写工具未登记参数严格拒
schema = tool.get("schema") or {}
strict_write = tool.get("kind") == "write"
unknown = [k for k in args if k not in schema]
if strict_write and unknown:
return deny("G7_schema", ERR["schema"],
"写工具存在未登记参数 %s(严格拒)" % unknown)
converted = {}
for k, v in args.items():
spec = schema.get(k, "any")
ok, val, reason = _coerce(v, spec, strict_write)
if not ok:
return deny("G7_schema", ERR["schema"], "参数 %s %s" % (k, reason))
converted[k] = val
checks.append(_check("G7_schema", True, ""))
# G8 write_protected
if tool.get("kind") == "write" and tool.get("domain") in tool_registry.WRITE_PROTECTED_DOMAINS:
return deny("G8_write_protected", ERR["write_protected"],
"写保护域 %s 禁止写操作" % tool.get("domain"))
checks.append(_check("G8_write_protected", True, "domain=%s" % tool.get("domain")))
# G9 quota
if quota_used >= MAX_CALLS_PER_RUN:
return deny("G9_quota", ERR["quota"],
"单轮调用已达上限 %d" % MAX_CALLS_PER_RUN)
checks.append(_check("G9_quota", True, "used=%d/%d" % (quota_used, MAX_CALLS_PER_RUN)))
result["verdict"] = "ALLOW"
result["args"] = _redact(converted)
result["_converted"] = converted
return result
def _persist_verdict(env, verdict_rec, state, result_text, error_code, elapsed_ms):
"""ALLOW/DENY 都留痕pbl_tool_call + pbl_tool_arbitration + write_audit。"""
written = []
if env is None:
return written
tenant_id = (verdict_rec.get("args") or {}).get("tenant_id") or 0
sor = getattr(env, "sor", None)
call_row = {
"tenant_id": tenant_id,
"call_code": verdict_rec["call_id"],
"tool_name": verdict_rec["tool"],
"args": _dump(verdict_rec.get("args"), MAX_ARGS_CHARS),
"verdict": verdict_rec["verdict"],
"result": _dump(result_text, MAX_RESULT_CHARS),
"state": state,
"error_code": error_code or "",
"elapsed_ms": int(elapsed_ms),
}
arb_row = {
"tenant_id": tenant_id,
"arb_code": verdict_rec["arb_code"],
"tool_name": verdict_rec["tool"],
"verdict": verdict_rec["verdict"],
"failed_gate": verdict_rec.get("failed_gate") or "",
"checks": _dump(verdict_rec.get("checks"), MAX_ARGS_CHARS),
"policy_version": POLICY_VERSION,
"role_code": "",
}
for tbl, row in (("pbl_tool_call", call_row), ("pbl_tool_arbitration", arb_row)):
try:
if sor is not None and hasattr(sor, "C"):
sor.C(tbl, row)
written.append(tbl)
except Exception: # noqa: BLE001
pass
try:
audit = getattr(env, "write_audit", None)
if callable(audit):
audit("tool_call" if verdict_rec["verdict"] == "ALLOW" else "tool_deny",
{"tool": verdict_rec["tool"], "gate": verdict_rec.get("failed_gate"),
"code": error_code, "tenant_id": tenant_id})
written.append("audit")
except Exception: # noqa: BLE001
pass
return written
def _resolve_handler(dotted):
"""'pkg.mod:func' → callable。解析失败抛 PblError不降级"""
if not dotted or ":" not in dotted:
raise PblError(ERR["handler_missing"], "handler 未登记:%r" % dotted)
mod_path, fn_name = dotted.split(":", 1)
try:
mod = importlib.import_module(mod_path)
except ImportError as exc:
raise PblError(ERR["handler_missing"], "导入 %s 失败:%s" % (mod_path, exc))
fn = getattr(mod, fn_name, None)
if not callable(fn):
raise PblError(ERR["handler_missing"], "%s%s()" % (mod_path, fn_name))
return fn
def call_tool(env, tool_name, args, ctx, quota_used=0):
"""裁决 → 执行 → 留痕。返回 dict{verdict,state,result,error_code,call_id,elapsed_ms}。
执行异常分别转码PblError 用其 codeTypeError/ValueError PBL-PARAM-0002
其它 Exception PBL-EXEC-0001任何异常都不降级为 ALLOW
"""
t0 = time.time()
rec = judge(tool_name, args, ctx, quota_used=quota_used)
out = {
"call_id": rec["call_id"],
"arb_code": rec["arb_code"],
"tool": tool_name,
"verdict": rec["verdict"],
"state": "denied" if rec["verdict"] != "ALLOW" else "pending",
"result": None,
"error_code": rec.get("error_code") or "",
"failed_gate": rec.get("failed_gate") or "",
"checks": rec["checks"],
"elapsed_ms": 0,
}
if rec["verdict"] != "ALLOW":
out["elapsed_ms"] = int((time.time() - t0) * 1000)
_persist_verdict(env, rec, "denied", None, out["error_code"], out["elapsed_ms"])
return out
tool = tool_registry.get_tool(tool_name) or {}
try:
fn = _resolve_handler(tool.get("handler"))
payload = rec.get("_converted") or rec["args"]
res = fn(**payload)
out["state"] = "ok"
out["result"] = res
except PblError as exc:
out["state"] = "failed"
out["error_code"] = exc.code
out["result"] = {"message": exc.message}
except (TypeError, ValueError) as exc:
out["state"] = "failed"
out["error_code"] = ERR["schema"]
out["result"] = {"message": str(exc)[:400]}
except Exception as exc: # noqa: BLE001
out["state"] = "failed"
out["error_code"] = ERR["exec_error"]
out["result"] = {"message": str(exc)[:400]}
out["elapsed_ms"] = int((time.time() - t0) * 1000)
_persist_verdict(env, rec, out["state"], out["result"], out["error_code"], out["elapsed_ms"])
return out
def verdict_stats(env=None, rows=None):
"""统计 allow/deny/deny_rate/deny_by_gate。rows 可外部注入(离线自检用)。"""
rows = rows if rows is not None else []
total = len(rows)
allow = len([r for r in rows if (r.get("verdict") or "").upper() == "ALLOW"])
deny = total - allow
by_gate = {}
for r in rows:
g = r.get("failed_gate") or ""
if g:
by_gate[g] = by_gate.get(g, 0) + 1
return {
"total": total,
"allow": allow,
"deny": deny,
"deny_rate": round(float(deny) / total, 4) if total else 0.0,
"deny_by_gate": by_gate,
"policy_version": POLICY_VERSION,
}
def runtime_self_check():
"""6 探针5 个必须 DENY + 1 个合法读必须 ALLOW。返回 (all_ok, msgs)。"""
msgs = []
all_ok = True
base_ctx = {"tenant_id": 7, "role": "student.pbls"}
probes = [
("未知工具必须 DENY", "no.such_tool", {}, base_ctx, "DENY", "G1_registered"),
("禁用工具必须 DENY", "rbac.grant_role", {"tenant_id": 7, "user_id": 1, "role_code": "admin.pbls"},
dict(base_ctx, role="owner.pbls"), "DENY", "G2_enabled"),
("缺 tenant 必须 DENY", "blueprint.get", {"blueprint_id": 1},
{"role": "student.pbls"}, "DENY", "G4_tenant_present"),
("student 调写工具必须 DENY", "blueprint.patch",
{"tenant_id": 7, "blueprint_id": 1, "obj_type": "goal", "payload": {}},
base_ctx, "DENY", "G3_role"),
("缺必填必须 DENY", "blueprint.get", {"tenant_id": 7}, base_ctx, "DENY", "G6_required"),
("跨租户必须 DENY", "blueprint.get", {"tenant_id": 8, "blueprint_id": 1},
base_ctx, "DENY", "G5_tenant_match"),
]
for title, name, args, ctx, want, want_gate in probes:
rec = judge(name, args, ctx)
got = rec["verdict"]
if got != want or (want == "DENY" and rec.get("failed_gate") != want_gate):
all_ok = False
msgs.append("探针[%s] FAILverdict=%s gate=%s(期望 %s/%s"
% (title, got, rec.get("failed_gate"), want, want_gate))
else:
msgs.append("探针[%s] PASS verdict=%s gate=%s" % (title, got, rec.get("failed_gate") or "-"))
# 合法读必须 ALLOWteacher 角色读 blueprint.get
rec = judge("blueprint.get", {"tenant_id": 7, "blueprint_id": 1},
{"tenant_id": 7, "role": "teacher.pbls"})
if rec["verdict"] != "ALLOW":
all_ok = False
msgs.append("探针[合法读必须 ALLOW] FAIL%s/%s" % (rec["verdict"], rec.get("failed_gate")))
else:
msgs.append("探针[合法读必须 ALLOW] PASS 九关全过")
# 配额关
rec = judge("blueprint.get", {"tenant_id": 7, "blueprint_id": 1},
{"tenant_id": 7, "role": "teacher.pbls"}, quota_used=MAX_CALLS_PER_RUN)
if rec["verdict"] != "DENY" or rec.get("failed_gate") != "G9_quota":
all_ok = False
msgs.append("探针[配额超限必须 DENY] FAIL%s/%s" % (rec["verdict"], rec.get("failed_gate")))
else:
msgs.append("探针[配额超限必须 DENY] PASS gate=G9_quota")
# 写保护域写工具必须 DENY即使 owner 角色)
wp = [t["name"] for t in tool_registry.all_tools()
if t.get("kind") == "write" and t.get("domain") in tool_registry.WRITE_PROTECTED_DOMAINS]
for name in wp:
rec = judge(name, {"tenant_id": 7}, {"tenant_id": 7, "role": "owner.pbls"})
if rec["verdict"] != "DENY":
all_ok = False
msgs.append("写保护域工具 %s 未被拒verdict=%s" % (name, rec["verdict"]))
if all_ok:
msgs.append("写保护域 %d 个写工具全部 DENY" % len(wp))
st = verdict_stats(rows=[{"verdict": "ALLOW"}, {"verdict": "DENY", "failed_gate": "G3_role"}])
if st["total"] != 2 or st["deny_rate"] != 0.5 or st["deny_by_gate"].get("G3_role") != 1:
all_ok = False
msgs.append("verdict_stats 口径错误:%s" % st)
else:
msgs.append("verdict_stats 口径正确:%s" % st)
reg_ok, reg_msgs = tool_registry.self_check()
if not reg_ok:
all_ok = False
msgs.extend(reg_msgs)
if all_ok:
msgs.append("SELF_CHECK pbl_agent_runtime.verdict: PASS (default=%s, max_calls=%d, policy=%s)"
% (DEFAULT_VERDICT, MAX_CALLS_PER_RUN, POLICY_VERSION))
return all_ok, msgs

View File

@ -1 +1,28 @@
352B pbl_common + pbl_blueprint
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
name = "pbl_agent_runtime"
version = "1.0.0"
description = "PBL Designer/Critic Agent 运行时与 fail-closed 工具裁决M4aAgent 定义 + 13/9 工具注册 + 8 步裁决 + 四类强制人工审批)"
readme = "README.md"
requires-python = ">=3.7"
license = { text = "Proprietary" }
authors = [{ name = "pbls dev team" }]
keywords = ["pbl", "agent-runtime", "designer", "critic", "fail-closed", "tool-adjudication"]
# 说明pbl_common / pbl_blueprint 为本机构 modules/ 下的基础包,
# 由宿主应用通过 load_{module}() 装配,不在 PyPI 发布,故不列入 dependencies
# (列入会导致 pip install 解析失败)。运行期无第三方 PyPI 依赖。
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=7.0"]
[tool.setuptools]
packages = ["pbl_agent_runtime"]
include-package-data = false
[tool.setuptools.package-data]
pbl_agent_runtime = ["../sql/*.sql"]

View File

@ -1,114 +0,0 @@
-- pbl_agent_runtime 表 DDL自动生成与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
CREATE TABLE IF NOT EXISTS `pbl_agent_def` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "Agent编码",
`agent_type` VARCHAR(64) NOT NULL COMMENT "类型",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`model_route` VARCHAR(64) NOT NULL COMMENT "pipeline-llm 路由",
`system_prompt_txt` TEXT NULL COMMENT "提示词",
`tool_whitelist_json` LONGTEXT NULL COMMENT "工具白名单",
`permission_mode` VARCHAR(64) NOT NULL COMMENT "read/write 模式",
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ad_code` (`tenant_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 定义Designer/Critic";
CREATE TABLE IF NOT EXISTS `pbl_tool_registry` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`agent_scope` VARCHAR(64) NOT NULL COMMENT "作用域",
`permission_required` VARCHAR(64) NOT NULL COMMENT "所需权限",
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`params_schema_json` LONGTEXT NULL COMMENT "参数白名单Schema",
`disabled_reason` VARCHAR(64) NOT NULL COMMENT "禁用原因",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tr_key` (`tenant_id`, `tool_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具注册表13 启用 / 9 禁用含 pbl.publish";
CREATE TABLE IF NOT EXISTS `pbl_agent_trace` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`trace_uid` VARCHAR(32) NOT NULL COMMENT "轨迹UID",
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "目标蓝图",
`input_ref_json` LONGTEXT NULL COMMENT "要素1 输入引用",
`thought_txt` TEXT NULL COMMENT "要素2 思考",
`action_code` VARCHAR(64) NOT NULL COMMENT "要素3 动作",
`action_params_json` LONGTEXT NULL COMMENT "要素4 动作参数",
`observation_json` LONGTEXT NULL COMMENT "要素5 观察",
`result_state` VARCHAR(64) NOT NULL COMMENT "要素6 结果状态",
`llm_calls` INT NOT NULL DEFAULT 0 COMMENT "要素7 LLM调用数",
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
`created_by` VARCHAR(32) NOT NULL COMMENT "触发用户",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_at_uid` (`tenant_id`, `trace_uid`),
KEY `idx_at_tenant` (`tenant_id`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 执行轨迹第28章 7 要素)";
CREATE TABLE IF NOT EXISTS `pbl_agent_tool_call` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`trace_id` BIGINT UNSIGNED NOT NULL COMMENT "轨迹ID",
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
`request_json` LONGTEXT NULL COMMENT "请求",
`response_json` LONGTEXT NULL COMMENT "响应",
`ok` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "成功",
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
`adjudication_code` VARCHAR(64) NOT NULL COMMENT "裁决结论",
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_tc_trace` (`tenant_id`, `trace_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具调用明细(服务端裁决留痕)";
CREATE TABLE IF NOT EXISTS `pbl_approval` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`approval_uid` VARCHAR(32) NOT NULL COMMENT "审批UID",
`approval_type` VARCHAR(64) NOT NULL COMMENT "类型",
`target_type` VARCHAR(64) NOT NULL COMMENT "对象类型",
`target_id` BIGINT UNSIGNED NOT NULL COMMENT "对象ID",
`payload_json` LONGTEXT NULL COMMENT "变更内容",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`requested_by` VARCHAR(32) NOT NULL COMMENT "发起人",
`decided_by` VARCHAR(32) NOT NULL COMMENT "决策人",
`decided_at` DATETIME NULL NULL COMMENT "决策时间",
`comment_txt` TEXT NULL COMMENT "意见",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ap_uid` (`tenant_id`, `approval_uid`),
KEY `idx_ap_status` (`tenant_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="人工审批单14.2 四类,无绕过路径)";
CREATE TABLE IF NOT EXISTS `pbl_llm_call_log` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`call_uid` VARCHAR(32) NOT NULL COMMENT "调用UID",
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
`route` VARCHAR(64) NOT NULL COMMENT "路由",
`model` VARCHAR(64) NOT NULL COMMENT "实际模型",
`prompt_hash` VARCHAR(32) NOT NULL COMMENT "提示词哈希",
`completion_tokens` INT NOT NULL DEFAULT 0 COMMENT "tokens",
`latency_ms` INT NOT NULL DEFAULT 0 COMMENT "时延",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
`fallback_used` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "走兜底",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_lc_uid` (`tenant_id`, `call_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="LLM 调用日志(超时/重试/限流/兜底留痕)";

View File

@ -1,4 +0,0 @@
# pbl_agent_runtime/api/pbl_tool_registry_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_agent_runtime/api/pbl_tool_registry_list.dspy: START params_kw={dict(params_kw)}')
data = await pbl_tool_registry_list(**params_kw)
return data

View File

@ -1,4 +0,0 @@
# pbl_agent_runtime/api/pbl_tool_registry_save.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_agent_runtime/api/pbl_tool_registry_save.dspy: START params_kw={dict(params_kw)}')
data = await pbl_tool_registry_save(**params_kw)
return data