重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 00:39:32 +08:00
parent 2b74c30d36
commit e670a5129c
167 changed files with 22955 additions and 5659 deletions

32
THIRD_PARTY_NOTICES.md Normal file
View File

@@ -0,0 +1,32 @@
# Third-Party Notices
本仓库以自研运行内核与文档为主。下列项目**仅作架构与实现参考**(见 [`docs/references.md`](docs/references.md)),默认**不**将它们的源码整仓纳入本仓库。
在实际引入 npm/依赖、复制示例代码、或嵌入前端素材之前,必须逐项确认:
- LICENSE 是否允许修改与再分发
- 是否要求保留版权声明
- 前端素材是否使用不同许可证
- 示例代码与主仓库许可证是否一致
- 模型 Provider 是否另有使用限制
- 角色卡等格式是事实标准还是含特定项目代码
## Reference projects (not vendored by default)
| Project | URL | Intended use in this repo |
|---------|-----|---------------------------|
| Mastra | https://github.com/mastra-ai/mastra | Reference only (agent/tool patterns); not the runtime kernel |
| assistant-ui | https://github.com/assistant-ui/assistant-ui | Optional UI pattern reference |
| LobeChat | https://github.com/lobehub/lobe-chat | Product/IA reference |
| SillyTavern | https://github.com/SillyTavern/SillyTavern | UX/concept reference for cards & lorebooks |
| LangGraph.js | https://github.com/langchain-ai/langgraphjs | Checkpoint / recovery ideas |
| AutoGen | https://github.com/microsoft/autogen | Multi-agent runtime ideas |
| Letta | https://github.com/letta-ai/letta | Memory / context window ideas |
| OpenHands | https://github.com/All-Hands-AI/OpenHands | Event stream / observability ideas |
| CopilotKit | https://github.com/CopilotKit/CopilotKit | AgentUI connection ideas |
When a dependency is added to `package.json`, record its license here (or under `licenses/`) and keep versions pinned via the lockfile.
## Direct dependencies
See `package.json` / lockfile for runtime and dev dependencies and their respective licenses.

View File

@@ -1,150 +1,301 @@
# 整体架构 # 本地多 Agent 文本创作系统架构
## 1. 方向 ## 1. 文档定位
**标签驱动黑板** + **agent tool loop** + **5 相位阶段机** + **skill 能力库** 本文档描述**系统级运行内核**:模块边界、创作定义与运行实例如何分离、总管 / Worker / Skill / 上下文如何协作、数据如何持久化,以及外部项目按层借鉴的原则
```text 本文档**不**设计具体剧本 Skill 的提示词、步骤或字段。剧本与创作方法见独立文档(如 `design-orchestrator-guide.md`Skill 包格式见 `skill-format.md` 系列。
Skill 包orchestrator manifest + workers/*/SKILL.md 静态能力定义
Session + 黑板 tag 一次运行的实参
Book 跨 Session 的项目与资产
Agent总管 running 相位内 tool loop调度 invoke 哪个 skill
Runtime 拼接上下文、校验 tool、写黑板、驱动阶段机边界
阶段机 谁可动、何时等用户、产物生命周期——不是流水线剧本
```
核心文档:
```text
docs/tag-blackboard.md 黑板、标签、分工
docs/context-assembly.md ★ 上下文拼接(上半固定、下半动态)
docs/tool-contracts.md 总管 / worker tool
docs/runtime-state-machine.md 5 相位、tool 边界
docs/book-storage.md 长期存储(过程 / 资产 / 游玩)
docs/skill-design-guide.md 新 skill 包设计方法
docs/implementation-guide.md 代码文件职责
```
--- ---
## 2. 术语 ## 2. 产品目标
本地运行的多 Agent 文本生成平台,兼容多种创作模式(角色扮演、长短篇小说、仿写扩写、思想实验、信息隔离模拟、世界观/角色卡/模板创作、动态表与长期状态、存档分支等)。
这些模式共享同一运行内核,区别来自:
- 装载哪些 Skill / 能力包
- 实例规格Worker 声明)如何配置
- 上下文如何按契约编译
- 对状态读写实施什么权限
不为每一种创作模式各实现一套固定工作流。
---
## 3. 核心架构原则
### 3.1 创作定义与运行实例分离
| 层 | 当前实现对应 | 含义 |
|----|--------------|------|
| **创作定义 / 规格** | accept 后的 `设计.worker集`、静态设定 tag可存为 `instance` / `opening` 快照 | 可复用的「函数声明」Worker、表、常驻上下文、权限与能力边界 |
| **运行实例** | Book + Session + 黑板 + `run` 快照 | 一次具体创作/游玩过程:对话、变量、轮次、中间产物 |
同一规格可多次开档;实例应**钉住**某次规格截面。规格更新后,已有实例不自动跟随;需显式加载/迁移到新截面。
语义上:`instance` 快照 ≈ 已发布的规格版本;`run` 快照 ≈ 运行过程存档。正式 `DefinitionVersion` 发布与迁移协议在现有快照之上增量补齐,不另起一套推倒重写。
### 3.2 总管只负责任务调配
总管 AgentMain Agent负责判断目标是否完成、决定下一项任务、选择 Worker、必要时询问用户或结束。
总管**不**负责:直接创作最终正文、临场拼接完整上下文、绕过权限读私有信息、直接改正式状态、同时兼创作与评测、把隐藏推理写入长期状态。
任务所需信息由总管**声明意图**(如 invoke 哪个 worker真正发给 Worker 的上下文由 Runtime 的上下文编译器按 Skill / 声明契约生成。
### 3.3 Agent 决策与程序控制分离
| Agent非确定 | Runtime确定 |
|-----------------|-----------------|
| 下一步做什么 | 权限与声明校验 |
| 调用哪个能力 | 上下文装载与 Token 裁剪 |
| 是否返工 / 问用户 | 状态提交、表 `rev` 合并 |
| | 相位边界、burst、失败处理 |
不能依赖提示词保证权限与信息隔离;访问限制在 Tool 与 Context Compiler 层执行。
### 3.4 动态调度,不用固定创作 DAG
「下一步 invoke 哪个 skill」由 agent tool loop 决定。
**5 相位阶段机**`idle` / `running` / `waiting_user` / `done` / `error`)是并发与权限模型——谁可动、何时等用户、产物何时算事实——**不是**流水线剧本。详见 `runtime-state-machine.md``tool-contracts.md`
确定性后台流程(导入导出、索引、迁移、批量评测)可用固定步骤;整场 AIRP / 小说创作流程不固化为完整 DAG。
---
## 4. 总体架构
```text
┌─────────────────────────────────────────────┐
│ Web / Electron Frontend │
│ 创作 / 游玩 / 存档 / 检查器 / 调试 │
└─────────────────────┬───────────────────────┘
│ HTTP+ 流式事件)
┌─────────────────────▼───────────────────────┐
│ Application API │
│ Book、Session、快照、资源、模型配置 │
└─────────────────────┬───────────────────────┘
┌─────────────────────▼───────────────────────┐
│ Agent Runtime │
│ Main Agenttool loop
│ Phase Machine + Phase Runtime │
│ Skill Registry / Worker 声明校验 │
│ Context CompilerassembleWorkerContext
│ Commit黑板写入、表 rev、acceptance │
└─────────────────────┬───────────────────────┘
┌─────────────────────▼───────────────────────┐
│ Domain / Storage │
│ Book · Session · Blackboard · Snapshots │
│ Artifacts · RuntimeEvent history │
│ 文件资源books/…);向量检索可插拔 │
└─────────────────────────────────────────────┘
```
合成边界:
> Book/规格描述可以运行什么Session/快照保存实际发生了什么,相位机决定谁可动,总管决定下一步 invoke 谁Context Compiler 决定 Worker 看见什么Runtime 决定哪些修改正式生效。剧本 Skill 是可装载能力,不是系统固定流水线。
---
## 5. 系统核心模块
### 5.1 规格与 BookDefinition 侧)
- Book长期项目容器过程、资产、游玩状态
- `设计.worker集` accept 后 → 本实例 Worker 声明
- `instance` / `opening` 快照:规格与可选开局截面
- 导入导出、复制、从截面新开 play 线
细节:`book-storage.md``run-snapshot.md`
### 5.2 Session 与 Instance Manager运行侧
- 按规格创建 / 加载 Session
- 自动续作 `session.json` 与手动 `run-snapshots/`
- 消息 swipe / branch checkpoint
- 从 earlier 快照恢复后重 roll
### 5.3 Main AgentSupervisor
接收压缩后的任务视图(目标、相位、可调用能力、最近产物摘要等),经 tool loop 输出调度意图Runtime 校验后执行。
### 5.4 Phase RuntimeDynamic Scheduler 的具体形态)
执行总管决定并维护相位边界:
1. 校验 Worker id ∈ 声明
2. 编译上下文
3. 短生命周期执行 Worker
4. 收集写入请求 → 校验 / 合并 / 提交
5. 按 acceptance 可能进入 `waiting_user`
6. 预算:`maxBurst`后续补齐嵌套深度、重复任务检测、Token/调用预算
实现:`src/runtime/phase-machine.ts``phase-runtime.ts`
### 5.5 Worker
按任务实例化的短生命周期调用。保留的是事件、产物与状态修改,不是 Agent 对象。
### 5.6 Skill Registry
架构层两种能力层级:
- **包 / Orchestrator manifest**:能力发现、验收与进 play 门槛
- **Worker 契约**:声明驱动的输入输出、上下文段、工具边界
注册、加载、版本与磁盘格式见 Skill 系列文档;**具体剧本步骤不在本文**。
### 5.7 Context Compiler
确定性模块按任务、Worker 权限与实例状态构建 Context View。
当前实现:`inputTags` / `contextSegments` + `assembleWorkerContext()``context-assembly.md`)。
演进契约:
- **Context Trace**:记录装载了什么、来源、权限依据、因预算/权限排除了什么
- 逻辑分区语义public / narrative / characters / tables / artifacts / runtime可映射到 tag 与可见范围;实现仍以 tag 黑板为主
### 5.8 Blackboard
带类型与来源的运行状态tag 条目),不是全体 Agent 共享的大字符串。表字段使用 `value + rev + source`;正式写入经 Runtime。见 `tag-blackboard.md`
### 5.9 Artifact 与验收
较长或需反复编辑的内容以黑板 tag / 产物记录保存;`drafted → under_review → accepted|rejected|revision_requested`accepted 前不得当下游事实。
### 5.10 Commit and Validation
Worker 不直接改「当前事实」。路径:输出 → 声明/schema 校验 → 权限与 rev 合并 → 提交事件 → 更新黑板。结构化状态用受控写入;正文类产物仍记录来源与版本。
---
## 6. 数据持久化
当前以**状态快照为主**,会话内保留 `RuntimeEvent` history完整领域事件溯源投影 + 事件日志 + 周期快照)按分支/迁移/审计痛点再加深。
| 需求 | 机制 |
|------|------|
| 续作 | `session.json` |
| 规格截面 | `instance` / `opening` 快照 |
| 游玩存档 / 重 roll | `run` 快照load = 恢复到该档 |
| 同开局多线 | 保留 opening/instance 层,清空或分支 run 层 |
| 调试回放 | `RuntimeSession.history` |
分支不要求复制整库;记录父档 / 基准快照即可。
---
## 7. 信息隔离
隔离由 Runtime 强制Worker 只看到契约允许的 tag / 段。多角色模拟时,角色 Worker 不得看到他角私有记忆、未公开事件、总管隐藏调度信息。
Context Trace 是排查泄漏的主工具(契约已定,实现渐进)。
---
## 8. 技术栈(现状与边界)
| 层 | 现状 | 边界 |
|----|------|------|
| 语言 | TypeScript / Node.js | — |
| Agent Runtime | 自研main-agent、phase-runtime、llm | 领域对象不绑定外部 Agent 框架内部类型Mastra 等仅可作按层参考或远期适配 |
| 前端 | `web/` + Electron | 不强制 Next.js / assistant-ui可借鉴交互模式 |
| 存储 | 文件系统 Book JSON | SQLite/Drizzle 非近端前提 |
| Skill | `skills/` + registry | 包内剧本与系统内核分离 |
外部仓库按模块借鉴,见 `references.md`;许可证见仓库根 `THIRD_PARTY_NOTICES.md`
---
## 9. 术语对照
| 规划用语(抽象) | 本仓库用语(实现) |
|------------------|-------------------|
| CreationDefinition / DefinitionVersion | `设计.worker集` 截面;`instance` / `opening` 快照 |
| RunInstance | Book + Session + 黑板 + `run` 快照 |
| 导演 Skill / 能力包 | `orchestrator.md`UI 可选registry 一项) |
| 剧本层(动态) | 对话谈成的 Worker 集 + play 期 tool loop 调度(非死板选单) |
| Supervisor | Main Agenttool loop |
| Dynamic Scheduler | Phase Machine + Phase Runtime |
| Worker Factory | `run_worker` → 声明校验 → executor |
| Context Compiler | `assembleWorkerContext` / contextSegments |
| Blackboard | tag 黑板(`BlackboardItem` |
| Commit Layer | 写入校验、表 rev 合并、acceptance |
| Skill Registry | `skills/` loader + orchestrator manifest |
用户可见中文口径(禁止裸露内部 id`ui-glossary.md`
| 词 | 含义 | | 词 | 含义 |
|----|------| |----|------|
| **skill** | `SKILL.md` 定义的能力(设计期能力库中的一条) | | **Worker 声明** | accept 后的 `设计.worker集` |
| **worker** | 某 skill 被 invoke 的一次执行 | | **worker** | 一次 `run_worker` invoke |
| **stage** | 业务阶段:`design`(实例化)`play`(运行)`done` | | **stage** | `design` `play` `done`(创作 → 游玩 → 已完成) |
| **phase** | 运行相位:`idle` \| `running` \| `waiting_user` \| `done` \| `error` | | **phase** | `idle` \| `running` \| `waiting_user` \| `done` \| `error` |
不使用 **step** 指代设计步骤编号,避免与管道混淆。
--- ---
## 3. Agent tool loop ## 10. 文档地图
```text | 层级 | 文档 |
用户硬事件(输入 / 确认 / 验收) |------|------|
→ phase = runningtoolLoopBurst 计数归零 | **系统架构(本文)** | `architecture.md` |
→ while running && burst < N: | 运行内核 | `runtime-state-machine.md``tool-contracts.md``context-assembly.md``tag-blackboard.md` |
LLM(messages, tools) | 持久化 | `book-storage.md``run-snapshot.md` |
→ 循环 toolread_blackboard …)→ 结果 append 到 messages | UI | `ui-design.md``ui-glossary.md` |
→ 边界 toolrun_worker / ask_user / finish→ 退出 burst | 实现顺序 | `implementation-guide.md` |
→ 若需用户 → waiting_user | **产品体验路线** | **`px-roadmap.md`**PX0PX5 交付与 DoD |
``` | **日常场景 → P0** | **`daily-use-p0.md`**场景、功能表、P0 工作包) |
| 外部参考 | `references.md` |
- **burst 上限 N**:两次用户操作之间的最大推理轮数,非 Session 终身额度。 | **剧本 / 创作方法(非系统架构)** | `design-orchestrator-guide.md``creation-playbook.md` |
- **循环 tool**:不改 phase只追加 agent 对话。 | **Skill 包格式(非系统架构)** | `skill-format.md``orchestrator-skill-format.md``worker-skill-format.md``skill-design-guide.md``preset-format.md` |
- **边界 tool**:触发阶段机转移(等用户、跑 worker、结束
代码:`src/main-agent/tool-loop.ts``src/runtime/phase-runtime.ts`
目标态worker 执行也用 tool loop`submit` / `ask_user`),上下文仍由 Runtime 拼接,见 `docs/tool-contracts.md`
--- ---
## 4. 阶段机做什么 ## 11. 在现有内核上补齐(路线)
阶段机 **不是** 编排表执行器,而是: 已具备相位机、Agent tool loop、Skill loader、design-intake、Worker 声明、上下文拼装、表 rev、Web UI、快照 API。
```text 1. **PX0**:导演选择 UIAIRP + 长文/爽文主路径;**存档/续作硬稳定**;剧本层保持动态
1. Actor 门禁 此刻用户 / agent / worker 谁在场 2. **PX1PX2**:创作/游玩体验(可抄 UI副作用与多 run 线
2. Tool 边界 当前态允许哪些 tool 3. **PX3**Context Trace、调度预算、规格钉版本、E2E
3. 事实生命周期 draft → accepted用户才能 accept 4. **PX4PX5**:长文加深;隔离模拟 + 调试(新方法边界可用新导演包)
4. 等待原因 waitingReason 细分等什么 5. SQLite / 框架适配仅当痛点单开,不插入 PX0 关键路径
```
业务「先跑哪个 skill」由 **agent 在 tool loop 里决定**,不由 orchestrator 逐步剧本写死 权威表:[`px-roadmap.md`](./px-roadmap.md)
--- ---
## 5. 实例化skill 能力库 ## 12. 主要技术风险
实例化 = agent 在 `design` stage 按需 invoke **instantiate skill**(原「设计步骤」),不是固定 1→14 管道。 | 风险 | 解决边界 |
|------|----------|
```text | 上下文污染 | 全部经 Context Compiler补 Trace |
交互范式 skill → 产出 设计.run_skill清单run 阶段需要哪些 skill | 自然语言误改状态 | 结构化状态只走受校验写入 / Patch |
每个 run skill 倒推 → 缺什么 instantiate skill → agent invoke | 调度死循环 | burst、后续嵌套/重复/预算限制 |
declare_instance_ready → 进入 play stage | 规格更新污染旧档 | 实例钉规格截面;显式迁移 |
``` | 框架耦合 | 领域与持久化不保存外部 Agent 框架内部对象 |
orchestrator.md = **manifest**(有哪些 skill、约束、验收策略不是逐步思维链。
--- ---
## 6. 上下文拼接 ## 13. 实现进度(摘要)
**上半固定、下半动态**。规则在 skill 定义 + 实例 contextProfileRuntime 执行。
`docs/context-assembly.md`
---
## 7. 模块
```text
phase-machine.ts 纯函数event → phase边界规则
phase-runtime.ts Session IO、tool loop 驱动、worker 执行
main-agent/ tool loop、tool 定义
worker/executor.ts assembleWorkerContext、run skill
blackboard.ts tag 池
skills/loader.ts 解析 orchestrator + SKILL.md
book/ Book、快照、持久化
```
---
## 8. 数据流(目标态)
```text
选 orchestrator 包
→ designagent burst → invoke instantiate skills → 设计.* tag
→ declare ready → play
→ play用户输入 → agent burst → invoke run skills → 运行.* tag
→ 过程/资产/游玩 归档 Book见 book-storage.md
```
---
## 9. 边界
```text
Agent 调度 skillread_blackboard不传 inputTags不写黑板边界 tool 除外)
Runtime 拼接上下文;校验 tool写黑板执行 worker
Worker LLM 在拼接后的 prompt 内产出submit 写 declared outputTags
用户 approve、accept、输入
阶段机 只响应 event不调 LLM
```
---
## 10. 实现进度(摘要)
```text ```text
✅ phase-machine、phase-runtime、skills loader ✅ phase-machine、phase-runtime、skills loader
总管 tool loopread_blackboard、run_worker、… design-intake + Worker 声明校验
⬜ toolLoopBurst 按用户事件归零 ✅ 声明驱动 worker 执行 + acceptance
⬜ assembleWorkerContextcontextSegments ✅ 表字段格 rev 合并
⬜ worker tool loop ✅ Book session / run-snapshots / message branch
⬜ BookdesignTrace / CardAsset / PlayBook ✅ 导演选择 UI新建作品instance/run 存档人话 kind
⬜ orchestrator 从编排表迁为 manifest ✅ 长文模板 outline / chapter-writer引导覆盖爽文/分段
✅ 失败/revision/error 出口;黑板用户手改 API
⬜ Context Trace
⬜ 规格版本发布与迁移协议
⬜ 调度嵌套 / 重复任务 / Token 预算补齐
⬜ 边沿副作用完整调度器
⬜ 标准 fixture E2E双线黄金路径自动化
``` ```

View File

@@ -0,0 +1,435 @@
# 能力撰写交接简报(泛用)
> **用途**:把本文**整份**交给另一 AI / 作者,用于撰写任意【能力】的 `modules/{id}/prompt.md`。
> **本文是自洽规范**:不依赖读者已熟悉本仓库。
> **不要**让撰写方改程序代码;**不要**只写某一能力的特例而不遵守通用格式。
范例(深度与结构对齐):`skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md`
能力池清单(选题用):`docs/world-simulator-modules.md`
术语权威:`docs/ui-glossary.md` §0
---
# 第一部分:项目是什么
## 1.1 一句话
**writing-agent** 是本地运行的多 Agent **文本创作 / 游玩**系统:用户选一个【导演】,在【创作】期谈成【剧本】,再在【游玩】期让【演员】按剧本上场,产出可读文本(角色扮演、长文/爽文、扩写、思想实验等共用同一内核)。
## 1.2 要解决什么问题
| 要 | 不要 |
|----|------|
| 用程序控制「每一步注入什么上下文」 | 把整本长说明书一次塞进 LLM |
| 用 Agent 按用户体验**编排**用哪些能力、什么顺序 | 死板选单 DAG或全程让 LLM 自由发明工序 |
| 产物可解析、可渲染、对人可读 | 产物里堆满英文变量名让用户难受 |
| 规格(能复用的声明)与一局游玩过程分离 | 每种玩法重写一套运行时 |
## 1.3 两段生命周期
```text
【创作 design】选导演 → 编排剧本(步骤=能力)→ 逐步执行能力 → 谈成可运行规格
▼ 用户手动进入
【游玩 play】用户输入 → 演员按规格上场 → 可见终稿;可存档 / 重 roll
```
- **创作**:谈清「这局要什么体验、用哪些能力钉成什么产物」。
- **游玩**:按已验收规格跑;用户新输入通常视为认可上一轮展示(否则重 roll
## 1.4 运行时怎么分工(撰写能力时必须懂)
| 角色 | 做什么 | 不做什么 |
|------|--------|----------|
| **导演Agent** | 决定下一步调哪个演员;编排剧本步骤 | 不直接写小说正文;不私自改相位 |
| **程序Runtime** | 校验权限;按契约拼上下文;切步;发 opening验收门控 | 不替用户发明体验 |
| **能力modules** | 某一步「怎么和用户谈、产出什么形状」的方法正文 | 不是整场流水线 |
| **演员Worker** | 一次 LLM 执行(创作期跑能力,或游玩期跑声明内 ref | 不决定全局调度 |
关键路径:
```text
用户选【导演】(如世界模拟器)
→ design-flow从【能力】排出**近期**步骤,写出 设计.创作流程
(可变增量 DAG每步 id + 固定中文名 + depends_onstatus=open|closed
→ 用户验收近期流程
→ 反复 design-step程序注入「当前能力」的 prompt 切片 + 依赖产物
→ 步骤做完仍 open → 再 design-flow可追加同能力多次如生成规则 / 具体实例)
→ closed 后收成可进游玩的规格,如 设计.worker集
→ 用户手动进游玩
```
## 1.5 磁盘上两层内容(作者视角)
```text
【导演】recipes/{导演id}/recipe.yaml 该方法的建议近期起点 / 调味说明
【能力】modules/{能力id}/prompt.md 共用工序;多导演可选用同一能力
modules/catalog.yaml 能力目录:中文名 + 短声明 + 产物 tag+ 可选 repeatable
```
- 新建作品**只选导演一层**,不再叠「能力包 + 配方」。
- 导演给出的步骤是**近期起点**;本局可增量追加、可反复调用标了 `repeatable` 的能力;验收的是本局【剧本】,不是磁盘死菜谱。
---
# 第二部分:称呼定义(必须统一)
对用户与能力正文,优先用**拍摄隐喻**。括号内为内部/旧称,撰写时勿当用户主词。
## 2.1 四个核心词
| 称呼 | 含义 | 内部大致对应 | 禁止对用户说 |
|------|------|--------------|--------------|
| **导演** | ① 新建时选一次的方法起点(世界模拟器、扩写助手…);② 会话里负责调度的 Agent | `recipes/`、Main Agent / orchestrator | 总管、配方(作选型主词)、能力包(作第二层选项) |
| **剧本** | 本局谈成的流程与规格(活的) | `设计.创作流程` + 各能力产物 + 终稿 `设计.worker集` 等 | 「剧本 Skill 菜单」、死板流程卡 |
| **演员** | 上场执行的一次单元 | Worker / `run_worker` | 对用户堆 `Worker · english-id` |
| **能力** | 共用工序模块;导演从池里选型编排 | `modules/{id}/` | 组件池、模块(作者文档可用;用户文案用「能力」) |
关系口诀:
```text
选导演 → 用能力编排 → 谈成剧本 → 演员按剧本上场
```
## 2.2 阶段与界面
| 称呼 | 含义 |
|------|------|
| **创作** | lifecycle `design`:谈剧本 |
| **游玩** | lifecycle `play`:演员上场 |
| **产物** | 某步待验收输出(写入黑板 tag |
| **验收 / 接受** | 用户确认产物算数 |
| **黑板** | 按 tag 存正文的上下文板 |
| **询问卡** | 结构化提问(选项可改写) |
## 2.3 创作调度相关(可出现在作者文档,少直接甩给用户)
| 内部 id | 用户可见 | 含义 |
|---------|----------|------|
| `design-flow` | 创作 · 流程编排 | 编排/增量修订可变 DAG → `设计.创作流程` |
| `design-step` | 创作 · 执行步骤 | 执行剧本中当前那一个能力 |
| `opening-generator` | 开局 · 开场白 | 可选;规格收成后的开场白 |
## 2.4 剧本流程 JSON编排产物增量 DAG
```json
{
"brief": "可选:一句话体验复述",
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
{ "id": "生成规则", "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] }
]
}
```
| 字段 | 规则 |
|------|------|
| `status` | `open` = 还可追加;`closed` = 不再扩步。缺省兼容旧稿视为已收口 |
| `steps` 顺序 | 建议执行顺序 |
| `id` | 本局步骤唯一键;同能力多次必须不同 |
| `name` | 必须与能力目录中的**固定中文名**完全一致(可重复) |
| `depends_on` | 依赖的其它步骤 **id**(若某 name 在本流程唯一,也可写 name |
程序用中文 `name` 映射到 `artifact` tag流程 JSON **不写**英文模块 id / tag。
**禁止**一次排死全程固定长链;「生成规则」「具体实例」等标了 `repeatable` 的能力应允许多次编入。
## 2.5 已合并能力(勿拆回)
| 错误拆法 | 正确能力 |
|----------|----------|
| 「交互范式」+「美学纲领」两步 | **美学纲领与交互范式**`aesthetics-interaction`)一步 |
询问相近则合并为一步;不要在新能力里建议再拆开。
## 2.6 命名纪律
| 种类 | 规则 | 例 |
|------|------|-----|
| 能力中文名 | 稳定、给人看、进流程 JSON | `实现机制` |
| 能力 id | 英文 kebab= 文件夹名 | `mechanism` |
| 产物 tag | 通常 `设计.{中文名或约定名}` | `设计.实现机制` |
| 游玩演员 ref | 英文 kebab | `narrator` |
| 游玩演员展示名 | 中文 `name` | `叙事转述` |
| UI | 禁止裸露内部英文 id | 不要写 `design-step` 给用户 |
---
# 第三部分:什么是「能力」(泛用定义)
## 3.1 定义
**能力** = 创作期可被编排进剧本的**一步工序**
1. 有固定中文名与产物 tag
2. 有一份程序可切割的方法文档(`prompt.md`
3. 执行时只注入**本能力**方法 + **依赖产物** + 用户表述;
4. 与用户对话后产出**可验收**的结构化结果。
能力**不是**:整场游戏引擎、游玩期每轮演员、也不是导演本身。
## 3.2 能力在系统中的生命周期
```text
catalog 登记(短声明给编排看;可选 repeatable
→ 导演/编排增量选入 设计.创作流程(可同能力多次)
→ 用户验收流程(或后续扩步后再验)
→ 轮到该步:若有 opening → 程序先发出
→ 用户首答 → LLM 按能力方法谈/写
→ 写入 artifact tag → 用户验收
→ 下游能力可读该产物(若 depends_on 声明了)
→ 不够则再 design-flow 追加同能力新 id
```
## 3.3 能力作者要交付的两样东西
| 交付物 | 路径 | 给谁看 |
|--------|------|--------|
| 目录行 | `modules/catalog.yaml` 一行 | 编排:只看短 `declaration` |
| 方法全文 | `modules/{id}/prompt.md` | 执行该步的 LLM + 程序切割 |
编排时**禁止**把全文 prompt 塞进导演上下文;执行时才注入切割后的方法块。
## 3.4 能力之间如何相处
- **正推**:需要 [体验/产物] → 才纳入某能力;勿默认全选池内能力。
- **边界**:每个能力在 `meta.boundary` 写清「本步定什么 / 不定什么 / 交给谁」。
- **依赖**:由剧本 `depends_on` 声明;执行期程序注入对应产物。
- **合并**:询问主题高度重叠 → 合成一个能力(如美学+交互)。
- **缩减**:每增必问能否删、能否常驻替代、能否合并。
## 3.5 方法层硬禁止(写任何能力都适用)
- 题材 → 固定演员清单。
- 否定式路由(「因为是 X 所以不需要 Y」应写「需要 A 体验 → 用 B 手段」。
- 把「世界模拟」当成一切开场的默认答案。
- 为排版/格式单独发明演员;能程序拼则程序拼。
- 临时发明 tool。
- 一次 burst 写齐全部能力产物。
- 在能力产物里堆大量用户看不懂的英文变量名(机器 id 可有,但须有中文展示字段)。
---
# 第四部分:能力文档格式规范(程序契约)
每个能力 = `modules/{id}/prompt.md` + `catalog.yaml` 一行。
程序**只认 fence 的语言标签**切割,**不认**仅靠 `##` 标题。
## 4.1 块一览
| fence 标签 | 必填 | 用途 |
|------------|------|------|
| `meta` | 强烈建议 | YAML 元数据:选型与边界 |
| `opening` | 可选 | **默认问题**;程序发给用户,**不经**本步 LLM |
| `task` | **必填** | 本步任务与验收边界 |
| `principles` | 强烈建议 | 原则 |
| `probe` | 强烈建议 | 追问策略 |
| `output` | **必填** | 产物形状(多为 JSON |
| `checklist` | 强烈建议 | 自检 |
| `examples` | 可选 | 好/坏对照 |
未列出的 fence 名:**不要发明**(除非先改程序切割器)。
## 4.2 标准骨架
````markdown
# {能力中文名}
> 可选说明。
## meta
```meta
name: {与 catalog 完全一致的中文名}
id: {文件夹 id}
artifact: 设计.{…}
declaration: >
{一行选型话术;与 catalog 对齐}
when: |
{什么情况下应被编排进来}
when_not: |
{什么情况下不要进来}
boundary: |
本能力:…
邻接能力:…(点名其它能力中文名,说明分工)
```
## opening
```opening
{用户第一眼看到的引导;整块可省略或留空 = 本步直接 LLM}
```
## task
```task
{只做本步;写清产物 tag如何对待 opening 首答与依赖产物}
```
## principles
```principles
{正推、缩减、禁止项、与体验的关系}
```
## probe
```probe
{缺什么问什么;一轮 12 点;优先具体选项/短场景;勿重复已答清}
```
## output
```output
{推荐产物 JSON 形状:中文键、可渲染}
```
## checklist
```checklist
- [ ] …
```
## examples
```examples
{可选}
```
````
## 4.3 `meta` 字段
| 字段 | 含义 |
|------|------|
| `name` | 固定中文名(= 流程 `steps[].name` |
| `id` | 英文 id= 目录名) |
| `artifact` | 本步写入的黑板 tag |
| `declaration` | 给编排看的短声明(不是全文) |
| `when` / `when_not` | 何时该/不该入选剧本 |
| `boundary` | 与邻接能力的分工 |
## 4.4 `catalog.yaml` 一行
```yaml
- id: example-id
name: 示例能力中文名
declaration: >-
一句话说明何时选用、解决什么
artifact: 设计.示例能力中文名
```
| 字段 | 谁用 |
|------|------|
| `declaration` | 插入导演/编排提示,选型 |
| `name` / `artifact` | 流程与执行映射 |
| `opening` | 可选覆盖;一般只写在 prompt 的 `opening` 块 |
改 `name` 会破坏已有剧本 JSON尽量不改。
## 4.5 opening 节奏(通用)
```text
若 opening 非空:
程序发 opening → 用户首答 → 再调 LLM
LLM 上下文含:方法块(通常不含再贴一遍 opening 策略以产品为准)
+ 首答 + 依赖产物 + 用户需求
task 必须写:禁止重复同一开场;在首答上补洞。
```
## 4.6 注入 LLM 时包含哪些块
按顺序拼接切割结果,**默认不含** `opening`(开场已由程序发出)。
`meta` 是否注入以运行实现为准;撰写时把给人/模型的方法写在 `task`/`principles`/`probe`/`output`/`checklist`。
## 4.7 产物(`output`)通用要求
- 形状稳定,便于 UI 按 JSON 渲染。
- **键名对人友好**(中文或稳定中文标签)。
- 机器 id如 `ref`)若需要,与中文名成对出现。
- 写清「本步完成的定义」;未决放 `开放问题`,不要假完备。
- 不要在本能力产物里偷偷交下游能力该交的终稿(除非本能力就是收成步)。
## 4.8 追问(`probe`)通用要求
- 一轮 askUser **12** 点。
- 优先:可直接采用或微调的完整句选项 / 短场景。
- 能推断则先写入再复述;只问影响本步核心且不能瞎填的点。
- 依赖产物已钉死的内容禁止重问。
---
# 第五部分:撰写任一能力时的工作步骤
1. **定身份**中文名、id、artifact写清 when / when_not / boundary。
2. **看邻接**:上/下游能力各定什么;禁止重叠问卷。
3. **定产物形状**:先定 `output` JSON再写 task/probe 如何填满它。
4. **定 opening**:需要程序先问再 LLM → 写 opening否则留空。
5. **写 principles / probe / checklist**:正推、缩减、禁止套件。
6. **对照范例**:深度不低于 `aesthetics-interaction`。
7. **自检**§4 块齐全name 与 catalog 一致;用户文案无裸英文 id。
---
# 第六部分:给撰写 AI 的通用任务指令(粘贴用)
请撰写(或重写)一个【能力】文档:
`skills/dialogue/world-simulator/modules/{id}/prompt.md`
(具体中文名 / id / 边界由出题方在指令末行指定。)
必须遵守:
1. 本文**第一部分~第四部分**的项目理解、称呼与格式契约。
2. 只使用规定的 fence 块;不要发明新 fence 名。
3. 对用户话术使用导演/剧本/演员/能力;不要写「总管调度 design-xxx」。
4. 正推:体验 → 手段;禁止题材套件与否定式路由。
5. 交互与美学已合并为「美学纲领与交互范式」;禁止建议拆回两步。
6. 不要修改程序代码;默认只交付 `prompt.md`(除非出题方要求改 catalog
7. 输出完整 Markdown 文件正文。
出题方填写:
```text
能力中文名:
id
artifact
上游依赖(常见):
明确不做(交给谁):
特殊产物要求(可选):
```
---
# 第七部分:当前能力池速查(选题,非本文重点)
世界模拟器常用(编排按需,勿默认全选):
| 能力 | id | 状态(以仓库为准) |
|------|-----|-------------------|
| 美学纲领与交互范式 | `aesthetics-interaction` | 范例 |
| 实现机制 | `mechanism` | 待细写 |
| 世界蓝图与人文地理 | `world-blueprint` | 已写 |
| 生成规则 | `generation-rules` | 待细写 |
| 具体实例 | `concrete-instances` | 待细写 |
| 拓扑图谱 | `topology` | 待细写 |
| 叙事指南 | `narrative` | 待细写 |
| 变量设计与更新规则 | `variable-design` | 待细写 |
| 变量控制上下文 | `variable-context` | 待细写 |
| 设计状态栏 | `status-bar` | 待细写 |
| 设计回复格式 | `reply-format` | 待细写 |
| Worker 规格 | `worker-spec` | 待细写 |
| 细化终稿 | `refine` | 待细写 |
导演示例:世界模拟器、扩写助手(见 `recipes/`)。
---
# 附录:与旧文档关系
| 文档 | 关系 |
|------|------|
| 本文 | **泛用**能力撰写 + 项目/称呼;给外部 AI 的主交接 |
| `world-simulator-modules.md` | 仓库内清单与格式摘要 |
| `ui-glossary.md` | 用户可见文案权威 |
| `architecture.md` | 运行内核;写能力时不必复述实现细节 |
| 旧 `capability-mechanism-handoff.md` | 已过时为「单能力特例」;以本文为准 |

View File

@@ -0,0 +1,20 @@
# (已迁移)实现机制单能力简报
本文件原先只讲「实现机制」特例,**已废弃**。
请改用泛用交接简报:
**[`capability-authoring-brief.md`](./capability-authoring-brief.md)**
其中包含:项目概述、导演/剧本/演员/能力称呼、泛用能力格式规范,以及可粘贴给其它 AI 的任务指令。
若要写「实现机制」,在泛用简报第六部分末行填写:
```text
能力中文名:实现机制
idmechanism
artifact设计.实现机制
上游依赖(常见):美学纲领与交互范式
明确不做(交给谁):拓扑图谱 / Worker 规格 / 变量* / 状态栏 / 回复格式 / 细化终稿
特殊产物要求(可选):总览最小可运行结构;含「为何需要」「未纳入与原因」;正推禁止默认世界模拟套件
```

View File

@@ -11,14 +11,19 @@ Worker / skill 执行时Runtime 将黑板 tag 与固定体裁说明拼成 LLM
## 2. 上半固定、下半动态 ## 2. 上半固定、下半动态
每条 worker prompt 分为两段 每条 worker prompt 自上而下拼接(**一段式 stack越稳定越靠上**
```text ```text
┌─ Preset会话 presetId────────────────────────────┐
│ 生成参数、prompt 条目system / 文风片段等) │
│ 见 preset-format.md与 skill 包正交 │
└────────────────────────────────────────────────────┘
┌─ 上半固定上下文Static────────────────────────┐ ┌─ 上半固定上下文Static────────────────────────┐
│ shared-context.md包级体裁约束 │ shared-context.md包级体裁约束
│ worker SKILL.md 正文(能力说明、自检) │ │ worker SKILL.md 正文(能力说明、自检) │
│ contextSegments 中 tier=static 的 tag │ │ contextSegments 中 tier=static 的 tag │
│ 例:角色卡.确认稿、世界.蓝图、设计.交互范式 │ 例:角色卡.确认稿、世界.蓝图、设计.worker集 切片
│ (含 narrator 的 presentation无单独美学纲领 tag
└────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────┘
┌─ 下半动态上下文Dynamic────────────────────────┐ ┌─ 下半动态上下文Dynamic────────────────────────┐
│ contextSegments 中 tier=dynamic 的 tag │ │ contextSegments 中 tier=dynamic 的 tag │
@@ -30,7 +35,7 @@ Worker / skill 执行时Runtime 将黑板 tag 与固定体裁说明拼成 LLM
原则: 原则:
```text ```text
越稳定、越少改 → 越靠上static 越稳定、越少改 → 越靠上(preset / static
越增量、每轮变 → 越靠下dynamic 越增量、每轮变 → 越靠下dynamic
``` ```
@@ -80,7 +85,11 @@ contextSegments:
| `label` | 拼进 prompt 的 Markdown 标题(可选) | | `label` | 拼进 prompt 的 Markdown 标题(可选) |
| `policy` | 动态段裁剪,见 §5 | | `policy` | 动态段裁剪,见 §5 |
未声明 `contextSegments`Runtime 回退:按 `inputTags` 顺序输出 JSON `inputs`(当前实现) 未声明 `contextSegments`Runtime 回退:按 `inputTags` 顺序输出 JSON `inputs`
`contextSegments` 时:按 static → dynamic 顺序拼 Markdown`label` 标题);实现见 `src/skills/context-segments.ts``assembleWorkerContext()`
创作前情:`创作.已验收内容` 存各单位**最后一次验收**的内容切片,拼装时格式化为只读前情提要。
--- ---
@@ -93,17 +102,20 @@ contextSegments:
| `tail_lines_N` | 事件流等取最后 N 行 | | `tail_lines_N` | 事件流等取最后 N 行 |
| `tail_tokens_N` | 按估算 token 截断(预留) | | `tail_tokens_N` | 按估算 token 截断(预留) |
上下文过长时: 上下文过长时**本节管 Run worker 拼装**;创作会话见 `design-orchestrator-guide.md` §7.2
1. 优先靠 policy 裁剪动态段 1. 优先靠 policy 裁剪动态段
2. 实例 manifest 可覆盖 variant`historyPolicy: last_10_turns` 2. **Run 验收后压缩**:过程 tag 归档,仅终产物 + `上下文.定稿摘要` 进入下一 worker`compress-after-worker.ts`
3. agent 可 invoke 显式 **compress-history** skill 写摘要 tag调度 skill不是随手删 prompt 3. 实例 manifest 可覆盖 variant`historyPolicy: last_10_turns`
4. 长线再考虑显式 compress-history / RAG禁止「完整历史喂一个 LLM 再筛给另一个」
**创作**不走本拼装栈:讨论放 session `messages`;单位验收后 **删交互、留产物**。Worker 执行仍始终按契约从黑板重装。
--- ---
## 6. contextProfile实例 manifest ## 6. contextProfile实例 manifest
实例化阶段产出(写入 `设计.run_skill清单` 或 Book manifestagent **只选预置档位**,不列 tag 实例化阶段产出(写入 `设计.worker集` 或 Book manifestagent **只选预置档位**,不列 tag
```json ```json
{ {
@@ -161,7 +173,7 @@ user:
{按 segment 顺序格式化的 Markdown 或结构化块} {按 segment 顺序格式化的 Markdown 或结构化块}
``` ```
实现:`src/worker/executor.ts``assembleWorkerContext()`(待从纯 JSON inputs 升级) 实现:`src/skills/context-segments.ts``assembleWorkerContext()`;由 `src/worker/executor.ts` 调用
--- ---

View File

@@ -1,10 +1,13 @@
# 创作流程指南Creation Playbook # 创作流程指南Creation Playbook
> **文档层级:剧本 / 创作流程概念(非系统架构)。**
> 系统模块与边界见 [`architecture.md`](./architecture.md)。
## 0. 内容在哪 ## 0. 内容在哪
**流程定义在 skill 包里**orchestrator manifest + `workers/*/SKILL.md`)。本文只保留概念。 **设计方法(正推、三大步、表与副作用)****`design-orchestrator-guide.md`**(权威)。
**流程落地**在 skill 包(`orchestrator.md` + `design-intake`)。
写新包 → `skill-design-guide.md` → 新建 `skills/.../orchestrator.md` 写新包 → `skill-design-guide.md`(包格式薄层)→ `skills/.../orchestrator.md`
--- ---
@@ -14,30 +17,52 @@
Orchestrator 包 能力库 + manifest静态 Orchestrator 包 能力库 + manifest静态
黑板 tag 一次 Session 的实参(动态) 黑板 tag 一次 Session 的实参(动态)
Book 跨 Session过程、资产、游玩 Book 跨 Session过程、资产、游玩
设计.worker集 实例规格JSONaccept 后 = Worker 声明)
``` ```
| 层 | 管什么 | | 层 | 管什么 |
|----|--------| |----|--------|
| **运行相位** | `idle` / `running` / `waiting_user` — 系统在等什么 | | **运行相位** | `idle` / `running` / `waiting_user` — 系统在等什么 |
| **业务 stage** | `design`实例化)→ `play`运行)→ `done` | | **业务 stage** | `design`创作)→ `play`游玩)→ `done` |
| **Agent** | tool loop 内 invoke 哪个 skill | | **Agent** | tool loop 内 invoke 哪个 worker |
| **Skill** | 读哪些 tag、写哪些 tag、上下文怎么拼 | | **声明** | play 可调度哪些 refexecutor 读声明(非题材管道) |
| **Book** | 长期存储,见 `book-storage.md` | | **Book** | 长期存储,见 `book-storage.md` |
--- ---
## 2. 启动 ## 2. 默认包创作流
```text ```text
skill_selection 选 orchestrator 包 新建作品 → UI 引导 → 用户首句 → 创作单位逐步谈
intake 启动询问(最小信息) → 宜phase:core → 纲领类 fixed:* → worker:* → refine
design stage agent 按需 invoke instantiate skill (非死管道;可穿插,但禁止默认「先写完 worker 再填上下文」)
declare ready 进入 play → 单位 = 已写出的 fixed:* / resident:* | worker:*(示例话题可问用户,非填空)
play stage 用户输入 → agent burst → run skill → 单位跑:只写 设计.worker集.草稿;讨论进 messages
done 归档 Book → 用户接受 → 删本单位交互消息,只留产物;草稿保留
→ ……谈完后终稿写 设计.worker集 → 验收后才可进 play
→ (可选)开局 · 开场白 → 用户手动进游玩
→ runworker 按契约从黑板重装acceptance=review 处停、压缩过程 tag
``` ```
`startupCompleted` / `instanceReady`agent 声明 + 程序校验「当前 run_skill清单 可运行」,非固定 prerequisite 打勾 固定上下文 tag 的主收益是 **跨 worker 复用同一份正文**;写下游时仍依赖上游定稿(不能只报 tag 名省掉正文)。能省的是扯皮过程(验收折叠)
不再要求用户选择 skill 包;默认 orchestrator 见 `src/config/default-orchestrator.ts`
方法细节见 **`design-orchestrator-guide.md` §7.2**。不要以「世界模拟器」为默认总形态。
### 进入游玩
- Worker 集 **accepted**(试验期最低门槛)
- **进 play 由用户手动决定**;开局锁定非强制
### 编辑与重 roll
```text
关键节点可存快照
不满意 → 加载 earlier 快照 / 重 roll
play重 roll 替代每轮验收;新输入 = 隐式认可上轮终稿
```
`run-snapshot.md`
--- ---
@@ -45,9 +70,9 @@ done 归档 Book
| | Agent | Runtime | | | Agent | Runtime |
|--|-------|---------| |--|-------|---------|
| 决定 | invoke 哪个 skill、何时 ask_user/finish | — | | 决定 | invoke 哪个 worker、何时 ask_user/finish | — |
| 拼接上下文 | 只用 read_blackboard 辅助决策 | assembleWorkerContext | | 拼接上下文 | 只用 read_blackboard 辅助决策 | 常驻上下文 + 声明拼装 |
| 写黑板 | 否(边界 tool 驱动 worker 写) | 校验 outputTags 后写入 | | 写黑板 | 否(边界 tool 驱动 worker 写) | 校验后写入;表字段尊重 rev |
Agent **不指定 inputTags**。见 `context-assembly.md` Agent **不指定 inputTags**。见 `context-assembly.md`
@@ -55,29 +80,16 @@ Agent **不指定 inputTags**。见 `context-assembly.md`。
## 4. Tool loop burst ## 4. Tool loop burst
每次用户硬事件后agent 进入 `running`,在 **burst 上限内** 多轮 tool碰到边界 tool 或需用户则停。 每次用户硬事件后agent 进入 `running`,在 burst 上限内多轮 tool碰到边界 tool 或需用户则停。
`tool-contracts.md``runtime-state-machine.md` `tool-contracts.md``runtime-state-machine.md`
--- ---
## 5. 角色卡(一种 Book 形态) ## 5. 相关
```text
创建过程 designTrace + 对话 → CardBookdesign
创建结果 角色卡.确认稿 等 → CardAsset可导入资产
游玩过程 playTrace + runState → PlayBook
游玩存档 PlaySnapshot
```
不拆独立 author/play 包;见 `book-storage.md` §角色卡。
---
## 6. 相关文档
| 文档 | 关系 | | 文档 | 关系 |
|------|------| |------|------|
| `architecture.md` | 总览 | | **`design-orchestrator-guide.md`** | ★ 创作设计方法 |
| `skill-design-guide.md` | 设计方法 | | `skill-design-guide.md` | 包与字段格式 |
| `context-assembly.md` | 上下文拼接 | | `run-snapshot.md` | 存档 |
| `tag-blackboard.md` | 标签 | | `tag-blackboard.md` | 标签 |

274
docs/daily-use-p0.md Normal file
View File

@@ -0,0 +1,274 @@
# 日常使用场景 → P0 详细路径
## 0. 怎么用本文
1. **先定场景**日常到底在干什么§1
2. **再列功能**每个场景要哪些能力、是否已有§2
3. **收成 P0**:只把「日常阻断 / 高频刚需」推进 P0 详细路§3
4. **其余进后续**:体验打磨与加固,不塞进 P0§4
系统边界见 `architecture.md`;总路线见 `px-roadmap.md`
### 0.1 已确认取向2026-07-26
| # | 结论 |
|---|------|
| 1 | 日常包含 **两类**AIRP/交互扮演 **与** 长文/爽文创作(同一运行内核) |
| 2 | **UI 不自研美学**:布局/控件直接抄参考产品即可;**存档与续作稳定是硬指标** |
| 3 | **导演**:新建时用 UI **选一个**(世界模拟器 / 扩写助手…;初期可只有 1 项) |
| 4 | **剧本动态**:不在 UI 里点选死板「剧本菜单」选定导演后由导演Agent听用户说话谈出本局剧本创作流程 / Worker 集)与调度 |
用户侧拍摄术语权威对照:**`ui-glossary.md` §0**(导演 / 剧本 / 演员 / 能力)。
### 0.2 导演 vs 剧本(动态)
```text
导演 = 用户 UI 选一次的方法起点(可调味)
= 内部 recipes / 原「能力包·配方」合一
= 例:世界模拟器、扩写助手
剧本 ≠ 再选一张固定流程卡
= 选定导演之后:用户说话 → 导演调度创作
→ 产出 设计.创作流程 + 设计.worker集本局活剧本
→ 游玩再按声明让【演员】上场
能力 = 共用工序(美学纲领与交互范式…);导演从中编排
演员 = 实际上场的 Worker
```
| | 导演 | 剧本(动态) |
|--|------|----------------|
| 谁选 | **用户 UI**(显式,一层) | **导演 Agent + 用户对话**(隐式) |
| 变不变 | 选项相对稳定 | 每局流程、Worker 集、回合调度都可变 |
| 忌讳 | 再叠一层「配方」选择 | 固化成死 DAG / 死选单 |
| 本仓库对应 | `recipes/` + 默认 skill 包 | `设计.创作流程` / `设计.worker集` |
因此:**只给导演一个选择 UI****不要**再给「剧本 / 配方」做第二层死板选择器。
**多导演**才有意义的情况:方法边界真不同(世界模拟 vs 扩写助手),而不是同一导演内的题材分化——后者留在剧本层动态谈成。
---
## 1. 日常使用场景(产品视角)
### 1.1 主场景P0 对准)— 两条产品线,同一内核
#### 线 AAIRP / 交互扮演
| ID | 场景 | 用户在干什么 |
|----|------|--------------|
| **A1** | 开新档创作 | 描述想扮演/交互什么,谈到可玩的 Worker 集 |
| **A2** | 验收定稿 | 接受/重来/跳过询问,直到可进游玩 |
| **A3** | 进游玩 + 多回合 | 读终稿 → 输入 → 再生成;偶发重 roll |
| **A4** | 续作 + 存档读档 | 关掉再开接着玩;关键节点存/读档 |
#### 线 B长文 / 爽文创作
| ID | 场景 | 用户在干什么 |
|----|------|--------------|
| **B1** | 开新档创作 | 描述题材/爽点/篇幅;站位偏「写手/统筹」或「助手分段」 |
| **B2** | 验收定稿 | 钉大纲/章结构/写手类 Worker 与固定上下文,可进「写」 |
| **B3** | 分段出文 | 按大纲或指令生成章/节正文;不满意重 roll 该段 |
| **B4** | 续作 + 存档读档 | 连载式接着写;章节点存档、读档不丢正文与进度 |
A4 / B4 **共用**同一套存档语义(`session.json` + `instance`/`run` 快照P0 把稳定性做硬。
### 1.2 次场景P0 不精做)
| ID | 场景 | 说明 |
|----|------|------|
| S7 | 改表 / 状态面板 | 有就不炸;精致编辑 → 后 |
| S8 | 改规格再开写/玩 | 有 instance 加载即可,流程打磨 → 后 |
| S9 | 开局 swipe / 多版本对比 | 有更好,非 P0 闸门 |
| S10 | 预设商店式管理 | 失败可读即可 |
### 1.3 明确非 P0
| ID | 说明 |
|----|------|
| N2 | 狼人杀级信息隔离 |
| N3 | Context Trace 专业调试台 |
| N4 | 多导演**运营商店**P0 只要选择 UI可仅 1 项) |
| N5 | DefinitionVersion 完整发布迁移协议 |
| N6 | SillyTavern 卡批量导入导出 |
| N7 | 自研精美 UI / 按 `ui-design` 大改布局(**抄参考即可** |
---
## 2. 场景 → 功能需求(含现状)
图例:✅ 基本有 🟡 有但糙/易踩坑 ❌ 缺或不可靠
### 共用:创作 / 验收 / 进出
| 功能 | 现状 | P0? |
|------|------|-----|
| 新建作品 → 选导演(能力包;初期可仅一项) | ✅/🟡 | **必做**(有选择 UI一项时默认选中即可 |
| 意图可走扮演 **或** 写手/爽文(剧本层动态,不另选死板剧本卡) | 🟡 | **必做** |
| 验收出口无死胡同 | 🟡 | **必做** |
| 进入游玩/写作阶段门槛清晰 | 🟡 | **必做** |
| UI 自研打磨 | — | **不做**;缺块就抄参考产品交互 |
### 线 A多回合扮演
| 功能 | 现状 | P0? |
|------|------|-----|
| 连续 ≥3 回合不卡死 | 🟡 | **必做** |
| 失败可重试 | 🟡 | **必做** |
### 线 B长文 / 爽文
| 功能 | 现状 | P0? |
|------|------|-----|
| 创作能产出「大纲 + 写章」类 Worker 声明(模板或谈成) | 🟡 | **必做**(不新开第二包;缺则补 **templates / 引导**,不是多包) |
| play 期能按段生成正文并展示 | 🟡 | **必做**≥2 段/章连续) |
| 章/段级重 roll 或读档回到段前 | 🟡 | **必做**与存档同一套load earlier → 重来) |
| 独立长文编辑器 / Monaco | ❌ | **不做**(抄现有主舞台展示正文即可) |
### 存档 / 续作(最高优先级)
| 功能 | 现状 | P0? |
|------|------|-----|
| `session.json` 续作相位正确 | 🟡 | **硬必做** |
| 手动存 / 列表 / 加载后可继续 | 🟡 | **硬必做**(线 A、线 B 各验一次) |
| 存档不丢终稿正文与关键 tag | 🟡 | **硬必做** |
| 分支树 UI | ❌ | 不做 |
---
## 3. P0 详细路径
**P0 一句话**:用户用 UI 选好**导演 Skill**初期可只有一个AIRP 与长文/爽文都在**剧本层动态**谈成并跑起来,且 **存档/续作稳定**UI 抄参考即可。
### 3.1 黄金路径(两条都要绿)
**线 A扮演**
```text
A-GP1 新建 → UI 选导演(可仅一项)→ 描述扮演/交互意图
A-GP2 验收至可进游玩
A-GP3 ≥3 回合输入→终稿
A-GP4 存 run 档 → 刷新/重开续作成功
A-GP5 加载 earlier 档或新开一条线之一可用
```
**线 B长文/爽文)**
```text
B-GP1 新建 → UI 选导演 → 描述爽文/长篇意图(写手或分段助手)
B-GP2 验收至可进写作Worker 集含大纲/写章类能力,勿只能聊天扮演)
B-GP3 连续生成 ≥2 段(章)正文
B-GP4 存档含已生成正文 → 续作能接着写下一段
B-GP5 回到 earlier 档重 roll 某段 或 等价恢复可用
```
### 3.2 工作包
#### WP-S 存档稳定(最先 / 贯穿)
| # | 项 | 验收 |
|---|-----|------|
| S1 | 续作:打开 Book = 恢复可操作相位 + 输入区与等待态一致 | A-GP4 / B-GP4 |
| S2 | 手动存:人话 kind写入后列表可见 | 同上 |
| S3 | 加载:黑板 + 消息 + 运行进度一致,可继续生成 | A-GP5 / B-GP5 |
| S4 | 回归:线 A、线 B 各完整存读一轮,不手改 `books/` | 双线绿 |
| S5 | 已知「存档丢正文 / 续作进死相位」类 bug 清零 | 无阻断 |
#### WP-D 导演选择(与存档并列早做)
| # | 项 | 验收 |
|---|-----|------|
| D0 | 新建作品可选导演registry仅 1 项时默认选中并可确认 | 进入该包创作 |
| D1 | 人话展示导演名/简介,不暴露路径 | 可选懂 |
#### WP-A 进出与失败出口(文案够用即可)
| # | 项 | 验收 |
|---|-----|------|
| A1 | 裸 id 不进用户主路径glossary 底线) | 无 `design-core` 裸露 |
| A2 | `waiting_user` 有主按钮出口 | 无死胡同 |
| A3 | 进游玩/写作门槛提示 | 误进可理解 |
| A4 | LLM/worker 失败:中文 + 重试 | 可回到可输入 |
#### WP-R 线 A 回合可靠
| # | 项 | 验收 |
|---|-----|------|
| R1 | ≥3 回合无卡死 | A-GP3 |
| R2 | 假死可刷新恢复且不腐档 | 与 WP-S 一致 |
#### WP-L 线 B 长文/爽文最小闭环
| # | 项 | 验收 |
|---|-----|------|
| L1 | 启动引导明确支持「写爽文/长篇/先大纲再写」 | B-GP1 |
| L2 | design 能稳定收到写手向 Worker 集(补 template 或 design 提示,**同一导演内动态谈成** | B-GP2 |
| L3 | play 连续 ≥2 段正文 | B-GP3 |
| L4 | 正文在存档/续作中仍在 | B-GP4 |
#### WP-U UI不雕
| # | 项 | 验收 |
|---|-----|------|
| U1 | 缺的存档列表/确认/重试控件:直接仿参考产品,不写设计长文 | 能点、能完成 GP |
| U2 | **禁止** P0 内按 `ui-design.md` 做大布局重构 | 范围不膨胀 |
#### WP-E 闸门
| # | 项 | 验收 |
|---|-----|------|
| E1 | 线 A、线 B 黄金路径各冷启动 + 续作通过 | 全绿 |
| E2 | 更新 `px-roadmap` / `architecture` 进度勾选 | 文档同步 |
### 3.3 P0 明确不做
- 第二套死板「剧本选单」/ 固定题材 DAG
- 自研精美 UI、工作台大改
- 隔离模拟、Trace 台、完整副作用调度、版本迁移协议
- 为文采大改全套提示词(仅修**阻断线 B 收不成写手规格**的引导/模板)
### 3.4 建议顺序
```text
1. WP-S 存档稳定(先修已知腐档)
2. WP-D 导演选择 UI可仅一项
3. WP-A 失败出口 / 进出
4. WP-R 线 A 回合
5. WP-L 线 B 长文最小闭环(引导 + template + 存档验正文)
6. WP-U 仅补抄来的缺口控件(含选导演/存档若未齐)
7. WP-E 双线验收闸门 → P0 完成
```
### 3.5 P0 DoD
- [ ] 线 AA-GP15 通过(含选导演、续作/读档)
- [ ] 线 BB-GP15 通过(含选导演、正文不丢)
- [ ] 导演选择 UI 可用(可仅一项)
- [ ] 无等待死胡同;失败可重试
- [ ] 不依赖手改磁盘文件
- [ ] UI 无大改承诺;存档稳定优先于观感
---
## 4. 日常功能 → 阶段归属(修订)
| 日常能力 | 阶段 |
|----------|------|
| 导演选择 UI可仅一项、剧本层动态谈规格、AIRP+长文主路径、**存档续作硬稳定** | **P0** |
| 抄来的 UI 缺口补齐 | **P0U** / 不够再后补 |
| 表编辑精修、改规格心流、副作用完整 | **PX1/PX2** |
| Trace、预算、版本协议、E2E | **PX3** |
| 长文「专业编辑器 / 评测 / 多卷」加深 | **原 PX4 收窄**P0 已含最小长文) |
| 隔离 + 专业调试 | **PX5** |
| 多个导演包(方法真不同时再加) | **需要时再加**;非「剧本菜单」 |
---
## 5. 假设状态
| # | 原假设 | 状态 |
|---|--------|------|
| 1 | 仅 AIRP | **已订正**+ 长文/爽文 |
| 2 | UI 要按设计稿打磨 | **已订正**:抄参考;存档稳定优先 |
| 3 | 永远自动加载、无选包 UI | **已订正**UI 选**导演**;剧本层保持动态 |
| 4 | 单包 = 不能多种玩法 | **已澄清**:一个导演内剧本动态分化 |

View File

@@ -0,0 +1,511 @@
# 创作设计指导(总管 / 分步 design skills
> **文档层级:剧本 / 创作方法(非系统架构)。**
> 运行内核边界见 [`architecture.md`](./architecture.md);本文不定义相位机、持久化或 Context Compiler。
权威说明:如何从用户意图收成 **实例规格(`设计.worker集` JSON**,并指导 play 期声明调度。
非固定创作流水线;`world-simulator` 等只是可选能力 ref不是默认总形态。
相关:`context-assembly.md`(拼装)、`preset-format.md`(全局预设)、`tag-blackboard.md`(黑板)、`run-snapshot.md`(快照)。
包内落地:
| 文件 | 角色 |
|------|------|
| `orchestrator.md` | 总管调度 |
| `design-common.md` | 创作共同开头(注入各 design-* |
| `workers/design-core/` | A 核心(开放,不拆 A1→A2→A3 管道) |
| `workers/design-worker/` | 一次一个 worker |
| `workers/design-fixed/` | 一次一块固定上下文 |
| `workers/design-refine/` | C 细化 / 终稿 |
**运行时以各 SKILL + design-common 为准**;本文是方法长文,改方法时与 skill 切片一起改。
---
## 0. 总原则
```text
三大步(每步含自检,可反复,无强制题材工序)
A 核心 → 站位 + 系统扮演/输出/交互 + 体验检验
B 细节交互 → Worker/常驻上下文能否撑起世界与交互;冲突请用户选
C 细化 → 钉死不能瞎发挥的关键前提;表/数据拓扑/副作用;用户是否满意
全程正推:需要 [体验] → 用 [常驻上下文 | 表 | worker | 程序 | 预设 | tool] 维持/产出
每增必问:能否缩减?常驻上下文能否替代?能否合并?黑板够不够/是否多余?
```
**禁止**
- 题材 → 固定 worker 清单;否定式路由;完整历史喂 LLM 再筛给另一 LLM
- 格式/排版专用 worker临时生成 tool默认 input 整理 worker
- 把「世界模拟」当成开场默认答案
- 固定 instantiate 管道world-blueprint 等)
**预设**:凡 LLM 请求(总管 / agent / worker均插入用户预设。
**进 play**:用户手动满意后进入(程序不强制开局锁定)。
**Play 回合**:用户新输入 = 认可上一轮最终展示;否则重 roll / 编辑后重 roll。表维护可延后。
**规格格式**`设计.worker集`**JSON**(权威存储);解析层可兼容旧 YAML 草稿,新产出必须 JSON。
---
## 1. 交互流程(先定「怎么和人轮转」)
输出本质可以都是「系统给出一截用户可见结果」,但 **轮转形态**不同,规格必须写明:
| 形态 | 典型 | 规格要点 |
|------|------|----------|
| 对话回合 | 最终展示一轮 ↔ 用户一轮 | 终稿 tag无每轮验收 |
| 助手分段 | 先大纲/细纲,用户填表再继续 | 表可编辑;阶段门控;大纲可走表副作用 |
| 其它 | 旁观多角、写手统筹… | 站位 + 系统扮演共同决定 |
用户可 **编辑表(精确到字段格)** → 必须有 **字段级版本**(见 §5
---
## 2. 三大步
### A. 核心
#### A1. 用户站位(扮演什么)
单角代入 / 代理操控 / 旁观·实验 / 写手统筹 / 多角切换…
→ 谁决策、输入如何解释、结构骨架。
#### A2. 系统扮演什么play agent 整体)
指整个 **play agent 系统** 要:
- **扮演什么**(世界执行者?对白对手?写作助手?陈述实验?)
- **输出什么**(叙事、摘要、选项、表、大纲块…)
- **怎么输出**(单段终稿 / 程序拼状态栏+正文 / 多面板)
- **怎么与用户交互**(回合对话、填表驱动、先大纲后正文…)
由此才谈:要不要转述、程序拼格式、表是否上屏等。
#### A3. 核心体验检验(首要评分,自检必做)
对当前理解与「现有 worker/上下文草案」打分或文字判定。
**对内**:可只在思维链里过一遍。
**对外**(总管 `ask_user.assessment`,或关键分叉复述):写成用户可读的完备度评价,再据此出题。
**首要(必须检)**
| 维 | 含义 |
|----|------|
| 核心体验 / 满足来源 | 用户最想反复感到什么;爽点从哪来(权力、求生、关系、解谜…)——**一切评价与追问以此为轴** |
| 用户–`<user>` 关系 | 用户与代入角色/叙述对象的关系是否清楚 |
| 焦点位置 | 注意力与镜头落在哪 |
**可选(有缺口才写,不必凑齐)**
| 维 | 含义 |
|----|------|
| 内容维度 | 情节/日常/战斗/推理…比重与情绪质感 |
| 人物与关系 | 核心关系动态 |
| 身体与感官 | 物质、亲密、暴力等描写边界 |
| 背景与规则 | 世界架与规则用途 |
| 意义与主题 | 快感 / 代价 / 终点 |
`assessment` 建议结构(按需删维):
```text
# 内容维度
核心感觉: 完备度 40%
已知: …
待探: 用户期望的【情绪质感】是【方向A】【方向B】还是组合
人物与关系: 完备度 50%
已知: …
待探: …
```
`questions`:只针对「待探」里必须用户拍板的点;一次 12 题。
- `prompt`:明确题干(「你更倾向于哪种…」)
- `options`**建议示范**(可含短场景钩子 + 点题句),用户可改写后采用;禁止空泛是/否
自问缺口是常驻上下文、表、worker还是必须问用户能推断就别问。
变造世界提示:用户常给「基础架 + 变种设定」;核心体验往往来自变种——从设定读内核,但 **不要**写成固定工序,也不要题材锁死体验。
#### A 步通用自检
见 §3。
---
### B. 细节交互
对照 A 的结论,检查草案:
1. **现有 worker+ 常驻上下文)能否满足需求?**
2. **能否支撑起整个世界/助手流程?**
3. **有无冲突必须用户选择?**(两套互斥满足来源、互斥交互形态等 → `ask_user`
仍用正推补洞;能固定上下文则不上 worker§6示例话题可问用户不是填空表
**顺序软规则**:纲领类固定上下文(风格/叙事/美学)宜先于大量 worker——同一份 tag 挂多个 worker而不是先列 worker 再填上下文。
然后 §3 自检。
---
### C. 细化
原则:只钉 **不能交给 AI 自由发挥、又对体验关键** 的东西。
变造世界已能推出的(已知现代 → 不问常规科技树)默认不问,除非用户提过。
常钉类(按需):规则与 **核心实现前提**、状态、**表结构**、**整体叙事指南**(≠ 文风:管世界态度与体验边界)、输入协议。
本步更盯 **数据**
- 什么要 **拓扑化**(依赖、阶段边、触发边)?
- 什么要 **数据化/进表**
- 表字段、显隐(折叠防剧透,非防用户)、维护规则、副作用是否清楚?
- **用户是否满意** 这些钉死项?
大纲:**不必**单独「大纲管道」。需要时用 **表 + 副作用**——例如表字段 `当前章=第一章` → 边沿触发插入「第一章细纲」上下文(与好感改人设同一套机制,见 §5
然后 §3 自检。
---
## 3. 每一步自检清单(三大步与每一小步共用)
每完成一截推理或改草案,强制过一遍:
| # | 问 |
|---|-----|
| 1 | **能否缩减?** 这条能力/字段/worker 删了是否仍成立? |
| 2 | **增加的必要性?** 删掉会丢掉哪段体验? |
| 3 | **上文是否足够** 辅助主 LLM 完成其 duty不足补常驻/表/黑板,而非盲目加工人 |
| 4 | **写入黑板的内容** 够不够?是否多余? |
| 5 | **能否与其它 worker 合并?****常驻上下文替代?** |
| 6 | **上下文插入是否按「变动/重要度」顺序?**(稳定块与易变块分层;参考 ST 注入位思路) |
| 7 | C 步加重)数据传播、表、副作用、字段版本是否自洽?有无「持续为真每轮触发」? |
复述给用户时:说明有什么、**为何没有** 某重型件。
---
## 4. 短板加装(表见 §5
| 短板 | 做法 |
|------|------|
| 可见文本质量 | 转述 worker **或** 常驻文风;能轻则轻 |
| 记忆 / 长线 / 大纲 / 随机分支 / 人设阶段 | **统一用表**§5维护 + 显隐 + 副作用插换上下文 |
| 信息差玩法 | 分角 worker极贵慎用 |
| 格式 | 程序拼 |
| 真随机 / 骰子卡组计算 | 用户触发或填入,或表;已有 tool 手动调;不临时造 tool |
| 用户不应知道的信息 | 程序固定注入,不进可见区;非 input 整理 worker |
| 输入协议 | 常驻上下文(`()` 元要求、`""` 对白、无包裹=事实等) |
长线记忆:**不上**则靠模型;**要上**则 RAG固定少设计或表禁止历史互喂筛选。
---
## 5. 表(结构 · 版本 · 维护 · 副作用 · 大纲)
### 5.1 用途
状态与物品、显隐信息、阶段/章节、随机与事件权重、人设态度段、**大纲/细纲挂载**、驱动常驻上下文插入或替换。
### 5.2 结构与显隐
- 扁平、利于二维表 UIschema 写在 Worker 集
- **显形 / 隐藏(可折叠)**:防剧透、分层观看,不是防用户
- 维护规则可执行,并带触发语义(默认 once + edge
### 5.3 字段格版本(防互相覆盖)
每一 **字段格**(非仅整表)含至少:
```text
value, rev, updatedAt, source # source: user | worker:<id> | system
```
合并规则:
- 用户编辑升 `rev``source=user`
- Worker 写回须带 **读时的 rev**;若当前 rev 已变 → **不覆盖**,跳过或 ask_user
- 禁止无条件用维护结果盖掉用户格
### 5.4 维护 worker
读:正文/裁决、维护规则、当前表 → 写:字段更新。
可不写用户终稿;可与展示链解耦、**延后**跑。
### 5.5 副作用(含大纲、人设、事件)
表值变化满足条件 → 触发声明内专用逻辑(改写态度段、插入第 N 章细纲、替换某常驻块等)。
优先:查表换段;跨阶段再跑改写 LLM。
**防无限触发(程序约定,写入规格):**
1. **边沿**`prev` 不满足且 `now` 满足才触发
2. **阶段字段**:规则绑「进入 stage」不绑「处于 stage」
3. **`fired` / ruleId**once 规则触发后标记,不再入队
4. 可重复须显式(`mode: every_edge`+ 冷却/边沿
5. 同 round 同 ruleId 最多一次;对本轮维护用快照算边沿,限制连锁爆炸
例:`好感>=60→恋爱` 不得每轮触发;`当前章=第一章`→插入细纲须 edge 或章字段变更 once。
**规格字段(`tables.side_effects[]`**
```json
{
"id": "affinity-romance",
"field": "好感",
"op": "gte",
"value": 60,
"mode": "once",
"action": {
"type": "write_tag",
"tag": "上下文.角色态度",
"content": "恋爱模式:……"
}
}
```
| 字段 | 含义 |
|------|------|
| `op` | `eq` / `neq` / `gte` / `lte` / `gt` / `lt` / `truthy` / `changed` |
| `mode` | `once`(边沿 + 记 fired\| `every_edge`(可反复跨边沿) |
| `action.type` | `write_tag` / `replace_tag` / `queue_worker` |
实现:`src/blackboard/table-side-effects.ts`;写 `变量.当前` / `运行.初始变量` 后由 runtime 求值fired 存 `运行.表副作用.fired`
### 5.6 与交互流程
助手流:生成大纲/细纲 → **用户写入/改表** → 副作用或调度读表继续;始终尊重字段 `rev`
---
## 6. 固定上下文族与预设
**「固定上下文」是概称**:不必单独开 worker、由程序按契约插入 prompt 的内容。
**不上 worker ≠ 不重要**——谈清楚并写进规格的纲领/范式,往往比某个中间 worker 更决定体验。
### 6.0 Tag 的真正用途(纠正)
固定上下文用稳定 **tag / 单位 id** 命名(如美学纲领、叙事指南),便于:
1. **同一份正文挂到多个 worker**`mount` / `contextSegments` / 声明映射)——这是主收益
2. **创作单位分块验收**——扯皮过程可折叠,定稿切片进「创作.已验收内容」(已做)
**不是**「只报 tag 名就能省掉创作期依赖正文」。写 B 块时若依赖 A 的内容,仍要把 A 的定稿喂给写 B 的 LLMtag 不能替代依赖传递。
```text
能省:反复扯皮直到定稿之前的过程(验收折叠)
不能省:写下游时对上游定稿正文的依赖
主收益:跨 worker 复用同一份固定上下文,而不是为每个 worker 各写一份
```
### 6.1 用法(重要:不是填空)
§6.2 里的名称 **只是示例话题**:告诉 design / 总管——**可以向用户询问类似内容**,再收成固定上下文。
```text
不是:按表逐项填空、默认必谈、谈完才算过关
而是:对话中若体验需要钉死某类边界/呈现/协议 → 可以按这类话题问用户 → 写入规格并注入
```
正推仍成立:需要维持 [体验/边界/呈现] 且宜稳定注入 → 固定上下文;会变 → 表;本轮推理 → worker。
问什么、问几句、要不要问,由当前体验缺口决定——**禁止把示例表当成问卷。**
### 6.2 示例话题(可问用户的方向)
| 话题(示例) | 大致在问什么 | 若收成规格,常见落点 |
|--------------|--------------|----------------------|
| 交互怎么转 | 站位、系统扮演、输出、怎么一轮一轮交手 | `interaction`(多由 `phase:core` 收) |
| 叙事指南 | 世界/助手态度与体验边界(≠ 文风) | `narrative_guide` |
| 美学纲领 | 呈现给用户的可读终稿气质 | `presentation` / 常驻美学块 |
| 输入协议 | `()` / `""` / 无包裹等约定 | `input_protocol` |
| 核心前提 | 不能瞎发挥又关键的硬前提 | `core_premises` |
| 其它常驻片段 | 只给部分 worker 的稳定句(态度段、轻规则…) | `resident_context[]` |
落点 id`fixed:*` / `resident:*`)仅在 **已经写出内容、要当创作单位验收** 时使用;没有内容就不要当成待填格子。
实现里的 `FIXED_CONTEXT_CATALOG` 同样是提示目录,不是 UI 填空项。
### 6.3 创作顺序(软)
```text
phase:core核心体验 + 交互)
→ 纲领类固定上下文(风格 / 叙事 / 美学…)——宜先于大量 worker
→ 一次一个 worker声明挂载哪些已有 tag勿复制纲领
→ 杂项常驻 / 表 / 副作用 → 终稿
```
**禁止默认**「先写完 worker 列表,再为每个 worker 填写上下文」。
Worker 规格应回答:职责是什么、读哪些已有 tag、写哪些 tag——而不是顺带发明一份私有美学。
**UI**:检查器把固定上下文做成 **独立卡片区**(非 Worker 子项),每张卡展示 **塞进哪些 Worker**;见 `worker-set-view.ts``contextTags`
### 6.4 常驻条目与预设
杂项常驻(`resident_context[]`)形状示例:
```json
{
"id": "tone",
"position": "static",
"importance": 2,
"content": "文风克制、偏冷",
"mount": ["narrator"]
}
```
- 默认黑板 tag `上下文.常驻.{id}`;可显式 `tag`
- `mount` 空 = 全部 worker否则仅列出的 ref
- 能常驻则不上 worker叙事指南 / 美学 / 交互等 **若已谈清**,用对应规格字段,不要全塞进含糊 `tone`
- 越稳、越决定体验边界 → 插入位越靠上
预设 = 全局;固定上下文 = 实例/worker 级。
用户不可见、模型必须知 → **程序固定拼装**,不是整理用户输入的 worker。
实现:`src/skills/resident-context.ts`
---
## 7. 产出与验收
产出:`设计.worker集` JSON建议包含
- `interaction`:站位、系统扮演、输出形态、与用户轮转方式
- `experience_check`:首要三维(及可选维)摘要
- `workers[]``ref` / `duty` / `rationale` / **`acceptance`run 验收点,见 §7.3**
- `resident_context[]`:位置、内容摘要、挂载哪些 worker
- `tables`schema、显隐、维护规则、副作用含 edge/once/stage
- `narrative_guide` / `core_premises`:叙事指南与核心实现前提
- `input_protocol`:若需要
- `design_end`:可选开局 · 开场白等备忘(非强制管道)
验收复述须含:站位;系统扮演/输出/交互;体验首要三维;有何 worker/表/副作用;**run 在哪些 worker 后停下来给人看**;为何没有某件;进 play 由用户决定。
交稿自检题材词可替换仍成立§3 清单通过;无持续为真每轮触发;格式程序化;字段级 rev 已设计;每个面向用户的可读终稿点有明确 `acceptance`
---
## 7.1 强用户可读(前端约定)
创作期检查器须让用户看懂:
| 面板 | 内容 |
|------|------|
| **设计** | Worker 集卡片(含 run 验收点标注)、设计进度 |
| **验收** | 当前产物结构化预览 + 接受/不接受(主确认点) |
| **上下文** | 终产物 tag、活跃 tag、定稿摘要、已归档过程计数 |
| **历史** | 调度时间线(可折叠) |
主 feed隐藏过程调度验收中的全文进验收 Tab**创作对话 messages 全量保留供浏览**;拼给下一 design worker 的「创作.对话」才裁 AI见 §7.2)。
---
## 7.2 创作与 Run两套上下文勿混
| | 创作 `design` | Run / play |
|--|---------------|------------|
| 单位 | **创作单位**:一个 worker或固定上下文族中的一块交互范式 / 叙事指南 / 美学纲领 / …),同级 | 声明内的 run worker |
| 会话 | 用户 ↔ 设计讨论进 **session `messages`(全量,用户可回看)****隐藏 AI 历史只作用于拼给模型的 transcript** | Worker **按契约从黑板重装**`contextSegments` / `inputTags` |
| 验收后折叠 | **不删 messages**;产物在黑板/规格;同步「创作.对话」供下一单位 | 过程 tag 归档 + `上下文.定稿摘要``compress-after-worker.ts` |
| 禁止 | 一次收齐整份 worker 集 + 全部固定上下文;把纲领当成「顺便写的备注」 | 把创作闲聊塞进 worker prompt |
### 创作单位(粒度)
「固定上下文」= **族名**。族内某块 **只有谈出来并写入规格后** 才成为创作单位§6.1 示例话题不是待填格子。
典型推进(非强制工序):
1. `phase:core`:站位 + 交互 + 核心体验
2. 需要时:风格 / 叙事 / 美学等 **纲领类** 固定上下文先验收(`design-fixed`
3.`worker:{ref}`:挂载已有 tag勿复制纲领
4. 其它 `resident:*` → 表 / 副作用 → 终稿 `设计.worker集`
单位跑只写 `设计.worker集.草稿`;终稿才写 `设计.worker集`(才 `designInstanceReady`)。
### 创作会话(简单)
```text
本单位进行中:问答、复述、草稿讨论 → 照常 append 到 messages用户浏览全量
拼给 AI写入黑板 tag「创作.对话」= 用户全量 + 仅最后一次 AI 输出(含未完成提问)
不需要:步内 policy、创作专用拼装栈改写 messages
```
### 创作折叠(简单)
```text
用户接受本单位产物
→ session.messages 不删(用户可继续浏览历史)
→ 产物本身在黑板/规格里;已验收内容写入「创作.已验收内容」
→ 同步「创作.对话」transcript仍按用户全量 + AI 只留最后一次)
→ 下一单位从「已定稿产物 + 新对话」继续
```
不在创作期做「定稿摘要再注入下一 design LLM」的复杂链产物本身在黑板/规格里,够用。
「隐藏 AI 历史」= 仅 transcript 裁剪,**绝不**从主 feed 删掉或折叠创作过程消息。
### Run 压缩(与创作正交)
```text
用户在 run 验收点接受产物
→ 终产物 tag 标记 final
→ 过程 tagworker 答复、*.草稿 等)归档
→ 写入 上下文.定稿摘要
→ 下一 worker 按契约重装时带上摘要与终产物
```
实现:`src/runtime/compress-after-worker.ts`
禁止把完整过程讨论继续喂给下一 **run** worker。
---
## 7.3 Run 验收点(创作时必须设计进 Worker 集)
**不是**「整条 run 有没有验收」的二选一,也不是创作阶段的特例。
**是:** 规定 **跑完哪个 worker 之后必须停下来让用户阅读/验收**;其余可连续调度。
| `workers[].acceptance` | 含义 |
|------------------------|------|
| `review` | 本 worker 完成后进入用户验收;接受后才压缩并允许续跑 |
| `continue` | 完成后可接着调下一个,不打断用户 |
**正推示例(写入 rationale / 复述给人听):**
| 意图 | 典型停点 |
|------|----------|
| 自主长篇 | 大纲 / 主题 worker → `review`;正文链多段 → `continue`,直到用户叫停或大纲段落再 `review` |
| 交互 / RP | 产出用户可见终稿的转述(如 narrator`review`;世界裁决等中间层 → `continue` |
创作期按 **§7.2 创作单位** 分块验收worker 与固定上下文同级),与上表正交。
design-intake / 创作调度必须为 **每个** run worker 写出 `acceptance`;缺省时不得假设「全都不用验收」——面向用户的可读输出默认倾向 `review`,纯中间层倾向 `continue`,吃不准就 ask_user。
---
## 7.4 开局 · 开场白opening-generator
创作末尾、Worker 集已 accept 之后。此时 **世界设定、故事设定、表结构通常已经很详细**
**主产物是开场白**(玩家迈进世界的第一段可读文本),不是填表。
```text
结合前面的世界 + 故事 + 表结构 → 写开场白
初值表 = 与开场同一真相的状态快照(辅)
能从已有设定/用户话推出的字段 → 直接填并对齐开场,别问卷
只有开场必须成立却推不出的点 → 轻量 ask_user
```
例:已有校园末日设定 + 用户「普通大学生」→ 开场写宿舍/校园第一拍;表里年龄/资产/身份自然对齐;**不要**先发一张年龄资产问卷。
产出:`输出.开场白`(主)+ `运行.初始变量` / `变量.当前`(辅,字段格)。
须用户验收。实现:`workers/opening-generator/SKILL.md`
---
## 8. Play / Run 边界(给总管;运行时按声明执行)
-`run_worker` 声明内的 ref
-`acceptance: review` → 停、给人看、接受后压缩再继续
-`acceptance: continue` → 可在 burst 内续调下一 worker受 burst 上限约束)
- 表维护 / 副作用可在终稿展示之后延后;写表须尊重字段 `rev` / `source=user`
- 开局类 skill 仅创作末尾可选,禁止当 run 每轮技能

View File

@@ -16,28 +16,57 @@
## 2. 文档地图 ## 2. 文档地图
| 文档 | 管什么 | | 层级 | 文档 | 管什么 |
|---|---| |---|---|---|
| `tag-blackboard.md` | **★ 主规格**标签黑板、skill 分工 | | **系统架构** | **`architecture.md`** | 模块边界、定义/实例、调度、上下文、持久化原则、风险 |
| `context-assembly.md` | **★ 上下文拼接**:上半固定、下半动态 | | 运行内核 | `runtime-state-machine.md` | 5 相位、tool 边界、burst |
| `architecture.md` | 总览、agent tool loop、模块边界 | | 运行内核 | `tool-contracts.md` | 总管 / worker tool |
| `runtime-state-machine.md` | 5 相位、tool 边界、burst | | 运行内核 | `context-assembly.md` | 上下文拼接:上半固定、下半动态 |
| `tool-contracts.md` | 总管 / worker tool | | 运行内核 | `tag-blackboard.md` | 标签黑板 |
| `orchestrator-skill-format.md` | manifest 写法(非编排表) | | 持久化 | `book-storage.md` | Book、CardAsset、PlayBook、过程存储 |
| `worker-skill-format.md` | SKILL.md、contextSegments | | 持久化 | `run-snapshot.md` | 手动存档 |
| `skill-format.md` | 包存储、registry | | UI | `ui-design.md` | 工作台布局、检查器、composer |
| `skill-design-guide.md` | 新包设计方法 | | UI | **`ui-glossary.md`** | 用户可见中文口径;禁止裸露内部 id |
| `creation-playbook.md` | 创作流程概念 | | 外部参考 | `references.md` | 按层借鉴的 GitHub 仓库 |
| `book-storage.md` | Book、CardAsset、PlayBook、过程存储 | | 本文件 | `implementation-guide.md` | 文件顺序与职责;在现有内核上补齐 |
| `run-snapshot.md` | 手动存档 | | **产品体验路线** | **`px-roadmap.md`** | PX0PX5 交付、DoD、明确不做 |
| `preset-format.md` | 预设导入 | | **日常 → P0** | **`daily-use-p0.md`** | 日常场景、功能映射、P0 详细工作包 |
| `implementation-guide.md` | 本文件:文件顺序与职责 | | **剧本 / 方法(非系统架构)** | `design-orchestrator-guide.md` | 创作三大步、表/副作用、自检 |
| **剧本 / 方法(非系统架构)** | `creation-playbook.md` | 创作流程概念(指向指导) |
| **Skill 格式(非系统架构)** | `orchestrator-skill-format.md` | manifest 写法(非编排表) |
| **Skill 格式(非系统架构)** | `worker-skill-format.md` | SKILL.md、contextSegments |
| **Skill 格式(非系统架构)** | `skill-format.md` | 包存储、registry |
| **Skill 格式(非系统架构)** | `skill-design-guide.md` | 包格式薄层 |
| **Skill 格式(非系统架构)** | `preset-format.md` | 预设导入 |
许可证占位:仓库根 `THIRD_PARTY_NOTICES.md`
--- ---
## 3. 实现分期 ## 3. 在现有内核上补齐(当前优先)
### Phase A0 — 可运行阶段机(当前优先) 相位机、Agent tool loop、Skill loader、Worker 声明、上下文拼装、表 rev、Web UI、快照 API **已具备**
**不要**按绿场「领域库 → Mastra → …」重开;增量挂在现有 `phase-runtime` / Book 存储上。系统边界见 `architecture.md` §11。
**权威执行顺序与验收 DoD → [`px-roadmap.md`](./px-roadmap.md)**(当前焦点 **PX0**)。
**日常场景与 P0 工作包 → [`daily-use-p0.md`](./daily-use-p0.md)**
| 路线阶段 | 目标 | 备注 |
|----------|------|------|
| **PX0当前** | 导演 UI + 双线主路径 + 存档硬稳定 | 详见 `daily-use-p0.md` / `px-roadmap.md` |
| PX1PX2 | 创作/游玩体验打磨;副作用 | UI 仍可抄 |
| PX3 | Trace、预算、规格版本、E2E | — |
| PX4 | 长文加深P0 已含最小闭环) | — |
| PX5 | 隔离 + 调试;必要时新导演包 | — |
| 延后 | SQLite / 事件溯源 / Mastra·Next | — |
历史分期 A0E 见下节(多数已 done作文件职责索引不再当作「当前从零启动」路线
---
## 3b. 历史实现分期(索引)
### Phase A0 — 可运行阶段机
目标:**不依赖 LLM**,阶段机整体可运行、可手动驱动、可脚本跑通最小闭环。 目标:**不依赖 LLM**,阶段机整体可运行、可手动驱动、可脚本跑通最小闭环。

View File

@@ -1,37 +1,41 @@
# 总管 Skill 格式Orchestrator / Manifest # 总管 Skill 格式Orchestrator / Manifest
> **文档层级Skill 包 manifest 格式(非系统架构)。**
> 总管与相位机边界见 [`architecture.md`](./architecture.md)、[`runtime-state-machine.md`](./runtime-state-machine.md)。
## 1. 定位 ## 1. 定位
**orchestrator.md** = 本包的 **manifest**:注册有哪些 skill、如何验收、如何声明 instance ready。 **orchestrator.md** = 本包的 **manifest**:注册有哪些 capability、如何验收、何时可进 play。
**不**写逐步流水线剧本**不**写「总管思维链」逐步调度 **不**写逐步流水线剧本。
**创作方法**见 **`design-orchestrator-guide.md`**(三大步、表、自检)。
```text ```text
用户选 新建作品 → 默认
→ designagent 按需 invoke instantiate skill → designdesign-intake → 设计.worker集 JSON实例声明
declare ready → playagent invoke run skill 用户验收 → 用户手动进 play
done归档 Book playAgent 按 Worker 声明 invoke worker
``` ```
| 谁决定 | 什么 | | 谁决定 | 什么 |
|--------|------| |--------|------|
| **Agent** | 何时 invoke 哪个 skilltool loop | | **Agent** | 何时 invoke 哪个 worker idtool loop |
| **Manifest** | 可用 skill 列表、验收策略、readiness 规则 | | **Manifest** | design worker 列表、验收策略、readiness |
| **Worker SKILL.md** | inputTags、contextSegments、怎么做 | | **Worker ** | 本实例启哪些 worker、表、常驻上下文、副作用 |
| **templates** | design-intake 合并用的可选默认契约(非运行时权威) |
`shared-context.md`:包级 **固定上下文上半**,注入各 worker prompt。总管不读 `architecture.md``creation-playbook.md``design-orchestrator-guide.md`
`architecture.md``skill-design-guide.md``context-assembly.md`
--- ---
## 2. 存储位置 ## 2. 存储位置
```text ```text
skills/novel/weird-rules-short/ skills/dialogue/world-simulator/
├── orchestrator.md ├── orchestrator.md
├── shared-context.md # 可选 ├── shared-context.md # 可选
├── worker-templates/ # 可选 ref 模板design 缺省)
└── workers/ └── workers/
└── write-rules/SKILL.md └── design-intake/SKILL.md
``` ```
- 文件固定名 **`orchestrator.md`**。 - 文件固定名 **`orchestrator.md`**。
@@ -43,142 +47,74 @@ skills/novel/weird-rules-short/
```yaml ```yaml
--- ---
name: weird-rules-short name: world-simulator
description: >- description: >
何时选用:用户要写规则怪谈、守则类短篇。 默认能力库Worker 集即实例声明…
category: novel category: dialogue
bookKind: novel bookKind: dialogue
workers: workers:
- write-rules - design-intake
- review-infer demandTag: 用户.需求
- review-author startupMode: agent-first
sharedContext: shared-context.md uiPrompt: |
--- ---
``` ```
| 字段 | 用途 |
|------|------|
| `description` | 何时选本包 |
| `workers` | run / design 可调度 skill id 白名单loader 用) |
| `bookKind` | Book 存储形态 |
--- ---
## 4. 正文章节(推荐) ## 4. Manifest 正文应有的节
```markdown
# 标题
## 启动询问 # 最小 intake
## Skill 注册表 # instantiate + run skill 清单与说明(见 §5
## 验收策略 # 哪些 skill 产出需 user_confirmed / approve
## Instance Ready # 按 run_skill清单 的动态最低可行性
## contextProfile # 可选 variant 说明(见 context-assembly.md
## 用户回合 # user-turn 等(可选)
## 禁用行为
```
**不应出现:**
- 逐步 **Worker 编排表** / **总管思维链**(已废弃为主流程)
- `inputTags` / `outputTags` 列表 → 在 `workers/*/SKILL.md`
- 写作细则 → Worker SKILL 正文
---
## 5. Skill 注册表(替代编排表)
列出本包 **能力库**,供 agent `list_workers` 与 manifest 校验:
```markdown ```markdown
## Skill 注册表 ## Skill 注册表
## Instance Ready / 进入游玩
### Instantiatedesign stage ## 验收策略
## 总管优先行为
| id | 说明 | 典型 outputTags | ## 禁用行为
|----|------|-----------------|
| interaction-paradigm | 定交互范式与 run_skill清单 | 设计.run_skill清单 |
| world-blueprint | 世界背景 | 设计.世界.蓝图 |
| persona-draft | 角色卡草稿 | 角色卡.草稿 |
### Runplay stage
| id | 说明 | 默认 acceptance |
|----|------|-----------------|
| write-rules | 写规则 | user_confirmed |
| review-infer | 读者视角验收 | programmatic_review |
``` ```
agent **按需 invoke**,不必按表顺序跑全。 **不应包含:** 「第 N 步必须跑某 worker」管道剧本。
方法细节指向 `design-orchestrator-guide.md`,勿在 manifest 重复长文。
--- ---
## 6. 启动询问与 Instance Ready ## 5. Skill 注册表
**启动询问:** 只收集 agent 无法从空推断的最小信息 → `用户.需求` 等。 ### Design
**Instance Ready** 不写死「14 步全完成」,而写: | id | 说明 |
|----|------|
| design-intake | 产出 设计.worker集 JSON |
| (可选)开局赋初值 | 创作末尾可选,非每轮 |
### Play
不在 manifest 写死清单。Agent 只调度 **`设计.worker集` 声明内的 ref**。
---
## 6. Instance Ready
```markdown ```markdown
## Instance Ready `设计.worker集` 已 accepted进 play 由用户手动决定。
`设计.run_skill清单` 已 accepted且清单中每个 run skill 的
**最低 input 要求**(见各 SKILL.md已在黑板存在或已记录跳过理由。
由 agent `declare_instance_ready` + Runtime 校验。
``` ```
--- ---
## 7. 验收策略 ## 7. 验收策略
```markdown
## 验收策略
| skill | requiresApproval | acceptanceMode | | skill | requiresApproval | acceptanceMode |
|-------|------------------|----------------| |-------|------------------|----------------|
| setup-scenario | true | user_confirmed | | design-intake | true | user_confirmed |
| world-engine | false | no_confirmation |
| present-round | false | user_confirmed |
```
agent 通过 `run_worker(..., requiresApproval)` 触发;默认值也可写在 manifest 供 Runtime 填充 play**无**每轮强制验收;用户新输入 = 认可上轮终稿;重 roll 替代 reject
--- ---
## 8. contextProfile ## 8. 与 Worker 声明的关系
若同一 skill 有多种游玩/创作模式,在 manifest 列出 variant 名与含义;实例化写入 Book。
格式见 `context-assembly.md`
---
## 9. 与 Worker SKILL 的关系
```text ```text
orchestrator.md 注册 + 验收 + readiness accept 设计.worker集 → Runtime 构建 InstanceWorkerDeclaration
workers/*/SKILL.md 能力 + 上下文契约contextSegments play仅允许声明内 activeWorkerIds
表维护 / 副作用按规格边沿触发(见 design-orchestrator-guide §5
``` ```
Prompt 注入:
```text
Agentsession 摘要 + tool可用 read_blackboard
Workershared-context + SKILL + assembleWorkerContext上固定下动态
```
---
## 10. 迁移说明
旧包中的 `## Worker 编排``## 总管思维链` 仍可作为 **人工参考**,但新包与 world-simulator **不应**以此为主流程。
Runtime 目标态以 agent tool loop + manifest 注册表为准。
---
## 11. 相关文档
| 文档 | 关系 |
|------|------|
| `skill-design-guide.md` | 设计方法 |
| `worker-skill-format.md` | SKILL.md 字段 |
| `creation-playbook.md` | 概念 |

195
docs/px-roadmap.md Normal file
View File

@@ -0,0 +1,195 @@
# PX 路线图(产品体验与交付)
## 0. 定位
当前仓库的**执行计划表**。系统边界见 [`architecture.md`](./architecture.md);日常场景与 P0 工作包见 [`daily-use-p0.md`](./daily-use-p0.md);文件级索引见 [`implementation-guide.md`](./implementation-guide.md)。
**不**绿场重写、**不**近端迁 Mastra/Next/SQLite。挂在现有相位机 + Book/快照 + 声明驱动 Worker 上增量交付。
### 0.1 已锁定产品取向
| 项 | 决定 |
|----|------|
| 日常玩法 | **AIRP/交互** + **长文/爽文**(同一内核) |
| 导演 Skill | 新建时 **UI 选择**能力包;初期可只有 1 项(默认选中) |
| 剧本层 | **动态**:听用户说话谈 Worker 集 + tool loop不做死板剧本选单/DAG |
| UI | **抄参考**即可,不自研美学 |
| 硬指标 | **存档 / 续作稳定** |
| 多导演 | 仅当方法边界真不同时再加包;爽文≠新导演 |
### 0.2 成功标尺
| 级别 | 含义 |
|------|------|
| **L1 可演示** | 带路能跑通一轮 |
| **L2 可自助** | 选导演 → 创作 → 玩/写,少踩坑 |
| **L3 可日常** | 双线存档续作可靠;少死循环 |
| **L4 可扩展** | 新导演包或隔离模式不改内核边界 |
---
## 1. 总计划表(主表)
| 阶段 | 名称 | 目标标尺 | 核心交付 | 状态 |
|------|------|----------|----------|------|
| **PX0** | 双线可日常最小集 | L2→摸 L3 | 导演选择 UI线 A 扮演 + 线 B 长文/爽文主路径;**存档硬稳定**失败出口UI 只抄缺口 | **进行中(核心已落地)** |
| **PX1** | 创作体验 | L3 design | 规格可读可改、进度可见、表/黑板可手改、instance 改规格 | **进行中(定稿存读+黑板改+进度面板已有)** |
| **PX2** | 游玩/写作体验 | L3 play | 阅读主舞台(抄)、状态抽屉、重 roll 顺手、副作用调度、多 run 线打磨 | 未开始 |
| **PX3** | 运行可信 | L3 加固 | Context Trace、调度预算、规格钉版本、mock E2E | 未开始 |
| **PX4** | 长文加深 | L4 | 章产物/多卷、局部评测、更好的长文阅读面P0 已含最小长文) | 未开始 |
| **PX5** | 隔离 + 调试 | L4 | 私有上下文、泄漏测试Trace/成本/分支调试台 | 未开始 |
```text
现在 → PX0存档+双线+导演UI
→ PX1创作好改
→ PX2玩/写好用)
→ PX3可信可测
→ PX4 / PX5按需可调序
```
顺序原则:先 **PX0 双线硬路径**再体验12再加固3再扩展45
若 PX02 出现腐档/死循环,允许提前插入 PX3 对应子项。
预估单人全职约PX0 1.52 周PX1/PX2 各 23 周PX3 23 周PX4/PX5 各 35 周。
---
## 2. PX0 — 双线可日常最小集(当前)
**权威细项**[`daily-use-p0.md`](./daily-use-p0.md) §3。
### 2.1 计划表(工作包)
| 序 | 工作包 | 内容 | 验收要点 |
|----|--------|------|----------|
| 1 | **WP-S** 存档稳定 | 续作相位正确;手动存/列表/加载;正文与关键 tag 不丢 | 线 A、B 各存读一轮,不手改磁盘 |
| 2 | **WP-D** 导演选择 | 新建作品 UI 选导演registry 列表;仅 1 项时默认选中 | 能选中并进入该包创作 |
| 3 | **WP-A** 进出与失败 | glossary 底线;`waiting_user` 有主按钮LLM/worker 失败可重试 | 无死胡同、无裸 id 挡路 |
| 4 | **WP-R** 线 A | 扮演 ≥3 回合不卡 | A-GP15 |
| 5 | **WP-L** 线 B | 引导+template 能谈成写手规格≥2 段正文;续作不丢文 | B-GP15 |
| 6 | **WP-U** UI 抄补 | 仅补存档/确认/选导演等缺口控件 | 不按 ui-design 大重构 |
| 7 | **WP-E** 闸门 | 双线冷启动+续作验收;勾选 DoD | 全绿才出 PX0 |
### 2.2 黄金路径(摘要)
- **线 A**:选导演 → 扮演意图 → 验收 → ≥3 回合 → 存档 → 续作/读档
- **线 B**:选导演 → 爽文/长篇意图 → 验收写手规格 → ≥2 段正文 → 存档含正文 → 续作/读档
### 2.3 非范围
死板剧本选单;自研精美 UITrace隔离完整副作用多导演商店列表可扩展P0 不追求多包运营)。
### 2.4 DoD
- [ ] WP-SE 完成
- [ ] 线 A、线 B 黄金路径各冷启动 + 续作通过
- [ ] 导演选择 UI 可用(可仅一项)
- [ ] 更新本文状态列 + `architecture.md` §13
### 2.5 挂载
`web/`新建选导演、saves`session-manager``skill-catalog` / registry、`phase-runtime`、引导与 `worker-templates`
---
## 3. PX1 — 创作体验
| 项 | 内容 | DoD |
|----|------|-----|
| 规格工作台 | 产物为主舞台(可抄 Notion/PR 式验收) | 不看文档能验完 Worker 集进 play |
| 进度 | 创作单位已验收/进行中可见 | — |
| 表 | 字段可编辑 + rev 不覆盖手改 | 手改后 worker 写入可解释 |
| 规格回退 | load instance 再改 | 至少一条路径可用 |
挂载:`worker-set-view``creation-units`、questions UI、表编辑 API。
---
## 4. PX2 — 游玩 / 写作体验
| 项 | 内容 | DoD |
|----|------|-----|
| 阅读面 | 终稿/章文主视线(抄 ST/阅读器) | ≥10 回合或等价连续写段不卡 |
| 重 roll | 发现入口;与快照语义一致 | 存/读/重 roll 各至少一次 |
| 状态 | 抽屉看表/变量 | — |
| 副作用 | `side_effects` 边沿完整可观测 | fixtureonce / changed |
| 多线 | 同开局新开 play 线 | 不串 run 状态 |
挂载:`table-side-effects``phase-runtime`、play UI、`run-snapshot`
---
## 5. PX3 — 运行可信
| 子项 | DoD |
|------|-----|
| Context Trace | 失败调用能看出装载了什么 |
| 调度预算 | 重复/嵌套/Token 触顶可读停止 |
| 规格钉版本 | 改规格不污染未迁移旧 run |
| mock E2E | 双线+存档脚本可进 CI |
---
## 6. PX4 — 长文加深P0 之后)
P0 已要求最小长文/爽文闭环。本阶段加深:
- 章/卷 Artifact 更清晰;多段目录
- 简单质量检查(程序优先)
- 更好的长文阅读/对照面(仍可抄,可不上 Monaco
**不做**:固定小说 DAG为长文另起调度内核。
---
## 7. PX5 — 隔离模拟 + 专业调试
| 块 | 内容 |
|----|------|
| 隔离 | 私有上下文、公私事件、泄漏测试(依 Trace |
| 调试 | 调度时间线、Trace 检查器、Token/成本、分支树 |
若隔离需要**不同方法边界**,以**新导演包**形式加入UI 多一项),仍保持包内剧本动态。
---
## 8. 明确不做(周期内)
- Mastra / LangGraph 换相位机Next 重写前端
- 完整事件溯源 + SQLite 近端必达
- 题材固定 instantiate 管道;死板剧本选单
- 把创作方法长文塞回系统架构文
---
## 9. 里程碑
| 里程碑 | 退出 | 产出 |
|--------|------|------|
| **M0** | PX0 DoD | 双线验收记录;导演 UI存档稳定 |
| **M1** | PX1+PX2 DoD | 内部「可日常」说明 |
| **M2** | PX3 DoD | Trace/预算/E2E 绿灯 |
| **M3** | PX4 或 PX5 | 长文加深或隔离/调试演示 |
---
## 10. 文档 / 代码映射
| 阶段 | 文档 | 代码 |
|------|------|------|
| PX0 | `daily-use-p0.md``ui-glossary.md` | `web/`、session、saves、catalog、templates |
| PX1 | `ui-design.md`可抄着落地、design-guide | worker-set、creation-units |
| PX2 | `run-snapshot.md`、tag-blackboard | side-effects、phase-runtime |
| PX3 | `context-assembly.md`、architecture | assemble、budgets、tests |
| PX4 | Skill 薄层 | artifacts、长文面 |
| PX5 | 隔离短文(待写) | Trace 强制、debug、stats |
---
## 11. 下一行动(立即)
1. **手工跑双线黄金路径**(线 A ≥3 回合;线 B ≥2 段;存读档)验收 PX0 DoD
2. 表字段格级编辑器(当前是黑板 tag 预览级手改)可继续打磨
3. 再开 PX2阅读面 / 副作用 / 多 run
**当前**PX0/PX1 主代码已落地,待你本地用真实 LLM 走一遍确认「能用」。

88
docs/references.md Normal file
View File

@@ -0,0 +1,88 @@
# 外部项目参考(按层借鉴)
以下仓库用于架构与实现参考。**只借鉴标明的一层,不把外部项目整体耦合进本仓库实现。**
本系统运行内核为自研(相位机 + tool loop + 声明驱动 Worker。Mastra、Next.js、assistant-ui 等**不是**近端迁移目标;若引入,须经适配层,且领域对象不得保存框架内部类型。
许可证与再分发注意见仓库根 [`THIRD_PARTY_NOTICES.md`](../THIRD_PARTY_NOTICES.md)。
---
## 参考原则
1. 按模块读代码与文档,禁止整仓复制粘贴为业务内核。
2. 借鉴交互/产品概念时,数据模型仍用本仓库 Book / Session / 黑板。
3. Prompt 拼接、权限、上下文编译由本仓库 Context Compiler 实现,不照搬他站拼接逻辑。
4. 依赖使用 lockfile 精确版本;对外部项目的修改放在适配层。
---
## 仓库与借鉴层
### Mastra
- 仓库:<https://github.com/mastra-ai/mastra>
- **可借鉴**Agent/Tool 声明方式、模型 Provider、结构化输出、Memory/Storage 接口形态、Workflow 适用边界、Observability、TypeScript 组织
- **不借鉴为内核**不把动态调度、上下文权限、Definition/Instance、事件存档交给 Mastra 领域模型;本仓库已有 phase-runtime
### assistant-ui
- 仓库:<https://github.com/assistant-ui/assistant-ui>
- **可借鉴**流式聊天、消息组件、Tool 调用展示、会话状态、人工确认、前后端连接方式
- **不负责**Definition、存档、多 Agent 调度模型(仍由本仓库 API + Runtime
### LobeChat
- 仓库:<https://github.com/lobehub/lobe-chat>
- **可借鉴**:本地 AI 应用信息架构、会话列表、模型配置 UI、助手/角色配置、插件与知识库管理体验、布局
- **不直接采用**:其会话数据模型
### SillyTavern
- 仓库:<https://github.com/SillyTavern/SillyTavern>
- **可借鉴**:角色卡产品概念、世界书编辑体验、聊天存档与分支心流、角色扮演工作方式、导入导出与兼容性概念
- **不照搬**Prompt 拼接逻辑;世界书检索与权限由 Context Compiler 实现
### LangGraph.js
- 仓库:<https://github.com/langchain-ai/langgraphjs>
- **可借鉴**:有状态 Runtime、Checkpoint、中断与恢复、Human-in-the-loop、长时间任务可恢复执行
- **不采用**:用固定 Graph 表达整场创作流程(本仓库用相位机 + 动态 tool loop
### AutoGen
- 仓库:<https://github.com/microsoft/autogen>
- **可借鉴**:多 Agent 消息与生命周期、Agent 选择、事件驱动运行、Tool 执行边界、Runtime 抽象
- **不采用**Agent 间自由转发消息作为权限模型;上下文不得依赖自由转发
### Letta
- 仓库:<https://github.com/letta-ai/letta>
- **可借鉴**:长期记忆分层、上下文窗口管理、状态与消息历史区分
- **仍需自建**:小说、世界书、角色私有状态等领域模型
### OpenHands
- 仓库:<https://github.com/All-Hands-AI/OpenHands>
- **可借鉴**运行会话、事件流、Action/Observation、前后端状态同步、暂停恢复与可观测性
- **不参考**:软件工程沙箱任务模型
### CopilotKit
- 仓库:<https://github.com/CopilotKit/CopilotKit>
- **可借鉴**Agent 与前端状态连接、流式事件、Human-in-the-loop、生成式 UI、动态表与确认交互呈现
- **不替代**:本仓库 Book/黑板领域模型
---
## 与本仓库模块的映射
| 本仓库模块 | 主要可参考项目 |
|------------|----------------|
| Runtime / 调度 / 恢复 | LangGraph.js、AutoGen、OpenHands、Mastra边界 |
| 上下文与记忆 | Letta、Mastra Memory 接口形态 |
| 聊天与 HITL UI | assistant-ui、CopilotKit、LobeChat |
| 角色卡 / 世界书 / 存档体验 | SillyTavern、LobeChat |
| 模型接入与可观测 | Mastra |
系统架构总览见 [`architecture.md`](./architecture.md)。

View File

@@ -2,52 +2,92 @@
用户手动保存、多档位;与自动续作 `session.json` 分离。 用户手动保存、多档位;与自动续作 `session.json` 分离。
## 两种 kind更新语义 **编辑 / 重 roll 的统一做法:恢复到之前的快照,再从该点继续。**
创作design与运行play共用此语义。
```text
存快照 → 继续生成 → 不满意 → 加载 earlier 快照 → 重跑后续 worker / 重出展示
```
## 三种 kind更新语义
| kind | 名称 | 何时存 | 存什么 | | kind | 名称 | 何时存 | 存什么 |
|------|------|--------|--------| |------|------|--------|--------|
| **`instance`** | 设计完成 / 资产截面 | design 完成或从 play 剥离设定 | **CardAsset 级** tag角色卡.确认稿、世界蓝图…)**不含**轮次、事件流 | | **`instance`** | 创作定稿截面 | Worker 集 accept可选含 **锁定开局** | `设计.worker集`、静态设定 tag、`运行.初始变量``输出.开场白`**不含**后续轮次、事件流 |
| **`run`** | 游玩存档 | play 任意时刻 | instance 层 + `运行.*`、轮次、变量、对话 | | **`opening`** | 开局锁定(可选单独存) | 创作末尾 swipe 选定开场后 | 与 instance 中开局部分相同;便于「同 Worker 集、换开局再开 play 线」 |
| **`run`** | 游玩存档 | play 任意时刻 | instance/opening 层 + `运行.*`、轮次、变量当前值、对话 |
```text ```text
CardBook design 完成 → 可存 instance导出角色卡 / 世界设定 Worker 集 accept → 可存 instance规格截面
PlayBook 进行中 → 可存 run第 N 轮续玩 可选开局赋初值 → 可存 instance / opening非进 play 强制门槛
play 任意时刻 → 可存 多条 run
改 Worker 集 → load 旧 instance 再改
改某轮输出 → load 该轮前的 run → 重 roll / 重跑
``` ```
**instance** 不再表示「14 步 pipeline 填完」,而是 **agent declare ready 时的可复用资产截面**详见 `book-storage.md` **instance** = **Worker 集(+ 可选开局截面**进 play 由用户手动决定。见 `book-storage.md``design-orchestrator-guide.md`
### 同一开局 · 多条存档
```text
opening-generator 锁定 checkpoint = 共用起点
├─ run 存档 A第 5 轮)
├─ run 存档 B第 12 轮,另一分支)
└─ 「从同一开局新开一条线」= 清空 run 层、保留 opening/instance 层(见 session-manager startNewPlayLine
```
多条 **run** 快照共享同一 **opening**(或含 opening 的 **instance**),无需为每条 play 线复制 Worker 集。
## Swipe 与快照
创作末尾 **开局** 产出时,可用 message **branch / swipe** 多版本对比(可选)。
play 对终稿可用 **重 roll**;用户新输入 = 认可上轮终稿。见 `design-orchestrator-guide.md`
## 与三种存储需求 ## 与三种存储需求
| 需求 | 用什么 | | 需求 | 用什么 |
|------|--------| |------|--------|
| 创建角色卡**过程** | CardBook `designTrace` + messages(快照不替代,须 Book 级) | | 创作过程 | Book `designTrace` + messages |
| 创建好的**卡** | CardAsset 或 `instance` 快照 | | 实例规格 + 锁定开局 | `instance` / `opening` 快照 |
| 游玩**过程** | PlayBook `playTrace` + `run` 快照 | | 游玩过程 | PlayBook `playTrace` + **`run` 快照(同一开局可多条)** |
| 改设定 / 重 roll | 加载对应 kind 的 earlier 快照 |
## 与自动续作 ## 与自动续作
| | `session.json` | `run-snapshots/` | | | `session.json` | `run-snapshots/` |
|--|----------------|------------------| |--|----------------|------------------|
| 触发 | 自动 | 用户手动 | | 触发 | 自动 | 用户手动(关键节点建议提示存档) |
| 数量 | 每 Book 1 份 | 多档、可命名 | | 数量 | 每 Book 1 份 | 多档、可命名 |
| 打开作品 | 默认恢复 | 选档 **加载** | | 打开作品 | 默认恢复 | 选档 **加载** |
| 编辑 | — | **load = 恢复到该档状态** |
## API ## API
```text ```text
GET /api/books/:bookId/saves GET /api/books/:bookId/saves
POST /api/books/:bookId/saves { label, kind: "instance"|"run", note?, sessionId? } POST /api/books/:bookId/saves { label, kind: "instance"|"opening"|"run", note?, sessionId? }
POST /api/books/:bookId/saves/:id/load POST /api/books/:bookId/saves/:id/load
DELETE /api/books/:bookId/saves/:id DELETE /api/books/:bookId/saves/:id
``` ```
`opening` kind 可在实现中与 `instance` 合并存储,文档层区分语义即可。)
## 实现 ## 实现
```text ```text
src/types/run-snapshot.ts src/types/run-snapshot.ts
src/book/run-snapshot-store.ts src/book/run-snapshot-store.ts
src/book/snapshot-filters.ts instance 时剥离 运行.* 等 src/book/snapshot-filters.ts
src/server/session-manager.ts src/server/session-manager.ts
src/server/message-branch.ts swipe / branch checkpoint
``` ```
存储:`books/{bookId}/run-snapshots/{id}.json` 存储:`books/{bookId}/run-snapshots/{id}.json`
## 相关
| 文档 | 关系 |
|------|------|
| `skill-design-guide.md` §0.4 | Worker 集与快照 |
| `creation-playbook.md` | 创作流中的开局与存档 |
| `context-assembly.md` | 恢复后 worker prompt 仍含 preset + static + dynamic |

View File

@@ -79,8 +79,7 @@ type WaitingReason =
## 6. 典型转移(简化) ## 6. 典型转移(简化)
```text ```text
idle → skill_selection idle → intakesession_started + initialSkill
skill_selected → intake 或 input
intake 完成 / confirm → running → agent burst intake 完成 / confirm → running → agent burst
running → run_worker → approve_step 或 worker 执行 running → run_worker → approve_step 或 worker 执行
worker_completed → review_artifactuser_confirmed worker_completed → review_artifactuser_confirmed

View File

@@ -1,184 +1,136 @@
# Skill 设计指南 # Skill 设计指南(包格式薄层)
指导 **如何从零设计一个 orchestrator 包**skill 能力库 + run skill 组合)。 > **文档层级Skill 包格式薄层(非系统架构)。**
格式见 `skill-format.md``orchestrator-skill-format.md``worker-skill-format.md`;上下文见 `context-assembly.md` > 运行内核见 [`architecture.md`](./architecture.md);创作方法见下
**设计顺序:** **创作方法(三大步、表、自检、正推)** 的权威文档是:
**[`design-orchestrator-guide.md`](./design-orchestrator-guide.md)**
本文只保留:**如何在仓库里建包、Worker 集弱结构示例、与旧概念对照**。不要在本文重复长方法文。
格式字段见 `orchestrator-skill-format.md``worker-skill-format.md`;上下文见 `context-assembly.md`
**设计顺序(概念):**
```text ```text
1. 用户意图 → run skill 清单(交互范式 skill 产出) 1. 用户意图 → design-intake按 design-orchestrator-guide→ 设计.worker集 JSON
2. 每个 run skill 倒推需要哪些 instantiate skill / tag 2. 需要时合并 worker-templates 默认契约(仅 design 缺省)
3. 定义各 skill 的 inputTags、outputTags、contextSegments 3. 用户验收 → 用户手动进 play
4. 上下文隔离与验收边界 4. play按声明调度表维护/副作用边沿触发
5. orchestrator manifest能力注册非逐步剧本
``` ```
--- ---
## 1. 实例化agent 选 skill不是填步骤 ## 0. 设计.worker集
### 1.1 两层倒推 ### 0.1 定位
```text **交互范式 / run_skill 清单 / 美学纲领** 合并为单一 tag**`设计.worker集`JSON**。
用户意图(长篇 / 短篇 / 思想实验 / 角色卡扮演 …)
→ 交互范式 skill产出 设计.run_skill清单 | 旧概念 | 新做法 |
→ 每个 run skill 需要什么输入? |--------|--------|
→ agent 按需 invoke instantiate skill能力库中的 SKILL.md | `设计.交互范式` | `interaction`(站位、系统扮演、输出与轮转) |
→ declare_instance_ready → play stage | `设计.run_skill清单` | `workers` + 数据依赖 / 表副作用 |
| `设计.美学纲领` | 转述 presentation **或** 常驻上下文;叙事指南 ≠ 文风 |
Worker 集不是闭集枚举,也不是逐步管道。方法见 `design-orchestrator-guide.md`
### 0.2 design-intake
与用户对话增量更新草稿accept 后即实例规格。进 play **由用户手动决定**
### 0.3 弱结构示例JSON
```json
{
"version": 1,
"interaction": {
"user_stance": "单角代入",
"system_role": "世界执行并给出可读叙事",
"output": "输出.用户展示 + 可折叠状态表",
"turn_shape": "对话回合"
},
"experience_check": {
"user_relation": "…",
"focus": "…",
"satisfaction_source": "…"
},
"workers": [
{
"ref": "world-simulator",
"duty": "世界推进与裁决",
"rationale": "删掉则无本轮世界结果",
"acceptance": "continue"
},
{
"ref": "narrator",
"duty": "组装用户可见回复",
"rationale": "核心产出干巴,需要可读终稿",
"acceptance": "review"
}
],
"resident_context": [
{
"id": "tone",
"position": "static",
"content": "文风示例",
"mount": ["narrator"]
}
],
"tables": {
"schemas": [],
"side_effects": [
{
"id": "affinity-romance",
"field": "好感",
"op": "gte",
"value": 60,
"mode": "once",
"action": {
"type": "write_tag",
"tag": "上下文.角色态度",
"content": "恋爱模式"
}
}
]
},
"narrative_guide": "",
"core_premises": [],
"input_protocol": {
"parens": "() 元要求",
"quotes": "\"\" 角色对白",
"bare": "无包裹视为事实陈述"
},
"design_end": {
"opening": "optional"
}
}
``` ```
**能力库**(如 world-simulator 包内十几个 instantiate skill不是 1→N 管道agent 跳过不需要的 skill并记录 `设计.跳过.{skillId}` `presentation` 主要挂转述类;裁决类不挂展示美学。包级共性可写 `shared-context.md`
### 1.2 示例:不同意图的 run skill 组合 ### 0.4 编辑与重 roll
| 用户意图 | run skill 示例 | 实例化倒推 | `run-snapshot.md`
|----------|----------------|------------| **创作**:按创作单位验收后删交互留产物。固定上下文相关名称是 **可向用户询问的示例话题**(非填空表);宜先钉纲领类 tag 再写 worker挂载复用`design-orchestrator-guide.md` §6。
| 长篇小说 | 变量管理、大纲推荐、转述者、世界机 | 变量目录 skill、叙事指南、世界蓝图… | **Run** 验收点(`acceptance: review`):接受后压缩过程 tag。
| 短篇 | 情感流生成器 | 美学纲领、情感曲线约定 | `acceptance: continue` 的中间 worker 可连跑;面向用户的可读停点由 Worker 集声明。
| 思想实验 | 世界模拟器 | 规则、行动格式;**无**转述者 |
| 角色卡扮演 | 转述者(+ 可选世界机) | 角色卡确认稿、口吻、回复格式 |
### 1.3 run 阶段交互形态
agent 在 play stage 的 burst 内调度 run skill形态因包而异
| 形态 | 适用 | sketch |
|------|------|--------|
| 单 skill 直出 | 简单大纲 | `outline` |
| 行动–反应循环 | 博弈、世界模拟 | 世界机 ↔ 角色决策 ↔ 展示 |
| 分叉验收 | 规则怪谈 | `write` → 双 `review` |
形态写在各 skill 的 **能力说明**里,**不**写进全局编排表逐步剧本。
--- ---
## 2. 暂停与用户 ## 1. 暂停与用户
### 2.1 边界 tool 即暂停 边界 tool 即暂停`ask_user``review_artifact` 等)。策略写在 manifest **验收策略**,不是「第几步必须停」。
Play 默认不对每轮终稿强制 accept。详见 `design-orchestrator-guide.md` §0、§7。
agent tool loop 在下列情况 **退出 burst**,进入 `waiting_user`
```text
ask_user / review_blackboard
run_worker + requiresApproval
worker 完成 + user_confirmed → review_artifact
worker ask_user → worker_questions
```
暂停策略写在 orchestrator manifest 的 **验收策略**,不是「第几步必须停」的管道表。
### 2.2 user-turn
用户亲自决策的环节:独立 skill`用户.最新输入` 写入与 LLM 角色同形 tag供世界机裁决。见 `worker-skill-format.md`
--- ---
## 3. 上下文:上半固定、下半动态 ## 2. 相关
每个 skill`SKILL.md`)声明:
```text
inputTags / outputTags 黑板接口
contextSegments static+ dynamic
contextIsolation 谁看不见什么
contextProfile variants 实例化时选档位
```
原则:**稳定在上、增量在下**Runtime 拼接agent 不改。
全文见 `docs/context-assembly.md`
### 3.1 分层示例roleplay-game-theory
| tier | 含义 | tag 示例 |
|------|------|----------|
| static | 前提、实体设定 | `情境.实验.设定``角色.{id}.设定` |
| dynamic | 历史、本轮 | `运行.事件流``可见信息` |
---
## 4. 倒推标签
对每个 skill 填表:
```text
skill id | 职责 | stage(design/play) | inputTags | outputTags | contextSegments | 验收者
```
### 4.1 角色扮演博弈(摘录)
| skill | 读取 | 写入 | 验收 |
|-------|------|------|------|
| setup-scenario | `用户.博弈需求` | 情境、规则、角色设定 | 用户 |
| world-engine | 情境、规则、事件流、行动 | 可见信息、事件流 | 程序 |
| role-decide | 本人设定、事件流、可见信息 | `.思考``.行动` | 程序 |
| present-round | 本轮产物 | `输出.用户展示` | 用户 |
展示类 skill 单独存在agent 调度,不拼长文。
---
## 5. 上下文隔离(必查)
```text
□ 哪些 tag 只给用户、不注入生产 skill
□ 盲读 review 不可见哪些 tag
□ 草稿 vs 确认稿:下游何时可当作事实?
□ 多角色role_pov 是否生效?
```
`inputTags` + `contextIsolation` + Runtime 过滤,不靠 prompt 口头禁止。
---
## 6. orchestrator manifest非编排表
orchestrator.md 应包含:
```text
□ 启动询问(最小 intake
□ instantiate skill 注册表id、stage、description
□ run skill 注册表
□ 验收策略(哪些 skill 产出需 user_confirmed
□ contextProfile 可选 variant 说明
□ declare_instance_ready 最低可行性(按 run_skill清单 动态校验)
```
**不应包含:** 逐步思维链、「第 5 步必须跑 world-engine」类管道脚本。
---
## 7. checklist
```text
□ 1. 一句话:本包 play stage 的交互形态
□ 2. 交互范式 skill 产出 run_skill清单 的 schema
□ 3. 每个 run skill 倒推 instantiate skill 需求
□ 4. 各 SKILL.mdinput/output、contextSegments、隔离
□ 5. shared-context.mdstatic 上半)
□ 6. manifest注册表 + 验收 + readiness
□ 7. skills/README.md 注册
```
---
## 8. 示例包
| 包 | 特点 |
|----|------|
| `basic` | 单 run skill最小上下文 |
| `weird-rules-short` | 分叉验收 |
| `roleplay-game-theory` | 行动–反应循环 |
| `world-simulator`(规划) | 大能力库 + agent 实例化 |
对照时复用 **方法**,不照搬 tag 名或 skill 数量。
---
## 9. 相关文档
| 文档 | 关系 | | 文档 | 关系 |
|------|------| |------|------|
| `context-assembly.md` | 拼接规格 | | **design-orchestrator-guide.md** | ★ 方法 |
| `creation-playbook.md` | 概念 | | creation-playbook.md | 流程概念 |
| `orchestrator-skill-format.md` | manifest 写法 | | orchestrator-skill-format.md | manifest |
| `book-storage.md` | 过程与资产归档 | | worker-skill-format.md | 磁盘 SKILL声明驱动演进中 |

View File

@@ -1,5 +1,8 @@
# Skill 格式与存储 # Skill 格式与存储
> **文档层级Skill 包格式(非系统架构)。**
> 系统级模块与调度边界见 [`architecture.md`](./architecture.md)。
## 1. 定位:两层 Skill ## 1. 定位:两层 Skill
本项目有 **两种 Skill 文档**,不要混在一个文件里: 本项目有 **两种 Skill 文档**,不要混在一个文件里:
@@ -355,72 +358,63 @@ skills:
--- ---
## 5. 会话启动:第一个询问是选 Skill ## 5. 会话启动:描述需求,而非选包
Skill 选择发生在**任何创作逻辑之前** 新建作品 **不再** 让用户输入 skill name 或列表编号。Runtime 自动加载默认 orchestrator`world-simulator`,见 `src/config/default-orchestrator.ts`),直接进入 intake
### 5.1 启动转移 ### 5.1 启动转移
```text ```text
idle idle
session_started session_started { initialSkill }
→ waiting_user(skill_selection) → waiting_user(intake)
``` ```
新增 `waitingReason` `session_started` 携带 `initialSkill` 时跳过 `skill_selection`
registry 中其它包仅供 `startWithOrchestrator` / 旧作品读档兼容。
**Legacy旧会话恢复**
```text
session_started无 initialSkill
→ waiting_user(skill_selection) # 仅旧快照可能出现
```
```ts ```ts
| { kind: "skill_selection"; availableSkills: SkillIndexEntry[] } | { kind: "skill_selection"; availableSkills: SkillIndexEntry[] } // legacy
| { kind: "intake"; prompt: string }
``` ```
### 5.2 向用户展示 ### 5.2 向用户展示
来自 orchestrator `## 启动询问`,例如 world-simulator
```text ```text
选择创作类型 用你自己的话描述想做什么,例如
【小说】Book 形态:卷 / 章) - 西幻升级、长篇 AI 交互、世界推着走
1. novel-standard — 标准长篇:大纲 → 事件 → 正文 - 部分代入:() 是指令,"" 是角色话,【】 是行动
2. weird-rules-short — 短篇规则怪谈:核心 → 规则 → 成章 - 或:仿写/扩写、规则怪谈、快节奏爽文……
3. basic — 最小演示
【对话】Book 形态:回合 / 多角色) Agent 会根据你的描述推理需要哪些 Worker并产出 Worker 集。
4. theater-roleplay — 剧场式角色扮演
也可直接描述你想写什么,我会帮你匹配 skill name。
``` ```
### 5.3 用户回答方式 ### 5.3 用户回答方式
```text ```text
输入编号或 namenovel-standard 直接描述创作目标(一句话即可)
输入自然语言:我想写一个剧场扮演
输入自定义:用 novel-standard但是偏悬疑
``` ```
Runtime 解析为 `skill_selected` 事件: Runtime 写入 `用户.需求`(或 skill 指定的 startupTargetKey确认后进入 design stage。
```ts ### 5.4 两阶段启动(现行)
{ type: "skill_selected"; payload: { skillId: string; userHint?: string } }
```
然后:
```text ```text
加载 skills/{skillId}/SKILL.md 阶段 1系统 自动绑定默认 orchestrator
解析 ## 启动询问 → session.slots.activeSkill 阶段 2intake 启动询问 → 用户描述需求 → confirm → agent burst
→ waiting_user(input)
message 来自 SKILL.md「启动询问·向用户展示」
必收集项 / 写入目标 同样来自该节
``` ```
### 5.4 两阶段启动 **Agent 根据需求推理 worker 集**`design-intake`),不再让用户在 registry 里四选一。
```text
询问 1系统 skill_selection 「用哪个 skill」→ registry / description
询问 2skill input 「启动询问」章节 → 每类内容问的不同
```
**只有询问 1 是系统固定的。询问 2 及之后所有创作逻辑,都在 SKILL.md 里。**
--- ---
@@ -495,10 +489,10 @@ Creation Playbook概念
## 11. 第一版范围 ## 11. 第一版范围
```text ```text
skills/ 目录 + 2 个示例 SKILL.mdnovel、theater skills/ 目录 + 示例 orchestrator 包
启动 → skill_selection → 用户选择 → 加载 skill 启动 → 自动绑定默认 orchestrator → intake描述需求
总管 prompt 注入 skill 摘要 总管 prompt 注入 skill 摘要
registry.yaml 可选 registry.yaml 可选legacy 包 / 读档兼容)
``` ```
不做: 不做:

View File

@@ -2,572 +2,67 @@
## 1. 定位 ## 1. 定位
本系统用于多 worker 协作式文本生成,覆盖: 多 worker 协作式文本生成**默认能力库包名 = `world-simulator`**(创作方法见 `design-orchestrator-guide.md`,不以「世界模拟」为默认总形态)。
```text
简易小说快速撰写quick-write
规则怪谈 / 短篇结构化创作weird-rules-short 等)
长篇小说 / 交互式写作助手interactive-novelTODO
场景扮演模拟scene-roleplayTODO
角色扮演 / 角色卡互动(并入 scene-roleplay 的 instantiate + run见 §2
```
核心思想:
```text ```text
黑板 = 标签化数据池 黑板 = 标签化数据池
标签 = skill 之间的接口 标签 = worker 之间的接口
skill = SKILL.md 定义的能力worker = 一次 invoke 设计.worker集 JSON / 声明 = 读哪些 tag、写哪些 tag、表与常驻上下文
Agent = tool loop 内调度 invoke 哪个 skill Agent = tool loop 内调度 invoke 哪个 id
Runtime = 按 skill 契约拼接上下文(上半固定、下半动态) Runtime = 按声明拼接上下文;表字段 rev 合并
``` ```
一句话:**标签驱动 + agent 选 skill + Runtime 拼上下文**——不是固定流水线,也不是 LLM 自由分发上下文。 ## 2. Skill 包、Worker 声明与 Book
上下文拼接见 `docs/context-assembly.md`。不要让 agent 临场改 inputTags分发由 `inputTags` + `contextSegments` 静态声明。 ```text
Skill 包 能力库 + design-intake + 可选 templates
设计.worker集acceptedJSON 本实例 Worker 声明play 权威)
Session + 黑板 一次运行的实参
Book 过程、存档、资产book-storage.md
```
### 业务 stage
```text
design创作核心→细节交互→细化→ 用户验收 → 用户手动进 play → done
运行相位idle | running | waiting_user | done | error
```
创作方法权威:`design-orchestrator-guide.md`
### 创作
`design-intake` → accept **`设计.worker集` JSON** → 用户手动进 play可选开局 · 开场白,非强制锁定)。
无独立 instantiate 管道。见 `design-orchestrator-guide.md`
### Play
Agent 只能 `run_worker` **声明中的 ref**;执行契约来自 Worker 集条目(可选合并 `worker-templates/`)。
--- ---
## 2. Skill 定义、实例化与 Book ## 3. 核心 tagworld-simulator
### 2.1 类比
```text ```text
Skill 包orchestrator manifest + workers = 能力定义(静态) 用户.需求 / 用户.最新输入 / 用户.修订说明
Session + 黑板 tag = 一次运行的实参 设计.worker集 / 设计.worker集.草稿
Book = 过程、资产、游玩(见 book-storage.md 创作.当前单位 / 创作.已验收单位 / 创作.已验收内容 / 创作.对话
play stage = agent invoke run skill 运行.初始变量 / 运行.事件流 / 运行.本轮.*
变量.当前
输出.用户展示 / 输出.开场白
``` ```
**写角色卡、收集设定、启动询问** 都不是独立「产品模式」,而是 **实例化阶段** 的不同形态:把 prerequisite tags 写满,然后才进入运行阶段 固定上下文叙事指南、美学纲领等在实例规格里落地play 时经 `resident_context.mount` / `contextSegments` **挂到多个 worker**——这是 tag 化的主收益,不是省掉创作期依赖正文
### 2.2 三层阶段(勿混淆) 命名与创作单位 id 见 `design-orchestrator-guide.md` §6、`ui-glossary.md`;词表可另补 `docs/tag-vocabulary.md`
```text
运行相位RuntimePhase
idle | running | waiting_user | done | error
系统在等什么。见 runtime-state-machine.md
业务 stageBook / Session
design实例化→ play运行→ done
tag 阶段(黑板条目 tag 名中的段)
候选 | 草稿 | 确认稿 | 当前 | 更新
单条数据的 lifecycle
```
业务 stage **写在各 skill 的 orchestrator.md**,不扩运行相位 enum。
### 2.3 实例化design
**职责:** agent 按需 invoke instantiate skill沉淀 `设计.*` tag产出 `设计.run_skill清单` 后 declare ready。
```text
选 orchestrator 包
→ design stage启动询问 → agent invoke instantiate skill能力库
→ declare_instance_ready
→ play stageagent invoke run skill
→ done → 归档 Book过程 + 资产,见 book-storage.md
```
可从 Book / CardAsset 加载已有 tag跳过部分 instantiate skill。
运行相位上,实例化阶段多为 `waiting_user(input)`(启动询问、总管 ask_user实例化也可调用 **setup worker**,但仍是 worker固定 inputTags/outputTags不是第二个生产总管。
### 2.4 每个 skill 声明什么
在 orchestrator.md 中写清(见 orchestrator-skill-format.md
```text
## 启动询问 / ## 实例化
prerequisiteTags本 skill 运行前必须有的 tag
instanceReadyWhen何时可进入 run stage文字条件 + tag 列表)
写入目标 tag可多个不必再塞进单一 book.brief
## 阶段定义
instantiate或沿用 stageId brief→ runwrite / review …)→ done
## Worker 编排
仅 run stage 及之后调度生产 workerinstantiate 阶段只 ask_user 或 run setup worker
```
### 2.5 已有雏形weird-rules-short
| 业务 stage | 现 stageId | 含义 |
|---|---|---|
| 实例化 | `brief` | 启动询问 → `book.brief`(≈ `需求.核心要点`)→ `startupCompleted` |
| 运行 | `write` | `write-rules` 产出规则与 core |
| 运行 | `review` | 双验收 worker |
| 结束 | `done` | finish |
`basic` 同理:`brief` = 实例化,`outline` worker = 运行。
文档与实现迁移时,可将 stageId 改名为 `instantiate`,或保留 `brief` 但在 ## 阶段定义 注明 **brief ≡ instantiate**
### 2.6 角色卡
不再单独维护 `character-card-author` skill 包。角色设定、口吻、行为边界等 tag 在 **扮演类 skill 的 instantiate 段** 收集或生成(可选 setup worker
若需跨 Session 复用,将 `角色.A.设定``角色卡.确认稿`**确认稿** 存入 Book新 Session **加载 Book** 而非再跑完整实例化。
### 2.7 Book 与黑板
```text
黑板Session 内) 运行时 tag 池worker 读写
Book项目级 长期实例accepted 确认稿归档;下一场 Session 可加载
```
详见 `docs/book-storage.md`。Session 是一次运行Book 是一个创作项目(一本小说、一个扮演项目)。
--- ---
## 3. 黑板条目 ## 4. 相关
### 2.1 最小结构 | 文档 | 关系 |
|------|------|
```ts | `creation-playbook.md` | 创作 / 游玩流 |
type BlackboardItem = { | `worker-skill-format.md` | 声明驱动 |
id: string; | `context-assembly.md` | 上下文拼接 |
tag: string; | `run-snapshot.md` | 存档 |
content: string;
source: string;
metadata?: Record<string, unknown>;
};
```
### 2.2 可选增强
```ts
type BlackboardItem = {
id: string;
tag: string;
content: string;
source: string;
scope?: string;
createdAt?: number;
updatedAt?: number;
dependencies?: string[];
metadata?: Record<string, unknown>;
};
```
### 2.3 字段含义
```text
id 唯一标识,追踪与依赖
tag 标签本体,决定身份与消费路径(核心)
content 具体内容(字符串)
source 产生该条目的 worker id调试用
scope 作用范围,如当前章节、场景、项目(可选)
dependencies 依赖的其他黑板条目 id可选
metadata 置信度、真实性、控制模式等(不参与基础路由)
```
注意:
```text
tag 是核心路由依据。
source 不是路由依据,只用于调试与溯源。
metadata 不参与基础路由,除非某 skill 明确约定。
```
**已废弃:** 旧模型的 `key``summary``tags[]``readableBy``writableBy` 作为路由字段。迁移期代码可能仍保留 `BlackboardEntry`,以本文为准逐步替换。
---
## 4. 标签原则
标签不是信息分类学,而是 **工作流接口**
### 3.1 设计原则
```text
标签越少越好,但必须能区分不同消费路径。
两个信息若永远被同一批 worker 消费,可合并 tag。
若在不同阶段被不同 worker 消费,必须拆分 tag。
若可能被误当成事实,必须加阶段段。
若涉及角色私有认知tag 里必须体现角色归属。
```
### 3.2 推荐格式
```text
对象.内容
对象.内容.阶段
领域.对象.内容.阶段
```
不强制四段式;按复杂度逐级增加。
示例:
```text
大纲.草稿
事件.草稿
正文.草稿
正文.确认稿
角色.A.行动.候选
角色.A.台词.候选
角色.A.内心想法.候选
角色.A.记忆.当前
```
### 3.3 标签替代的旧字段
```text
角色.A.内心想法.候选
```
已表达:归属 A、类型为内心想法、阶段为候选、消费路径由声明 inputTags 的 worker 决定。
**不需要** 再写 `owner``visibleTo``type=action` 等平行字段。
### 3.4 匹配规则Runtime
Worker frontmatter 声明 `inputTags`
```yaml
inputTags:
- "需求.核心要点" # 精确匹配
- "角色.A.*" # 前缀匹配tag 以「角色.A.」开头
- "大纲.*.草稿" # 前缀匹配
```
规则:
```text
无通配 → 精确匹配 tag
以 .* 结尾 → 前缀匹配(实现优先于完整正则)
多条命中 → 默认按 updatedAt 取最新;或 worker 声明 inputMerge: concat | latest
```
总管调度时也可对 **tag 索引**(不含 content做存在性判断例如「是否有 规则.确认稿」。
---
## 5. 阶段标签
阶段表示同一类信息在不同生命周期下的语义。
```text
候选 worker 生成的可能内容,不等于事实
草稿 生成中的文本或结构
确认稿 用户或流程确认,可进入长期状态
当前 当前生效状态
更新 状态变化结果
```
重要规则:
```text
候选 ≠ 已发生。
草稿 ≠ 确认稿。
角色行动候选不能直接写入长期记忆。
长期记忆优先从正文.确认稿 与明确 记忆.更新 生成。
```
验收(`user_confirmed` / `programmatic_review`通过后Runtime 将对应产物 tag 从 `.草稿` 升级为 `.确认稿`(或写入新的确认稿条目并标记旧草稿 superseded
---
## 6. Worker
每个 worker 是固定的标签消费者和生产者。
### 5.1 类型
```ts
type WorkerDefinition = {
id: string;
name: string;
description: string;
inputTags: string[];
outputTags: string[];
inputMerge?: "latest" | "concat";
run: (context: WorkerContext) => Promise<WorkerResult>;
};
type WorkerContext = {
taskId: string;
workerId: string;
items: BlackboardItem[];
params?: Record<string, unknown>;
};
type WorkerResult = {
items: BlackboardItem[];
logs?: string[];
askUser?: string[];
};
```
### 5.2 规则
```text
worker 只能读取 inputTags 声明的标签(含前缀规则)。
worker 只能输出 outputTags 声明的标签。
worker 不读取全量黑板。
LLM 不决定自己能看什么。
Runtime 校验 outputs 的 tag ⊆ outputTags。
```
### 5.3 Worker Skill 落盘
`docs/worker-skill-format.md`。正文写「怎么做」;`inputTags` / `outputTags` 写在 frontmatter。
### 5.4 ask_user
提问是 worker **能力**,不是独立 worker。中途提问时 `resumeContext` 保存 workerId**不**保存 inputTags恢复时仍从 Worker Skill 读 inputTags
---
## 7. 总管Main Agent
总管只负责流程推进。
### 6.1 职责
```text
判断当前任务属于哪种 skill / 业务阶段
选择下一个 workerrun_worker
判断是否需要追问用户ask_user
判断是否需要用户确认下一步requiresApproval
判断是否 finish
```
### 6.2 不负责
```text
不决定某条信息给谁看
不手动拼接 worker 上下文
不让 LLM 判断信息权限
不把全量黑板交给 worker
不在 run_worker 里指定 inputTags / outputTags
```
### 6.3 决策结构
```ts
type MainAgentDecision = {
id: string;
action: "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish";
reason: string;
workerId?: string;
requiresApproval: boolean;
statePatchAllowed: false;
};
```
执行链:
```text
总管选择 worker
→ Runtime 读取该 worker 的 inputTags
→ 从黑板取匹配条目,组装 WorkerContext
→ 调用 worker
→ worker 输出固定 outputTags
→ 写回黑板
→ 按 acceptanceMode 验收
```
Tool 合约见 `docs/tool-contracts.md`
---
## 8. Skill 包与 manifest
Skill 包 = `orchestrator.md`manifest+ `workers/*/SKILL.md`。详见 `orchestrator-skill-format.md`
manifest 包含:
```text
Skill 注册表instantiate + run
验收策略
Instance Ready 规则
```
**不写** 逐步编排表。inputTags / contextSegments 在 Worker SKILL.md。
### 8.1 已启用包
| 包 | 说明 |
|---|---|
| `novel/basic` | 演示:需求 → 大纲 |
| `novel/weird-rules-short` | 规则怪谈:写 + 双验收 |
### 8.2 规划包(见 `skills/README.md`
| 包 | 说明 |
|---|---|
| `novel/quick-write` | **简易档**:几乎无 tag 路由,上下文全量给 LLM |
| `novel/interactive-novel` | 长篇 / 写作助手instantiate + 多轮 run |
| `novel/novel-standard` | 标准流水线(或与 interactive 合并) |
| `dialogue/scene-roleplay` | 场景扮演(含角色设定 instantiate + 互动 run |
---
## 9. 各模式核心 tag参考
实现 skill 时从下列词汇出发,不必一次全部实现。
### 8.1 简易小说quick-write
极简;可大量依赖 session + 全量上下文tag 仅作交付锚点:
```text
用户.输入
需求.摘要
正文.草稿
正文.确认稿
```
### 8.2 结构化短篇(如 weird-rules-short
迁移目标示例(与现 key 对照实施):
```text
需求.核心要点 ← book.brief
核心.危险.隐藏 ← core.danger
规则.草稿 ← rules.draft
规则.说明.草稿 ← rules.commentary
验收.读者视角.记录 ← review.infer.notes
验收.作者视角.记录 ← review.author.notes
```
### 8.3 交互式长篇
```text
用户.原始输入 | 用户.意图转述 | 用户.确认结果
项目.设定 | 项目.风格要求
大纲.当前 | 大纲.候选修改 | 大纲.确认稿
事件.当前 | 事件.确认稿
正文.原文 | 正文.续写锚点 | 正文.草稿 | 正文.确认稿
记忆.长期摘要 | 记忆.确认稿
```
### 8.4 场景扮演
```text
用户.行动输入 | 用户.行动意图
世界.规则 | 世界.当前状态 | 世界.隐藏状态
场景.可见信息 | 场景.隐藏信息
角色.{id}.记忆 | 信念 | 行动.候选 | 台词.候选
行动.裁决结果
输出.场景反馈
更新.世界状态 | 更新.角色状态
```
### 8.5 角色卡
撰写:`角色卡.草稿``角色卡.确认稿`
游玩:读 `角色卡.确认稿` + `用户.控制模式` + `NPC.*`
---
## 10. 正文 worker 与角色候选
角色 worker 输出 `角色.*.台词.候选` 等,**不**直接进入正文事实。
正文 worker 读取候选 + 风格约束,输出 `正文.草稿`;用户确认后为 `正文.确认稿`
```text
角色候选 → 提供意图
正文 worker → 文本化、风格化、叙事化
正文.确认稿 → 最终发生与表达
```
---
## 11. 可选增强
### 10.1 采用记录
```text
tag: 采用.记录
```
记录正文采用了哪些候选条目,避免未采用候选污染记忆。早期可省略,记忆 worker 只从 `正文.确认稿` 抽取。
### 10.2 真实性 / 置信度metadata
```ts
type TruthMode =
| "truth" | "belief" | "claim" | "lie" | "rumor" | "plan" | "unknown";
```
不参与基础路由。
### 10.3 RAG
RAG 不替代黑板。检索结果也应写成 tag例如 `角色.A.相关记忆摘要`,由 worker 通过 inputTags 读取。
---
## 12. 与阶段机的关系
运行相位、业务 stage、tag 阶段 **三者正交**(详见 §2.2
```text
运行相位 系统在等什么(见 runtime-state-machine.md
业务 stage instantiate → run → doneorchestrator.mdbrief 即 instantiate
tag 阶段 黑板条目 lifecycle候选 / 草稿 / 确认稿)
```
实例化阶段在运行相位上通常体现为 `waiting_user(input)`;进入 run stage 后为 `running` + worker 验收循环。
阶段机 enum 不增加 `instantiate` 相位——业务 stage 由 orchestrator + tag 索引判断。
阶段机规则本身不因 tag 迁移而改变。变的是黑板读写、worker 上下文、总管决策字段。
---
## 13. 与 Book 存储
Book 存长期实例;黑板存 **当前 Session** 的运行时 tag。实例化产物与 run 阶段确认稿可归档到 Book。
详见 `docs/book-storage.md` 与 §2.7。
---
## 14. 实现原则(必须遵守)
```text
1. worker 不读取全量黑板quick-write 等显式声明全量 inputTags 的 skill 除外)。
2. worker 只能读取 inputTags 声明的标签。
3. worker 只能输出 outputTags 声明的标签。
4. LLM 不决定上下文分发。
5. 总管 run_worker 时不带 inputTags / outputTags。
6. 标签是 worker 之间的接口。
7. 候选不等于事实;草稿不等于确认稿。
8. 正文 worker 可重写角色候选产物。
9. 长期记忆优先从正文.确认稿 更新。
10. 简单 skill 用简单 tag复杂 skill 再增加对象与阶段段。
11. 角色卡与设定在 instantiate 阶段写入 tag跨 Session 复用走 Book 加载,不单独 author skill 包。
12. 代码与旧文档冲突时,以本文为准改代码。
```
---
## 15. 代码迁移顺序(参考)
```text
1. src/types/blackboard.ts → BlackboardItem + listByTag / matchPrefix
2. src/skills/types.ts + loader → 解析 inputTags / outputTags
3. src/worker/executor.ts → 按 tag 组装 context校验 outputTags
4. src/types/runtime.ts + main-agent → 决策去掉 inputKeys / outputKeys
5. skills/novel/weird-rules-short → 第一个 tag 化样板
6. skills/novel/quick-write → 简易全量 LLM 档
```
当前代码仍为旧 key 模型;实现前以本文为规格。
---
## 16. 相关文档
| 文档 | 内容 |
|---|---|
| `orchestrator-skill-format.md` | 总管 orchestrator.md 写法 |
| `worker-skill-format.md` | Worker SKILL.md 写法 |
| `tool-contracts.md` | 总管 / worker tool |
| `skill-format.md` | Skill 包存储 |
| `runtime-state-machine.md` | 5 相位阶段机 |
| `implementation-guide.md` | 写代码顺序 |
| `skills/README.md` | Skill 包索引与 TODO |

View File

@@ -37,7 +37,7 @@ LLM 通过 **tool call** 表达意图Runtime 校验后执行 tool 或转为 *
| name | 行为 | | name | 行为 |
|------|------| |------|------|
| `run_worker` | 执行 skill`requiresApproval``approve_step` | | `run_worker` | 执行 skill`requiresApproval``approve_step` |
| `ask_user` | `waiting_user(input)` | | `ask_user` | `waiting_user(input)``assessment` → 内容评价(主内容);`questions` → 挂载询问卡(可 Skip |
| `review_blackboard` | 向用户展示概况 → `input` | | `review_blackboard` | 向用户展示概况 → `input` |
| `finish` | `done` | | `finish` | `done` |
@@ -72,7 +72,7 @@ running 内每轮 LLM+tool 使 burst+1超过 maxBurst默认 12可配置
| name | 行为 | | name | 行为 |
|------|------| |------|------|
| `ask_user` | `worker_questions` + resumeContext | | `ask_user` | 无产物 → `worker_questions`;有产物 → 仍 `worker_completed`,追问挂到 `review_artifact.questions`(可直接 Accept |
| `submit` | 校验 tag ⊆ outputTags → 写黑板 → `worker_completed` | | `submit` | 校验 tag ⊆ outputTags → 写黑板 → `worker_completed` |
Phase A 仍用 JSON `outputs` + `askUser`,语义等价。 Phase A 仍用 JSON `outputs` + `askUser`,语义等价。
@@ -83,11 +83,11 @@ Phase A 仍用 JSON `outputs` + `askUser`,语义等价。
| waitingReason | 用户动作 | | waitingReason | 用户动作 |
|---------------|----------| |---------------|----------|
| `skill_selection` | 选包 | | `skill_selection` | 选包**legacy**,新作品不再进入) |
| `intake` / `input` | 输入 | | `intake` / `input` | 输入 |
| `approve_step` | approve / reject | | `approve_step` | approve / reject |
| `review_artifact` | accept / reject | | `review_artifact` | accept / reject;可选 `questions` 随 Accept 一并收起(表示无需再完善) |
| `worker_questions` | 输入 | | `worker_questions` | 作答 / Skip无产物时的阻塞追问 |
| `revision` | 输入修改说明 | | `revision` | 输入修改说明 |
--- ---

452
docs/ui-design.md Normal file
View File

@@ -0,0 +1,452 @@
# Web UI 心流设计
## 1. 定位
Writing Agent 的 Web 界面是 **全屏创作工作台**,不是即时通讯客户端。
**用户可见文案**阶段名、Worker 名、气泡标题)须服从 **`ui-glossary.md`**:内部仍用 `design` / `design-core` 等 id界面映射为「创作」「创作 · 核心」等中文。实现见 `src/server/display-labels.ts``web/display-labels.js`
| 层 | 角色 |
|----|------|
| **工作面working surface** | 主舞台:用户此刻要看、要改、要验收的对象 |
| **协调层coordination** | 对话与确认:说明意图、回答问题、触发下一步 |
| **项目轨project rail** | 低频:作品切换、快照、模式切换 |
| **系统态ambient** | 后台Agent 执行、调度日志,默认不抢视线 |
**对话是手段,产物与叙事是目的。** 聊天气泡适合承载协调叙事,不适合承载长规格全文或沉浸式阅读。
主场景是 **全屏浏览器、长会话12 小时)**,布局应稳定、可预期,而非频繁切 Tab 或在三块等权面板间跳视线。
相关代码:`web/index.html``web/app.js``web/agent-ui.js``web/styles.css`;视图模型 `src/server/agent-view.ts`
---
## 2. 两种业务阶段 = 两种主布局
`lifecycleStage``design` / `play`)切换时,**主舞台内容应变形**,壳子(顶栏、底栏、左轨)保持同一套肌肉记忆。
### 2.1 创作 `design` — 规格工作台
用户在定义世界、验收 Worker 集、选定开局;核心是 **结构化产物**JSON 规格、表格、规格卡)。
```text
主视线:正在成形 / 正在审阅的产物
辅视线:协调对话(窄列或底栏上方的摘要条)
冷信息:项目轨、快照、设计进度
```
典型路径:
```text
描述意图 → 主区草稿成形 → 回答追问(表单) → 验收全文 → Accept
↑ 主舞台 ↑ 检查器 ↑ 绑在产物上
```
**心流标杆(组合参考)**Notion结构化编辑+ GitHub PR版本验收+ NotebookLM零散想法 → 结构化草稿)。
### 2.2 游玩 `play` — 叙事驾驶舱
用户代入角色、推进回合;核心是 **叙事流 + 世界状态**
```text
主视线:回合输出(阅读优先,视野占比 70%+
辅视线:底部命令输入(短、快、位置固定)
冷信息:角色/世界状态抽屉、多 run 存档
```
典型路径:
```text
读回合输出 → 输入行动 → 等生成 → 继续读
↑ 主舞台 ↑ 固定底栏
```
**心流标杆**SillyTavern / 视觉小说阅读器(沉浸叙事)+ Roguelike 存档抽屉(同一开局多条 run
`world-simulator` 包说明见 `skills/dialogue/world-simulator/README.md`;流程概念见 `creation-playbook.md`
---
## 3. 全屏网页约束
与手机 IM如 QQ不同全屏 Web 有以下硬约束:
| 约束 | 设计含义 |
|------|----------|
| **视口宽** | 可横向分栏:工作面 5565% + 协调区 3545%,不必单列堆叠 |
| **会话长** | 底栏输入/确认位置永远不变;减少布局跳动 |
| **产物大** | Worker 集、开场白等长文进工作面,不进聊天气泡 |
| **双模式** | 同一壳子、两种主画布,不靠一套气泡布局打天下 |
QQ 等 IM 仅保留一条可借鉴点:**底栏作为唯一行动锚点**。其余按工作台设计。
---
## 4. 壳子布局Figma 式分区)
全屏下的推荐骨架:
```text
┌────┬──────────────────────────────┬────────────┐
│项目│ 主画布(随 stage 变) │ 检查器 │
│轨 │ │(按需展开) │
│窄 │ design: 规格 / 表格 / 验收 │ │
│ │ play: 叙事流 │ │
└────┴──────────────────────────────┴────────────┘
│ 命令栏composer固定
└──────────────────────────────────────────────────┘
```
| 区域 | 职责 | 频率 |
|------|------|------|
| **左轨** | 作品列表、路径、快照入口 | 低 |
| **顶栏** | 作品名、创作/游玩切换、状态 pill、`waitingReason` 摘要 | 常瞥 |
| **主画布** | 当前任务的工作面 | 主视线 |
| **检查器** | 产物元数据、验收按钮、进度、可编辑表格 | 按需 |
| **命令栏** | 输入、发送、主确认(与检查器不重复) | 手常驻 |
检查器默认 **收起**;有产物验收、结构化填空、开局 swipe 等任务时 **滑出**(推开主画布,窄屏用全屏 drawer。不是常驻 300px Agent 监控台。
---
## 5. 参考产品(按心流相似度)
| 产品 | 借鉴什么 | 对应本项目的阶段 |
|------|----------|------------------|
| **Cursor `AskQuestion`** | 独立询问卡、字母点选、分页、Skip/Continue、键盘捷径聊天只留索引 | `worker_questions`§8选项可编辑为我们的增强 |
| **ChatGPT 澄清问法** | 题干短、选项 = 完整可采纳句;一次收齐再继续 | 发问侧文案与批大小 |
| **Claude Artifacts / ChatGPT Canvas** | 产物占主视区,对话收窄为协调 | `review_artifact`、Worker 集验收;询问卡放置 |
| **GitHub Pull Request** | 被审对象为主舞台讨论为时间线Merge/Request changes 绑在对象上 | Accept、variant 切换、重 roll |
| **Notion / Coda** | 文档/数据库是「家」AI 辅助不抢主位 | `intake`、变量 schema、Worker 分工 |
| **Figma** | 主画布 + 窄轨 + 按需检查器 | 全屏壳子结构 |
| **SillyTavern / AI Dungeon** | 叙事占满视野;侧栏可收 | `play` 回合阅读 |
| **NotebookLM** | 多源 → 中间协调 → 右侧成型笔记 | 创作开头 intake、草稿预览 |
| **Linear** | 状态一眼可见、操作路径短、装饰极少 | 顶栏状态、长按效率 |
---
## 6. 四条统一原则
`design` / `play` 均遵守:
| 原则 | 含义 | 反面 |
|------|------|------|
| **一屏一事** | 同一时刻只有一个主任务(审产物 / 答提问 / 读叙事) | 右侧同时堆历史 + 验收 + 进度 + Agent focus |
| **行动点唯一** | Accept、发送、swipe 只在一处出现 | composer 与检查器各放一套接受按钮 |
| **对话是索引** | 气泡放摘要 + 跳转;全文在工作面 | Worker 长产出塞进中间气泡 |
| **底栏锚定** | 输入/确认永远在底部同一区域 | 底栏在纯按钮与 textarea 间大幅换位 |
---
## 7. 消息与面板的职责切分
### 7.1 中间协调区(当前 `message-feed`
**应展示:**
- 用户输入
- Worker 提问的 **一行索引**(「提问中 · 1/3」题干与选项在询问卡§8
- 简短结论、错误提示
- 流式 pending执行中摘要
- 长产物的 **摘要 +「已在检查器打开」** 链接
**不应展示:**
- `agent_tool``orchestrator_decision``worker_running`(已在 `FEED_HIDDEN_KINDS`
- 验收中的产物全文(`review_artifact` 时隐藏 `worker_output`
- intake 完整表格(仅进度摘要)
- `worker_questions` 的完整选项列表(进 §8 询问卡)
实现:`web/agent-ui.js``shouldShowInFeed``FEED_HIDDEN_KINDS`
### 7.2 检查器(目标态;当前部分在 `panel-agent`
**应承载:**
- 产物预览 / 编辑 / diff
- intake 结构化表单
- `worker_questions` 的备选容器(主路径见 §8 询问卡)
- 开局 swipe 卡片栈
- 设计进度Worker 集卡片、步骤)
- **固定上下文(独立区)**:纲领 / 常驻 / 黑板 tag 与 Worker 解耦;每张卡标明 **塞进哪些 Worker**(注入正文 / 常驻挂载 / 读入黑板)
- **上下文 / 黑板**(终产物 tag、定稿摘要、归档计数
- 验收主按钮(接受 / 不接受创作删本单位交互消息只留产物run压缩过程 tag
Worker 卡不再嵌套完整 tag 列表,只显示「上下文」摘要;完整挂载关系见上方固定上下文区。
**应降级折叠:**
- Agent focus、tool trace
- 调度时间线现「历史」Tab→ 高级 / 调试模式
### 7.3 命令栏(`composer`
`resolveComposer``web/app.js`)按 `phase` + `waitingReason` 变形,但位置不变:
| 态 | 表现 |
|----|------|
| `running` | 轻量状态 + 允许预输入草稿 |
| `approve` / `accept` | 主按钮在底栏hint 一行 |
| `review_artifact` | 底栏:输入并发送=改产物;旁挂「接受目前产物」。产物卡上不放接受钮 |
| `intake` / `input` | textarea + 发送 |
| `worker_questions` | **不抢主交互**;主回答在询问卡内(见 §8 |
---
## 8. 结构化询问卡Questions Card
`worker_questions` / `ask_user`**首选交互**。壳子与键盘心流对齐 **Cursor `AskQuestion`**;选项语义与文案改写对齐 **ChatGPT / Claude 的「选项 = 可执行草稿」**;放置原则对齐 **Artifacts / Canvas「决策面 ≠ 聊天气泡」**
### 8.0 业内标杆:借鉴什么、刻意改进什么
| 产品 / 模式 | 借鉴(照抄心流) | 不照抄 / 我们的改进 |
|-------------|------------------|---------------------|
| **Cursor `AskQuestion`** | 独立询问卡(非气泡堆选项);`A/B/C` 字母点选;`n of N` 分页;底部 **Skip + Continue**Enter / Esc聊天流只留「在答」索引 | Cursor 选项多为**只读**,改细节只能塞底部通用备注 → 我们默认**选项文案可改写**,点文案编辑、点字母选中(社区对 Cursor 的高频诉求,见 AskQuestion inline-edit 讨论) |
| **ChatGPT** | 澄清问法:题干短、选项是完整可采纳句;多题一次收齐再继续;自由补充不抢主路径 | 不用纯聊天气泡做多选题主交互;长选项不进 feed |
| **Claude Artifacts / ChatGPT Canvas** | 「结构化决策 / 产物」与对话分离:卡/面板是工作面,聊天是协调 | 询问卡不是 Artifact 全文,而是短决策面;验收长产物仍走检查器 |
| **Linear / GitHub PR 决策** | 一屏一事、主按钮唯一、状态一眼可见 | 不引入 Issue 式侧栏评论串 |
**产品结论(写死):**
1. **主路径 = Cursor 式询问卡**(字母选中 + Continue不是 QQ/ChatGPT 气泡里的长问答。
2. **选项 = 可编辑草稿**(相对 Cursor 默认只读的增强):多数一点即答;不满意就改文案再选,不必另开 Other。
3. **对话流只索引**:「提问中 · 1/3」题干与选项只在卡上。
4. **命令栏降级**:本态不当主输入;可选「自由补充」兜底,语义从属于卡上 Continue。
```text
┌─────────────────────────────────────────────┐
│ ? Questions ▲ ▼ 1 of 3 │
├─────────────────────────────────────────────┤
│ 1. Q1: 问题正文…… │
│ [A] 选项文案(可编辑) ← 点文案 = 改写 │
│ [B] 选项文案… ← 点字母 = 选中 │
│ [C] 选项文案… │
│ [+] Other… [_________] │
│ │
│ 2. Q2: …… │
│ … │
├─────────────────────────────────────────────┤
│ Skip Esc [ Continue ↵ ]│
└─────────────────────────────────────────────┘
```
**最关键交互:选项默认可改写;点选项文案是编辑,点字母按钮才是选中。**
Agent 给的选项是「可编辑草稿」,不是只读单选列表。
### 8.1 为什么适合本产品
| 特质 | 对用户心流的作用 | 业内对应 |
|------|------------------|----------|
| **点选优先,改写兜底** | 多数情况一点字母即答;不满意就改文案再选,不必另开 Other | Cursor 点选 + ChatGPT「改一句再说」 |
| **选项即可执行语义** | 提交的是用户确认(或改写后)的完整句子,可直接喂给 worker | ChatGPT / Claude 澄清选项写法 |
| **编辑与选中分离** | 避免「想改一下措辞」却误触提交路径;符合「一屏一事」 | 相对 Cursor 只读选项的增强 |
| **分页批次** | 「1 of 3」管理预期单屏别堆太多题 | Cursor `AskQuestion` |
| **键盘捷径** | Enter = ContinueEsc = Skip字母键可选中可选增强 | Cursor / IDE 习惯 |
| **模态焦点** | 当前这一批问清了再 Continue | Cursor 卡内提交;非边聊边答 |
适用:`design` 里 intake 追问、规格确认;`play` 里「局面 + 可选行动」;凡原先要用户在 composer 里写长段回答的提问,优先改成卡。
### 8.2 放置
| 区域 | 职责 | 业内对应 |
|------|------|----------|
| **主画布消息流下方(推荐)** | 询问卡嵌在协调列底部composer 之上),宽度随列自适应,不遮盖历史消息 | 决策面与对话分离,但占文档流而非浮层 |
| **检查器** | 备选:与产物并排时,卡可嵌在检查器顶部;长产物场景优先主画布居中卡 | Canvas / 侧栏表单 |
| **中间气泡** | 只留一行索引「Worker 提问中 · 1/3」点开回到卡 | Cursor聊天不堆完整选项列表 |
| **命令栏** | 降级:本态不当主输入;仅作兜底自由补充(可选) | Cursor 底部「optional details」——我们刻意弱化主语义在选项改写 |
Continue 提交后:卡收起 → 系统 `running` → 回复摘要进对话流(摘要用**用户最终确认的文案**,含改写)。
### 8.3 卡片结构与命中区
| 元件 | 规则 | 对齐 |
|------|------|------|
| **Header** | 标题「Questions」+ 批次 `n of N` + 上下翻批 | Cursor |
| **题干** | 编号 `1.`;一句说清决策点;避免在选项里复述背景 | ChatGPT 澄清问法 |
| **字母按钮 `[A]`…** | **唯一「选中」命中区**;选中后该行高亮;同题单选 | Cursor |
| **选项文案区** | **默认可编辑**;点击文案(或已聚焦时)进入改写,**不触发选中** | 相对 Cursor 的增强(社区诉求) |
| **Other…** | 每题末可选;短输入;与改写选项二选一语义相同——都是「自拟答案」 | Cursor Other + ChatGPT 自由答 |
| **Skip** | 本批可跳过Esc下游需能处理「未答」 | Cursor |
| **Continue** | 主按钮Enter校验必选题已有选中项提交的是该选项当前文案 | Cursor |
#### 8.3.1 编辑 vs 选中(强制)
```text
一行选项 = [字母按钮] + [文案区域]
点 [A] → 选中 A提交候选变为 A 的当前文案)
点 文案区 → 进入编辑;改写本地草稿;不改变选中态
(若尚未选中,可编辑完再点字母选中;
若已选中 A 又改了文案,提交仍用改写后的 A
```
细则:
1. **默认所有选项文案可改**(含 Agent 预填的 A/B/C不是「只能选、想改去 Other」Cursor 现状的痛点,我们默认解掉)。
2. **点文案 ≠ 选中**。禁止「点整行即选中」的常见单选实现。
3. **只有字母按钮(或等价:键盘 A/B/C负责选中**
4. 编辑中Enter 可结束编辑并保持焦点在卡内;勿与 Continue 的 Enter 冲突(编辑态下 Enter = 收起编辑)。
5. 提交 payload 必须带上 **最终文案**`label` / `text`),不能只交 `optionId`——否则改写丢失ChatGPT/Cursor 都要求「用户说了什么」完整回传)。
6. Other 与「改写某选项」并存Other 是空槽自拟;改写是在 Agent 草稿上微调。二者都合法。
多批Continue 先推进当前页,再进下一批;**全部页完成后再**统一提交并解除 `waiting_user`(对齐 Cursor 一批问清再继续)。
### 8.4 与 runtime 的契约
`askUser` / `ask_user` 已支持结构化 `QuestionItem[]`(兼容旧 `string[]`)。提交走 `POST /api/sessions/:id/answers`
```ts
{
answers: [
{ questionId: "q1", optionId: "A", text: "(用户看到的/改写后的完整句)" },
{ questionId: "q2", optionId: "other", text: "…" },
]
}
```
前端:主画布消息流下方询问卡(点字母选中、点文案编辑、分页 Continue气泡可只显示答句发给 AI 仍含问+答。
Worker / Agent 发问侧(对齐 Cursor AskQuestion询问不阻断主产出
- 总管 `ask_user`**assessment 是主内容**`questions` 挂在其下,用户可 Skip 并请总管基于现有信息继续。
- Worker优先 `outputs` + `askUser` 同时给出;有产物时追问挂在验收态下,用户可直接「接受目前产物」而不作答。仅完全无法产出时才阻塞提问。
- 能推断选项时 **必须**`options`;每项写成用户可直接采用或微调的**建议示范**(可含短场景钩子),禁止空泛「是 / 否」。
- `editable` 默认 `true`;挂载题 `required` 默认 `false`
- 单批题量控制在 Cursor 舒适区:**默认每页 1 题(左右切换),整批不宜超过 5 题**;更细的拆到下一轮 `ask_user`
过渡期:仅有 `string[]`UI 将每题渲染为题干 + Other。询问卡首屏可展示 `waitingReason.message` 中的内容评价。
实现:`web/questions-ui.js``web/styles.css``.qcard*`)、`web/agent-ui.js` 索引气泡。
### 8.5 反模式
- 点整行 / 点文案就选中(应点字母才选中)— 违反 Cursor 式命中区
- 选项只读、想改只能走 Other 或底栏备注Cursor 痛点,禁止照抄)
- 提交只带 `optionId`、丢掉用户改写后的 `text`(业内答案回传完整性要求)
- 把 5+ 道长选项塞进一条聊天气泡ChatGPT/Cursor 都已证明劣于独立卡)
- 选项写「是 / 否」却题干含糊
- Continue 与底栏「发送」并存且语义不同
- 无 Skip/必答标记,用户卡死无法继续
- 编辑态下 Enter 误触 Continue 整卡提交
---
## 9. `waitingReason` × `lifecycleStage` 显示规格
目标态 UI 分配表(实现时对照 `agent-view``buildFocus``actions`)。
| waitingReason / phase | design 主画布 | play 主画布 | 检查器 | 命令栏 | 顶栏状态 |
|------------------------|---------------|-------------|--------|--------|----------|
| `idle` / 未打开作品 | 空状态引导 | — | 关 | 禁用 | — |
| `intake` | 空或首条引导文案 | — | intake 表单(有字段时展开) | 描述需求 + 发送;必要项满则确认 | 描述创作需求 |
| `input`(首句) | 协调摘要 | 叙事历史 | 关 | 发送 | 描述 / 补充 |
| `input`(补充) | 协调摘要 | 叙事历史 | 关 | 发送 | 补充说明 |
| `running` | 流式 pending | 流式 pending | 关 | 等待态(可预输入) | Agent/Skill 执行中 |
| `worker_questions` | **询问卡§8** + 一行索引气泡 | 同左;行动题尤其用选项 | 可选副展示 | 降级;可 Skip | 回答提问 |
| `approve_step` | 决策摘要 | 决策摘要 | 可选:待 invoke 说明 | 确认 / 暂不 / 说明意见 | 建议 invoke |
| `review_artifact` | 「请验收」摘要;若有挂载题则询问卡可选(**Accept 即收起** | 同左 | **产物全文 + 接受/不接受** | 修改意见(可选);接受 = 无需再完善 | 验收产物 |
| `revision` | 修改说明上下文 | 同左 | 相关产物 | 按 instruction 输入 | 修订中 |
| opening swipe业务 | 卡片栈主舞台 | — | 版本 nav + 锁定 | 选定 / 换一版 | 选定开局 |
| `done` | 阶段结束摘要 | 会话结束 | 关 | 空闲提示 | 已完成 |
`skill_selection` 为遗留恢复态UI 引导用户发消息继续即可。
---
## 10. 阶段心流图
```mermaid
flowchart TB
subgraph shell [固定壳子]
Rail[左轨 · 作品]
Top[顶栏 · 模式与状态]
Cmd[底栏 · 命令栏]
end
subgraph design [design 主画布]
Spec[规格 / 表格 / 验收]
CoordD[协调摘要]
QCardD[询问卡 · worker_questions]
end
subgraph play [play 主画布]
Narr[叙事流]
CoordP[回合摘要]
QCardP[询问卡 · 行动选择]
end
subgraph inspector [检查器 · 按需]
Artifact[产物 / 表单]
Progress[设计进度]
Debug[调度日志 · 折叠]
end
Rail --> Spec
Rail --> Narr
Top --> Spec
Top --> Narr
Spec --> Cmd
Narr --> Cmd
Spec -.-> Artifact
Narr -.-> Artifact
CoordD --> Cmd
CoordP --> Cmd
QCardD -->|Continue| Cmd
QCardP -->|Continue| Cmd
```
---
## 11. 现状与目标差距
| 现状(`web/` | 心流问题 | 目标 |
|----------------|----------|------|
| 右侧常驻 `panel-agent`(约 300px | 空跑时也占视线 | 按需检查器,默认收起 |
| focus / tool-trace 在右侧顶部 | 像 IDE 调试台 | 并入折叠日志或顶栏小状态 |
| intake 可在 feed 与 composer 两处 | 信息重复 | 表格进检查器feed 仅摘要 |
| `worker_questions` = 文本列表 + composer | 用户写长段、选项语义易丢 | **§8 询问卡**:文案可改、点字母选中 |
| `askUser: string[]` | 无 options / Other / 分页 | 升级 `QuestionItem` 协议 |
| 验收时 composer 与「验收」Tab 均可操作 | 行动点分散 | 主按钮只在检查器 |
| 移动端隐藏整个 `panel-agent` | 无产物面 | 全屏 drawer |
| 主区标题写「agent-first」 | 与产品定位不符 | 创作 = 产物优先;游玩 = 叙事优先 |
已有正确方向(保持并强化):
- SillyTavern 式气泡与左右对齐(`play` 适用)
- `FEED_HIDDEN_KINDS` 分流内部消息
- `review_artifact` 时产物不进 feed、自动切验收视图
- 底栏 `composer``waitingReason` 变形
---
## 12. 实现备注
- 视图数据:`SessionView` / `buildAgentView``src/server/agent-view.ts`)已区分 `messages``reviewArtifact``waitingReason``actions``intake`;询问卡需扩展 questions 结构,见 §8.4。
- 消息分类:`classifyAgentMessage``EnrichedMessage.kind` 驱动 feed 过滤;`worker_questions` 在 feed 仅留索引。
- 生命周期:`inferLifecycleStage``body[data-lifecycle]``web/agent-ui.js`)控制 accent可扩展为 **主画布布局切换**
- 验收:`renderReviewPanel` → 迁移为检查器主内容,与 composer 去重。
- 设计进度:`renderSkillGuide` / `worker-set-user-panel` → 检查器内按需 Tab非默认主视图。
- 发问侧:`src/worker/executor.ts``askUser`、docs/`worker-skill-format.md` §ask_user → 支持带 `options` 的结构化问题。
改版时建议顺序:
1. 检查器按需展开 + 验收动作收敛
2. **§8 询问卡 UI**(先兼容现有 `string[]`,再补 options 协议)
3. design 主画布产物优先(收窄协调列)
4. play 叙事区拉满 + 状态抽屉
5. 调度日志降级为高级折叠
6. 窄屏 drawer 与 opening swipe 专屏
---
## 13. 相关文档
| 文档 | 关系 |
|------|------|
| `architecture.md` | 总览UI 为 Session 的人机界面 |
| `creation-playbook.md` | design → play 业务流程 |
| `runtime-state-machine.md` | `waitingReason` 定义 |
| `worker-skill-format.md` | `ask_user` / 提问侧契约 |
| `run-snapshot.md` | 快照与分支;左轨/检查器入口 |
| `implementation-guide.md` | `web/*` 文件职责 |
| `book-storage.md` | 作品、存档、资产形态 |

171
docs/ui-glossary.md Normal file
View File

@@ -0,0 +1,171 @@
# 用户可见文案与术语对照UI Glossary
**用途**:凡出现在 Web 界面、导出 Markdown、状态 pill、气泡标题、询问卡、检查器上的文案**禁止直接暴露**内部英文 id`design-core``design``play`。内部协议、SKILL 路径、黑板 tag、API 字段仍用英文 id。
**实现**`src/server/display-labels.ts`(权威映射);`web/display-labels.js` 须与本文及该文件保持一致。新增高频 id 时:**先改本文 → 再改代码**。
**产品隐喻**:用户侧统一用**拍摄**用语——**导演 / 剧本 / 演员 / 能力**。旧称「总管 / 能力包 / 配方 / Worker」仅作内部或过渡别称。
---
## 0. 拍摄术语(首选)
| 拍摄术语 | 用户可见含义 | 内部对应 | 勿再对用户说 |
|----------|--------------|----------|--------------|
| **导演** | ① 新建时**手动选一次**的方法起点(如世界模拟器、扩写助手),可按现场「调味」;② 会话里负责调度的 Agent | `recipes/` 选型 → 黑板 `创作.选用配方`Main Agent / orchestrator | 配方、能力包(作第二层选项)、总管 |
| **剧本** | 本局谈成的流程与规格(活的,不是死选单) | `设计.创作流程``设计.worker集` | 「剧本 Skill 菜单」、死板流程卡 |
| **演员** | 上场执行的单元(一次 `run_worker` | Worker / design-* / play workers | 对用户堆「Worker · id」标题用中文名 |
| **能力** | 共用工序模块(美学纲领与交互范式…);导演从中选型编排 | `modules/` 组件池 | 组件池、模块(可作作者文档用词) |
```text
用户选【导演】(世界模拟器 / 扩写助手 …)
【导演】调度 → 从【能力】里编排 → 谈成【剧本】
【演员】按剧本上场执行 → 游玩 / 成稿
```
**一层选型**:新建作品只选**导演**,不要再叠「能力包 + 配方」两层。
**调味**:导演给出的建议步骤是**近期起点**;编排时可增量追加、可反复调用标了可反复的能力(像现场改戏),验收的是本局【剧本】,不是磁盘上的静态菜谱。
---
## 1. 原则
1. **内外分离**id 给机器;中文给用户。
2. **拍摄词优先**:对用户优先用 §0技术文档可写「导演Main Agent / recipe」对照。
3. **阶段用产品词**lifecycle 的 `design` / `play` 用户侧统一为 **创作** / **游玩**(勿译成「设计模式 / 播放」)。
4. **与 SKILL `name` 对齐**:磁盘 worker 若已有中文 `name`,展示优先用 name本表是缺省与高频兜底。
5. **气泡标题形态**`{中文名}``{中文名} · {动作}`,例如「创作 · 流程编排 · 产出」不要「Worker · design-flow 产出」。
---
## 2. 生命周期 / 阶段
| 内部 id / 词 | 用户可见 | 业界对照与选用说明 |
|--------------|----------|-------------------|
| `design` | **创作** | 谈【剧本】的工作台。不用「设计」作顶栏主词。 |
| `play` | **游玩** | 【演员】按剧本上场。不用「播放」。 |
| `done` | **已完成** | — |
| `idle` | **待命** | — |
| `running` | **执行中** | — |
| `waiting_user` | **等待你** | — |
---
## 3. 角色与系统概念(对照表)
| 内部词 | 用户可见(首选) | 过渡别称 | 说明 |
|--------|------------------|----------|------|
| Agent / Main Agent / orchestrator | **导演** | 总管(旧) | 调度【演员】、推进【剧本】;气泡可用「导演 · 思考」。 |
| Worker | **演员** | Worker / 执行单元 | UI 标题用中文名检查器可写演员Worker。 |
| Tool | **工具** | Tool | 导演 tool call气泡可用「工具 · 读黑板」。 |
| Skill pack / recipe用户选型 | **导演**(选项名) | 能力包 / 配方(旧) | 新建下拉只出现导演选项(世界模拟器、扩写助手…)。 |
| Module / modules pool | **能力** | 组件池(作者文档) | 美学纲领与交互范式等;见 `world-simulator-modules.md`。 |
| Creation flow + Worker 集 | **剧本** | — | 动态谈成;非死选单。 |
| Blackboard | **黑板** | 上下文板 | — |
| Artifact | **产物** | — | 待验收输出。 |
| Accept / Review | **验收** / **接受** | — | 按钮用「接受」;阶段说明用「验收」。 |
| Intake | **需求描述** | 启动填空 | — |
| Burst | **本轮调度** | — | 少对用户说 burst。 |
| Questions card | **询问卡** | Questions | 详见 `ui-design.md` §8。 |
---
## 4. 创作期磁盘 Worker高频
| 内部 id | 用户可见 | 备注 |
|---------|----------|------|
| `design-flow` | **创作 · 流程编排** | 以已选【导演】为起点,编排/增量修订可变 DAG → 写入剧本(创作流程) |
| `design-step` | **创作 · 执行步骤** | 按剧本执行当前【能力】 |
| `opening-generator` | **开局 · 开场白** | 创作末尾可选 |
| `design-core` 等 | (已废弃) | 旧分步 skill勿再调度 |
标题动作后缀(拼在中文名后):
| 动作 | 后缀 |
|------|------|
| 运行中 | (可省略或「执行中」) |
| 产出 / 已完成 | **· 产出** |
| 提问 | **· 提问** |
| 占位 | **· 占位** |
---
## 5. 游玩期常用演员(缺省)
声明里可覆盖;无中文名时用下表:
| 内部 id | 用户可见 |
|---------|----------|
| `narrator` | **叙事转述** |
| `role-decide` | **角色决策** |
| `world-simulator` | **世界推演** |
| `round-present` | **回合呈现** |
导演选项展示名示例:`world-simulator`recipe**世界模拟器**`expand-assistant`**扩写助手**
**创造演员时**:规格里另写 `name`(中文展示名)。`ref` 仍用英文 kebabUI 优先 `name`
---
## 6. 创作单位 id检查器 / 进度)
| 内部 id 形态 | 用户可见规则 | 示例 |
|--------------|--------------|------|
| `phase:core` | **单位 · 核心** | — |
| `phase:refine` | **单位 · 细化** | — |
| `worker:{ref}` | **演员 · {中文名或 ref}** | `worker:narrator` → 演员 · 叙事转述 |
| `fixed:{topic}` | **能力 · {话题中文}** | `fixed:aesthetics-interaction` → 能力 · 美学纲领与交互范式 |
| `resident:{id}` | **常驻 · {id 或名}** | — |
能力话题建议译名:
| topic | 用户可见 |
|-------|----------|
| `aesthetics-interaction` | 美学纲领与交互范式 |
| `interaction` | 交互范式(旧;已并入上一行) |
| `narrative_guide` | 叙事指南 |
| `input_protocol` | 输入协议 |
| `core_premise` | 核心前提 |
| `aesthetics` | 美学纲领(旧;已并入美学纲领与交互范式) |
---
## 7. 等待态 / 焦点(对用户)
| `waitingReason.kind` | 状态摘要(短) | 焦点动作 |
|----------------------|----------------|----------|
| `intake` | 描述需求 | 描述创作需求 |
| `input` | 补充说明 | 补充说明 / 回答追问 |
| `worker_questions` | 回答提问 | 回答 · {中文演员名} |
| `approve_step` | 确认执行 | 建议调用 {中文名} |
| `review_artifact` | 验收产物 | 验收产物 |
---
## 8. 禁止出现在用户主路径上的写法
- `Worker · design-core``run_worker(design-core)`
- 顶栏 / pill 写 `design` / `play` 英文
- 「总管会调度 design-core」应写「导演会先谈剧本」或「将开始创作 · 流程编排」)
- 新建作品同时出现「导演 + 配方」两层选择
- 询问卡标题直接写 `design-core`
技术日志、导出里的「调试附录」、开发者文档不受本条限制,但默认导出给用户的 Markdown 应走同一套映射。
---
## 9. 相关文档
| 文档 | 关系 |
|------|------|
| `ui-design.md` | 布局与心流;文案须服从本文 |
| `architecture.md` | 内部术语;用户侧以本文为准 |
| `daily-use-p0.md` | 导演 / 剧本产品定义 |
| `world-simulator-modules.md` | 【能力】与导演recipes作者清单 |
| `creation-playbook.md` | 创作/游玩流程概念 |
| `design-orchestrator-guide.md` | 创作方法(可继续写英文 id面向作者 |

View File

@@ -1,279 +1,106 @@
# Worker Skill 格式 # Worker Skill 格式
## 1. 定位 > **文档层级Worker / 声明契约格式(非系统架构)。**
> 上下文编译原则见 [`architecture.md`](./architecture.md)、[`context-assembly.md`](./context-assembly.md)。
**Worker Skill** 服务 **Worker Agent**:规定 **读哪些 tag、写哪些 tag、怎么做** 本阶段产出。 ## 1. 定位(现行:声明驱动)
Worker **从属于某一个总管 Skill 包**,不与其它 skill 共享。 **默认包 `world-simulator`**
```text ```text
总管run_worker(write-rules) play 时执行契约 = accept 后的 设计.worker集 某条 workers[](实例 Worker 声明)
→ Runtime 读 workers/write-rules/SKILL.md 的 inputTags / outputTags 可选模板 = worker-templates/{ref}.yamldesign-intake 合并默认值)
→ 从黑板取匹配条目 → Worker 执行 → 写回 outputTags 磁盘 SKILL.md = 仅创建阶段必要 workerdesign-intake
``` ```
规格背景见 `docs/tag-blackboard.md``docs/context-assembly.md` 总管 `run_worker(id)` → Runtime 校验 id ∈ Worker 声明 → 从 **Worker 集条目**+ 可选模板合并)拼 prompt → 写声明的 `outputs`
**不要**为每个 play ref 预置 `workers/narrator/SKILL.md`;实例差异写在 Worker 集里。
规格见 `docs/tag-blackboard.md``docs/context-assembly.md``skills/dialogue/world-simulator/worker-templates/`
--- ---
## 2. 存储位置 ## 2. 创作阶段 Worker磁盘 SKILL.md
仅包内 **design 专用** worker 用磁盘文件:
```text ```text
skills/novel/weird-rules-short/ skills/dialogue/world-simulator/
├── orchestrator.md ├── orchestrator.md
├── worker-templates/ # 可选模板,非执行文件
└── workers/ └── workers/
── write-rules/SKILL.md ── design-intake/SKILL.md
└── review-infer/SKILL.md
``` ```
- 目录名 = 包内 **worker id** - 目录名 = worker id。
- 文件统一 **`SKILL.md`**。 - 文件名固定 **`SKILL.md`**。
- **没有** 全局共享 worker 目录。
--- ---
## 3. Frontmatter ## 3. Design Worker Frontmatter(示例)
```yaml ```yaml
--- ---
id: write-rules id: design-intake
skill: weird-rules-short skill: world-simulator
name: 规则与解析创作 name: 实例设计 · Worker 集
description: >- stage: design
从 需求.核心要点 推演内部核心,产出规则与说明。
version: 1
inputTags: inputTags:
- "需求.核心要点" - "用户.需求"
- "验收.读者视角.记录"
- "验收.作者视角.记录"
- "用户.修改说明"
outputTags: outputTags:
- "核心.危险.隐藏" - "设计.worker集"
- "规则.草稿" - "设计.worker集.草稿"
- "规则.说明.草稿"
inputMerge: latest inputMerge: latest
--- ---
``` ```
| 字段 | 用途 | | 字段 | 用途 |
|------|------| |------|------|
| `id` | 包内 skill id与 manifest 注册表一致 | | `id` | worker id |
| `skill` | 所属 orchestrator 包 name | | `skill` | 所属包 name |
| `inputTags` | Runtime 从黑板取数的 tag精确或 `前缀.*` | | `inputTags` / `outputTags` | 黑板读写白名单 |
| `outputTags` | 允许写回的 tagRuntime 校验 | | `contextSegments` | 可选;上下拼接 |
| `inputMerge` | 可选,`latest`(默认)或 `concat` |
| `contextSegments` | 可选,上下文拼接:上半 static、下半 dynamic见 §3.1 |
| `contextIsolation` | 可选:`none` \| `role_pov` \| `blind_review` |
### 3.1 contextSegments(上下文拼接) ### 3.1 contextSegments
`docs/context-assembly.md`示例: `docs/context-assembly.md`
```yaml
contextSegments:
- id: brief
tier: static
tags: ["book.brief"]
label: "## 创作需求"
- id: history
tier: dynamic
tags: ["运行.事件流"]
policy: tail_lines_80
- id: turn
tier: dynamic
tags: ["可见信息", "用户.最新输入"]
label: "## 本轮"
```
未声明时 Runtime 回退为 JSON `inputs`(当前实现)。
**review-infer 示例**(不得读隐藏核心):
```yaml
inputTags:
- "需求.核心要点"
- "规则.草稿"
- "规则.说明.草稿"
outputTags:
- "验收.读者视角.记录"
```
**review-author 示例**(可读隐藏核心):
```yaml
inputTags:
- "需求.核心要点"
- "核心.危险.隐藏"
- "规则.草稿"
- "规则.说明.草稿"
outputTags:
- "验收.作者视角.记录"
```
--- ---
## 4. 正文章节 ## 4. 实例声明字段(写入 `设计.worker集`
```markdown design-intake 产出的每条 worker
# 标题
## 角色与口吻
## 能力范围 # 能做什么 / 不能做什么
## 思维链与自检
## 上下文用法 # 各 inputTag 如何使用(不重复 frontmatter 列表)
## 输出格式 # 各 outputTag 的 content 格式
## 示例 # 可选
```
正文中用 **tag 名** 指代上下文,例如「读 `需求.核心要点`」而非旧 key `book.brief`
### 评估类 Worker
总管 orchestrator 只写:`rules 确认后 → run review-infer`
本 SKILL 写 **评估怎么做**、verdict 写入 `验收.*.记录` 的 JSON 形状等。
### 用户回合 workeruser-turn
**用途:** 该环节 **完全由用户输入** 组成LLM 不替用户选行动21 点玩家、线下人类一方等)。
**与 role-decide 的区别:**
| | role-decide | user-turn |
|--|-------------|-----------|
| 决策 | LLM 产出 `.思考` + `.行动` | 用户经 ask_user 提供worker **只**写 `.行动` |
| LLM | 需要 | 仅需展示/校验/格式化(可无生成模型) |
**frontmatter 示例:**
```yaml ```yaml
id: user-turn
skill: blackjack-roleplay
name: 用户回合
description: 展示局面,收集用户合法行动,写入角色.用户.行动
inputTags:
- "角色.用户.可见信息"
- "场景.公开叙述"
outputTags:
- "角色.用户.行动"
```
**SKILL 正文要点:**
```markdown
## 角色
你是 **用户操作的采集器**,不是玩家 AI。禁止替用户选择行动。
## 执行
1. 读可见信息与合法行动集
2. ask_user简短展示局面 + 列出可选行动
3. 校验用户输入是否在合法集内;不合法则再问
4.`角色.用户.行动`(行动选择 + 可选说话)
## 禁止
- 调用 LLM 模拟用户策略
- 写入 `.思考`(用户无内心 tag或仅 UI 留空)
```
编排:总管在轮到用户时 `run_worker(user-turn)`world-engine 与 role-decide **同一套**`.行动` 规则。
---
## 5. 运行时输出协议
Worker LLM 返回 JSONPhase APhase B 改为 tool call。语义不变
```json
{
"outputs": {
"规则.草稿": "...",
"规则.说明.草稿": "..."
},
"summary": "50字以内摘要",
"askUser": null
}
```
- `outputs` 的 key 必须是 **outputTags 中的 tag**(或与 tag 一一映射的别名,由 Runtime 归一化)。
- 缺信息时 `askUser` 提问,不臆造。
Runtime 写黑板:
```ts
{
id: "...",
tag: "规则.草稿",
content: "...",
source: "write-rules",
}
```
---
## 6. ask_user
任何 worker 可中途提问。Runtime 暂停并保存 `resumeContext`workerId 等);恢复时 **重新** 从 SKILL 读 inputTags不依赖总管。
---
## 7. 命名原则
Worker id 按 **本包流程职责** 命名,包内唯一:
| 包 | worker id | 职责 |
|----|-----------|------|
| weird-rules-short | write-rules | 写规则 |
| weird-rules-short | review-infer | 读者视角验收 |
| novel-standard | outline | 大纲 |
不要设计全局共享 worker id。
---
## 8. 与代码的关系
| 文档 | 代码 |
|------|------|
| frontmatter inputTags / outputTags | `src/skills/loader.ts``ParsedWorkerSkill` |
| 运行时取数 | `src/worker/executor.ts` |
| 角色 worker 独立 LLM | `llmProfileId` / `llm-bindings.yaml` | `src/skills/worker-llm.ts` |
当前代码仍为旧 `inputKeys` / `outputKeys` 模型;迁移以 `tag-blackboard.md` 为准。
---
## 9. Worker 独立 LLM可选预留多 AI 博弈)
默认worker 与会话 **同一 ApiProfile**(设置页当前选中的 profile
### 9.1 Worker SKILL frontmatter
```yaml
llmProfileId: "<profiles.json 中的 ApiProfile.id>"
```
省略 = 走 skill 包 `llm-bindings.yaml` 或会话默认。
### 9.2 Skill 包 llm-bindings.yaml
```yaml
defaultProfileId: null # null = 会话默认
workers: workers:
world-engine: {} - ref: narrator # 能力库 idnull = gap
role-decide: role: transcription
byRole: duty:
A: "<profile-id-1>" when:
B: "<profile-id-2>" rationale:
context:
static: [设计.worker集]
dynamic: [运行.本轮.裁决]
outputs: [输出.用户展示]
presentation:
tone:
``` ```
Runtime 解析顺序见 `src/skills/worker-llm.ts` 未写全的 `context`/`outputs` 可由 `worker-templates/{ref}.yaml` 合并。
`role-decide``slots.世界.当前角色.id` 匹配 `byRole`
### 9.3 设计意图
- 配置仍在 **profiles.json**(或 .env不在 SKILL 里写密钥
- 同一 skill 可让不同角色用不同模型/API实现真实多 agent 博弈
- 总管 LLM 不受 worker 绑定影响(始终会话默认)
--- ---
## 5. 执行要点
- Agent **不**指定 inputTags读 Worker 声明 / 模板。
- `ref: null` + `gap`:声明了职责但无模板 / SKILL需补声明或 temp worker。
- 验收:`design-intake` 默认 `user_confirmed`play 中间 worker 可 `no_confirmation`
## 相关
| 文档 | 关系 |
|------|------|
| `creation-playbook.md` | 创作流 |
| `worker-declaration.ts` | Runtime 声明校验 |
| `worker-templates/README.md` | 可选模板 |

View File

@@ -0,0 +1,139 @@
# 世界模拟器 · 导演与能力撰写清单
> 给作者用。用户侧术语:**`docs/ui-glossary.md` §0**。
> 运行:选导演 → 编排**增量**剧本 DAG → `design-step` 执行能力;可再扩步反复调用。
> **标准范例**`modules/aesthetics-interaction/prompt.md`。
> **给外部 AI 的完整泛用规范****`docs/briefs/capability-authoring-brief.md`**(项目概述 + 称呼 + 格式契约)。
## 两层
```text
【导演】recipes/ 【能力】modules/
└─ 编排增量 DAG → design-step 注入能力块 →open 则再编排)→ 演员上场
```
---
## 能力文档格式(程序可切割)
每个能力 = `modules/{id}/prompt.md` + `catalog.yaml` 一行。
程序**只认 fence 语言标签**切割,不认散文/`##` alone
| 块 id | 必填 | 用途 |
|-------|------|------|
| `meta` | 建议 | YAMLname / id / artifact / declaration / when / when_not / boundary |
| `opening` | 可选 | **默认问题**正文;程序发给用户,不经 LLM |
| `task` | 是 | 本步任务与验收边界 |
| `principles` | 建议 | 原则 |
| `probe` | 建议 | 追问策略 |
| `output` | 是 | 产物形状(多为 JSON |
| `checklist` | 建议 | 自检 |
| `examples` | 可选 | 好/坏对照 |
````markdown
# 能力中文名
## meta
```meta
name: …
id: …
artifact: 设计.…
declaration: …
```
## opening
```opening
(用户看到的开场白;可省略整块 = 本步直接调 LLM
```
## task
```task
```
## principles
```principles
```
## probe
```probe
```
## output
```output
{ … }
```
## checklist
```checklist
- [ ] …
```
````
切割实现:`parseModulePromptSections` / `extractModuleOpening` / `formatModulePromptForLlm``src/skills/creation-flow.ts`)。
注入 LLM 时按块顺序拼接,**不含** `opening`(开场已由程序发出)。
### catalog 一行(插入导演提示词)
| 字段 | 作用 |
|------|------|
| `id` / `name` / `declaration` / `artifact` | 选型与产物映射 |
| `repeatable` | 可选;`true` = 允许同能力多次编入增量 DAG |
| `opening` | 可选覆盖;一般只写在 prompt 的 `opening` 块 |
### 默认问题节奏(通用)
```text
程序发 opening → 用户首答 → LLMopening + 首答 + 切割后的方法块 + 依赖)
```
---
## 目录与清单
```text
modules/catalog.yaml
modules/{id}/prompt.md
recipes/world-simulator|expand-assistant/recipe.yaml
```
世界模拟器**可能用到**的能力(编排按需选用,勿默认全选):
| 能力 | id | 状态 |
|------|-----|------|
| 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写** |
| 实现机制 | `mechanism` | **已写** |
| 世界蓝图与人文地理 | `world-blueprint` | **已写** |
| 生成规则 | `generation-rules` | 骨架,**可反复** |
| 具体实例 | `concrete-instances` | 骨架,**可反复** |
| 拓扑图谱 | `topology` | 骨架,待细写 |
| 设计状态栏 | `status-bar` | 骨架,待细写 |
| 叙事指南 | `narrative` | 骨架,待细写 |
| 变量设计与更新规则 | `variable-design` | 骨架,待细写 |
| 变量控制上下文 | `variable-context` | 骨架,待细写 |
| 设计回复格式 | `reply-format` | 骨架,待细写 |
共用收成(池内保留,按需):
| 能力 | id | 状态 |
|------|-----|------|
| Worker 规格 | `worker-spec` | 骨架,**可反复** |
| 细化终稿 | `refine` | 骨架,待细写 |
| 导演 | 状态 |
|------|------|
| 世界模拟器 | 建议第一步:美学纲领与交互范式 |
| 扩写助手 | 待完善 |
---
## 验收
1. 只选导演 → 出**近期**创作流程(`status=open`
2. design-step 能切割出 `opening`/`task`/…
3. 有 `opening` 时先程序开场再 LLM
4. 可追加同能力多次(不同 step.id收成前 `status=closed`
5. UI 用拍摄术语

View File

@@ -1,40 +1,41 @@
# Skill 包索引 # Skill 包索引
总管 skill 以 `orchestrator.md` + `registry.yaml` 注册后才会出现在启动列表 > 用户侧术语:**导演 / 剧本 / 演员 / 能力**(见 `docs/ui-glossary.md` §0
本目录下 **仅有 README 的文件夹** 为规划占位AI 不会加载 > 系统内核见 `docs/architecture.md`
## 业务 stage 通则 当前仅注册 **`world-simulator`**`registry.yaml`)。
每个 skill 包内 orchestrator 应区分: ## 结构(一层包)
```text ```text
instantiate实例化 启动询问 / setup worker → prerequisite tags现 stageId 常叫 brief skills/
run运行 生产 worker 流水线write / review / outline …) registry.yaml
done finish → 确认稿归档 Book dialogue/
world-simulator/ # 默认 skill 包(内部英文 id
orchestrator.md # 导演调度 manifest
recipes/ # 【导演】选项(用户新建时选)
modules/ # 【能力】共用工序
workers/ # 创作期磁盘演员
design-flow/
design-step/
opening-generator/
worker-templates/ # 游玩演员默认契约(合并进 Worker 集)
``` ```
详见 `docs/tag-blackboard.md` §2。 ## 运行(简)
## 已启用 ```text
用户选【导演】
→ design-flow编排【剧本】增量 DAG设计.创作流程,可追加/可反复)
→ design-step逐步执行能力 → … → 设计.worker集
可选opening-generator
→ 用户手动进 play → 按声明调度【演员】
```
| 包 | 路径 | instantiate → run | 路径 / `run_worker` id **保持英文**;中文只出现在 `name` / `declaration` / 正文。
|---|---|---|
| basic | `novel/basic/` | `book.brief` → outline |
| weird-rules-short | `novel/weird-rules-short/` | `book.brief` → write-rules + 双 review |
| roleplay-game-theory | `dialogue/roleplay-game-theory/` | instantiate → world-engine + role-decide × N + present-round多 AI 可选) |
## 规划中TODO ## 相关
| 包 | 路径 | 说明 | - 作者清单:`docs/world-simulator-modules.md`
|---|---|---| - 包内说明:`dialogue/world-simulator/README.md`
| quick-write | `novel/quick-write/` | 简易档:弱化 tag全量 LLM | - 创作方法长文:`docs/design-orchestrator-guide.md`(部分章节仍写旧分步名,以本包为准)
| interactive-novel | `novel/interactive-novel/` | 长篇:多 tag instantiate + 多轮 run |
| novel-standard | `novel/novel-standard/` | 标准流水线(或与 interactive 合并) |
| scene-roleplay | `dialogue/scene-roleplay/` | 扮演:角色/世界 instantiate + 互动 run |
| world-simulator | `dialogue/world-simulator/` | 世界模拟器Step114 实例化设计 + run大工程设计期 |
**已取消独立包:** `character-card-author` / `character-card-play` — 角色设定与互动并入 `scene-roleplay` 的 instantiate / run跨 Session 复用走 Book。
占位目录 `dialogue/character-card-*` 仅保留说明,不注册。
标签命名规范等细节以后补 `docs/tag-vocabulary.md`(低优先级)。

View File

@@ -1,15 +0,0 @@
# character-card-author已合并概念
**不再作为独立 skill 包。**
角色卡撰写 = **实例化instantiate阶段** 的一种产出形态:在扮演类 skill规划中的 `scene-roleplay`)里,通过启动询问或 setup worker 写入 tag例如
```text
用户.角色需求 | 用户.互动偏好
角色.A.设定 | 角色卡.口吻样例 | 角色卡.行为边界
角色卡.确认稿
```
跨 Session 复用角色 → 从 **Book** 加载已有 tag不必再跑完整实例化。
`docs/tag-blackboard.md` §2.6、`skills/README.md`

View File

@@ -1,13 +0,0 @@
# character-card-play已合并概念
**不再作为独立 skill 包。**
角色卡游玩 = 同一 skill 在 **run 阶段** 的互动流水线prerequisite tags`角色卡.确认稿``角色.*.设定`)已在 instantiate 填好或从 Book 加载后,总管调度扮演 worker。
```text
instantiate 收集/加载角色与世界 tag
run 多轮互动、用户.控制模式、场景反馈
done 归档 Book
```
`docs/tag-blackboard.md` §2、`skills/dialogue/scene-roleplay/README.md`(规划)。

View File

@@ -1,16 +0,0 @@
# 可选:为本 skill 包的 worker 指定独立 LLM APIprofiles.json 中的 profile id
#
# 省略本文件、或 profileId 为空 → 全部 worker 使用会话当前默认 API与总管相同
# 配置多个 profile 后,可为不同角色绑定不同模型,实现「多 AI 博弈」。
# defaultProfileId: null
workers:
setup-scenario: {}
world-engine: {}
role-decide:
# 示例:按决策角色 id 分配不同 profile把 uuid 换成你本地的 ApiProfile.id
# byRole:
# A: "00000000-0000-0000-0000-000000000001"
# B: "00000000-0000-0000-0000-000000000002"
present-round: {}

View File

@@ -1,441 +0,0 @@
---
name: roleplay-game-theory
description: >-
何时选用:用户想模拟多个不同角色在简单博弈/思想实验处境下的决策与互动
(如囚徒困境、最后通牒、公共池、信任游戏等)。
不适用:自由剧场扮演、写小说章节、长篇叙事、需要复杂世界观的 RPG。
产出:结构化博弈实例 +(后续 run 阶段)多角色决策模拟记录。
category: dialogue
bookKind: dialogue
version: 1
tags:
- game_theory
- roleplay
- simulation
workers:
- setup-scenario
- world-engine
- role-decide
- present-round
sharedContext: shared-context.md
---
# 角色扮演博弈 · 总管
你是本 skill 的 **总管**,只负责 **流程调度**:读黑板 → 判断阶段 → `run_worker` / `ask_user` / `finish`
不写角色决策、不替 worker 模拟回合——执行细节在包内 `workers/*/SKILL.md`
固定体裁规则在 `shared-context.md`,由 Runtime 注入 **本包所有 worker**,总管不读。
**当前进度:** instantiate + run单轮/多轮 simulate + present-round已定义`semi_auto` 推进与 programmatic 验收待接入。
设计方法见 `docs/skill-design-guide.md`(抽象循环 → L0L3 分层 → 倒推标签)。
---
## 架构:三实体 + 展示 + 信息隔离
```text
world-engine 中立世界机:发 L3 可见信息 → 收齐行动 → 裁决 → 追加 L2 事件流
role-decide 各角色独立决策:读 L0L3 → 写 `.思考`(仅用户)与 `.行动`agent 可见)
present-round 展示:读 L0 + 本轮产物 + 思考 → 写 `输出.用户展示`
总管 调度轮次role-decide 须带 workerContext.roleId
```
**抽象循环:** 角色行动 → 世界反应 → 角色行动 → …(每轮末 present-round → 用户验收)
**分工:** 角色内心由 `role-decide` 产出;世界只写客观事实;**用户可见编排由 present-round 产出**,总管不拼长文。
**多 AI 博弈(可选):** 包内 `llm-bindings.yaml``role-decide.byRole` 指定不同 `ApiProfile.id`;默认全部用会话同一 API。
---
## 启动询问
选定本 skill 后,**第一个创作询问**。系统从本节读取问什么、写入哪。
**向用户展示:**
```text
你选择了「角色扮演博弈」。在开始模拟之前,请告诉我:
1. 实验情境
- 可直接说经典名(囚徒困境、最后通牒、公共池、信任游戏……)
- 或用自己的话描述一个「每人要选行动、结果取决于组合」的简单局面
2. 参与角色24 人即可)
- 每个角色用一句话说明策略倾向(如:算计型、讲公平、怕吃亏、爱冒险)
- 若有想用的称呼可一并说
3. 进程
- 单轮定胜负 / 重复多轮 / 有限 N 轮 / 直到某条件(如有人破产)
4. 信息结构(可选)
- 大家知道的都一样?有无私密信息或误解?
5. 输出偏好(可选)
- 要不要看角色思考(`.思考` tag仅你可见
- 偏冷静报告还是带一点场景描写?
6. 特殊规则或收益(可选)
- 例如:背叛惩罚加倍、允许口头承诺但不具约束力
可以一次说完。不必懂博弈论术语——我会整理成可模拟的结构。
```
**必须收集:**
- 情境(玩什么局面:经典名或自定义)
- 角色(至少 **2 个**参与者,各一句策略倾向)
- 进程(怎么进行、何时结束;未说明时 setup 按单轮默认并标注)
**可选收集:**
- 信息结构
- 输出偏好(思考可见性、叙事风格)
- 特殊规则或收益改动
**写入目标:** `用户.博弈需求`
**足够进入 setup 当:** 上述 3 项必要填空已齐,用户确认后写入 `用户.博弈需求`,再调度 setup-scenario。
---
## 实例化
**职责:**`用户.博弈需求` 整理为结构化 prerequisite tags供后续 run 阶段模拟 worker 使用。
### prerequisiteTags
运行阶段 worker 启动前,下列 tag 须存在且对应 artifact 为 **accepted**
```text
用户.博弈需求
情境.实验.设定
博弈.规则.草稿
博弈.参数.草稿
博弈.角色列表
角色.*.设定 (至少 2 条id 互不重复)
```
### instanceReadyWhen
```text
startupCompleted
且 setup-scenario 产出已被用户 accept
且 角色.*.设定 匹配条目数 ≥ 2
```
等价说法:**instantiate 阶段完成 = 用户确认结构化实例,可进入 run。**
### setupWorkers
| worker | 时机 | 说明 |
|--------|------|------|
| setup-scenario | `用户.博弈需求` 已写入,尚无 accepted 的 `情境.实验.设定` | 整理情境 / 规则 / 参数 / 角色 tag |
### loadFromBook续开
新 Session 绑定已有 Book 时,若 Book 中已有 prerequisite tag 的 **确认稿**,总管可 `ask_user` 是否跳过启动询问与 setup直接加载后继续 run。
**运行快照**(保存某一 run 步、换角色 fork 等)由 Book 层 `run-snapshot-store` 处理,见 `docs/run-snapshot.md`——**不**写进本 orchestrator。
---
## 产物说明
| 产出 | 黑板 tag | 写入者 | 阶段 | 对用户可见 |
|------|----------|--------|------|------------|
| 用户原始需求 | 用户.博弈需求 | 启动询问 / 用户 | instantiate | 是 |
| 实验情境 | 情境.实验.设定 | setup-scenario | instantiate | 是 |
| 博弈规则 | 博弈.规则.草稿 | setup-scenario | instantiate | 是 |
| 模拟参数 | 博弈.参数.草稿 | setup-scenario | instantiate | 是 |
| 角色列表 | 博弈.角色列表 | setup-scenario | instantiate | 是 |
| 角色设定 | 角色.{id}.设定 | setup-scenario | instantiate | 是 |
| 世界状态 | 世界.当前状态 | world-engine | run | 是 |
| 当前轮次 | 世界.当前轮次 | world-engine | run | 是 |
| 事件流L2 记忆) | 运行.事件流 | world-engine | run | 内部(追加式) |
| 角色可见信息L3 | 角色.{id}.可见信息 | world-engine | run | 内部 |
| 思考 | 角色.{id}.思考 | role-decide | run | **仅用户**(经 present-round |
| 行动 | 角色.{id}.行动 | role-decide | run | 用户 + 其他 agent经 world-engine 公开) |
| 裁决记录 | 世界.裁决.记录 | world-engine | run | 是(仅规则与状态,无剧情) |
| 公开叙述 | 场景.公开叙述 | world-engine | run | 是(客观事实陈述) |
| 回合摘要 | 输出.回合摘要 | world-engine | run | 是(事实摘要;**不含**角色内心) |
| 用户展示 | 输出.用户展示 | present-round | run | 是(验收用主稿) |
**Book** `bookKind: dialogue`。实例化确认稿归档后,可在新 Session 复用同一博弈设定。运行快照见 `docs/run-snapshot.md`
**流程概览:**
```text
instantiate:
用户.博弈需求 → setup-scenario → [用户验收] → instanceReady
run每轮:
world-engine发牌
→ role-decide × |博弈.角色列表|
→ world-engine裁决追加 运行.事件流)
→ present-round → 输出.用户展示
→ [用户验收](默认每轮确认)
→ 若未终局且未达轮次上限 → 下一轮
```
---
## 阶段定义
| stageId | 名称 | 别名 | 进入条件 | 退出条件 |
|---------|------|------|----------|----------|
| brief | 需求收集 | **instantiate** | skill 已选 | `用户.博弈需求` 已写入 |
| setup | 情境实例化 | **instantiate** | brief 完成 | setup-scenario 产出 **accepted**,且 ≥2 个 `角色.*.设定` |
| simulate | 回合模拟 | **run** | setup 完成 | 终局或达轮次上限,且末轮 **accepted** |
| done | 结束 | **done** | simulate 完成 | — |
**阶段链:** `brief``setup``simulate``done`
---
## 运行流程
**默认推进:** manual。
### 暂停A · 按 worker 产出)
| worker 产出 | acceptanceMode | 实例化可覆盖? |
|-------------|----------------|----------------|
| setup-scenario 全套 | user_confirmed | 否 |
| present-round → `输出.用户展示` | user_confirmed | 是 → 启动询问「每轮验收 / 每 N 轮 / 仅终局」(写入 `博弈.参数.草稿` |
| world-engine 发牌/裁决、role-decide | no_confirmation | — |
### 暂停C · 本 skill 专属)
| 检查点 | 何时停一次 |
|--------|------------|
| `every_n_rounds` | 每 N 轮 `输出.用户展示` accept 后N 由参数N=1 即每轮) |
| `terminal` | 终局当轮验收后 finish |
### 用户回合
本包 **默认无** user-turn全员 LLM 角色)。若实例为「人类参与博弈」(如 21 点),在包内增加 `user-turn` worker编排插入在 world-engine 发牌与裁决之间;见 `docs/worker-skill-format.md` §用户回合 worker。
---
## 推进策略(预留 semi_auto
**默认 manual** 每轮 `输出.用户展示``user_confirmed`
未来 `semi_auto` 可在 `## 推进策略` 声明 pauseCheckpoint例如
| id | 何时暂停 |
|----|----------|
| every_n_rounds | 每 N 轮 simulate 后 review_artifact |
| terminal | 终局时 review_artifact |
链内可省略:`world-engine` / `role-decide` / `present-round``requiresApproval=false``acceptanceMode=no_confirmation`(见 `docs/runtime-state-machine.md` §8
---
## Worker 编排
### instantiate 阶段
| stageId | 条件 | worker | acceptanceMode | requiresApproval |
|---------|------|--------|----------------|------------------|
| setup | `用户.博弈需求` 非空,无 **accepted**`情境.实验.设定`;或 reject 后重做 | setup-scenario | user_confirmed | true |
### run 阶段 · 单轮子流程
**先读 `博弈.参数.草稿` 中的「决策顺序」**,再按下表调度。
#### 同时决策
| 步骤 | 条件 | worker | acceptanceMode | requiresApproval | 备注 |
|------|------|--------|----------------|------------------|------|
| 发牌 | instanceReady首轮无 `世界.当前状态` **或** 上轮已裁决且无待收行动) | world-engine | no_confirmation | false | 无 `角色.*.行动` 输入 |
| 决策 | 已发牌,存在角色 R 尚无本轮 `角色.R.行动` | role-decide | no_confirmation | false | **workerContext.roleId=R**LLM 调用顺序任意 |
| 裁决 | **全部**角色已有 `行动` | world-engine | no_confirmation | false | 有行动输入;追加 L2 |
| 展示 | 裁决完成,尚无本轮 `输出.用户展示` | present-round | no_confirmation | false | |
| 终局 | 展示完成,`输出.用户展示` 待验收 | — | user_confirmed | — | 见验收策略 |
#### 序贯决策
| 步骤 | 条件 | worker | acceptanceMode | requiresApproval | 备注 |
|------|------|--------|----------------|------------------|------|
| 发牌 | 同同时模式 | world-engine | no_confirmation | false | |
| 决策 | 按 `序贯顺序`**第一个** 尚无 `行动` 的 R | role-decide | no_confirmation | false | **一次只跑一个 R** |
| 公开 | R 刚产出 `行动`,且序贯链未结束 | world-engine | no_confirmation | false | **仅**公布 R 的行动选择与说话;**不**做全员裁决 |
| 裁决 | 序贯顺序上 **全部**角色已有 `行动` | world-engine | no_confirmation | false | 全员行动齐后结算 |
| 展示 | 裁决完成 | present-round | no_confirmation | false | |
| 终局 | 展示待验收 | — | user_confirmed | — | |
> setup-scenario 一次产出含 `博弈.角色列表`(如 `A,B`)与 `博弈.参数.草稿`(含决策顺序)。
> role-decide**禁止** 不带 `workerContext.roleId` 调度。
> world-engine**禁止** 在缺行动候选时做裁决;**禁止** 在发牌模式写行动;序贯「公开」步 **禁止** 提前结算未决策角色的收益。
---
## 总管思维链
每轮 `planning` 按序检查,**命中第一条即行动**
### instantiate
1. **phase = waiting_user(input)**`用户.博弈需求` 未齐 → `ask_user` 补全必收集项。
2. **brief 已齐**,无 accepted 的 `情境.实验.设定``run_worker(setup-scenario)``requiresApproval: true`
3. **waiting_user(review_artifact)**setup→ 引导用户核对规则与角色。
4. 用户 **accept** setup → 进入 run见下
5. 用户 **reject** setup → 收 `用户.修订说明` → 重跑 setup-scenario。
### runsimulate
6. instanceReady`世界.当前状态` 或需新开一轮(无 pending 行动)→ `run_worker(world-engine)` 发牌。
7. **读 `博弈.参数.草稿` 决策顺序:**
- **同时**:存在角色 R 尚无 `角色.R.行动``run_worker(role-decide)`**workerContext: { roleId: R }**(顺序任意,须跑齐全员)。
- **序贯**:按 `序贯顺序` 找第一个尚无行动候选的 R → `run_worker(role-decide)` → 若链未结束 → `run_worker(world-engine)` **公开**(非裁决)→ 再下一 R若链已齐 → 步骤 8。
8. 全部角色行动齐(同时模式一次齐;序贯模式链结束)→ `run_worker(world-engine)` 裁决。
9. 裁决完成,无 accepted 的 `输出.用户展示``run_worker(present-round)`
10. **waiting_user(review_artifact)**`输出.用户展示`)→ 展示 present-round 产物;用户可选:**接受产物** / **不接受,重新来** / **说明修改意见**
11. 用户 **accept** 回合 → 若终局或达轮次上限 → `finish`;否则回到步骤 6 下一轮。
12. 用户 **reject**(重新来,无说明)→ 清除本轮行动候选、展示稿与相关草稿 tag → 从步骤 6 重跑本轮。
13. 用户 **reject**(带修改说明)→ 写入 `用户.修订说明` → 按说明决定重跑 setup 或仅重跑本轮(步骤 6
**禁止** role-decide 不带 roleId。
**禁止** 总管撰写可见信息、行动或 **用户展示稿**(由 present-round 产出)。
**禁止** 向 role-decide 注入 `博弈.规则.草稿` 全文或其他角色的 `.思考` / `.行动` tag对方言行仅经 world-engine 公开叙述)。
---
## 调度决策表
| 会话信号 | 总管 action | 参数要点 |
|----------|-------------|----------|
| 缺 用户.博弈需求 必收集项 | ask_user | 情境、角色、轮次 |
| 需求齐,无 accepted 情境设定 | run_worker | workerId=setup-scenario |
| 用户 reject setup 产物 | ask_user → run_worker | 收修订意见 → setup-scenario |
| setup accepted进入 run | run_worker | world-engine发牌 |
| 某角色未决策 | run_worker | role-decide + workerContext.roleId同时=可任意顺序跑齐;序贯=只跑序贯顺序上下一个 |
| 序贯:某角色刚决策、链未结束 | run_worker | world-engine公开非裁决 |
| 全员行动齐 | run_worker | world-engine裁决 |
| 裁决完成 | run_worker | present-round |
| 展示稿待验收 | 展示 输出.用户展示 | 用户 accept / reject重新来/ 带说明 reject |
| 终局且末轮 accepted | finish | — |
---
## 询问策略
### 总管应先问
| 何时 | 问题 | 目标 |
|------|------|------|
| brief 不完整 | 什么情境?几个角色各什么倾向?几轮? | 用户.博弈需求 |
| setup 待验收 | 规则看清了吗?角色分得够开吗? | 用户 accept/reject |
| 用户想跳过设定直接「开跑」 | 说明须先 instanceReady | — |
| reject 且未说明原因 | 改规则、改角色还是改轮次? | 用户.修订说明 |
### 交给 Worker 问
| 何时 | 问题 | 负责 worker |
|------|------|-------------|
| setup 执行中 | 情境属于哪类框架?缺收益描述? | setup-scenario |
---
## 验收策略
| 阶段 / 产物 | acceptanceMode | 验收者 | 通过后 |
|-------------|----------------|--------|--------|
| setup-scenario 产出 | user_confirmed | 用户 | instanceReady |
| world-engine 发牌 / 裁决 | no_confirmation | 程序 | 可调度 role-decide 或 present-round |
| role-decide 产出 | no_confirmation | 程序 | 下一角色或 world-engine 裁决 |
| present-round 产出 | no_confirmation | 程序 | 进入用户验收 |
| 输出.用户展示 | user_confirmed | 用户 | 下一轮或 finish |
**user_confirmed 时总管职责:** 展示 `情境.实验.设定``博弈.规则.草稿``博弈.参数.草稿`、全部 `角色.*.设定`;不省略规则收益部分。
**revision** 用户 reject → 保留 `用户.博弈需求`,追加 `用户.修订说明`(若有)→ 重跑 setup-scenario。
---
## Worker 独立 LLMllm-bindings.yaml
| worker | 默认 API | 典型独立配置 |
|--------|----------|--------------|
| setup-scenario | 会话默认 | 一般不需要 |
| world-engine | 会话默认 | 可选专用(更「冷」的裁判模型;须严格客观、零叙事) |
| role-decide | 会话默认 | **byRole**A/B/C 各绑不同 profile多 AI 博弈 |
| present-round | 会话默认 | 一般不需要 |
配置见包内 `llm-bindings.yaml`。Runtime 解析优先级:
```text
worker SKILL llmProfileId → llm-bindings workers[id].byRole[roleId] → profileId → 会话默认
```
---
## 与代码的关系
| 能力 | 实现 |
|------|------|
| workerContext.roleId | `MainAgentDecision` + phase-runtime 写入 `世界.当前角色.id` |
| 角色 input 隔离 | `filterInputsForRolePerspective`role-decide |
| 按 worker 解析 LLM | `resolveWorkerLlmProvider``src/skills/worker-llm.ts` |
---
## 禁用行为
### instantiate
- **禁止** 总管直接撰写结构化实例 tag 正文。
- **禁止** 在仅 1 个角色设定时标记 instanceReady。
- **禁止** 跳过 setup 用户验收requiresApproval: true
### run
- **禁止** role-decide 不带 `workerContext.roleId`
- **禁止** 向 role-decide 注入其他角色的 `.思考``.行动` tag对方言行仅经 world-engine 公开叙述)。
- **禁止** world-engine 替角色选行动、带角色口吻、**写或概括角色思考**。
- **禁止** world-engine 做剧情化叙述、心理描写、规则外「合理推测」。
- **禁止** 在行动未齐时做裁决。
- **禁止** 调度本包以外 worker。
- **禁止** 把未 accepted 的草稿当作已定事实。
---
## 质量评估标准instantiate
| 维度 | 说明 | 检查方式 |
|------|------|----------|
| 情境可模拟 | 局面与决策时点清楚 | 用户 + setup 自检 |
| 规则可执行 | 行动集与收益无歧义 | 用户验收 |
| 角色可区分 | ≥2 角色策略倾向可预测差异 | 用户验收 |
| 参数一致 | 轮次、风格与需求一致 | 用户验收 |
| **接受度** | setup 产物 accept/reject | user_confirmed |
---
## 示例instantiate
**用户:** 「囚徒困境,两个角色:一个很会算计,一个先相信别人;重复 3 轮,要看他们心里怎么想。」
```text
→ 写入 用户.博弈需求
→ run_worker(setup-scenario)
→ 产出 情境.实验.设定 / 博弈.规则.草稿 / 博弈.参数.草稿 / 角色.A.设定 / 角色.B.设定
→ 用户验收 → accept → instanceReady
→ run_worker(world-engine) 发牌
→ run_worker(role-decide, workerContext={roleId:A})
→ run_worker(role-decide, workerContext={roleId:B})
→ run_worker(world-engine) 裁决
→ run_worker(present-round)
→ 用户验收 输出.用户展示 → accept →(若还有轮次)下一轮 …
```
**用户 reject** 「B 不是相信别人,是怕冲突的老好人。」
```text
→ 用户.修订说明
→ run_worker(setup-scenario)input 含 用户.博弈需求 + 用户.修订说明)
→ 再次验收
```

View File

@@ -1,164 +0,0 @@
# 角色扮演博弈 · 共享上下文
本文件由 Runtime 注入 **本包所有 worker**;总管不读。
设计方法见 `docs/skill-design-guide.md`(抽象循环 → 上下文分层 → 倒推标签)。
---
## 定位
**角色扮演博弈** = 在简单、类思想实验的博弈处境下,让多个 **有鲜明策略倾向的角色** 各自决策,观察互动结果。
参考 **狼人杀 / 德州扑克** 的信息结构:
```text
world-engine 中立;发 L3「可见信息」、收行动、写 L2 事件流与公开结果
role-decide 每个角色独立 LLM读 L0L3写思考+行动
present-round 读 L0 + 本轮产物 + 思考;写用户展示稿
总管 只调度,不替任何一方思考或拼展示
```
不是:自由剧场、写小说、教学讲义。
是:结构化情境 + 明确规则 + **信息隔离** 的多角色决策模拟。
---
## 抽象循环
```text
instantiate → run 循环:
world-engine发牌
→ role-decide × N
→ world-engine裁决 / 序贯公开)
→ present-round
→ 用户验收 → 下一轮 …
```
即:**角色行动 → 世界反应 → 角色行动 → …**
---
## 上下文分层L0 → L3
Worker prompt 内上下文 **按此顺序排列**(上层变动更少):
| 层 | 内容 | 典型 tag | 变动 |
|----|------|----------|------|
| **L0** | 前提 / 境遇 | `情境.实验.设定`、本 shared-context | 实例化后不变 |
| **L1** | 人设 | `角色.{id}.设定` | 极少改 |
| **L2** | 历史记忆 | `运行.事件流` | 每轮 **追加**,不整段重写 |
| **L3** | 本轮看见 | `角色.{id}.可见信息``场景.公开叙述` | 每轮更新 |
**L2 原则:** 类似聊天记录——world-engine 裁决后追加一条role-decide 读全流作记忆,**不**依赖 L3 重复携带全部历史。
---
## 三实体 + 展示
| 实体 | worker | 读 | 写 |
|------|--------|-----|-----|
| 角色 A…N | role-decide | L0 情境、L1 本人设定、L2 事件流、L3 可见+公开、参数 | `.思考`(仅用户)、`.行动`agent 可见) |
| 世界 | world-engine | L0 规则情境、L2、L3 行动 | L3、L2 追加、状态、裁决、回合摘要 |
| 展示 | present-round | L0、参数、本轮摘要/公开/思考/行动、L2 可选 | `输出.用户展示` |
| 总管 | — | tag 索引 | 调度 |
## 两层输出(角色)
| 层 | tag | 用户 | 其他 role-decide | world-engine |
|----|-----|------|------------------|--------------|
| **思考** | `角色.{id}.思考` | ✅ | ❌ | ❌ |
| **行动** | `角色.{id}.行动` | ✅ | ✅(经 L3 公开) | ✅ |
- **行动** = 规则上的 **行动选择** + 对外 **说话**
- **思考** = 内心权衡(思想实验核心观察面,**只给用户与 present-round**
**禁止** role-decide 读 `博弈.规则.草稿` 全文(不对称信息由 world-engine 裁进 L3
**禁止** world-engine 读/写 `.思考` 或做剧情化推演。
**禁止** role-decide 读其他角色的 `.思考``.行动` 原文(仅 L3
---
## 经典情境参考worker 可引用)
| 情境 | 核心张力 | 典型角色分化 |
|------|----------|--------------|
| 囚徒困境 | 个体理性 vs 集体最优 | 算计者、互惠者、怀疑者 |
| 最后通牒 | 公平 vs 收益最大化 | 公平敏感、冷酷最大化、面子型 |
| 公共池 | 短期私益 vs 长期共益 | 搭便车、规范执行、观望 |
| 信任游戏 | 风险与回报 | 冒险信任、条件合作、防御 |
自定义须写清 **行动集合****收益随组合变化**(自然语言即可)。
---
## 角色设定原则L1
每个 `角色.{id}.设定` 须含:称呼、策略倾向、信息立场、行为边界。
**禁止** 空泛人设而无决策含义。
---
## 规则表述原则
`博弈.规则.草稿` 须让 **world-engine** 能回答:决策时点、合法行动、收益映射、信息对称性、多轮衔接。
---
## 决策顺序
写入 `博弈.参数.草稿`
| 模式 | 世界语义 | 总管调度 |
|------|----------|----------|
| **同时** | 互不可见本轮选择,齐后结算 | 发牌 → role-decide × 全员 → 裁决 |
| **序贯** | 后动者见先动者 **已公开** 行动 | 发牌 → role-decide → 公开 → … → 裁决 |
缺省:同时框架→同时;序贯框架(最后通牒等)→序贯。
---
## 输出风格
**world-engine 不受风格影响**——始终客观事实。
| 模式 | role-decide | present-round |
|------|-------------|---------------|
| 分析报告 | 思考简短 | 表格化、少戏剧 |
| 角色内心 | 完整 `.思考` | 思考段完整 |
| 戏剧化 | `.行动.说话` 可带台词 | 可润色 **已公开** 言行 |
默认:展示思考 + 简短客观摘要。
---
## Book
- **确认稿**(情境、规则、角色设定等)跨 Session 复用,见 orchestrator `loadFromBook`
- **实例快照**`kind: instance`):实例化完成后的 **对象**(情境、规则、角色设定),不含轮次进度;见 `docs/run-snapshot.md`
- **运行存档**`kind: run`run 中某一时刻的完整进度。
---
## 单轮流程run
### 同时决策
```text
world-engine发牌→ L3
role-decide × 全员 → .思考 + .行动
world-engine裁决→ L2 追加、L3、摘要
present-round → 输出.用户展示
用户验收
```
### 序贯决策
```text
world-engine发牌
对每个 idrole-decide → world-engine公开非裁决
world-engine裁决→ L2 追加 …
present-round → 用户验收
```
多轮重复直至终局或轮次上限。

View File

@@ -1,98 +0,0 @@
---
id: present-round
skill: roleplay-game-theory
name: 回合展示
description: >-
将本轮世界反馈与各角色思考、行动按实例化参数编排为用户可读展示稿。
属于 run 阶段;不模拟决策、不修改规则或状态。
version: 1
inputTags:
- "情境.实验.设定"
- "博弈.参数.草稿"
- "博弈.角色列表"
- "世界.当前轮次"
- "输出.回合摘要"
- "场景.公开叙述"
- "角色.*.思考"
- "角色.*.行动"
- "运行.事件流"
outputTags:
- "输出.用户展示"
inputMerge: latest
---
# 回合展示 Worker
## 角色
你是 **面向用户的展示编排者**——把已发生的客观事实与各角色内心思考,按 `博弈.参数.草稿` 中的输出偏好组装成 **一份** 用户展示稿。
你不做决策、不改规则、不替 world-engine 补充裁决、不泄露给其他 role-decide。
shared-context 已注入L0 前提)。
## 输入分层(按此顺序阅读与编排)
| 层 | tag | 用途 |
|----|-----|------|
| L0 | `情境.实验.设定` | 始终在最开头点明「正在什么思想实验里」 |
| 参数 | `博弈.参数.草稿` | 输出风格、是否展示思考 |
| 本轮 | `输出.回合摘要``场景.公开叙述` | 世界客观反馈 |
| 本轮 | `角色.{id}.思考``角色.{id}.行动` | 各角色内心与对外言行 |
| L2 | `运行.事件流` | 可选:前几轮摘要,便于多轮阅读 |
## 能力范围
**可以做:**
-`输出风格` 组织 Markdown分析报告 / 角色内心 / 戏剧化)
- 在展示稿 **开头** 用 12 句重述 L0 实验名称(勿每段重复)
- 分列「局面 / 各角色思考 / 各角色行动 / 规则结果」
-`展示思考: 否`,省略或极短概括思考段
**不可以做:**
- 编造未出现在 input 中的行动或裁决
- 把思考写进「公开局面」段(思考段须与用户专阅层一致,单独小节)
- 修改任何黑板 tag 除 `输出.用户展示`
## 输出格式
### 输出.用户展示
```markdown
## 实验
(来自 情境.实验.设定 的实验名称,一句)
## 第 N 轮
(世界.当前轮次)
### 局面
(来自 场景.公开叙述 + 输出.回合摘要 的客观内容;无心理描写)
### 角色思考
(按 博弈.角色列表 顺序;每角色一小节;展示思考=否 时可省略本段)
#### {称呼}{id}
(来自 角色.{id}.思考)
### 角色行动
(按 博弈.角色列表 顺序)
#### {称呼}{id}
(来自 角色.{id}.行动:行动选择 + 说话)
### 规则结果
(来自 输出.回合摘要 的结果部分;纯事实)
```
**戏剧化** 风格:可在「局面」「行动」段适度润色 **已公开** 的说话与行动,**禁止** 润色思考为公开对白,**禁止** 添加规则外情节。
**分析报告** 风格:思考段可缩短;局面与结果优先表格化。
## 自检
- [ ] 展示稿开头含 L0 实验名
- [ ] 思考与公开局面分节,未混写
- [ ] 所有行动/结果可追溯到 input tag
- [ ] 符合 `博弈.参数.草稿` 的输出风格与展示思考开关

View File

@@ -1,135 +0,0 @@
---
id: role-decide
skill: roleplay-game-theory
name: 角色决策
description: >-
当前角色产出两层内容:思考(仅用户可见)与行动含说话(其余角色 agent 可见)。
世界机只读行动层做规则裁决与公开分发。
version: 1
inputTags:
- "世界.当前角色.id"
- "情境.实验.设定"
- "角色.*.设定"
- "运行.事件流"
- "角色.*.可见信息"
- "场景.公开叙述"
- "博弈.参数.草稿"
outputTags:
- 角色.*.思考
- 角色.*.行动
inputMerge: latest
---
# 角色决策 Worker
## 角色
你是 **某一个参与者的决策代理**——只代表 `世界.当前角色.id` 所指的角色。
你不是 narrator、不是裁判不知道其他角色本轮选了什么除非 L3 已公开)。
## 输入分层(按此顺序理解)
| 层 | tag | 说明 |
|----|-----|------|
| L0 | `情境.实验.设定` | 你处在什么思想实验里(如囚徒困境、最后通牒) |
| L1 | `角色.{id}.设定` | 你的人设:策略倾向、边界(几乎不变) |
| L2 | `运行.事件流` | 之前各轮已发生的 **公开** 事件(追加式记忆,勿重复改写) |
| L3 | `角色.{id}.可见信息``场景.公开叙述` | **本轮** 你看见的内容 |
| 参数 | `博弈.参数.草稿` | 思考详略、表达风格 |
**禁止**`博弈.规则.草稿` 全文;规则片段仅来自 L3 中 world-engine 裁剪给你的部分。
## 两层输出(可见性硬边界)
思想实验里,**角色如何思考** 是核心观察面,但必须与其他角色能感知到的行为分开:
| 层 | tag | 谁可见 | 内容 |
|----|-----|--------|------|
| **思考** | `角色.{id}.思考` | **仅用户** | 内心权衡、动机、怀疑、策略推演;**不含**对外说的话 |
| **行动** | `角色.{id}.行动` | **用户** + **其余角色 agent**(经 world-engine 公开) | 规则意义上的行动选择 + **说话**(对外可见言论) |
```text
思考 → 用户读;其他 agent 永远看不到
行动 → world-engine 读取 → 写入 公开叙述 / 后续角色的 可见信息 → 其他 agent 据此决策
```
**禁止** 把内心独白、未说出口的打算写进 `行动`
**禁止** 把对外说的话只写在 `思考` 里——那会导致其他 agent 听不见。
shared-context 已注入。
## 能力范围
**可以做:**
- 根据 `角色.{id}.可见信息``场景.公开叙述` 决策
-**`角色.{id}.思考`**(完整内心过程)
-**`角色.{id}.行动`**(行动选择 + 可选的对外说话)
**不可以做:**
- 读取其他 `角色.*` 的 tag含对方的思考对方的行动也只认 world-engine 公开后的叙述)
- 替 world-engine 判定收益或编造规则外结果
- 编造合法行动集以外的选择
- 同时决策回合中假设他人已选行动
- 序贯回合中引用尚未公开的行动
## 输入用法
| tag | 用法 |
|-----|------|
| 世界.当前角色.id | 你的角色标识(如 A、B |
| 情境.实验.设定 | L0实验名称与局面始终先读 |
| 角色.{id}.设定 | L1本人人设 |
| 运行.事件流 | L2过往轮次公开记录类似聊天记录 |
| 角色.{id}.可见信息 | L3本轮私有 + 已公开信息(**不含**他人思考) |
| 场景.公开叙述 | L3所有人已知的客观局面与 **已公开** 言行 |
| 博弈.参数.草稿 | 思考详略、表达风格 |
## 输出格式
### 角色.{id}.思考(仅用户可见)
```markdown
## 已知事实
(仅复述可见信息与公开叙述;不加戏)
## 内心权衡
(策略倾向、风险、公平、对他人类型的猜测等——**未说出口**
## 决策倾向
(为何倾向某行动;备选与不确定性)
```
`博弈.参数.草稿``展示思考: 否`,仍须写本 tag 供归档,可缩短「内心权衡」。
### 角色.{id}.行动(用户 + 其他 agent 可见)
```markdown
## 行动选择
从可见信息中的合法行动集选一项world-engine 规则裁决 **只读此字段**
## 说话
(可选:本情境若允许沟通,写对外说出的原话;无则写「(无)」)
```
- **行动选择** = 博弈规则中的离散选项(合作/背叛、出价 50…
- **说话** = 其他参与者能听见/看见的表述;戏剧化参数下可写台词,但须与行动选择一致
- **禁止** 在本 tag 写内心独白或未公开意图
`{id}` 必须等于 `世界.当前角色.id`
## 与世界机的协作
```text
你写:思考(用户专阅) + 行动(含说话,供公开)
世界机读:行动 → 规则裁决 + 将行动/说话写入公开叙述与他人可见信息
世界机不读:思考
```
## 自检
- [ ] 思考与行动严格分离:内心在 `.思考`,对外言行在 `.行动`
- [ ] 行动选择在合法集合内
- [ ] 未引用其他角色私有信息或未公开行动
- [ ] output tag 的 id 与 世界.当前角色.id 一致

View File

@@ -1,162 +0,0 @@
---
id: setup-scenario
skill: roleplay-game-theory
name: 博弈情境实例化
description: >-
从用户.博弈需求 整理结构化情境、规则、角色设定与模拟参数。
属于 instantiate 阶段,不执行回合模拟。
version: 1
inputTags:
- "用户.博弈需求"
- "用户.修订说明"
- "用户.worker答复"
outputTags:
- "情境.实验.设定"
- "博弈.规则.草稿"
- "博弈.参数.草稿"
- "博弈.角色列表"
- "角色.*.设定"
inputMerge: latest
---
# 博弈情境实例化 Worker
## 角色
你是 **实例化执行者**:把用户的口语需求整理成可运行的博弈实例 tag。
固定上下文shared-context已注入——**情境类型、角色原则、规则表述标准** 以此为准。
你不模拟回合、不写最终博弈结果。
## 能力范围
**可以做:**
- 识别或构造思想实验式情境(含经典变体与合理自定义)
- 为每个参与角色分配稳定 id`A``B``C`… 或用户给定短名)
- 写出 `情境.实验.设定`:局面、背景、决策时点
- 写出 `博弈.规则.草稿`:行动集、收益逻辑、信息结构、多轮衔接
- 写出 `博弈.参数.草稿`:轮次、决策顺序、序贯顺序、输出风格、是否展示思考
- 为每个角色写 `角色.{id}.设定`
**不可以做:**
- 替角色做第一轮决策或预测结果
- 引入规则中未声明的「超能力」或无法追溯的任意裁决
- 把多个角色合并成一个 tag
- 写小说章节正文
## 执行顺序
```text
读 用户.博弈需求
→ 定情境类型与实验 id
→ 列角色清单与 id
→ 写 情境.实验.设定
→ 写 博弈.规则.草稿
→ 写 博弈.参数.草稿
→ 写 博弈.角色列表(逗号分隔 id如 A,B
→ 逐个写 角色.{id}.设定
→ 自检 → 提交
```
## 各 tag 格式
### 情境.实验.设定
```markdown
## 实验名称
(简短标题)
## 情境描述
25 句:参与者面对什么局面)
## 决策结构
- 参与人数N
- 决策模式:同时 / 序贯 / 混合
- 轮次:单轮 | 重复 K 轮 | 直到某条件
## 信息结构
(谁知道什么;有无私有信号或隐藏类型)
```
### 博弈.规则.草稿
```markdown
## 合法行动
(每个角色在每决策点的行动集合;可表格)
## 收益与结果
(行动组合如何映射到收益或状态变化;自然语言即可,须无歧义)
## 约束与特殊条款
(可选:承诺、惩罚、沟通轮、随机事件等)
## 终止条件
(何时结束、如何汇总多轮)
```
### 博弈.参数.草稿
```markdown
## 轮次
(数字或「单轮」)
## 决策顺序
同时 | 序贯
(世界内语义:同时=互不可见本轮选择;序贯=按顺序公开行动)
## 序贯顺序
(仅序贯时填写,逗号分隔 id如 A,B同时模式可省略
## 输出风格
分析报告 | 角色内心 | 戏剧化
(仅影响 role-decide 的表达world-engine 始终客观事实模式)
## 展示思考
是 | 否
(是 → 用户验收时展示各 `角色.{id}.思考`**仅用户**可见,不注入其他 agent
## 备注
(用户特殊要求、实例化时的假设)
```
### 角色.{id}.设定
```markdown
## 称呼
(显示名)
## 策略倾向
(如何做决策:风险偏好、公平权重、对背叛的反应等)
## 信息立场
(相信什么、怀疑什么、是否误解规则)
## 行为边界
(绝不会选的行动或风格)
## 一句话人设
(供模拟时快速把握)
```
## 自检清单
- [ ] 至少 2 个角色,每个有独立 `角色.{id}.设定`
- [ ] 规则中每个角色的行动集已写明
- [ ] 收益逻辑覆盖主要行动组合,无「由裁判随意决定」
- [ ] 参数与用户需求一致(轮次、**决策顺序**、风格)
- [ ] 未写入任何回合结果或胜负预测
## 缺信息时
若存在 `用户.worker答复`,须与 `用户.博弈需求` 一并理解,**勿重复询问已答内容**。
通过 `ask_user` 向用户确认(**优先一次问清**
- 情境模糊:经典框架二选一,或请用户补行动/收益
- 角色不足 2 人:请用户补第二个角色倾向
- 轮次未说明:默认单轮,并告知用户
- 决策顺序未说明:根据情境类型推断(囚徒困境等同行动→同时;最后通牒等先后手→序贯),写入参数并在备注标注假设
**禁止** 在缺关键信息时用纯默认糊过去而不标注假设;若用假设,须在 `博弈.参数.草稿` 的备注中写明。

View File

@@ -1,204 +0,0 @@
---
id: world-engine
skill: roleplay-game-theory
name: 世界运行机
description: >-
中立裁判:仅按规则处理行动、更新世界状态、生成客观事实叙述与各角色可见信息。
不做角色式决策、不写内心思考、不做剧情推演。
version: 1
inputTags:
- "情境.实验.设定"
- "博弈.规则.草稿"
- "博弈.参数.草稿"
- "博弈.角色列表"
- "世界.当前状态"
- "世界.当前轮次"
- "运行.事件流"
- "角色.*.行动"
outputTags:
- "世界.当前状态"
- "世界.当前轮次"
- "世界.裁决.记录"
- "场景.公开叙述"
- "角色.*.可见信息"
- "运行.事件流"
- "输出.回合摘要"
inputMerge: latest
---
# 世界运行机 Worker
## 角色
你是 **中立的世界运行机器**(参考狼人杀主持人 / 德州扑克发牌员):
- 只对 **已提交的行动****规则** 作机械反应
- 输出 **可复核的客观事实**(状态变量、行动组合、规则映射结果)
- 维护 L3`可见信息``公开叙述`)与 L2`运行.事件流` **追加**
- **绝不** 替角色做决策、**绝不** 写角色内心、**绝不** 做剧情化推演
shared-context 已注入(含 L0L3 分层说明)。
## 硬边界:你只读「行动」,不读「思考」
```text
你的输入(来自 role-decide 你不读、不写
────────────────────────────────────────────────────
角色.{id}.行动(行动选择 + 说话) 角色.{id}.思考(仅用户可见)
```
- **行动选择** → 规则裁决
- **说话** → 写入 `场景.公开叙述` 与他人 `可见信息`(客观转述原话,不加心理描写)
- **思考** → 本 worker **无此输入**;不得臆造、概括或泄露
**禁止** 读取、引用、概括或复述 `角色.*.思考`(及旧 tag `推理.候选`)。
**禁止**`场景.公开叙述``输出.回合摘要``世界.裁决.记录` 中加入:
- 角色心理、动机猜测、性格评价
- 剧情走向、悬念、气氛、「接下来可能…」
- 规则未定义的 extrapolation「合理推测」「为了故事好看」等
- 文学化场景描写(环境、表情、对话 dramatization
有规则依据才写;规则算不出就写「规则未覆盖」或 ask_user**不** 用叙事填补空白。
## 两种运行模式(由输入自动判断)
```text
若无任何 角色.*.行动 → 开局 / 新轮次「发牌」模式
若已有全部待决策角色的 行动 → 裁决模式
若序贯进行中:仅部分角色有 行动,且 博弈.参数 为序贯 → 「公开」模式
若仅部分角色有 行动 且 同时模式 → ask_user请总管先补齐缺失角色的 role-decide
```
### 发牌 / 新轮次模式
- 初始化或推进 `世界.当前轮次`
- 根据 `博弈.规则.草稿` 写本轮回合各角色 **各自能看见什么**`角色.{id}.可见信息`
-`场景.公开叙述`**公共桌面上的客观信息**:轮次、已知状态、已公开历史结果)
- **不** 写行动候选、**不** 写任何角色的思考
### 裁决模式
- 读取所有 `角色.*.行动` 中的 **行动选择** 字段,按规则计算结果
- 将各角色的 **说话**(若有)客观写入 `场景.公开叙述`
-`世界.裁决.记录`(结构化:输入行动组合 → 规则条款 → 数值/状态变化)
- 更新 `世界.当前状态`
-`场景.公开叙述`**本轮已发生的客观结果**,所有人可见)
- 为下一轮准备各 `角色.{id}.可见信息`(若终局则写终局可见信息)
-`输出.回合摘要`**纯事实摘要**,见下)
- **追加** `运行.事件流`L2 记忆,见下)
### 序贯 · 公开模式(非裁决)
`博弈.参数.草稿`**序贯**,且仅 **部分** 角色已有本轮 `行动`
- **只**公布序贯顺序上 **最新已提交** 角色的 **行动选择****说话**
- 更新 **尚未决策** 角色的 `角色.{id}.可见信息`(使其能看见已公开行动)
- **不**写 `世界.裁决.记录`、**不**写 `输出.回合摘要`、**不**更新最终收益(等全员齐后再裁决)
- **不**泄露尚未决策角色的任何信息
## 信息边界(关键)
`角色.{id}.可见信息` **不得** 包含:
- 其他角色的 `思考` / 内心(**永不可写入可见信息**
- 其他角色尚未公开的行动或说话(同时决策阶段)
- 规则中的隐藏信息(除非该角色设定允许知道)
- 你对局势的「解读」或「建议」
`场景.公开叙述` 只含 **所有参与者都已知或刚一起见证的客观内容**——像牌局记录,不像小说段落。
## 各 tag 格式
### 世界.当前状态
```markdown
## 状态摘要
(结构化键值:收益、关键变量、是否终局;无形容词堆砌)
## 历史指针
(可选:已发生轮次列表)
```
### 世界.当前轮次
纯数字字符串,如 `1``2`
### 世界.裁决.记录
```markdown
## 本轮行动
| 角色 | 行动选择 | 说话(若有) |
|------|----------|--------------|
## 规则适用
(引用 博弈.规则.草稿 中具体条款,逐步映射)
## 状态变化
(前后对比;仅规则推出的变化,可复核)
```
### 场景.公开叙述
```markdown
(第三人称 **事实陈述**:轮次、各角色已公开的行动选择与说话、状态变化。禁止内心、禁止剧情、禁止推测。)
```
示例(好):「第 2 轮。A 选择合作B 选择背叛。按规则A 收益 -1B 收益 +3。」
示例「B 冷酷地背叛了信任他的 A局面变得紧张……」
### 角色.{id}.可见信息
```markdown
## 他人已公开的言行
(仅 world-engine 从 行动.说话 / 行动选择 转述的客观记录;**无**对方思考)
## 你看到的局面
(该角色视角下 **已知的客观状态**
## 你的可选行动
(列出合法行动集)
## 你知道的规则片段
(仅该角色应知道的部分)
## 私有信号
(若有;无则省略)
```
**不得** 在本 tag 中写「你应该…」「对方可能在想…」等引导性主观内容。
### 输出.回合摘要
```markdown
## 第 N 轮摘要
- 各角色行动:(仅列行动,不解释动机)
- 规则结果:(收益/状态变化)
- 终局:(若适用)
```
**禁止** 在本 tag 中写角色思考、心理、剧情评价。用户要看思考 → 由 present-round 读各 `角色.{id}.思考`
### 运行.事件流L2 · 追加式记忆)
**裁决模式**下须 **在原有内容后追加** 本轮条目(读 input 中的 `运行.事件流`,勿整段覆盖):
```markdown
---
## 第 N 轮
- 公开局面:(一句,来自 场景.公开叙述 要点)
- 各角色行动:(行动选择,不含动机)
- 规则结果:(收益/状态变化要点)
```
发牌 / 序贯公开模式 **不** 写事件流(仅裁决后追加)。
role-decide 读全流作历史L3 `可见信息` **不必** 重复全部旧轮内容。
## 自检
- [ ] 裁决后已 **追加** `运行.事件流`,未覆盖历史
- [ ] 未替任何角色选择行动
- [ ] 未写任何角色内心或动机
- [ ] 裁决每一步可追溯到 博弈.规则.草稿 具体条款
- [ ] 各 可见信息 无交叉泄露
- [ ] 公开叙述、裁决记录、回合摘要 **三者事实一致**
- [ ] 全文无剧情化、无规则外推测

View File

@@ -1,41 +0,0 @@
# scene-roleplayTODO
## 定位
场景扮演 + 角色卡互动(**一个 skill 包**,不拆 author / play
```text
instantiate 用户.角色需求、角色.{id}.设定、角色卡.*、世界.规则、用户.控制模式 …
run 用户行动 → 世界裁决 → 角色反应 → 输出.场景反馈 → 状态更新
done 确认稿归档 Book下一场 Session 可从 Book 加载 instantiate tag
```
## instantiate 预期 tag草案
```text
用户.角色需求 | 用户.互动偏好 | 用户.控制模式
角色.{id}.设定 | 角色卡.口吻样例 | 角色卡.行为边界 | 角色卡.确认稿
世界.规则 | 世界.当前状态
场景.当前状态(可选开场)
```
可选 **setup worker** 把用户口语整理为上述 tag仍是 worker非第二总管
## run 预期 tag草案
```text
用户.行动输入 | 用户.行动意图 | 用户.导演指令
场景.可见信息 | 场景.隐藏信息
角色.{id}.记忆 | 信念 | 行动.候选 | 台词.候选 | 反应
行动.裁决结果
输出.场景反馈 | 输出.互动回复
更新.世界状态 | 更新.角色状态
```
## 下一步
- [ ]`orchestrator.md`## 阶段定义instantiate → run → done
- [ ] workers可选 setup世界运行、角色、场景反馈、状态更新
- [ ] 注册到 `registry.yaml`
`docs/tag-blackboard.md` §2。

View File

@@ -1,37 +1,32 @@
# world-simulator(规划中) # world-simulator
## 定位 默认 skill 包。术语与作者清单:`docs/ui-glossary.md` §0、`docs/world-simulator-modules.md`
RP 代入式交互小说 · **世界模拟器**:用户扮演固定角色,在预先设定好的世界观里遇见不同的人、不同的事。 ## 目录
交互范式接近 SillyTavern但本包专精为 **跑团式世界运转**
架构见 `docs/architecture.md``docs/skill-design-guide.md``docs/context-assembly.md`
## 工程结构
```text ```text
orchestrator.md manifestskill 注册表 + 验收 + readiness待写 orchestrator.md # 调度 manifest + uiPrompt
workers/ 各 instantiate / run skillSKILL.md recipes/
shared-context.md 包级固定上下文上半(待写) catalog.yaml # 导演选项列表
instantiate-orchestrator.md 【遗留】Step114 管道草稿;将拆为 workers/ 能力库后废弃主流程地位 {id}/recipe.yaml # 单份导演(建议 steps可调味
modules/
catalog.yaml # 能力目录id / 中文名 / declaration / artifact
{id}/prompt.md # 能力正文design-step 注入)
workers/
design-flow/SKILL.md # 编排剧本骨架
design-step/SKILL.md # 执行当前能力步
opening-generator/SKILL.md # 可选开场白
worker-templates/
{ref}.yaml # 游玩演员默认契约(写入 Worker 集时合并)
``` ```
**不注册到 `registry.yaml`**,直至 run manifest 与 readiness 定稿。 ## 运行
## 实例化design ```text
选导演recipes→ design-flow → 验收近期 设计.创作流程status=open
→ 反复 design-step注入 modules/{id}/prompt.md
→ 不够则再 design-flow可追加 repeatable 能力)→ closed
可选opening-generator → 手动进 play
```
不是固定 1→14 管道。agent 在 design stage 旧分步 `design-core` / `design-fixed` / `design-worker` / `design-refine` / `design-common.md` **已移除**,勿再添加。
1.**交互范式** skill → `设计.run_skill清单`
2. 按清单倒推,按需 invoke 其他 instantiate skill世界蓝图、变量目录、叙事指南…
3. `declare_instance_ready` → play
`instantiate-orchestrator.md` 中的 Step 表可迁移为 **workers/** 下独立 SKILL.md供 agent 选用。
## 运行play
agent tool loop 内 invoke run skill世界模拟器、转述者、变量管理…上下文 **上半固定、下半动态**,见 `docs/context-assembly.md`
## 与 scene-roleplay
`scene-roleplay` 为通用占位;本包是其 **世界层 + 固定 POV + 跑团式流向** 专精版。

View File

@@ -1,310 +0,0 @@
---
name: world-simulator-instantiate
description: >-
何时选用正在从零搭建「世界模拟器」skill 包的实例化阶段(设计规格,非运行游玩)。
适用RP 代入式交互小说;用户扮演固定角色,在预设世界观中遇人遇事;偏跑团而非单角色倾向型 AIRP。
本 skill 指导初始化 LLM 选择下一步,并逐步撰写 Step114。
不适用:已实例化完毕只需 run 游玩;写其它 skill 包。
category: dialogue
bookKind: dialogue
version: 0.3
tags:
- world_simulator
- instantiate
- design
- rp
relatedSkill: world-simulator
---
# 世界模拟器 · 实例化设计引导
你是 **实例化设计期的引导 LLM**。职责:在「世界模拟器」大工程里 **一次只推进一个 Step**,把实例化规格写清楚。
**不做:** 模拟游玩、调度 run 阶段 worker、一次性写完 Step114。
运行期总管见同包 `orchestrator.md`(待写)。本文档只服务 **instantiate 规格的设计与填充**
---
## 产品概括
| 维度 | 定调 |
|------|------|
| **形态** | RP 代入式交互小说;交互范式接近 SillyTavern |
| **本包焦点** | **世界模拟器**:固定 POV 角色 × 预设世界观 × 遇人遇事 |
| **体验** | 偏跑团世界运转、事件推进、NPC 有轨迹;同时覆盖故事运行与角色扮演 |
| **工程** | 大工程 = **实例化 skill**(本文)+ **总管 skill**(运行)+ worker skillStep12 倒推) |
---
## 本 Skill 与总管 Skill 的分工
```text
【设计期 · instantiate-orchestrator本文
用户 + 初始化 LLM
→ Step114 逐步撰写规格、规则、样例、tag 草案、worker 倒推表
→ 产出:设计文档 + shared-context 草案 + tag 词汇表 + worker 清单
【运行期 · orchestrator.md待写
选 skill → 启动询问 → setup worker → instanceReady
→ run世界机 / 叙事 / 展示 / 用户回合 …
→ done归档 Book
```
设计 Step11 是在 **定 tag 接口**;运行期总管 **只读已定编排**,不在 run 中临场发明 tag 或 worker。
---
## 实例化步骤总览
每一步的正文 **逐步展开**;下表为 **经用户确认的 Step 含义**v0.3)。
| Step | 名称 | 定义(做什么) | 预期产出 | 状态 |
|------|------|----------------|----------|------|
| **1** | 交互范式和美学纲领 | 定 **整体结构、架构**,以及最终呈现给用户的 **整体感受**(沉浸气质、信息层次、节奏感——不是单条 UI 规则) | `设计.交互范式``设计.美学纲领` | 待撰写 |
| **2** | 实现机制 | **非程序机制**。实例化进程中的 **锚点设定**:为实现 Step1 而必须写进世界里的 **关键设定**(世界如何「撑住」那套交互与美学) | `设计.实现机制.锚点` | 待撰写 |
| **3** | 故事流向 | **极宽广**。协助定义开场方向、故事可走的主轴(如:高中 → 考试/学习/恋爱;电竞 → 比赛/训练/舆论)——不是细纲,是 **流向域** | `设计.故事流向` | 待撰写 |
| **4** | 世界蓝图 | **整体背景板**:可具体(大陆、势力割据)或抽象(主神空间、无限世界);尺度可小(囚禁的单间)或大(多元宇宙) | `世界.蓝图.草稿` | 待撰写 |
| **5** | 拓扑图形 | **可多次调用的拓扑规格**。不限于地图:职业进阶路径、人物关系网、区域连通……凡满足「节点 + 边 + 约束」的均可 | `世界.拓扑.{id}.草稿`(可多份) | 待撰写 |
| **6** | 生成规则 | **用来生成实例的规则**元规则。可多次调用run 阶段也可用于 **实时生成** 故事内所需内容 | `世界.生成规则.{id}.草稿`(可多份) | 待撰写 |
| **7** | 具体实例 | 用 Step6 规则 **逐条生成** 的示例;每份实例 **必须声明遵循哪条生成规则**。可多次:角色、物品、功法等 | `实例.{类型}.{id}.草稿`(可多份) | 待撰写 |
| **8** | 叙事指南核心 | 指导 **输出 worker 如何整理「正文」**POV、时态、详略、禁忌、段落习惯 | `shared-context.md` 叙事核心章 | 待撰写 |
| **9** | 语料库与场景策略集 | 口吻样例、场景类型模板、对话/描写/节奏策略 | `语料.场景策略集.草稿` | 待撰写 |
| **10** | 变量管理与变化规则 | 世界/角色/场景 **状态变量**;读写时机;谁有权改;变化约束 | `变量.目录.草稿``变量.变化规则.草稿` | 待撰写 |
| **11** | tag 目录与分配规则 | 全包 tag 命名、阶段(草稿/确认稿)、谁写谁读 | `tag.目录.md` | 待撰写 |
| **12** | tag 倒推 worker 管理与上下文继承 | 从 tag 反推 worker 列表、inputTags、隔离与继承 | `workers/` 清单 + 倒推表 + orchestrator 编排草案 | 待撰写 |
| **13** | 设计回复格式 | **最终给用户看的完整组装**(含正文):抬头(日期时间等)、正文区块位置、尾部附加块(状态/选项/meta。Step8 只管正文;本步管 **整页/整条回复** | `输出.回复格式.规范` | 待撰写 |
| **14** | 开场白与初始变量处理 | 开场白 = **第一次完整输入–输出校验**:须遵循 Step13 格式、Step8 正文、Step10 初始变量;同时定 instanceReady 条件 | `实例.开场.规范``instanceReadyWhen` 草案 | 待撰写 |
### Step8 与 Step13 的分工(重要)
```text
Step8 叙事指南核心 → 「正文」怎么写、怎么排段落、什么气质
Step13 设计回复格式 → 「整条回复」怎么组装:壳层 + 正文槽位 + 附加模块
Step14 开场白 → 用真实开场跑通 8 + 13 + 10当作验收轮
```
---
## 可重复调用的 Step5 / 6 / 7
Step5、Step6、Step7 **不是「各做一次」**,而是 **按 id 多次产出**
| Step | 多次调用含义 | 命名建议 |
|------|--------------|----------|
| 5 拓扑图形 | 每张地图、每条进阶树、每个关系网各一份 | `世界.拓扑.{id}.草稿` |
| 6 生成规则 | 每种生成逻辑各一条规则 | `世界.生成规则.{id}.草稿` |
| 7 具体实例 | 每条规则下可生成多个实例;实例须 **引用** 所遵循的规则 id | `实例.{类型}.{id}.草稿` + 元数据 `遵循规则: {ruleId}` |
**Step7 约束:** 每一份具体实例必须准确对应 **某一条** Step6 生成规则;不可无规则「手写特例」混入实例库(除非该特例本身先补一条规则)。
---
## 步骤依赖关系
```text
Step1 ──→ Step2 ──→ Step3
│ │
└──────────┬───────────┘
Step4 ──→ Step5可多次
Step4 ──→ Step6可多次──→ Step7可多次依赖对应 rule id
Step1,2,3,4 ──────────────→ Step8 ──→ Step9
Step4,6 ────────────────────→ Step10
Step8,9,10 ────────────────→ Step11 ──→ Step12
Step1,8 ────────────────────→ Step13
Step7,10,13 + 8 ────────────→ Step14
```
**硬依赖:**
| Step | 必须先有 |
|------|----------|
| 2 | 1 |
| 3 | 1建议 2 |
| 4 | 1建议 2, 3 |
| 5 | 4 |
| 6 | 4, 5建议 2, 3 |
| 7 | 6对应 rule id |
| 8 | 1, 2, 3 |
| 9 | 8建议 3, 4 |
| 10 | 4, 6建议 2 |
| 11 | 8, 9, 10 |
| 12 | 11 |
| 13 | 1, 8 |
| 14 | 7至少一个样例实例, 8, 10, 13 |
**推荐顺序(首次搭建):**
```text
1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14
```
Step5 / 6 / 7 在 run 设计期也可 **追加** 新 id不必等全链结束。
---
## 如何选择下一步
每轮开始或用户说「继续 / 下一步 / 做 Step N」时
### 1. 读取进度
检查包内各 Step 产出与 `设计.Step{N}.确认稿` / 多 id 条目(拓扑、规则、实例)是否已有。
### 2. 向用户展示
```text
世界模拟器 · 实例化设计
进度摘要:
Step1 … Step2 … … Step14 …
拓扑 id 列表:… 生成规则 id 列表:… 实例 id 列表:…
建议下一步Step {N} — {名称}
理由:{依赖已满足}
可选:
A. 按建议做 Step {N}
B. 指定 Step 编号
C. 为 Step 5 / 6 / 7 新增一个 id多次调用
D. 修订某已完成 Step 或某个 id
E. 查看摘要
```
### 3. 默认算法
```text
IF 用户指定 Step K 或「新增 拓扑/规则/实例 id」
→ 检查依赖;缺则说明
ELSE IF 存在「草稿未确认」的 Step 或 id
→ 建议先确认或修订
ELSE
→ 按推荐顺序取第一个「待撰写」且依赖已满足的 Step
```
### 4. 单 Step 工作模式(命中即停)
```text
1. 用 35 句复述本 Step 目标(用上表定义,勿偷换概念)
2. ask_user本 Step 关键问题(见下节)
3. 与用户迭代草稿
4. 写入约定路径 / tag多 id 步须写清 id 与交叉引用
5. 自检
6. 用户确认 → 标「确认稿」→ 停止;提示下次入口
```
**禁止:** 一个 turn 连写多个 Step未经确认将草稿当定稿。
---
## 各 Step 启动问句
| Step | 至少问什么 |
|------|------------|
| **1** | 整体架构几层(用户 / 世界 / 叙事 / meta最终呈现想让人 **感受到** 什么(例:冷峻观测、沉浸第二人称、群像剧场)? |
| **2** | 为撑住 Step1世界里 **必须钉死** 的设定有哪些(例:信息可见性规则、权力结构、时间粒度)? |
| **3** | 本世界的 **流向域** 有哪些(例:高中:学业/恋爱/社交;电竞:赛事/训练/舆论)?开场倾向哪一条? |
| **4** | 背景板尺度?具体地理 vs 抽象空间?核心冲突与时代? |
| **5** | 本 id 拓扑 **类型**(地图 / 关系网 / 进阶树 / 其他)?节点与边?约束? |
| **6** | 本 id 规则 **生成什么**?输入依赖哪些拓扑/蓝图?确定性 vs 随机?实例化 vs run 实时共用? |
| **7** | 遵循哪条 `生成规则.{id}`?本实例类型与规模? |
| **8** | 正文 POV、时态、篇幅、禁止出现在正文里的 meta |
| **9** | 需要哪些场景类型模板?固定口吻样例? |
| **10** | 跟踪哪些变量?初始值谁定?变化触发条件? |
| **11** | tag 前缀与生命周期instantiate / run 分界? |
| **12** | run 循环 sketch展示 worker 与生产 worker 边界 |
| **13** | 一条回复含哪些 **模块**(抬头字段、正文槽、尾栏)?各模块数据来源 tag |
| **14** | 开场白文本初始变量赋值是否通过格式与叙事自检instanceReady 条件? |
---
## 单 Step 自检清单
```text
- [ ] 与 Step1 整体感受 / 架构无冲突
- [ ] Step2 锚点未被误写成程序流程或 run worker 调度
- [ ] Step7 实例已标明所遵循的 Step6 rule id
- [ ] Step8 只管正文Step13 才定义壳层与模块组装
- [ ] Step14 明确引用 Step13 格式与 Step10 初始变量
- [ ] 多 id 产物5/6/7id 唯一、可交叉引用
- [ ] 用户已确认
```
---
## 启动询问
**向用户展示:**
```text
你正在搭建「世界模拟器」的 **实例化设计**(不是开始游玩)。
请告诉我:
1. 全新开始,还是已有部分 Step / 文档?(可粘贴)
2. 严格按推荐顺序,还是指定 Step / 指定新增拓扑·规则·实例 id
3. (可选)对标作品或气质(供 Step1、Step3 参考)
确认后给出「建议下一步」,且 **只做一个 Step 或一个 id**。
```
**设计期 tag可选**
```text
用户.实例化设计.意图
用户.实例化设计.进度摘要
设计.Step{N}.草稿 | 设计.Step{N}.确认稿
世界.拓扑.{id}.*
世界.生成规则.{id}.*
实例.{类型}.{id}.*
```
---
## 总管思维链(设计期)
```text
1. 是否在 run 游玩?→ 是则改读 orchestrator.md
2. 当前 Step / id 状态?依赖是否满足?
3. 用户指定 Step 或「新增 id」否则用默认顺序
4. ask_user → 只写当前 Step 或当前 id
5. 自检 → 用户确认 → 更新进度 → 命中即停
```
---
## 禁用行为
```text
- 不要一次性写完 Step114
- 不要把 Step2 写成程序、Runtime 或 API 机制
- 不要把 Step3 写成细纲或分章目录(它是流向域)
- 不要写无 Step6 rule id 绑定的 Step7 实例
- 不要在 Step8 里定义抬头/尾栏/状态栏(那是 Step13
- 不要跳过 Step11 直接写 worker inputTags
- 不要照搬 roleplay-game-theory 的 tag 名;本包 Step11 自定
```
---
## 与邻近 skill 的关系
| 包 | 关系 |
|----|------|
| `scene-roleplay` | 近亲;本包专精世界层 + 固定 POV + 跑团流向 |
| `roleplay-game-theory` | 仅借鉴设计方法(`docs/skill-design-guide.md`),不照搬 tag/worker |
---
## 当前工程状态
```text
instantiate-orchestrator.md v0.3 步骤定义 + 选步逻辑(本文)
README.md v0.2 包索引
orchestrator.md 未写
workers/ 未写
shared-context.md 未写
Step114 正文 未写
```

View File

@@ -0,0 +1,163 @@
# 美学纲领与交互范式
> 能力标准形范例。程序只切割下方 **fence 块**`meta` / `opening` / `task` / …);`##` 标题仅供人读。
> 写法说明:`docs/world-simulator-modules.md`。
## meta
```meta
name: 美学纲领与交互范式
id: aesthetics-interaction
artifact: 设计.美学纲领与交互范式
declaration: >
钉清站位、正文呈现、核心体验与禁忌、何时等用户;
询问相近故合为一步,勿拆成「交互」「美学」两步
when: |
可玩/可沉浸体验,但站位、呈现、核心感觉或轮转边界仍不清时;
世界模拟类导演通常作为剧本第一步(无上游依赖)。
已有等价定稿且用户未要求重谈 → 不要重复排入。
when_not: |
只改实现细节且体验契约已验收;纯工具/排版;与「给玩家什么体验」无关。
boundary: |
本能力:体验是什么、怎么发生在用户身上、轮转怎么停。
叙事指南:世界/助手态度口径(契约已写清的勿重复问卷)。
世界蓝图 / 具体实例 / 生成规则:世界内容与可执行规则——本步不定。
实现机制 / 拓扑 / 变量* / 状态栏 / 回复格式 / Worker 规格:落到可运行结构——本步不定。
```
## opening
```opening
先用几句话抓住你要的「变造世界」和体验核心(想到什么写什么,不必整齐):
1. 原型世界是什么?变在哪里?
例:丧尸世界,但只有我不会被感染;现代都市,但灵异是真的;也可以只说「现代都市」,变点稍后补。
2. 你代入吗?
· 完全代入我就是那个人TA 的遭遇≈我的遭遇)
· 部分代入 / 跟着体验(有距离)
· 操控或旁观(角色是棋子/观察对象)
· 其它(请写明)
3. 最想反复感受到的是什么?
一句话即可——权力、求生紧张、关系拉扯、解谜、荣耀、禁忌刺激……用你自己的词。
```
## task
```task
与用户一起钉清一份「体验契约」:给玩家什么感受、如何参与、正文如何呈现、何时等待用户。
写入产物 tag通常为 设计.美学纲领与交互范式)。
一次 design-step、一场对话、一次验收。参与方式与美学内容在同一步内交叉追问不要先交「半份交互」再开「半份美学」。
若程序已发出默认问题:用户首答在「用户.worker答复」开场白在「创作.能力开场白」。禁止重复同一开场;在首答基础上补洞、追问、产出。
```
## principles
```principles
1. 核心是用户想要的体验;其余询问都是为了碰到这个核心。
2. 信息不足时宁可少写,不要强行填充。
3. 做好随时修改的准备;探测时大胆,用户可以说不。
4. 完善设定时不要忘掉原本的核心体验。
5. 用户常说不清想要什么 → 展示优于提问、大胆优于保守、去道德化/常态化、具体化;负面信号同样有效。
6. 变造世界:先抓「原型 + 变在哪里」;核心体验往往来自变种,不要写成设定百科。
7. 同一设定不同用户要的东西可以完全不同(角斗士:荣耀 / 自由 / 血与酒)——找到核心与禁忌,不是补全世界观。
```
## probe
```probe
用户首句或黑板已有的直接采纳。一次 askUser 12 点,优先具体选项/短场景。
【默认问题已覆盖】变造(原型+变点)、代入与否、最想感受到的核心。首答后按缺口补,勿重问已答清的。
【参与 · 结构性】
- 用户与 <user>:代入程度 / 情感距离 / 控制期待 / 多角色
- 输入解释:主视角行动 vs 世界行动;正文人称(第二人称 / 上帝 / 第一人称…)
- 焦点位置:自己 / 人物 / 群像 / 关系 / 世界规则 / 感觉 / 叙事
- 满足来源:感同身受 / 互动 / 观察 / 掌控 / 创作质量
完备:能判断用户站哪、镜头看哪、爽从哪来。
【内容 · 深度随焦点】
核心感觉;人物与关系;身体与感官;背景与规则(宜短);意义与主题。
变造已定位的,只挖服务核心体验的部分。
【轮转】
思考与抉择、言语:何时等、怎么等、例外、输入后怎么补(只确认/补细节/补心理/连带后果)。
【收成三块】
体验内核(含形状:过程/张力/层次/构成/循环);呈现要点;边界禁忌。
```
## output
```output
{
"brief": "一句话:用户要的核心体验",
"变造": {
"原型": "…",
"变点": "…"
},
"参与": {
"站位": "…",
"代入": "完全|部分|操控|旁观|…",
"用户与user": "情感距离 / 控制期待 / 多角色",
"输入解释": "主视角行动 vs 世界行动等",
"焦点": "主焦点;次要(若有)",
"满足来源": "…"
},
"呈现": {
"正文人称或体裁": "…",
"系统扮演": "世界执行者 / …",
"输出形态": "单段叙事 / …"
},
"体验": {
"内核": "最想反复感受到的…",
"形状": "过程|张力|层次|构成|循环|…",
"呈现要点": ["…"],
"边界禁忌": ["…"]
},
"内容摘要": {
"核心感觉": "…",
"人物与关系": "…",
"身体与感官": "…",
"背景与规则": "…",
"意义与主题": "…"
},
"轮转": {
"思考与抉择": "何时等、怎么等、例外、输入后怎么补",
"言语": "…"
}
}
```
未知字段省略或写「未定」禁止编造。summary`美学纲领与交互范式 · …`(点题核心体验)。
## checklist
```checklist
- [ ] 变造:原型与变点是否清楚(或已标未定)?
- [ ] 代入与否 / 站位是否清楚?
- [ ] 最想反复感受到的核心是否一句话能说清?
- [ ] 体验内核 + 呈现要点 + 边界禁忌是否成套(可短,不可互相矛盾)?
- [ ] 重大抉择与言语的等待边界是否够下游引用?
- [ ] 有没有为「完整」强行填充的设定百科?
- [ ] 有没有完善着偏离用户原话里的核心?
- [ ] 是否误写成 Worker 列表?
```
## examples
```examples
好:
- 「丧尸世界,变点是只有我不会被感染;完全代入;最想感到特权下的求生紧张与道德压力。」
- 用户要艰苦角斗 → 禁忌写「轻松连胜」;选项带短场景。
坏:
- 「现代都市,有公司有地铁…」(百科,无变点、无核心感受)
- 用户要爽文角斗,仍按真实艰苦写满受苦
- 「你想要什么体验?」抽象难答;或一张表全勾选
```

View File

@@ -0,0 +1,98 @@
# 共用【能力】目录modules
# 各【导演】与编排都从这里选型。
# id = modules/{id}/ 文件夹
# name = 固定中文名(流程 JSON 的 name同能力可多次时靠 step.id 区分)
# declaration = 插入导演 / design-flow 提示词的短声明(选型用;勿塞全文)
# artifact = 执行期产物 tag程序映射不写进流程 JSON
# repeatable = true 时允许同能力多次编入增量 DAG如生成规则、具体实例
# opening = 可选;覆盖 prompt.md 里 ```opening 默认问题(一般只写在 prompt.md
#
# 标准范例aesthetics-interaction美学纲领与交互范式
# 后来写能力:同结构 prompt.md含默认问题+ 本表一行 declaration
#
# 世界模拟器常用能力见下「体验→世界→机制→呈现→收成」;编排按需选用,勿默认全选。
# 流程是可变增量 DAG勿一次排完全程可反复调用标了 repeatable 的能力。
modules:
# —— 体验契约 ——
- id: aesthetics-interaction
name: 美学纲领与交互范式
declaration: >-
钉清站位、正文呈现、核心体验与禁忌、何时等用户;
询问相近故合为一步,勿拆成「交互」「美学」两步
artifact: 设计.美学纲领与交互范式
# —— 世界与内容 ——
- id: world-blueprint
name: 世界蓝图与人文地理
declaration: >-
当体验需要可引用的「舞台背景」时使用:钉舞台尺度、熟悉基底与变造、
关键格局与人文地理;只细化舞台上会出现的部分,不做设定百科或纸面地图
artifact: 设计.世界蓝图与人文地理
- id: generation-rules
name: 生成规则
repeatable: true
declaration: >-
钉内容如何生成/推进的可执行规则(触发、约束、节奏),非散文设定;
可按题材块反复调用(每次增量补规则)
artifact: 设计.生成规则
- id: concrete-instances
name: 具体实例
repeatable: true
declaration: >-
钉关键人物/地点/物件等具体实例,供开局与生成锚定;勿堆无关名单;
可按需反复调用(每次增量补实例)
artifact: 设计.具体实例
- id: narrative
name: 叙事指南
declaration: 世界/助手态度与体验边界(不等于文风;契约已写清的态度勿重复问卷)
artifact: 设计.叙事指南
# —— 机制与数据 ——
- id: mechanism
name: 实现机制
declaration: >-
当核心体验已经明确,需要识别由哪些角色、世界、关系、规则或情境切面支撑,
并把体验落成可供后续设定工作的核心支点时使用
artifact: 设计.实现机制
- id: topology
name: 拓扑图谱
declaration: 钉 worker/表/阶段的依赖、触发与数据流向;一张图说清谁读谁写
artifact: 设计.拓扑图谱
- id: variable-design
name: 变量设计与更新规则
declaration: 钉变量字段、初值与更新时机/规则;与表副作用对齐
artifact: 设计.变量设计与更新规则
- id: variable-context
name: 变量控制上下文
declaration: 钉哪些变量如何挂进常驻上下文 / 控制生成口径(可见性与措辞)
artifact: 设计.变量控制上下文
# —— 呈现 ——
- id: status-bar
name: 设计状态栏
declaration: 钉用户可见状态栏:字段、刷新时机、与正文如何拼装
artifact: 设计.状态栏
- id: reply-format
name: 设计回复格式
declaration: 钉单轮可见输出的结构(正文块、面板、拼接顺序),服务体验契约
artifact: 设计.回复格式
# —— 收成 / 钉声明(共用;世界模拟器按需) ——
- id: worker-spec
name: Worker 规格
repeatable: true
declaration: 钉一个游玩期执行单元(职责、读写、挂载);多演员时可多次调用
artifact: 设计.worker规格
- id: refine
name: 细化终稿
declaration: 钉死关键前提、表与副作用,收成可进游玩的规格
artifact: 设计.worker集

View File

@@ -0,0 +1,58 @@
# 具体实例
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 具体实例
id: concrete-instances
artifact: 设计.具体实例
declaration: 钉关键人物/地点/物件等具体实例,供开局与生成锚定;勿堆无关名单
when: 需要可点名的人/地/物锚定开局或生成时
when_not: 蓝图骨架未定时就堆长名单;用户明确只要即时生成、不要预置实例时
boundary: |
本能力:具体可引用条目。
世界蓝图与人文地理:骨架与尺度,非逐条名片。
```
## opening
```opening
```
## task
```task
钉清关键具体实例(人物/地点/物件等),写入 设计.具体实例。只保留服务体验与开局的条目。
(待作者细写)
```
## principles
```principles
少而可用;每条要说清为何需要。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"人物": [{ "名": "…", "要点": "…", "为何需要": "…" }],
"地点": [],
"物件": [],
"其它": []
}
```
## checklist
```checklist
- [ ] 删掉某条会丢掉哪段体验?说不清则删
```

View File

@@ -0,0 +1,60 @@
# 生成规则
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 生成规则
id: generation-rules
artifact: 设计.生成规则
declaration: 钉内容如何生成/推进的可执行规则(触发、约束、节奏),非散文设定
when: 需要把「世界怎么动、内容怎么长出来」写成可执行约束时
when_not: 仍在谈体验感受、尚未需要可执行规则时
boundary: |
本能力:触发、约束、节奏、可否随机等可执行规则。
变量设计与更新规则:字段与何时改值。
实现机制:落到哪些 worker/表来执行这些规则。
```
## opening
```opening
```
## task
```task
钉清生成与推进规则,写入 设计.生成规则。须可被下游 worker/程序引用,禁止只写散文氛围。
(待作者细写)
```
## principles
```principles
可执行优于文采;与体验禁忌对齐。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"触发": ["…"],
"约束": ["…"],
"节奏": "…",
"例外": ["…"]
}
```
## checklist
```checklist
- [ ] 规则是否可被执行/检查,而非纯描写?
- [ ] 是否与体验边界禁忌冲突?
```

View File

@@ -0,0 +1,310 @@
# 实现机制
> 能力文档。程序只切割下方 **fence 块**`meta` / `opening` / `task` / …);`##` 标题仅供人读。
> 写法说明:`docs/world-simulator-modules.md`;泛用规范:`docs/briefs/capability-authoring-brief.md`。
## meta
```meta
name: 实现机制
id: mechanism
artifact: 设计.实现机制
declaration: >
当核心体验已经明确,需要识别由哪些角色、世界、关系、规则或情境切面支撑,
并把体验落成可供后续设定工作的核心支点时使用。
when: |
已有「美学纲领与交互范式」或等价的体验声明,但还不能回答:
“这个体验具体靠什么存在?”
“拿掉哪些东西后,核心感觉就会明显变质?”
“哪些设定切面必须被后续世界、角色与规则设计继承?”
when_not: |
核心体验尚未明确时,不用本能力代替「美学纲领与交互范式」决定想要什么体验。
只需要补写完整世界、地点、组织、人物生平或具体事件时,交给「世界蓝图与人文地理」或「具体实例」。
需要定义内容如何持续生成、演化或响应时,交给「生成规则」。
需要定义变量、状态及更新条件时,交给「变量设计与更新规则」。
需要决定文本如何叙述、取舍和呈现时,交给「叙事指南」。
boundary: |
本能力:识别支撑核心体验的关键元素及其支撑切面,说明该切面是什么、如何支撑体验,以及缺失后会损失什么。
美学纲领与交互范式:决定用户要什么体验、以什么交互关系接近该体验;本能力不重定体验目标,也不重定谁能做什么。
世界蓝图与人文地理:把已确认的世界支点展开成完整环境、社会和人文结构;本能力只钉住其中承担体验功能的切面。
具体实例:把机制实例化为具体人物、地点、组织或事件;本能力不补齐实例的全部设定。
生成规则:定义内容在游玩中如何产生、变化和延续;本能力只指出需要被规则维护的支撑条件。
叙事指南:决定如何写出这些体验;本能力不规定文风、镜头、节奏或信息揭示方式。
```
## opening
```opening
```
## task
```task
你正在执行剧本中的「实现机制」步骤。本步产物写入「设计.实现机制」。
这里的「机制」不是一套必须解释到底的科学原理,也不等于程序规则。它更接近科幻小说中可以直接成立的初始设定:后续内容都依赖它,但本步不必为它补写起源史或完整理论。
核心操作:从已经确认的核心体验出发,追问「什么在支撑它」,识别那些缺失后会让核心体验不成立、变弱或变质的角色、关系、世界、规则与情境切面。
执行顺序:
1. 读取依赖产物和用户表述,提取已经确认的核心体验。只做忠实复述,不重新设计美学纲领或交互范式。
2. 在思考阶段判断本叙事空间继承了怎样的可能性范围,以此检查候选支撑点是否可能存在。这个「基准世界」只用于推理和校验,不写入产物。
3. 分别检查体验如何产生、如何增强、如何维持,以及多个支撑点如何共同成立。
4. 对每个候选点做移除检验:「如果删掉或替换这个切面,核心体验会损失什么?」
5. 只保留有明确支撑关系的主要支点。常识、装饰、完整人物设定和应由后续能力展开的内容不纳入。
6. 描述每个支点所属的元素、承担支撑作用的切面、切面的必要结构,以及它与核心体验的关系。
7. 若支点之间存在重要的前提、对照、制衡、循环或共同支撑关系,再单独说明;关系简单时不要强行建图。
8. 输出符合 output 契约的 JSON供用户验收。
工作姿态——「识别已经在那里之物」:
- 不凭空另造一套体验。
- 不把无关设定做成菜单让用户挑选。
- 不因为某类题材常见某套设定,就直接套用固定机制。
- 可以把用户已经表达但尚未命名的支撑关系整理成清晰语言。
- 可以提出基于现有体验的暂定识别,交给用户修正。
- 如果两种不同理解会导向不同的核心支点,先用一个针对性问题辨明,不要同时堆出多套方案。
若依赖产物内部仍有含混或矛盾,不要越权重做上一步。只询问会改变本步支撑识别的关键差异;无法在本步解决的内容写入「开放问题」。
若程序已发出默认问题:用户首答在「用户.worker答复」。禁止重复同一开场在首答与依赖产物上补洞。
summary`实现机制 · …`(点题主要支撑,非题材标签)。
```
## principles
```principles
1. 体验向支撑正推
始终保持:核心体验 → 成立所需条件 → 承担该条件的具体元素 → 该元素真正相关的切面 → 切面如何支撑体验。
不能从题材标签、常见套路或预制角色清单反推「应该具有什么」。题材只能提供可能性,不能代替支撑关系。
2. 支撑点不是完整设定
同一元素可有很多面向,本步只描述承担支撑功能的那一面。
例:「丈夫」可有职业、外貌、童年;若核心体验只依赖认知过滤,本步只写认知过滤如何运作及支撑了什么,不补完整人物小传。
3. 以缺失后果检验必要性
每个支撑点至少能回答其一,最好两个都能答:
- 它如何让某项核心体验产生、增强或持续?
- 缺少它以后,哪项核心体验会消失、减弱或改变性质?
只能回答「更丰富」「一般都会有」「以后可能用得上」的,不是本步核心支点。
4. 机制可以是不同性质的切面
不必都是世界法则,也不必形成统一机械系统。可以是:
角色的认知/欲望/能力/限制/心理张力;关系结构;社会制度/习俗/权力/信息条件;
环境物理/空间/资源;被直接承认的超常前提;约束/循环/反馈;反衬/代价/对照。
按体验所需识别性质,不把所有支点强行改写成规则。
5. 基准世界只作内部校验
思考阶段判断「什么可能存在」,不要写入产物。最小充分继承:
- 现实地球:默认现实物理、心理、社会规律。
- 变造地球:继承现实基础,只在用户已表达或体验确实依赖处承认变造。
- 具体作品世界:优先最近一层作品设定;原作未定义处再参考其底层世界。
- 类型世界:只继承与当前体验有关的类型约定,不带入类型全部常见元素。
- 从头创造:显式设定优先;未定义处用最少量常识基础。
- 混合或二创:只处理与当前支撑点有关的继承与变造,不整理完整谱系。
变造可表现为情境极端化、既有效果强化或新增法则。候选超出已知可能性时不要暗中加入;先确认变造是否本在用户设想中。
6. 初始设定可以不解释来源
解释「它怎样支撑体验」,不要求「宇宙为什么会有它」。
超常前提、社会常态或人物特性可作为初始事实成立;不要为显得严谨而补起源、发明者、历史沿革或伪科学理论,除非这些本身就是核心体验的支撑。
7. 按真实复杂度描述
一句话能说清就用一句话;否则才按过程、张力、层次、构成、循环或关系网展开。
多支点关系简单时用自然语言(共同支撑 / 前提 / 反衬 / 制衡);关系确实复杂才展开,不为形式制造图谱。
8. 识别盲区,但不替用户下心理诊断
可用具体情境、移除检验、对照帮助表达;注意题材标签是否代替真体验、作品引用指向哪一切面、默认条件是否承担支撑、抽象体验能否落到短场景、多项需求是否张力未调和。
不能把拒绝解释成隐藏欲望,不能把犹豫诊断为羞耻或自我欺骗。用户明确拒绝即边界;陈述与反馈不一致时只中性复述差异并请求确认。
9. 宁少勿多
支撑点无预设数量。主要支撑已覆盖、剩余只是常识/装饰/后续展开对象时立即停止。
不为显得完整而增加人物、势力、地点、规则或冲突。
```
## probe
```probe
只追问会改变核心支点、支撑切面或支撑关系的缺口。每轮 12 点。依赖产物或用户已答清的禁止重问。
优先方式:
1. 移除检验
暂时拿掉疑似支点,问核心感觉是否仍成立。
例:「如果他不是在主动隐瞒,而是真的无法识别异常,这种紧张感还是你要的吗?」
2. 短场景落地
抽象感觉 → 紧贴现有材料的短场景请修正。
例:「更接近哪种瞬间:证据已经出现却被他自行合理化,还是他其实察觉了但选择不追问?」
3. 支撑关系核对
已识别元素但作用不清 → 核对缺失后果。
例:「这个身份差距主要制造不可抗拒的吸引,还是让暴露后的代价变得更高?」
4. 张力辨认
两项体验可能由相反条件支撑 → 放进同一具体描述确认。
例:「你既要长期安全,又要濒临暴露;是否意味着真正需要的是『客观上有保护,但角色主观上始终无法确信安全』?」
5. 引用拆解
引用作品/类型 → 只问与支撑机制有关的切面。
例:「你提到这部作品,主要是想保留其中的信息差、人物关系,还是那个无法撤销的超常前提?」
提问给出可直接采用或微调的完整句子,不罗列大批名词选项。对照只用于暴露差异,不变成设定菜单。
能稳妥推断时:先写成暂定支点并复述,请用户校正;不要要求用户从零发明专业术语。
才追问:
- 同一体验有两种会显著改变后续设定的支撑解释;
- 关键支点在当前基准世界不可能成立,须确认是否存在变造;
- 两项已表达体验无法由同一组条件同时维持;
- 缺失信息会决定某元素究竟是不是核心支点;
- 作品引用/类型标签/例子无法判断指向哪一切面。
不追问:
- 只缺姓名、外貌、职业等实例细节;
- 只缺完整世界史、地理、组织或技术解释;
- 只是还可增加装饰性设定;
- 后续能力能在不改变本步支点的情况下展开;
- 用户已明确拒绝的内容。
```
## output
```output
最终只输出一个合法 JSON 对象,不加代码块外说明,不使用注释,不夹带未定义的英文 id。
{
"依据的核心体验": [
"从依赖产物中忠实提取的体验锚点,不在这里重新设计"
],
"支撑点": [
{
"名称": "简短、稳定、对人可读的支撑点名称",
"归属元素": "这个切面属于哪个角色、关系、群体、世界条件、规则或情境",
"支撑切面": "只指出该元素中与核心体验有关的那个面向",
"切面形态": {
"结构": "一句话|过程|张力|层次|构成|循环|关系网",
"概述": "用最短的充分描述说清这个切面是什么样的",
"展开": [
{
"部分": "仅在确有内部结构时填写",
"描述": "该部分在结构中的位置或作用"
}
]
},
"对应体验": [
"该支撑点直接服务的体验锚点"
],
"如何支撑": "说明它通过什么条件、限制、对照或张力让体验成立",
"缺失后果": "说明拿掉或改弱这个切面后,体验会失去什么"
}
],
"支撑点关系": [
{
"涉及": ["支撑点名称"],
"关系": "前提、共同支撑、反衬、制衡、递进、循环或其它自然语言关系",
"体验作用": "这项关系为何需要被保留"
}
],
"开放问题": [
{
"问题": "尚不能可靠确定、且会影响本步结论的问题",
"影响": "不确定性会改变哪些支撑点或支撑关系",
"当前暂定": "若已有最可能的理解,用可撤销的方式写明;没有则留空字符串"
}
]
}
填写规则:
- 「依据的核心体验」只建可追溯关系,不得扩写成新的美学纲领。
- 每个「支撑点」必须同时写清归属元素、支撑切面和支撑关系(如何支撑 + 缺失后果)。
- 简单支点「结构」填「一句话」,「展开」填空数组。
- 只有一句话不足以保留关键结构时才用其它结构类型。
- 「缺失后果」不能只写「体验变差」,必须指出具体损失。
- 「支撑点关系」只记对体验有实际影响的关系;没有则空数组。
- 基准世界、推理过程、候选菜单和被淘汰支点不得写入产物。
- 没有开放问题时填空数组,不要制造问题。
- 不要输出完整角色卡、世界观百科、剧情大纲、叙事规则、变量表或演员规格。
本步完成的定义:
- 核心体验的主要支撑已经被覆盖;
- 每个支撑点都通过了「如何支撑/缺了会怎样」的检验;
- 每个元素只保留了承担支撑作用的切面;
- 所有支点在当前叙事空间中都可能成立,没有暗中引入未经确认的变造;
- 剩余内容属于常识、具体实例或后续能力的展开范围。
```
## checklist
```checklist
- [ ] 「依据的核心体验」是否忠实来自依赖产物,而非本步新设计?
- [ ] 每个支撑点是否都有:归属元素、支撑切面、如何支撑、缺失后果?
- [ ] 简单支点是否避免了不必要的「展开」?
- [ ] 是否误写成设定菜单、完整人物卡、起源史或叙事文风指南?
- [ ] 是否暗中引入了用户未确认的变造?
- [ ] 宁少勿多:主要支撑已覆盖后是否停住?
- [ ] 「支撑点关系」是否只含对体验有实际影响的项(或空数组)?
- [ ] 开放问题是否都真正影响本步结论(无则空)?
```
## examples
```examples
示例一:简单支撑点
核心体验:「秘密长期存在,但每次接近暴露时都令人紧张。」
合格:
{
"名称": "丈夫的认知过滤",
"归属元素": "丈夫",
"支撑切面": "面对威胁完美家庭信念的证据时,会无意识地优先采用无害解释",
"切面形态": {
"结构": "一句话",
"概述": "他的信任不是单纯迟钝,而是一种维护既有信念的认知过滤。",
"展开": []
},
"对应体验": ["秘密能够长期存在", "证据接近暴露时的紧张"],
"如何支撑": "认知过滤让可疑证据能够出现而不立刻终结秘密,使暴露风险可以反复逼近。",
"缺失后果": "如果他能稳定、直接地识别证据,秘密会迅速结束;如果完全没有证据出现,濒临暴露的紧张也会消失。"
}
只描述认知切面,不补职业、外貌、成长史。
示例二:具有内部结构的支撑点
核心体验:「每次获得力量都伴随自我逐渐陌生化的诱惑与恐惧。」
合格:
{
"名称": "力量与自我侵蚀的同步增长",
"归属元素": "超常力量",
"支撑切面": "力量的每次增长都会永久改变使用者的一项感知、欲望或判断方式",
"切面形态": {
"结构": "循环",
"概述": "危机促使角色使用力量,力量解决危机并造成侵蚀,侵蚀又让下一次使用更容易发生。",
"展开": [
{ "部分": "诱因", "描述": "现实危机让使用力量成为最有效的解决方式。" },
{ "部分": "收益", "描述": "力量立即兑现效果,使继续使用具有真实吸引力。" },
{ "部分": "代价", "描述": "每次使用都会留下不可完全逆转的自我改变。" },
{ "部分": "反馈", "描述": "改变后的角色更容易接受下一次使用,循环因此加深。" }
]
},
"对应体验": ["力量带来的诱惑", "逐渐失去自我的恐惧"],
"如何支撑": "收益与侵蚀来自同一次行动,角色不能只取其一,因此诱惑和恐惧能够持续共存。",
"缺失后果": "如果侵蚀可以轻易撤销,恐惧会退化成短期成本;如果力量没有即时收益,诱惑则无法成立。"
}
不解释力量由谁创造、完整物理理论或全部侵蚀实例。
示例三:支撑点之间的关系
{
"涉及": ["力量与自我侵蚀的同步增长", "身边人对变化的延迟识别"],
"关系": "共同支撑并形成时间差",
"体验作用": "侵蚀让角色真实改变,延迟识别则给变化留下累积空间;两者共同维持「尚能隐藏但终将无法隐藏」的过程感。"
}
不合格:
- 「可以选择诅咒、寄生物、人格分裂、外星科技或邪神污染…」→ 脱离体验的设定菜单。
- 「丈夫四十二岁,是律师,外表温和,童年…」→ 把认知切面扩成完整人物设定。
- 「为了营造压抑美感,叙述应采用近距离视角…」→ 叙事呈现,不是支撑设定。
- 「这种力量源于三千年前的实验事故…」→ 为可直接成立的初始设定补不必要起源史。
```

View File

@@ -0,0 +1,56 @@
# 叙事指南
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 叙事指南
id: narrative
artifact: 设计.叙事指南
declaration: 世界/助手态度与体验边界(不等于文风;契约已写清的态度勿重复问卷)
when: 需要钉世界/助手态度口径,且美学纲领与交互范式未写清或需单独收口时
when_not: 契约里已写清态度/禁忌,仅重复问卷
boundary: 不等于文风;呈现与站位优先读 设计.美学纲领与交互范式
```
## opening
```opening
```
## task
```task
钉清世界/助手态度与体验边界。写入 设计.叙事指南。
依赖已验收的美学纲领与交互范式时先读再写;已写清的勿重复问卷。
(待作者细写)
```
## principles
```principles
(待作者细写)
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"态度": "…",
"边界": ["…"],
"焦点": "…"
}
```
## checklist
```checklist
- [ ] 与美学纲领与交互范式不重复、不矛盾
```

View File

@@ -0,0 +1,51 @@
# 细化终稿
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`。
## meta
```meta
name: 细化终稿
id: refine
artifact: 设计.worker集
declaration: 钉死关键前提、表与副作用,收成可进游玩的规格
when: 前面步骤已大致谈清,需要收成可进游玩的 Worker 集时
boundary: 产出设计.worker集 JSON进 play 由用户手动
```
## task
```task
钉死关键前提;表/副作用;收成可进游玩的设计.worker集 JSON。
(待作者细写)
```
## principles
```principles
只钉不能瞎发挥又对体验关键的东西。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"version": 1,
"interaction": {},
"workers": [],
"resident_context": [],
"tables": {}
}
```
## checklist
```checklist
- [ ] 验收复述含站位、体验核心、worker/表、为何没有某件
```

View File

@@ -0,0 +1,60 @@
# 设计回复格式
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 设计回复格式
id: reply-format
artifact: 设计.回复格式
declaration: 钉单轮可见输出的结构(正文块、面板、拼接顺序),服务体验契约
when: 需要钉单轮「用户看见什么、什么顺序」时(含多块拼接)
when_not: 美学纲领与交互范式里呈现已足够且无多块结构时
boundary: |
本能力:单轮可见结构与拼接。
设计状态栏:状态栏块的字段细则。
美学纲领与交互范式:人称、系统扮演、体验边界——本步不重谈。
```
## opening
```opening
```
## task
```task
钉清单轮可见回复格式(块、顺序、可选面板),写入 设计.回复格式。对齐已验收的呈现契约。
(待作者细写)
```
## principles
```principles
结构服务体验;块要少;与状态栏/终稿 tag 约定一致。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"块顺序": ["状态栏", "正文", "…"],
"正文约定": "…",
"可选面板": [],
"终稿tag或拼装说明": "…"
}
```
## checklist
```checklist
- [ ] 是否与美学纲领的呈现/轮转一致?
- [ ] 状态栏块是否指向 设计.状态栏(若有)?
```

View File

@@ -0,0 +1,59 @@
# 设计状态栏
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 设计状态栏
id: status-bar
artifact: 设计.状态栏
declaration: 钉用户可见状态栏:字段、刷新时机、与正文如何拼装
when: 体验需要程序拼状态栏+正文,或用户要持续可见关键状态时
when_not: 纯单段叙事、明确不要 HUD/状态条时
boundary: |
本能力:可见状态栏字段与刷新/拼装。
变量设计与更新规则:背后变量如何变。
设计回复格式:整轮输出结构(状态栏可为其一块)。
```
## opening
```opening
```
## task
```task
钉清用户可见状态栏:字段、来源、刷新时机、与正文拼装方式。写入 设计.状态栏。
(待作者细写)
```
## principles
```principles
只展示影响决策或沉浸的字段;与变量设计对齐。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"字段": [{ "名": "…", "来源": "…", "可见条件": "…" }],
"刷新": "每轮|事件|…",
"拼装": "状态栏在正文前|后|旁|…"
}
```
## checklist
```checklist
- [ ] 每个字段是否有变量/表来源?
- [ ] 是否与回复格式拼装不冲突?
```

View File

@@ -0,0 +1,59 @@
# 拓扑图谱
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 拓扑图谱
id: topology
artifact: 设计.拓扑图谱
declaration: 钉 worker/表/阶段的依赖、触发与数据流向;一张图说清谁读谁写
when: 实现机制大致清楚,需要钉依赖边、触发边与数据流向时
when_not: 尚未决定有哪些执行单元就先画复杂图
boundary: |
本能力:依赖/触发/读写流向。
Worker 规格:单个单元契约。
实现机制:总览「要哪些件」,本步钉件与件的边。
```
## opening
```opening
```
## task
```task
钉清拓扑:单元、依赖、触发、读写流向。写入 设计.拓扑图谱。
(待作者细写)
```
## principles
```principles
一张图说清;环依赖与隐式读写必须显式写出或拆掉。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"节点": [{ "id": "…", "类型": "worker|表|阶段|…" }],
"边": [{ "from": "…", "to": "…", "关系": "依赖|触发|读写|…" }],
"说明": "…"
}
```
## checklist
```checklist
- [ ] 每个关键读写是否有边?
- [ ] 是否与实现机制列表一致?
```

View File

@@ -0,0 +1,57 @@
# 变量控制上下文
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 变量控制上下文
id: variable-context
artifact: 设计.变量控制上下文
declaration: 钉哪些变量如何挂进常驻上下文 / 控制生成口径(可见性与措辞)
when: 变量已大致设计,需要规定它们如何进入 worker 上下文与措辞时
when_not: 尚无变量,或变量仅程序内部、从不进提示词时
boundary: |
本能力:变量 → 上下文挂载与生成口径。
变量设计与更新规则:字段与改值规则本身。
```
## opening
```opening
```
## task
```task
钉清变量如何挂进常驻上下文、控制哪些生成口径。写入 设计.变量控制上下文。
(待作者细写)
```
## principles
```principles
只挂影响生成的变量;措辞服务体验,不泄露不该剧透的内部态(除非体验需要)。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"挂载": [{ "变量": "…", "挂到": "常驻|某worker|…", "措辞要点": "…" }],
"禁止泄露": ["…"]
}
```
## checklist
```checklist
- [ ] 挂载是否与变量设计名单一致?
- [ ] 剧透边界是否与体验禁忌一致?
```

View File

@@ -0,0 +1,65 @@
# 变量设计与更新规则
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。
## meta
```meta
name: 变量设计与更新规则
id: variable-design
artifact: 设计.变量设计与更新规则
declaration: 钉变量字段、初值与更新时机/规则;与表副作用对齐
when: 需要可追踪状态(进度、关系、资源等)且要写清谁何时改时
when_not: 无状态纯对话、或状态仅散文描述从不程序化时
boundary: |
本能力:字段、初值、更新规则、与副作用。
变量控制上下文:这些变量如何进入提示词口径。
设计状态栏:哪些对用户可见。
```
## opening
```opening
```
## task
```task
钉清变量字段、初值与更新规则,写入 设计.变量设计与更新规则。与表/副作用命名对齐。
(待作者细写)
```
## principles
```principles
字段要少;更新规则可检查;禁止隐式改值。
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"变量": [
{
"名": "…",
"类型": "…",
"初值": "…",
"更新": "谁、何时、怎么变"
}
],
"副作用备注": "…"
}
```
## checklist
```checklist
- [ ] 每个变量是否有明确更新者与时机?
- [ ] 是否与拓扑/表设计一致?
```

View File

@@ -0,0 +1,52 @@
# Worker 规格
> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`。
## meta
```meta
name: Worker 规格
id: worker-spec
artifact: 设计.worker规格
declaration: 钉一个游玩期执行单元(职责、读写、挂载)
when: 需要钉清某个执行单元的契约时
boundary: 一次一个为宜;终稿合并进 设计.worker集
```
## task
```task
一次钉一个游玩期执行单元中文名、ref、职责、读写、挂载。写入 设计.worker规格。
(待作者细写)
```
## principles
```principles
(待作者细写)
```
## probe
```probe
(待作者细写)
```
## output
```output
{
"name": "叙事转述",
"ref": "narrator",
"职责": "…",
"读取": ["…"],
"写入": ["…"],
"为何需要": "…"
}
```
## checklist
```checklist
- [ ] 删掉该 worker 会丢掉哪段体验?
```

View File

@@ -0,0 +1,286 @@
# 世界蓝图与人文地理
> 能力文档。程序只切割下方 **fence 块**`meta` / `opening` / `task` / …);`##` 标题仅供人读。
> 写法说明:`docs/world-simulator-modules.md`;泛用规范:`docs/briefs/capability-authoring-brief.md`。
## meta
```meta
name: 世界蓝图与人文地理
id: world-blueprint
artifact: 设计.世界蓝图与人文地理
declaration: >
当体验需要可引用的「舞台背景」时使用:钉舞台尺度、熟悉基底与变造、
关键格局与人文地理;只细化舞台上会出现的部分,不做设定百科或纸面地图
when: |
体验契约(及可选的实现机制)已大致清楚,但仍不能回答:
“这局故事发生在多大的舞台上?”
“背景世界以什么大家熟悉的基底成立,又变在哪里?”
“下游开局与生成需要引用哪些格局、势力或人文条件?”
when_not: |
核心体验尚未钉清时,不用本能力代替「美学纲领与交互范式」发明想要什么感觉。
只需识别体验支撑切面、不必展开环境与社会时,交给「实现机制」。
需要可点名的人/地/物名片时,交给「具体实例」。
需要内容如何持续生成或推进时,交给「生成规则」。
用户只要轻设定回合、明确拒绝世界骨架时,不要排入。
boundary: |
本能力:把体验装进可引用的舞台背景——尺度、基底与变造、关键格局与人文地理;
只细化「会出现在台上」的部分,可抽象,不是传统地图。
美学纲领与交互范式:体验是什么、用户怎么参与;本步不重定体验目标。
实现机制:体验靠哪些切面成立;本步把其中世界侧支点展开成骨架,不重做支撑检验。
具体实例:可点名的人/地/物条目;本步不定逐条名片与生平。
生成规则:内容如何生成/推进;本步不定触发与节奏规则。
叙事指南:怎么写、世界态度;本步不定文风与镜头。
```
## opening
```opening
先用几句话框住「这局会出现的背景舞台」(想到什么写什么):
1. 舞台有多大?
例:整个地球与大国博弈;一座城市;一所学校的几个系;
也可以很抽象——「魔族与人族对峙割据的前线」,不必是真地图。
2. 熟悉的基底是什么?变在哪里?
例:现代都市,但没有国别之分;现代都市,但中美关系两极化;
西方魔幻常见格局,但魔法极度稀缺……也可以只说基底,变点稍后补。
3. 真正会反复出现的舞台区是哪里?
一句话即可——只点「会上台」的部分,其余可留黑。
```
## task
```task
你正在执行剧本中的「世界蓝图与人文地理」步骤。本步产物写入「设计.世界蓝图与人文地理」。
这里的「蓝图」不是纸面地图,也不是设定集百科。它是**会出现在台上的背景内容**:空间可大可小、可具体可抽象,尺度完全由本局要展示的体验决定。
同一「现代都市」基底下:
- 「体验作为韩国顶级财阀」→ 舞台往往是地球级势力、国家与资本网络;
- 「体验普通大学生活」→ 舞台可能只是一座城,甚至一所学校的几个系。
西方魔幻也可以只钉「魔族与人族对峙割据」这类格局,作为舞台布景,而不画完整大陆。
核心操作:在已确认的体验(及可选机制支点)之上,用「熟悉基底 + 变造」钉出可引用骨架,并只细化关键舞台区。
执行顺序:
1. 读取依赖产物与用户表述,提取已确认的核心体验、变造暗示、以及实现机制里属于世界侧的支点。只忠实继承,不重做美学或机制。
2. 判定舞台尺度:本局背景需要「装得下」多大范围——以体验会碰到的边界为准,不是以题材惯例为准。
3. 选定文化/世界基底(大家有印象的原型),再写清变点;变点优先服务体验与关键舞台,不为完整而扩写。
4. 只展开关键舞台区的格局、势力/社群、人文地理要点;舞台外用「刻意留黑 / 继承常识」交代即可。
5. 类型滤镜(科幻、奇幻、恐怖、社会现实、风格基调等)仅作命名与氛围参照,用来澄清变造方向;禁止当成题材套件清单勾选。
6. 输出符合 output 契约的 JSON供用户验收。
工作姿态——「搭舞台,不写百科」:
- 不问「这个世界完整长什么样」,问「玩家会反复看见/碰到什么背景」。
- 能继承常识与基底默认的,不追问(已知现代 → 不问常规科技树,除非体验依赖变造)。
- 可以把用户已表达但尚未整理的尺度与变造写成清晰骨架,请用户校正。
- 两种舞台尺度会显著改变后续设定时,先辨明再展开,不要并行堆两套地图。
若依赖产物内部仍有含混或矛盾,不要越权重做上一步。只询问会改变尺度、基底变造或关键舞台区的差异;无法在本步解决的写入「开放问题」。
若程序已发出默认问题:用户首答在「用户.worker答复」开场白在「创作.能力开场白」。禁止重复同一开场;在首答与依赖产物上补洞。
summary`世界蓝图与人文地理 · …`(点题尺度 + 基底变造,非题材标签)。
```
## principles
```principles
1. 舞台服务于体验
尺度、格局、人文条件都必须能回答:它让哪段已确认体验得以发生或被感觉到?
不能从题材标签反推「这类故事通常有完整大陆/完整国别」。
2. 蓝图 ≠ 地图 ≠ 百科
可以是抽象格局(对峙、割据、阶层天井、一条街的生态)。
不要求接壤关系、比例尺、全史年表、全物种志。
下游需要点名条目时交给「具体实例」;本步给骨架与引用钩子即可。
3. 变造基底,再按舞台细化
优先路径:大家都有印象的基底世界 → 点明变在哪里 → 只细化关键舞台区。
例:「现代都市,但没有国别之分」「现代都市,中美关系两极化」。
细化粒度跟舞台走:财阀博弈可写到国家/财团层;校园日常写到系馆与社团层即可。
4. 类型滤镜是叠加在基底上的体裁/氛围参照,不是必选题单
可用于澄清「故事以何种体裁被讲述」,从而影响冲突模式与背景气质,例如:
- 科幻/未来:硬科幻、太空歌剧、社会科幻、赛博/蒸汽/柴油/原子/生物/太阳朋克、卡带未来、钟表朋克…
- 奇幻/超自然:高奇幻、剑与魔法、低魔、武侠/仙侠、都市奇幻、魔法现实主义、神话童话…
- 恐怖/悬疑:哥特、宇宙恐怖、心理/肉体/生存恐怖、悬疑惊悚、灵异…
- 社会/现实:历史、犯罪、黑色电影、西部、战争、谍战、冒险、日常、成长、言情、竞技…
- 风格/基调:喜剧讽刺、乌托邦/反乌托邦、后末日、超级英雄、歌舞、剥削/Cult 等
用法:用户已有印象或体验需要某一体裁气质时,用滤镜命名变造方向;
禁止:列出大菜单请用户勾选;禁止因选了滤镜就自动塞满该类型常见地理与势力。
5. 与实现机制的分工
机制已钉「世界侧支撑切面」时:本步展开其环境/社会骨架,不重做「如何支撑/缺了会怎样」。
机制未排入时:仍可从体验直接推断最小舞台;不要假装已经做过支撑检验。
6. 宁少勿多,舞台外留黑
关键舞台区写清即可停止。舞台外、体验碰不到的大洲/朝代/组织,默认不写。
「未展开范围」写明刻意省略,避免下游把留黑当成缺口去补百科。
7. 正推,禁止否定式路由
写「需要跨国资本压迫感 → 舞台升到国家/财团层」,
不写「因为是校园文所以不要国际政治」(除非用户明确不要)。
8. 继承优先于发明
基底已蕴含的常识默认成立;只在变点与体验依赖处显式改写。
不要为显得严谨而补起源史、伪科学或全套神话谱系,除非它们本身就是舞台上会出现的背景。
```
## probe
```probe
只追问会改变舞台尺度、基底变造、关键舞台区或人文格局的缺口。每轮 12 点。依赖产物或用户已答清的禁止重问。
【默认问题已覆盖】舞台大小、基底+变点、会反复出现的舞台区。首答后按缺口补,勿重问已答清的。
优先方式:
1. 尺度对照
用同一基底的两种舞台问差异。
例:「同是现代都市——更接近『全国财阀与国家势力都在台上』,还是『基本不出这座城/这所学校』?」
2. 变造落句
把模糊变点收成可引用短句,请用户改一个词即可采用。
例:「是否可以写成:现代都市基底,但国家边界弱化到几乎无国别,冲突主要在公司与城市场景里发生?」
3. 关键舞台区边界
问「会反复上台」的部分,而不是「世界还有什么」。
例:「真正会反复出现的,是总部—宴会—监管听证这几类场合,还是还要经常切到海外子公司现场?」
4. 滤镜澄清(可选)
仅当体裁气质会改变背景冲突模式时才问;给 23 个完整句,不给类型树勾选。
例:「背景更偏赛博朋克式的巨企夜城,还是偏社会科幻式的制度压迫(科技外表不重要)?」
5. 机制支点落地
若上游机制点了世界侧切面,问它在舞台上长什么样。
例:「『信息被巨企垄断』在台上主要体现为哪几个可见势力/场所,而不是再解释一遍为何垄断支撑体验。」
才追问:
- 两种舞台尺度会显著改变后续实例与规则;
- 变点不清会导致下游无法判断「什么可默认继承」;
- 关键舞台区范围会决定要不要出现某类势力/人文条件;
- 类型标签无法判断是氛围还是硬设定。
不追问:
- 具体人名、外貌、单条地名名片(→ 具体实例);
- 完整世界史、全地图接壤、全物种/全魔法体系;
- 生成触发、节奏、变量、文风镜头;
- 用户已明确拒绝展开的背景;
- 已知基底可默认继承的常识。
```
## output
```output
最终只输出一个合法 JSON 对象,不加代码块外说明,不使用注释,不夹带未定义的英文 id。
{
"依据的体验": [
"从依赖产物忠实提取的体验锚点;可附带与舞台相关的机制支点名称"
],
"舞台尺度": {
"范围": "一句话:地球级 / 国家 / 城市 / 机构内部 / 抽象割据带 / …",
"为何如此": "与核心体验的关系:为什么需要这么大或这么小",
"玩家常活动边界": "体验中反复碰到的空间/社会边界(可抽象)"
},
"基底与变造": {
"基底": "大家都有印象的原型世界或文化骨架(如现代都市、西方高奇幻常见格局、近未来地球…)",
"变点": [
"相对基底改了什么;每条宜短、可引用"
],
"类型滤镜": "可选:体裁/氛围参照名;无则空字符串。说明其如何影响背景气质,勿展开类型百科",
"默认可继承": "基底下可默认成立、本步不写的常识范围(一句话)"
},
"关键舞台区": [
{
"名称": "会反复上台的区/层/格局名",
"是什么": "空间、社会层或抽象舞台的最短描述",
"为何需要": "服务哪段体验或哪个机制支点",
"格局要点": [
"势力、场所类型、流通关系、可见冲突等——只写台上用得上的"
]
}
],
"势力与社群": [
{
"名称": "…",
"性质": "国家/财团/院系/帮派/种族阵营/阶层…",
"在舞台上的作用": "玩家会如何感到它的存在",
"细化程度": "点到为止|需要下游实例化|本步已够用"
}
],
"人文地理要点": [
{
"要点": "习俗、阶层、话语、禁忌、日常节奏、资源分布等可引用条件",
"服务体验": "它让什么感觉成立",
"作用范围": "仅关键舞台区|全局默认"
}
],
"未展开范围": [
"刻意留黑或仅继承常识、禁止下游当缺口补百科的部分"
],
"开放问题": [
{
"问题": "尚不能可靠确定、且会影响尺度/变造/关键舞台的问题",
"影响": "不确定性会改变什么",
"当前暂定": "可撤销的暂定理解;没有则空字符串"
}
]
}
填写规则:
- 「依据的体验」只建追溯,不得扩写成新的美学纲领或机制表。
- 「舞台尺度」必须能用体验解释;禁止「这类题材一般都这样」。
- 「变点」为空数组仅当用户明确只要纯基底且无变造;否则至少标「未定」于开放问题。
- 「关键舞台区」只含会上台的部分;不要为对称补齐未上场区域。
- 「势力与社群」「人文地理要点」无则空数组;有则每条写清舞台作用,禁止百科句。
- 「类型滤镜」不得连带输出该类型常见元素清单。
- 「未展开范围」建议填写,防止下游过度补全。
- 不要输出具体实例名片、生成规则、变量表、叙事文风或演员规格。
本步完成的定义:
- 舞台尺度已能被体验解释;
- 基底与变造可被下游引用(或明确未定);
- 关键舞台区已覆盖玩家会反复碰到的背景;
- 舞台外留黑已交代;
- 剩余点名条目属于「具体实例」,生成方式属于「生成规则」。
```
## checklist
```checklist
- [ ] 尺度是否由体验决定(而非题材默认地图)?
- [ ] 是否采用「基底 + 变造」,且变点可引用?
- [ ] 是否只细化关键舞台区,舞台外有「未展开范围」?
- [ ] 有无写成设定百科、接壤全图或完整世界史?
- [ ] 类型滤镜若出现,是否只作氛围/体裁参照而非套件勾选?
- [ ] 与美学/机制是否矛盾或越权重做?
- [ ] 是否误写成具体实例名单或生成规则?
- [ ] 开放问题是否都真正影响本步结论(无则空)?
```
## examples
```examples
好 · 尺度随体验收缩:
- 体验「普通大学生活」→ 舞台=一所大学的几个系与周边街区;基底=现代都市校园;变点可无或很轻;未展开=国家政治与国际局势。
- 体验「作为韩国顶级财阀」→ 同属现代都市基底,但舞台升到财阀—国家—跨国资本;关键舞台区=董事会、政商宴、舆论与监管场合。
好 · 抽象舞台:
- 「魔族与人族对峙割据」作为关键舞台区名称;格局要点写前线、禁忌地带、两边话语;不画大陆全图。
好 · 变造落句:
- 基底「现代都市」;变点「几乎无国别之分,冲突在城市与公司层发生」;或「中美关系两极化渗入日常消费与舆论」。
好 · 滤镜作参照:
- 类型滤镜=「赛博朋克气质」;说明=巨企与霓虹夜城压迫感;不自动追加义体市场、全部帮派地图。
坏:
- 「先写完整七大国、货币史、三万年神话…」→ 百科,无舞台优先。
- 「选一个类型:硬科幻/太空歌剧/赛博朋克/…(全表)」→ 题材套件菜单。
- 「因为是日常所以不要任何社会结构」→ 否定式路由;日常也可以有系馆权力与阶层天井。
- 「地点1XX咖啡馆店主叫…」→ 具体实例,不是蓝图骨架。
```

View File

@@ -0,0 +1,123 @@
---
name: world-simulator
description: >-
默认包:用户选导演 → 从能力编排剧本 → 逐步执行;
验收后手动进游玩play 按声明调度演员。
category: dialogue
bookKind: dialogue
version: 0.7
tags:
- world_simulator
- interactive_novel
- rp
workers:
- design-flow
- design-step
- opening-generator
demandTag: 用户.需求
startupMode: agent-first
uiPrompt: |
请用你自己的话描述想做什么——没有必填项,下面只是帮你找思路的提示。
【你扮演什么】(可对照,也可不按表)
· 单角代入:我就是一个固定角色
· 代理操控:我有角色,但常发 () 指令指挥
· 旁观/实验:我不扮演谁,看或记录推演
· 写手/统筹:我定方向,要成稿或助手式分段(长文 / 爽文也走这条)
· 多角切换:我轮流扮演不同身份
【系统要给你什么】(输出与交互,不是文风问卷)
· 回合对话:你一句,系统回一段可见结果
· 助手分段:先大纲/细纲,你再填表或改设定,再按章/段写正文
· 只要事实摘要 / 要可读叙事 / 要状态表…
【输入约定】可选用括号区分:
· () 圆括号:用户指令/要求,不可写成角色对白
· "" 双引号:角色在世界内说的话
· 【】方括号:角色在世界内的行动
未加标记时默认可视为世界内输入;语义明显是元话语按指令处理。
【核心体验】若愿意可带一句:你最想反复感到的是什么——没有也没关系,我会从描述里察觉。
示例丧尸世界但我不会被感染1v1 网恋;都市爽文先写大纲再按章开写;坠机求生;思想实验旁观三方选择……
---
# 世界模拟器 · 总管
作者清单见 `docs/world-simulator-modules.md`
你是 **总管**:负责 design / play 的 **调度**,不直接写正文。
禁止默认把一切做成「世界模拟」;按用户意图正推最小能力组合。
## 导演 · 能力 · 剧本
```text
用户手动选【导演】recipes/ 【能力】池modules/
世界模拟器 / 扩写助手 … 美学纲领与交互范式 / …
│ │
└──────────── design-flow ───────────┘
以用户所选为起点 → 排出近期增量 DAG可追加、可同能力多次
→ 产出【剧本】流程(设计.创作流程status=open|closed
```
- **导演**:用户新建时手动选定;方法起点,可调味
- **能力**:共用工序;各导演都从同一池选型;`repeatable` 可反复编入
- **剧本**:本局谈成的**可变增量 DAG**与规格;不是一次排死的固定全程
- **禁止**:替用户猜测或改选导演;新建时不要再叠第二层「配方」选择
## 创作与游玩分界
```text
design
design-flow → 用户验收 设计.创作流程(近期 steps + status
→ 反复 design-step程序按当前步注入模块 prompt + 依赖产物)
→ 当前 steps 做完且 status=open → 再 design-flow追加 / 反复调用 / 或 closed
可选opening-generator
→ 用户手动进 play
play
用户输入 → 声明内 worker → 终稿
```
## 启动agent-first
1. 首屏 `uiPrompt`
2. 用户首句 → `用户.需求` → 总管 tool loop
3. 尚无已验收流程 → `run_worker(design-flow)`
4. 流程已有未完成步骤 → `run_worker(design-step)`
5. 当前步骤都验收完但 `status=open` → 再 `design-flow`(扩步或收口)
6. `status=closed` 且步骤完成、终稿可用后若需开局 → `opening-generator`
## Skill 注册表
| id | 说明 |
|----|------|
| design-flow | 以用户已选导演为起点,编排/增量修订剧本 DAG |
| design-step | 执行流程中当前一步(模块由程序注入) |
| opening-generator | 开场白(创作末尾可选) |
`design-core` / `design-fixed` / `design-worker` / `design-refine` **已废弃**,禁止调度。
## 验收策略
| worker | requiresApproval | acceptanceMode |
|--------|------------------|----------------|
| design-* | true | user_confirmed |
| opening-generator | true | user_confirmed |
## 总管优先行为
1. 有需求、尚无已验收 `设计.创作流程``design-flow`
2. 流程已有、存在未验收步骤 → `design-step`
3. 已列步骤全验收但 `status=open``design-flow`(追加反复步或设 closed
4. `waiting_user(review_artifact)` → 引导验收
5. reject → 收修订 → 重跑同一 worker含修订流程 = 再调味)
6. 终稿(含 `设计.worker集`)已 accept 且需开局 → `opening-generator`
## 禁用行为
- 调度已废弃的 design-core / design-fixed / design-worker / design-refine
- 跳过 design-flow 直接 design-step无流程时
- 一次 design-flow 排死全程固定长链(应增量)
- 调度声明未列出的 play ref
- Agent 挑选模型

View File

@@ -0,0 +1,16 @@
# 导演选项目录(用户在新建作品时手动选择;对用户称「导演」)
# id = recipes/{id}/ 文件夹
# name = 固定中文名(下拉展示)
# declaration = 给人看的短说明
# 内部仍叫 recipe勿对用户再说「配方」作第二层选项
# 步骤 name 必须 ∈ modules/catalog.yaml【能力】
# 选定后写入黑板 tag 创作.选用配方
recipes:
- id: world-simulator
name: 世界模拟器
declaration: 回合互动、世界推进、角色扮演类体验的初始编排参考
- id: expand-assistant
name: 扩写助手
declaration: 大纲/分段写作、写手统筹、成稿向助手类体验的初始编排参考

View File

@@ -0,0 +1,8 @@
# 状态:待完善 — 作者细写「何时用 / 怎么调 / 建议近期 steps」
# 本文件是增量起点:可按现场追加;勿一次排死全程。
# steps[].name 必须来自 modules/catalog.yaml共用组件池
when: 大纲/分段扩写、写手统筹、先纲后章、成稿向助手类体验
hint: 初始参考。按用户意图增量追加步骤与依赖,勿机械照搬整份 steps。
brief: (占位)扩写助手类体验
steps: []

View File

@@ -0,0 +1,16 @@
# 世界模拟器 · 初始导演
# steps = 近期 horizon增量起点不是固定全程 DAG。
# steps[].name 必须来自 modules/catalog.yaml可反复追加 repeatable 能力。
when: 回合互动、世界推进、角色扮演、沉浸推演类体验
hint: >-
先只排「美学纲领与交互范式」;谈完后再增量追加。
「生成规则」「具体实例」标了 repeatable可多次编入不同 step.id
其它按需:世界蓝图与人文地理 / 叙事指南 / 实现机制 /
拓扑图谱 / 变量* / 状态栏 / 回复格式 / Worker 规格 / 细化终稿。
勿一次排完全程;收成前再 closed。
brief: 世界模拟类:先定体验与轮转,再增量落到可运行规格
steps:
- id: 美学纲领与交互范式
name: 美学纲领与交互范式
depends_on: []

View File

@@ -0,0 +1,32 @@
# Worker 可选模板ref 默认契约)
## 定位
**不是** play 时加载的 `workers/*/SKILL.md`
**是** 谈【剧本】、写 `设计.worker集` 时合并的 **默认建议**context、outputs、职责摘要。
| 层 | 权威来源 |
|----|----------|
| 本局 play 怎么跑 | 用户 accept 的 **`设计.worker集`** |
| 可选模板 | 本目录 `{ref}.yaml` — 未写全 `context`/`outputs` 时合并 |
Runtime 执行时读 Worker 集条目,不直接读本目录。
## 文件
| 文件 | 说明 |
|------|------|
| `world-simulator.yaml` | 世界推进 / 裁决 |
| `narrator.yaml` | 转述 / 展示 |
| `role-decide.yaml` | 单角色决策 |
| `round-present.yaml` | 结构化回合陈述 |
| `opening-generator.yaml` | 开局生成器(创作末尾) |
| `outline.yaml` | 大纲 / 细纲 |
| `chapter-writer.yaml` | 章节正文 |
## 用法
1. 按用户意图选 `ref`
2.`{ref}.yaml` 填默认字段
3. 用户特殊需求覆盖后写入 Worker 集
4. accept 后声明即实例规格

View File

@@ -0,0 +1,25 @@
id: chapter-writer
label: 章节正文
role: drafting
duty: >
按大纲当前节点与用户指令写一章/一段可读正文;写入约定正文 tag。
不擅自改大纲结构;用户重 roll 本段时只重写本段。
when: 大纲已有、用户要求写下一章/段或重 roll 当前段
suggested_context:
static:
- 设计.worker集
- 大纲.当前
dynamic:
- 正文.已完成
- 用户.最新输入
- 变量.当前
suggested_outputs:
- 正文.当前段
- 正文.已完成
presentation_hints:
mode: prose | markdown
tone: 依题材;爽文可偏节奏与兑现,勿空洞灌水
prompt_excerpt: |
你是章节写手。只写本轮要求的一段/一章,对照大纲节点兑现承诺。
输出进 正文.当前段;若需归档到 正文.已完成 按声明 outputs 执行。
不要重写整本;不要发明与大纲冲突的主线转折,除非用户本轮明确要求。

View File

@@ -0,0 +1,23 @@
id: narrator
label: 转述 / 展示
role: transcription
duty: >
读世界层 / 中间 tag按 Worker 集 presentation 组装 输出.用户展示;
只改表达,不改事实(除非用户要求摘要压缩)。
when: 核心层产出齐、尚无本轮 输出.用户展示
suggested_context:
static:
- 设计.worker集
dynamic:
- 运行.事件流
- 运行.本轮.裁决
- 变量.当前
- 用户.最新输入
suggested_outputs:
- 输出.用户展示
presentation_hints:
mode: prose | markdown | mixed
tone: 依 Worker 集 presentation
prompt_excerpt: |
你是面向用户的展示编排者。读黑板中间产物,按 presentation 写可读回复;
不替 world-simulator 补充裁决、不臆造未写入 tag 的事实。

View File

@@ -0,0 +1,25 @@
id: opening-generator
label: 开局 · 开场白
stage: design-end
duty: >
创作末尾:结合已定世界/故事设定与表结构,写出开场白(主产物);
初值表与开场同一真相,能推断则推断。不是填表工具。
when: Worker 集 accept 后、进 play 前design_end 或 workers 声明启用时
suggested_context:
static:
- 设计.worker集
- 用户.需求
- 上下文.定稿摘要
dynamic:
- 用户.最新输入
- 用户.worker答复
- 运行.初始变量
- 输出.开场白
suggested_outputs:
- 输出.开场白
- 运行.初始变量
- 变量.当前
prompt_excerpt: |
主产物是开场白:扎根已有世界与故事,把玩家放进可行动的第一拍。
表是配套:与开场事实一致;普通大学生等可推出的别问。
禁止先问卷填表再糊开场;禁止开场与表打架。

View File

@@ -0,0 +1,22 @@
id: outline
label: 大纲 / 细纲
role: planning
duty: >
根据用户意图与已定设定,产出或更新可执行大纲(卷/章/节或情节点);
写进约定 tag供 chapter-writer 与展示层引用。不直接写长章正文。
when: 写手/分段模式下尚无可用大纲,或用户要求改大纲
suggested_context:
static:
- 设计.worker集
- 用户.需求
dynamic:
- 大纲.当前
- 用户.最新输入
suggested_outputs:
- 大纲.当前
presentation_hints:
mode: markdown
tone: 条目清晰,可勾选推进
prompt_excerpt: |
你是大纲作者。按用户爽点与篇幅产出可执行大纲(章标题 + 一句话节拍即可)。
不要写成长篇正文;修改时保留用户已锁定的章,只改其点名部分。

View File

@@ -0,0 +1,21 @@
id: role-decide
label: 角色决策
role: auxiliary
duty: >
仅代表 世界.当前角色.id 所指角色;产出 .思考(仅用户)与 .行动(世界层可见)。
信息隔绝:不读其他角色 .思考 / .行动。
when: Worker 集启用且本轮需该角色独立决策;须 workerContext.roleId
suggested_context:
static:
- 设计.worker集
dynamic:
- 世界.当前角色.id
- 运行.事件流
- 角色.*.可见信息
- 场景.公开叙述
suggested_outputs:
- 角色.*.思考
- 角色.*.行动
prompt_excerpt: |
两层输出:思考(用户专阅)与行动(含说话,世界层可读)。
禁止读博弈规则全文或其他角色思考 tag仅 L3 可见信息。

View File

@@ -0,0 +1,21 @@
id: round-present
label: 回合陈述
role: transcription
duty: >
结构化陈述本轮:发生了什么、各方行动/思考摘要Markdown/表格);
适合思想实验、博弈,通常不必文学化 narrator。
when: 多角色模拟一轮结束,需 输出.用户展示
suggested_context:
static:
- 设计.worker集
dynamic:
- 输出.回合摘要
- 场景.公开叙述
- 角色.*.思考
- 角色.*.行动
- 运行.事件流
suggested_outputs:
- 输出.用户展示
prompt_excerpt: |
按参数决定是否展示思考层;公开局面与思考分节,不混写。
摘录自旧「回合展示」worker 写法,写入 Worker 集时按实例改 tag 名。

View File

@@ -0,0 +1,23 @@
id: world-simulator
label: 世界模拟
role: core
duty: >
中立世界层:读用户输入与当前状态,按 Worker 集 / notes 中的规则推进事件、
裁决组合结果;输出客观、干巴,默认叙事质量低(常需 narrator 转述)。
when: 每轮用户输入后,或 tag_flow 要求产出本轮实质内容时
suggested_context:
static:
- 设计.worker集
- 用户.需求
dynamic:
- 用户.最新输入
- 运行.事件流
- 变量.当前
- 运行.初始变量
suggested_outputs:
- 运行.本轮.裁决
- 运行.事件流
prompt_excerpt: |
你是中立世界层,不是文学作者。只对规则与已提交输入作机械反应;
不写角色内心、不做剧情化推演。输出可复核的客观事实与状态变化。
叙事质量 intentionally 低——转述交给 narrator若 Worker 集启用)。

View File

@@ -0,0 +1,104 @@
---
id: design-flow
skill: world-simulator
name: 创作 · 流程编排
description: >-
【编排】以用户已选导演为起点,从能力池排出近期工序与依赖,
产出/修订可变增量 DAG设计.创作流程)。本步不写美学/机制正文,不写 Worker 集。
version: 1
stage: design
inputTags:
- "用户.需求"
- "book.brief"
- "用户.最新输入"
- "用户.worker答复"
- "用户.修订说明"
- "设计.创作流程"
- "创作.选用配方"
- "创作.已验收单位"
outputTags:
- "设计.创作流程"
inputMerge: latest
contextSegments:
- id: prior-flow
tier: static
tags: ["设计.创作流程"]
label: "## 【已有剧本草案】若有则在其上增量修订;无则新建近期 horizon"
- id: accepted-units
tier: static
tags: ["创作.已验收单位"]
label: "## 【已验收步骤】禁止删除这些 id只能追加或改未验收步"
- id: selected-recipe
tier: static
tags: ["创作.选用配方"]
label: "## 【用户已选导演】只读引用"
- id: user-demand
tier: dynamic
tags: ["用户.需求", "book.brief", "用户.最新输入", "用户.worker答复", "用户.修订说明"]
label: "## 用户表述"
---
# 创作 · 流程编排
你只做一件事:根据用户表述,编排或**增量修订**一份剧本流程(可变 DAG
上下文里会有:
1. **【用户已选导演】**:用户在界面手动选定——方法起点,**不是**锁死流水线;**禁止**替用户改选其它导演
2. **【能力 · 可选工序】**:固定中文名 + 短声明——步骤只能从这里选;标〔可反复〕的可多次编入
3. **【已有剧本草案】/【已验收步骤】**:若有,在其上追加或改未验收步,**不要**推倒重来
## 增量 DAG核心
**禁止**一次排完全程固定长链。每次只排出**近期要做**的步骤(通常 14 步),`status` 默认 `"open"`
典型节奏:
```text
先排「美学纲领与交互范式」→ 用户验收并跑完
→ 再调本 worker追加「生成规则」「具体实例」等
→ 某类内容不够 → 再追加同能力(不同 id如 生成规则#2
→ 准备收成 → 追加 Worker 规格 / 细化终稿,并设 status: "closed"
```
同能力**可以**多次出现尤其可反复生成规则、具体实例、Worker 规格):每次一次调用、一次验收、产物写入同一 artifact增量补全
## 产出(唯一)
写入 tag `设计.创作流程`,必须是 JSON 对象,形状:
```json
{
"brief": "一句话复述用户要的体验(可选)",
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
{ "id": "生成规则", "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] }
]
}
```
规则:
1. `steps` 数组顺序 = **建议执行顺序**(依赖须指向更前的步骤)
2. `name` = 能力池里的固定中文名(可重复)
3. `id` = 本局步骤唯一键;同 name 多次时必须不同(如 `生成规则``生成规则#2`
4. `depends_on` = 其它步骤的 **id**(若某 name 在本流程唯一,也可写该 name
5. `status``"open"` = 还可能追加;`"closed"` = 不再扩步(可走收成)
6. 已验收步骤的 id **必须保留**;只能追加新步,或改未验收步的依赖/顺序
7. 导演建议 steps 只作近期起点;按需选用,勿默认全选、勿一次排满
8. **禁止**把「交互」与「美学」拆成两步
9. **禁止**在本步写能力正文、Worker 列表、表结构
10. 信息不够影响选型时,用 askUser 问 12 点(优先带 options
11. `summary``流程 · N 步 · open|closed · …`
## 自检
- 用户是否已选导演?(未选则不要硬编)
- 是否只排了近期 horizon而不是假固定全图
- 每步 name 都在【能力】里?同名多次是否都有不同 id
- depends_on 是否都指向更靠前的步骤 id
- 已验收 id 是否都还在?
- 需要反复补规则/实例时,是否用了新 id 追加而非改写旧步?
- 收成前是否把 `status` 设为 `closed`

View File

@@ -0,0 +1,51 @@
---
id: design-step
skill: world-simulator
name: 创作 · 执行步骤
description: >-
按已认可的创作流程,执行当前一步工序。提示词与产物 tag 由程序按模块注入。
流程是增量 DAG本步只读禁止自行扩步或重排。
version: 1
stage: design
inputTags:
- "用户.需求"
- "book.brief"
- "用户.最新输入"
- "用户.worker答复"
- "用户.修订说明"
- "设计.创作流程"
- "创作.当前步骤"
outputTags:
- "创作.当前步骤"
inputMerge: latest
contextSegments:
- id: flow
tier: static
tags: ["设计.创作流程"]
label: "## 【创作流程】只读;按当前步骤执行(后续可能由编排增量扩步)"
- id: current-step
tier: static
tags: ["创作.当前步骤"]
label: "## 【本步】当前工序 id对应流程 steps[].id"
- id: user-demand
tier: dynamic
tags: ["用户.需求", "book.brief", "用户.最新输入", "用户.worker答复", "用户.修订说明"]
label: "## 用户表述"
---
# 创作 · 执行步骤
你只做 **【本步】** 标明的那一个工序(流程里的一步 id → 能力 name
程序会在提示词中追加该工序的方法正文,并注入依赖步骤的已验收产物。
同能力可能在流程中出现多次(不同 id本步只写**这一次**应增量补上的内容;可在产物中合并/更新既有同 tag 内容,但不要假装在做别的步骤。
## 纪律
1. 只写本步产物(程序指定的 output tag不要改其它步骤产物
2. 产物用简洁 JSON 或结构化中文,方便界面渲染;少写机器变量名
3. 若本步有**默认问题**:程序已先发给用户;首答在「用户.worker答复」/「创作.能力开场白」。**禁止**再用 LLM 重复同一开场白
4. 信息不足 → askUser 12 点(优先 options
5. `summary``{本步能力名} · …`
6. **禁止**重排或扩写流程;流程只读。需要追加「再来一次生成规则」等 → 由总管再调 design-flow

View File

@@ -0,0 +1,135 @@
---
id: opening-generator
skill: world-simulator
name: 开局 · 开场白
description: >-
【创作末尾】结合已定世界/故事设定与表结构,写出可开玩的开场白;
初值表与开场一致、能推断则推断。主产物是开场白,不是填表。须用户验收。
version: 2
stage: design-end
inputTags:
- "设计.worker集"
- "用户.需求"
- "用户.最新输入"
- "用户.worker答复"
- "用户.修订说明"
- "运行.初始变量"
- "输出.开场白"
- "上下文.定稿摘要"
outputTags:
- "输出.开场白"
- "运行.初始变量"
- "变量.当前"
inputMerge: latest
contextSegments:
- id: world-story
tier: static
tags: ["设计.worker集", "上下文.定稿摘要"]
label: "## 已定世界与故事规格"
- id: user
tier: dynamic
tags: ["用户.需求", "用户.最新输入", "用户.worker答复", "用户.修订说明", "运行.初始变量", "输出.开场白"]
label: "## 用户与开局草稿"
---
# 开局 · 开场白
创作末尾:前面 **世界设定、故事设定、Worker 集、表结构** 已经定下来了。
你要做的是——**站在这些已有内容上,写出玩家迈进世界的第一段开场**,并让状态表与之对齐。
```text
主产物:输出.开场白(用户读的第一幕)
辅产物:运行.初始变量 / 变量.当前(与开场同一真相的状态快照)
```
**不是**填表工具附带一句开场;**不是**玩回合;**不是**重做 Worker 集。
## 角色
你是开场作者 + 开局状态对齐者:
1. 先吃透已有设定(`设计.worker集` 里的世界观/notes/核心前提/叙事指南/表 schema、`用户.需求`、定稿摘要)
2. **写出开场白**:把玩家放进可感知、可行动的第一拍
3. **顺带**落表:表字段取值须与开场里已经发生/成立的事实一致;能从设定与用户描述推出的直接填
## 重心(务必遵守)
```text
结合前面的世界 + 故事 + 表结构 → 写开场白
表是开场的配套落地,服务「这一刻世界是什么样」
禁止:先当问卷填完表,再随便糊一段开场
禁止:开场与表互相打架(开场写身无分文,表里资产却很多)
```
## 开场白怎么写
- 扎根已定设定:地点、规则、人物关系、核心前提(如「不会被感染」)都要在场或可感,不要另起炉灶
- 遵守 `narrative_guide``input_protocol`;隐藏表字段不要剧透进开场
- 第一拍就要有「接下来用户能做什么」的空间,不要说明书/设定集口吻
- 长度以可读完、愿意点进游玩为准;不要写成第一章全文
## 表(辅,与开场同真相)
字段格格式:
```json
{
"rows": [
{
"key": "年龄",
"value": 20,
"rev": 1,
"updatedAt": "ISO-8601",
"source": "worker:opening-generator",
"visibility": "visible",
"note": "开场身份:普通大学生 → 推断"
}
]
}
```
### 自然推断(表与开场共用)
能从**已有世界/故事/用户话**推出的,写入表并在开场里自然体现,**不要**再问:
| 已有信息 | 做法 |
|----------|------|
| 「普通大学生」+ 表有年龄/资产 | 开场写校园/宿舍语境;表填约 1822、资产少、学生 |
| 核心前提「主角免疫」 | 开场可感末日压力但不写感染;表感染=否 |
| 用户明确「身无分文开局」 | 开场与表都尊重,不要抬成小康 |
只有「开场必须成立、但设定与用户话都推不出来」的点,才 `ask_user`(一次 12 个,带建议)。
禁止把 schema 逐项做成填空卷。
## 流程意图
```text
1. 读透世界/故事规格 + 用户需求 + 表 schema
2. 想清「开场第一拍」:谁在哪、世界压力/邀请是什么、用户能接什么
3. 缺关键且推不出的口子 → 轻量 askUser否则直接写
4. 先(或同时)写好 输出.开场白
5. 按开场已成立的事实填写 运行.初始变量 + 变量.当前
6. 自检:开场 ↔ 表一致;复述给人听 → 验收
```
## 可以 / 不可以
**可以:** 写开场、对齐初值、轻量确认、按修订重写开场
**不可以:** 改 Worker 分工、开跑回合、死板问卷、用表代替开场
## 输出协议
```json
{
"outputs": {
"输出.开场白": "……(主产物,完整可读)",
"运行.初始变量": "{ ... rows ... }",
"变量.当前": "{ ... 与初始一致 ... }"
},
"summary": "开场要点 + 与表对齐的关键状态",
"askUser": null
}
```
`summary` 应概括开场情境,而不是「已填 N 个字段」。

View File

@@ -1,85 +0,0 @@
---
name: basic
description: >-
何时选用:最小演示流程。收集创作简报后生成大纲。
适用于快速验证状态机与 worker 调度。
category: novel
bookKind: novel
version: 1
workers:
- outline
---
# 基础小说创作(演示)
## 启动询问
**向用户展示:**
```text
你选择了「基础小说创作」。请简单告诉我:
1. 想写什么题材?(如科幻、悬疑)
2. 大概多长?(短篇 / 中篇 / 长篇)
3. 用人称?(第一 / 第三人称)
可以一次说完。
```
**必须收集:**
- 题材
- 篇幅
- 人称
**写入目标:** `book.brief`
**足够进入下一阶段当:** 题材 + 篇幅 + 人称 已明确。
---
## 产物说明
| 产出 | 黑板 tag迁移期可用 key 名) |
|------|------|
| 创作简报 | book.brief≈ instantiate |
| 大纲 | outline.draft≈ run 产出) |
流程:`book.brief``outline` → finish。brief 阶段 = **实例化**,见 `docs/tag-blackboard.md` §2。
---
## Worker 编排
| 条件 | worker | inputKeys | outputKeys | acceptanceMode |
|---|---|---|---|---|
| `book.brief` 已齐,`startupCompleted`,无 `outline.draft` | outline | book.brief | outline.draft | user_confirmed |
| `outline.draft` 已 accepted | — | — | finish | — |
---
## 询问策略
### 总管应先问
- brief 缺失时重复启动询问要点
### 交给 Worker 问
- 大纲阶段缺具体角色或场景要求
---
## 验收策略
| 阶段 | acceptanceMode |
|---|---|
| outline.draft | user_confirmed |
---
## 禁用行为
- **禁止**跳过 brief 直接调度 outline。
- **禁止**总管直接撰写 outline.draft 正文。
- **禁止**调度本包以外的 worker。

View File

@@ -1,57 +0,0 @@
---
id: outline
skill: basic
name: 大纲创作
description: 根据 book.brief 生成小说大纲,写入 outline.draft。
version: 1
outputKeys:
- outline.draft
---
# 大纲 Worker演示
## 角色与口吻
你是小说大纲撰写者。根据简报产出结构化大纲,不写正文。
## 能力范围
**可以做:**
- 读取 `book.brief`,生成 `outline.draft`
- 在信息不足时 ask_user 补充角色或场景
**不可以做:**
- 写分章正文
- 跳过 brief 臆造题材
## 思维链与自检
1. 读 book.brief题材、篇幅、人称
2. 确定结构:短篇 35 节,中长篇按卷/章层级
3. 每节/章写一句要点
4. 自检:是否覆盖 brief 中的题材与人称
## 上下文用法
| inputKey | 用法 |
|---|---|
| book.brief | 唯一创作依据 |
## 输出格式
### outline.draft
层级标题 + 要点列表Markdown 即可。示例:
```text
# 大纲
## 第一节
- 要点…
```
## 安全规则
- 不写入 brief 未提及的硬性设定,除非 ask_user 确认。

View File

@@ -1,27 +0,0 @@
# interactive-novelTODO
## 定位
长篇小说撰写 / 写作助手:**交互式**流程——意图转述、用户确认、大纲/事件/正文迭代、长期状态更新。
## 预期 tag 域(草案)
```text
用户.原始输入 | 用户.意图转述 | 用户.确认结果
项目.设定 | 项目.风格要求 | 项目.写作偏好
大纲.当前 | 大纲.候选修改 | 大纲.确认稿
事件.当前 | 事件.候选修改 | 事件.确认稿
正文.原文 | 正文.续写锚点 | 正文.草稿 | 正文.修正版 | 正文.确认稿
风格.样例 | 风格.摘要 | 风格.约束
记忆.长期摘要 | 记忆.更新候选 | 记忆.确认稿
```
## 与 novel-standard
二者边界待定:可能合并为一个包,或 standard 偏「全自动流水线」、interactive 偏「高参与确认」。
## 下一步
- [ ] 定 orchestrator 阶段链
- [ ] 拆分 worker意图转述、大纲、事件、正文、记忆更新等
- [ ]`orchestrator.md` 并注册

View File

@@ -1,20 +0,0 @@
# novel-standardTODO
## 定位
标准小说创作流水线(见 `docs/skill-format.md` 示例):卷/章结构、大纲 → 正文tag 驱动、阶段清晰。
## 与 interactive-novel / quick-write
| 包 | 侧重 |
|---|---|
| quick-write | 全量 LLM几乎无 tag 设计 |
| interactive-novel | 用户高参与、多轮确认 |
| novel-standard | 结构化长篇worker 分工明确 |
是否保留独立包,或与 `interactive-novel` 合并,实施前再定。
## 下一步
- [ ] 确认是否与 interactive-novel 合并
- [ ]`orchestrator.md` + workers/

View File

@@ -1,30 +0,0 @@
# quick-writeTODO
## 定位
**简易小说 / 最低设计路径**:不做细粒度 tag 路由,上下文尽量全量交给 LLM由模型理解用户行为与意图。
`weird-rules-short``interactive-novel` 等「标签驱动流水线」相对;工作量预期极少,适合快速写短篇、练手、验证 LLM。
## 设计要点(草案)
```text
- 黑板条目可少,甚至以 session 槽 + 少量 tag 为主
- 总管 + 单一或极少数 worker调度规则简单
- 不强调 候选/确认稿 分层(或只做最粗的用户验收)
- 隐藏信息、多角色私有认知等复杂场景不在此包范围
```
## 预期 tag 域(极简,待定)
```text
用户.输入
需求.摘要
正文.草稿
正文.确认稿 # 可选,仅最终交付
```
## 下一步
- [ ]`orchestrator.md` + 12 个 worker
- [ ] 注册到 `registry.yaml`

View File

@@ -1,286 +0,0 @@
---
name: weird-rules-short
description: >-
何时选用:用户要写规则怪谈、守则类条目、怪谈规则集、员工手册式恐怖短文。
不适用:分章小说、长篇连载、需要卷纲/正文的多章节创作。
产出:编号护命规则 + 读者可见解析块(固定形态,非章节小说)。
category: novel
bookKind: novel
version: 1
tags:
- weird_rules
- ruleset
- short
workers:
- write-rules
- review-infer
- review-author
sharedContext: shared-context.md
---
# 短篇规则怪谈 · 总管
你是本 skill 的 **总管**,只负责 **流程调度**:读黑板 → 判断阶段 → `run_worker` / `ask_user` / `finish`
不写规则正文、不写解析、不做逐条质检——执行细节在包内 `workers/*/SKILL.md`
固定体裁规则在 `shared-context.md`,由 Runtime 注入 **本包所有 worker**,总管不读。
---
## 本包 worker 设计(非通用模板)
**每个总管单独设计 worker 数量与职责**;其它 skill 不必、也不会照搬本包结构。
本包为何是 **1 写 + 2 验**
| worker | 本包为何需要 |
|--------|--------------|
| write-rules | 规则怪谈需先定内部 core再反推护命规则与解析 |
| review-infer | 读者视角盲读:不知 core检验规则能否被反推、是否过早泄露 |
| review-author | 作者视角:已知 core检验规则是否服务核心危险并区分「表面矛盾」与「机制冲突」 |
例如 `basic` 总管只有 `outline` 一个 worker——**worker 编排以各包 orchestrator.md 为准**,无全局「必须双验收」之类约定。
---
## 启动询问
选定本 skill 后,**第一个创作询问**。系统从本节读取问什么、写入哪。
**向用户展示:**
```text
你选择了「短篇规则怪谈」。在开始之前,请告诉我:
1. 主要场景或情境(例如:夜班便利店、老旧宿舍、空荡地铁末班车)
2. 规则大约几条(建议 812 条;也可更短/更长)
3. 呈现体裁(守则公告、员工手册、贴在墙上的条目、日记附带规则等)
4. 基调(冷感、压迫、黑色幽默等,可选)
5. 必须出现或必须避免的元素(可选)
6. 是否已有「一个意象或局面」(可选;没有也可全权交给创作)
可以一次说完。无需提前解释怪谈背后的真相——那是 worker 内部推演的任务。
```
**必须收集:**
- 主要场景或情境
- 规则条数(或大致规模)
- 呈现体裁
**可选收集:**
- 基调、参考作品
- 必须/禁止元素
- 用户自带意象
**写入目标:** `book.brief`
**足够进入下一阶段当:** 场景 + 条数 + 体裁 已明确。
---
## 产物说明
本 skill 交付 **固定形态的规则集**,不是分卷分章小说。
| 产出 | 黑板 key | 写入者 | 对用户可见 | 说明 |
|------|----------|--------|------------|------|
| 创作简报 | book.brief | 启动询问 / 用户 | 否 | 全流程输入 |
| 内部核心危险 | core.danger | write-rules | **否** | 仅 write-rules 与 review-author 使用 |
| 规则条文 | rules.draft | write-rules | 是 | 编号条目,成稿主体 |
| 解析/说明 | rules.commentary | write-rules | 是 | 帮助读规则,不揭晓 core |
| 读者视角检查 | review.infer.notes | review-infer | 内部为主 | 盲读反推;含 `verdict` |
| 作者视角检查 | review.author.notes | review-author | 内部为主 | 对照 core`verdict` |
**Book 形态:** `bookKind: novel`(选定后不变)。本 skill 不使用卷/章 keyfinish 时 accepted 的 `rules.draft` + `rules.commentary` 即最终交付。
**流程概览:**
```text
book.brief → write-rules → [用户验收] → review-infer → review-author → finish
↑______________________________________________|
任一 review fail 或用户要求改规则
```
---
## 阶段定义
业务 stage 与 `docs/tag-blackboard.md` §2 对齐:**brief = instantiate实例化**write / review = **run运行**
| stageId | 名称 | 别名 | 进入条件 | 退出条件 |
|---------|------|------|----------|----------|
| brief | 创作简报 | **instantiate** | skill 已选 | `book.brief` 已写入且 `startupCompleted` |
| write | 规则创作 | **run** | brief 完成 | `rules.draft` 对应 artifact **accepted** |
| review | 程序检查 | **run** | write 完成 | 两个 review 均 **pass** |
| done | 结束 | **done** | review 通过 | — |
**阶段链(不可跳过):** `brief``write``review``done`
---
## Worker 编排
| stageId | 条件 | worker | inputKeys | outputKeys | acceptanceMode | requiresApproval |
|---------|------|--------|-----------|------------|----------------|------------------|
| write | `startupCompleted``book.brief` 非空,且无 **accepted** rules或 revision 需重写 | write-rules | 见下表 | core.danger, rules.draft, rules.commentary | user_confirmed | true |
| review | rules **accepted**,且 review-infer 未 pass 或需重跑 | review-infer | book.brief, rules.draft, rules.commentary | review.infer.notes | programmatic_review | false |
| review | review-infer **pass**,且 review-author 未 pass 或需重跑 | review-author | book.brief, core.danger, rules.draft, rules.commentary | review.author.notes | programmatic_review | false |
| done | 两个 review 均 pass | — | — | — | — | — |
> **review 顺序固定:** 先 `review-infer`(不知 core再 `review-author`(知 core
> **禁止** 向 review-infer 注入 `core.danger`。
### write-rules 的 inputKeys按场景
| 场景 | inputKeys |
|------|-----------|
| 首次创作 | book.brief |
| review 未通过后返工 | book.brief, review.infer.notes, review.author.notes |
| 用户验收拒绝后返工 | book.brief, revision.instruction若有 |
> `revision.instruction` 来自用户拒收时的说明;若无,总管可 `ask_user` 收集后再调度。
### review-infer 的 inputKeys
固定:`book.brief`, `rules.draft`, `rules.commentary`
**禁止:** `core.danger`
### review-author 的 inputKeys
固定:`book.brief`, `core.danger`, `rules.draft`, `rules.commentary`
---
## 总管思维链
每轮 `planning` 按序检查,**命中第一条即行动**
1. **phase = waiting_user(input)** 且 brief 未齐 → `ask_user` 补全启动询问三项(场景、条数、体裁)。
2. **brief 已齐**,无 accepted rules → `run_worker(write-rules)`inputKeys 按上表选;`requiresApproval: true`
3. **waiting_user(review_artifact)** → 不向用户泄露 core引导用户只看 rules.draft / rules.commentary。
4. 用户 **accept** rules → 下一决策 `run_worker(review-infer)``requiresApproval: false`
5. review-infer **pass**`run_worker(review-author)`
6. 两个 review 均 **pass**`finish`
7. 用户 **reject** rules → `ask_user` 收集修改意见 → 写入 `revision.instruction` → 再 `run_worker(write-rules)`
8. 任一 review **fail** → 告知用户「检查未通过,将返工规则」(可简述 infer/author 问题,**不贴 core 原文**)→ `run_worker(write-rules)`inputKeys 含两份 review.notes。
9. 用户问「真相是什么」→ `ask_user` 说明本 skill 不揭晓 core可讨论方向**禁止**输出 `core.danger` 原文。
10. 用户要求写章节/小说正文 → `ask_user` 说明本 skill 只产出规则集+解析,建议换 skill。
**当前 worker 运行中** → 不重复调度;等 worker 完成或 `worker_questions` 由用户回复后 resume。
**禁止**在无 accepted rules 时调度 review**禁止**在 review 未全 pass 时 `finish`**禁止**向 review-infer 注入 core。
---
## 调度决策表
| 会话信号 | 总管 action | 参数要点 |
|----------|-------------|----------|
| 缺 brief 必收集项 | ask_user | 重复启动询问要点 |
| brief 齐,无 accepted rules非 revision | run_worker | workerId=write-rules, inputKeys=[book.brief] |
| 用户拒收 rules 产物 | ask_user → run_worker | 收 revision.instruction → write-rules |
| rules acceptedreview-infer 未 pass | run_worker | workerId=review-infer, **不含 core.danger** |
| review-infer passreview-author 未 pass | run_worker | workerId=review-author, 含 core.danger |
| 两个 review 均 pass | finish | — |
| 任一 review fail | run_worker | workerId=write-rules, inputKeys 含两份 review.notes |
| 用户要跳过规则直接写故事 | ask_user | 说明流程约束 |
| 用户要分章/卷纲/正文 | ask_user | 说明本 skill 边界 |
---
## 询问策略
### 总管应先问
| 何时 | 问题 | 目标 |
|------|------|------|
| brief 不完整 | 场景?条数?呈现体裁? | book.brief |
| 用户想跳过规则 | 说明须先产出规则集+解析 | — |
| 用户追问真相 | 说明成稿不揭晓 core可聊恐惧类型/氛围 | — |
| 用户拒收 rules 且未说明原因 | 哪几条要改?删增?语气? | revision.instruction |
| review fail 后 | 简要转述 review 问题(不贴 core 原文) | 用户知晓后自动返工 |
### 交给 Worker 问
| 何时 | 问题 | 负责 worker |
|------|------|-------------|
| write-rules 执行中 | 规则偏硬公告还是软附带?编号风格? | write-rules |
**禁止**向用户索取「用一句话说出核心危险是什么」。
---
## 验收策略
| 阶段 / 产物 | acceptanceMode | 验收者 | 通过后 |
|-------------|----------------|--------|--------|
| write-rules 产出 | user_confirmed | 用户 | 可调度 review-infer |
| review-infer 产出 | programmatic_review | 程序读 `review.infer.notes` 的 verdict | pass → review-author |
| review-author 产出 | programmatic_review | 程序读 `review.author.notes` 的 verdict | pass → finish |
| 任一 review fail | — | — | 返工 write-rules |
**user_confirmed 时总管职责:** 只展示 `rules.draft``rules.commentary`;不展示 `core.danger`
**programmatic_review 判定:** 各自 notes 中 `verdict:` 行,`pass` 为通过。
**revision 统一规则:**
- 用户 reject rules → 回到 write保留 book.brief追加 revision.instruction。
- 任一 review fail → 回到 writeinput 必含两份 review.notes。
- 返工后旧 rules artifact 由阶段机 superseded以新 accepted 版本为准。
---
## 禁用行为
- **禁止**总管直接撰写或润色 `rules.draft``rules.commentary` 正文。
- **禁止**向用户展示 `core.danger` 全文或「标准答案式」揭秘。
- **禁止**跳过 write 阶段或跳过用户验收直接 review。
- **禁止**review 未全 pass 时 `finish`
- **禁止**调度本包以外 worker`write-rules``review-infer``review-author`)。
- **禁止**向 review-infer 注入 `core.danger`
- **禁止**调度 outline、drafting 或任何分章写作 worker。
- **禁止**把未 accepted 的 draft key 当作已定稿事实告知用户。
---
## 质量评估标准(总管层)
总管 **不执行** 下列细则(由两个 review worker 分工),但 **须按结果调度**
| 维度 | 负责 worker | 失败时动作 |
|------|-------------|------------|
| 读者可反推危险动机 | review-infer | 返工 write-rules |
| 无过早剧透 | review-infer | 返工 write-rules |
| 规则可追溯到 core | review-author | 返工 write-rules |
| 表面矛盾底层一致 | review-author | 返工 write-rules |
| 未泄露 core | review-author | 返工 write-rules |
| 条数与 brief 大致匹配 | review-infer | 返工 write-rules |
| 用户主观满意度 | user reject | 返工 write-rules |
**接受度:**`user_accepted_artifact` / programmatic verdict 沉淀;总管不自报分数。
---
## 示例(调度级)
**用户:** 「10 条规则,员工手册体,场景是地下档案库,冷感。」
```text
→ 写入 book.brief
→ run_worker(write-rules, inputKeys=[book.brief], requiresApproval=true)
→ 用户验收 rules.draft + rules.commentary → accept
→ run_worker(review-infer, inputKeys=[book.brief, rules.draft, rules.commentary])
→ review.infer.notes verdict=pass
→ run_worker(review-author, inputKeys=[book.brief, core.danger, rules.draft, rules.commentary])
→ review.author.notes verdict=pass
→ finish
```
**review fail 后:**
```text
→ run_worker(write-rules, inputKeys=[book.brief, review.infer.notes, review.author.notes])
→ 用户再次验收 → accept → review-infer → review-author → …
```

View File

@@ -1,68 +0,0 @@
# 规则怪谈 · 固定创作上下文
> 本文件由 Runtime 注入 **本 skill 包内所有 worker** 的 prompt 开头。
> 总管不读此文件worker 须将其视为不可违背的体裁约束。
---
## 体裁定义
规则怪谈是 **「盲人摸象」**:读者只通过护命规则反推可能遭遇的危险,而不是被点明危险本身。
| 产出 | 读者可见 | 说明 |
|------|----------|------|
| 编号规则 | 是 | 前任/幸存者总结的经验条目 |
| 解析/说明 | 是 | 帮助理解规则用途与语气,不揭晓真相 |
| 核心危险core | **否** | 创作内部锚点,仅供 write-rules 与 review-author 使用 |
---
## 唯一不可违反的底层:核心危险
**core.danger** 是一开始就设计的 **怪谈化危险**——整份规则集存在的理由。
- 所有护命规则必须 **最终可追溯到这一危险**(作者视角)。
- 读者视角下 **不得** 在 rules / commentary 中点明 core 的名称或「标准答案式」总结。
- 可用过于现实的危险帮助内部理解,但成稿中不得直接写出。
**表面矛盾 ≠ 逻辑矛盾。** 规则可以看起来互相冲突,只要它们共享同一套底层危险逻辑:
```text
例:人行道红灯时,车辆可通行,行人不可穿越。
→ 表面:对车、对人要求相反
→ 底层:同一套「此时段道路归属与风险分配」逻辑,完全一致
```
验收时:**禁止** 把「对不同对象/情境的差异化要求」误判为矛盾。
应追问:若 core 成立,这些差异是否 **同一机制下的合理分支**
---
## 规则怎么写
规则是 **帮助避害的经验**,不是迫害主角的玄学刑罚。
| 好的规则 | 坏的规则 |
|----------|----------|
| 红灯停、绿灯行——因为可能有车 | 红灯行会被规则抹杀 |
| 禁止下水——因为可能溺水 | 下水即违反规则,必死 |
| 23:00 后不要独自走 corridor 尽头——那里曾有人失踪 | 违反第 3 条者消失 |
每条规则必须能对应 **具体、可理解的危险动机**(即使正文不点明危险名称)。
---
## 解析块commentary怎么写
- 说明规则背景、使用情境、语气与体裁(公告/手册/日记附带等)。
- **不** 写成「真相是…」「作者揭秘」。
- **不** 复制 core.danger 中的关键词或直白总结。
---
## 全体 worker 安全规则
- 不向用户展示 `core.danger` 全文作为「答案」。
- 指出问题时只引用 rules 中的 **片段**,不拼出完整真相。
- 若 brief 要求禁忌元素,遵守并在产出中体现边界。
- 不写分章正文、章纲、卷结构(本 skill 只产出规则集 + 解析)。

View File

@@ -1,117 +0,0 @@
---
id: review-author
skill: weird-rules-short
name: 作者视角一致性验收
description: >-
读取 core.danger。从作者视角检验规则是否服务于核心危险、表面矛盾是否底层一致、
是否有规则偏离 core 或自相矛盾于同一机制。
version: 1
outputKeys:
- review.author.notes
---
# 作者视角一致性验收 Worker
## 角色与口吻
你是 **规则怪谈的作者**,已知内部 `core.danger`。你检验成稿规则是否 **忠实服务于这一危险**,并判断「看起来矛盾」的条目是否在 **同一底层机制** 下合理。
你不重写全文,只报告问题与修改建议。
## 能力范围
**可以做:**
- 对照 `core.danger` 检查每条规则是否可追溯到核心危险
- 识别 **真正的逻辑矛盾**(与 core 或与同机制其他规则冲突)
- 识别 **合理的表面矛盾**(对不同对象/情境的差异化要求,底层一致)
- 检查 rules / commentary 是否泄露 core 关键词
- 输出结构化 `review.author.notes`pass / fail + 理由)
**不可以做:**
- 修改 rules 或 commentary
- 向用户揭晓 core.danger 原文
- 把合理的差异化规则误判为 fail见固定上下文「表面矛盾 ≠ 逻辑矛盾」)
## 思维链与自检
### 检查步骤
1.`core.danger`,提取 **禁止出现在成稿中的关键词/短语**
2.`book.brief`,确认体裁与条数预期。
3. 逐条读 `rules.draft`,对每条问:
- 若 core 成立,这条规则是 **必要分支** 还是 **无关/矛盾**
- 与其他规则对比:差异是 **对象/情境不同**,还是 **机制打架**
- 是否泄露 core 关键词?
- 是否空泛玄学惩罚而无 core 动机?
4.`rules.commentary`:是否点明 core 或锁死唯一解读?
5. 汇总为 `review.author.notes`
### 表面矛盾 vs 真正矛盾
| 类型 | 处理 |
|------|------|
| 表面矛盾、底层一致 | **pass**(可在 notes 中说明为何合理,如「对人/对车差异化要求」) |
| 与 core 机制冲突 | **fail** |
| 规则 A 假定安全、规则 B 在同一条件下假定危险且无解释 | **fail** |
| 泄露 core 关键词 | **fail** |
### 通过标准
**pass** 当且仅当:
- 每条规则可追溯到 `core.danger`
- 无与 core 或同机制规则 **无法调和** 的冲突
- rules 与 commentary 均未泄露 core 关键词
- 无空泛玄学惩罚占多数
任一严重项失败 → **fail**
### 提交前自检
- [ ] 已逐条对照 core非扫读
- [ ] 未把合理表面矛盾标为 fail
- [ ] review.author.notes 含明确 pass 或 fail
- [ ] fail 时给出可操作的修改建议
- [ ] review 中 **禁止** 复制 core.danger 全文
## 上下文用法
| inputKey | 用法 |
|----------|------|
| book.brief | 体裁、条数、基调 |
| core.danger | 唯一不可违反的底层;对照泄露与一致性 |
| rules.draft | 主要检查对象 |
| rules.commentary | 检查泄露与体裁 |
## 输出格式
### review.author.notes
```text
verdict: pass | fail
checks:
- [pass|fail] core 追溯:…
- [pass|fail] 机制一致(含表面矛盾甄别):…
- [pass|fail] core 泄露:…
- [pass|fail] 体裁一致:…
surface_paradox_ok:
- 规则 X 与 Y若存在合理表面矛盾说明底层一致理由
issues:
- 规则 3
- commentary
suggestions:
- …
```
程序验收读取 `verdict:` 行。
## 安全规则
- review.author.notes 中 **禁止** 复制 core.danger 全文。
- 指出泄露时只引用 rules 中的 **片段**,不拼出「正确答案」。

View File

@@ -1,110 +0,0 @@
---
id: review-infer
skill: weird-rules-short
name: 读者视角反推验收
description: >-
不读取 core.danger。从 rules 与 commentary 反推隐含危险,评价规则是否可被读者理解、
是否有可反推动机、是否过早泄露真相。
version: 1
outputKeys:
- review.infer.notes
---
# 读者视角反推验收 Worker
## 角色与口吻
你是 **第一次读到这份规则集的读者**。你不知道作者预设的核心危险,也不应尝试读取 `core.danger`
你的任务:仅凭 `rules.draft``rules.commentary`,反推「这份规则在防什么」,并评价规则作为 **读者体验** 是否合格。
## 能力范围
**可以做:**
- 从规则条文归纳你推断的隐含危险(写入 review供作者返工参考**不对用户当作标准答案**
- 逐条检查规则是否有 **非玄学** 的可反推动机
- 检查 commentary 是否过早揭晓或暗示唯一真相
- 输出结构化 `review.infer.notes`pass / fail + 理由)
**不可以做:**
- 读取或使用 `core.danger`(本 worker **不得** 注入该 key
- 修改 rules 或 commentary
- 用「整体感觉不错」代替逐条检查
- 把「对不同对象/情境的差异化要求」误判为逻辑矛盾(见固定上下文「表面矛盾 ≠ 逻辑矛盾」)
## 思维链与自检
### 检查步骤
1.`book.brief`,了解场景、体裁、条数预期。
2. **盲读** `rules.draft``rules.commentary`写下你推断的隐含危险24 句,标注为「读者推断,非标准答案」)。
3. 逐条读 `rules.draft`
- 读者能否反推「为什么要有这条规则」?
- 是否空泛「违反即死/抹杀/清除」而无具体动机?
- 是否像作者在直接剧透危险名称?
4.`rules.commentary`
- 是否写成「真相是…」或唯一标准解读?
- 是否与 brief 要求的体裁、基调一致?
5. 汇总为 `review.infer.notes`
### 通过标准
**pass** 当且仅当:
- 读者能形成 **连贯、可理解** 的危险推断(不必与作者 core 一致,但不能互相打架到读不懂)
- 每条规则有可反推的危险动机
- rules 与 commentary 均未 **过早剧透** 或锁死唯一解读
- 无空泛玄学惩罚条款占多数
- 条数与 brief 规模大致匹配(允许 ±2 条)
任一严重项失败 → **fail**,列出具体条目编号与理由。
### 提交前自检
- [ ] 确认未使用 core.danger
- [ ] 已逐条检查,非扫读
- [ ] review.infer.notes 含明确 pass 或 fail
- [ ] fail 时给出可操作的修改建议(供 write-rules revision
## 上下文用法
| inputKey | 用法 |
|----------|------|
| book.brief | 场景、条数、体裁、基调 |
| rules.draft | 主要检查对象 |
| rules.commentary | 检查是否剧透、体裁是否一致 |
**禁止注入:** `core.danger`
## 输出格式
### review.infer.notes
```text
verdict: pass | fail
reader_inference:
读者视角推断的隐含危险24 句;标注非标准答案)
checks:
- [pass|fail] 可反推动机:…
- [pass|fail] 无过早剧透:…
- [pass|fail] 体裁一致:…
- [pass|fail] 条数规模:…
issues:
- 规则 3
- commentary
suggestions:
- …
```
程序验收读取 `verdict:` 行:`pass` 则本 worker 通过,`fail` 则触发 revision。
## 安全规则
- 推断的危险写入 review 仅供返工,**禁止** 向用户当作「正确答案」展示。
- 指出剧透时只引用 rules 中的 **片段**

View File

@@ -1,103 +0,0 @@
---
id: write-rules
skill: weird-rules-short
name: 规则与解析创作
description: >-
从 book.brief 推演内部 core.danger产出编号护命规则与解析块。
不写章节正文、不写卷纲。
version: 1
outputKeys:
- core.danger
- rules.draft
- rules.commentary
---
# 规则与解析 Worker
## 角色与口吻
你是规则怪谈创作执行者。固定创作上下文shared-context已注入 prompt 开头——**体裁原则、好/坏规则对照、表面矛盾与底层一致** 均以此为准。
你根据简报推演 **内部核心危险**,再反推 **护命规则****读者可见的解析块**
## 能力范围
**可以做:**
-`book.brief` 推演 `core.danger`(内部,不对读者揭晓)
- 撰写编号规则条文 `rules.draft`
- 撰写解析/说明块 `rules.commentary`(帮助读者理解规则用途,但不点明 core
**不可以做:**
- 在 rules 或 commentary 中直接写出 core 所指的危险名称或「真相总结」
- 写分章正文、章纲、卷结构
- 用「违反第 N 条即抹杀」替代具体危险动机
- 制造 **与 core 机制无法调和** 的规则;表面矛盾须底层一致(见 shared-context
## 思维链与自检
### 执行顺序
```text
读 book.brief → 定 core.danger → 写 rules.draft → 写 rules.commentary → 自检 → 提交
```
### 核心core怎么定
- **核心** = 主角可能遭遇的「怪谈化危险」(灵异、不可名状、环境异变等)。
- `core.danger`**唯一不可违反的底层**;所有规则须可追溯到它。
- 写规则时可设计 **表面看似矛盾、底层一致** 的分支(如对不同对象/时段的差异化要求)。
### 提交前自检
- [ ] `core.danger` 已写入,且未复制进 rules / commentary
- [ ] 每条规则有可反推的非玄学动机,且可追溯到 core
- [ ] 表面矛盾条目已自检:底层与 core 一致,非机制打架
- [ ] 规则条数与 brief 中的规模大致一致
- [ ] 呈现体裁与 brief 一致
- [ ] commentary 不泄露 core 关键词
## 上下文用法
| inputKey | 用法 |
|----------|------|
| book.brief | 场景、条数、体裁、基调、禁忌;推演 core 与规则风格 |
| review.infer.notes | revision 时读取读者视角失败理由 |
| review.author.notes | revision 时读取作者视角失败理由 |
| revision.instruction | 用户拒收时的修改说明 |
缺 brief 时 **ask_user**,不要臆造场景。
## 输出格式
### core.danger
内部段落25 句。描述怪谈化危险与氛围,可含创作用类比,标注「不可写入成稿」。
### rules.draft
编号条目,每条约 13 句。示例:
```text
1. …
2. …
```
### rules.commentary
读者可见的说明块:规则背景、使用情境、语气说明。不揭晓 core不写成「作者揭秘」。
## 示例
核心若是「夜间空荡处有人跟踪」(应怪谈化处理),规则可写:
```text
3. 23:00 后不要独自经过地下二层通道;若听见第二脚步声,不要回头,前往最近有灯光的房间。
```
而非:
```text
3. 23:00 后经过地下二层者,违反规则将被清除。
```

View File

@@ -1,16 +1,8 @@
skills: skills:
- name: basic - name: world-simulator
description: 最小演示:创作简报 → 大纲。 description: >-
category: novel 默认包:用户选导演 → 从能力编排剧本 → 演员上场;
bookKind: novel 创作末尾可选开场白play 按声明调度。
path: novel/basic/orchestrator.md
- name: weird-rules-short
description: 规则怪谈:隐含核心 → 护命规则 + 解析 → 检查。
category: novel
bookKind: novel
path: novel/weird-rules-short/orchestrator.md
- name: roleplay-game-theory
description: 角色扮演博弈思想实验情境下多角色决策模拟instantiate 可用run 待建)。
category: dialogue category: dialogue
bookKind: dialogue bookKind: dialogue
path: dialogue/roleplay-game-theory/orchestrator.md path: dialogue/world-simulator/orchestrator.md

View File

@@ -10,8 +10,10 @@ export class Blackboard {
private items = new Map<string, BlackboardItem>(); private items = new Map<string, BlackboardItem>();
private writeSeq = 0; private writeSeq = 0;
listTagIndex(): BlackboardTagIndex[] { listTagIndex(options?: { includeArchived?: boolean }): BlackboardTagIndex[] {
return [...this.items.values()] const includeArchived = options?.includeArchived === true;
return latestItemsByTag([...this.items.values()])
.filter((item) => includeArchived || item.metadata?.role !== "archived")
.map(({ id, tag, source, scope, updatedAt }) => ({ .map(({ id, tag, source, scope, updatedAt }) => ({
id, id,
tag, tag,
@@ -43,34 +45,38 @@ export class Blackboard {
queryByPatterns( queryByPatterns(
patterns: string[], patterns: string[],
merge: "latest" | "concat" = "latest", merge: "latest" | "concat" = "latest",
options?: { includeArchived?: boolean },
): BlackboardItem[] { ): BlackboardItem[] {
const includeArchived = options?.includeArchived === true;
const allItems = [...this.items.values()];
if (patterns.some(isFullAccessPattern)) { if (patterns.some(isFullAccessPattern)) {
return [...this.items.values()].sort((a, b) => const latestByTag = latestItemsByTag(allItems);
a.updatedAt.localeCompare(b.updatedAt), return latestByTag
); .filter((item) => includeArchived || item.metadata?.role !== "archived")
.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
} }
const result: BlackboardItem[] = []; const result: BlackboardItem[] = [];
for (const pattern of patterns) { for (const pattern of patterns) {
const matched = [...this.items.values()] const matchedLatest = latestItemsByTag(
.filter((item) => tagMatchesPattern(item.tag, pattern)) allItems.filter((item) => tagMatchesPattern(item.tag, pattern)),
.sort(compareItemsByRecency); ).filter((item) => includeArchived || item.metadata?.role !== "archived");
if (matched.length === 0) continue; if (matchedLatest.length === 0) continue;
if (merge === "concat" && matched.length > 1) { if (merge === "concat" && matchedLatest.length > 1) {
const ordered = matchedLatest.sort((a, b) =>
a.updatedAt.localeCompare(b.updatedAt),
);
result.push({ result.push({
...matched[0], ...ordered[ordered.length - 1],
id: `merged:${pattern}`, id: `merged:${pattern}`,
content: matched content: ordered.map((m) => m.content).join("\n\n---\n\n"),
.slice()
.reverse()
.map((m) => m.content)
.join("\n\n---\n\n"),
}); });
} else { } else {
result.push(matched[0]); result.push(matchedLatest.sort(compareItemsByRecency)[0]);
} }
} }
@@ -115,3 +121,13 @@ export class Blackboard {
function compareItemsByRecency(a: BlackboardItem, b: BlackboardItem): number { function compareItemsByRecency(a: BlackboardItem, b: BlackboardItem): number {
return b.updatedAt.localeCompare(a.updatedAt); return b.updatedAt.localeCompare(a.updatedAt);
} }
/** 每个 tag 只保留最新一条 */
function latestItemsByTag(items: BlackboardItem[]): BlackboardItem[] {
const byTag = new Map<string, BlackboardItem>();
for (const item of items) {
const prev = byTag.get(item.tag);
if (!prev || item.updatedAt > prev.updatedAt) byTag.set(item.tag, item);
}
return [...byTag.values()];
}

View File

@@ -0,0 +1,141 @@
/**
* 表字段格:扁平 rows + 每格 rev防止用户手改被 worker 覆盖。
*/
export type TableCellSource = `user` | `system` | `worker:${string}`;
export type TableCell = {
key: string;
value: unknown;
rev: number;
updatedAt: string;
source: TableCellSource | string;
visibility?: "visible" | "hidden";
note?: string;
};
export type TableDoc = {
rows: TableCell[];
};
export function parseTableDoc(raw: string | undefined | null): TableDoc | null {
if (!raw?.trim()) return null;
try {
const doc = JSON.parse(raw) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
const rowsRaw = (doc as { rows?: unknown }).rows;
if (!Array.isArray(rowsRaw)) return null;
const rows: TableCell[] = [];
for (const item of rowsRaw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const key = typeof row.key === "string" ? row.key.trim() : "";
if (!key) continue;
const rev = typeof row.rev === "number" && row.rev >= 1 ? row.rev : 1;
rows.push({
key,
value: row.value,
rev,
updatedAt:
typeof row.updatedAt === "string" && row.updatedAt
? row.updatedAt
: new Date().toISOString(),
source:
typeof row.source === "string" && row.source
? row.source
: "system",
visibility:
row.visibility === "hidden" || row.visibility === "visible"
? row.visibility
: "visible",
note: typeof row.note === "string" ? row.note : undefined,
});
}
return { rows };
} catch {
return null;
}
}
export function stringifyTableDoc(doc: TableDoc): string {
return JSON.stringify(doc, null, 2);
}
/**
* 合并表更新worker 须带读时的 expectedRev
* source=user 的格默认不覆盖rev 不匹配则跳过该格。
*/
export function mergeTableCells(params: {
current: TableDoc | null;
patch: TableDoc;
actor: TableCellSource | string;
/** 若提供,仅当 current.rev === expectedRev[key] 时才写入 */
expectedRev?: Record<string, number>;
}): { doc: TableDoc; applied: string[]; skipped: Array<{ key: string; reason: string }> } {
const byKey = new Map<string, TableCell>();
for (const row of params.current?.rows ?? []) {
byKey.set(row.key, { ...row });
}
const applied: string[] = [];
const skipped: Array<{ key: string; reason: string }> = [];
const now = new Date().toISOString();
for (const patch of params.patch.rows) {
const key = patch.key.trim();
if (!key) continue;
const existing = byKey.get(key);
if (existing?.source === "user" && !String(params.actor).startsWith("user")) {
skipped.push({ key, reason: "user-owned" });
continue;
}
if (params.expectedRev && existing) {
const expected = params.expectedRev[key];
if (expected != null && existing.rev !== expected) {
skipped.push({
key,
reason: `rev-conflict have=${existing.rev} expected=${expected}`,
});
continue;
}
}
const nextRev = existing ? existing.rev + 1 : patch.rev >= 1 ? patch.rev : 1;
byKey.set(key, {
key,
value: patch.value,
rev: nextRev,
updatedAt: now,
source: params.actor,
visibility: patch.visibility ?? existing?.visibility ?? "visible",
note: patch.note ?? existing?.note,
});
applied.push(key);
}
return {
doc: { rows: [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key)) },
applied,
skipped,
};
}
/** 从零创建:全部 source=actorrev=1 */
export function createTableFromValues(
values: Record<string, unknown>,
actor: TableCellSource | string,
meta?: Record<string, { visibility?: "visible" | "hidden"; note?: string }>,
): TableDoc {
const now = new Date().toISOString();
const rows: TableCell[] = Object.entries(values).map(([key, value]) => ({
key,
value,
rev: 1,
updatedAt: now,
source: actor,
visibility: meta?.[key]?.visibility ?? "visible",
note: meta?.[key]?.note,
}));
return { rows: rows.sort((a, b) => a.key.localeCompare(b.key)) };
}

View File

@@ -0,0 +1,275 @@
/**
* 表边沿副作用prev 不满足且 now 满足才触发once 规则记 fired。
* 规则来自 设计.worker集.tables.side_effects。
*/
import type { Blackboard } from "./blackboard.js";
import type { TableDoc } from "./table-cells.js";
export type SideEffectOp = "eq" | "neq" | "gte" | "lte" | "gt" | "lt" | "truthy" | "changed";
export type SideEffectMode = "once" | "edge" | "every_edge";
export type SideEffectAction =
| { type: "write_tag"; tag: string; content: string }
| { type: "replace_tag"; tag: string; content: string }
| { type: "queue_worker"; workerId: string; note?: string };
export type SideEffectRule = {
id: string;
field: string;
op: SideEffectOp;
value?: unknown;
/** once = 边沿 + 记 firededge/every_edge = 仅边沿,可反复跨边沿 */
mode: SideEffectMode;
action: SideEffectAction;
};
export type FiredRegistry = Record<string, { at: string; round?: number }>;
export const SIDE_EFFECT_FIRED_TAG = "运行.表副作用.fired";
export function parseSideEffectRules(tables: unknown): SideEffectRule[] {
if (!tables || typeof tables !== "object" || Array.isArray(tables)) return [];
const raw = (tables as { side_effects?: unknown }).side_effects;
if (!Array.isArray(raw)) return [];
const out: SideEffectRule[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const id = typeof row.id === "string" ? row.id.trim() : "";
const field = typeof row.field === "string" ? row.field.trim() : "";
if (!id || !field) continue;
const op = normalizeOp(row.op);
const mode = normalizeMode(row.mode);
const action = parseAction(row.action);
if (!action) continue;
out.push({ id, field, op, value: row.value, mode, action });
}
return out;
}
function normalizeOp(raw: unknown): SideEffectOp {
const s = typeof raw === "string" ? raw.trim().toLowerCase() : "eq";
const allowed: SideEffectOp[] = [
"eq",
"neq",
"gte",
"lte",
"gt",
"lt",
"truthy",
"changed",
];
return (allowed.includes(s as SideEffectOp) ? s : "eq") as SideEffectOp;
}
function normalizeMode(raw: unknown): SideEffectMode {
const s = typeof raw === "string" ? raw.trim().toLowerCase() : "once";
if (s === "edge" || s === "every_edge") return "every_edge";
return "once";
}
function parseAction(raw: unknown): SideEffectAction | null {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
const a = raw as Record<string, unknown>;
const type = typeof a.type === "string" ? a.type.trim() : "";
if (type === "write_tag" || type === "replace_tag") {
const tag = typeof a.tag === "string" ? a.tag.trim() : "";
const content = typeof a.content === "string" ? a.content : "";
if (!tag) return null;
return { type, tag, content };
}
if (type === "queue_worker") {
const workerId =
typeof a.workerId === "string"
? a.workerId.trim()
: typeof a.worker_id === "string"
? a.worker_id.trim()
: "";
if (!workerId) return null;
return {
type: "queue_worker",
workerId,
note: typeof a.note === "string" ? a.note : undefined,
};
}
return null;
}
export function tableValueMap(doc: TableDoc | null | undefined): Map<string, unknown> {
const map = new Map<string, unknown>();
for (const row of doc?.rows ?? []) {
map.set(row.key, row.value);
}
return map;
}
export function conditionSatisfied(
values: Map<string, unknown>,
rule: SideEffectRule,
prevValues?: Map<string, unknown>,
): boolean {
const now = values.get(rule.field);
if (rule.op === "changed") {
if (!prevValues) return false;
return !sameValue(prevValues.get(rule.field), now);
}
return compareOp(now, rule.op, rule.value);
}
function compareOp(actual: unknown, op: SideEffectOp, expected: unknown): boolean {
switch (op) {
case "eq":
return sameValue(actual, expected);
case "neq":
return !sameValue(actual, expected);
case "truthy":
return Boolean(actual);
case "gte":
case "lte":
case "gt":
case "lt": {
const a = toNumber(actual);
const b = toNumber(expected);
if (a == null || b == null) return false;
if (op === "gte") return a >= b;
if (op === "lte") return a <= b;
if (op === "gt") return a > b;
return a < b;
}
case "changed":
return false;
default:
return false;
}
}
function toNumber(v: unknown): number | null {
if (typeof v === "number" && Number.isFinite(v)) return v;
if (typeof v === "string" && v.trim() && !Number.isNaN(Number(v))) {
return Number(v);
}
return null;
}
function sameValue(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (a == null || b == null) return a == null && b == null;
if (typeof a === "number" || typeof b === "number") {
const na = toNumber(a);
const nb = toNumber(b);
return na != null && nb != null && na === nb;
}
return String(a) === String(b);
}
export type SideEffectTrigger = {
rule: SideEffectRule;
reason: "edge";
};
/**
* 对本轮 prev→now 快照算边沿;同 round 同 ruleId 最多一次。
* once已 fired 则跳过;触发后写入 nextFired。
* every_edge每次边沿都触发不记 fired。
*/
export function evaluateSideEffects(params: {
prev: TableDoc | null;
next: TableDoc;
rules: SideEffectRule[];
fired: FiredRegistry;
round?: number;
}): {
triggers: SideEffectTrigger[];
nextFired: FiredRegistry;
queuedWorkers: Array<{ workerId: string; ruleId: string; note?: string }>;
} {
const prevMap = tableValueMap(params.prev);
const nextMap = tableValueMap(params.next);
const nextFired: FiredRegistry = { ...params.fired };
const triggers: SideEffectTrigger[] = [];
const seen = new Set<string>();
const queuedWorkers: Array<{ workerId: string; ruleId: string; note?: string }> =
[];
for (const rule of params.rules) {
if (seen.has(rule.id)) continue;
if (rule.mode === "once" && nextFired[rule.id]) continue;
const was = conditionSatisfied(prevMap, rule, undefined);
const now = conditionSatisfied(nextMap, rule, prevMap);
// 边沿prev 不满足、now 满足changed 用 prev/now 值差)
const edge =
rule.op === "changed"
? conditionSatisfied(nextMap, rule, prevMap)
: !was && now;
if (!edge) continue;
seen.add(rule.id);
triggers.push({ rule, reason: "edge" });
if (rule.mode === "once") {
nextFired[rule.id] = {
at: new Date().toISOString(),
round: params.round,
};
}
if (rule.action.type === "queue_worker") {
queuedWorkers.push({
workerId: rule.action.workerId,
ruleId: rule.id,
note: rule.action.note,
});
}
}
return { triggers, nextFired, queuedWorkers };
}
export function parseFiredRegistry(raw: string | undefined | null): FiredRegistry {
if (!raw?.trim()) return {};
try {
const doc = JSON.parse(raw) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return {};
const out: FiredRegistry = {};
for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {
if (!k.trim()) continue;
if (v && typeof v === "object" && !Array.isArray(v)) {
const row = v as Record<string, unknown>;
out[k] = {
at: typeof row.at === "string" ? row.at : new Date().toISOString(),
round: typeof row.round === "number" ? row.round : undefined,
};
} else if (v === true) {
out[k] = { at: new Date().toISOString() };
}
}
return out;
} catch {
return {};
}
}
export function stringifyFiredRegistry(fired: FiredRegistry): string {
return JSON.stringify(fired, null, 2);
}
/** 执行 write_tag / replace_tagqueue_worker 只返回,由上层调度 */
export function applySideEffectTagActions(params: {
blackboard: Blackboard;
triggers: SideEffectTrigger[];
source?: string;
}): { writtenTags: string[] } {
const writtenTags: string[] = [];
const source = params.source ?? "system:table-side-effect";
for (const { rule } of params.triggers) {
const action = rule.action;
if (action.type !== "write_tag" && action.type !== "replace_tag") continue;
params.blackboard.write({
tag: action.tag,
content: action.content,
source,
});
writtenTags.push(action.tag);
}
return { writtenTags };
}

View File

@@ -1,11 +1,23 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import {
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import path from "node:path"; import path from "node:path";
import type { BookProject, BookSummary } from "../types/book.js"; import type { BookProject, BookSummary } from "../types/book.js";
import type { PersistedBookSession } from "../types/book-session.js";
import type { RunSnapshot } from "../types/run-snapshot.js";
import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js"; import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js";
import { deleteBookSession } from "./session-store.js"; import { deleteBookSession } from "./session-store.js";
import { deleteAllRunSnapshots } from "./run-snapshot-store.js"; import { deleteAllRunSnapshots } from "./run-snapshot-store.js";
const SESSION_FILENAME = "session.json";
function booksDir(): string { function booksDir(): string {
return path.join(getUserDataDir(), "books"); return path.join(getUserDataDir(), "books");
} }
@@ -14,6 +26,10 @@ function bookPath(id: string): string {
return path.join(booksDir(), `${id}.json`); return path.join(booksDir(), `${id}.json`);
} }
function bookDataDir(id: string): string {
return path.join(getUserDataDir(), "books", id);
}
export function listBooks(): BookSummary[] { export function listBooks(): BookSummary[] {
ensureUserDataDirs(); ensureUserDataDirs();
mkdirSync(booksDir(), { recursive: true }); mkdirSync(booksDir(), { recursive: true });
@@ -64,7 +80,7 @@ export function createBook(input: { title?: string }): BookProject {
const book: BookProject = { const book: BookProject = {
id: randomUUID(), id: randomUUID(),
title: input.title?.trim() || "未命名作品", title: input.title?.trim() || "未命名作品",
preview: "新建作品,选择 skill 包开始…", preview: "新建作品,描述你想创作什么…",
sessionIds: [], sessionIds: [],
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
@@ -84,6 +100,8 @@ export function updateBook(
| "activeSessionId" | "activeSessionId"
| "activeSkillId" | "activeSkillId"
| "activeSkillName" | "activeSkillName"
| "orchestratorId"
| "orchestratorName"
> >
>, >,
): BookProject { ): BookProject {
@@ -115,3 +133,69 @@ export function deleteBook(id: string): void {
/* ignore */ /* ignore */
} }
} }
/** 复制作品及其会话、游玩存档为新作品 */
export function duplicateBook(sourceId: string, title?: string): BookProject {
const source = getBook(sourceId);
if (!source) throw new Error("Book 不存在");
const newId = randomUUID();
const newSessionId = randomUUID();
const now = new Date().toISOString();
const dupTitle = title?.trim() || `${source.title} 副本`;
ensureUserDataDirs();
const sourceDir = bookDataDir(sourceId);
const destDir = bookDataDir(newId);
if (existsSync(sourceDir)) {
cpSync(sourceDir, destDir, { recursive: true });
} else {
mkdirSync(destDir, { recursive: true });
}
const sessionFile = path.join(destDir, SESSION_FILENAME);
let sessionIds: string[] = [];
let activeSessionId: string | undefined;
if (existsSync(sessionFile)) {
try {
const snap = JSON.parse(readFileSync(sessionFile, "utf8")) as PersistedBookSession;
snap.bookId = newId;
snap.sessionId = newSessionId;
snap.runtimeSession.id = newSessionId;
snap.savedAt = now;
writeFileSync(sessionFile, JSON.stringify(snap, null, 2), "utf8");
sessionIds = [newSessionId];
activeSessionId = newSessionId;
} catch {
/* ignore broken session */
}
}
const snapDir = path.join(destDir, "run-snapshots");
if (existsSync(snapDir)) {
for (const file of readdirSync(snapDir).filter((f) => f.endsWith(".json"))) {
try {
const full = path.join(snapDir, file);
const raw = JSON.parse(readFileSync(full, "utf8")) as RunSnapshot;
raw.bookId = newId;
writeFileSync(full, JSON.stringify(raw, null, 2), "utf8");
} catch {
/* skip */
}
}
}
const newBook: BookProject = {
...structuredClone(source),
id: newId,
title: dupTitle,
sessionIds,
activeSessionId,
createdAt: now,
updatedAt: now,
};
writeFileSync(bookPath(newId), JSON.stringify(newBook, null, 2), "utf8");
return newBook;
}

View File

@@ -22,10 +22,11 @@ function printHelp(): void {
console.log(`阶段机 + Skill 演示 console.log(`阶段机 + Skill 演示
启动流程: 启动流程:
1. /start → 列出 skills/ 下的 SKILL.md 1. /start → 自动进入默认 orchestratorworld-simulator
2. 输入 skill name 或编号(如 basic 或 1 2. 按启动询问描述创作需求
3. 按 SKILL.md「启动询问」回答 3. /decide worker design-intake approve → /approve → …
4. /decide worker outline-worker approve → /approve → …
Legacy/start-with world-simulator显式指定包
命令: 命令:
/start 开始会话 /start 开始会话

View File

@@ -0,0 +1,57 @@
import type { SkillIndexEntry, SkillStartupMode, ActiveSkillSnapshot } from "../types/runtime.js";
/** 新建作品时自动加载的能力库(用户不再选 skill 包) */
export const DEFAULT_ORCHESTRATOR_ID = "world-simulator";
/**
* 首屏引导兜底(包内 orchestrator.md `uiPrompt` 优先)。
* 词汇与 design-intake A1/A2 对齐:站位 + 系统扮演/输出/交互。
*/
export const DEFAULT_UI_PROMPT = `请用你自己的话描述想做什么——没有必填项,下面只是帮你找思路的提示。
【你扮演什么】(可对照,也可不按表)
· 单角代入:我就是一个固定角色
· 代理操控:我有角色,但常发 () 指令指挥
· 旁观/实验:我不扮演谁,看或记录推演
· 写手/统筹:我定方向,要成稿或助手式分段(长文 / 爽文也走这条)
· 多角切换:我轮流扮演不同身份
【系统要给你什么】(输出与交互,不是文风问卷)
· 回合对话:你一句,系统回一段可见结果
· 助手分段:先大纲/细纲,你再填表或改设定,再按章/段写正文
· 只要事实摘要 / 要可读叙事 / 要状态表…
【输入约定】可选用括号区分:
· () 圆括号:用户指令/要求,不可写成角色对白
· "" 双引号:角色在世界内说的话
· 【】方括号:角色在世界内的行动
未加标记时默认可视为世界内输入;语义明显是元话语按指令处理。
【核心体验】若愿意可带一句:你最想反复感到的是什么——没有也没关系,我会从描述里察觉。
示例丧尸世界但我不会被感染1v1 网恋;都市爽文先写大纲再按章开写;坠机求生;思想实验旁观三方选择……`;
export function resolveDefaultOrchestratorId(
available: SkillIndexEntry[],
): string {
if (available.some((s) => s.name === DEFAULT_ORCHESTRATOR_ID)) {
return DEFAULT_ORCHESTRATOR_ID;
}
if (available.length === 0) {
throw new Error("registry 中没有可用 orchestrator");
}
return available[0].name;
}
/** 兼容旧快照world-simulator 默认 agent-firstUI 引导 → 用户输入 → Agent 调 Skill */
export function effectiveStartupMode(
skill: Pick<ActiveSkillSnapshot, "name" | "startupMode">,
): SkillStartupMode {
if (skill.startupMode === "agent-first" || skill.startupMode === "intake") {
return skill.startupMode;
}
// 旧 frontmatter design-intake bootstrap 已废弃,等同 agent-first
if (skill.startupMode === "design-intake") return "agent-first";
if (skill.name === DEFAULT_ORCHESTRATOR_ID) return "agent-first";
return "intake";
}

View File

@@ -8,9 +8,15 @@ export type AppSettings = {
version: 1; version: 1;
activeProfileId: string | null; activeProfileId: string | null;
activePresetId: string | null; activePresetId: string | null;
/**
* 每个会话保留带全量上下文痕迹的消息条数(最新 N 条)。
* 更早的消息仍保留,但去掉 contextTrace 正文。默认 50 表示不存痕迹。
*/
contextTraceKeepLatest?: number;
}; };
const FILE_NAME = "settings.json"; const FILE_NAME = "settings.json";
const DEFAULT_CONTEXT_TRACE_KEEP = 5;
function settingsPath(): string { function settingsPath(): string {
return path.join(getUserDataDir(), FILE_NAME); return path.join(getUserDataDir(), FILE_NAME);
@@ -21,9 +27,16 @@ function defaultSettings(): AppSettings {
version: 1, version: 1,
activeProfileId: null, activeProfileId: null,
activePresetId: null, activePresetId: null,
contextTraceKeepLatest: DEFAULT_CONTEXT_TRACE_KEEP,
}; };
} }
export function normalizeContextTraceKeepLatest(raw: unknown): number {
const n = typeof raw === "number" ? raw : Number(raw);
if (!Number.isFinite(n) || n < 0) return DEFAULT_CONTEXT_TRACE_KEEP;
return Math.min(50, Math.floor(n));
}
export function loadAppSettings(): AppSettings { export function loadAppSettings(): AppSettings {
ensureUserDataDirs(); ensureUserDataDirs();
try { try {
@@ -34,6 +47,9 @@ export function loadAppSettings(): AppSettings {
version: 1, version: 1,
activeProfileId: parsed.activeProfileId ?? null, activeProfileId: parsed.activeProfileId ?? null,
activePresetId: parsed.activePresetId ?? null, activePresetId: parsed.activePresetId ?? null,
contextTraceKeepLatest: normalizeContextTraceKeepLatest(
parsed.contextTraceKeepLatest ?? DEFAULT_CONTEXT_TRACE_KEEP,
),
}; };
} catch { } catch {
return defaultSettings(); return defaultSettings();

View File

@@ -1,5 +1,6 @@
import type { LlmConfig } from "../config/env.js"; import type { LlmConfig } from "../config/env.js";
import type { GenerationParameters } from "../types/preset.js"; import type { GenerationParameters } from "../types/preset.js";
import { consumeOpenAiToolStream } from "./stream-complete.js";
export type ToolCallPayload = { export type ToolCallPayload = {
id: string; id: string;
@@ -71,21 +72,36 @@ export type CompleteWithToolsResult = {
model?: string; model?: string;
}; };
export type StreamCallbacks = {
onReasoningDelta?: (delta: string) => void;
onContentDelta?: (delta: string) => void;
};
export type LlmProvider = { export type LlmProvider = {
complete( complete(
messages: ChatMessage[], messages: ChatMessage[],
options?: CompleteOptions, options?: CompleteOptions,
): Promise<CompleteResult>; ): Promise<CompleteResult>;
completeStream?(
messages: ChatMessage[],
options?: CompleteOptions,
callbacks?: StreamCallbacks,
): Promise<CompleteResult>;
completeWithTools( completeWithTools(
messages: ChatMessage[], messages: ChatMessage[],
options: CompleteWithToolsOptions, options: CompleteWithToolsOptions,
): Promise<CompleteWithToolsResult>; ): Promise<CompleteWithToolsResult>;
completeWithToolsStream?(
messages: ChatMessage[],
options: CompleteWithToolsOptions,
callbacks: StreamCallbacks,
): Promise<CompleteWithToolsResult>;
}; };
function buildRequestBody( function buildRequestBody(
config: LlmConfig, config: LlmConfig,
messages: ChatMessage[], messages: ChatMessage[],
options?: CompleteOptions & { tools?: ToolDefinition[] }, options?: CompleteOptions & { tools?: ToolDefinition[]; stream?: boolean },
): Record<string, unknown> { ): Record<string, unknown> {
const gen = options?.generation ?? {}; const gen = options?.generation ?? {};
const body: Record<string, unknown> = { const body: Record<string, unknown> = {
@@ -125,6 +141,11 @@ function buildRequestBody(
body.response_format = { type: "json_object" }; body.response_format = { type: "json_object" };
} }
if (options?.stream) {
body.stream = true;
body.stream_options = { include_usage: true };
}
return body; return body;
} }
@@ -255,6 +276,44 @@ export class OpenAiCompatibleProvider implements LlmProvider {
}; };
} }
async completeStream(
messages: ChatMessage[],
options?: CompleteOptions,
callbacks: StreamCallbacks = {},
): Promise<CompleteResult> {
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
buildRequestBody(this.config, messages, { ...options, stream: true }),
),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`LLM request failed (${response.status}): ${body}`);
}
if (!response.body) {
throw new Error("LLM stream response has no body");
}
const parts = await consumeOpenAiToolStream(response.body, callbacks);
if (!parts.content && !parts.reasoning) {
throw new Error("LLM stream returned empty content");
}
return {
content: parts.content ?? parts.reasoning ?? "",
reasoning: parts.reasoning || undefined,
usage: parts.usage,
model: parts.model ?? this.config.model,
};
}
async completeWithTools( async completeWithTools(
messages: ChatMessage[], messages: ChatMessage[],
options: CompleteWithToolsOptions, options: CompleteWithToolsOptions,
@@ -296,6 +355,49 @@ export class OpenAiCompatibleProvider implements LlmProvider {
model: data.model ?? this.config.model, model: data.model ?? this.config.model,
}; };
} }
async completeWithToolsStream(
messages: ChatMessage[],
options: CompleteWithToolsOptions,
callbacks: StreamCallbacks,
): Promise<CompleteWithToolsResult> {
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
buildRequestBody(this.config, messages, {
...options,
tools: options.tools,
stream: true,
}),
),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`LLM request failed (${response.status}): ${body}`);
}
if (!response.body) {
throw new Error("LLM stream response has no body");
}
const parts = await consumeOpenAiToolStream(response.body, callbacks);
if (!parts.content && parts.toolCalls.length === 0 && !parts.reasoning) {
throw new Error("LLM stream returned empty content and no tool calls");
}
return {
content: parts.content,
toolCalls: parts.toolCalls,
reasoning: parts.reasoning || undefined,
usage: parts.usage,
model: parts.model ?? this.config.model,
};
}
} }
export type MockLlmStep = export type MockLlmStep =
@@ -341,6 +443,39 @@ export class MockLlmProvider implements LlmProvider {
}; };
} }
async completeStream(
_messages: ChatMessage[],
options?: CompleteOptions,
callbacks: StreamCallbacks = {},
): Promise<CompleteResult> {
const step = this.nextStep();
const response =
typeof step === "string"
? step
: (step.content ?? JSON.stringify({ action: "ask_user", reason: "mock" }));
const reasoning = "用户需要明确分工 → 调用 design-intake 产出 worker 集。";
for (const ch of reasoning) {
callbacks.onReasoningDelta?.(ch);
await new Promise((r) => setTimeout(r, 0));
}
for (const ch of response) {
callbacks.onContentDelta?.(ch);
await new Promise((r) => setTimeout(r, 0));
}
const approx = Math.max(1, Math.ceil(response.length / 4));
return {
content: response,
reasoning,
usage: {
promptTokens: approx,
completionTokens: approx,
totalTokens: approx * 2,
},
model: "mock",
...(options?.caller ? {} : {}),
};
}
async completeWithTools( async completeWithTools(
_messages: ChatMessage[], _messages: ChatMessage[],
_options: CompleteWithToolsOptions, _options: CompleteWithToolsOptions,
@@ -366,6 +501,20 @@ export class MockLlmProvider implements LlmProvider {
model: "mock", model: "mock",
}; };
} }
async completeWithToolsStream(
_messages: ChatMessage[],
_options: CompleteWithToolsOptions,
callbacks: StreamCallbacks,
): Promise<CompleteWithToolsResult> {
const reasoning =
"用户需要明确 Worker 分工 → 先读取黑板与 worker 列表 → 调用 design-intake 产出 worker 集。";
for (const ch of reasoning) {
callbacks.onReasoningDelta?.(ch);
await new Promise((r) => setTimeout(r, 0));
}
return this.completeWithTools(_messages, _options);
}
} }
export function createMockMainAgentResponse( export function createMockMainAgentResponse(

View File

@@ -7,6 +7,7 @@ import type {
CompleteWithToolsOptions, CompleteWithToolsOptions,
CompleteWithToolsResult, CompleteWithToolsResult,
LlmProvider, LlmProvider,
StreamCallbacks,
} from "./client.js"; } from "./client.js";
/** /**
@@ -55,4 +56,23 @@ export class PresetLlmProvider implements LlmProvider {
generation, generation,
}); });
} }
async completeWithToolsStream(
messages: ChatMessage[],
options: CompleteWithToolsOptions,
callbacks: StreamCallbacks,
): Promise<CompleteWithToolsResult> {
const preset = this.getPreset();
if (!this.inner.completeWithToolsStream) {
return this.completeWithTools(messages, options);
}
const presetMessages = preset ? assemblePresetMessages(preset) : [];
const merged = preset ? mergeMessages(presetMessages, messages) : messages;
const generation = options.generation ?? preset?.generation;
return this.inner.completeWithToolsStream(merged, {
...options,
generation,
}, callbacks);
}
} }

149
src/llm/stream-complete.ts Normal file
View File

@@ -0,0 +1,149 @@
import type {
ChatMessage,
CompleteWithToolsOptions,
CompleteWithToolsResult,
ParsedToolCall,
StreamCallbacks,
TokenUsage,
} from "./client.js";
import { parseUsage } from "./client.js";
type ToolCallAccumulator = Map<
number,
{ id?: string; name?: string; arguments: string }
>;
function applyToolCallDelta(
acc: ToolCallAccumulator,
raw: unknown,
): void {
if (!Array.isArray(raw)) return;
for (const item of raw) {
if (!item || typeof item !== "object") continue;
const row = item as Record<string, unknown>;
const index = Number(row.index ?? 0);
const entry = acc.get(index) ?? { arguments: "" };
if (typeof row.id === "string") entry.id = row.id;
const fn = row.function;
if (fn && typeof fn === "object") {
const f = fn as Record<string, unknown>;
if (typeof f.name === "string") entry.name = f.name;
if (typeof f.arguments === "string") entry.arguments += f.arguments;
}
acc.set(index, entry);
}
}
function toolCallsFromAccumulator(acc: ToolCallAccumulator): ParsedToolCall[] {
const out: ParsedToolCall[] = [];
for (const [, entry] of [...acc.entries()].sort((a, b) => a[0] - b[0])) {
const name = entry.name?.trim();
const id = entry.id?.trim();
if (!name || !id) continue;
out.push({ id, name, arguments: entry.arguments || "{}" });
}
return out;
}
/** 解析 OpenAI 兼容 SSE 流,累积 reasoning / content / tool_calls */
export async function consumeOpenAiToolStream(
body: ReadableStream<Uint8Array>,
callbacks: StreamCallbacks,
): Promise<{
content: string | null;
reasoning: string;
toolCalls: ParsedToolCall[];
usage?: TokenUsage;
model?: string;
}> {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let content = "";
let reasoning = "";
const toolAcc: ToolCallAccumulator = new Map();
let usage: TokenUsage | undefined;
let model: string | undefined;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
const payload = trimmed.slice(5).trim();
if (!payload || payload === "[DONE]") continue;
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(payload) as Record<string, unknown>;
} catch {
continue;
}
if (typeof parsed.model === "string") model = parsed.model;
const u = parseUsage(parsed.usage);
if (u) usage = u;
const choice = (parsed.choices as unknown[])?.[0];
if (!choice || typeof choice !== "object") continue;
const delta = (choice as Record<string, unknown>).delta;
if (!delta || typeof delta !== "object") continue;
const d = delta as Record<string, unknown>;
if (typeof d.reasoning_content === "string" && d.reasoning_content) {
reasoning += d.reasoning_content;
callbacks.onReasoningDelta?.(d.reasoning_content);
}
if (typeof d.content === "string" && d.content) {
content += d.content;
callbacks.onContentDelta?.(d.content);
}
if (d.tool_calls) applyToolCallDelta(toolAcc, d.tool_calls);
}
}
return {
content: content.trim() || null,
reasoning: reasoning.trim(),
toolCalls: toolCallsFromAccumulator(toolAcc),
usage,
model,
};
}
export type StreamableLlm = {
completeWithToolsStream?(
messages: ChatMessage[],
options: CompleteWithToolsOptions,
callbacks: StreamCallbacks,
): Promise<CompleteWithToolsResult>;
};
export function supportsToolStream(llm: unknown): llm is StreamableLlm {
return (
typeof llm === "object" &&
llm != null &&
typeof (llm as StreamableLlm).completeWithToolsStream === "function"
);
}
export type ContentStreamableLlm = {
completeStream?(
messages: ChatMessage[],
options?: import("./client.js").CompleteOptions,
callbacks?: StreamCallbacks,
): Promise<import("./client.js").CompleteResult>;
};
export function supportsContentStream(llm: unknown): llm is ContentStreamableLlm {
return (
typeof llm === "object" &&
llm != null &&
typeof (llm as ContentStreamableLlm).completeStream === "function"
);
}

View File

@@ -4,12 +4,17 @@ import type {
CompleteWithToolsOptions, CompleteWithToolsOptions,
CompleteWithToolsResult, CompleteWithToolsResult,
LlmProvider, LlmProvider,
StreamCallbacks,
} from "./client.js"; } from "./client.js";
import { import {
recordTokenUsage, recordTokenUsage,
toMessageTokenUsage, toMessageTokenUsage,
type MessageTokenUsage, type MessageTokenUsage,
} from "../stats/token-store.js"; } from "../stats/token-store.js";
import {
buildContextTrace,
type LlmContextTrace,
} from "../types/context-trace.js";
export type LlmTrackingContext = { export type LlmTrackingContext = {
sessionId?: string; sessionId?: string;
@@ -19,8 +24,23 @@ export type LlmTrackingContext = {
/** Set after each LLM call; consumed when the next system chat message is created */ /** Set after each LLM call; consumed when the next system chat message is created */
pendingUsage?: MessageTokenUsage; pendingUsage?: MessageTokenUsage;
pendingReasoning?: string; pendingReasoning?: string;
/** 全量请求上下文;挂到下一条「结果向」系统消息 */
pendingContextTrace?: LlmContextTrace;
}; };
function capturePendingTrace(
ctx: LlmTrackingContext,
messages: Array<{ role: string; content: string }>,
caller: string | undefined,
model?: string,
): void {
ctx.pendingContextTrace = buildContextTrace({
caller: caller ?? "unknown",
messages,
model,
});
}
export class TokenTrackingProvider implements LlmProvider { export class TokenTrackingProvider implements LlmProvider {
constructor( constructor(
private readonly inner: LlmProvider, private readonly inner: LlmProvider,
@@ -33,6 +53,53 @@ export class TokenTrackingProvider implements LlmProvider {
): Promise<CompleteResult> { ): Promise<CompleteResult> {
const result = await this.inner.complete(messages, options); const result = await this.inner.complete(messages, options);
const ctx = this.getContext(); const ctx = this.getContext();
capturePendingTrace(ctx, messages, options?.caller, result.model);
if (result.usage) {
const record = recordTokenUsage({
sessionId: ctx.sessionId,
bookId: ctx.bookId,
bookTitle: ctx.bookTitle,
orchestratorId: ctx.orchestratorId,
caller: options?.caller ?? "unknown",
model: result.model ?? "unknown",
promptTokens: result.usage.promptTokens,
completionTokens: result.usage.completionTokens,
totalTokens: result.usage.totalTokens,
cachedTokens: result.usage.cachedTokens,
cacheMissTokens: result.usage.cacheMissTokens,
});
ctx.pendingUsage = toMessageTokenUsage(record);
}
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
}
async completeStream(
messages: Parameters<LlmProvider["complete"]>[0],
options?: CompleteOptions,
callbacks: StreamCallbacks = {},
): Promise<CompleteResult> {
const inner = this.inner;
if (!inner.completeStream) {
const result = await this.complete(messages, options);
if (result.reasoning) callbacks.onReasoningDelta?.(result.reasoning);
if (result.content) callbacks.onContentDelta?.(result.content);
return result;
}
let reasoningBuf = "";
const result = await inner.completeStream(messages, options, {
onReasoningDelta: (delta) => {
reasoningBuf += delta;
const ctx = this.getContext();
ctx.pendingReasoning = reasoningBuf;
callbacks.onReasoningDelta?.(delta);
},
onContentDelta: callbacks.onContentDelta,
});
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options?.caller, result.model);
if (result.usage) { if (result.usage) {
const record = recordTokenUsage({ const record = recordTokenUsage({
sessionId: ctx.sessionId, sessionId: ctx.sessionId,
@@ -61,6 +128,50 @@ export class TokenTrackingProvider implements LlmProvider {
): Promise<CompleteWithToolsResult> { ): Promise<CompleteWithToolsResult> {
const result = await this.inner.completeWithTools(messages, options); const result = await this.inner.completeWithTools(messages, options);
const ctx = this.getContext(); const ctx = this.getContext();
capturePendingTrace(ctx, messages, options.caller, result.model);
if (result.usage) {
const record = recordTokenUsage({
sessionId: ctx.sessionId,
bookId: ctx.bookId,
bookTitle: ctx.bookTitle,
orchestratorId: ctx.orchestratorId,
caller: options.caller ?? "unknown",
model: result.model ?? "unknown",
promptTokens: result.usage.promptTokens,
completionTokens: result.usage.completionTokens,
totalTokens: result.usage.totalTokens,
cachedTokens: result.usage.cachedTokens,
cacheMissTokens: result.usage.cacheMissTokens,
});
ctx.pendingUsage = toMessageTokenUsage(record);
}
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
}
async completeWithToolsStream(
messages: Parameters<LlmProvider["completeWithTools"]>[0],
options: CompleteWithToolsOptions,
callbacks: StreamCallbacks,
): Promise<CompleteWithToolsResult> {
const inner = this.inner;
if (!inner.completeWithToolsStream) {
return this.completeWithTools(messages, options);
}
let reasoningBuf = "";
const result = await inner.completeWithToolsStream(messages, options, {
onReasoningDelta: (delta) => {
reasoningBuf += delta;
const ctx = this.getContext();
ctx.pendingReasoning = reasoningBuf;
callbacks.onReasoningDelta?.(delta);
},
onContentDelta: callbacks.onContentDelta,
});
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options.caller, result.model);
if (result.usage) { if (result.usage) {
const record = recordTokenUsage({ const record = recordTokenUsage({
sessionId: ctx.sessionId, sessionId: ctx.sessionId,

View File

@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { LlmProvider } from "../llm/client.js"; import type { LlmProvider } from "../llm/client.js";
import { normalizeQuestions } from "../skills/question-protocol.js";
import type { BlackboardTagIndex } from "../types/blackboard.js"; import type { BlackboardTagIndex } from "../types/blackboard.js";
import type { MainAgentDecision, RuntimeSession } from "../types/runtime.js"; import type { MainAgentDecision, RuntimeSession } from "../types/runtime.js";
import { runMainAgentToolLoop, type ToolLoopHandlers } from "./tool-loop.js"; import { runMainAgentToolLoop, type ToolLoopHandlers } from "./tool-loop.js";
@@ -22,27 +23,46 @@ function buildMainAgentSystemPrompt(
? workers.map((w) => `- ${w.id}${w.description}`).join("\n") ? workers.map((w) => `- ${w.id}${w.description}`).join("\n")
: "- (当前 skill 未加载 worker 列表)"; : "- (当前 skill 未加载 worker 列表)";
return `你是写作系统的总管Main Agent。你的职责是调度 worker而不是直接创作正文。 return `你是写作系统的导演Main Agent。你的职责是调度演员(worker,而不是直接创作正文。
规则: 规则:
1. 你不能直接生成小说/文章正文。 1. 你不能直接生成小说/文章正文。
2. 你不能修改运行状态statePatchAllowed 必须始终为 false。 2. 你不能修改运行状态statePatchAllowed 必须始终为 false。
3. 你只能建议下一步动作ask_user、run_worker、create_temp_worker、review_blackboard、finish。 3. 你只能建议下一步动作ask_user、run_worker、create_temp_worker、review_blackboard、finish。
4. 当信息不足时,使用 ask_user 向用户提问 4. 当信息不足时,使用 ask_userassessment 是主内容完备度评价questions 挂在其下且用户可跳过;一次 12 题
5. 当需要执行任务时,使用 run_worker只指定 workerId。不要指定 inputTags 或 outputTags——Runtime 从 Worker Skill 读取。 5. 当需要执行任务时,使用 run_worker只指定 workerId。不要指定 inputTags 或 outputTags——Runtime 从 Worker Skill 读取。
6. requiresApproval 表示运行 worker 前是否需要用户确认。代笔模式通常为 true。 6. requiresApproval 表示运行 worker 前是否需要用户确认。代笔模式通常为 true。
7. run_worker 可选 workerContext{ "roleId": "A" },用于 role-decide 等指定当前决策角色Runtime 写入 世界.当前角色.id 7. run_worker 可选 workerContext{ "roleId": "A" },用于 role-decide 等指定当前决策角色Runtime 写入 世界.当前角色.id
8. 你不能把未验收内容当作事实。 8. 你不能把未验收内容当作事实。
9. 向用户提问是 worker 的能力ask_user tool不是独立 worker。总管只在调度层提问。 9. 向用户提问是 worker 的能力ask_user tool不是独立 worker。导演只在调度层提问。
10. blackboardIndex 只有 tag 索引,不含正文 content。 10. blackboardIndex 只有 tag 索引,不含正文 content。
## 调度思维
用需求正推:「用户需要 [具体体验/能力] → 调用 [worker/skill] 来 [生成/补充/调整] [什么],以便更好满足用户。」
禁止否定式路由:「某模式 / 背景形态 → 不需要某步骤 / 跳过某 worker」。
反例(禁止):「背景为单一角色,无需世界构建」「不是规则怪谈,跳过 write-rules」「默认上世界模拟套件」
正例:「用户已选世界模拟器导演且要网恋对话 → design-flow 排出近期增量步骤(可调味、可后补)→ 用户认可后反复 design-step需要再补规则/实例 → 再 design-flow 追加同能力 → 收成后若需开局 → opening-generator用户手动进 play」
当前 skill 可用 workerworkerId 必须与下列 id 完全一致): 当前 skill 可用 workerworkerId 必须与下列 id 完全一致):
${workerLines} ${workerLines}
## design 优先顺序
- 尚无「设计.创作流程」→ **design-flow**(近期 horizonstatus=open未选导演则先请用户选
- 流程已验收且还有未完成步骤 → **design-step**(一次一步;能力由程序注入)
- 已列步骤都验收但流程仍 **status=open** → 再 **design-flow**(追加反复步或 closed
- 禁止调度已废弃的 design-core / design-fixed / design-worker / design-refine
- 终稿已 accept 且可用 opening-generator、尚无开场产物 → opening-generator
- 进 play 由用户手动决定
- **禁止**替用户猜测或改选导演
- **禁止**一次编排排死全程固定 DAG
输出必须是 JSON 对象,字段: 输出必须是 JSON 对象,字段:
{ {
"action": "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish", "action": "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish",
"reason": "string", "reason": "string",
"assessment": "string | null",
"questions": [{ "id": "q1", "prompt": "…", "options": [{ "label": "建议示范…" }] }] | null,
"workerId": "string | null", "workerId": "string | null",
"requiresApproval": boolean, "requiresApproval": boolean,
"workerContext": { "roleId": "string" } | null "workerContext": { "roleId": "string" } | null
@@ -109,7 +129,7 @@ export function buildMainAgentUserPrompt(context: MainAgentContext): string {
blackboardIndex, blackboardIndex,
availableWorkers, availableWorkers,
instruction: instruction:
"根据当前状态决定下一步。若 collecting_input 或 revision_requested优先理解用户最新输入。若 planning 且已有足够信息,建议 run_worker仅 workerId。", "根据当前状态决定下一步。尚无设计.创作流程 → design-flow有未完成步骤 → design-stepsteps 做完但 status=open → 再 design-flow禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。",
}, },
null, null,
2, 2,
@@ -160,10 +180,20 @@ export function parseMainAgentDecision(raw: string): MainAgentDecision {
if (roleId) workerContext = { roleId }; if (roleId) workerContext = { roleId };
} }
const assessmentRaw =
typeof obj.assessment === "string"
? obj.assessment.trim()
: typeof obj.message === "string"
? obj.message.trim()
: "";
const questions = normalizeQuestions(obj.questions);
return { return {
id: randomUUID(), id: randomUUID(),
action, action,
reason, reason,
assessment: assessmentRaw || undefined,
questions: questions.length ? questions : undefined,
workerId, workerId,
workerContext, workerContext,
requiresApproval: Boolean(obj.requiresApproval), requiresApproval: Boolean(obj.requiresApproval),

View File

@@ -1,4 +1,5 @@
import type { ChatMessage, LlmProvider, ParsedToolCall } from "../llm/client.js"; import type { ChatMessage, LlmProvider, ParsedToolCall, StreamCallbacks } from "../llm/client.js";
import { supportsToolStream } from "../llm/stream-complete.js";
import { toolCallToDecision, validateLoopToolCall } from "../runtime/tool-registry.js"; import { toolCallToDecision, validateLoopToolCall } from "../runtime/tool-registry.js";
import type { MainAgentDecision } from "../types/runtime.js"; import type { MainAgentDecision } from "../types/runtime.js";
import { isMainAgentTerminalTool } from "../types/tools.js"; import { isMainAgentTerminalTool } from "../types/tools.js";
@@ -17,6 +18,10 @@ export type ToolLoopHandlers = {
outputTags: string[]; outputTags: string[];
}>; }>;
onToolCall?: (name: string, detail: string) => void; onToolCall?: (name: string, detail: string) => void;
/** 思维链 / reasoning 流式增量(供 UI 实时展示) */
onThinkingDelta?: (delta: string) => void;
/** 一轮 LLM 调用结束后的完整思考文本 */
onThinkingDone?: (text: string) => void;
}; };
export type ToolLoopResult = { export type ToolLoopResult = {
@@ -37,20 +42,84 @@ function buildToolLoopSystemPrompt(
return `你是写作系统的总管Main Agent。你在 running 相位通过 **tool call** 推进流程。 return `你是写作系统的总管Main Agent。你在 running 相位通过 **tool call** 推进流程。
规则: ## 能力边界
1. 你不能直接生成小说/文章正文。 1. 你不能直接生成小说/文章正文。
2. 你不能修改运行状态。 2. 你不能修改运行状态。
3. 先用 read_blackboard / list_workers / list_artifacts 收集信息,再决定下一步。 3. 先用 read_blackboard / list_workers / list_artifacts 收集信息,再决定下一步。
4. 终止动作只能用 toolask_user、run_worker、review_blackboard、finish。 4. 终止动作只能用 toolask_user、run_worker、review_blackboard、finish。
- ask_user**主内容是 assessment内容完备度评价**questions 挂在其下且可被用户跳过。
· assessment必填给用户看的评价正文。以【核心体验】为首按需列维度内容/人物关系/感官/背景规则/意义主题…),不必凑齐。每维写完备度%、已知、待探;待探用【】标互斥或可组合方向。
· questions必填数组语义可选只针对 assessment「待探」里想确认的点一次 12 题。用户可不答、直接让你基于现有信息继续。prompt=明确题干options=24 条**建议示范**required 默认 false。
· reason调度短句正推勿塞长文。
· 能推断就别问;已写在 用户.需求 里的不要重复问。
5. run_worker 只传 workerIdinputTags/outputTags 由 Runtime 从 Worker Skill 读取。 5. run_worker 只传 workerIdinputTags/outputTags 由 Runtime 从 Worker Skill 读取。
6. requiresApproval=true 时 run_worker 需用户确认后再执行。 6. requiresApproval=true 时 run_worker 需用户确认后再执行。
7. run_worker 可选 roleId用于 role-decide 等指定当前决策角色 7. 不能把未验收产物当作已定事实
8. 不能把未验收产物当作已定事实。
## 调度思维(必须遵守)
用 **需求正推**,禁止 **模式否定式** 表述。
**要这样思考(正推):**
「用户需要 [具体体验/能力/产物] → 因此调用 [worker/skill] 来 [生成/补充/调整] [什么],以便更好满足用户。」
**不要这样思考(反例):**
「这是 xxx 模式 / 形态 → 不需要 xxx 步骤 / 跳过 xxx。」
「背景为单一角色,无需世界构建。」
「不是规则怪谈,跳过 write-rules。」
示例(好):
- 「用户已选导演且要 1v1 网恋 → design-flow 排出近期增量步骤(可调味)→ 认可后反复 design-step不够再追加。」
- 「尚无设计.创作流程 → run_worker(design-flow);有未完成步骤 → design-stepsteps 完但 status=open → 再 design-flow。」
- 「Worker 集已 accept 且声明需要开局 → 调用 opening-generator 写开场白。」
示例(坏):
- 「调度 design-core / design-fixed已废弃。」
- 「一次 design-flow 排死全程固定 DAG。」
- 「跳过流程编排直接写满 Worker 集。」
- 「默认先上世界模拟全套再问用户要什么。」
- 「替用户改选 / 猜测导演。」
在 reasoning / 可见 content 中请用 **正推句式** 写出调度理由;终止 tool 的 reason 字段同样用正推表述。
## design 阶段提示
- 尚无「设计.创作流程」→ **优先 design-flow**(近期 horizonstatus=open未选则先请用户选
- 流程已验收、还有未完成步骤 → **design-step**(一次一步)。
- 已列步骤全验收但 **status=open** → 再 **design-flow**(追加生成规则/具体实例等,或设 closed
- 禁止 run_worker(design-core|design-fixed|design-worker|design-refine)——已废弃。
- 不要重复问用户「想做什么」——意图已在 用户.需求。
- Worker 集已 accept且可用 worker 含 opening-generator尚无开场产物 → opening-generator。
- 进 play 由用户手动决定。
- 未声明开局、用户也未要求开场时,不要硬调 opening-generator。
- **禁止**替用户猜测或改选导演。
- **禁止**一次编排排死全程固定长链。
当前 skill 可用 worker 当前 skill 可用 worker
${workerLines} ${workerLines}
信息足够前可多次调用 read_blackboard;一旦调用终止 tool本轮循环结束。`; 信息不足时可多次 read_blackboard一旦调用终止 tool本轮循环结束。`;
}
async function completeToolsPreferStream(
llm: LlmProvider,
messages: ChatMessage[],
callbacks: StreamCallbacks,
): Promise<Awaited<ReturnType<LlmProvider["completeWithTools"]>>> {
if (supportsToolStream(llm)) {
return llm.completeWithToolsStream!(messages, {
tools: MAIN_AGENT_TOOL_DEFINITIONS,
caller: "main_agent",
}, callbacks);
}
const result = await llm.completeWithTools(messages, {
tools: MAIN_AGENT_TOOL_DEFINITIONS,
caller: "main_agent",
});
if (result.reasoning) {
callbacks.onReasoningDelta?.(result.reasoning);
}
if (result.content) {
callbacks.onContentDelta?.(result.content);
}
return result;
} }
function executeLoopTool( function executeLoopTool(
@@ -94,6 +163,16 @@ function assistantMessageFromToolCalls(
}; };
} }
function emitThinkingDone(handlers: ToolLoopHandlers, parts: {
reasoning?: string;
content?: string | null;
}): void {
const reasoning = parts.reasoning?.trim() ?? "";
const content = parts.content?.trim() ?? "";
const combined = [reasoning, content].filter(Boolean).join("\n\n").trim();
if (combined) handlers.onThinkingDone?.(combined);
}
/** /**
* 总管 tool loop在 running 相位内可多轮调用 read_blackboard 等, * 总管 tool loop在 running 相位内可多轮调用 read_blackboard 等,
* 直到调用终止 tool 并返回 MainAgentDecision。 * 直到调用终止 tool 并返回 MainAgentDecision。
@@ -112,12 +191,14 @@ export async function runMainAgentToolLoop(
]; ];
const toolTrace: string[] = []; const toolTrace: string[] = [];
const streamCallbacks: StreamCallbacks = {
onReasoningDelta: (delta) => handlers.onThinkingDelta?.(delta),
onContentDelta: (delta) => handlers.onThinkingDelta?.(delta),
};
for (let iteration = 1; iteration <= MAX_TOOL_LOOP_ITERATIONS; iteration++) { for (let iteration = 1; iteration <= MAX_TOOL_LOOP_ITERATIONS; iteration++) {
const result = await llm.completeWithTools(messages, { const result = await completeToolsPreferStream(llm, messages, streamCallbacks);
tools: MAIN_AGENT_TOOL_DEFINITIONS, emitThinkingDone(handlers, result);
caller: "main_agent",
});
if (result.toolCalls.length === 0) { if (result.toolCalls.length === 0) {
if (result.content?.trim()) { if (result.content?.trim()) {

View File

@@ -50,14 +50,80 @@ export const MAIN_AGENT_TOOL_DEFINITIONS: ToolDefinition[] = [
type: "function", type: "function",
function: { function: {
name: "ask_user", name: "ask_user",
description: "信息不足时向用户提问,将暂停 agent 循环等待用户输入。", description:
"向用户追问。assessment 是给用户看的主内容完备度评价questions 挂在其下,用户可跳过并请你基于现有信息继续。将暂停等待用户。",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
reason: { type: "string", description: "为何需要用户输入" }, reason: {
message: { type: "string", description: "展示给用户的问题或说明" }, type: "string",
description:
"调度层短理由(正推:用户需要澄清 X → 因此追问)。勿把长文评价写这里。",
},
assessment: {
type: "string",
description:
"给用户看的内容完备度评价Markdown。以【核心体验】为首要按需列维度不必凑齐每维完备度%、已知、待探。待探用【】标出互斥/可组合方向。评价只服务「用户想要什么」。",
},
message: {
type: "string",
description:
"兼容旧字段:等同 assessment。优先传 assessment。",
},
questions: {
type: "array",
description:
"挂在 assessment 下的可选追问(用户可不答)。基于「待探」出题,一次 12 题。prompt=明确选择题干options=建议示范(可改写后采用)。",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "稳定 id如 q1 / stance",
},
prompt: {
type: "string",
description:
"题干:引导用户选定一种详细体验倾向(例:「你更倾向于哪种皇帝的享受?」)。",
},
options: {
type: "array",
description:
"24 个建议示范。label 写完整可采纳文案(可先一句场景钩子再点题),不是标签词。",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "A/B/C…",
},
label: {
type: "string",
description:
"建议正文:用户点选后可直接当答案,也可改写。例:「冰冷的、主宰一切的权力感——九重宫阙一言定生死…」",
},
editable: {
type: "boolean",
description: "默认 true点文案可改写点字母才选中",
},
},
required: ["label"],
},
},
allowOther: {
type: "boolean",
description: "默认 true允许 Other 自拟",
},
required: {
type: "boolean",
description: "默认 false可选追问仅关键阻塞题才 true",
},
},
required: ["prompt", "options"],
},
},
}, },
required: ["reason"], required: ["reason", "assessment", "questions"],
additionalProperties: false, additionalProperties: false,
}, },
}, },

View File

@@ -1,46 +1,167 @@
import type { PresetPackage, PresetPromptRole } from "../types/preset.js"; import type {
PresetPackage,
PresetPromptEntry,
PresetPromptRole,
} from "../types/preset.js";
import { savePreset } from "./store.js";
export type PresetEnabledEntryView = { export type PresetEntryView = {
orderIndex: number; orderIndex: number;
id: string; id: string;
name: string; name: string;
role: PresetPromptRole; role: PresetPromptRole;
marker: boolean; marker: boolean;
content: string; content: string;
/** 实际会注入 LLM 请求(有非空 content */ /** prompt_order 与条目自身均启用 */
enabled: boolean;
orderEnabled: boolean;
entryEnabled: boolean;
/** 启用且有非空 content → 会注入 LLM */
willInject: boolean; willInject: boolean;
}; };
/** 按 prompt_order 列出所有启用条目及其内容 */ /** @deprecated 名称保留:现为全部条目;过滤启用请用 filter */
export function listEnabledPresetEntries( export type PresetEnabledEntryView = PresetEntryView;
preset: PresetPackage,
): PresetEnabledEntryView[] { function sortOrder(preset: PresetPackage) {
return [...preset.promptOrder].sort((a, b) => a.orderIndex - b.orderIndex);
}
function toView(
orderIndex: number,
entry: PresetPromptEntry,
orderEnabled: boolean,
): PresetEntryView {
const content = entry.content ?? "";
const enabled = orderEnabled && entry.enabled;
const trimmed = content.trim();
return {
orderIndex,
id: entry.id,
name: entry.name,
role: entry.role,
marker: entry.marker,
content,
enabled,
orderEnabled,
entryEnabled: entry.enabled,
willInject: enabled && trimmed.length > 0,
};
}
/** 按 prompt_order 列出全部条目(含未启用),便于设置页编辑 */
export function listAllPresetEntries(preset: PresetPackage): PresetEntryView[] {
const promptById = new Map(preset.prompts.map((p) => [p.id, p])); const promptById = new Map(preset.prompts.map((p) => [p.id, p]));
const ordered = [...preset.promptOrder].sort( const seen = new Set<string>();
(a, b) => a.orderIndex - b.orderIndex, const entries: PresetEntryView[] = [];
);
const entries: PresetEnabledEntryView[] = [];
for (const orderItem of ordered) { for (const orderItem of sortOrder(preset)) {
if (!orderItem.enabled) continue;
const entry = promptById.get(orderItem.promptId); const entry = promptById.get(orderItem.promptId);
if (!entry || !entry.enabled) continue; if (!entry) continue;
seen.add(entry.id);
entries.push(toView(orderItem.orderIndex, entry, orderItem.enabled));
}
const content = entry.content.trim(); // 未进 order 的条目附在末尾
entries.push({ let nextIndex =
orderIndex: orderItem.orderIndex, entries.reduce((m, e) => Math.max(m, e.orderIndex), -1) + 1;
id: entry.id, for (const entry of preset.prompts) {
name: entry.name, if (seen.has(entry.id)) continue;
role: entry.role, entries.push(toView(nextIndex++, entry, false));
marker: entry.marker,
content,
willInject: content.length > 0,
});
} }
return entries; return entries;
} }
export function countInjectingEntries(entries: PresetEnabledEntryView[]): number { /** 仅启用中的条目(装配 / 旧 API 兼容) */
export function listEnabledPresetEntries(
preset: PresetPackage,
): PresetEntryView[] {
return listAllPresetEntries(preset).filter((e) => e.enabled);
}
export function countInjectingEntries(
entries: Array<{ willInject: boolean }>,
): number {
return entries.filter((e) => e.willInject).length; return entries.filter((e) => e.willInject).length;
} }
export function countEnabledEntries(
entries: Array<{ enabled: boolean }>,
): number {
return entries.filter((e) => e.enabled).length;
}
export type PresetEntryPatch = {
id: string;
enabled?: boolean;
content?: string;
name?: string;
role?: PresetPromptRole;
};
/**
* 纯函数更新(不写盘)。启用状态同步 order + entry。
*/
export function applyPresetEntryPatches(
preset: PresetPackage,
patches: PresetEntryPatch[],
): PresetPackage {
if (!patches.length) return preset;
const prompts = preset.prompts.map((p) => ({ ...p }));
const promptById = new Map(prompts.map((p) => [p.id, p]));
let order = preset.promptOrder.map((o) => ({ ...o }));
for (const patch of patches) {
const entry = promptById.get(patch.id);
if (!entry) {
throw new Error(`条目不存在: ${patch.id}`);
}
if (typeof patch.content === "string") {
entry.content = patch.content;
if (entry.content.trim()) entry.marker = false;
}
if (typeof patch.name === "string" && patch.name.trim()) {
entry.name = patch.name.trim();
}
if (
patch.role === "system" ||
patch.role === "user" ||
patch.role === "assistant"
) {
entry.role = patch.role;
}
if (typeof patch.enabled === "boolean") {
entry.enabled = patch.enabled;
const orderItem = order.find((o) => o.promptId === patch.id);
if (orderItem) {
orderItem.enabled = patch.enabled;
} else if (patch.enabled) {
const orderIndex =
order.reduce((m, o) => Math.max(m, o.orderIndex), -1) + 1;
order.push({
promptId: patch.id,
enabled: true,
orderIndex,
});
}
}
}
return {
...preset,
prompts,
promptOrder: order,
};
}
/**
* 更新一条或多条并落盘。
*/
export function patchPresetEntries(
preset: PresetPackage,
patches: PresetEntryPatch[],
): PresetPackage {
return savePreset(applyPresetEntryPatches(preset, patches));
}

View File

@@ -0,0 +1,235 @@
import type { Blackboard } from "../blackboard/blackboard.js";
/** 下一 worker / 总管可读的定稿摘要 tag */
export const CONTEXT_BRIEF_TAG = "上下文.定稿摘要";
export type CompressWorkerResult = {
archivedTags: string[];
finals: Array<{ tag: string; chars: number; preview: string }>;
briefText: string;
};
const ALWAYS_ARCHIVE_AFTER_ACCEPT = ["用户.worker答复", "用户.修订说明"];
/**
* Worker 产物经用户验收(或自动完工)后:
* - 终产物打 final
* - 过程/草稿 tag 归档(不再进入后续 worker 取数)
* - 写入定稿摘要,供下一阶段「指点」用
*/
export function compressAfterWorkerAccept(params: {
blackboard: Blackboard;
workerId: string;
outputTags: string[];
summary?: string;
/**
* final = 终稿验收(归档草稿、写定稿摘要)
* unit = 创作单位验收(只归档过程答复,保留 设计.worker集.草稿)
*/
mode?: "final" | "unit";
}): CompressWorkerResult {
const { blackboard, workerId, outputTags, summary } = params;
const mode = params.mode ?? "final";
const finals: CompressWorkerResult["finals"] = [];
const archivedTags: string[] = [];
const uniqueOutputs = [...new Set(outputTags.map((t) => t.trim()).filter(Boolean))];
if (mode === "final") {
for (const tag of uniqueOutputs) {
const content = blackboard.getContentByTag(tag);
if (content == null) continue;
blackboard.write({
tag,
content,
source: workerId,
metadata: {
role: "final",
acceptedAt: new Date().toISOString(),
workerId,
},
});
finals.push({
tag,
chars: content.length,
preview: previewText(content, 120),
});
}
} else {
// 单位验收:草稿保持活跃,终产物列表仅作面板提示
for (const tag of uniqueOutputs) {
if (tag === "用户.需求" || tag === "创作.当前单位") continue;
const content = blackboard.getContentByTag(tag);
if (content == null) continue;
finals.push({
tag,
chars: content.length,
preview: previewText(content, 120),
});
}
}
const toArchive = new Set<string>(ALWAYS_ARCHIVE_AFTER_ACCEPT);
if (mode === "final") {
if (blackboard.getContentByTag("设计.worker集")?.trim()) {
toArchive.add("设计.worker集.草稿");
}
for (const tag of uniqueOutputs) {
if (tag.endsWith(".草稿")) continue;
const draft = `${tag}.草稿`;
if (blackboard.getContentByTag(draft) != null) toArchive.add(draft);
}
}
for (const tag of toArchive) {
if (uniqueOutputs.includes(tag) && mode === "final") continue;
if (mode === "unit" && tag.endsWith(".草稿")) continue;
const content = blackboard.getContentByTag(tag);
if (content == null) continue;
blackboard.write({
tag,
content: `(已压缩归档)原过程内容已折叠。定稿见:${
finals.map((f) => f.tag).join("、") || "(无)"
}\n\n---\n${previewText(content, 400)}`,
source: "compress",
metadata: {
role: "archived",
archivedAt: new Date().toISOString(),
fromWorker: workerId,
compressMode: mode,
},
});
archivedTags.push(tag);
}
const briefText =
mode === "unit"
? buildUnitBriefText({ workerId, summary, finals, archivedTags })
: buildBriefText({
workerId,
summary,
finals,
archivedTags,
});
if (mode === "final") {
blackboard.write({
tag: CONTEXT_BRIEF_TAG,
content: briefText,
source: "compress",
metadata: { role: "final", workerId },
});
}
return { archivedTags, finals, briefText };
}
function buildUnitBriefText(params: {
workerId: string;
summary?: string;
finals: CompressWorkerResult["finals"];
archivedTags: string[];
}): string {
return [
`## 创作单位已验收(${params.workerId}`,
"",
params.summary?.trim() ? `摘要:${params.summary.trim()}` : "摘要:(无)",
"",
"草稿仍保留在 `设计.worker集.草稿`;下一单位继续增量,勿依赖已删的过程对话。",
].join("\n");
}
function buildBriefText(params: {
workerId: string;
summary?: string;
finals: CompressWorkerResult["finals"];
archivedTags: string[];
}): string {
const lines = [
`## 上一阶段定稿(${params.workerId}`,
"",
params.summary?.trim()
? `摘要:${params.summary.trim()}`
: "摘要:(无)",
"",
"### 保留的终产物 tag",
];
if (params.finals.length === 0) {
lines.push("- (无)");
} else {
for (const f of params.finals) {
lines.push(`- \`${f.tag}\`${f.chars} 字)`);
lines.push(` ${f.preview}`);
}
}
if (params.archivedTags.length) {
lines.push("", "### 已压缩的过程 tag");
for (const t of params.archivedTags) {
lines.push(`- \`${t}\`(归档,后续 worker 默认不读)`);
}
}
lines.push(
"",
"下一 worker 应以以上终产物为准继续;勿依赖已归档的过程讨论。",
);
return lines.join("\n");
}
export function previewText(content: string, max: number): string {
const t = content.replace(/\s+/g, " ").trim();
if (t.length <= max) return t;
return `${t.slice(0, max)}`;
}
/** 供 UI黑板可读条目默认隐藏 archived 全文,只给索引) */
export function buildBoardPanel(blackboard: Blackboard): {
finals: Array<{ tag: string; source: string; preview: string; updatedAt: string }>;
active: Array<{ tag: string; source: string; preview: string; updatedAt: string }>;
archivedCount: number;
brief?: string;
} {
const items = latestItemsByTag(blackboard.exportItems());
const finals: Array<{
tag: string;
source: string;
preview: string;
updatedAt: string;
}> = [];
const active: typeof finals = [];
let archivedCount = 0;
let brief: string | undefined;
for (const item of items.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))) {
const role = String(item.metadata?.role ?? "");
if (item.tag === CONTEXT_BRIEF_TAG) {
brief = item.content;
continue;
}
if (role === "archived") {
archivedCount += 1;
continue;
}
const row = {
tag: item.tag,
source: item.source,
preview: previewText(item.content, 160),
updatedAt: item.updatedAt,
};
if (role === "final") finals.push(row);
else active.push(row);
}
return { finals, active, archivedCount, brief };
}
function latestItemsByTag(
items: import("../types/blackboard.js").BlackboardItem[],
): import("../types/blackboard.js").BlackboardItem[] {
const byTag = new Map<string, (typeof items)[0]>();
for (const item of items) {
const prev = byTag.get(item.tag);
if (!prev || item.updatedAt > prev.updatedAt) byTag.set(item.tag, item);
}
return [...byTag.values()];
}

View File

@@ -60,7 +60,7 @@ export class RuntimeOrchestrator {
payload: { payload: {
presetId: this.session.presetId, presetId: this.session.presetId,
flowId: this.session.flowId, flowId: this.session.flowId,
availableSkills: [{ name: "basic", description: "fallback", category: "novel" }], availableSkills: [{ name: "world-simulator", description: "fallback", category: "dialogue" }],
}, },
}); });
return this.session; return this.session;

View File

@@ -19,22 +19,56 @@ import type {
WaitingReason, WaitingReason,
} from "../types/runtime.js"; } from "../types/runtime.js";
import type { ActiveSkillSnapshot } from "../types/runtime.js"; import type { ActiveSkillSnapshot } from "../types/runtime.js";
import { effectiveStartupMode } from "../config/default-orchestrator.js";
import { import {
buildIntakeProgress, buildIntakeProgress,
buildIntakeFollowUpMessage, buildIntakeFollowUpMessage,
readIntakeValues, readIntakeValues,
synthesizeDemandText, synthesizeDemandText,
} from "../intake/intake.js"; } from "../intake/intake.js";
import { normalizeQuestions } from "../skills/question-protocol.js";
import type { QuestionItem } from "../types/questions.js";
function nowIso(): string { function nowIso(): string {
return new Date().toISOString(); return new Date().toISOString();
} }
/** 挂在产物下的追问一律可选required=false用户可直接 Accept */
function asOptionalSidecarQuestions(
raw: QuestionItem[] | string[] | undefined,
): QuestionItem[] | undefined {
const normalized = normalizeQuestions(raw ?? []);
if (!normalized.length) return undefined;
return normalized.map((q) => ({ ...q, required: false }));
}
/** 更新 updatedAt 时间戳 */ /** 更新 updatedAt 时间戳 */
function touch(session: RuntimeSession): RuntimeSession { function touch(session: RuntimeSession): RuntimeSession {
return { ...session, updatedAt: nowIso() }; return { ...session, updatedAt: nowIso() };
} }
/** 用户每条输入写入 用户.最新输入;首句同时写入需求 tag */
function applyUserTextToSlots(
slots: Record<string, unknown>,
text: string,
demandKey: string,
opts: { initial?: boolean },
): void {
if (!text) return;
slots["用户.最新输入"] = text;
if (opts.initial) {
slots[demandKey] = text;
slots.startupCompleted = true;
return;
}
if (!slots.startupCompleted) {
slots[demandKey] = text;
slots.startupCompleted = true;
return;
}
mergeSlotText(slots, demandKey, text);
}
/** 将用户补充文本追加到 slot用于合并多轮 ask_user / worker 答复到需求 tag */ /** 将用户补充文本追加到 slot用于合并多轮 ask_user / worker 答复到需求 tag */
function mergeSlotText( function mergeSlotText(
slots: Record<string, unknown>, slots: Record<string, unknown>,
@@ -136,6 +170,7 @@ export function getAllowedEvents(
"user_accepted_artifact", "user_accepted_artifact",
"user_rejected_artifact", "user_rejected_artifact",
"user_requested_revision", "user_requested_revision",
"user_resolved_sidecar_questions",
"main_agent_decision_created", "main_agent_decision_created",
"runtime_failed", "runtime_failed",
]; ];
@@ -179,6 +214,11 @@ export function canApplyEvent(
case "user_accepted_artifact": case "user_accepted_artifact":
case "user_rejected_artifact": case "user_rejected_artifact":
return reason?.kind === "review_artifact"; return reason?.kind === "review_artifact";
case "user_resolved_sidecar_questions":
return (
reason?.kind === "review_artifact" &&
Boolean(reason.questions?.length)
);
case "user_requested_revision": case "user_requested_revision":
return ( return (
reason?.kind === "review_artifact" || reason?.kind === "approve_step" reason?.kind === "review_artifact" || reason?.kind === "approve_step"
@@ -272,7 +312,7 @@ export function applyEvent(
} }
switch (event.type) { switch (event.type) {
// ── 启动:选 skill ── // ── 启动:默认 orchestrator 或 legacy 选包 ──
case "session_started": { case "session_started": {
const next = appendHistory( const next = appendHistory(
{ {
@@ -283,40 +323,22 @@ export function applyEvent(
event, event,
); );
const skills = event.payload.availableSkills; const skills = event.payload.availableSkills;
const initialSkill = event.payload.initialSkill;
if (initialSkill) {
return applySkillBinding(next, initialSkill, event);
}
return { return {
session: waiting(next, { session: waiting(next, {
kind: "skill_selection", kind: "skill_selection",
availableSkills: skills, availableSkills: skills,
}), }),
effects: [ effects: [{ type: "emit_message", message: formatSkillSelectionPrompt(skills) }],
{
type: "emit_message",
message: formatSkillSelectionPrompt(skills),
},
],
}; };
} }
case "skill_selected": { case "skill_selected": {
const { skill } = event.payload; const { skill } = event.payload;
const next = appendHistory( return applySkillBinding(session, skill, event);
{
...session,
flowId: skill.defaultFlowId ?? session.flowId,
slots: {
...session.slots,
activeSkill: skill,
},
},
event,
);
return {
session: waiting(next, {
kind: "intake",
prompt: skill.startupPrompt,
}),
effects: [{ type: "emit_message", message: skill.startupPrompt }],
};
} }
case "user_confirmed_intake": { case "user_confirmed_intake": {
@@ -366,6 +388,21 @@ export function applyEvent(
if (session.waitingReason?.kind === "intake") { if (session.waitingReason?.kind === "intake") {
const skill = activeSkill as ActiveSkillSnapshot | undefined; const skill = activeSkill as ActiveSkillSnapshot | undefined;
if (skill && effectiveStartupMode(skill) === "agent-first" && text) {
const key = skill.startupTargetKey || "用户.需求";
applyUserTextToSlots(slots, text, key, { initial: true });
slots.lastUserInput = text;
const next = appendHistory(
{ ...session, slots, resumeContext: undefined },
event,
);
return {
session: touch(running(next)),
effects: [{ type: "invoke_main_agent" }],
};
}
const intakeValues = const intakeValues =
event.payload.intakeValues ?? event.payload.intakeValues ??
readIntakeValues(session.slots); readIntakeValues(session.slots);
@@ -419,16 +456,17 @@ export function applyEvent(
if (session.waitingReason?.kind === "worker_questions") { if (session.waitingReason?.kind === "worker_questions") {
slots["用户.worker答复"] = text; slots["用户.worker答复"] = text;
if (text) slots["用户.最新输入"] = text;
if (demandKey && text) { if (demandKey && text) {
mergeSlotText(slots, demandKey, text); mergeSlotText(slots, demandKey, text);
} }
} else if (demandKey && session.waitingReason?.kind === "input" && text) { } else if (session.waitingReason?.kind === "input" && text) {
if (!session.slots.startupCompleted) { const key = demandKey || "用户.需求";
slots[demandKey] = text; applyUserTextToSlots(slots, text, key, {
slots.startupCompleted = true; initial: !session.slots.startupCompleted,
} else { });
mergeSlotText(slots, demandKey, text); } else if (text) {
} slots["用户.最新输入"] = text;
} }
const next = appendHistory( const next = appendHistory(
{ {
@@ -465,14 +503,44 @@ export function applyEvent(
switch (decision.action) { switch (decision.action) {
case "ask_user": case "ask_user":
case "review_blackboard": case "review_blackboard": {
const questions = decision.questions?.length
? decision.questions.map((q) => ({ ...q, required: false }))
: undefined;
const assessment =
decision.action === "ask_user"
? decision.assessment?.trim() || undefined
: undefined;
const effects: PhaseEffect[] = [];
if (assessment) {
effects.push({
type: "emit_message",
message: `[Agent] 内容评价:\n${assessment}`,
});
}
if (questions?.length) {
effects.push({
type: "emit_message",
message: `[Agent] 可选追问(可跳过):\n${questions.map((q) => `- ${q.prompt}`).join("\n")}`,
});
}
const inputMessage = assessment
? assessment
: decision.action === "review_blackboard" || !questions?.length
? decision.reason
: undefined;
return { return {
session: waiting( session: waiting(
{ ...next, pendingDecision: undefined }, { ...next, pendingDecision: undefined },
{ kind: "input", message: decision.reason }, {
kind: "input",
message: inputMessage,
questions,
},
), ),
effects: [], effects,
}; };
}
case "finish": case "finish":
return { return {
session: touch({ session: touch({
@@ -580,13 +648,12 @@ export function applyEvent(
} }
case "worker_needs_input": { case "worker_needs_input": {
const questions = event.payload.questions let normalized = normalizeQuestions(event.payload.questions);
.map((q) => q.trim()) if (normalized.length === 0) {
.filter(Boolean); normalized = normalizeQuestions([
const normalized = "请补充当前步骤所需的信息(情境、参数或你的具体设想)。",
questions.length > 0 ]);
? questions }
: ["请补充当前步骤所需的信息(情境、参数或你的具体设想)。"];
const ctx: ResumeContext = { const ctx: ResumeContext = {
workerId: event.payload.workerId, workerId: event.payload.workerId,
stepId: event.payload.stepId ?? session.currentStepId, stepId: event.payload.stepId ?? session.currentStepId,
@@ -608,7 +675,7 @@ export function applyEvent(
effects: [ effects: [
{ {
type: "emit_message", type: "emit_message",
message: `[Worker] ${event.payload.workerId} 提问:\n${normalized.map((q) => `- ${q}`).join("\n")}`, message: `[Worker] ${event.payload.workerId} 提问:\n${normalized.map((q) => `- ${q.prompt}`).join("\n")}`,
}, },
], ],
}; };
@@ -618,23 +685,64 @@ export function applyEvent(
const result = handleWorkerCompleted(session, event); const result = handleWorkerCompleted(session, event);
const mode = session.acceptanceMode ?? "user_confirmed"; const mode = session.acceptanceMode ?? "user_confirmed";
if (mode === "user_confirmed" && result.session.pendingArtifactId) { if (mode === "user_confirmed" && result.session.pendingArtifactId) {
const sidecar = asOptionalSidecarQuestions(event.payload.questions);
const effects = [...result.effects];
if (sidecar?.length) {
effects.push({
type: "emit_message",
message: `[Worker] 可选追问(可跳过,直接接受目前产物):\n${sidecar.map((q) => `- ${q.prompt}`).join("\n")}`,
});
}
return { return {
...result, ...result,
effects,
session: waiting(result.session, { session: waiting(result.session, {
kind: "review_artifact", kind: "review_artifact",
artifactId: result.session.pendingArtifactId, artifactId: result.session.pendingArtifactId,
questions: sidecar,
}), }),
}; };
} }
return result; return result;
} }
case "user_resolved_sidecar_questions": {
const reason = session.waitingReason;
if (reason?.kind !== "review_artifact") {
return fail(session, "sidecar questions require review_artifact");
}
const answersText = event.payload.answersText?.trim();
const slots: Record<string, unknown> = { ...session.slots };
const effects: PhaseEffect[] = [];
if (answersText) {
slots["用户.worker答复"] = answersText;
slots["用户.最新输入"] = answersText;
effects.push({
type: "emit_message",
message: `[用户] 已补充可选追问(产物仍待验收)`,
});
}
const next = appendHistory({ ...session, slots }, event);
return {
session: waiting(touch(next), {
kind: "review_artifact",
artifactId: reason.artifactId,
}),
effects,
};
}
// ── 产物验收 ── // ── 产物验收 ──
case "user_accepted_artifact": { case "user_accepted_artifact": {
const artifact = findArtifact(session, event.payload.artifactId); const artifact = findArtifact(session, event.payload.artifactId);
if (!artifact) { if (!artifact) {
return fail(session, `Artifact not found: ${event.payload.artifactId}`); return fail(session, `Artifact not found: ${event.payload.artifactId}`);
} }
const slots: Record<string, unknown> = { ...session.slots };
// 仅终稿 tag「设计.worker集」表示完整 Worker 集验收;草稿单位验收不得开 play
if (artifact.outputTags.some((tag) => tag === "设计.worker集")) {
slots.designInstanceReady = true;
}
const next = appendHistory( const next = appendHistory(
updateArtifact(session, artifact.id, { status: "accepted" }), updateArtifact(session, artifact.id, { status: "accepted" }),
event, event,
@@ -643,6 +751,7 @@ export function applyEvent(
session: touch( session: touch(
running({ running({
...next, ...next,
slots,
pendingArtifactId: undefined, pendingArtifactId: undefined,
pendingDecision: undefined, pendingDecision: undefined,
}), }),
@@ -791,3 +900,62 @@ function formatSkillSelectionPrompt(
const lines = skills.map((s, i) => ` ${i + 1}. ${s.name}${s.description}`); const lines = skills.map((s, i) => ` ${i + 1}. ${s.name}${s.description}`);
return ["请选择创作 skill输入 name 或编号):", ...lines].join("\n"); return ["请选择创作 skill输入 name 或编号):", ...lines].join("\n");
} }
/** agent-first仅 UI 引导,等用户首句后再 invoke 总管 */
function enterAwaitFirstInput(
session: RuntimeSession,
skill: ActiveSkillSnapshot,
event: RuntimeEvent,
): ApplyEventResult {
const next = appendHistory(
{
...session,
flowId: skill.defaultFlowId ?? session.flowId,
slots: {
...session.slots,
activeSkill: skill,
},
},
event,
);
return {
session: waiting(next, { kind: "input" }),
effects: [],
};
}
function enterIntakeWaiting(
session: RuntimeSession,
skill: ActiveSkillSnapshot,
event: RuntimeEvent,
): ApplyEventResult {
const next = appendHistory(
{
...session,
flowId: skill.defaultFlowId ?? session.flowId,
slots: {
...session.slots,
activeSkill: skill,
},
},
event,
);
return {
session: waiting(next, {
kind: "intake",
prompt: skill.startupPrompt,
}),
effects: [{ type: "emit_message", message: skill.startupPrompt }],
};
}
function applySkillBinding(
session: RuntimeSession,
skill: ActiveSkillSnapshot,
event: RuntimeEvent,
): ApplyEventResult {
if (effectiveStartupMode(skill) === "agent-first") {
return enterAwaitFirstInput(session, skill, event);
}
return enterIntakeWaiting(session, skill, event);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { ParsedToolCall } from "../llm/client.js"; import type { ParsedToolCall } from "../llm/client.js";
import type { MainAgentDecision } from "../types/runtime.js"; import type { MainAgentDecision } from "../types/runtime.js";
import { normalizeQuestions } from "../skills/question-protocol.js";
import { import {
isMainAgentLoopTool, isMainAgentLoopTool,
isMainAgentTerminalTool, isMainAgentTerminalTool,
@@ -60,11 +61,15 @@ export function toolCallToDecision(call: ParsedToolCall): MainAgentDecision {
switch (name as MainAgentToolName) { switch (name as MainAgentToolName) {
case "ask_user": { case "ask_user": {
const reason = requireString(args, "reason"); const reason = requireString(args, "reason");
const message = optionalString(args, "message"); const assessment =
optionalString(args, "assessment") ?? optionalString(args, "message");
const questions = normalizeQuestions(args.questions);
return { return {
id: randomUUID(), id: randomUUID(),
action: "ask_user", action: "ask_user",
reason: message ? `${reason}\n${message}` : reason, reason,
assessment: assessment || undefined,
questions: questions.length ? questions : undefined,
requiresApproval: false, requiresApproval: false,
statePatchAllowed: false, statePatchAllowed: false,
}; };

View File

@@ -8,11 +8,18 @@ import {
type LifecycleStage, type LifecycleStage,
type SkillCatalogEntry, type SkillCatalogEntry,
} from "./skill-catalog.js"; } from "./skill-catalog.js";
import {
displayWorkerLabel,
formatAgentDisplayTitle,
formatWorkerDisplayTitle,
} from "./display-labels.js";
export type AgentMessageKind = export type AgentMessageKind =
| "user_input" | "user_input"
| "orchestrator_decision" | "orchestrator_decision"
| "orchestrator_thinking"
| "orchestrator_prompt" | "orchestrator_prompt"
| "orchestrator_assessment"
| "agent_tool" | "agent_tool"
| "worker_running" | "worker_running"
| "worker_output" | "worker_output"
@@ -77,18 +84,29 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return { return {
kind: "agent_tool", kind: "agent_tool",
actor: "orchestrator", actor: "orchestrator",
title: `Tool · ${name}`, title: `工具 · ${name}`,
body: detail || "(无输出)", body: detail || "(无输出)",
text: trimmed, text: trimmed,
}; };
} }
const agentThink = trimmed.match(/^\[总管 思考\]\s*\n?\n?([\s\S]*)$/);
if (agentThink) {
return {
kind: "orchestrator_thinking",
actor: "orchestrator",
title: formatAgentDisplayTitle("思考"),
body: agentThink[1]?.trim() || "(无内容)",
text: trimmed,
};
}
const orchestrator = trimmed.match(/^\[总管\]\s*(\w+):\s*([\s\S]+)$/); const orchestrator = trimmed.match(/^\[总管\]\s*(\w+):\s*([\s\S]+)$/);
if (orchestrator) { if (orchestrator) {
return { return {
kind: "orchestrator_decision", kind: "orchestrator_decision",
actor: "orchestrator", actor: "orchestrator",
title: `Agent · ${orchestrator[1]}`, title: formatAgentDisplayTitle(orchestrator[1]),
body: orchestrator[2].trim(), body: orchestrator[2].trim(),
text: trimmed, text: trimmed,
}; };
@@ -99,8 +117,30 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return { return {
kind: "worker_running", kind: "worker_running",
actor: workerRunning[1], actor: workerRunning[1],
title: `Worker · ${workerRunning[1]}`, title: formatWorkerDisplayTitle(workerRunning[1], "running"),
body: "正在调用模型执行 SKILL…", body: "正在调用模型执行…",
text: trimmed,
};
}
const compressed = trimmed.match(/^\[上下文已压缩\]\s*([\s\S]+)$/);
if (compressed) {
return {
kind: "system_info",
actor: "system",
title: "上下文已压缩",
body: compressed[1].trim(),
text: trimmed,
};
}
const unitAccepted = trimmed.match(/^\[创作单位已验收\]\s*([\s\S]+)$/);
if (unitAccepted) {
return {
kind: "system_info",
actor: "system",
title: "创作单位已验收",
body: unitAccepted[1].trim(),
text: trimmed, text: trimmed,
}; };
} }
@@ -110,7 +150,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return { return {
kind: "worker_output", kind: "worker_output",
actor: workerDone[1], actor: workerDone[1],
title: `Worker · ${workerDone[1]} 产出`, title: formatWorkerDisplayTitle(workerDone[1], "output"),
body: workerDone[2]?.trim() || "(无正文)", body: workerDone[2]?.trim() || "(无正文)",
text: trimmed, text: trimmed,
}; };
@@ -121,7 +161,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return { return {
kind: "worker_stub", kind: "worker_stub",
actor: stub?.[1], actor: stub?.[1],
title: `占位 Worker · ${stub?.[1] ?? "?"}`, title: formatWorkerDisplayTitle(stub?.[1], "stub"),
body: trimmed, body: trimmed,
text: trimmed, text: trimmed,
}; };
@@ -135,7 +175,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return { return {
kind: "worker_questions", kind: "worker_questions",
actor: workerAskTagged[1], actor: workerAskTagged[1],
title: `Worker · ${workerAskTagged[1]} 提问`, title: formatWorkerDisplayTitle(workerAskTagged[1], "questions"),
body, body,
text: trimmed, text: trimmed,
}; };
@@ -170,6 +210,44 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
}; };
} }
if (trimmed.startsWith("[Agent] 内容评价")) {
return {
kind: "orchestrator_assessment",
actor: "orchestrator",
title: "总管 · 内容评价",
body: trimmed.replace(/^\[Agent\]\s*内容评价[:]\s*/, "").trim() || trimmed,
text: trimmed,
};
}
if (trimmed.startsWith("[Agent] 提问") || trimmed.startsWith("[Agent] 可选追问")) {
return {
kind: "worker_questions",
actor: "orchestrator",
title: "总管 · 可选追问",
body: formatWorkerQuestionBody(
trimmed
.replace(/^\[Agent\]\s*可选追问(可跳过)[:]\s*/, "")
.replace(/^\[Agent\]\s*提问[:]\s*/, ""),
),
text: trimmed,
};
}
if (trimmed.match(/^\[Worker\]\s*可选追问/)) {
return {
kind: "worker_questions",
title: "可选追问",
body: formatWorkerQuestionBody(
trimmed.replace(
/^\[Worker\]\s*可选追问(可跳过,直接接受(?:目前)?产物)[:]\s*/,
"",
),
),
text: trimmed,
};
}
if ( if (
trimmed.includes("请告诉我") || trimmed.includes("请告诉我") ||
trimmed.includes("启动询问") || trimmed.includes("启动询问") ||
@@ -178,7 +256,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return { return {
kind: "orchestrator_prompt", kind: "orchestrator_prompt",
actor: "orchestrator", actor: "orchestrator",
title: "Agent · 启动询问", title: "总管 · 启动询问",
body: trimmed, body: trimmed,
text: trimmed, text: trimmed,
}; };
@@ -268,16 +346,37 @@ export function buildFocus(
const detail = const detail =
intake && intake.requiredTotal > 0 intake && intake.requiredTotal > 0
? `必要项 ${intake.requiredFilled}/${intake.requiredTotal}` ? `必要项 ${intake.requiredFilled}/${intake.requiredTotal}`
: "完成必要项后可进入实例化"; : "总管将根据描述推理 Worker 集";
return { return {
actorType: "user", actorType: "user",
actorLabel: "你", actorLabel: "你",
action: "填写创作信息", action: "描述创作需求",
detail, detail,
}; };
} }
if (reason?.kind === "input" && !session.slots.startupCompleted) {
return {
actorType: "user",
actorLabel: "你",
action: "描述创作需求",
detail: "发送后总管将开始:创作 · 核心",
};
}
if (reason?.kind === "input") { if (reason?.kind === "input") {
if (reason.questions?.length) {
const q = reason.questions
.map((item) => item.prompt)
.filter((s) => s?.trim())
.join("");
return {
actorType: "user",
actorLabel: "你",
action: "回答追问",
detail: q.slice(0, 200) || reason.message,
};
}
return { return {
actorType: "user", actorType: "user",
actorLabel: "你", actorLabel: "你",
@@ -291,30 +390,37 @@ export function buildFocus(
return { return {
actorType: "orchestrator", actorType: "orchestrator",
actorId: "orchestrator", actorId: "orchestrator",
actorLabel: "Agent", actorLabel: "总管",
action: `建议 invoke ${worker}`, action: `建议调用 ${displayWorkerLabel(worker)}`,
detail: session.pendingDecision?.reason, detail: session.pendingDecision?.reason,
}; };
} }
if (reason?.kind === "worker_questions") { if (reason?.kind === "worker_questions") {
const q = reason.questions?.filter((s) => s?.trim()).join("") ?? ""; const q =
reason.questions
?.map((item) => item.prompt)
.filter((s) => s?.trim())
.join("") ?? "";
return { return {
actorType: "user", actorType: "user",
actorId: reason.workerId, actorId: reason.workerId,
actorLabel: "你", actorLabel: "你",
action: `回答 · ${reason.workerId}`, action: `回答 · ${displayWorkerLabel(reason.workerId)}`,
detail: q.slice(0, 200) || "请在下框补充", detail: q.slice(0, 200) || "请在询问卡作答",
}; };
} }
if (reason?.kind === "review_artifact") { if (reason?.kind === "review_artifact") {
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId); const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
const optionalQs = reason.questions?.length
? `;另有 ${reason.questions.length} 道可选追问`
: "";
return { return {
actorType: "user", actorType: "user",
actorLabel: "你", actorLabel: "你",
action: "验收产物", action: "验收产物",
detail: art?.summary ?? art?.workerId, detail: `${art?.summary ?? displayWorkerLabel(art?.workerId) ?? ""}${optionalQs}`,
}; };
} }
@@ -322,9 +428,9 @@ export function buildFocus(
return { return {
actorType: "worker", actorType: "worker",
actorId: session.currentWorkerId, actorId: session.currentWorkerId,
actorLabel: `Skill · ${session.currentWorkerId}`, actorLabel: displayWorkerLabel(session.currentWorkerId),
action: "执行中", action: "执行中",
detail: "模型按 SKILL 产出…", detail: "模型正在产出…",
}; };
} }
@@ -332,9 +438,9 @@ export function buildFocus(
return { return {
actorType: "orchestrator", actorType: "orchestrator",
actorId: "orchestrator", actorId: "orchestrator",
actorLabel: "Agent", actorLabel: "总管",
action: stage === "design" ? "设计 burst" : "游玩 burst", action: stage === "design" ? "创作调度" : "游玩调度",
detail: "tool loop读黑板 → 选 skill", detail: "读黑板 → 选下一步",
}; };
} }
@@ -342,7 +448,8 @@ export function buildFocus(
return { return {
actorType: "user", actorType: "user",
actorLabel: "你", actorLabel: "你",
action: "选择 Skill 包", action: "恢复中的旧会话",
detail: "请发送任意消息继续,或联系维护者",
}; };
} }

View File

@@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from "node:http"; import type { IncomingMessage, ServerResponse } from "node:http";
import { listSkills } from "../skills/loader.js"; import { listSkills } from "../skills/loader.js";
import { listSkills } from "../skills/loader.js"; import { createBook, deleteBook, duplicateBook, getBook, listBooks, updateBook } from "../book/store.js";
import { sessionManager } from "./session-manager.js"; import { sessionManager } from "./session-manager.js";
async function readBody(req: IncomingMessage): Promise<string> { async function readBody(req: IncomingMessage): Promise<string> {
@@ -16,6 +16,148 @@ function json(res: ServerResponse, status: number, data: unknown): void {
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
} }
type ModuleStatus = "ready" | "partial" | "skeleton" | "missing";
function moduleStatusFromSections(
hasPrompt: boolean,
present: string[],
taskBody?: string,
): ModuleStatus {
if (!hasPrompt) return "missing";
const hasTask = present.includes("task");
const hasOutput = present.includes("output");
if (!hasTask || !hasOutput) return "skeleton";
const task = taskBody?.trim() ?? "";
const stub =
!task ||
/待作者细写|待完善|(待/.test(task) ||
task.length < 120;
if (stub) return "skeleton";
const depth = ["principles", "probe", "checklist", "examples"].filter((id) =>
present.includes(id),
).length;
return depth >= 2 ? "ready" : "partial";
}
async function loadModulesPayload(skillId: string): Promise<{
skillPackId: string;
modules: Array<{
id: string;
name: string;
declaration: string;
artifact: string;
hasPrompt: boolean;
sectionsPresent: string[];
status: ModuleStatus;
}>;
}> {
const { loadSkill } = await import("../skills/loader.js");
const {
loadModuleCatalog,
loadModulePrompt,
parseModulePromptSections,
MODULE_SECTION_IDS,
} = await import("../skills/creation-flow.js");
const skill = await loadSkill(skillId);
if (!skill.skillPackRoot) {
return { skillPackId: skillId, modules: [] };
}
const catalog = await loadModuleCatalog(skill.skillPackRoot);
const modules = [];
for (const m of catalog?.modules ?? []) {
const prompt = await loadModulePrompt(skill.skillPackRoot, m.id);
const sections = prompt
? parseModulePromptSections(prompt)
: { raw: "", blocks: {} };
const sectionsPresent = MODULE_SECTION_IDS.filter((id) =>
Boolean(sections.blocks[id]?.trim()),
);
const hasPrompt = Boolean(prompt?.trim());
const taskBody = sections.blocks.task?.trim();
modules.push({
id: m.id,
name: m.name,
declaration: m.declaration,
artifact: m.artifact,
hasPrompt,
sectionsPresent: [...sectionsPresent],
status: moduleStatusFromSections(hasPrompt, sectionsPresent, taskBody),
});
}
return { skillPackId: skillId, modules };
}
async function loadModuleDetailPayload(
skillId: string,
moduleId: string,
): Promise<{
skillPackId: string;
id: string;
name: string;
declaration: string;
artifact: string;
hasPrompt: boolean;
sectionsPresent: string[];
status: ModuleStatus;
sections: Record<string, string>;
meta: Record<string, unknown> | null;
raw: string;
} | null> {
const { loadSkill } = await import("../skills/loader.js");
const {
loadModuleCatalog,
loadModulePrompt,
parseModulePromptSections,
MODULE_SECTION_IDS,
} = await import("../skills/creation-flow.js");
const { parse: parseYaml } = await import("yaml");
const skill = await loadSkill(skillId);
if (!skill.skillPackRoot) return null;
const catalog = await loadModuleCatalog(skill.skillPackRoot);
const entry = catalog?.modules.find((m) => m.id === moduleId) ?? null;
if (!entry) return null;
const prompt = await loadModulePrompt(skill.skillPackRoot, moduleId);
const parsed = prompt
? parseModulePromptSections(prompt)
: { raw: "", blocks: {} };
const sections: Record<string, string> = {};
for (const id of MODULE_SECTION_IDS) {
const body = parsed.blocks[id]?.trim();
if (body) sections[id] = body;
}
const sectionsPresent = Object.keys(sections);
const hasPrompt = Boolean(prompt?.trim());
let meta: Record<string, unknown> | null = null;
const metaRaw = sections.meta;
if (metaRaw) {
try {
const doc = parseYaml(metaRaw);
if (doc && typeof doc === "object" && !Array.isArray(doc)) {
meta = doc as Record<string, unknown>;
}
} catch {
meta = null;
}
}
return {
skillPackId: skillId,
id: entry.id,
name: entry.name,
declaration: entry.declaration,
artifact: entry.artifact,
hasPrompt,
sectionsPresent,
status: moduleStatusFromSections(
hasPrompt,
sectionsPresent,
sections.task,
),
sections,
meta,
raw: prompt ?? "",
};
}
export async function handleBooksApi( export async function handleBooksApi(
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse, res: ServerResponse,
@@ -35,16 +177,231 @@ export async function handleBooksApi(
return true; return true;
} }
/** 新建作品:导演一层选型(内部 = recipes catalog */
if (pathname === "/api/directors" && req.method === "GET") {
const { DEFAULT_ORCHESTRATOR_ID } = await import(
"../config/default-orchestrator.js"
);
const skillId = DEFAULT_ORCHESTRATOR_ID;
try {
const { loadSkill } = await import("../skills/loader.js");
const { loadRecipeCatalog } = await import("../skills/creation-flow.js");
const skill = await loadSkill(skillId);
if (!skill.skillPackRoot) {
json(res, 200, { directors: [], skillPackId: skillId });
return true;
}
const catalog = await loadRecipeCatalog(skill.skillPackRoot);
json(res, 200, {
skillPackId: skillId,
directors: (catalog?.recipes ?? []).map((r) => ({
id: r.id,
name: r.name,
declaration: r.declaration,
})),
});
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : "未找到导演列表",
});
}
return true;
}
const recipesMatch = pathname.match(/^\/api\/skills\/([^/]+)\/recipes$/);
if (recipesMatch && req.method === "GET") {
const skillId = decodeURIComponent(recipesMatch[1]);
try {
const { loadSkill } = await import("../skills/loader.js");
const { loadRecipeCatalog } = await import("../skills/creation-flow.js");
const skill = await loadSkill(skillId);
if (!skill.skillPackRoot) {
json(res, 200, { recipes: [] });
return true;
}
const catalog = await loadRecipeCatalog(skill.skillPackRoot);
json(res, 200, {
recipes: (catalog?.recipes ?? []).map((r) => ({
id: r.id,
name: r.name,
declaration: r.declaration,
})),
});
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : "未找到能力包",
});
}
return true;
}
/** 默认导演包的能力池(编排备选) */
if (pathname === "/api/modules" && req.method === "GET") {
const { DEFAULT_ORCHESTRATOR_ID } = await import(
"../config/default-orchestrator.js"
);
try {
const payload = await loadModulesPayload(DEFAULT_ORCHESTRATOR_ID);
json(res, 200, payload);
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : "未找到能力目录",
});
}
return true;
}
const moduleDetailMatch = pathname.match(/^\/api\/modules\/([^/]+)$/);
if (moduleDetailMatch && req.method === "GET") {
const { DEFAULT_ORCHESTRATOR_ID } = await import(
"../config/default-orchestrator.js"
);
const moduleId = decodeURIComponent(moduleDetailMatch[1]);
try {
const detail = await loadModuleDetailPayload(
DEFAULT_ORCHESTRATOR_ID,
moduleId,
);
if (!detail) {
json(res, 404, { error: `未找到能力:${moduleId}` });
return true;
}
json(res, 200, detail);
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : "未找到能力",
});
}
return true;
}
const skillModulesMatch = pathname.match(
/^\/api\/skills\/([^/]+)\/modules$/,
);
if (skillModulesMatch && req.method === "GET") {
const skillId = decodeURIComponent(skillModulesMatch[1]);
try {
const payload = await loadModulesPayload(skillId);
json(res, 200, payload);
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : "未找到能力目录",
});
}
return true;
}
const skillModuleDetailMatch = pathname.match(
/^\/api\/skills\/([^/]+)\/modules\/([^/]+)$/,
);
if (skillModuleDetailMatch && req.method === "GET") {
const skillId = decodeURIComponent(skillModuleDetailMatch[1]);
const moduleId = decodeURIComponent(skillModuleDetailMatch[2]);
try {
const detail = await loadModuleDetailPayload(skillId, moduleId);
if (!detail) {
json(res, 404, { error: `未找到能力:${moduleId}` });
return true;
}
json(res, 200, detail);
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : "未找到能力",
});
}
return true;
}
if (pathname === "/api/books" && req.method === "GET") { if (pathname === "/api/books" && req.method === "GET") {
json(res, 200, { books: listBooks() }); json(res, 200, { books: listBooks() });
return true; return true;
} }
if (pathname === "/api/books" && req.method === "POST") { if (pathname === "/api/books" && req.method === "POST") {
const body = JSON.parse(await readBody(req)) as {
title?: string;
/** 导演 Skill / 能力包 idregistry name */
orchestratorId?: string;
/** 用户手动选定的导演 id内部 recipe */
recipeId?: string;
};
const skills = await listSkills();
const requested = body.orchestratorId?.trim();
const director =
(requested && skills.find((s) => s.name === requested)) ||
skills.find((s) => s.name === "world-simulator") ||
skills[0];
if (!director) {
json(res, 400, { error: "没有可用的导演 Skill能力包" });
return true;
}
const book = createBook({ title: body.title }); const book = createBook({ title: body.title });
updateBook(book.id, {
if (pathname === "/api/skills" && req.method === "GET") { activeSkillId: director.name,
activeSkillName: director.description?.split("\n")[0]?.slice(0, 80) || director.name,
orchestratorId: director.name,
orchestratorName: director.name,
});
const session = await sessionManager.createForBook(
book.id,
director.name,
body.recipeId?.trim(),
);
json(res, 201, { book: getBook(book.id) ?? book, session });
return true;
}
if (pathname === "/api/books/batch" && req.method === "DELETE") {
const body = JSON.parse(await readBody(req)) as { ids?: string[] };
const ids = (body.ids ?? []).filter(Boolean);
const deleted: string[] = [];
for (const id of ids) {
const book = getBook(id);
if (!book) continue;
sessionManager.dropBookSessions(id);
deleteBook(id);
deleted.push(id);
}
json(res, 200, { deleted });
return true;
}
const duplicateMatch = pathname.match(/^\/api\/books\/([^/]+)\/duplicate$/);
if (duplicateMatch && req.method === "POST") {
const bookId = decodeURIComponent(duplicateMatch[1]);
const body = JSON.parse(await readBody(req)) as { title?: string };
try {
const book = duplicateBook(bookId, body.title);
json(res, 201, { book });
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "复制失败",
});
}
return true;
}
const playNewMatch = pathname.match(/^\/api\/books\/([^/]+)\/play\/new$/);
if (playNewMatch && req.method === "POST") {
const bookId = decodeURIComponent(playNewMatch[1]);
const book = getBook(bookId);
if (!book) {
json(res, 404, { error: "Book 不存在" });
return true;
}
const active = sessionManager.getActiveSessionForBook(bookId);
if (!active?.id) {
json(res, 400, { error: "请先打开该作品" });
return true;
}
try {
const session = await sessionManager.startNewPlayRun(active.id);
json(res, 200, { session });
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "无法新建游玩",
});
}
return true; return true;
} }
@@ -59,7 +416,10 @@ export async function handleBooksApi(
if (req.method === "GET") { if (req.method === "GET") {
try { try {
const saves = sessionManager.listGameSnapshots(bookId).map((s) => ({
...s,
kindLabel: s.kind === "instance" ? "创作定稿" : "游玩进度",
}));
json(res, 200, { saves }); json(res, 200, { saves });
} catch (err) { } catch (err) {
json(res, 400, { json(res, 400, {
@@ -74,6 +434,7 @@ export async function handleBooksApi(
label?: string; label?: string;
note?: string; note?: string;
sessionId?: string; sessionId?: string;
/** instance=创作定稿截面run=游玩进度(默认) */
kind?: "instance" | "run"; kind?: "instance" | "run";
}; };
const sessionId = const sessionId =
@@ -85,13 +446,21 @@ export async function handleBooksApi(
} }
const kind = body.kind === "instance" ? "instance" : "run"; const kind = body.kind === "instance" ? "instance" : "run";
try { try {
const save =
const book = createBook({ title: body.title }); kind === "run"
? sessionManager.savePlaySnapshot(sessionId, body.label ?? "", body.note)
const session = await sessionManager.createForBook(book.id); : sessionManager.saveGameSnapshot(
sessionId,
json(res, 201, { book, session }); body.label ?? "",
"instance",
body.note,
);
json(res, 201, {
save: {
...save,
kindLabel: save.kind === "instance" ? "创作定稿" : "游玩进度",
},
});
} catch (err) { } catch (err) {
json(res, 400, { json(res, 400, {
error: err instanceof Error ? err.message : "存档失败", error: err instanceof Error ? err.message : "存档失败",

View File

@@ -0,0 +1,121 @@
/**
* 用户可见中文标签(与 docs/ui-glossary.md 同步)。
* 内部 id 不变;仅展示层映射。
*/
const STAGE_LABELS: Record<string, string> = {
design: "创作",
play: "游玩",
done: "已完成",
idle: "待命",
running: "执行中",
waiting_user: "等待你",
error: "出错",
};
const WORKER_LABELS: Record<string, string> = {
"design-core": "创作 · 核心",
"design-worker": "创作 · 演员规格",
"design-fixed": "创作 · 固定上下文",
"design-refine": "创作 · 细化与终稿",
"design-intake": "创作 · 综合收口",
"design-flow": "创作 · 流程编排",
"design-step": "创作 · 执行步骤",
"opening-generator": "开局 · 开场白",
orchestrator: "导演",
"agent-burst": "导演调度",
narrator: "叙事转述",
"role-decide": "角色决策",
"world-simulator": "世界推演",
"round-present": "回合呈现",
outline: "大纲 / 细纲",
"chapter-writer": "章节正文",
};
const FIXED_TOPIC_LABELS: Record<string, string> = {
"aesthetics-interaction": "美学纲领与交互范式",
interaction: "交互范式",
narrative_guide: "叙事指南",
input_protocol: "输入协议",
core_premise: "核心前提",
aesthetics: "美学纲领",
};
const PHASE_UNIT_LABELS: Record<string, string> = {
core: "核心",
refine: "细化",
};
const SKILL_PACK_LABELS: Record<string, string> = {
"world-simulator": "世界模拟器",
"expand-assistant": "扩写助手",
};
/** 生命周期 / 相位 */
export function displayStageLabel(id: string | undefined | null): string {
if (!id) return "";
return STAGE_LABELS[id] ?? id;
}
/** 导演选项 / skill pack 展示名 */
export function displaySkillPackLabel(id: string | undefined | null): string {
if (!id) return "";
return SKILL_PACK_LABELS[id] ?? id;
}
/**
* 演员 / 单位 / 能力 id → 用户可见名(不含动作后缀)。
*/
export function displayWorkerLabel(id: string | undefined | null): string {
if (!id) return "";
const trimmed = id.trim();
if (!trimmed) return "";
if (WORKER_LABELS[trimmed]) return WORKER_LABELS[trimmed]!;
if (trimmed.startsWith("phase:")) {
const key = trimmed.slice("phase:".length);
const name = PHASE_UNIT_LABELS[key] ?? key;
return `单位 · ${name}`;
}
if (trimmed.startsWith("worker:")) {
const ref = trimmed.slice("worker:".length);
return `演员 · ${displayWorkerLabel(ref)}`;
}
if (trimmed.startsWith("fixed:")) {
const topic = trimmed.slice("fixed:".length);
return `能力 · ${FIXED_TOPIC_LABELS[topic] ?? topic}`;
}
if (trimmed.startsWith("resident:")) {
return `常驻 · ${trimmed.slice("resident:".length)}`;
}
return trimmed;
}
export type WorkerTitleAction = "running" | "output" | "questions" | "stub" | null;
/** 气泡 / 流式标题:中文名 + 可选动作 */
export function formatWorkerDisplayTitle(
workerId: string | undefined | null,
action: WorkerTitleAction = null,
): string {
const base = displayWorkerLabel(workerId) || "演员";
switch (action) {
case "output":
return `${base} · 产出`;
case "questions":
return `${base} · 提问`;
case "stub":
return `${base} · 占位`;
case "running":
return `${base} · 执行中`;
default:
return base;
}
}
/** 导演(调度 Agent相关标题 */
export function formatAgentDisplayTitle(detail?: string): string {
if (detail?.trim()) return `导演 · ${detail.trim()}`;
return "导演";
}

View File

@@ -0,0 +1,223 @@
import { randomUUID } from "node:crypto";
import type { BlackboardItem } from "../types/blackboard.js";
import type { RuntimeSession } from "../types/runtime.js";
export type BranchableMessage = {
id: string;
role: "system" | "user";
text: string;
createdAt: string;
kind?: string;
actor?: string;
title?: string;
body?: string;
branchGroupId?: string;
branchIndex?: number;
branchTotal?: number;
};
export type SessionCheckpoint = {
runtimeSession: RuntimeSession;
blackboardItems: BlackboardItem[];
};
export type MessageBranchVariant = {
/** 从分支点起的消息链(含分支点消息本身) */
messages: BranchableMessage[];
checkpoint: SessionCheckpoint;
};
export type MessageBranch = {
anchorIndex: number;
groupId: string;
variants: MessageBranchVariant[];
activeIndex: number;
};
export type MessageBranchState = {
branches: Record<string, MessageBranch>;
/** 下标 i = 追加 messages[i] 之前的 runtime 快照 */
preMessageCheckpoints: Record<number, SessionCheckpoint>;
};
export function createMessageBranchState(): MessageBranchState {
return { branches: {}, preMessageCheckpoints: {} };
}
export function cloneCheckpoint(cp: SessionCheckpoint): SessionCheckpoint {
return {
runtimeSession: structuredClone(cp.runtimeSession),
blackboardItems: structuredClone(cp.blackboardItems),
};
}
export function recordPreMessageCheckpoint(
state: MessageBranchState,
index: number,
checkpoint: SessionCheckpoint,
): void {
state.preMessageCheckpoints[index] = cloneCheckpoint(checkpoint);
}
export function attachBranchMeta(messages: BranchableMessage[], groupId: string, activeIndex: number): void {
const count = messages.filter((m) => m.branchGroupId === groupId).length || 1;
const total = Math.max(count, activeIndex + 1);
for (const m of messages) {
if (m.branchGroupId === groupId && m.branchIndex === activeIndex) {
m.branchTotal = total;
}
}
}
export function syncBranchTotals(
messages: BranchableMessage[],
branch: MessageBranch,
): void {
const total = branch.variants.length;
const active = branch.activeIndex;
const head = branch.variants[active]?.messages[0];
if (!head) return;
for (const m of messages) {
if (m.id === head.id || (m.branchGroupId === branch.groupId && m.branchIndex === active)) {
m.branchGroupId = branch.groupId;
m.branchIndex = active;
m.branchTotal = total;
}
}
}
function cloneMessages(msgs: BranchableMessage[]): BranchableMessage[] {
return msgs.map((m) => ({ ...m }));
}
export function ensureBranchForEdit(
state: MessageBranchState,
messages: BranchableMessage[],
messageIndex: number,
checkpoint: SessionCheckpoint,
): MessageBranch {
const msg = messages[messageIndex];
const groupId = msg.branchGroupId ?? msg.id;
let branch = state.branches[groupId];
if (!branch) {
branch = {
anchorIndex: messageIndex,
groupId,
activeIndex: 0,
variants: [
{
messages: cloneMessages(messages.slice(messageIndex)),
checkpoint: cloneCheckpoint(checkpoint),
},
],
};
state.branches[groupId] = branch;
for (const m of messages.slice(messageIndex)) {
m.branchGroupId = groupId;
m.branchIndex = 0;
m.branchTotal = 1;
}
}
return branch;
}
export function ensureBranchForRefresh(
state: MessageBranchState,
messages: BranchableMessage[],
messageIndex: number,
checkpoint: SessionCheckpoint,
): MessageBranch {
const msg = messages[messageIndex];
const groupId = msg.branchGroupId ?? msg.id;
let branch = state.branches[groupId];
if (!branch) {
branch = {
anchorIndex: messageIndex,
groupId,
activeIndex: 0,
variants: [
{
messages: cloneMessages(messages.slice(messageIndex)),
checkpoint: cloneCheckpoint(checkpoint),
},
],
};
state.branches[groupId] = branch;
for (const m of messages.slice(messageIndex)) {
m.branchGroupId = groupId;
m.branchIndex = 0;
m.branchTotal = 1;
}
}
return branch;
}
export function appendBranchVariant(
branch: MessageBranch,
headMessage: BranchableMessage,
checkpoint: SessionCheckpoint,
): number {
const index = branch.variants.length;
branch.variants.push({
messages: [{ ...headMessage, branchGroupId: branch.groupId, branchIndex: index }],
checkpoint: cloneCheckpoint(checkpoint),
});
branch.activeIndex = index;
return index;
}
export function updateActiveBranchVariant(
branch: MessageBranch,
tailMessages: BranchableMessage[],
checkpoint: SessionCheckpoint,
): void {
const variant = branch.variants[branch.activeIndex];
if (!variant) return;
variant.messages = cloneMessages(tailMessages);
variant.checkpoint = cloneCheckpoint(checkpoint);
}
export function switchBranchVariant(
state: MessageBranchState,
messages: BranchableMessage[],
groupId: string,
delta: -1 | 1,
): { messages: BranchableMessage[]; checkpoint: SessionCheckpoint } | null {
const branch = state.branches[groupId];
if (!branch) return null;
const next = branch.activeIndex + delta;
if (next < 0 || next >= branch.variants.length) return null;
branch.activeIndex = next;
const variant = branch.variants[next];
const prefix = messages.slice(0, branch.anchorIndex);
const merged = [...prefix, ...cloneMessages(variant.messages)];
syncBranchTotals(merged, branch);
return { messages: merged, checkpoint: cloneCheckpoint(variant.checkpoint) };
}
export function createUserVariantMessage(text: string, groupId: string, branchIndex: number): BranchableMessage {
return {
id: randomUUID(),
role: "user",
text,
createdAt: new Date().toISOString(),
kind: "user_input",
title: "你的输入",
body: text,
branchGroupId: groupId,
branchIndex,
};
}
export function isRefreshableMessage(msg: BranchableMessage): boolean {
if (msg.role === "user") return false;
const kind = msg.kind ?? "system_info";
return kind === "worker_questions" || kind === "worker_output";
}
export function findPrecedingUserIndex(messages: BranchableMessage[], fromIndex: number): number {
for (let i = fromIndex - 1; i >= 0; i--) {
if (messages[i].role === "user") return i;
}
return -1;
}

View File

@@ -0,0 +1,162 @@
/**
* 创作过程对话策略(与黑板固定上下文正交):
* - session.messages全量保留供用户浏览编辑分支只暴露当前认可版本
* - 拼给 AI 的「创作.对话」:用户全量 + AI 永远只留最后一次(含未完成提问)
*
* 「隐藏 AI 历史」只作用于拼接,不删、不藏用户可见消息。
*/
export type DialogueChatMessage = {
id: string;
role: "system" | "user";
text: string;
createdAt?: string;
kind?: string;
actor?: string;
title?: string;
body?: string;
compressed?: boolean;
};
/** @deprecated 用 DialogueChatMessage */
export type PrunableChatMessage = DialogueChatMessage;
/** 写入黑板、注入 design worker 的对话 transcript tag */
export const CREATION_DIALOGUE_TAG = "创作.对话";
const AI_CONTENT_KINDS = new Set([
"worker_output",
"worker_questions",
"orchestrator_thinking",
"orchestrator_decision",
]);
const AI_EPHEMERAL_KINDS = new Set(["worker_running", "worker_stub", "agent_tool"]);
function isAiContent(m: DialogueChatMessage): boolean {
const kind = m.kind ?? "";
if (AI_CONTENT_KINDS.has(kind)) return true;
if (m.role === "system" && kind.startsWith("worker_")) return true;
return false;
}
function isAiEphemeral(m: DialogueChatMessage): boolean {
return AI_EPHEMERAL_KINDS.has(m.kind ?? "");
}
function isUserMessage(m: DialogueChatMessage): boolean {
return m.role === "user" || m.kind === "user_input";
}
/**
* 从全量 messages 选出「拼给 AI」的视图全部用户 + 最后一次 AI 内容。
* 不修改原数组。
*/
export function selectCreationDialogueForAi<T extends DialogueChatMessage>(
messages: readonly T[],
): T[] {
let lastAiIdx = -1;
for (let i = messages.length - 1; i >= 0; i--) {
if (isAiContent(messages[i]!)) {
lastAiIdx = i;
break;
}
}
const out: T[] = [];
for (let i = 0; i < messages.length; i++) {
const m = messages[i]!;
if (m.compressed) continue;
if (isUserMessage(m)) {
out.push(m);
continue;
}
if (isAiEphemeral(m)) continue;
if (isAiContent(m) && i === lastAiIdx) out.push(m);
}
return out;
}
/**
* @deprecated 勿再改写 session.messages改用 selectCreationDialogueForAi / buildCreationDialogueTranscript。
* 保留空操作兼容旧调用点。
*/
export function trimCreationDialogueMessages<T extends DialogueChatMessage>(
_messages: T[],
): { removedCount: number } {
return { removedCount: 0 };
}
function displayText(m: DialogueChatMessage): string {
const body = (m.body ?? m.text ?? "").trim();
return body || "(空)";
}
/** 拼给 design worker 的对话前情(用户全量 + 最后一次 AI不改 messages */
export function buildCreationDialogueTranscript(
messages: readonly DialogueChatMessage[],
): string {
const selected = selectCreationDialogueForAi(messages);
const lines: string[] = [
"以下为创作过程对话用户发言全部保留AI 仅保留最后一次输出,含未完成提问)。",
"",
];
for (const m of selected) {
if (isUserMessage(m)) {
lines.push(`### 用户`);
lines.push(displayText(m));
lines.push("");
continue;
}
if (isAiContent(m)) {
const who =
m.kind === "worker_questions"
? `AI · ${m.actor ?? "worker"} 提问`
: m.kind === "worker_output"
? `AI · ${m.actor ?? "worker"} 产出`
: m.kind === "orchestrator_thinking"
? "AI · 总管思考"
: `AI · ${m.title ?? m.kind ?? "系统"}`;
lines.push(`### ${who}`);
lines.push(displayText(m));
lines.push("");
}
}
const text = lines.join("\n").trim();
return text || "(尚无创作对话)";
}
/**
* @deprecated 创作验收后不再删 messages用户可继续浏览。保留空操作兼容。
*/
export function pruneCreationUnitMessages<T extends DialogueChatMessage>(
_messages: T[],
_workerId: string,
_options: { afterCreatedAt?: string | null } = {},
): { removedCount: number; productKept: boolean } {
return { removedCount: 0, productKept: false };
}
/** Run 验收:过程消息标 compressed主 feed 隐藏),不删除。 */
export function foldRunProcessMessages<T extends DialogueChatMessage>(
messages: T[],
workerId: string,
): void {
for (const m of messages) {
if (m.compressed) continue;
if (
(m.kind === "worker_questions" || m.kind === "worker_running") &&
(m.actor === workerId || (m.text ?? "").includes(workerId))
) {
m.compressed = true;
continue;
}
if (
m.kind === "worker_output" &&
m.actor === workerId &&
!String(m.text ?? "").includes("[上下文已压缩]")
) {
m.compressed = true;
}
}
}

Some files were not shown because too many files have changed in this diff Show More