将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。 同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
213 lines
10 KiB
Python
213 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
"""用 prompt 管理 writer 角色生成第 1 章基线候选。
|
||
|
||
第 1 章没有合法 asOf,不能构造完整 WriterContext,因此本脚本直接把冻结的
|
||
WriterCreativeInput 交给 muse_role。角色 prompt、schema、预算、上下文上限和模型策略由
|
||
RoleExecutionProfile 绑定;调用回执和候选仍落既有证据表。第 1 章不伪造机械/语义门通过,
|
||
正式采纳仍受 write_canonical 的实时状态门约束。
|
||
"""
|
||
import hashlib
|
||
import json
|
||
import sys
|
||
import uuid
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
|
||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||
AGENT_ROOT = SCRIPT_DIR.parents[1]
|
||
SKILLS = AGENT_ROOT / ".agent" / "skills"
|
||
for sub in (
|
||
"write-next-chapter/scripts",
|
||
"decide-candidate/scripts",
|
||
"assemble-context/scripts",
|
||
):
|
||
p = str(SKILLS / sub)
|
||
if p not in sys.path:
|
||
sys.path.insert(0, p)
|
||
|
||
from muse_db import connect # noqa: E402
|
||
from muse_role import run_role # noqa: E402
|
||
from run_writer import build_writer_execution_profile # noqa: E402
|
||
from writer_contract import han_count, normalize_text, validate_writer_draft # noqa: E402
|
||
from write_canonical import accept # noqa: E402
|
||
|
||
WORK_ID = 12
|
||
TARGET_CHAPTER = 1
|
||
# 冻结 writer 提示词:从 replay-eval config 读(不复制粘贴,sha256 可与配置核对)
|
||
GATE_A_CONFIG_PATH = (
|
||
SKILLS
|
||
/ "replay-writer-gate"
|
||
/ "configs"
|
||
/ "writer-gate-a-deep-space-v1.json"
|
||
)
|
||
GATE_A_WRITER_PROFILE = json.loads(GATE_A_CONFIG_PATH.read_text(encoding="utf-8"))[
|
||
"executionProfiles"]["writer"]
|
||
SYSTEM_PROMPT = GATE_A_WRITER_PROFILE["systemPrompt"]
|
||
SYSTEM_PROMPT_ID = GATE_A_WRITER_PROFILE["systemPromptId"]
|
||
SYSTEM_PROMPT_SHA256 = "sha256:" + hashlib.sha256(SYSTEM_PROMPT.encode("utf-8")).hexdigest()
|
||
assert SYSTEM_PROMPT_SHA256 == GATE_A_WRITER_PROFILE["systemPromptSha256"], \
|
||
"config 里的 systemPrompt 与其冻结 sha256 不一致,停止(事实来源已漂移)"
|
||
|
||
ARTIFACTS_DIR = SCRIPT_DIR / "artifacts"
|
||
|
||
# 第 1 章细纲硬骨架(与库内 planning section id=5 confirmed payload 同源)
|
||
HARD_CONSTRAINTS = [
|
||
"主角是天才机师,初次驾驶生物机甲。",
|
||
"深渊异族(克苏鲁式)首次出现,营造微恐/精神压迫。",
|
||
"建立黑暗森林/舰队/巨舰的世界观基调。",
|
||
"章末钩子:主角觉醒生物机甲的代价(精神污染的预兆)。",
|
||
]
|
||
ADJUSTABLE_BEATS = [
|
||
"开场节奏(侦察船日常 → 异常信号 → 异族现身)可调整。",
|
||
"精神压迫的描写强度可调整(微恐基调)。",
|
||
]
|
||
DECLARED_NEW_FACTS = [
|
||
{"factId": "fact-ch1-1", "text": "主角林深:天才机师。"},
|
||
{"factId": "fact-ch1-2", "text": "生物机甲:以异种核心驱动,机师与机甲精神链接。"},
|
||
{"factId": "fact-ch1-3", "text": "深渊异族:克苏鲁式,造成精神污染。"},
|
||
]
|
||
|
||
# 篇幅合同:细纲显式 targetChars=4000,区间按生产算法 = 目标值 ±30%(硬边界 2000-10000)
|
||
LENGTH_CONTRACT = {"targetChars": 4000, "minChars": 2800, "maxChars": 5200,
|
||
"frontmatterRequired": False}
|
||
|
||
# 文风约束:投影自设定包 style 字段(planning section id=3)。第 1 章无历史原文摘录,
|
||
# 文风画像是唯一文风锚点;生产投影从第 2 章起有 proseEvidence,本字段可为空。
|
||
STYLE_TEXT = ("冷峻、克制、微恐。战斗场面宏大(舰队/巨舰/机甲),心理描写细腻"
|
||
"(克苏鲁式精神压迫)。句式偏短,节奏紧。")
|
||
|
||
# 创作输入——严格按 build_writer_creative_input 的生产投影形状
|
||
CREATIVE_INPUT = {
|
||
"fineOutline": {
|
||
"hardConstraints": HARD_CONSTRAINTS,
|
||
"adjustableBeats": ADJUSTABLE_BEATS,
|
||
"declaredNewFacts": DECLARED_NEW_FACTS,
|
||
},
|
||
"narrativeState": {
|
||
"time": "故事开篇",
|
||
"location": "深渊星域,人类舰队侦察船",
|
||
"characterPositions": {"林深": "侦察船"},
|
||
"immediateSituation": "侦察船在深渊星域捕获异常信号,未知存在逼近。",
|
||
},
|
||
"factConstraints": [],
|
||
"proseExcerpts": [],
|
||
"patternReferences": [],
|
||
"lengthContract": LENGTH_CONTRACT,
|
||
"styleConstraints": [{"styleId": "style-baseline-v1", "text": STYLE_TEXT}],
|
||
}
|
||
|
||
|
||
def main():
|
||
run_id = f"run-prod-work12-ch1-{uuid.uuid4().hex[:8]}"
|
||
ARTIFACTS_DIR.mkdir(exist_ok=True)
|
||
(ARTIFACTS_DIR / f"{run_id}-creative-input.json").write_text(
|
||
json.dumps(CREATIVE_INPUT, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
profile = build_writer_execution_profile(
|
||
max_budget_usd_per_call=Decimal("5.0"),
|
||
timeout_seconds=1200,
|
||
max_context_chars=200000,
|
||
system_prompt=SYSTEM_PROMPT,
|
||
system_prompt_id=SYSTEM_PROMPT_ID,
|
||
)
|
||
|
||
print(
|
||
f"writer 角色调用开始: run_id={run_id}, 模型策略={profile.model_alias}, "
|
||
f"提示词={SYSTEM_PROMPT_ID}"
|
||
)
|
||
started = datetime.now()
|
||
invocation = run_role(
|
||
profile,
|
||
CREATIVE_INPUT,
|
||
business_validator=validate_writer_draft,
|
||
run_id=run_id,
|
||
caller="writer",
|
||
)
|
||
finished = datetime.now()
|
||
|
||
receipt = invocation.receipt
|
||
body = normalize_text(invocation.structured_output["candidateBody"])
|
||
actual_han = han_count(body)
|
||
candidate_sha256 = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||
|
||
print("── 回执 ──")
|
||
print(f" actual_model_id: {receipt.actual_model_id} model_match: {receipt.model_match}")
|
||
print(f" terminal_reason: {receipt.terminal_reason} stop_reason: {receipt.stop_reason} "
|
||
f"is_error: {receipt.is_error} exit_code: {receipt.exit_code}")
|
||
print(f" total_cost_usd: {receipt.total_cost_usd} usage: "
|
||
f"{json.dumps(dict(receipt.usage or {}), ensure_ascii=False)[:160]}")
|
||
print(f"── 候选 ──\n 汉字数: {actual_han}(合同 {LENGTH_CONTRACT['minChars']}"
|
||
f"-{LENGTH_CONTRACT['maxChars']},目标 {LENGTH_CONTRACT['targetChars']})")
|
||
print(f" sha256: {candidate_sha256[:16]}...")
|
||
print(f" 正文前 120 字: {body[:120]}")
|
||
|
||
# 落库前硬门:篇幅不出界才落库(3 字级垃圾不许入库污染看板)
|
||
if not LENGTH_CONTRACT["minChars"] <= actual_han <= LENGTH_CONTRACT["maxChars"]:
|
||
print(f"[拒绝落库] 汉字数 {actual_han} 超出篇幅合同,候选只留档 artifacts 不入库")
|
||
raise SystemExit(1)
|
||
if not receipt.model_match:
|
||
print("[拒绝落库] 实际模型不属于冻结治理策略")
|
||
raise SystemExit(1)
|
||
|
||
# ── 落库:run + 候选 + 回执 + 实跑长度检测(同事务)──
|
||
with connect() as conn:
|
||
conn.execute(
|
||
"INSERT INTO example_run(run_id, work_id, target_chapter, trigger_source, "
|
||
"trigger_detail, started_at, finished_at, terminal_state, creator) "
|
||
"VALUES (%s,%s,%s,'user',%s,%s,%s,'completed','writer')",
|
||
(run_id, WORK_ID, TARGET_CHAPTER,
|
||
json.dumps({"note": "第1章基线:writer 受治理角色调用;asOf 合同不适用,"
|
||
"未走生产组装与门",
|
||
"systemPromptId": SYSTEM_PROMPT_ID,
|
||
"systemPromptSha256": SYSTEM_PROMPT_SHA256,
|
||
"executionProfileSha256": receipt.execution_profile_sha256},
|
||
ensure_ascii=False),
|
||
started, finished))
|
||
cand_id = conn.execute(
|
||
"INSERT INTO example_candidate(work_id, target_chapter, run_id, attempt, run_type, "
|
||
"candidate_version, candidate_sha256, candidate_body, state, mode, source_role, "
|
||
"acceptance_eligible, creator) "
|
||
"VALUES (%s,%s,%s,1,'production','1',%s,%s,'passed','continuation','writer',true,"
|
||
"'writer') RETURNING id",
|
||
(WORK_ID, TARGET_CHAPTER, run_id, candidate_sha256, body)).fetchone()[0]
|
||
receipt_id = conn.execute(
|
||
"INSERT INTO example_run_receipt(run_id, sample_id, revision, adapter_role, "
|
||
"stage_kind, attempt, candidate_version, candidate_sha256, requested_model_id, "
|
||
"actual_model_id, model_match, effort, total_cost_usd, usage, stop_reason, "
|
||
"terminal_reason, is_error, creator) "
|
||
"VALUES (%s,'writer-ch1',1,'writer','generation',1,'1',%s,%s,%s,%s,%s,%s,"
|
||
"%s::jsonb,%s,%s,%s,'writer') RETURNING id",
|
||
(run_id, candidate_sha256, receipt.requested_model_id, receipt.actual_model_id,
|
||
receipt.model_match, receipt.effort, str(receipt.total_cost_usd),
|
||
json.dumps(dict(receipt.usage or {}), ensure_ascii=False),
|
||
receipt.stop_reason, receipt.terminal_reason,
|
||
bool(receipt.is_error))).fetchone()[0]
|
||
# 长度检测是脚本真跑过的机械检查,如实落账;语义门对基线章不适用,不伪造通过。
|
||
conn.execute(
|
||
"INSERT INTO example_quality_result(run_id, receipt_id, candidate_sha256, "
|
||
"judge_kind, dimension, scale_version, conclusion, detail, creator) "
|
||
"VALUES (%s,%s,%s,'detection','length','writer-production-v1','pass',%s::jsonb,"
|
||
"'writer')",
|
||
(run_id, receipt_id, candidate_sha256,
|
||
json.dumps({"actualHanChars": actual_han, **LENGTH_CONTRACT,
|
||
"note": "基线章:仅篇幅机械检查;无 WriterContext,语义门不适用"},
|
||
ensure_ascii=False)))
|
||
conn.commit()
|
||
print(f"候选落库: candidate_id={cand_id}, run_id={run_id}")
|
||
|
||
# 接受为正式正文(confirm 唯一通道)——第 1 章无正文块,expected_revision=0
|
||
accept_result = accept(
|
||
cand_id, decided_by="1",
|
||
rationale=("开篇基线章:writer 受治理角色调用产出,联合回执校验与篇幅"
|
||
"机械检查通过。第 1 章无前文,asOf 合同无解,生产组装与机械/语义门不适用;"
|
||
"文风锚定设定包 style。"),
|
||
basis_ref="细纲@v1 (planning_section id=5)",
|
||
expected_revision=0,
|
||
command_id=f"accept-work12-ch1-{run_id}")
|
||
print(f"接受结果: {json.dumps(accept_result, ensure_ascii=False)}")
|
||
print(f"\nCANDIDATE_ID={cand_id}\nRUN_ID={run_id}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|