package agent import ( "context" "encoding/json" "fmt" "sort" "strings" "time" "github.com/YspCoder/clawgo/pkg/tools" "github.com/YspCoder/clawgo/pkg/world" ) type WorldRuntime struct { store *world.Store engine *world.Engine profiles *tools.AgentProfileStore dispatcher *tools.AgentDispatcher manager *tools.AgentManager maxCatchUp int maxNPCPerTick int } func NewWorldRuntime(workspace string, profiles *tools.AgentProfileStore, dispatcher *tools.AgentDispatcher, manager *tools.AgentManager) *WorldRuntime { return &WorldRuntime{ store: world.NewStore(workspace), engine: world.NewEngine(), profiles: profiles, dispatcher: dispatcher, manager: manager, maxCatchUp: 3, maxNPCPerTick: 8, } } func (wr *WorldRuntime) Enabled() bool { if wr == nil { return false } profiles, err := wr.worldProfiles() return err == nil && len(profiles) > 0 } func (wr *WorldRuntime) Snapshot(limit int) (interface{}, error) { state, npcStates, err := wr.ensureState() if err != nil { return nil, err } events, err := wr.store.Events(limit) if err != nil { return nil, err } return wr.engine.Snapshot(state, npcStates, events, 0, limit), nil } func (wr *WorldRuntime) Tick(ctx context.Context, source string) (string, error) { res, err := wr.advance(ctx, world.WorldTickRequest{Source: source}) if err != nil { return "", err } return res.Text, nil } func (wr *WorldRuntime) PlayerGet() (map[string]interface{}, error) { state, _, err := wr.ensureState() if err != nil { return nil, err } return map[string]interface{}{ "player": state.Player, }, nil } func (wr *WorldRuntime) PlayerAction(ctx context.Context, args map[string]interface{}) (map[string]interface{}, error) { state, _, err := wr.ensureState() if err != nil { return nil, err } action := strings.ToLower(strings.TrimSpace(tools.MapStringArg(args, "action"))) if action == "" { return nil, fmt.Errorf("action is required") } player := state.Player if strings.TrimSpace(player.PlayerID) == "" { player = world.DefaultWorldState().Player } switch action { case "move": target := normalizeWorldID(firstNonEmpty(tools.MapStringArg(args, "location_id"), tools.MapStringArg(args, "target_location"))) if target == "" { return nil, fmt.Errorf("location_id is required") } current := state.Locations[player.CurrentLocation] if player.CurrentLocation != target && !runtimeContainsString(current.Neighbors, target) { return nil, fmt.Errorf("location_not_reachable") } player.CurrentLocation = target player.LastActionTick = state.Clock.Tick state.Player = player evt := world.WorldEvent{ ID: fmt.Sprintf("evt-player-move-%d", time.Now().UnixNano()), Type: "player_move", Source: "player", ActorID: player.PlayerID, LocationID: target, Content: target, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } wr.engine.AppendRecentEvent(&state, evt, 20) if err := wr.store.SaveWorldState(state); err != nil { return nil, err } if err := wr.store.AppendWorldEvent(evt); err != nil { return nil, err } return map[string]interface{}{"ok": true, "message": fmt.Sprintf("moved to %s", target), "player": state.Player}, nil case "speak": target := strings.TrimSpace(firstNonEmpty(tools.MapStringArg(args, "target_npc_id"), tools.MapStringArg(args, "target_id"))) prompt := strings.TrimSpace(firstNonEmpty(tools.MapStringArg(args, "prompt"), tools.MapStringArg(args, "speech"))) content := prompt if target != "" { content = fmt.Sprintf("我在 %s 对 NPC %s 说:%s", firstNonEmpty(player.CurrentLocation, "commons"), target, firstNonEmpty(prompt, "你好。")) } return wr.handlePlayerInput(ctx, content, player.CurrentLocation, player.PlayerID) case "interact": target := strings.TrimSpace(firstNonEmpty(tools.MapStringArg(args, "target_entity_id"), tools.MapStringArg(args, "target_id"))) if target == "" { return nil, fmt.Errorf("target_entity_id is required") } prompt := strings.TrimSpace(firstNonEmpty(tools.MapStringArg(args, "prompt"), tools.MapStringArg(args, "speech"))) content := fmt.Sprintf("我在 %s 与实体 %s 互动:%s", firstNonEmpty(player.CurrentLocation, "commons"), target, firstNonEmpty(prompt, "检查它并告诉我发生了什么。")) return wr.handlePlayerInput(ctx, content, player.CurrentLocation, player.PlayerID) case "quest_accept", "quest_progress", "quest_complete": questID := strings.TrimSpace(firstNonEmpty(tools.MapStringArg(args, "quest_id"), tools.MapStringArg(args, "target_id"))) if questID == "" { return nil, fmt.Errorf("quest_id is required") } var content string switch action { case "quest_accept": content = fmt.Sprintf("接受任务 %s", questID) case "quest_progress": content = fmt.Sprintf("推进任务 %s %s", questID, strings.TrimSpace(tools.MapStringArg(args, "prompt"))) case "quest_complete": content = fmt.Sprintf("完成任务 %s", questID) } return wr.handlePlayerInput(ctx, strings.TrimSpace(content), player.CurrentLocation, player.PlayerID) default: return nil, fmt.Errorf("unsupported action: %s", action) } } func (wr *WorldRuntime) NPCList() ([]map[string]interface{}, error) { _, npcStates, err := wr.ensureState() if err != nil { return nil, err } profiles, err := wr.worldProfiles() if err != nil { return nil, err } out := make([]map[string]interface{}, 0, len(profiles)) for _, profile := range profiles { state := npcStates[profile.AgentID] out = append(out, map[string]interface{}{ "npc_id": profile.AgentID, "display_name": profile.Name, "persona": profile.Persona, "home_location": profile.HomeLocation, "current_location": state.CurrentLocation, "default_goals": append([]string(nil), profile.DefaultGoals...), "status": state.Status, "kind": profile.Kind, "perception_scope": profile.PerceptionScope, }) } sort.Slice(out, func(i, j int) bool { return fmt.Sprint(out[i]["npc_id"]) < fmt.Sprint(out[j]["npc_id"]) }) return out, nil } func (wr *WorldRuntime) NPCGet(id string) (map[string]interface{}, bool, error) { _, npcStates, err := wr.ensureState() if err != nil { return nil, false, err } profile, ok, err := wr.profiles.Get(id) if err != nil || !ok { return nil, false, err } state, ok := npcStates[profile.AgentID] if !ok { return nil, false, nil } return map[string]interface{}{ "profile": profile, "state": state, }, true, nil } func (wr *WorldRuntime) EntityList() ([]map[string]interface{}, error) { state, _, err := wr.ensureState() if err != nil { return nil, err } out := make([]map[string]interface{}, 0, len(state.Entities)) for _, entity := range state.Entities { out = append(out, map[string]interface{}{ "id": entity.ID, "name": entity.Name, "type": entity.Type, "location_id": entity.LocationID, "state": entity.State, }) } sort.Slice(out, func(i, j int) bool { return fmt.Sprint(out[i]["id"]) < fmt.Sprint(out[j]["id"]) }) return out, nil } func (wr *WorldRuntime) EntityGet(id string) (map[string]interface{}, bool, error) { state, _, err := wr.ensureState() if err != nil { return nil, false, err } entity, ok := state.Entities[strings.TrimSpace(id)] if !ok { return nil, false, nil } return map[string]interface{}{ "id": entity.ID, "name": entity.Name, "type": entity.Type, "location_id": entity.LocationID, "state": entity.State, }, true, nil } func (wr *WorldRuntime) WorldGet() (map[string]interface{}, error) { state, _, err := wr.ensureState() if err != nil { return nil, err } return map[string]interface{}{ "world_state": state, }, nil } func (wr *WorldRuntime) EventLog(limit int) ([]map[string]interface{}, error) { events, err := wr.store.Events(limit) if err != nil { return nil, err } out := make([]map[string]interface{}, 0, len(events)) for _, evt := range events { out = append(out, map[string]interface{}{ "id": evt.ID, "type": evt.Type, "actor_id": evt.ActorID, "location_id": evt.LocationID, "content": evt.Content, "tick": evt.Tick, "created_at": evt.CreatedAt, }) } return out, nil } func (wr *WorldRuntime) QuestList() ([]map[string]interface{}, error) { state, _, err := wr.ensureState() if err != nil { return nil, err } out := make([]map[string]interface{}, 0, len(state.ActiveQuests)) for _, quest := range state.ActiveQuests { out = append(out, map[string]interface{}{ "id": quest.ID, "title": quest.Title, "status": quest.Status, "owner_npc_id": quest.OwnerNPCID, "participants": append([]string(nil), quest.Participants...), "summary": quest.Summary, }) } sort.Slice(out, func(i, j int) bool { return fmt.Sprint(out[i]["id"]) < fmt.Sprint(out[j]["id"]) }) return out, nil } func (wr *WorldRuntime) QuestGet(id string) (map[string]interface{}, bool, error) { state, _, err := wr.ensureState() if err != nil { return nil, false, err } quest, ok := state.ActiveQuests[strings.TrimSpace(id)] if !ok { return nil, false, nil } return map[string]interface{}{ "id": quest.ID, "title": quest.Title, "status": quest.Status, "owner_npc_id": quest.OwnerNPCID, "participants": append([]string(nil), quest.Participants...), "summary": quest.Summary, }, true, nil } func (wr *WorldRuntime) CreateQuest(ctx context.Context, args map[string]interface{}) (map[string]interface{}, error) { _ = ctx state, _, err := wr.ensureState() if err != nil { return nil, err } questID := normalizeWorldID(tools.MapStringArg(args, "id")) if questID == "" { questID = normalizeWorldID(tools.MapStringArg(args, "title")) } if questID == "" { return nil, fmt.Errorf("id or title is required") } quest := state.ActiveQuests[questID] quest.ID = questID quest.Title = firstNonEmpty(strings.TrimSpace(tools.MapStringArg(args, "title")), quest.Title, questID) quest.Status = firstNonEmpty(strings.TrimSpace(tools.MapStringArg(args, "status")), quest.Status, "open") quest.OwnerNPCID = firstNonEmpty(strings.TrimSpace(tools.MapStringArg(args, "owner_npc_id")), quest.OwnerNPCID) quest.Participants = append([]string(nil), tools.MapStringListArg(args, "participants")...) quest.Summary = firstNonEmpty(strings.TrimSpace(tools.MapStringArg(args, "summary")), quest.Summary) state.ActiveQuests[questID] = quest if err := wr.store.SaveWorldState(state); err != nil { return nil, err } evt := world.WorldEvent{ ID: fmt.Sprintf("evt-quest-%d", time.Now().UnixNano()), Type: "quest_updated", Source: "world", ActorID: quest.OwnerNPCID, Content: questID, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } _ = wr.store.AppendWorldEvent(evt) return map[string]interface{}{"quest_id": questID}, nil } func (wr *WorldRuntime) CreateNPC(ctx context.Context, args map[string]interface{}) (map[string]interface{}, error) { _ = ctx if wr == nil || wr.profiles == nil || wr.store == nil { return nil, fmt.Errorf("world runtime not ready") } npcID := normalizeWorldID(tools.MapStringArg(args, "npc_id")) if npcID == "" { return nil, fmt.Errorf("npc_id is required") } persona := strings.TrimSpace(tools.MapStringArg(args, "persona")) home := normalizeWorldID(tools.MapStringArg(args, "home_location")) goals := tools.MapStringListArg(args, "default_goals") if persona == "" || home == "" || len(goals) == 0 { return nil, fmt.Errorf("persona, home_location, default_goals are required") } profile, err := wr.profiles.Upsert(tools.AgentProfile{ AgentID: npcID, Name: strings.TrimSpace(tools.MapStringArg(args, "name")), Kind: "npc", Persona: persona, HomeLocation: home, DefaultGoals: goals, Role: strings.TrimSpace(tools.MapStringArg(args, "role")), MemoryNamespace: npcID, Status: "active", }) if err != nil { return nil, err } state, npcStates, err := wr.ensureState() if err != nil { return nil, err } npcStates[profile.AgentID] = wr.engine.EnsureNPCState(wr.profileBlueprint(*profile), npcStates[profile.AgentID]) if err := wr.store.SaveNPCStates(npcStates); err != nil { return nil, err } evt := world.WorldEvent{ ID: fmt.Sprintf("evt-create-npc-%d", time.Now().UnixNano()), Type: "npc_created", Source: "world", ActorID: profile.AgentID, LocationID: home, Content: profile.Name, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } wr.engine.AppendRecentEvent(&state, evt, 20) if err := wr.store.SaveWorldState(state); err != nil { return nil, err } if err := wr.store.AppendWorldEvent(evt); err != nil { return nil, err } return map[string]interface{}{"npc_id": profile.AgentID}, nil } func (wr *WorldRuntime) CreateEntity(ctx context.Context, args map[string]interface{}) (map[string]interface{}, error) { _ = ctx state, _, err := wr.ensureState() if err != nil { return nil, err } entityID := normalizeWorldID(firstNonEmpty(tools.MapStringArg(args, "entity_id"), tools.MapStringArg(args, "id"), tools.MapStringArg(args, "name"))) locationID := normalizeWorldID(firstNonEmpty(tools.MapStringArg(args, "location_id"), tools.MapStringArg(args, "home_location"))) if entityID == "" || locationID == "" { return nil, fmt.Errorf("entity_id and location_id are required") } if _, ok := state.Locations[locationID]; !ok { return nil, fmt.Errorf("unknown location_id: %s", locationID) } entity := state.Entities[entityID] entity.ID = entityID entity.Name = firstNonEmpty(tools.MapStringArg(args, "name"), entity.Name, entityID) entity.Type = firstNonEmpty(tools.MapStringArg(args, "entity_type"), entity.Type, "landmark") entity.LocationID = locationID if entity.State == nil { entity.State = map[string]interface{}{} } state.Entities[entityID] = entity if err := wr.store.SaveWorldState(state); err != nil { return nil, err } evt := world.WorldEvent{ ID: fmt.Sprintf("evt-entity-%d", time.Now().UnixNano()), Type: "entity_created", Source: "world", LocationID: locationID, Content: entityID, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } _ = wr.store.AppendWorldEvent(evt) return map[string]interface{}{"entity_id": entityID}, nil } func (wr *WorldRuntime) handleUserQuestInput(content string) (string, bool, error) { content = strings.TrimSpace(content) if content == "" { return "", false, nil } state, _, err := wr.ensureState() if err != nil { return "", false, err } lower := strings.ToLower(content) switch { case containsAnyQuestPhrase(lower, "list quests", "show quests", "任务列表", "查看任务", "有哪些任务"): if len(state.ActiveQuests) == 0 { return "当前没有活跃任务。", true, nil } items := make([]string, 0, len(state.ActiveQuests)) for _, quest := range state.ActiveQuests { items = append(items, fmt.Sprintf("%s [%s]", firstNonEmpty(quest.Title, quest.ID), firstNonEmpty(quest.Status, "open"))) } sort.Strings(items) return "当前任务:\n" + strings.Join(items, "\n"), true, nil case containsAnyQuestPhrase(lower, "accept quest", "take quest", "接受任务", "接取任务"): id := resolveQuestReference(content, state) if id == "" { return "没有找到要接受的任务。", true, nil } quest := state.ActiveQuests[id] quest.Status = "accepted" state.ActiveQuests[id] = quest if err := wr.saveQuestMutation(state, quest, "quest_accepted"); err != nil { return "", true, err } return fmt.Sprintf("已接受任务:%s", firstNonEmpty(quest.Title, quest.ID)), true, nil case containsAnyQuestPhrase(lower, "complete quest", "finish quest", "完成任务"): id := resolveQuestReference(content, state) if id == "" { return "没有找到要完成的任务。", true, nil } quest := state.ActiveQuests[id] quest.Status = "completed" state.ActiveQuests[id] = quest if err := wr.saveQuestMutation(state, quest, "quest_completed"); err != nil { return "", true, err } return fmt.Sprintf("已完成任务:%s", firstNonEmpty(quest.Title, quest.ID)), true, nil case containsAnyQuestPhrase(lower, "progress quest", "advance quest", "推进任务", "更新任务进度"): id := resolveQuestReference(content, state) if id == "" { return "没有找到要推进的任务。", true, nil } quest := state.ActiveQuests[id] quest.Status = "in_progress" if summary := extractTailAfterQuestVerb(content); summary != "" { quest.Summary = summary } state.ActiveQuests[id] = quest if err := wr.saveQuestMutation(state, quest, "quest_progressed"); err != nil { return "", true, err } return fmt.Sprintf("已推进任务:%s", firstNonEmpty(quest.Title, quest.ID)), true, nil default: return "", false, nil } } func (wr *WorldRuntime) saveQuestMutation(state world.WorldState, quest world.QuestState, eventType string) error { if err := wr.store.SaveWorldState(state); err != nil { return err } evt := world.WorldEvent{ ID: fmt.Sprintf("evt-%s-%d", eventType, time.Now().UnixNano()), Type: eventType, Source: "user", Content: quest.ID, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } return wr.store.AppendWorldEvent(evt) } func (wr *WorldRuntime) HandleUserInput(ctx context.Context, content, channel, chatID string) (string, error) { _ = channel _ = chatID if out, handled, err := wr.handleUserQuestInput(content); handled || err != nil { return out, err } out, err := wr.handlePlayerInput(ctx, content, "commons", "user") if err != nil { return "", err } return tools.MapStringArg(out, "message"), nil } func (wr *WorldRuntime) handlePlayerInput(ctx context.Context, content, locationID, actorID string) (map[string]interface{}, error) { if out, handled, err := wr.handleUserQuestInput(content); handled || err != nil { return map[string]interface{}{"ok": err == nil, "message": out}, err } res, err := wr.advance(ctx, world.WorldTickRequest{ Source: "player", UserInput: content, LocationID: locationID, ActorID: actorID, }) if err != nil { return nil, err } state, _, stateErr := wr.ensureState() if stateErr != nil { return nil, stateErr } return map[string]interface{}{ "ok": true, "message": res.Text, "player": state.Player, }, nil } func (wr *WorldRuntime) advance(ctx context.Context, req world.WorldTickRequest) (world.RenderedResult, error) { state, npcStates, err := wr.ensureState() if err != nil { return world.RenderedResult{}, err } catchUp := wr.computeCatchUp(state) if req.CatchUpTicks > 0 { catchUp = req.CatchUpTicks } if catchUp > wr.maxCatchUp { catchUp = wr.maxCatchUp } var recentEvents []world.WorldEvent for i := 0; i < catchUp; i++ { wr.engine.NextTick(&state) res, err := wr.runTick(ctx, &state, npcStates, nil, "background") if err != nil { return world.RenderedResult{}, err } recentEvents = append(recentEvents, res.RecentEvents...) } var userEvent *world.WorldEvent if strings.TrimSpace(req.UserInput) != "" { wr.engine.NextTick(&state) evt := wr.engine.BuildUserEvent(&state, req.UserInput, req.LocationID, req.ActorID) userEvent = &evt wr.engine.AppendRecentEvent(&state, evt, 20) if err := wr.store.AppendWorldEvent(evt); err != nil { return world.RenderedResult{}, err } } var visible []world.WorldEvent if userEvent != nil { visible = append(visible, *userEvent) } res, err := wr.runTick(ctx, &state, npcStates, visible, firstNonEmpty(req.Source, "user")) if err != nil { return world.RenderedResult{}, err } recentEvents = append(recentEvents, res.RecentEvents...) if err := wr.store.SaveNPCStates(npcStates); err != nil { return world.RenderedResult{}, err } if err := wr.store.SaveWorldState(state); err != nil { return world.RenderedResult{}, err } res.RecentEvents = recentEvents if strings.TrimSpace(res.Text) == "" { res.Text = wr.renderEvents(userEvent, recentEvents) } return res, nil } func (wr *WorldRuntime) runTick(ctx context.Context, state *world.WorldState, npcStates map[string]world.NPCState, seedEvents []world.WorldEvent, source string) (world.RenderedResult, error) { profiles, err := wr.worldProfiles() if err != nil { return world.RenderedResult{}, err } maxNPC := wr.maxNPCPerTick if maxNPC <= 0 { maxNPC = len(profiles) } intents := make([]world.ActionIntent, 0, maxNPC) appliedEvents := make([]world.WorldEvent, 0, maxNPC) count := 0 for _, profile := range profiles { if count >= maxNPC { break } npcState := npcStates[profile.AgentID] visible := wr.engine.VisibleEventsForNPC(*state, npcState, seedEvents, profile.PerceptionScope) if len(visible) == 0 && !wr.shouldWakeNPC(profile, npcState, state.Clock.Tick) { continue } intent, err := wr.decideNPCIntent(ctx, *state, profile, npcState, visible) if err != nil { return world.RenderedResult{}, err } intents = append(intents, intent) delta := wr.engine.ApplyIntent(state, &npcState, intent) if delta.Applied && strings.EqualFold(strings.TrimSpace(intent.Action), "delegate") && wr.manager != nil && strings.TrimSpace(intent.TargetAgent) != "" { _, _ = wr.manager.SendAgentMessage( profile.AgentID, strings.TrimSpace(intent.TargetAgent), "delegate", firstNonEmpty(intent.Speech, intent.TargetEntity, "delegated task"), "", ) } if !delta.Applied { rejected := world.WorldEvent{ ID: fmt.Sprintf("evt-rejected-%d", time.Now().UnixNano()), Type: "rejected_intent", Source: source, ActorID: intent.ActorID, LocationID: npcState.CurrentLocation, Content: delta.Reason, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } wr.engine.AppendRecentEvent(state, rejected, 20) if err := wr.store.AppendWorldEvent(rejected); err != nil { return world.RenderedResult{}, err } appliedEvents = append(appliedEvents, rejected) } else if delta.Event != nil { npcStates[profile.AgentID] = npcState wr.engine.AppendRecentEvent(state, *delta.Event, 20) if err := wr.store.AppendWorldEvent(*delta.Event); err != nil { return world.RenderedResult{}, err } appliedEvents = append(appliedEvents, *delta.Event) } count++ } return world.RenderedResult{ Text: wr.renderEvents(nil, appliedEvents), Tick: state.Clock.Tick, Intents: intents, RecentEvents: appliedEvents, }, nil } func (wr *WorldRuntime) decideNPCIntent(ctx context.Context, state world.WorldState, profile tools.AgentProfile, npcState world.NPCState, visible []world.WorldEvent) (world.ActionIntent, error) { if wr.dispatcher == nil { return wr.fallbackIntent(profile, npcState, visible, state), nil } worldSnapshot := map[string]interface{}{ "tick": state.Clock.Tick, "locations": state.Locations, "global_facts": state.GlobalFacts, } npcSnapshot := map[string]interface{}{ "npc_id": npcState.NPCID, "display_name": profile.Name, "persona": profile.Persona, "traits": append([]string(nil), profile.Traits...), "current_location": npcState.CurrentLocation, "goals_long_term": append([]string(nil), npcState.Goals.LongTerm...), "goals_short_term": append([]string(nil), npcState.Goals.ShortTerm...), } visibleMaps := make([]map[string]interface{}, 0, len(visible)) for _, evt := range visible { visibleMaps = append(visibleMaps, map[string]interface{}{ "id": evt.ID, "type": evt.Type, "actor_id": evt.ActorID, "location_id": evt.LocationID, "content": evt.Content, "tick": evt.Tick, }) } taskText := wr.buildDecisionTask(profile, npcState, visible) task, err := wr.dispatcher.DispatchTask(ctx, tools.AgentDispatchRequest{ Task: taskText, RunKind: "world_npc", AgentID: profile.AgentID, Origin: &tools.OriginRef{Channel: "world", ChatID: "world"}, WorldDecision: &tools.WorldDecisionContext{ WorldTick: state.Clock.Tick, WorldSnapshot: worldSnapshot, NPCSnapshot: npcSnapshot, VisibleEvents: visibleMaps, IntentSchemaVersion: "v1", }, }) if err != nil { return wr.fallbackIntent(profile, npcState, visible, state), nil } reply, err := wr.dispatcher.WaitReply(ctx, task.ID, 100*time.Millisecond) if err != nil { return wr.fallbackIntent(profile, npcState, visible, state), nil } intent, err := parseWorldIntent(reply.Result) if err != nil || strings.TrimSpace(intent.Action) == "" { return wr.fallbackIntent(profile, npcState, visible, state), nil } if strings.TrimSpace(intent.ActorID) == "" { intent.ActorID = profile.AgentID } return intent, nil } func (wr *WorldRuntime) fallbackIntent(profile tools.AgentProfile, npcState world.NPCState, visible []world.WorldEvent, state world.WorldState) world.ActionIntent { intent := world.ActionIntent{ ActorID: profile.AgentID, Action: "wait", } for _, evt := range visible { if evt.Type == "user_input" && evt.LocationID == npcState.CurrentLocation { speech := fmt.Sprintf("%s notices the user: %s", firstNonEmpty(profile.Name, profile.AgentID), strings.TrimSpace(evt.Content)) return world.ActionIntent{ ActorID: profile.AgentID, Action: "speak", Speech: speech, InternalReasoningSummary: "responded to nearby user activity", } } } for _, goal := range npcState.Goals.LongTerm { g := strings.ToLower(strings.TrimSpace(goal)) if strings.Contains(g, "patrol") { loc := state.Locations[npcState.CurrentLocation] if len(loc.Neighbors) > 0 { return world.ActionIntent{ ActorID: profile.AgentID, Action: "move", TargetLocation: loc.Neighbors[0], InternalReasoningSummary: "patrolling according to long-term goal", } } } if strings.Contains(g, "watch") || strings.Contains(g, "guard") || strings.Contains(g, "observe") { return world.ActionIntent{ ActorID: profile.AgentID, Action: "observe", InternalReasoningSummary: "maintains awareness of the surroundings", } } } return intent } func (wr *WorldRuntime) buildDecisionTask(profile tools.AgentProfile, npcState world.NPCState, visible []world.WorldEvent) string { payload := map[string]interface{}{ "npc_id": profile.AgentID, "display_name": profile.Name, "persona": profile.Persona, "traits": profile.Traits, "current_location": npcState.CurrentLocation, "long_term_goals": npcState.Goals.LongTerm, "short_term_goals": npcState.Goals.ShortTerm, "visible_events": visible, "allowed_actions": []string{"move", "speak", "observe", "interact", "delegate", "wait"}, "response_contract": "return JSON object with actor_id, action, target_location, target_entity, target_agent, speech, internal_reasoning_summary, proposed_effects", } data, _ := json.Marshal(payload) return string(data) } func (wr *WorldRuntime) ensureState() (world.WorldState, map[string]world.NPCState, error) { state, err := wr.store.LoadWorldState() if err != nil { return world.WorldState{}, nil, err } wr.engine.EnsureWorld(&state) npcStates, err := wr.store.LoadNPCStates() if err != nil { return world.WorldState{}, nil, err } profiles, err := wr.worldProfiles() if err != nil { return world.WorldState{}, nil, err } changed := false for _, profile := range profiles { current, exists := npcStates[profile.AgentID] next := wr.engine.EnsureNPCState(wr.profileBlueprint(profile), current) if !exists || strings.TrimSpace(current.NPCID) == "" || strings.TrimSpace(current.CurrentLocation) == "" { changed = true } npcStates[profile.AgentID] = next } if changed { if err := wr.store.SaveNPCStates(npcStates); err != nil { return world.WorldState{}, nil, err } } if err := wr.store.SaveWorldState(state); err != nil { return world.WorldState{}, nil, err } return state, npcStates, nil } func (wr *WorldRuntime) worldProfiles() ([]tools.AgentProfile, error) { if wr == nil || wr.profiles == nil { return nil, nil } items, err := wr.profiles.List() if err != nil { return nil, err } out := make([]tools.AgentProfile, 0, len(items)) for _, item := range items { if !strings.EqualFold(strings.TrimSpace(item.Status), "active") { continue } if !isWorldNPCProfile(item) { continue } out = append(out, item) } sort.Slice(out, func(i, j int) bool { return out[i].AgentID < out[j].AgentID }) return out, nil } func isWorldNPCProfile(profile tools.AgentProfile) bool { if !strings.EqualFold(strings.TrimSpace(profile.Kind), "npc") { return false } return strings.TrimSpace(profile.HomeLocation) != "" || strings.TrimSpace(profile.Persona) != "" || len(profile.DefaultGoals) > 0 || len(profile.WorldTags) > 0 } func (wr *WorldRuntime) profileBlueprint(profile tools.AgentProfile) world.NPCBlueprint { return world.NPCBlueprint{ NPCID: profile.AgentID, DisplayName: profile.Name, Kind: profile.Kind, Role: profile.Role, Persona: profile.Persona, Traits: append([]string(nil), profile.Traits...), Faction: profile.Faction, HomeLocation: firstNonEmpty(profile.HomeLocation, "commons"), DefaultGoals: append([]string(nil), profile.DefaultGoals...), PerceptionScope: profile.PerceptionScope, ScheduleHint: profile.ScheduleHint, WorldTags: append([]string(nil), profile.WorldTags...), MemoryNamespace: profile.MemoryNamespace, PromptFile: profile.PromptFile, } } func (wr *WorldRuntime) shouldWakeNPC(profile tools.AgentProfile, state world.NPCState, tick int64) bool { if tick == 0 { return true } if len(state.Goals.LongTerm) == 0 { return false } if state.LastActiveTick == 0 { return true } return tick-state.LastActiveTick >= 2 } func (wr *WorldRuntime) computeCatchUp(state world.WorldState) int { if state.Clock.LastAdvance <= 0 || state.Clock.TickDuration <= 0 { return 0 } delta := time.Now().Unix() - state.Clock.LastAdvance if delta <= state.Clock.TickDuration { return 0 } return int(delta / state.Clock.TickDuration) } func (wr *WorldRuntime) renderEvents(userEvent *world.WorldEvent, events []world.WorldEvent) string { lines := make([]string, 0, len(events)+1) if userEvent != nil && strings.TrimSpace(userEvent.Content) != "" { lines = append(lines, "世界感知到你的行动:"+strings.TrimSpace(userEvent.Content)) } for _, evt := range events { switch evt.Type { case "npc_speak": lines = append(lines, fmt.Sprintf("%s 说:%s", evt.ActorID, strings.TrimSpace(evt.Content))) case "npc_move": lines = append(lines, fmt.Sprintf("%s 移动到了 %s", evt.ActorID, strings.TrimSpace(evt.LocationID))) case "rejected_intent": lines = append(lines, fmt.Sprintf("%s 的行动被世界拒绝:%s", evt.ActorID, strings.TrimSpace(evt.Content))) case "npc_observe": lines = append(lines, fmt.Sprintf("%s 正在观察局势", evt.ActorID)) case "npc_interact": lines = append(lines, fmt.Sprintf("%s 发起了交互:%s", evt.ActorID, strings.TrimSpace(evt.Content))) case "npc_delegate": lines = append(lines, fmt.Sprintf("%s 发出了委托:%s", evt.ActorID, strings.TrimSpace(evt.Content))) } } if len(lines) == 0 { return "世界安静地推进了一拍。" } return strings.Join(lines, "\n") } func parseWorldIntent(raw string) (world.ActionIntent, error) { var intent world.ActionIntent if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &intent); err != nil { return world.ActionIntent{}, err } return intent, nil } func normalizeWorldID(in string) string { in = strings.TrimSpace(strings.ToLower(in)) if in == "" { return "" } var sb strings.Builder for _, r := range in { switch { case r >= 'a' && r <= 'z': sb.WriteRune(r) case r >= '0' && r <= '9': sb.WriteRune(r) case r == '-' || r == '_' || r == '.': sb.WriteRune(r) case r == ' ': sb.WriteRune('-') } } return strings.Trim(sb.String(), "-_.") } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func runtimeContainsString(items []string, target string) bool { target = strings.TrimSpace(target) for _, item := range items { if strings.TrimSpace(item) == target { return true } } return false } func containsAnyQuestPhrase(text string, needles ...string) bool { for _, needle := range needles { if strings.Contains(text, strings.ToLower(strings.TrimSpace(needle))) { return true } } return false } func resolveQuestReference(content string, state world.WorldState) string { content = strings.ToLower(strings.TrimSpace(content)) for id, quest := range state.ActiveQuests { if strings.Contains(content, strings.ToLower(id)) { return id } if strings.TrimSpace(quest.Title) != "" && strings.Contains(content, strings.ToLower(quest.Title)) { return id } } return "" } func extractTailAfterQuestVerb(content string) string { raw := strings.TrimSpace(content) lower := strings.ToLower(raw) for _, marker := range []string{"推进任务", "更新任务进度", "progress quest", "advance quest"} { idx := strings.Index(lower, strings.ToLower(marker)) if idx >= 0 { out := strings.TrimSpace(raw[idx+len(marker):]) out = strings.Trim(out, " :,-") return out } } return "" }