Browse Source

feat: 核心引擎和类型重写(simulation-engine/types/agent-interview/agent-profile-builder)

Mochocyang 3 tháng trước cách đây
mục cha
commit
9a7779bea8

+ 257 - 0
src/lib/novel/story-simulation/agent-interview.ts

@@ -0,0 +1,257 @@
+import type { ChatMessage } from "@/lib/llm-client"
+import { streamChat } from "@/lib/llm-client"
+import type { LlmConfig } from "@/stores/wiki-store"
+import type {
+  AgentChatMessage,
+  AgentChatSession,
+  NovelAgent,
+  SimulationState,
+  TimelineEvent,
+} from "@/lib/novel/story-simulation/types"
+import { formatTimelineEvent, getVisibleEvents } from "@/lib/novel/story-simulation/agent-profile-builder"
+
+// ── 对外接口 ──
+
+export interface InterviewOptions {
+  llmConfig: LlmConfig
+  agent: NovelAgent
+  simulationState: SimulationState
+  userPrompt: string
+  signal?: AbortSignal
+  /** 流式 token 回调 */
+  onToken?: (token: string) => void
+  /** 对话历史(可选,用于多轮采访) */
+  session?: AgentChatSession
+  /** 完成回调,返回完整回复文本和更新后的会话 */
+  onDone?: (fullText: string, session: AgentChatSession) => void
+  /** 错误回调 */
+  onError?: (error: Error) => void
+}
+
+// ── 内部辅助:构建采访系统提示词 ──
+
+function buildInterviewSystemPrompt(agent: NovelAgent): string {
+  const personalityLine =
+    agent.personality.length > 0
+      ? `你的性格:${agent.personality.join("、")}`
+      : ""
+  const styleLine = agent.speakingStyle
+    ? `你的说话风格:${agent.speakingStyle}`
+    : ""
+
+  return [
+    `你正在以小说角色「${agent.name}」的身份接受采访/对话。`,
+    "",
+    "【核心规则 - 必须严格遵守】",
+    "1. 你就是「" + agent.name + "」本人,不是AI助手,不要跳出角色。",
+    "2. 严格基于你的认知范围回答:你只知道你亲身经历、亲眼看到、亲耳听到的事情。",
+    "3. 你不知道的事情必须明确表示「我不知道」或「我不清楚」,绝不能编造你不知道的信息。",
+    "4. 绝对禁止全知视角:你不知道其他角色的内心想法,不知道剧情走向,不知道作者的安排。",
+    "5. 保持你的性格和说话风格,回答要自然、像真实的人物对话。",
+    "6. 用纯文本回复,不要输出JSON,不要使用markdown格式,不要加入舞台指示或动作描写括号。",
+    "",
+    personalityLine,
+    styleLine,
+  ]
+    .filter((line) => line !== null && line !== undefined && line !== "")
+    .join("\n")
+}
+
+// ── 内部辅助:构建采访上下文 ──
+
+function buildInterviewContext(
+  agent: NovelAgent,
+  simulationState: SimulationState,
+): string {
+  const sections: string[] = []
+
+  // 角色基本信息
+  sections.push("【你的身份】")
+  sections.push(`姓名:${agent.name}`)
+  if (agent.profile) {
+    sections.push(`档案:${agent.profile}`)
+  }
+  if (agent.soul) {
+    sections.push(`角色灵魂:${agent.soul}`)
+  }
+
+  // 你知道的信息
+  sections.push("")
+  sections.push("【你知道的信息】")
+  if (agent.knowledgeScope.length > 0) {
+    sections.push(agent.knowledgeScope.slice(-20).join("\n"))
+  } else {
+    sections.push("(目前你了解的信息有限)")
+  }
+
+  // 你不知道的信息(提醒不要越界)
+  if (agent.cognition?.doesNotKnow && agent.cognition.doesNotKnow.length > 0) {
+    sections.push("")
+    sections.push("【你绝对不知道的事情(禁止提及)】")
+    sections.push(agent.cognition.doesNotKnow.join("\n"))
+  }
+
+  // 人际关系/情感
+  sections.push("")
+  sections.push("【你对其他角色的情感】")
+  let hasRelation = false
+  for (const [otherId, value] of agent.memory.sentiments.entries()) {
+    if (otherId === agent.characterId) continue
+    const otherAgent = simulationState.activeAgents.get(otherId)
+    const otherName = otherAgent?.name ?? otherId
+    const desc =
+      value > 30
+        ? "有好感"
+        : value < -30
+          ? "有敌意"
+          : value > 0
+            ? "印象不错"
+            : value < 0
+              ? "有些不满"
+              : "态度中立"
+    sections.push(`- 对 ${otherName}:${desc}(好感度 ${value})`)
+    hasRelation = true
+  }
+  if (!hasRelation) {
+    sections.push("(你目前对其他角色没有特别的情感倾向)")
+  }
+
+  // 你最近观察到的事件(最近10条可见事件)
+  const visibleEvents = getVisibleEvents(
+    agent.characterId,
+    simulationState.timelineEvents,
+    10,
+  )
+  if (visibleEvents.length > 0) {
+    sections.push("")
+    sections.push("【你最近经历/观察到的事情】")
+    for (const ev of visibleEvents) {
+      sections.push(`- ${formatTimelineEvent(ev)}`)
+    }
+  }
+
+  // 当前情绪和目标
+  sections.push("")
+  sections.push("【你当前的状态】")
+  sections.push(`当前目标:${agent.currentGoal}`)
+  sections.push(`情绪状态:${agent.emotionalState}`)
+
+  return sections.join("\n")
+}
+
+// ── 内部辅助:生成消息 ID ──
+
+function nextMsgId(): string {
+  return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
+}
+
+// ── 主入口:采访/对话 Agent ──
+
+export async function interviewAgent(
+  options: InterviewOptions,
+): Promise<{ fullText: string; session: AgentChatSession }> {
+  const {
+    llmConfig,
+    agent,
+    simulationState,
+    userPrompt,
+    signal,
+    onToken,
+    session: existingSession,
+    onDone,
+    onError,
+  } = options
+
+  // 初始化或复用会话
+  const session: AgentChatSession = existingSession
+    ? { ...existingSession, messages: [...existingSession.messages] }
+    : {
+        agentId: agent.characterId,
+        agentName: agent.name,
+        messages: [],
+      }
+
+  try {
+    // 构建消息列表
+    const messages: ChatMessage[] = [
+      { role: "system", content: buildInterviewSystemPrompt(agent) },
+    ]
+
+    // 添加上下文作为第一条 user 消息(只在会话开始时添加)
+    if (session.messages.length === 0) {
+      const context = buildInterviewContext(agent, simulationState)
+      messages.push({
+        role: "user",
+        content:
+          context +
+          "\n\n以上是你的背景信息。采访即将开始,请根据后续提问自然地以角色身份回答。",
+      })
+      messages.push({
+        role: "assistant",
+        content: "我准备好了,请问吧。",
+      })
+    }
+
+    // 添加历史消息
+    for (const msg of session.messages) {
+      if (msg.role === "user") {
+        messages.push({ role: "user", content: msg.content })
+      } else if (msg.role === "agent") {
+        messages.push({ role: "assistant", content: msg.content })
+      }
+    }
+
+    // 添加当前用户提问
+    const userMsg: AgentChatMessage = {
+      id: nextMsgId(),
+      role: "user",
+      content: userPrompt,
+      timestamp: new Date().toISOString(),
+    }
+    session.messages.push(userMsg)
+    messages.push({ role: "user", content: userPrompt })
+
+    // 调用 LLM 流式回复
+    let fullText = ""
+    let streamError: Error | null = null
+
+    await streamChat(
+      llmConfig,
+      messages,
+      {
+        onToken: (token) => {
+          fullText += token
+          onToken?.(token)
+        },
+        onDone: () => {},
+        onError: (err) => {
+          streamError = err
+        },
+      },
+      signal,
+    )
+
+    if (streamError) {
+      throw streamError
+    }
+
+    // 记录 Agent 回复到会话
+    const agentMsg: AgentChatMessage = {
+      id: nextMsgId(),
+      role: "agent",
+      agentId: agent.characterId,
+      agentName: agent.name,
+      content: fullText,
+      timestamp: new Date().toISOString(),
+    }
+    session.messages.push(agentMsg)
+
+    onDone?.(fullText, session)
+
+    return { fullText, session }
+  } catch (err) {
+    const error = err instanceof Error ? err : new Error(String(err))
+    onError?.(error)
+    throw error
+  }
+}

+ 161 - 22
src/lib/novel/story-simulation/agent-profile-builder.ts

@@ -1,11 +1,13 @@
 import type { CharacterAura } from "@/lib/novel/character-aura"
 import type {
+  AgentMemory,
   AgentRelation,
   ExtractionResult,
   ExtractedCharacter,
   NovelAgent,
   StoryFramework,
   StoryNode,
+  TimelineEvent,
 } from "@/lib/novel/story-simulation/types"
 
 /**
@@ -27,12 +29,92 @@ function inferGoalFromFramework(
   return framework.premise || "待定"
 }
 
+/**
+ * 从角色 soul 和 aura 中提取性格关键词。
+ * 简单策略:取 soul 文本的前若干关键词 + aura 中相关字段的关键短语。
+ */
+function extractPersonalityKeywords(
+  soul: string,
+  aura: CharacterAura | null,
+): string[] {
+  const keywords: string[] = []
+
+  // 从 soul 中提取简短关键词(按标点分割,取短句)
+  if (soul) {
+    const parts = soul
+      .split(/[。;,、\n,;.]/)
+      .map((s) => s.trim())
+      .filter((s) => s.length > 0 && s.length <= 12)
+    for (const part of parts.slice(0, 5)) {
+      if (!keywords.includes(part)) {
+        keywords.push(part)
+      }
+    }
+  }
+
+  // 从 aura 关键字段提取
+  if (aura) {
+    const fields = [aura.styleDescription, aura.behaviorRules, aura.mentalModel]
+    for (const field of fields) {
+      if (!field) continue
+      const parts = field
+        .split(/[。;,、\n,;.]/)
+        .map((s) => s.trim())
+        .filter((s) => s.length > 0 && s.length <= 10)
+      for (const part of parts.slice(0, 2)) {
+        if (!keywords.includes(part)) {
+          keywords.push(part)
+        }
+      }
+    }
+  }
+
+  return keywords.slice(0, 8)
+}
+
+/**
+ * 从 aura 的表达 DNA 和风格描述中提取说话风格。
+ */
+function extractSpeakingStyle(aura: CharacterAura | null, soul: string): string {
+  const parts: string[] = []
+  if (aura?.expressionDna) {
+    parts.push(aura.expressionDna.trim())
+  }
+  if (aura?.styleDescription) {
+    parts.push(aura.styleDescription.trim())
+  }
+  if (parts.length === 0 && soul) {
+    // 兜底:取 soul 的前 100 字作为风格参考
+    parts.push(soul.slice(0, 100))
+  }
+  return parts.join(";").slice(0, 300)
+}
+
+/**
+ * 初始化 Agent 记忆。
+ */
+function initMemory(
+  allCharacterIds: string[],
+  initialSentiments?: Map<string, number>,
+): AgentMemory {
+  const sentiments = new Map<string, number>()
+  for (const id of allCharacterIds) {
+    sentiments.set(id, initialSentiments?.get(id) ?? 0)
+  }
+  return {
+    observedEvents: [],
+    knownSecrets: new Set<string>(),
+    sentiments,
+    recentDecisions: [],
+  }
+}
+
 /**
  * 根据提取结果与故事框架构建仿真用 Agent 列表。
  *
  * - 从框架节点中收集所有涉及的角色名
  * - 如果框架未指定角色,则使用全部提取到的角色
- * - 为每个角色构建 NovelAgent,初始化目标、情绪、已知事实与关系
+ * - 为每个角色构建 NovelAgent,初始化目标、情绪、已知事实、关系、记忆、性格、说话风格、认知范围
  */
 export function buildAgents(
   extraction: ExtractionResult,
@@ -60,12 +142,12 @@ export function buildAgents(
   const allCharacterIds = selectedCharacters.map((c) => c.id)
 
   const agents: NovelAgent[] = selectedCharacters.map((character) => {
-    // 已知事实从认知的 knows 初始化
+    // 已知事实从认知的 knows 初始化(保留兼容)
     const knownFacts = new Set<string>(
       character.cognition?.knows ?? [],
     )
 
-    // 初始化与其他角色的关系:relationType="neutral", sentiment=0
+    // 初始化与其他角色的关系(保留兼容)
     const relationships = new Map<string, AgentRelation>()
     for (const otherId of allCharacterIds) {
       if (otherId === character.id) continue
@@ -76,6 +158,18 @@ export function buildAgents(
       })
     }
 
+    // 知识范围从 cognition.knows 构建
+    const knowledgeScope: string[] = [...(character.cognition?.knows ?? [])]
+
+    // 性格关键词
+    const personality = extractPersonalityKeywords(character.soul, character.aura)
+
+    // 说话风格
+    const speakingStyle = extractSpeakingStyle(character.aura, character.soul)
+
+    // 记忆初始化
+    const memory = initMemory(allCharacterIds)
+
     return {
       characterId: character.id,
       name: character.name,
@@ -88,23 +182,54 @@ export function buildAgents(
       knownFacts,
       relationships,
       powerLevel: "",
+      memory,
+      knowledgeScope,
+      personality,
+      speakingStyle,
     }
   })
 
   return agents
 }
 
+/**
+ * 从 SimulationState 中筛选某个 Agent 可见的时间线事件(最近 N 条)。
+ */
+export function getVisibleEvents(
+  agentId: string,
+  timelineEvents: TimelineEvent[],
+  limit = 10,
+): TimelineEvent[] {
+  return timelineEvents
+    .filter((e) => e.observableBy.includes(agentId))
+    .slice(-limit)
+}
+
+/**
+ * 格式化时间线事件为简短描述(供上下文拼接使用)。
+ */
+export function formatTimelineEvent(event: TimelineEvent): string {
+  const targetDesc = event.targetName ? ` → ${event.targetName}` : ""
+  const visibilityTag =
+    event.observableBy.length > 2
+      ? "[公开]"
+      : event.observableBy.length === 2
+        ? "[私聊]"
+        : "[内心]"
+  return `第${event.round + 1}轮 ${visibilityTag} ${event.actorName}${targetDesc} [${event.actionType}]:${event.content}`
+}
+
 /**
  * 构建 Agent 决策时的上下文文本。
  *
- * 包含当前场景、Agent 身份、认知边界、当前状态、人际关系、近期事件与世界规则。
- * CharacterAura 的字段以实际源码为准,不存在的字段会被跳过。
+ * 包含当前场景、Agent 身份(含性格与说话风格)、认知边界、记忆/情感、人际关系、可见时间线事件与世界规则。
  */
 export function buildAgentContext(
   agent: NovelAgent,
   node: StoryNode,
   recentEvents: string[],
   worldRules: string,
+  visibleTimelineEvents?: TimelineEvent[],
 ): string {
   const sections: string[] = []
 
@@ -133,6 +258,12 @@ export function buildAgentContext(
   if (agent.soul) {
     sections.push(`灵魂:${agent.soul}`)
   }
+  if (agent.personality.length > 0) {
+    sections.push(`性格关键词:${agent.personality.join("、")}`)
+  }
+  if (agent.speakingStyle) {
+    sections.push(`说话风格:${agent.speakingStyle}`)
+  }
 
   // 光环各字段(以实际源码为准,不存在的字段跳过)
   if (agent.aura) {
@@ -151,16 +282,15 @@ export function buildAgentContext(
   }
 
   // ── 认知边界 ──
-  if (agent.cognition) {
-    sections.push("")
-    sections.push("【认知边界】")
-    if (agent.cognition.knows.length > 0) {
-      sections.push(`已知:${agent.cognition.knows.join(";")}`)
-    }
-    if (agent.cognition.doesNotKnow.length > 0) {
-      sections.push(`未知:${agent.cognition.doesNotKnow.join(";")}`)
-    }
+  sections.push("")
+  sections.push("【认知边界】")
+  if (agent.knowledgeScope.length > 0) {
+    sections.push(`你知道的信息:${agent.knowledgeScope.join(";")}`)
+  }
+  if (agent.cognition?.doesNotKnow && agent.cognition.doesNotKnow.length > 0) {
+    sections.push(`你不知道的信息:${agent.cognition.doesNotKnow.join(";")}`)
   }
+  sections.push("【重要提醒】你只能基于你知道的信息行动,你不知道的事情绝对不能使用,绝不能表现出全知视角。")
 
   // ── 当前状态 ──
   sections.push("")
@@ -168,18 +298,18 @@ export function buildAgentContext(
   sections.push(`当前目标:${agent.currentGoal}`)
   sections.push(`情绪状态:${agent.emotionalState}`)
 
-  // ── 人际关系 ──
-  if (agent.relationships.size > 0) {
+  // ── 人际关系与情感(从 memory.sentiments 读取) ──
+  if (agent.memory.sentiments.size > 0) {
     sections.push("")
-    sections.push("【人际关系】")
-    for (const relation of agent.relationships.values()) {
-      sections.push(
-        `对 ${relation.targetId}:${relation.relationType}(情感值 ${relation.sentiment})`,
-      )
+    sections.push("【你对其他角色的情感】")
+    for (const [otherId, value] of agent.memory.sentiments.entries()) {
+      if (otherId === agent.characterId) continue
+      // 尝试找到角色名
+      sections.push(`对角色[${otherId}]:好感度 ${value}`)
     }
   }
 
-  // ── 近期事件 ──
+  // ── 近期事件(旧接口字符串列表,保留兼容) ──
   if (recentEvents.length > 0) {
     sections.push("")
     sections.push("【近期事件】")
@@ -188,6 +318,15 @@ export function buildAgentContext(
     }
   }
 
+  // ── 你能观察到的时间线事件(新引擎) ──
+  if (visibleTimelineEvents && visibleTimelineEvents.length > 0) {
+    sections.push("")
+    sections.push("【你观察到的最近事件】")
+    for (const ev of visibleTimelineEvents) {
+      sections.push(`- ${formatTimelineEvent(ev)}`)
+    }
+  }
+
   // ── 世界规则 ──
   if (worldRules) {
     sections.push("")

+ 742 - 182
src/lib/novel/story-simulation/simulation-engine.ts

@@ -1,13 +1,22 @@
 import type { ChatMessage } from "@/lib/llm-client"
 import { streamChat } from "@/lib/llm-client"
 import type { LlmConfig } from "@/stores/wiki-store"
-import { buildAgentContext } from "@/lib/novel/story-simulation/agent-profile-builder"
+import {
+  buildAgentContext,
+  getVisibleEvents,
+} from "@/lib/novel/story-simulation/agent-profile-builder"
 import type {
+  ActionVisibility,
   AgentAction,
+  AgentActionType,
+  EventImpact,
   ExtractionResult,
   NovelAgent,
   SimulationEvent,
   SimulationInput,
+  SimulationState,
+  StoryNode,
+  TimelineEvent,
 } from "@/lib/novel/story-simulation/types"
 import { calcMaxRoundsPerNode } from "@/lib/novel/story-simulation/types"
 
@@ -18,8 +27,15 @@ export interface SimulationCallbacks {
   onProgress: (progress: number, label: string) => void
   onComplete: (events: SimulationEvent[]) => void
   onError: (error: Error) => void
+  /** 新引擎时间线事件回调 */
+  onTimelineEvent?: (event: TimelineEvent) => void
 }
 
+// ── 常量 ──
+
+const MAX_ROUNDS_PER_NODE_FALLBACK = 3
+const REACT_CHAIN_LIMIT = 2
+
 // ── 内部辅助:将 streamChat 的流式回调收拢为一个完整字符串 ──
 
 async function collectStream(
@@ -49,9 +65,9 @@ async function collectStream(
   return result
 }
 
-// ── 内部辅助:Agent 快照(避免事件间共享可变状态) ──
+// ── 内部辅助:Agent 深拷贝(避免事件间共享可变状态) ──
 
-function snapshotAgent(agent: NovelAgent): NovelAgent {
+function cloneAgent(agent: NovelAgent): NovelAgent {
   return {
     ...agent,
     knownFacts: new Set(agent.knownFacts),
@@ -61,39 +77,96 @@ function snapshotAgent(agent: NovelAgent): NovelAgent {
         { ...v },
       ]),
     ),
+    memory: {
+      observedEvents: [...agent.memory.observedEvents],
+      knownSecrets: new Set(agent.memory.knownSecrets),
+      sentiments: new Map(agent.memory.sentiments),
+      recentDecisions: [...agent.memory.recentDecisions],
+    },
+    knowledgeScope: [...agent.knowledgeScope],
+    personality: [...agent.personality],
+  }
+}
+
+function cloneAgentsToMap(agents: NovelAgent[]): Map<string, NovelAgent> {
+  const map = new Map<string, NovelAgent>()
+  for (const a of agents) {
+    map.set(a.characterId, cloneAgent(a))
   }
+  return map
+}
+
+// ── 内部辅助:生成唯一 ID ──
+
+let eventCounter = 0
+function nextEventId(): string {
+  eventCounter++
+  return `evt_${Date.now()}_${eventCounter}`
 }
 
 // ── 内部辅助:构建 Agent 系统提示词 ──
 
 function buildSystemPrompt(agent: NovelAgent): string {
+  const personalityLine =
+    agent.personality.length > 0
+      ? `你的性格关键词:${agent.personality.join("、")}`
+      : ""
+  const styleLine = agent.speakingStyle
+    ? `你的说话风格:${agent.speakingStyle}`
+    : ""
+
   return [
-    `你正在扮演小说角色「${agent.name}」。`,
+    `你正在扮演小说中的真实角色「${agent.name}」。你不是AI助手,你就是这个角色本人。`,
+    "",
+    "【核心原则 - 必须严格遵守】",
+    "1. 你是小说中的真实角色,只能基于你知道的信息行动,绝不能使用你不知道的信息。",
+    "2. 绝对禁止全知视角:你不知道其他角色的内心想法,不知道没有发生在你面前的事情,不知道剧情走向。",
+    "3. 严格遵循你的性格特征、说话风格和行为逻辑,不要跳出角色。",
+    "4. 你的每个行为都应该有合理的动机,符合角色设定。",
     "",
-    "请严格遵循以下要求:",
-    "1. 以该角色的视角思考、感受和行动,不要跳出角色。",
-    "2. 严格遵循角色的性格特征、心智模型和决策方式。",
-    "3. 遵守认知边界:角色不知道的信息绝对不能使用,不能表现出全知视角。",
-    "4. 你的回复必须是一个 JSON 对象,格式如下:",
-    '   { "type": "行为类型", "target": "目标角色名(可选)", "content": "行为内容", "motivation": "动机说明" }',
+    personalityLine,
+    styleLine,
     "",
-    "行为类型(type)只能是以下之一:",
-    "- speak:对他人说话(target 为对话对象,可省略表示自言自语)",
-    "- act:执行一个行动",
-    "- react:对某人的行为做出反应(target 为反应对象)",
-    "- decide:做出一个决定",
-    "- investigate:调查或探索某事",
-    "- conflict:与某人发生冲突(target 为冲突对象)",
-    "- cooperate:与某人合作(target 为合作对象)",
-    "- withhold:隐瞒或保留信息",
+    "【行为类型说明】你只能选择以下一种行为类型:",
+    "- evaluate:评价某人或某事,表达你的看法和判断",
+    "- pushPlot:主动采取推动剧情发展的关键行动",
+    "- observe:观察周围环境、人物或事态(不改变现状,只是获取信息)",
+    "- react:对其他角色刚做出的行为做出即时反应",
+    "- speak:与其他角色对话(说出台词)",
+    "- ally:寻求结盟、合作、示好",
+    "- confront:对抗、质疑、挑衅",
+    "- conceal:隐瞒信息、假装不知道、掩饰真实想法",
+    "- investigate:调查、探索、打听消息",
     "",
-    "只输出 JSON 对象,不要输出任何其他文字。",
-  ].join("\n")
+    "【输出格式】你必须输出一个严格的JSON对象,不要输出任何其他文字,不要使用markdown代码块:",
+    "{",
+    '  "type": "行为类型(从上面列表选一个)",',
+    '  "content": "行为的具体内容/说的话/内心想法",',
+    '  "target": "目标角色名(可选,没有目标就不填)",',
+    '  "visibility": "all(所有人可见) 或 target_only(仅目标可见) 或 self(仅自己可见/内心活动)",',
+    '  "motivation": "你为什么做出这个行为的内心动机",',
+    '  "plot_push": "这个行为如何推动剧情向节点目标发展"',
+    "}",
+    "",
+    "【可见性规则】",
+    "- 公开的言行(speak/ally/confront/pushPlot的公开部分)用 all",
+    "- 私下对话(speak带target)用 target_only",
+    "- 内心想法(evaluate/observe的心理活动/conceal)用 self",
+    "",
+    "只输出JSON对象,不要输出任何其他文字。",
+  ]
+    .filter((line) => line !== null && line !== undefined)
+    .join("\n")
 }
 
 // ── 内部辅助:构建用户消息(上下文 + 指令) ──
 
-function buildUserMessage(context: string, injectionEvent?: string): string {
+function buildUserMessage(
+  context: string,
+  node: StoryNode,
+  agentName: string,
+  injectionEvent?: string,
+): string {
   const parts: string[] = [context]
   if (injectionEvent) {
     parts.push("")
@@ -101,7 +174,9 @@ function buildUserMessage(context: string, injectionEvent?: string): string {
     parts.push(injectionEvent)
   }
   parts.push("")
-  parts.push("请根据以上信息,以角色视角决定你接下来要做的一个行为,并输出 JSON。")
+  parts.push(
+    `当前是节点「${node.title}」,节点目标是:${node.goal}。请根据以上信息,以「${agentName}」的视角决定你接下来要做的一个行为,并严格按JSON格式输出。`,
+  )
   return parts.join("\n")
 }
 
@@ -145,169 +220,518 @@ function extractJson(text: string): string | null {
   return null
 }
 
-// ── 内部辅助:根据解析出的字段构建 AgentAction ──
+// ── 内部辅助:验证行为类型是否合法 ──
+
+const VALID_ACTION_TYPES: AgentActionType[] = [
+  "evaluate",
+  "pushPlot",
+  "observe",
+  "react",
+  "speak",
+  "ally",
+  "confront",
+  "conceal",
+  "investigate",
+]
+
+function isValidActionType(t: string): t is AgentActionType {
+  return (VALID_ACTION_TYPES as string[]).includes(t)
+}
 
-function buildAction(
-  type: string,
-  content: string,
-  target?: string,
-): AgentAction {
-  switch (type) {
-    case "speak":
-      return target !== undefined
-        ? { type: "speak" as const, target, content }
-        : { type: "speak" as const, content }
-    case "react":
-      return { type: "react" as const, target: target ?? "", content }
-    case "decide":
-      return { type: "decide" as const, content }
-    case "investigate":
-      return { type: "investigate" as const, content }
-    case "conflict":
-      return { type: "conflict" as const, target: target ?? "", content }
-    case "cooperate":
-      return { type: "cooperate" as const, target: target ?? "", content }
-    case "withhold":
-      return { type: "withhold" as const, content }
-    case "act":
-    default:
-      return { type: "act" as const, content }
-  }
+function isValidVisibility(v: string): v is ActionVisibility {
+  return v === "all" || v === "target_only" || v === "self"
 }
 
+// ── 内部辅助:解析 LLM 输出为行为(健壮:失败时作为 observe 处理) ──
+
 interface ParsedAction {
   action: AgentAction
   motivation: string
+  plotPush: string
+  visibility: ActionVisibility
 }
 
-// ── 内部辅助:解析 LLM 输出为行为(健壮:失败时作为 act 处理) ──
-
 function parseAgentAction(raw: string): ParsedAction {
+  const fallback: ParsedAction = {
+    action: {
+      type: "observe",
+      content: raw.trim().slice(0, 200) || "沉默地观察周围",
+      visibility: "self",
+    },
+    motivation: "",
+    plotPush: "",
+    visibility: "self",
+  }
+
   const jsonText = extractJson(raw)
   if (!jsonText) {
-    return { action: { type: "act", content: raw.trim() }, motivation: "" }
+    return fallback
   }
 
   try {
     const data = JSON.parse(jsonText) as Record<string, unknown>
-    const type = String(data.type ?? "act").toLowerCase()
-    const content = String(data.content ?? "")
-    const target = data.target !== undefined ? String(data.target) : undefined
-    const motivation = String(data.motivation ?? "")
+    const typeRaw = String(data.type ?? "observe").toLowerCase()
+    const type: AgentActionType = isValidActionType(typeRaw)
+      ? typeRaw
+      : "observe"
+    const content = String(data.content ?? "").trim() || fallback.action.content
+    const target =
+      data.target !== undefined && data.target !== null && data.target !== ""
+        ? String(data.target)
+        : undefined
+    const visRaw = String(data.visibility ?? "all").toLowerCase()
+    const visibility: ActionVisibility = isValidVisibility(visRaw)
+      ? visRaw
+      : type === "speak" && target
+        ? "target_only"
+        : type === "evaluate" || type === "observe" || type === "conceal"
+          ? "self"
+          : "all"
+    const motivation = String(data.motivation ?? "").trim()
+    const plotPush = String(data.plot_push ?? "").trim()
+
+    // 根据行为类型推断默认可见性(LLM未明确指定时)
+    let finalVisibility = visibility
+    if (!data.visibility) {
+      if (type === "evaluate" || type === "observe" || type === "conceal") {
+        finalVisibility = "self"
+      } else if ((type === "speak" || type === "ally" || type === "confront") && target) {
+        finalVisibility = "target_only"
+      } else {
+        finalVisibility = "all"
+      }
+    }
 
-    return { action: buildAction(type, content, target), motivation }
+    return {
+      action: {
+        type,
+        content,
+        target,
+        visibility: finalVisibility,
+        motivation: motivation || undefined,
+        plot_push: plotPush || undefined,
+      },
+      motivation,
+      plotPush,
+      visibility: finalVisibility,
+    }
   } catch {
-    return { action: { type: "act", content: raw.trim() }, motivation: "" }
+    return fallback
   }
 }
 
 // ── 内部辅助:根据名字或 ID 查找目标 Agent ──
 
 function resolveTarget(
-  targetName: string,
-  agents: NovelAgent[],
+  targetName: string | undefined,
+  agents: Map<string, NovelAgent>,
 ): NovelAgent | undefined {
-  return agents.find(
-    (a) => a.name === targetName || a.characterId === targetName,
-  )
+  if (!targetName) return undefined
+  for (const agent of agents.values()) {
+    if (agent.name === targetName || agent.characterId === targetName) {
+      return agent
+    }
+  }
+  return undefined
 }
 
-// ── 内部辅助:应用行为效果,返回状态变更描述 ──
+// ── 内部辅助:根据可见性确定 observableBy 列表 ──
 
-function applyAction(
-  agent: NovelAgent,
-  parsed: ParsedAction,
-  allAgents: NovelAgent[],
+function determineObservableBy(
+  actor: NovelAgent,
+  target: NovelAgent | undefined,
+  visibility: ActionVisibility,
+  allAgents: Map<string, NovelAgent>,
 ): string[] {
-  const changes: string[] = []
-  const { action, motivation } = parsed
+  switch (visibility) {
+    case "self":
+      return [actor.characterId]
+    case "target_only":
+      if (target) {
+        return [actor.characterId, target.characterId]
+      }
+      return [actor.characterId]
+    case "all":
+    default: {
+      const ids: string[] = []
+      for (const id of allAgents.keys()) {
+        ids.push(id)
+      }
+      return ids
+    }
+  }
+}
+
+// ── 内部辅助:计算事件对角色的影响 ──
 
-  if (motivation) {
-    changes.push(`动机:${motivation}`)
+function computeImpacts(
+  actor: NovelAgent,
+  parsed: ParsedAction,
+  target: NovelAgent | undefined,
+): EventImpact[] {
+  const impacts: EventImpact[] = []
+  const { action } = parsed
+
+  // 对目标的情感影响
+  if (target) {
+    switch (action.type) {
+      case "ally":
+        impacts.push({
+          characterId: target.characterId,
+          type: "relationship",
+          detail: `${actor.name}向${target.name}示好结盟,好感度上升`,
+        })
+        break
+      case "confront":
+        impacts.push({
+          characterId: target.characterId,
+          type: "relationship",
+          detail: `${actor.name}与${target.name}对抗,好感度下降`,
+        })
+        break
+      case "speak":
+        impacts.push({
+          characterId: target.characterId,
+          type: "knowledge",
+          detail: `${target.name}听到了${actor.name}说的话`,
+        })
+        break
+      case "react":
+        impacts.push({
+          characterId: target.characterId,
+          type: "sentiment",
+          detail: `${actor.name}对${target.name}的行为做出了反应`,
+        })
+        break
+    }
   }
 
+  // 对自己的影响
   switch (action.type) {
-    case "speak":
-    case "investigate": {
-      agent.knownFacts.add(action.content)
-      changes.push(`${agent.name} 获知新信息`)
+    case "investigate":
+    case "observe":
+      impacts.push({
+        characterId: actor.characterId,
+        type: "knowledge",
+        detail: `${actor.name}获取了新信息`,
+      })
       break
-    }
-    case "conflict": {
-      const targetAgent = resolveTarget(action.target, allAgents)
-      if (targetAgent) {
-        const relation = agent.relationships.get(targetAgent.characterId)
-        if (relation) {
-          relation.sentiment = Math.max(-100, relation.sentiment - 20)
-          relation.relationType = "hostile"
-          changes.push(
-            `${agent.name} 对 ${action.target} 的好感度下降 20,关系变为敌对`,
-          )
-        }
-      }
-      agent.emotionalState = "tense"
-      changes.push(`${agent.name} 情绪变为紧张`)
+    case "conceal":
+      impacts.push({
+        characterId: actor.characterId,
+        type: "knowledge",
+        detail: `${actor.name}隐藏了某些信息`,
+      })
       break
-    }
-    case "cooperate": {
-      const targetAgent = resolveTarget(action.target, allAgents)
-      if (targetAgent) {
-        const relation = agent.relationships.get(targetAgent.characterId)
-        if (relation) {
-          relation.sentiment = Math.min(100, relation.sentiment + 15)
-          relation.relationType = "ally"
-          changes.push(
-            `${agent.name} 对 ${action.target} 的好感度上升 15,关系变为盟友`,
-          )
+    case "evaluate":
+      impacts.push({
+        characterId: actor.characterId,
+        type: "sentiment",
+        detail: `${actor.name}形成了看法/评价`,
+      })
+      break
+  }
+
+  // 公开事件对所有人产生知识影响
+  if (action.visibility === "all") {
+    impacts.push({
+      characterId: "__all__",
+      type: "knowledge",
+      detail: `所有人都观察到了${actor.name}的公开行为`,
+    })
+  }
+
+  return impacts
+}
+
+// ── 内部辅助:将事件应用到 Agent 记忆 ──
+
+function applyEventToMemory(
+  agent: NovelAgent,
+  event: TimelineEvent,
+): void {
+  agent.memory.observedEvents.push(event.id)
+
+  // 处理影响
+  for (const impact of event.impacts) {
+    if (impact.characterId === "__all__" || impact.characterId === agent.characterId) {
+      if (impact.type === "knowledge") {
+        // 知识类影响:加入到 knowledgeScope
+        if (!agent.knowledgeScope.includes(impact.detail)) {
+          agent.knowledgeScope.push(impact.detail)
+        }
+      } else if (impact.type === "sentiment") {
+        // 情感变化
+        agent.emotionalState = impact.detail.includes("正面") ? "positive" : agent.emotionalState
+      } else if (impact.type === "relationship" && event.targetId) {
+        // 关系变化
+        const current = agent.memory.sentiments.get(event.targetId) ?? 0
+        let delta = 0
+        if (impact.detail.includes("上升") || impact.detail.includes("结盟") || impact.detail.includes("示好")) {
+          delta = 10
+        } else if (impact.detail.includes("下降") || impact.detail.includes("对抗") || impact.detail.includes("冲突")) {
+          delta = -10
+        }
+        agent.memory.sentiments.set(
+          event.targetId,
+          Math.max(-100, Math.min(100, current + delta)),
+        )
+        // 同步到旧 relationships 字段以兼容
+        const oldRel = agent.relationships.get(event.targetId)
+        if (oldRel) {
+          oldRel.sentiment = Math.max(-100, Math.min(100, current + delta))
+          oldRel.relationType = delta > 0 ? "ally" : delta < 0 ? "hostile" : oldRel.relationType
         }
       }
-      agent.emotionalState = "hopeful"
-      changes.push(`${agent.name} 情绪变为充满希望`)
-      break
     }
-    case "decide": {
-      agent.emotionalState = "determined"
-      changes.push(`${agent.name} 情绪变为坚定`)
-      break
+  }
+
+  // 记录最近决策(仅对行为发起者)
+  if (event.actorId === agent.characterId) {
+    agent.memory.recentDecisions.push(
+      `[R${event.round + 1}] ${event.actionType}: ${event.content.slice(0, 50)}`,
+    )
+    if (agent.memory.recentDecisions.length > 20) {
+      agent.memory.recentDecisions = agent.memory.recentDecisions.slice(-20)
     }
-    // act / react / withhold:无特殊状态变更
-    default:
-      break
   }
+}
+
+// ── 内部辅助:创建时间线事件 ──
+
+function createTimelineEvent(
+  actor: NovelAgent,
+  parsed: ParsedAction,
+  target: NovelAgent | undefined,
+  round: number,
+  nodeIndex: number,
+  allAgents: Map<string, NovelAgent>,
+): TimelineEvent {
+  const observableBy = determineObservableBy(
+    actor,
+    target,
+    parsed.visibility,
+    allAgents,
+  )
+  const impacts = computeImpacts(actor, parsed, target)
 
-  return changes
+  return {
+    id: nextEventId(),
+    round,
+    nodeIndex,
+    actorId: actor.characterId,
+    actorName: actor.name,
+    actionType: parsed.action.type,
+    content: parsed.action.content,
+    targetId: target?.characterId,
+    targetName: target?.name,
+    observableBy,
+    impacts,
+    timestamp: new Date().toISOString(),
+  }
 }
 
-// ── 内部辅助:将事件格式化为简短描述(供 recentEvents 使用) ──
+// ── 内部辅助:将 TimelineEvent 转换为旧 SimulationEvent(兼容报告生成) ──
 
-function formatEventDescription(event: SimulationEvent): string {
-  const { agent, action } = event
-  if (!agent || !action) return ""
-  const name = agent.name
-  switch (action.type) {
+function timelineEventToSimulationEvent(
+  tlEvent: TimelineEvent,
+  agent: NovelAgent,
+  parsed: ParsedAction,
+  node: StoryNode,
+  round: number,
+): SimulationEvent {
+  const stateChanges: string[] = []
+  if (parsed.motivation) {
+    stateChanges.push(`动机:${parsed.motivation}`)
+  }
+  if (parsed.plotPush) {
+    stateChanges.push(`剧情推动:${parsed.plotPush}`)
+  }
+  for (const impact of tlEvent.impacts) {
+    if (impact.characterId !== "__all__") {
+      stateChanges.push(impact.detail)
+    }
+  }
+
+  // 将新行为类型映射为旧 AgentAction 结构(扁平接口,直接赋值)
+  const action: AgentAction = {
+    type: parsed.action.type,
+    content: parsed.action.content,
+    target: parsed.action.target,
+    visibility: parsed.action.visibility,
+    motivation: parsed.motivation,
+    plot_push: parsed.plotPush,
+  }
+
+  return {
+    type: "agent-action",
+    agent: cloneAgent(agent),
+    action,
+    round,
+    node,
+    stateChanges,
+    timestamp: tlEvent.timestamp,
+    timelineEvent: tlEvent,
+  }
+}
+
+// ── 内部辅助:为旧 SimulationEvent 生成行为描述 ──
+
+function formatActionDescription(action: AgentAction, agentName: string): string {
+  const { type, content, target } = action
+  switch (type) {
     case "speak":
-      return action.target
-        ? `${name} 对 ${action.target} 说:「${action.content}」`
-        : `${name} 自言自语:「${action.content}」`
-    case "act":
-      return `${name} 行动:${action.content}`
+      return target
+        ? `${agentName} 对 ${target} 说:「${content}」`
+        : `${agentName} 说:「${content}」`
+    case "evaluate":
+      return `${agentName} 心中评价:${content}`
+    case "pushPlot":
+      return `${agentName} 采取行动:${content}`
+    case "observe":
+      return `${agentName} 观察到:${content}`
     case "react":
-      return `${name} 对 ${action.target} 做出反应:${action.content}`
-    case "decide":
-      return `${name} 做出决定:${action.content}`
+      return target
+        ? `${agentName} 对 ${target} 的反应:${content}`
+        : `${agentName} 做出反应:${content}`
+    case "ally":
+      return target
+        ? `${agentName} 向 ${target} 示好结盟:${content}`
+        : `${agentName} 寻求合作:${content}`
+    case "confront":
+      return target
+        ? `${agentName} 与 ${target} 对抗:${content}`
+        : `${agentName} 采取对抗姿态:${content}`
+    case "conceal":
+      return `${agentName} 隐瞒了内心想法:${content}`
     case "investigate":
-      return `${name} 调查:${action.content}`
+      return `${agentName} 调查:${content}`
+    case "act":
+      return `${agentName} 行动:${content}`
+    case "decide":
+      return `${agentName} 做出决定:${content}`
     case "conflict":
-      return `${name} 与 ${action.target} 发生冲突:${action.content}`
+      return target
+        ? `${agentName} 与 ${target} 发生冲突:${content}`
+        : `${agentName} 冲突:${content}`
     case "cooperate":
-      return `${name} 与 ${action.target} 合作:${action.content}`
+      return target
+        ? `${agentName} 与 ${target} 合作:${content}`
+        : `${agentName} 合作:${content}`
     case "withhold":
-      return `${name} 隐瞒信息:${action.content}`
+      return `${agentName} 隐瞒信息:${content}`
+    default:
+      return `${agentName}:${content}`
+  }
+}
+
+// ── 内部辅助:检查节点是否达成目标(简单启发式) ──
+
+function isNodeGoalReached(
+  node: StoryNode,
+  nodeTimelineEvents: TimelineEvent[],
+  maxRounds: number,
+  currentRound: number,
+): boolean {
+  // 达到最大轮次
+  if (currentRound >= maxRounds - 1) {
+    return true
+  }
+  // 启发式:产生了 pushPlot 类型的关键事件(剧情推动行为)
+  const pushPlotCount = nodeTimelineEvents.filter(
+    (e) => e.actionType === "pushPlot",
+  ).length
+  if (pushPlotCount >= 2) {
+    return true
   }
-  return `${name} 行为`
+  // 启发式:节点内事件数足够多(>=6个事件表示有足够互动)
+  if (nodeTimelineEvents.length >= 6) {
+    return true
+  }
+  return false
+}
+
+// ── 内部辅助:单个 Agent 决策并产生事件 ──
+
+async function agentDecideAndAct(
+  agent: NovelAgent,
+  node: StoryNode,
+  state: SimulationState,
+  llmConfig: LlmConfig,
+  extraction: ExtractionResult,
+  recentEventDescs: string[],
+  injectionEvent: string | undefined,
+  signal?: AbortSignal,
+): Promise<{ parsed: ParsedAction; tlEvent: TimelineEvent; simEvent: SimulationEvent } | null> {
+  // 1. 观察:筛选该 Agent 可见的时间线事件
+  const visibleEvents = getVisibleEvents(
+    agent.characterId,
+    state.timelineEvents,
+    10,
+  )
+
+  // 2. 构建上下文(基于认知边界)
+  const context = buildAgentContext(
+    agent,
+    node,
+    recentEventDescs.slice(-8),
+    extraction.worldRules,
+    visibleEvents,
+  )
+
+  // 3. 构建 LLM 消息
+  const messages: ChatMessage[] = [
+    { role: "system", content: buildSystemPrompt(agent) },
+    {
+      role: "user",
+      content: buildUserMessage(context, node, agent.name, injectionEvent),
+    },
+  ]
+
+  // 4. 调用 LLM
+  const rawResponse = await collectStream(llmConfig, messages, signal)
+  if (signal?.aborted) return null
+
+  // 5. 解析行为
+  const parsed = parseAgentAction(rawResponse)
+
+  // 6. 解析目标
+  const target = resolveTarget(parsed.action.target, state.activeAgents)
+
+  // 7. 创建时间线事件
+  const tlEvent = createTimelineEvent(
+    agent,
+    parsed,
+    target,
+    state.currentRound,
+    node.index,
+    state.activeAgents,
+  )
+
+  // 8. 应用事件到相关 Agent 的记忆
+  for (const id of tlEvent.observableBy) {
+    const observer = state.activeAgents.get(id)
+    if (observer) {
+      applyEventToMemory(observer, tlEvent)
+    }
+  }
+
+  // 9. 写入状态
+  state.timelineEvents.push(tlEvent)
+
+  // 10. 转换为旧 SimulationEvent
+  const simEvent = timelineEventToSimulationEvent(
+    tlEvent,
+    agent,
+    parsed,
+    node,
+    state.currentRound,
+  )
+
+  return { parsed, tlEvent, simEvent }
 }
 
-// ── 主入口:运行仿真 ──
+// ── 主入口:运行仿真(多智能体,基于认知边界) ──
 
 export async function runSimulation(
   input: SimulationInput,
@@ -318,9 +742,20 @@ export async function runSimulation(
   const events: SimulationEvent[] = []
   const { agents, framework, wordBudget, llmConfig, injectionEvent } = input
   const totalNodes = framework.nodes.length
-  const maxRounds = calcMaxRoundsPerNode(wordBudget)
+  const maxRounds = Math.max(
+    1,
+    calcMaxRoundsPerNode(wordBudget) || MAX_ROUNDS_PER_NODE_FALLBACK,
+  )
   let aborted = false
 
+  // 初始化仿真状态
+  const state: SimulationState = {
+    currentRound: 0,
+    timelineEvents: [],
+    activeAgents: cloneAgentsToMap(agents),
+    worldState: {},
+  }
+
   try {
     for (let ni = 0; ni < totalNodes; ni++) {
       if (signal?.aborted) {
@@ -331,13 +766,28 @@ export async function runSimulation(
       const node = framework.nodes[ni]
 
       // 确定参与角色:从 involvedCharacters(角色名)过滤 agents
-      let nodeAgents = agents.filter((a) =>
-        node.involvedCharacters.includes(a.name),
-      )
-      // 防御:若过滤结果为空,使用全部 agents 避免空仿真
-      if (nodeAgents.length === 0) {
-        nodeAgents = agents
+      const nodeAgentIds = new Set<string>()
+      for (const a of agents) {
+        if (node.involvedCharacters.includes(a.name)) {
+          nodeAgentIds.add(a.characterId)
+        }
+      }
+      // 防御:若过滤结果为空,使用全部 agents
+      let nodeAgentList: NovelAgent[]
+      if (nodeAgentIds.size === 0) {
+        nodeAgentList = Array.from(state.activeAgents.values())
+      } else {
+        nodeAgentList = Array.from(state.activeAgents.values()).filter((a) =>
+          nodeAgentIds.has(a.characterId),
+        )
+      }
+
+      // 设置本轮活跃 Agent
+      const activeMap = new Map<string, NovelAgent>()
+      for (const a of nodeAgentList) {
+        activeMap.set(a.characterId, a)
       }
+      state.activeAgents = activeMap
 
       // 产出 node-start 事件
       const startEvent: SimulationEvent = {
@@ -355,7 +805,7 @@ export async function runSimulation(
 
       // 当前节点内的事件描述(供 recentEvents 使用)
       const recentEventDescs: string[] = []
-      let nodeActionCount = 0
+      const nodeTimelineEvents: TimelineEvent[] = []
 
       // 节点内多轮交互
       for (let round = 0; round < maxRounds; round++) {
@@ -364,64 +814,78 @@ export async function runSimulation(
           break
         }
 
-        for (const agent of nodeAgents) {
+        state.currentRound = round
+
+        // a. 每个活跃 Agent 观察并决策
+        for (const agent of nodeAgentList) {
           if (signal?.aborted) {
             aborted = true
             break
           }
 
-          // 构建上下文
-          const recentSlice = recentEventDescs.slice(-8)
-          const context = buildAgentContext(
-            agent,
+          // 获取最新状态的 agent 引用(可能被之前的事件更新了记忆)
+          const currentAgent = state.activeAgents.get(agent.characterId)
+          if (!currentAgent) continue
+
+          const result = await agentDecideAndAct(
+            currentAgent,
             node,
-            recentSlice,
-            extraction.worldRules,
+            state,
+            llmConfig,
+            extraction,
+            recentEventDescs,
+            round === 0 ? injectionEvent : undefined,
+            signal,
           )
 
-          // 构建 LLM 消息
-          const messages: ChatMessage[] = [
-            { role: "system", content: buildSystemPrompt(agent) },
-            {
-              role: "user",
-              content: buildUserMessage(context, injectionEvent),
-            },
-          ]
-
-          // 调用 LLM 生成行为决策
-          const rawResponse = await collectStream(llmConfig, messages, signal)
-          if (signal?.aborted) {
-            aborted = true
+          if (!result) {
+            if (signal?.aborted) {
+              aborted = true
+            }
             break
           }
 
-          // 解析行为
-          const parsed = parseAgentAction(rawResponse)
+          const { parsed, tlEvent, simEvent } = result
 
-          // 应用行为效果
-          const stateChanges = applyAction(agent, parsed, agents)
+          events.push(simEvent)
+          callbacks.onEvent(simEvent)
+          callbacks.onTimelineEvent?.(tlEvent)
 
-          // 产出 SimulationEvent
-          const event: SimulationEvent = {
-            type: "agent-action",
-            agent: snapshotAgent(agent),
-            action: parsed.action,
-            round,
-            node,
-            stateChanges,
-            timestamp: new Date().toISOString(),
+          recentEventDescs.push(
+            formatActionDescription(parsed.action, currentAgent.name),
+          )
+          nodeTimelineEvents.push(tlEvent)
+
+          // d. 如果行为有目标且目标是可见的,触发目标的 react(反应链,限制深度)
+          if (
+            tlEvent.targetId &&
+            parsed.action.type !== "react" &&
+            parsed.visibility !== "self"
+          ) {
+            const targetAgent = state.activeAgents.get(tlEvent.targetId)
+            if (targetAgent && REACT_CHAIN_LIMIT > 0) {
+              await triggerReaction(
+                targetAgent,
+                currentAgent,
+                tlEvent,
+                node,
+                state,
+                llmConfig,
+                extraction,
+                recentEventDescs,
+                events,
+                callbacks,
+                nodeTimelineEvents,
+                signal,
+              )
+            }
           }
-          events.push(event)
-          callbacks.onEvent(event)
-
-          recentEventDescs.push(formatEventDescription(event))
-          nodeActionCount++
         }
 
         if (aborted) break
 
-        // 检查节点目标是否达成(简单启发式:事件数 >= 4 则完成)
-        if (nodeActionCount >= 4) {
+        // e. 检查节点目标是否达成
+        if (isNodeGoalReached(node, nodeTimelineEvents, maxRounds, round)) {
           break
         }
       }
@@ -454,3 +918,99 @@ export async function runSimulation(
     throw error
   }
 }
+
+// ── 内部辅助:触发目标 Agent 对事件的反应(react 行为) ──
+
+async function triggerReaction(
+  targetAgent: NovelAgent,
+  actor: NovelAgent,
+  triggerEvent: TimelineEvent,
+  node: StoryNode,
+  state: SimulationState,
+  llmConfig: LlmConfig,
+  extraction: ExtractionResult,
+  recentEventDescs: string[],
+  events: SimulationEvent[],
+  callbacks: SimulationCallbacks,
+  nodeTimelineEvents: TimelineEvent[],
+  signal?: AbortSignal,
+): Promise<void> {
+  if (signal?.aborted) return
+
+  // 构建反应专用上下文:强调对刚才事件的反应
+  const visibleEvents = getVisibleEvents(
+    targetAgent.characterId,
+    state.timelineEvents,
+    10,
+  )
+
+  const reactionNote = `\n\n【刚才发生的事情】\n${actor.name}刚刚对你做出了行为:[${triggerEvent.actionType}] ${triggerEvent.content}\n请你立即对此做出反应(react类型行为)。`
+
+  const baseContext = buildAgentContext(
+    targetAgent,
+    node,
+    recentEventDescs.slice(-8),
+    extraction.worldRules,
+    visibleEvents,
+  )
+
+  const context = baseContext + reactionNote
+
+  const messages: ChatMessage[] = [
+    { role: "system", content: buildSystemPrompt(targetAgent) },
+    {
+      role: "user",
+      content:
+        context +
+        `\n\n请以「${targetAgent.name}」的视角,对${actor.name}刚才的行为立即做出反应,输出JSON。`,
+    },
+  ]
+
+  try {
+    const rawResponse = await collectStream(llmConfig, messages, signal)
+    if (signal?.aborted) return
+
+    const parsed = parseAgentAction(rawResponse)
+    // 强制行为类型为 react
+    parsed.action.type = "react"
+    parsed.action.target = actor.name
+
+    const tlEvent = createTimelineEvent(
+      targetAgent,
+      parsed,
+      actor,
+      state.currentRound,
+      node.index,
+      state.activeAgents,
+    )
+
+    // 应用记忆
+    for (const id of tlEvent.observableBy) {
+      const observer = state.activeAgents.get(id)
+      if (observer) {
+        applyEventToMemory(observer, tlEvent)
+      }
+    }
+
+    state.timelineEvents.push(tlEvent)
+    nodeTimelineEvents.push(tlEvent)
+
+    const simEvent = timelineEventToSimulationEvent(
+      tlEvent,
+      targetAgent,
+      parsed,
+      node,
+      state.currentRound,
+    )
+
+    events.push(simEvent)
+    callbacks.onEvent(simEvent)
+    callbacks.onTimelineEvent?.(tlEvent)
+
+    recentEventDescs.push(
+      formatActionDescription(parsed.action, targetAgent.name),
+    )
+  } catch {
+    // 反应失败不中断主流程
+  }
+}

+ 134 - 33
src/lib/novel/story-simulation/types.ts

@@ -6,6 +6,135 @@ import type { LlmConfig } from "@/stores/wiki-store"
 // ── 仿真模式 ──
 export type SimulationMode = "event-driven" | "free-emergence" | "decision-tree" | "hybrid"
 
+// ── Agent 行为类型 ──
+export type AgentActionType =
+  | "evaluate"    // 角色评价/看法
+  | "pushPlot"    // 事态推动/主动行动
+  | "observe"     // 观察/感知
+  | "react"       // 对他人行为的反应
+  | "speak"       // 对话
+  | "ally"        // 结盟/合作
+  | "confront"    // 对抗
+  | "conceal"     // 隐瞒
+  | "investigate" // 调查
+  // 保留旧类型以兼容已有代码(新引擎不再产生)
+  | "act"
+  | "decide"
+  | "conflict"
+  | "cooperate"
+  | "withhold"
+
+export type ActionVisibility = "all" | "target_only" | "self"
+
+/**
+ * Agent 行为(扁平化结构,与 LLM 输出 JSON 对齐)
+ */
+export interface AgentAction {
+  type: AgentActionType
+  content: string
+  target?: string
+  /** 行为可见性:公开(all)/仅目标可见(target_only)/仅自己可见(self) */
+  visibility?: ActionVisibility
+  /** 行为动机 */
+  motivation?: string
+  /** 如何推动剧情 */
+  plot_push?: string
+}
+
+// ── 事件影响类型 ──
+export type EventImpactType = "sentiment" | "knowledge" | "relationship"
+
+export interface EventImpact {
+  characterId: string
+  type: EventImpactType
+  detail: string
+}
+
+// ── 时间线事件(新仿真引擎核心事件结构) ──
+export interface TimelineEvent {
+  id: string
+  round: number
+  nodeIndex: number
+  actorId: string
+  actorName: string
+  actionType: AgentActionType
+  content: string
+  targetId?: string
+  targetName?: string
+  /** 能观察到该事件的角色 ID 列表 */
+  observableBy: string[]
+  /** 事件对角色的影响 */
+  impacts: EventImpact[]
+  timestamp: string
+}
+
+// ── Agent 记忆 ──
+export interface AgentMemory {
+  /** 已观察到的事件 ID 列表 */
+  observedEvents: string[]
+  /** 已知秘密集合 */
+  knownSecrets: Set<string>
+  /** 对其他角色的情感值:key=角色ID, value=-100~100 */
+  sentiments: Map<string, number>
+  /** 最近决策记录 */
+  recentDecisions: string[]
+}
+
+// ── Agent ──
+export interface NovelAgent {
+  characterId: string
+  name: string
+  profile: string
+  aura: CharacterAura | null
+  cognition: { knows: string[]; doesNotKnow: string[] } | null
+  soul: string
+  currentGoal: string
+  emotionalState: string
+  /** @deprecated 使用 memory.sentiments 替代,保留以兼容旧代码 */
+  knownFacts: Set<string>
+  /** @deprecated 使用 memory.sentiments 替代,保留以兼容旧代码 */
+  relationships: Map<string, AgentRelation>
+  powerLevel: string
+  /** 新增:Agent 记忆 */
+  memory: AgentMemory
+  /** 新增:角色知道的信息范围 */
+  knowledgeScope: string[]
+  /** 新增:性格关键词数组 */
+  personality: string[]
+  /** 新增:说话风格描述 */
+  speakingStyle: string
+}
+
+export interface AgentRelation {
+  targetId: string
+  relationType: string
+  sentiment: number
+}
+
+// ── 仿真状态(新引擎核心状态) ──
+export interface SimulationState {
+  currentRound: number
+  timelineEvents: TimelineEvent[]
+  activeAgents: Map<string, NovelAgent>
+  worldState: Record<string, unknown>
+}
+
+// ── Agent 对话(采访/私聊) ──
+export interface AgentChatMessage {
+  id: string
+  role: "agent" | "user"
+  agentId?: string
+  agentName?: string
+  content: string
+  timestamp: string
+}
+
+export interface AgentChatSession {
+  agentId: string
+  agentName: string
+  messages: AgentChatMessage[]
+}
+
 // ── 提取结果 ──
 export interface ExtractionResult {
   characters: ExtractedCharacter[]
@@ -49,6 +178,8 @@ export interface ExtractedMemoryData {
 export interface StoryFramework {
   id: string
   title: string
+  /** 简短标题,不超过10字 */
+  shortTitle?: string
   premise: string
   targetWords: number
   simulationMode: SimulationMode
@@ -69,39 +200,7 @@ export interface StoryNode {
   expectedOutcome: string
 }
 
-// ── Agent ──
-export interface NovelAgent {
-  characterId: string
-  name: string
-  profile: string
-  aura: CharacterAura | null
-  cognition: { knows: string[]; doesNotKnow: string[] } | null
-  soul: string
-  currentGoal: string
-  emotionalState: string
-  knownFacts: Set<string>
-  relationships: Map<string, AgentRelation>
-  powerLevel: string
-}
-
-export interface AgentRelation {
-  targetId: string
-  relationType: string
-  sentiment: number
-}
-
-// ── Agent 行为 ──
-export type AgentAction =
-  | { type: "speak"; target?: string; content: string }
-  | { type: "act"; content: string }
-  | { type: "react"; target: string; content: string }
-  | { type: "decide"; content: string }
-  | { type: "investigate"; content: string }
-  | { type: "conflict"; target: string; content: string }
-  | { type: "cooperate"; target: string; content: string }
-  | { type: "withhold"; content: string }
-
-// ── 仿真事件 ──
+// ── 仿真事件(保留以兼容报告生成器和旧流程) ──
 export interface SimulationEvent {
   type: "agent-action" | "node-complete" | "node-start"
   agent?: NovelAgent
@@ -110,6 +209,8 @@ export interface SimulationEvent {
   node?: StoryNode
   stateChanges?: string[]
   timestamp: string
+  /** 关联的时间线事件(新引擎填充) */
+  timelineEvent?: TimelineEvent
 }
 
 // ── 推演报告 ──