zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

442 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

#!/usr/bin/env python3
"""用当前 writer prompt 合同刷新 provider-neutral 角色运行探针。
探针只使用固定合成输入,通过 muse_role.run_role 执行一次受治理角色调用,验证
profile、prompt、schema、输入、结构化输出、模型策略、成本和回执绑定。成功结果写到
新文件;任何失败都不修改原配置,也不输出 prompt、response 或 raw 路径。
"""
from __future__ import annotations
import argparse
import copy
import json
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any, Mapping, Protocol
SCRIPT_DIR = Path(__file__).resolve().parent
SKILLS_DIR = SCRIPT_DIR.parents[1]
GATE_ADJUDICATION_DIR = SKILLS_DIR / "adjudicate-quality-gate" / "scripts"
WRITER_REPLAY_DIR = SKILLS_DIR / "replay-writer-gate" / "scripts"
EVIDENCE_DIR = SKILLS_DIR / "record-run-evidence" / "scripts"
for _import_dir in (GATE_ADJUDICATION_DIR, WRITER_REPLAY_DIR, EVIDENCE_DIR):
if str(_import_dir) not in sys.path:
sys.path.insert(0, str(_import_dir))
from muse_role import ( # noqa: E402
HASH_PATTERN,
RUNTIME_ADAPTER,
RUNTIME_ADAPTER_VERSION,
RoleExecutionProfile,
RoleExecutionReceipt,
RoleInvocationResult,
RoleRuntimeError,
model_matches_profile,
run_role,
sha256_json,
)
from gate_input_builder import canonical_sha256 # noqa: E402
from persist_llm_call import persist_call as persist_llm_event # noqa: E402
from run_writer_replay import profile_from_mapping # noqa: E402
PROBE_BUSINESS_INPUT: dict[str, Any] = {
"fineOutline": {
"hardConstraints": ["本段为运行探针合成任务,不引用任何真实作品或真实正文。"],
"adjustableBeats": ["写一个守塔人在黎明前点亮灯塔的极短瞬间。"],
"declaredNewFacts": [],
},
"narrativeState": {
"asOfChapter": 0,
"summary": "合成探针场景:一座孤岛灯塔,黎明前。无任何真实作品人物或情节。",
},
"factConstraints": [],
"proseExcerpts": [],
"patternReferences": [],
"lengthContract": {
"targetChars": 60,
"minChars": 20,
"maxChars": 120,
"frontmatterRequired": False,
},
"styleConstraints": [],
}
DRY_RUN_CANDIDATE_BODY = (
"黎明前最暗的一刻,守塔人划亮火柴,灯芯燃起一小团光,海面被照出一线金边。"
)
PROBE_SCHEMA_VERSION = "runtime-probe-v2"
class ProbeRefreshError(RuntimeError):
"""携带稳定错误码的失败关闭错误,不附带模型原文。"""
def __init__(self, code: str, message: str) -> None:
super().__init__(f"{code}: {message}")
self.code = code
self.message = message
class ProbeInvoker(Protocol):
"""角色运行边界;真实实现是 run_role,测试可注入纯内存假实现。"""
def __call__(
self,
profile: RoleExecutionProfile,
business_input: Mapping[str, Any],
) -> RoleInvocationResult: ...
@dataclass(frozen=True)
class RefreshResult:
refreshed_config: dict[str, Any]
probe: dict[str, Any]
summary: dict[str, Any]
def build_writer_profile(config: Mapping[str, Any]) -> RoleExecutionProfile:
"""复用正式执行门的构造器重建 writer 冻结 profile。"""
profiles = config.get("executionProfiles")
if not isinstance(profiles, Mapping):
raise ProbeRefreshError("PROBE_CONFIG_INVALID", "配置缺少 executionProfiles")
return profile_from_mapping(profiles.get("writer"), role="writer")
def _validate_writer_output(value: Any) -> None:
"""独立复核 writer-draft-v2 的最小闭集结构。"""
if (
not isinstance(value, Mapping)
or set(value) != {"candidateBody"}
or not isinstance(value.get("candidateBody"), str)
or not value["candidateBody"]
):
raise ProbeRefreshError("PROBE_SCHEMA_INVALID", "结构化输出不符合 writer schema")
def verify_probe_result(
profile: RoleExecutionProfile,
result: RoleInvocationResult,
) -> None:
"""纵深复核角色调用结果,任一绑定不成立即失败关闭。"""
_validate_writer_output(result.structured_output)
receipt = result.receipt
if receipt.execution_profile_sha256 != profile.execution_profile_sha256:
raise ProbeRefreshError(
"PROBE_PROFILE_BINDING_MISMATCH",
"回执身份哈希与当前 writer profile 不一致",
)
if receipt.json_schema_sha256 != profile.json_schema_sha256:
raise ProbeRefreshError("PROBE_SCHEMA_BINDING_MISMATCH", "回执 schema 哈希不一致")
if receipt.input_sha256 != sha256_json(PROBE_BUSINESS_INPUT):
raise ProbeRefreshError("PROBE_INPUT_HASH_INVALID", "回执未绑定固定探针输入")
output_hash = receipt.structured_output_sha256
if (
not isinstance(output_hash, str)
or not HASH_PATTERN.fullmatch(output_hash)
or output_hash != sha256_json(result.structured_output)
):
raise ProbeRefreshError(
"PROBE_OUTPUT_HASH_INVALID",
"结构化输出哈希缺失、非法或与产出不一致",
)
if (
receipt.requested_model_id != profile.model_alias
or receipt.model_match is not True
or not model_matches_profile(profile, receipt.actual_model_id)
):
raise ProbeRefreshError("PROBE_MODEL_MISMATCH", "实际模型不属于冻结治理策略")
if receipt.is_error is not False:
raise ProbeRefreshError("PROBE_RECEIPT_ERROR", "回执标记为错误")
if receipt.exit_code is not None:
raise ProbeRefreshError("PROBE_TRANSPORT_INVALID", "角色运行回执不得携带进程退出码")
if receipt.terminal_reason != "completed":
raise ProbeRefreshError("PROBE_TERMINAL_REASON_INVALID", "角色调用未正常完成")
if receipt.api_error_status is not None:
raise ProbeRefreshError("PROBE_API_ERROR", "回执携带 API 错误状态")
if receipt.total_cost_usd is None:
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "回执缺少可信成本")
try:
cost = Decimal(receipt.total_cost_usd)
except (InvalidOperation, ValueError) as exc:
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "回执成本不可解析") from exc
if not cost.is_finite() or cost < 0 or cost > profile.max_budget_usd_per_call:
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "单次成本超过冻结预算 cap")
def build_probe_record(
profile: RoleExecutionProfile,
result: RoleInvocationResult,
*,
checked_at: str,
) -> dict[str, Any]:
"""构建 runtime-probe-v2,并使用 Gate 同源算法计算自哈希。"""
receipt = result.receipt
record: dict[str, Any] = {
"schemaVersion": PROBE_SCHEMA_VERSION,
"status": "successful",
"checkedAt": checked_at,
"runtimeAdapter": RUNTIME_ADAPTER,
"runtimeAdapterVersion": RUNTIME_ADAPTER_VERSION,
"modelPolicyVersion": profile.model_policy_version,
"role": profile.adapter_role,
"profileVersion": profile.profile_version,
"modelAlias": profile.model_alias,
"resolvedModelId": profile.resolved_model_id,
"executionProfileSha256": receipt.execution_profile_sha256,
"jsonSchemaId": profile.json_schema_id,
"jsonSchemaSha256": profile.json_schema_sha256,
"systemPromptId": profile.system_prompt_id,
"systemPromptSha256": profile.system_prompt_sha256,
"inputSha256": receipt.input_sha256,
"requestedModelId": receipt.requested_model_id,
"actualModelId": receipt.actual_model_id,
"modelMatch": receipt.model_match,
"executionReceiptSha256": canonical_sha256(receipt.as_dict()),
"structuredOutputSha256": receipt.structured_output_sha256,
"terminalReason": receipt.terminal_reason,
"totalCostUsd": receipt.total_cost_usd,
}
record["receiptSha256"] = canonical_sha256(record)
return record
def apply_probe(config: Mapping[str, Any], probe: Mapping[str, Any]) -> dict[str, Any]:
"""只替换深拷贝配置的 runtimeProbe;其它授权记录保持不变。"""
authorization = config.get("executionAuthorization")
if not isinstance(authorization, Mapping):
raise ProbeRefreshError("PROBE_CONFIG_INVALID", "配置缺少 executionAuthorization")
refreshed = copy.deepcopy(dict(config))
refreshed["executionAuthorization"] = copy.deepcopy(dict(authorization))
refreshed["executionAuthorization"]["runtimeProbe"] = copy.deepcopy(dict(probe))
return refreshed
def _audit_summary(
profile: RoleExecutionProfile,
probe: Mapping[str, Any],
*,
dry_run: bool,
) -> dict[str, Any]:
"""生成不含 prompt、response 和 raw 的审计摘要。"""
return {
"tool": "refresh_runtime_probe",
"dryRun": dry_run,
"status": probe.get("status"),
"runtimeAdapterVersion": RUNTIME_ADAPTER_VERSION,
"modelPolicyVersion": profile.model_policy_version,
"executionProfileSha256": probe.get("executionProfileSha256"),
"structuredOutputSha256": probe.get("structuredOutputSha256"),
"executionReceiptSha256": probe.get("executionReceiptSha256"),
"probeReceiptSha256": probe.get("receiptSha256"),
"totalCostUsd": probe.get("totalCostUsd"),
"modelAlias": profile.model_alias,
"actualModelId": probe.get("actualModelId"),
"checkedAt": probe.get("checkedAt"),
}
def refresh_runtime_probe(
config: Mapping[str, Any],
*,
invoker: ProbeInvoker,
checked_at: str,
dry_run: bool = False,
) -> RefreshResult:
"""重建 profile、调用角色、验证结果、重签探针并返回新配置。"""
profile = build_writer_profile(config)
result = invoker(profile, PROBE_BUSINESS_INPUT)
verify_probe_result(profile, result)
probe = build_probe_record(profile, result, checked_at=checked_at)
if dry_run:
# dry-run 只证明本地构建/重签链可运行,不能冒充真实模型能力证明。
probe["status"] = "dry_run"
probe["receiptSha256"] = canonical_sha256(
{key: value for key, value in probe.items() if key != "receiptSha256"}
)
refreshed = apply_probe(config, probe)
return RefreshResult(
refreshed_config=refreshed,
probe=probe,
summary=_audit_summary(profile, probe, dry_run=dry_run),
)
def make_role_invoker(*, run_id: str) -> ProbeInvoker:
"""构造真实角色 invoker,并把探针调用纳入统一运行证据。"""
def _invoke(
profile: RoleExecutionProfile,
business_input: Mapping[str, Any],
) -> RoleInvocationResult:
return run_role(
profile,
business_input,
run_id=run_id,
caller="refresh-runtime-probe",
persist_call=persist_llm_event,
)
return _invoke
def make_dry_run_invoker() -> ProbeInvoker:
"""构造不访问模型的固定 invoker,仅用于验证配置与重签链。"""
def _dry_run_invoker(
profile: RoleExecutionProfile,
business_input: Mapping[str, Any],
) -> RoleInvocationResult:
structured_output = {"candidateBody": DRY_RUN_CANDIDATE_BODY}
receipt = RoleExecutionReceipt(
adapter_role=profile.adapter_role,
invocation_id="dry-run-probe",
execution_profile_sha256=profile.execution_profile_sha256,
requested_model_id=profile.model_alias,
actual_model_id=profile.resolved_model_id,
model_match=True,
effort="governed",
max_budget_usd_per_call=format(profile.max_budget_usd_per_call, "f"),
total_cost_usd="0.001000",
usage={"input_tokens": 1, "output_tokens": 1},
model_usage={profile.resolved_model_id: {"costUSD": "0.001000"}},
stop_reason="stop",
terminal_reason="completed",
is_error=False,
api_error_status=None,
exit_code=None,
duration_ms=1,
input_sha256=sha256_json(business_input),
structured_output_sha256=sha256_json(structured_output),
json_schema_sha256=profile.json_schema_sha256,
)
return RoleInvocationResult(structured_output=structured_output, receipt=receipt)
return _dry_run_invoker
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def _write_json(path: Path, value: Mapping[str, Any]) -> None:
path.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def main(argv: list[str] | None = None) -> int:
"""读取 base 配置并把刷新结果写到新文件。"""
parser = argparse.ArgumentParser(
description="按当前 writer prompt 合同重测角色能力探针并刷新配置授权。"
)
parser.add_argument("--config", required=True, help="base 配置路径(只读)")
parser.add_argument("--output", required=True, help="刷新后配置的新文件路径")
parser.add_argument(
"--dry-run",
action="store_true",
help="用固定假产出验证 profile、探针和写文件链,不调用模型",
)
parser.add_argument("--checked-at", default=None, help="显式注入 checkedAt")
args = parser.parse_args(argv)
config_path = Path(args.config)
output_path = Path(args.output)
if output_path.resolve() == config_path.resolve():
print(json.dumps({
"tool": "refresh_runtime_probe",
"error": "PROBE_OUTPUT_SAME_AS_CONFIG",
"message": "--output 必须是不同于 --config 的新文件",
}, ensure_ascii=False))
return 2
try:
config = json.loads(config_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
print(json.dumps({
"tool": "refresh_runtime_probe",
"error": "PROBE_CONFIG_UNREADABLE",
"message": "base 配置读取失败",
}, ensure_ascii=False))
return 2
checked_at = args.checked_at or _now_iso()
invoker: ProbeInvoker = (
make_dry_run_invoker()
if args.dry_run
else make_role_invoker(run_id=f"runtime-probe:{checked_at}")
)
try:
result = refresh_runtime_probe(
config,
invoker=invoker,
checked_at=checked_at,
dry_run=args.dry_run,
)
except ProbeRefreshError as exc:
print(json.dumps({
"tool": "refresh_runtime_probe",
"error": exc.code,
"message": exc.message,
"dryRun": args.dry_run,
"status": "failed_closed",
}, ensure_ascii=False))
return 1
except RoleRuntimeError as exc:
print(json.dumps({
"tool": "refresh_runtime_probe",
"error": exc.code,
"message": "角色调用失败关闭",
"dryRun": args.dry_run,
"status": "failed_closed",
}, ensure_ascii=False))
return 1
try:
_write_json(output_path, result.refreshed_config)
except OSError:
print(json.dumps({
"tool": "refresh_runtime_probe",
"error": "PROBE_OUTPUT_UNWRITABLE",
"message": "刷新配置写出失败",
"dryRun": args.dry_run,
"status": "failed_closed",
}, ensure_ascii=False))
return 1
summary = dict(result.summary)
summary["outputFile"] = str(output_path)
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
return 0
__all__ = [
"DRY_RUN_CANDIDATE_BODY",
"PROBE_BUSINESS_INPUT",
"PROBE_SCHEMA_VERSION",
"ProbeInvoker",
"ProbeRefreshError",
"RefreshResult",
"apply_probe",
"build_probe_record",
"build_writer_profile",
"make_dry_run_invoker",
"make_role_invoker",
"main",
"refresh_runtime_probe",
"verify_probe_result",
]
if __name__ == "__main__":
raise SystemExit(main())