|
|
@@ -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 {
|
|
|
+ // 反应失败不中断主流程
|
|
|
+ }
|
|
|
+}
|