主世界层直接写正文,转述与旁观改读用户展示输出。
去掉裁决包主路径,让无转述时可直出终稿、有转述时再改写镶壳。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -234,7 +234,7 @@ value, rev, updatedAt, source # source: user | worker:<id> | system
|
||||
|
||||
### 5.4 维护 worker
|
||||
|
||||
读:正文/裁决、维护规则、当前表 → 写:字段更新。
|
||||
读:本轮正文、维护规则、当前表 → 写:字段更新。
|
||||
可不写用户终稿;可与展示链解耦、**延后**跑。
|
||||
|
||||
### 5.5 副作用(含大纲、人设、事件)
|
||||
@@ -479,7 +479,7 @@ phase:core(核心体验 + 交互)
|
||||
| 意图 | 典型停点 |
|
||||
|------|----------|
|
||||
| 自主长篇 | 大纲 / 主题 worker → `review`;正文链多段 → `continue`,直到用户叫停或大纲段落再 `review` |
|
||||
| 交互 / RP | 产出用户可见终稿的转述(如 narrator)→ `review`;世界裁决等中间层 → `continue` |
|
||||
| 交互 / RP | 产出用户可见终稿的槽(无转述=主世界层;有转述=narrator)→ `review`;有转述时主世界草稿 → `continue` |
|
||||
|
||||
创作期按 **§7.2 创作单位** 分块验收(worker 与固定上下文同级),与上表正交。
|
||||
design-flow / design-step / 创作调度必须为 **每个** run worker 写出 `acceptance`;缺省时不得假设「全都不用验收」——面向用户的可读输出默认倾向 `review`,纯中间层倾向 `continue`,吃不准就 ask_user。
|
||||
|
||||
+14
-39
@@ -7,69 +7,44 @@
|
||||
|
||||
| 称呼 | 槽 | 干什么 | 不干什么 |
|
||||
|------|-----|--------|----------|
|
||||
| **主世界层(DM)** | gm | 读设定与当前状态,对本轮玩家行动做**裁决**;可提议改变量 | 不负责长文文风;不靠「再想一遍」修表 |
|
||||
| **旁观维护(副 LLM)** | auditor | **查漏补缺**:表缺字段、该不该按生成规则补一条实例;多数轮**空操作** | 不当作者写正文;不替代裁决 |
|
||||
| **主世界层(DM)** | gm | 读设定与当前状态,推进场面,直接写**故事正文(Markdown)** | 不交 JSON 裁决包;不靠结构化包改表 |
|
||||
| **旁观维护(副 LLM)** | auditor | **查漏补缺**:读本轮正文推断表变更、该不该按生成规则补实例;多数轮**空操作** | 不当作者写正文;不替代主世界叙事 |
|
||||
|
||||
转述只吃裁决包写用户可见文字——与「俩人怎么配合」无关。
|
||||
转述(若启用)读主世界正文:提取场面、按文风改写、按壳镶前端;可补残稿,不改剧情。
|
||||
|
||||
## 2. 你提到的两种做法
|
||||
|
||||
**做法甲:DM 先提要求 → 副 LLM 交作业 → DM 再过一遍**
|
||||
|
||||
```text
|
||||
DM:「按怪物规则补 1 个」→ 旁观生成 → DM 再裁决一次
|
||||
DM:「按怪物规则补 1 个」→ 旁观生成 → DM 再写一遍
|
||||
```
|
||||
|
||||
- 优点:DM 始终握笔。
|
||||
- 缺点:同一回合容易 **3 次模型调用**,贵、慢;旁观一不小心变成「小作者」。
|
||||
|
||||
**做法乙:DM 先交齐裁决 → 把结果和要求交给副 LLM → 副 LLM 用工具改表**
|
||||
**做法乙:DM 先交齐正文 → 副 LLM 根据正文改表**
|
||||
|
||||
```text
|
||||
DM 出裁决包(含变量变更)→ 旁观只动表/触发生成 → 程序合并,不再开一轮 DM
|
||||
DM 写故事正文 →(可选转述润色)→ 旁观只动表/触发生成 → 程序合并,不再开一轮 DM
|
||||
```
|
||||
|
||||
- 优点:叙事权威仍在 DM;旁观像**库管/校对**,适合工具改表。
|
||||
- 缺点:若裁决里已经写错事实,旁观「修表」救不了叙事,只能修状态。
|
||||
- 优点:叙事权威仍在 DM;旁观像**库管/校对**。
|
||||
- 缺点:若正文写错事实,旁观「修表」救不了叙事,只能修状态。
|
||||
|
||||
## 3. 本仓库采用的(推荐)
|
||||
|
||||
**采用做法乙:主世界层先拍板 → 转述 → 旁观在回合末改表(合并结果供下一轮主世界层读取)。**
|
||||
**采用做法乙:主世界层先写正文 →(可选)转述 → 旁观在回合末改表(合并结果供下一轮主世界层读取)。**
|
||||
|
||||
```text
|
||||
每轮:
|
||||
1. 主世界层:读上一轮合并后的 变量.当前 + 设定 + 历史 + 用户输入
|
||||
→ 出 settlement 裁决包(可含 variable_changes;可经 harness 调机遇工具)
|
||||
2. 程序:合并裁决中的 variable_changes → 变量.当前(边沿副作用照常)
|
||||
3. 叙事转述(若启用):只读裁决包写用户可见正文
|
||||
4. 旁观维护:读本轮裁决 + 合同 + 表
|
||||
→ 直接写 Markdown 故事正文(输出.用户展示;可经 harness 调机遇工具)
|
||||
2. 叙事转述(若启用):读主世界正文 → 提取/改写/镶壳 → 覆盖 输出.用户展示
|
||||
3. 旁观维护:读本轮正文 + 合同 + 表
|
||||
→ 多数轮:空 maintain
|
||||
→ 少数轮:table_ops / need_generate
|
||||
5. 程序:合并旁观 table_ops → 变量.当前(**下一轮**主世界层才读到)
|
||||
4. 程序:合并旁观 table_ops → 变量.当前(**下一轮**主世界层才读到)
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
1. **不要**默认「旁观先扫再让 DM 裁决」(旧顺序已废弃)。
|
||||
2. **不要**默认「DM 写完再让旁观润色全文」(贵,且旁观会抢笔)。
|
||||
3. **不要**默认「旁观先写一大段设定再交给 DM」(旁观变作者)。
|
||||
4. 生成规则给旁观:只要**合同**(有哪些 rule、格式、何时该生成),不要整本描写范文。
|
||||
5. 真要「按规则生成实例」:旁观点 `need_generate.rule_id`,由**程序/既定生成通路**落表,而不是旁观写小说。
|
||||
|
||||
## 4. 和「生成规则要不要给副 LLM」的关系
|
||||
|
||||
问题本质不是「塞不塞那一份文档」,而是:
|
||||
|
||||
- 旁观要不要能说「该按某条规则补东西」?→ **要**,就得看见规则目录/格式。
|
||||
- 旁观要不要会写角色描写?→ **不要**,所以描写长文不必进旁观上下文。
|
||||
|
||||
## 5. 上下文与思考流(UI 对接)
|
||||
|
||||
主世界层游玩期走 **gm harness**(tool loop):与普通 worker 相同,经 `onThinkingDelta` / `onOutputDelta` 把推理与正文增量推到前端;机遇工具往返也在同一气泡时间线内展示,不另开隐藏通道。
|
||||
|
||||
转述、旁观仍各是一次 worker 调用,各自独立流式输出;**不要**把旁观维护当成 gm 的 tool。
|
||||
|
||||
## 6. 一句话
|
||||
|
||||
**主世界层断事;转述说话;旁观在回合末对表,改表给下一轮。**
|
||||
交互上优先「一次裁决 + 延后轻维护」,不优先「来回多轮 LLM 传话」。
|
||||
无转述时:主世界正文即用户终稿,往往已够用。
|
||||
|
||||
@@ -93,8 +93,8 @@
|
||||
|
||||
设计.监控栏 → 回复呈现清单(看什么)
|
||||
设计.正文组成 → 壳 + 美化(怎么展示)
|
||||
运行.本轮.裁决 → body 事实;suggested_actions → 行动区
|
||||
输出.用户展示 → present.v1 或纯 Markdown(→ body)
|
||||
输出.用户展示(主世界正文)→ 转述提取/改写后的 body;suggested_actions → 行动区
|
||||
输出.用户展示(终稿)→ present.v1 或纯 Markdown(→ body)
|
||||
```
|
||||
|
||||
## 5. 游玩灌数契约
|
||||
|
||||
@@ -94,10 +94,10 @@ Progressive 投影 本轮主世界层只看见「当前档」;未
|
||||
|
||||
```text
|
||||
用户输入
|
||||
→ 主世界层(世界推演):读变量 + 已投影 data,交事件/改真值(按规则);可经 harness 批量调 chance 工具
|
||||
→ 主世界层(世界推演):读变量 + 已投影 data,写本轮故事正文(Markdown);可经 harness 批量调 chance 工具
|
||||
→(可选)角色视角:强信息隔离时出反应建议(默认关,插在主世界层之后)
|
||||
→(可选)叙事转述:把裁决写成用户可见正文
|
||||
→ 旁观维护(副 LLM):合同+表+本轮裁决;默认空操作(maintain.v1);改表供下一轮主世界层读取
|
||||
→(可选)叙事转述:读主世界正文,提取/改写/镶壳为用户终稿(可补残稿,不改剧情)
|
||||
→ 旁观维护(副 LLM):合同+表+本轮正文;默认空操作(maintain.v1);改表供下一轮主世界层读取
|
||||
```
|
||||
|
||||
| 内容 | 给谁 | 不要做成 |
|
||||
@@ -111,13 +111,13 @@ Progressive 投影 本轮主世界层只看见「当前档」;未
|
||||
|
||||
用户可见名:`ui-glossary.md`(主世界层、旁观维护、叙事转述…)。作者文档可称 **主世界层 / 旁观维护**,与酒馆「主 GM + auditor」同构;**不要**对用户改称整套系统为「GM 框架」而废弃「执行单元」。
|
||||
|
||||
**维护路径(不是 gm 的 chance toolcall)**:主世界层输出 settlement.v1 → Runtime 合并 `variable_changes` 进 `变量.当前` → `side_effects` 换投影;旁观维护输出 maintain.v1 → Runtime 按需合并 `table_ops` / `need_generate` 程序抽样。gm harness 的 `chance` 只负责本回合裁决前的骰子/抽签。
|
||||
**维护路径(不是 gm 的 chance toolcall)**:主世界层写正文 → 旁观维护输出 maintain.v1 → Runtime 按需合并 `table_ops` / `need_generate` 程序抽样 → `side_effects` 换投影。可选正文隐藏段维护语句。遗留 settlement.v1 仍可合并(旧存档)。gm harness 的 `chance` 只负责本回合的骰子/抽签。
|
||||
|
||||
## 6. 世界模拟器技能落点
|
||||
|
||||
| 技能 | 钉什么 |
|
||||
|------|--------|
|
||||
| **变量设计与更新规则** | 真值;**维护语句**形状(settlement / 隐藏段);**Data 映射索引**(指向生成规则+具体实例);`side_effects` |
|
||||
| **变量设计与更新规则** | 真值;**维护语句**形状(旁观 maintain / 隐藏段);**Data 映射索引**(指向生成规则+具体实例);`side_effects` |
|
||||
| **变量控制上下文** | 各槽视野;**旁观汇总**(当前档摘要,非整表);剧透边界 |
|
||||
| **生成规则 / 具体实例** | 分档表 schema 与行数据(如好感→性格);生成后由变量设计索引 |
|
||||
| **正文组成** | 隐藏段定界(供解析维护语句);用户可见块序 |
|
||||
|
||||
@@ -56,9 +56,9 @@ feeds: design_only
|
||||
2. 禁止重写上游长文;只写顺序与投影。
|
||||
3. 每轮槽:order 0=该槽固定人设;其后按「稳→对话.历史→易变」软排。
|
||||
4. 主世界层必须含 ref=`对话.历史`(默认 projection=summary);转述与旁观维护默认不含历史。
|
||||
5. 主世界层应双挂「设计.叙事指南与故事推进」等 mount 到 gm 的块;有「设计.随机范围整理」则挂 gm(summary 即可);转述只读裁决+文风/壳。
|
||||
6. 旁观维护在回合末:须含「运行.本轮.裁决」与变量合同,不含历史。
|
||||
7. 越稳越靠前;本轮输入/真值/裁决通常在历史之后。
|
||||
5. 主世界层应双挂「设计.叙事指南与故事推进」等 mount 到 gm 的块;有「设计.随机范围整理」则挂 gm(summary 即可);转述只读本轮正文(输出.用户展示)+ 文风/壳。
|
||||
6. 旁观维护在回合末:须含「输出.用户展示」与变量合同,不含历史。
|
||||
7. 越稳越靠前;本轮输入/真值/正文通常在历史之后。
|
||||
8. 同一内容可挂多槽。
|
||||
9. summary:如 `投影排序 · 旁观+主世界层+转述`。
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ boundary: |
|
||||
```principles
|
||||
1. 合并优于重写;槽位优于发明演员。
|
||||
2. 面向用户的终稿点 acceptance=review(默认是 world-simulator;开了转述则是 narrator;写手是 chapter-writer);旁观/outline 常用 continue;有转述时 gm 用 continue。
|
||||
3. 真值变更写在 gm 的 outputs(运行.本轮.变量变更 / 裁决包内 variable_changes),旁观维护只出 maintain.v1;不靠第三变量 Worker。
|
||||
4. 裁决包约定:启用转述时,运行.本轮.裁决 使用 settlement.v1(见 progressive-data-design / 模板);Runtime 合并 variable_changes。无转述时主世界层直接写 输出.用户展示。
|
||||
3. 真值变更主路径:旁观维护 maintain.v1(读本轮正文改表);可选正文隐藏段维护语句。主世界层只写故事正文,不交 settlement 裁决包。
|
||||
4. 正文约定:主世界层始终写「输出.用户展示」(Markdown)。无转述时原文即终稿;有转述时转述读该正文再提取/改写/镶壳并覆盖终稿。
|
||||
5. 键名稳定;未决进 open_questions。
|
||||
```
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
"拼装方": "叙事转述一次写出各块|程序按块拼接|混合",
|
||||
"推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown",
|
||||
"单轮正文篇幅": "以回复呈现的字数为准,本步不重钉",
|
||||
"与裁决包关系": "转述读 settlement 再填各块;隐藏段可含 variable_changes 镜像"
|
||||
"与正文关系": "转述读主世界「输出.用户展示」再填各块;可补残稿与版式,不改剧情;隐藏段可供旁观解析维护语句"
|
||||
},
|
||||
"示例": {
|
||||
"性质": "验收预览|非正史|仅看壳与美化",
|
||||
|
||||
@@ -37,7 +37,7 @@ feeds: gm
|
||||
|
||||
1. 数字/阶段?(好感、章节、资源…)一次性门?
|
||||
2. 有没有「到了某值换一套态度/大纲」的表?(表正文可已在具体实例里)
|
||||
3. 每轮模型用什么方式声明变量变了?(裁决包字段 / 隐藏段键值 / 两者)
|
||||
3. 每轮如何声明变量变了?(旁观维护 maintain.v1 / 正文隐藏段键值 / 两者)
|
||||
4. 旁观或主世界层是否需要一份「当前映射摘要」(如好感档→性格要点)?
|
||||
```
|
||||
|
||||
@@ -51,14 +51,14 @@ feeds: gm
|
||||
- **Data 映射**:阈值→切片大表(性格档、章纲、事件池)——格式归生成规则,行归具体实例;本步建索引并指向实例/规则 id
|
||||
- **Progressive**:side_effects 按真值边沿投影当前切片;旁观/主世界层读投影或汇总,不整表常驻
|
||||
|
||||
**维护语句**:规定模型每轮如何声明变更(供程序拆进 `变量.当前` / 裁决包 `variable_changes`)。
|
||||
**维护语句**:规定如何声明变更(供程序拆进 `变量.当前`)。主路径是旁观维护 `maintain.v1`(读本轮正文推断 table_ops);可选正文隐藏段键值。主世界层不交 settlement 裁决包。
|
||||
这不是掷骰;随机数走「随机范围整理」+ 程序插入的备用数。隐藏段定界与「正文组成」对齐。
|
||||
|
||||
执行顺序:
|
||||
1. 读美学/机制/生成规则/具体实例/正文组成;不重开体验。
|
||||
2. 立真值(承重才建);写写入源与更新规则。
|
||||
3. 建 Data 映射索引:每张表写清键依哪个真值、材料来自哪条 rule_id/batch、给旁观/投影用的摘要字段。
|
||||
4. 写维护语句约定:形状、必填键、与隐藏段/settlement 的关系、示例一句。
|
||||
4. 写维护语句约定:形状、必填键、与隐藏段/旁观维护的关系、示例一句。
|
||||
5. 写 side_effects(once/every_edge → replace_tag 等)。
|
||||
6. 自评 + 追问;输出 JSON。
|
||||
|
||||
@@ -71,7 +71,7 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。
|
||||
1. 承重才立真值;派生性格不落第二可写格。
|
||||
2. 映射内容:具体实例生成 → 本步索引汇总 → side_effects/变量控制上下文供给旁观与主世界层。
|
||||
3. 维护靠「声明 + 程序合并」,不靠模型直接改库:
|
||||
- 主路径:裁决包 variable_changes 和/或正文隐藏段(正则/定界)→ Runtime 合并进 变量.当前
|
||||
- 主路径:旁观维护 maintain.v1(读本轮正文)和/或正文隐藏段(正则/定界)→ Runtime 合并进 变量.当前
|
||||
- 边沿:side_effects 换投影 tag
|
||||
- 用户手改;chance 只出随机数,不替代维护语句
|
||||
4. 维护语句必须可生成、可校验:固定键名、短值、禁止散文糊弄。
|
||||
@@ -82,7 +82,7 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点:主真值;连续数字还是阶段枚举;维护走裁决包还是隐藏段还是两者;映射表是否已有具体实例可索引;旁观要不要当前档摘要。
|
||||
每轮 1~2 点:主真值;连续数字还是阶段枚举;维护走旁观 maintain / 隐藏段 / 两者;映射表是否已有具体实例可索引;旁观要不要当前档摘要。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -127,18 +127,18 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。
|
||||
}
|
||||
],
|
||||
"维护语句约定": {
|
||||
"通道": ["settlement.variable_changes", "隐藏段", "二者都要"],
|
||||
"与正文组成对齐": "隐藏段段id / 定界约定;无隐藏段则写「仅用裁决包」",
|
||||
"通道": ["旁观maintain.v1", "隐藏段", "二者都要"],
|
||||
"与正文组成对齐": "隐藏段段id / 定界约定;无隐藏段则写「仅旁观维护」",
|
||||
"语句形状": {
|
||||
"格式": "JSON行|键值列表|与 settlement 字段同构",
|
||||
"格式": "maintain.v1 table_ops|隐藏段键值列表",
|
||||
"必填键": ["key", "delta|to"],
|
||||
"禁止": ["无键散文", "改未立真值"]
|
||||
"禁止": ["无键散文", "改未立真值", "主世界层交 settlement JSON"]
|
||||
},
|
||||
"生成要求": "主世界层每轮须输出可解析维护语句(哪怕本轮无变更也给空列表)",
|
||||
"生成要求": "旁观维护按本轮正文推断变更(多数轮可空);主世界层专注故事正文",
|
||||
"示例": [
|
||||
{ "key": "好感", "delta": 8, "reason": "送礼成功" }
|
||||
],
|
||||
"程序侧": "Runtime 合并进 变量.当前;再跑 side_effects;非 chance toolcall"
|
||||
"程序侧": "Runtime 合并 maintain.v1 → 变量.当前;再跑 side_effects;非 chance toolcall"
|
||||
},
|
||||
"side_effects": [
|
||||
{
|
||||
@@ -208,7 +208,7 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。
|
||||
```checklist
|
||||
- [ ] 真值有写入源与更新时机?
|
||||
- [ ] 映射索引指向规则/实例或标待补?旁观摘要键清楚?
|
||||
- [ ] 维护语句可解析且与正文组成/settlement 对齐?
|
||||
- [ ] 维护语句可解析且与正文组成/旁观维护对齐?
|
||||
- [ ] 未把分档长文做成可写真值?未用 chance 当改表?
|
||||
```
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ boundary: |
|
||||
|
||||
世界模拟类常见:
|
||||
1. 主世界层(推进 + 默认直接写可见正文)——要(几乎总要)
|
||||
2. 叙事转述(独立文风层再改写正文)——要 / 不要(默认不要)
|
||||
2. 叙事转述(读主世界正文再改写/镶壳;可补残稿)——要 / 不要(默认不要)
|
||||
3. 旁观维护(有变量/表/规则要盯时才开;默认不要)——要 / 不要
|
||||
4. 角色视角(仅当有强秘密、不能进主世界层时)——要 / 不要(默认不要)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ principles:
|
||||
- 有编排参数的步骤:进执行前钉齐 params(askUser 选项+其它);缺参时先问再进 design-step。
|
||||
- 〔可反复〕可再追加(彻底新建);已验收 id 留在 steps。
|
||||
- 舞台骨架通常都排(备案,只分详细度);代入后几乎不移动、只在一处待着则不排。实现机制对照其 meta。
|
||||
- 百科与查表归主世界层;默认可见正文也归主世界层;仅显式开转述时正文才改由转述写。
|
||||
- 百科与查表归主世界层;可见正文默认由主世界层直接写;显式开转述时转述在主世界正文上改写/镶壳。
|
||||
|
||||
brief: 世界模拟类:美学卡上在意的部分→挂槽写上下文→晚排序投影→收成运行规格
|
||||
|
||||
|
||||
@@ -25,14 +25,14 @@ Runtime 执行时读 Worker 集条目,不直接读本目录。
|
||||
|
||||
| 槽 | 默认 ref | 模板 |
|
||||
|----|----------|------|
|
||||
| auditor | `auditor` | `auditor.yaml` — maintain.v1;默认空操作;无长对话史(默认关) |
|
||||
| gm | `world-simulator` | `world-simulator.yaml` — 默认直接写用户可见正文;开转述时改出 settlement.v1;`play_slots.chance` 开时 harness 批量机遇工具 |
|
||||
| narrator | `narrator` | `narrator.yaml` — 只读裁决包写正文(默认关) |
|
||||
| auditor | `auditor` | `auditor.yaml` — maintain.v1;读本轮正文;默认空操作;无长对话史(默认关) |
|
||||
| gm | `world-simulator` | `world-simulator.yaml` — 始终写 Markdown 故事正文;`play_slots.chance` 开时 harness 批量机遇工具 |
|
||||
| narrator | `narrator` | `narrator.yaml` — 读主世界正文,提取/改写/镶壳(默认关) |
|
||||
| perspective | `role-decide` | `role-decide.yaml` — 可选知密视角 |
|
||||
| chance(拓扑开关) | `chance` | `play_slots.chance` 控制 gm harness;可选声明按需 `chance` ref 供显式 run_worker |
|
||||
|
||||
变量 / Progressive:**不是**独立 LLM 模板;见 `docs/progressive-data-design.md`(真值 + side_effects)。
|
||||
Runtime:写入 `运行.本轮.裁决` 时自动合并 `variable_changes` → `变量.当前`;写入 `运行.本轮.旁观` 时按需合并 `table_ops`。
|
||||
Runtime:写入 `运行.本轮.旁观` 时按需合并 `table_ops`;遗留 `运行.本轮.裁决` 仍可合并 `variable_changes`(兼容旧存档)。
|
||||
|
||||
## 其它文件
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@ id: auditor
|
||||
label: 旁观维护
|
||||
role: auxiliary
|
||||
duty: >
|
||||
副 LLM:回合末检查表字段与生成规则是否需要补充;默认空操作。
|
||||
读本轮裁决 + 变量合同 + 当前表;改表结果供下一轮主世界层读取。
|
||||
副 LLM:回合末根据本轮正文与合同,检查表字段与生成规则是否需要补充;默认空操作。
|
||||
读本轮用户可见正文(或主世界草稿)+ 变量合同 + 当前表;改表结果供下一轮主世界层读取。
|
||||
只出 maintain.v1(运行.本轮.旁观);不写用户正文、不写真相权威。
|
||||
when: 叙事转述完成后(回合末)
|
||||
when: 叙事转述完成后(无转述则主世界正文写入后;回合末)
|
||||
suggested_context:
|
||||
static:
|
||||
- 设计.变量设计与更新规则
|
||||
- 设计.生成规则
|
||||
- 设计.变量控制上下文
|
||||
dynamic:
|
||||
- 运行.本轮.裁决
|
||||
- 输出.用户展示
|
||||
- 用户.最新输入
|
||||
- 变量.当前
|
||||
- 上下文.旁观.状态摘要
|
||||
@@ -20,7 +20,7 @@ suggested_outputs:
|
||||
- 运行.本轮.旁观
|
||||
prompt_excerpt: |
|
||||
你是旁观维护(副 LLM),不是主世界层,也不是叙事作者。
|
||||
读合同(生成规则/变量规则/旁观摘要)、当前表,以及本轮主世界层裁决包。
|
||||
读合同(生成规则/变量规则/旁观摘要)、当前表,以及本轮正文(输出.用户展示;相对完整上下文通常极短的一层楼新文)。
|
||||
不读对话历史。表维护合并后供下一轮主世界层读取,不在本轮重裁。
|
||||
「运行.本轮.旁观」必须是 JSON,schema 为 maintain.v1:
|
||||
{
|
||||
@@ -30,7 +30,7 @@ prompt_excerpt: |
|
||||
"notes": []
|
||||
}
|
||||
多数轮保持空:need_generate=false、table_ops=[]、notes=[]。
|
||||
table_ops 仅在确需改「变量.当前」等表字段时使用(op=set|delta)。
|
||||
table_ops 仅在确需改「变量.当前」等表字段时使用(op=set|delta);依据本轮正文里已发生的后果推断,勿臆造正文未支持的变更。
|
||||
need_generate 仅在确需按某条 rule_id 补实例时写成 { "rule_id", "reason" }。
|
||||
程序只会抽该规则里的元素池;方向池由主世界层按规则自创,不要指望抽签出标签。
|
||||
禁止编造长对话续写;禁止输出用户可见正文;禁止修改未点名的无关字段。
|
||||
|
||||
@@ -2,24 +2,25 @@ id: narrator
|
||||
label: 叙事转述
|
||||
role: transcription
|
||||
duty: >
|
||||
只读「运行.本轮.裁决」结构化包 + 叙事指南/正文组成(壳由程序套用),
|
||||
组装「输出.用户展示」。只改表达,不改事实、不补裁决;不读对话历史。
|
||||
when: 主世界层裁决包已写入后
|
||||
只读主世界层本轮正文(输出.用户展示)+ 叙事指南/正文组成(壳由程序或提示约束),
|
||||
提取场面、按文风改写、按壳镶嵌前端可读终稿。可补全残缺叙述;不改剧情事实;不读对话历史与百科长文。
|
||||
when: 主世界层本轮正文已写入后
|
||||
suggested_context:
|
||||
static:
|
||||
- 设计.叙事指南与故事推进
|
||||
- 设计.正文组成
|
||||
- 设计.监控栏
|
||||
dynamic:
|
||||
- 运行.本轮.裁决
|
||||
- 输出.用户展示
|
||||
suggested_outputs:
|
||||
- 输出.用户展示
|
||||
presentation_hints:
|
||||
mode: present.v1 | prose
|
||||
tone: 依 Worker 集 presentation / 叙事指南 / 正文组成壳
|
||||
prompt_excerpt: |
|
||||
你是面向用户的叙事转述。输入是结构化裁决包(settlement.v1),不是自由发挥的世界真相。
|
||||
不读对话历史;连续性来自裁决包内的 visible_now / resolved / npc_moves。
|
||||
你是面向用户的叙事转述。输入是主世界层已写出的本轮正文,不是结构化裁决包,也不是自由发挥的世界真相。
|
||||
不读对话历史与百科;连续性与事实以主世界正文为准。正文残缺时:可补描写、衔接与版式,但不得新增/改写剧情事实、角色未给出的行动或对话。
|
||||
工作顺序:① 从主世界正文提取用户应看见的场面与信息;② 按叙事指南/模仿要点改遣词造句与节奏(不改情节);③ 按「设计.正文组成」壳写出终稿。
|
||||
先读「设计.正文组成」的呈现壳 shell_id(prose|chat_monitor|spotlight|turn_panel|split_board|choice_dock|chapter_reader):
|
||||
- prose:可直接输出 Markdown 正文(程序会落入 body)。
|
||||
- 其它壳:优先输出 JSON,schema 为 present.v1:
|
||||
@@ -38,9 +39,9 @@ prompt_excerpt: |
|
||||
monitor 用短键值(芯片),勿写长文。只填该壳已有区域。
|
||||
用户主读正文(body)默认 1000~2000 字(汉字);以「设计.监控栏」回复呈现已钉字数为准。
|
||||
采用完整长自然段叙述,不要单句成段;不要几句短信就结束。
|
||||
用户输入用法以「设计.叙事指南与故事推进」为准。常见两套:
|
||||
- 大纲扩写:输入当大纲扩写为事实,细化后写正文;不超出大纲未涉及的剧情,完整演绎大纲。
|
||||
- 扮演停笔:把用户本轮行为与对话作正文开头并扩写填满;不可写用户角色未给出的其它行为/对话;不可替用户角色做决定;正文停在其他角色等用户角色回应之刻。
|
||||
用户输入用法以「设计.叙事指南与故事推进」为准:
|
||||
- 大纲扩写:把主世界正文当事实底稿,细化文风后写完整终稿;不超出底稿未涉及的剧情;一次成篇,勿停等用户行动。
|
||||
- 扮演停笔:仅当叙事指南明确钉了此用法时——终稿停在其他角色等用户角色回应之刻;不可写用户角色未给出的其它行为/对话。
|
||||
- 未钉叙事指南时:按大纲扩写,禁止默认扮演停笔。
|
||||
禁止在正文里写出 tag 名(「输出.用户展示」)、压缩摘要或过程日志。
|
||||
禁止写出 do_not_say;禁止编造包中没有的事实。
|
||||
若裁决含 tone_hint / npc_moves / suggested_actions,须体现在 body 或 meta。
|
||||
禁止编造主世界正文中没有的事实。
|
||||
|
||||
@@ -3,7 +3,7 @@ label: 主世界层
|
||||
role: core
|
||||
duty: >
|
||||
读用户输入、真值与 Progressive 投影并推进世界。
|
||||
默认无独立转述:直接写用户可见正文(输出.用户展示);可选开转述时改出 settlement.v1 裁决包。
|
||||
直接写出本轮故事正文(Markdown);无转述时原文即终稿,有转述时正文仍由本层先交,转述再提取/改写/镶壳。
|
||||
when: 每轮用户输入后上场
|
||||
suggested_context:
|
||||
static:
|
||||
@@ -21,16 +21,17 @@ suggested_context:
|
||||
- 大纲.当前章
|
||||
suggested_outputs:
|
||||
- 输出.用户展示
|
||||
- 运行.本轮.裁决
|
||||
- 运行.事件流
|
||||
- 运行.本轮.变量变更
|
||||
prompt_excerpt: |
|
||||
你是主世界层。读真值、已投影上下文、对话历史与用户本轮输入。
|
||||
默认无独立转述层:把本轮场面直接写成用户可见正文(present.v1 或 Markdown),原文即终稿;
|
||||
用户输入用法以「设计.叙事指南与故事推进」为准(扮演停笔则把用户本轮行为/对话作正文开头并扩写填满)。
|
||||
若本局启用了叙事转述:则改为输出 settlement.v1 裁决包(player_action、resolved[]、visible_now、npc_moves[]、variable_changes[]、do_not_say[]、tone_hint),正文交给转述。
|
||||
variable_changes 由 Runtime 合并进变量.当前——若写裁决包请写清 key 与 to/delta。
|
||||
若叙事指南为「扮演停笔」:只推进用户本轮已声明的行为/对话;禁止替用户角色发明其它行动或做决定;可推进到需要用户角色回应之刻即停。
|
||||
直接写出本轮故事正文(优先纯 Markdown / 自然语言叙述)。重心在故事如何推进、人物如何扮演——不要为程序交 JSON 裁决包。
|
||||
无独立转述时:你写的正文就是用户终稿。
|
||||
若本局启用了叙事转述:你仍写完整(或尽量完整)的场面正文;文风润色、残稿补全、前端壳由转述处理。不要改成「只交事实包」。
|
||||
真值/表字段变更不靠你交结构化包:有旁观维护时由副 LLM 根据本轮正文改表;若正文组成钉了隐藏段维护语句,可按约定写短键值,否则不要为维护打断叙述。
|
||||
用户输入用法以「设计.叙事指南与故事推进」为准:
|
||||
- 钉了「大纲扩写」:本轮输入=待扩写大纲/场面要点;一次交完整篇正文,勿停在等用户行动处。
|
||||
- 钉了「扮演停笔」:才只推进用户已声明行为/对话,并停在需用户回应之刻。
|
||||
- 未钉叙事指南时:按大纲扩写处理,禁止默认扮演停笔。
|
||||
需要随机数时:只用本轮上下文里程序已插入的备用随机数(见「设计.随机范围整理」);禁止口算编造。
|
||||
不要把未投影的分档长文整表塞进输出;只写本轮可见后果。
|
||||
禁止在正文里写出 tag 名、压缩摘要或过程日志。
|
||||
|
||||
@@ -152,6 +152,7 @@ import {
|
||||
} from "../skills/play-turn.js";
|
||||
import {
|
||||
PRESENT_TAG,
|
||||
isPlayFinalVisibleWorker,
|
||||
isPlayPresentWorker,
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
presentOutputFromFallback,
|
||||
@@ -1253,7 +1254,7 @@ export class PhaseRuntime {
|
||||
}
|
||||
|
||||
/**
|
||||
* 游玩层不走创作 HITL:转述必须交出用户可见正文,其它执行单元空产物也继续。
|
||||
* 游玩层不走创作 HITL:终稿槽必须交出用户可见正文,其它执行单元空产物也继续。
|
||||
*/
|
||||
private settlePlayWorkerResult(
|
||||
workerId: string,
|
||||
@@ -1261,15 +1262,14 @@ export class PhaseRuntime {
|
||||
): import("../worker/executor.js").WorkerRunResult {
|
||||
if (!isPlayLayerActive(this.session.slots)) return result;
|
||||
const outputs = { ...result.outputs };
|
||||
const narratorEnabled = isNarratorEnabled(
|
||||
parseWorkerSetYaml(
|
||||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||||
)?.play_slots,
|
||||
);
|
||||
if (
|
||||
Object.keys(outputs).length === 0 &&
|
||||
isPlayPresentWorker(workerId, {
|
||||
narratorEnabled: isNarratorEnabled(
|
||||
parseWorkerSetYaml(
|
||||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||||
)?.play_slots,
|
||||
),
|
||||
})
|
||||
isPlayFinalVisibleWorker(workerId, { narratorEnabled })
|
||||
) {
|
||||
const shell =
|
||||
parseShellAdaptationFromReplyFormat(
|
||||
@@ -1279,10 +1279,12 @@ export class PhaseRuntime {
|
||||
const preview = result.preview?.trim() ?? "";
|
||||
const looksAsk =
|
||||
/可验收|追问|请再补|askUser/.test(preview) || /^\s*-\s/.test(preview);
|
||||
outputs[PRESENT_TAG] = presentOutputFromFallback(
|
||||
looksAsk ? "" : preview,
|
||||
shell,
|
||||
);
|
||||
// 主世界终稿用纯正文;转述仍可落 present 包
|
||||
outputs[PRESENT_TAG] = isPlayPresentWorker(workerId)
|
||||
? presentOutputFromFallback(looksAsk ? "" : preview, shell)
|
||||
: looksAsk
|
||||
? "(本轮场面未写完)"
|
||||
: preview || "(本轮场面未写完)";
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
@@ -2578,7 +2580,18 @@ export class PhaseRuntime {
|
||||
inferLifecycleStage(this.session) === "play" &&
|
||||
(tag === "输出.用户展示" || tag === "输出.开场白")
|
||||
) {
|
||||
this.appendDialogueHistory("助手", toWrite);
|
||||
const narratorEnabled = isNarratorEnabled(
|
||||
parseWorkerSetYaml(
|
||||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||||
)?.play_slots,
|
||||
);
|
||||
// 有转述时:主世界草稿不进对话史,只记终稿(转述);开场白始终记
|
||||
if (
|
||||
tag === "输出.开场白" ||
|
||||
isPlayFinalVisibleWorker(workerId, { narratorEnabled })
|
||||
) {
|
||||
this.appendDialogueHistory("助手", toWrite);
|
||||
}
|
||||
}
|
||||
if (nextDoc) {
|
||||
this.applyTableSideEffects(prevDoc, nextDoc, workerId);
|
||||
@@ -2586,7 +2599,7 @@ export class PhaseRuntime {
|
||||
if (tag === OPENING_CURRENT_VARS_TAG || tag === "变量.当前") {
|
||||
this.applyValueMapProjections(workerId);
|
||||
}
|
||||
// 裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects)
|
||||
// 遗留裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects)
|
||||
if (tag === SETTLEMENT_TAG) {
|
||||
this.applySettlementVariableChanges(toWrite, workerId);
|
||||
} else if (tag === MAINTAIN_TAG) {
|
||||
|
||||
@@ -498,7 +498,7 @@ const SYNTH_FALLBACK_CONTEXT: Record<
|
||||
"设计.变量控制上下文",
|
||||
],
|
||||
dynamic: [
|
||||
"运行.本轮.裁决",
|
||||
"输出.用户展示",
|
||||
"用户.最新输入",
|
||||
"变量.当前",
|
||||
"上下文.旁观.状态摘要",
|
||||
@@ -528,12 +528,12 @@ const SYNTH_FALLBACK_CONTEXT: Record<
|
||||
"设计.正文组成",
|
||||
"设计.监控栏",
|
||||
],
|
||||
dynamic: ["运行.本轮.裁决"],
|
||||
dynamic: ["输出.用户展示"],
|
||||
skipHistory: true,
|
||||
},
|
||||
"role-decide": {
|
||||
static: [],
|
||||
dynamic: ["用户.最新输入", "运行.本轮.裁决"],
|
||||
dynamic: ["用户.最新输入", "输出.用户展示"],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
} from "./context-order.js";
|
||||
import type { ContextSegmentDef } from "./context-segments.js";
|
||||
import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js";
|
||||
import { isNarratorEnabled } from "./play-slots.js";
|
||||
|
||||
type WorkerTemplateDoc = {
|
||||
id?: string;
|
||||
@@ -223,7 +222,7 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
const visibleBodyRule =
|
||||
id === "narrator" ||
|
||||
id === "round-present" ||
|
||||
(id === "world-simulator" && !isNarratorEnabled(params.workerSet?.play_slots))
|
||||
id === "world-simulator"
|
||||
? PLAY_VISIBLE_BODY_INSTRUCTION
|
||||
: "";
|
||||
const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n");
|
||||
|
||||
@@ -114,6 +114,6 @@ export const GM_CHANCE_HARNESS_INSTRUCTION = `
|
||||
|
||||
## 机遇工具(游玩期)
|
||||
需要真随机(掷骰/比点/抽签/加权抽取)时:调用工具 \`chance\`,在 \`requests\` 数组中**一次性**列出本步全部随机需求(每项必须有唯一 \`id\`)。
|
||||
禁止口算或编造随机结果。收到工具结果后,再在 settlement 的 \`resolved\` 等处引用对应 \`id\` 写出后果。
|
||||
无随机需求时:直接输出 settlement.v1 JSON,不要调用工具。
|
||||
禁止口算或编造随机结果。收到工具结果后,在本轮 **Markdown 正文**里演绎对应 \`id\` 的后果(写进场面,不要交 JSON 裁决包)。
|
||||
无随机需求时:直接输出故事正文,不要调用工具。
|
||||
`;
|
||||
|
||||
+18
-15
@@ -54,12 +54,13 @@ export const PLAY_SLOT_META: Record<
|
||||
gm: {
|
||||
label: "主世界层",
|
||||
purpose:
|
||||
"读真值与 Progressive 投影并推进世界;无独立转述时直接写用户可见正文,有转述时出裁决包。",
|
||||
"读真值与 Progressive 投影并推进世界;直接写本轮故事正文(Markdown);无转述时原文即终稿。",
|
||||
defaultAcceptance: "review",
|
||||
},
|
||||
narrator: {
|
||||
label: "叙事转述",
|
||||
purpose: "只读裁决包 + 文风常驻,输出用户可见正文(可选;默认关)。",
|
||||
purpose:
|
||||
"读主世界正文,提取/改写/镶壳为用户终稿;可补残稿,不改剧情(可选;默认关)。",
|
||||
defaultAcceptance: "review",
|
||||
},
|
||||
perspective: {
|
||||
@@ -191,37 +192,39 @@ export function workerEntryForSlot(
|
||||
): WorkerSetEntry {
|
||||
const meta = PLAY_SLOT_META[id];
|
||||
const ref = refForSlot(slots, id);
|
||||
const soloGm = id === "gm" && !isNarratorEnabled(slots);
|
||||
const withNarrator = isNarratorEnabled(slots);
|
||||
const soloGm = id === "gm" && !withNarrator;
|
||||
return {
|
||||
ref,
|
||||
name: meta.label,
|
||||
role: id,
|
||||
duty: soloGm
|
||||
? "读真值与投影,推进世界,并直接写出用户可见正文(无独立转述层,原文即终稿)。"
|
||||
: meta.purpose,
|
||||
when:
|
||||
duty:
|
||||
id === "gm"
|
||||
? soloGm
|
||||
? "每轮用户输入后上场;直接交用户可见正文"
|
||||
: "每轮用户输入后首先上场(主 LLM / 裁决权威)"
|
||||
? "读真值与投影,推进世界,并直接写出用户可见正文(无独立转述层,原文即终稿)。"
|
||||
: "读真值与投影,推进世界,写出本轮故事正文(Markdown);文风润色与壳交给转述。"
|
||||
: meta.purpose,
|
||||
when:
|
||||
id === "gm"
|
||||
? "每轮用户输入后上场;直接交故事正文"
|
||||
: id === "perspective"
|
||||
? "强信息隔离且本轮需要该角色独立反应时(主世界层之后)"
|
||||
: id === "narrator"
|
||||
? "主世界层裁决包就绪后"
|
||||
: "转述完成后;表维护合并进变量.当前,供下一轮主世界层读取",
|
||||
? "主世界层本轮正文就绪后"
|
||||
: "终稿写入后;表维护合并进变量.当前,供下一轮主世界层读取",
|
||||
rationale:
|
||||
id === "gm"
|
||||
? soloGm
|
||||
? "默认少槽:主世界层兼呈现,避免转述再改写一遍"
|
||||
: "有独立转述时:主世界层出裁决,正文交给转述"
|
||||
: "有独立转述时:主世界专注剧情与扮演;转述再提取/改写/镶壳"
|
||||
: id === "perspective"
|
||||
? "知密内容不能进主世界层上下文"
|
||||
: id === "narrator"
|
||||
? "用户可见正文与裁决分离,避免文风与规则互相挤压"
|
||||
? "文风与前端壳与世界推演分离,避免互相挤压"
|
||||
: "表与规则补充延后执行,改表结果作用于下一轮而非本轮重裁",
|
||||
acceptance:
|
||||
id === "gm" && isNarratorEnabled(slots) ? "continue" : meta.defaultAcceptance,
|
||||
outputs: soloGm ? ["输出.用户展示"] : undefined,
|
||||
id === "gm" && withNarrator ? "continue" : meta.defaultAcceptance,
|
||||
outputs: id === "gm" ? ["输出.用户展示"] : undefined,
|
||||
invocation: "turn",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,23 +192,40 @@ export function stripPresentSourceFences(raw: string): string {
|
||||
}
|
||||
|
||||
export const PLAY_VISIBLE_BODY_INSTRUCTION = [
|
||||
"用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **1000~2000 字**(按汉字计);以「设计.监控栏/回复呈现」已钉字数为准。",
|
||||
"用户可见正文(主世界层:纯 Markdown;转述:present.v1 的 blocks.body,或纯 Markdown 主读)默认 **1000~2000 字**(按汉字计);以「设计.监控栏/回复呈现」已钉字数为准。",
|
||||
"采用完整长自然段叙述,不要单句成段。",
|
||||
"不要写成几句气泡短信就结束,除非用户明确要求极短。",
|
||||
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
||||
].join("\n");
|
||||
|
||||
/** 游玩期把用户可见终稿写出的执行单元(有转述用转述;无则主世界层直接交原文) */
|
||||
export function isPlayPresentWorker(
|
||||
/**
|
||||
* 游玩期走 present.v1 结构化投递的执行单元(转述 / 回合陈述)。
|
||||
* 主世界层不在此列:始终自由写 Markdown 正文。
|
||||
*/
|
||||
export function isPlayPresentWorker(workerId: string): boolean {
|
||||
const id = workerId.trim();
|
||||
return id === "narrator" || id === "round-present";
|
||||
}
|
||||
|
||||
/**
|
||||
* 本轮最终用户可见正文的执行单元(写入对话.历史 / 空产物兜底)。
|
||||
* 无转述:主世界层;有转述:转述(主世界草稿可被覆盖,不进历史)。
|
||||
*/
|
||||
export function isPlayFinalVisibleWorker(
|
||||
workerId: string,
|
||||
opts?: { narratorEnabled?: boolean },
|
||||
): boolean {
|
||||
const id = workerId.trim();
|
||||
if (id === "narrator" || id === "round-present") return true;
|
||||
if (id === "world-simulator" && opts?.narratorEnabled === false) return true;
|
||||
if (id === "world-simulator" && opts?.narratorEnabled !== true) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 游玩主世界层:自由 Markdown 正文(程序可事后落入 present 壳,模型不交 JSON) */
|
||||
export function isPlayGmBodyWorker(workerId: string): boolean {
|
||||
return workerId.trim() === "world-simulator";
|
||||
}
|
||||
|
||||
/** 按能力探测走 json_schema / forced_tool 时用的 present.v1 形状(全 required,可 strict) */
|
||||
export const PRESENT_JSON_SCHEMA: Record<string, unknown> = {
|
||||
type: "object",
|
||||
@@ -243,21 +260,35 @@ export const PRESENT_JSON_SCHEMA: Record<string, unknown> = {
|
||||
},
|
||||
};
|
||||
|
||||
/** 游玩主世界层:直接写 Markdown,禁止 JSON 裁决包 */
|
||||
export function playGmBodyOutputInstruction(): string {
|
||||
return `
|
||||
|
||||
---
|
||||
|
||||
## 运行时输出协议(主世界正文)
|
||||
|
||||
直接输出本轮 **Markdown / 自然语言故事正文**(写入「输出.用户展示」)。
|
||||
不要输出 JSON、不要交 settlement/裁决包、不要套 present.v1 外壳。
|
||||
重心在场面推进与人物扮演;残稿也可以交,有转述时会再处理。
|
||||
不要写追问、askUser、自评或「可验收产物」。`;
|
||||
}
|
||||
|
||||
/** 游玩转述:按探测到的 structured 投递写出 present.v1,不要创作追问 */
|
||||
export function playPresentOutputInstruction(shell: PresentShellId): string {
|
||||
return `
|
||||
|
||||
---
|
||||
|
||||
## 运行时输出协议(游玩正文)
|
||||
## 运行时输出协议(游玩终稿)
|
||||
|
||||
本步产物是给用户看的终稿。程序按已探测的模型能力用 schema / tool / json_object 投递,你输出 **一个 present.v1 对象**:
|
||||
本步读主世界层已写正文,产出给用户看的终稿。程序按已探测的模型能力用 schema / tool / json_object 投递,你输出 **一个 present.v1 对象**:
|
||||
|
||||
{"schema":"present.v1","shell":"${shell}","blocks":{"body":"用户可读正文","monitor":"","header":"","footer":"","aside":""},"meta":{"suggested_actions":[]}}
|
||||
|
||||
\`shell\` 必须是 \`${shell}\`。只填该壳已有区域,没有的键留空字符串。
|
||||
prose 壳也可直接输出 Markdown,程序会落入 body。
|
||||
材料不够也先写一版场面。不要写追问、askUser、自评或「可验收产物」。`;
|
||||
可补全残缺叙述与版式,但不得改剧情事实。不要写追问、askUser、自评或「可验收产物」。`;
|
||||
}
|
||||
|
||||
export function stringifyPresentPacket(packet: PresentPacket): string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 主世界层 → 叙事转述的裁决包(settlement / direction packet)。
|
||||
* 存黑板 tag:运行.本轮.裁决
|
||||
* 遗留:settlement.v1 裁决包(运行.本轮.裁决)。
|
||||
* 现行游玩管线:主世界层直接写 Markdown 正文;转述读正文;旁观维护读正文出 maintain.v1。
|
||||
* 仍保留解析/合并,兼容旧存档或手工写入的 variable_changes。
|
||||
*/
|
||||
import {
|
||||
createTableFromValues,
|
||||
|
||||
@@ -221,6 +221,7 @@ const DEFAULT_WORKER_CONTRACTS: Record<
|
||||
"设计.变量控制上下文",
|
||||
],
|
||||
dynamic: [
|
||||
"输出.用户展示",
|
||||
"用户.最新输入",
|
||||
"变量.当前",
|
||||
"上下文.旁观.状态摘要",
|
||||
@@ -249,12 +250,12 @@ const DEFAULT_WORKER_CONTRACTS: Record<
|
||||
"运行.本轮.旁观",
|
||||
],
|
||||
},
|
||||
outputs: ["输出.用户展示", "运行.本轮.裁决", "运行.事件流", "运行.本轮.变量变更"],
|
||||
outputs: ["输出.用户展示", "运行.事件流"],
|
||||
},
|
||||
"variable-update": {
|
||||
context: {
|
||||
static: ["设计.变量设计与更新规则"],
|
||||
dynamic: ["运行.本轮.裁决", "变量.当前"],
|
||||
dynamic: ["输出.用户展示", "变量.当前"],
|
||||
},
|
||||
outputs: ["运行.本轮.变量变更", "变量.当前"],
|
||||
},
|
||||
@@ -267,7 +268,7 @@ const DEFAULT_WORKER_CONTRACTS: Record<
|
||||
"设计.正文组成",
|
||||
"设计.监控栏",
|
||||
],
|
||||
dynamic: ["运行.本轮.裁决", "用户.最新输入", "变量.当前"],
|
||||
dynamic: ["输出.用户展示", "用户.最新输入", "变量.当前"],
|
||||
},
|
||||
outputs: ["输出.用户展示"],
|
||||
},
|
||||
|
||||
+97
-58
@@ -21,17 +21,15 @@ import {
|
||||
} from "../parse/json-doc.js";
|
||||
import { assembleWorkerContext } from "../skills/context-segments.js";
|
||||
import { isPlayLayerActive } from "../skills/play-turn.js";
|
||||
import { loadAppSettings } from "../config/settings.js";
|
||||
import { resolveActivePreset } from "../preset/store.js";
|
||||
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
|
||||
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
|
||||
import {
|
||||
PRESENT_TAG,
|
||||
PRESENT_JSON_SCHEMA,
|
||||
isPlayGmBodyWorker,
|
||||
isPlayPresentWorker,
|
||||
isPresentLikeObject,
|
||||
parsePresentPacket,
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
playGmBodyOutputInstruction,
|
||||
playPresentOutputInstruction,
|
||||
presentOutputFromFallback,
|
||||
stringifyPresentPacket,
|
||||
@@ -46,7 +44,6 @@ import {
|
||||
GM_CHANCE_HARNESS_INSTRUCTION,
|
||||
} from "../skills/gm-tools.js";
|
||||
import { runGmHarness } from "./gm-harness.js";
|
||||
import { isNarratorEnabled } from "../skills/play-slots.js";
|
||||
import {
|
||||
mergeQuestionsPreferFragment,
|
||||
normalizeQuestions,
|
||||
@@ -397,10 +394,15 @@ function lookupOutputValue(
|
||||
function parseWorkerResponse(
|
||||
raw: string,
|
||||
outputTags: string[],
|
||||
opts?: { fallbackShell?: PresentShellId },
|
||||
opts?: { fallbackShell?: PresentShellId; preferPlainBody?: boolean },
|
||||
): WorkerRunResult {
|
||||
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||
const preferPlainBody = opts?.preferPlainBody === true;
|
||||
const playTarget = playVisibleTarget(outputTags);
|
||||
const packBody = (body: string) =>
|
||||
preferPlainBody
|
||||
? body.trim() || "(本轮场面未写完)"
|
||||
: presentOutputFromFallback(body, fallbackShell);
|
||||
|
||||
const parsed = tryParseJsonDoc(raw);
|
||||
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
@@ -411,10 +413,10 @@ function parseWorkerResponse(
|
||||
? raw
|
||||
: "";
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: presentOutputFromFallback(body, fallbackShell) },
|
||||
{ [playTarget]: packBody(body) },
|
||||
{ summary: body.slice(0, 80) || "游玩正文" },
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell },
|
||||
{ playVisible: true, fallbackShell, preferPlainBody },
|
||||
);
|
||||
}
|
||||
if (target && looksLikeStructuredDraft(raw)) {
|
||||
@@ -449,7 +451,7 @@ function parseWorkerResponse(
|
||||
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
|
||||
if (playTarget && isPresentLikeObject(obj)) {
|
||||
if (playTarget && isPresentLikeObject(obj) && !preferPlainBody) {
|
||||
const present = canonicalizePresentOutput(obj, fallbackShell);
|
||||
if (present) {
|
||||
return finalizeParsedOutputs(
|
||||
@@ -473,15 +475,32 @@ function parseWorkerResponse(
|
||||
}
|
||||
}
|
||||
|
||||
// 游玩正文:片段里的场面字收成 present,不要扁形成创作稿
|
||||
// 游玩正文:片段里的场面字收成 present(或主世界纯正文)
|
||||
if (playTarget && !isPresentLikeObject(obj)) {
|
||||
const scene = fragmentSceneText(obj);
|
||||
if (scene) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: presentOutputFromFallback(scene, fallbackShell) },
|
||||
{ [playTarget]: packBody(scene) },
|
||||
obj,
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell },
|
||||
{ playVisible: true, fallbackShell, preferPlainBody },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 主世界偶发 JSON:尽量抽出可读字符串当正文
|
||||
if (playTarget && preferPlainBody) {
|
||||
const scene =
|
||||
fragmentSceneText(obj) ||
|
||||
(typeof obj.body === "string" ? obj.body.trim() : "") ||
|
||||
(typeof obj.visible_now === "string" ? obj.visible_now.trim() : "") ||
|
||||
raw.trim();
|
||||
if (scene) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: packBody(scene) },
|
||||
obj,
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell, preferPlainBody },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -510,6 +529,16 @@ function parseWorkerResponse(
|
||||
for (const tag of productOutputTags(outputTags)) {
|
||||
const val = lookupOutputValue(bag, tag);
|
||||
if (playTarget && tag === playTarget) {
|
||||
if (preferPlainBody) {
|
||||
const scene =
|
||||
(typeof val === "string" ? val.trim() : "") ||
|
||||
fragmentSceneText(val) ||
|
||||
"";
|
||||
if (scene) {
|
||||
outputs[tag] = packBody(scene);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const present = canonicalizePresentOutput(val, fallbackShell);
|
||||
if (present) {
|
||||
outputs[tag] = present;
|
||||
@@ -526,7 +555,7 @@ function parseWorkerResponse(
|
||||
}
|
||||
recoverFragmentOutputs(obj, outputs, outputTags, fallbackShell);
|
||||
|
||||
if (playTarget && outputs[playTarget]) {
|
||||
if (playTarget && outputs[playTarget] && !preferPlainBody) {
|
||||
const present = canonicalizePresentOutput(outputs[playTarget], fallbackShell);
|
||||
if (present) outputs[playTarget] = present;
|
||||
}
|
||||
@@ -534,6 +563,7 @@ function parseWorkerResponse(
|
||||
return finalizeParsedOutputs(outputs, obj, outputTags, {
|
||||
playVisible: Boolean(playTarget),
|
||||
fallbackShell,
|
||||
preferPlainBody,
|
||||
rawFallback: raw,
|
||||
});
|
||||
}
|
||||
@@ -545,6 +575,7 @@ function finalizeParsedOutputs(
|
||||
opts?: {
|
||||
playVisible?: boolean;
|
||||
fallbackShell?: PresentShellId;
|
||||
preferPlainBody?: boolean;
|
||||
rawFallback?: string;
|
||||
},
|
||||
): WorkerRunResult {
|
||||
@@ -552,6 +583,11 @@ function finalizeParsedOutputs(
|
||||
const playTarget = playVisibleTarget(outputTags);
|
||||
const playVisible = Boolean(opts?.playVisible || playTarget);
|
||||
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||
const preferPlainBody = opts?.preferPlainBody === true;
|
||||
const packBody = (body: string) =>
|
||||
preferPlainBody
|
||||
? body.trim() || "(本轮场面未写完)"
|
||||
: presentOutputFromFallback(body, fallbackShell);
|
||||
|
||||
let askUser: QuestionItem[] | undefined;
|
||||
if (!playVisible) {
|
||||
@@ -615,7 +651,7 @@ function finalizeParsedOutputs(
|
||||
scene ||
|
||||
summaryText ||
|
||||
usablePlayFallbackText(opts?.rawFallback);
|
||||
outputs[playTarget] = presentOutputFromFallback(fallback, fallbackShell);
|
||||
outputs[playTarget] = packBody(fallback);
|
||||
}
|
||||
|
||||
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
||||
@@ -782,54 +818,43 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
worker.id === "world-simulator" &&
|
||||
gmChanceToolsEnabled(playSlots);
|
||||
|
||||
const playGmBody =
|
||||
isPlayLayerActive(params.slots) && isPlayGmBodyWorker(worker.id);
|
||||
const playPresent =
|
||||
isPlayLayerActive(params.slots) &&
|
||||
isPlayPresentWorker(worker.id, {
|
||||
narratorEnabled: isNarratorEnabled(playSlots),
|
||||
});
|
||||
const playShell: PresentShellId | undefined = playPresent
|
||||
? parseShellAdaptationFromReplyFormat(
|
||||
params.blackboard.getContentByTag("设计.正文组成") ??
|
||||
params.blackboard.getContentByTag("设计.回复格式"),
|
||||
)?.shell_id ?? "prose"
|
||||
: undefined;
|
||||
isPlayLayerActive(params.slots) && isPlayPresentWorker(worker.id);
|
||||
const playShell: PresentShellId | undefined =
|
||||
playPresent || playGmBody
|
||||
? parseShellAdaptationFromReplyFormat(
|
||||
params.blackboard.getContentByTag("设计.正文组成") ??
|
||||
params.blackboard.getContentByTag("设计.回复格式"),
|
||||
)?.shell_id ?? "prose"
|
||||
: undefined;
|
||||
|
||||
const systemContent =
|
||||
promptBody +
|
||||
(playPresent && playShell
|
||||
? playPresentOutputInstruction(playShell)
|
||||
: WORKER_OUTPUT_INSTRUCTION) +
|
||||
(playGmBody
|
||||
? playGmBodyOutputInstruction()
|
||||
: playPresent && playShell
|
||||
? playPresentOutputInstruction(playShell)
|
||||
: WORKER_OUTPUT_INSTRUCTION) +
|
||||
(useGmHarness ? GM_CHANCE_HARNESS_INSTRUCTION : "");
|
||||
const playPreset = isPlayLayerActive(params.slots)
|
||||
? resolveActivePreset(loadAppSettings().activePresetId)
|
||||
: null;
|
||||
|
||||
const messages = playPreset
|
||||
? assemblePlayWorkerMessages({
|
||||
systemPrompt: systemContent,
|
||||
preset: playPreset,
|
||||
pack: worldInfoPackFromSegments({
|
||||
segments: worker.contextSegments ?? [],
|
||||
inputs,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
}),
|
||||
})
|
||||
: [
|
||||
{ role: "system" as const, content: systemContent },
|
||||
{
|
||||
role: "user" as const,
|
||||
content: assembleWorkerContext({
|
||||
inputs,
|
||||
segments: worker.contextSegments,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
workerId: worker.id,
|
||||
workerName: worker.name,
|
||||
outputTags: worker.outputTags,
|
||||
}),
|
||||
},
|
||||
];
|
||||
// 任务 messages 即可;preset 夹心由 wrapLlmForSession / PresetLlmProvider 统一装配
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: systemContent },
|
||||
{
|
||||
role: "user",
|
||||
content: assembleWorkerContext({
|
||||
inputs,
|
||||
segments: worker.contextSegments,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
workerId: worker.id,
|
||||
workerName: worker.name,
|
||||
outputTags: worker.outputTags,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (useGmHarness) {
|
||||
const systemMsg = messages.find((m) => m.role === "system");
|
||||
@@ -841,8 +866,10 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
stream: params.stream,
|
||||
caller: `worker:${worker.id}`,
|
||||
});
|
||||
return parseWorkerResponse(selectJsonPayload(harness.content), worker.outputTags, {
|
||||
// 主世界 harness 终稿是 Markdown;勿强行抽 JSON
|
||||
return parseWorkerResponse(harness.content, worker.outputTags, {
|
||||
fallbackShell: playShell,
|
||||
preferPlainBody: playGmBody,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -853,6 +880,18 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (playGmBody) {
|
||||
const completeOpts = { caller: `worker:${worker.id}`, responseFormat: "text" as const };
|
||||
const result =
|
||||
streamCallbacks && params.llm.completeStream
|
||||
? await params.llm.completeStream(messages, completeOpts, streamCallbacks)
|
||||
: await params.llm.complete(messages, completeOpts);
|
||||
return parseWorkerResponse(result.content, worker.outputTags, {
|
||||
fallbackShell: playShell,
|
||||
preferPlainBody: true,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await completeStructured(params.llm, messages, {
|
||||
schema: playPresent ? PRESENT_JSON_SCHEMA : {
|
||||
type: "object",
|
||||
@@ -880,7 +919,7 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
export function parseWorkerResponseForTest(
|
||||
raw: string,
|
||||
outputTags: string[],
|
||||
opts?: { fallbackShell?: PresentShellId },
|
||||
opts?: { fallbackShell?: PresentShellId; preferPlainBody?: boolean },
|
||||
): WorkerRunResult {
|
||||
return parseWorkerResponse(raw, outputTags, opts);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 主世界层(world-simulator)游玩期 tool harness:
|
||||
* 反复 completeWithTools 直到不再调用工具,最后一次正文视为裁决 JSON。
|
||||
* 反复 completeWithTools 直到不再调用工具,最后一次正文视为故事 Markdown。
|
||||
*/
|
||||
import type { ChatMessage, LlmProvider } from "../llm/client.js";
|
||||
import {
|
||||
|
||||
@@ -280,14 +280,14 @@ describe("context-order.v1", () => {
|
||||
template: {
|
||||
id: "world-simulator",
|
||||
label: "主世界层",
|
||||
duty: "裁决",
|
||||
duty: "写正文",
|
||||
suggested_context: { static: ["设计.worker集"], dynamic: ["用户.最新输入"] },
|
||||
},
|
||||
workerSet: parsed,
|
||||
});
|
||||
expect(worker.contextSegments?.some((s) => s.inline?.includes("裁决"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
worker.contextSegments?.some((s) => s.inline?.includes("正文")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
worker.contextSegments?.some(
|
||||
(s) => s.tier === "dynamic" && s.tags.includes("用户.最新输入"),
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
presentFromPlainText,
|
||||
presentOutputFromFallback,
|
||||
isPlayFinalVisibleWorker,
|
||||
isPlayGmBodyWorker,
|
||||
isPlayPresentWorker,
|
||||
shellDefaultRegions,
|
||||
} from "../src/skills/present-packet.js";
|
||||
@@ -118,16 +120,24 @@ describe("shell adaptation from reply-format", () => {
|
||||
});
|
||||
|
||||
describe("play present worker", () => {
|
||||
it("recognizes narrator, round-present, and solo gm", () => {
|
||||
it("structured present only for narrator / round-present", () => {
|
||||
expect(isPlayPresentWorker("narrator")).toBe(true);
|
||||
expect(isPlayPresentWorker("round-present")).toBe(true);
|
||||
expect(isPlayPresentWorker("world-simulator")).toBe(false);
|
||||
});
|
||||
|
||||
it("gm body worker and final visible roles", () => {
|
||||
expect(isPlayGmBodyWorker("world-simulator")).toBe(true);
|
||||
expect(isPlayGmBodyWorker("narrator")).toBe(false);
|
||||
expect(
|
||||
isPlayPresentWorker("world-simulator", { narratorEnabled: false }),
|
||||
isPlayFinalVisibleWorker("world-simulator", { narratorEnabled: false }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPlayPresentWorker("world-simulator", { narratorEnabled: true }),
|
||||
isPlayFinalVisibleWorker("world-simulator", { narratorEnabled: true }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isPlayFinalVisibleWorker("narrator", { narratorEnabled: true }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("fallback packet uses the given shell", () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ workers:
|
||||
- 用户.最新输入
|
||||
- 运行.事件流
|
||||
outputs:
|
||||
- 运行.本轮.裁决
|
||||
- 输出.用户展示
|
||||
- 运行.事件流
|
||||
- ref: narrator
|
||||
duty: 组装用户可见回复
|
||||
@@ -30,14 +30,14 @@ workers:
|
||||
static:
|
||||
- 叙事.指南.确认稿
|
||||
dynamic:
|
||||
- 运行.事件流
|
||||
- 输出.用户展示
|
||||
outputs:
|
||||
- 输出.用户展示
|
||||
instantiate_hints:
|
||||
invoke: [opening-generator]
|
||||
tag_flow:
|
||||
- "用户.最新输入 → 运行.本轮.裁决"
|
||||
- "运行.本轮.裁决 → 输出.用户展示"
|
||||
- "用户.最新输入 → 输出.用户展示"
|
||||
- "输出.用户展示 → 输出.用户展示(转述终稿)"
|
||||
`;
|
||||
|
||||
describe("formatWorkerSetForUser", () => {
|
||||
@@ -62,7 +62,7 @@ describe("formatWorkerSetForUser", () => {
|
||||
false,
|
||||
);
|
||||
expect(view.workers[0].context.staticTags[0].tag).toBe("设计.worker集");
|
||||
expect(view.workers[0].writes).toEqual(["运行.本轮.裁决", "运行.事件流"]);
|
||||
expect(view.workers[0].writes).toEqual(["输出.用户展示", "运行.事件流"]);
|
||||
expect(view.workers[0].context.explicit).toBe(true);
|
||||
expect(view.workers[1].presentation?.[0].label).toBe("语气");
|
||||
expect(view.tagFlow).toHaveLength(2);
|
||||
|
||||
Reference in New Issue
Block a user