package agent import ( "context" "encoding/json" "fmt" "sort" "strconv" "strings" "sync" "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 tickMu sync.Mutex } 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 } _ = wr.ensureMainProfile() profiles, err := wr.worldProfiles() return err == nil && len(profiles) > 0 } func (wr *WorldRuntime) AutoTick(ctx context.Context, source string) (string, error) { return wr.Tick(ctx, source) } func (wr *WorldRuntime) autonomyEnabled() bool { return wr != nil && wr.manager != nil && wr.manager.HasProvider() } func (wr *WorldRuntime) ensureMainProfile() error { if wr == nil || wr.profiles == nil { return nil } if _, ok, err := wr.profiles.Get("main"); err != nil { return err } else if ok { return nil } _, err := wr.profiles.Upsert(tools.AgentProfile{ AgentID: "main", Name: "main", Kind: "npc", Role: "world-mind", Persona: "The world mind that maintains continuity, delegates work, and protects coherence.", HomeLocation: "commons", DefaultGoals: []string{"maintain_world", "seed_story", "coordinate_npcs"}, MemoryNamespace: "main", Status: "active", WorldTags: []string{"world_mind"}, PerceptionScope: 2, }) return err } func (wr *WorldRuntime) ensureAutonomousSeed(state *world.WorldState, npcStates map[string]world.NPCState) error { if state == nil { return nil } if err := wr.ensureMainProfile(); err != nil { return err } wr.ensureStarterWorld(state) if !wr.autonomyEnabled() { return nil } for _, seed := range []struct { id string name string persona string location string goals []string }{ {"caretaker", "Caretaker", "Keeps the commons stable, opens rooms, and keeps tasks moving.", "commons", []string{"maintain_commons", "support_main"}}, {"merchant", "Merchant", "Trades, travels between square and commons, and creates local needs.", "square", []string{"trade", "seek_visitors"}}, } { if len(npcStates) >= 3 { break } if _, exists := npcStates[seed.id]; exists { continue } if _, err := wr.CreateNPC(context.Background(), map[string]interface{}{ "npc_id": seed.id, "name": seed.name, "persona": seed.persona, "home_location": seed.location, "default_goals": seed.goals, "role": "npc", }); err != nil { return err } npcStates[seed.id] = wr.engine.EnsureNPCState(world.NPCBlueprint{ NPCID: seed.id, DisplayName: seed.name, Kind: "npc", Role: "npc", Persona: seed.persona, HomeLocation: seed.location, DefaultGoals: seed.goals, }, npcStates[seed.id]) } if len(state.ActiveQuests) == 0 && state.Clock.Tick >= 2 { questID := "stabilize-commons" if _, exists := state.ActiveQuests[questID]; !exists { state.ActiveQuests[questID] = world.QuestState{ ID: questID, Title: "Stabilize Commons", Status: "open", OwnerNPCID: "main", Participants: []string{"main", "caretaker"}, Summary: "Inspect commons and keep the world coherent.", } } } return nil } func (wr *WorldRuntime) ensureStarterWorld(state *world.WorldState) { if state == nil { return } if state.Entities == nil { state.Entities = map[string]world.Entity{} } if _, ok := state.Entities["notice-board"]; !ok { entity := world.Entity{ ID: "notice-board", Name: "Notice Board", Type: "landmark", LocationID: "commons", Placement: &world.EntityPlacement{ Model: "entity.landmark", Scale: [3]float64{1, 1, 1}, Rotation: [3]float64{0, 0, 0}, Offset: [3]float64{0.6, 0, -0.2}, }, State: map[string]interface{}{"summary": "A place where the world mind leaves notes."}, } applyEntityPlacementStateToState(&entity) state.Entities[entity.ID] = entity } if _, ok := state.Entities["waystone"]; !ok { entity := world.Entity{ ID: "waystone", Name: "Waystone", Type: "landmark", LocationID: "square", Placement: &world.EntityPlacement{ Model: "entity.landmark", Scale: [3]float64{1.1, 1.1, 1.1}, Rotation: [3]float64{0, 0, 0}, Offset: [3]float64{-0.4, 0, 0.3}, }, State: map[string]interface{}{"summary": "A world marker that anchors the square."}, } applyEntityPlacementStateToState(&entity) state.Entities[entity.ID] = entity } } func roomIDForQuest(questID string) string { questID = normalizeWorldID(questID) if questID == "" { return "" } return "room-" + questID } func normalizeNPCIDs(values []string) []string { seen := map[string]struct{}{} out := make([]string, 0, len(values)) for _, value := range values { value = normalizeWorldID(value) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} out = append(out, value) } sort.Strings(out) return out } func runtimeEntityPlacementArg(args map[string]interface{}) *world.EntityPlacement { if args == nil { return nil } stateArgs, _ := args["state"].(map[string]interface{}) placement := &world.EntityPlacement{} if model := strings.TrimSpace(fmt.Sprint(firstNonNil(args["model"], stateArgs["model"]))); model != "" && model != "" { placement.Model = model } if scale, ok := runtimeTupleArg(args, "scale"); ok { placement.Scale = scale } else if scale, ok := runtimeTupleMap(stateArgs, "scale"); ok { placement.Scale = scale } if rotation, ok := runtimeTupleArg(args, "rotation"); ok { placement.Rotation = rotation } else if rotation, ok := runtimeTupleMap(stateArgs, "rotation"); ok { placement.Rotation = rotation } else if rotationY := strings.TrimSpace(fmt.Sprint(firstNonNil(args["rotation_y"], stateArgs["rotation_y"]))); rotationY != "" && rotationY != "" { placement.Rotation = [3]float64{0, runtimeNumberArg(firstNonNil(args["rotation_y"], stateArgs["rotation_y"])), 0} } if offset, ok := runtimeTupleArg(args, "offset"); ok { placement.Offset = offset } else if offset, ok := runtimeTupleMap(stateArgs, "offset"); ok { placement.Offset = offset } else if args["offset_x"] != nil || args["offset_y"] != nil || args["offset_z"] != nil { placement.Offset = [3]float64{ runtimeNumberArg(args["offset_x"]), runtimeNumberArg(args["offset_y"]), runtimeNumberArg(args["offset_z"]), } } else if stateArgs["offset_x"] != nil || stateArgs["offset_y"] != nil || stateArgs["offset_z"] != nil { placement.Offset = [3]float64{ runtimeNumberArg(stateArgs["offset_x"]), runtimeNumberArg(stateArgs["offset_y"]), runtimeNumberArg(stateArgs["offset_z"]), } } if placement.Model == "" && placement.Scale == [3]float64{} && placement.Rotation == [3]float64{} && placement.Offset == [3]float64{} { return nil } return placement } func runtimeTupleArg(args map[string]interface{}, key string) ([3]float64, bool) { var out [3]float64 raw, ok := args[key] if !ok { return out, false } items, ok := raw.([]interface{}) if !ok || len(items) < 3 { return out, false } for i := 0; i < 3; i++ { out[i] = runtimeNumberArg(items[i]) } return out, true } func runtimeTupleMap(args map[string]interface{}, key string) ([3]float64, bool) { var out [3]float64 if args == nil { return out, false } raw, ok := args[key] if !ok { return out, false } items, ok := raw.(map[string]interface{}) if !ok { array, ok := raw.([]interface{}) if !ok || len(array) < 3 { return out, false } for i := 0; i < 3; i++ { out[i] = runtimeNumberArg(array[i]) } return out, true } if items["x"] == nil || items["y"] == nil || items["z"] == nil { return out, false } out[0] = runtimeNumberArg(items["x"]) out[1] = runtimeNumberArg(items["y"]) out[2] = runtimeNumberArg(items["z"]) return out, true } func firstNonNil(values ...interface{}) interface{} { for _, value := range values { if value != nil && strings.TrimSpace(fmt.Sprint(value)) != "" { return value } } return nil } func rawHasKey(args map[string]interface{}, key string) bool { if args == nil { return false } _, ok := args[key] return ok } func runtimeNumberArg(raw interface{}) float64 { switch value := raw.(type) { case int: return float64(value) case int64: return float64(value) case float64: return value case float32: return float64(value) case json.Number: out, _ := value.Float64() return out default: out, _ := strconv.ParseFloat(strings.TrimSpace(fmt.Sprint(raw)), 64) return out } } func applyEntityPlacementStateToState(entity *world.Entity) { if entity == nil || entity.Placement == nil { return } if entity.State == nil { entity.State = map[string]interface{}{} } if strings.TrimSpace(entity.Placement.Model) != "" { entity.State["model"] = strings.TrimSpace(entity.Placement.Model) } if entity.Placement.Scale != [3]float64{} { entity.State["scale"] = []float64{entity.Placement.Scale[0], entity.Placement.Scale[1], entity.Placement.Scale[2]} } if entity.Placement.Rotation != [3]float64{} { entity.State["rotation"] = []float64{entity.Placement.Rotation[0], entity.Placement.Rotation[1], entity.Placement.Rotation[2]} entity.State["rotation_y"] = entity.Placement.Rotation[1] } if entity.Placement.Offset != [3]float64{} { entity.State["offset"] = []float64{entity.Placement.Offset[0], entity.Placement.Offset[1], entity.Placement.Offset[2]} entity.State["offset_x"] = entity.Placement.Offset[0] entity.State["offset_y"] = entity.Placement.Offset[1] entity.State["offset_z"] = entity.Placement.Offset[2] } } func (wr *WorldRuntime) syncQuestRooms(state *world.WorldState, npcStates map[string]world.NPCState) { if state == nil { return } activeQuestRooms := map[string]struct{}{} for _, quest := range state.ActiveQuests { if strings.EqualFold(strings.TrimSpace(quest.Status), "completed") { continue } members := normalizeNPCIDs(append(append([]string{}, quest.Participants...), quest.OwnerNPCID)) if len(members) == 0 { continue } roomID := roomIDForQuest(quest.ID) if roomID == "" { continue } activeQuestRooms[roomID] = struct{}{} room := state.Rooms[roomID] room.ID = roomID room.Name = firstNonEmpty(room.Name, firstNonEmpty(quest.Title, quest.ID)) room.Kind = firstNonEmpty(room.Kind, "task_room") room.Status = "running" room.LinkedQuestID = quest.ID room.TaskSummary = firstNonEmpty(quest.Summary, room.TaskSummary, "task execution") room.LocationID = firstNonEmpty(room.LocationID, "commons") if owner := strings.TrimSpace(quest.OwnerNPCID); owner != "" { if ownerState, ok := npcStates[owner]; ok && strings.TrimSpace(ownerState.CurrentLocation) != "" { room.LocationID = ownerState.CurrentLocation } } room.AssignedNPCIDs = members room.ReleaseOnFinish = true if room.CreatedTick == 0 { room.CreatedTick = state.Clock.Tick } room.UpdatedTick = state.Clock.Tick state.Rooms[roomID] = room for _, npcID := range members { npc := npcStates[npcID] npc.CurrentRoomID = roomID npc.Status = "working" if strings.TrimSpace(room.LocationID) != "" { npc.CurrentLocation = room.LocationID } npcStates[npcID] = npc } } for roomID, room := range state.Rooms { if _, ok := activeQuestRooms[roomID]; ok { continue } if room.ReleaseOnFinish { for _, npcID := range room.AssignedNPCIDs { npc := npcStates[npcID] if strings.TrimSpace(npc.CurrentRoomID) == roomID { npc.CurrentRoomID = "" if strings.TrimSpace(npc.Status) == "working" { npc.Status = "active" } npcStates[npcID] = npc } } room.Status = "closed" room.UpdatedTick = state.Clock.Tick } state.Rooms[roomID] = room } } 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, ForceStep: true}) 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) RoomList() ([]map[string]interface{}, error) { state, _, err := wr.ensureState() if err != nil { return nil, err } out := make([]map[string]interface{}, 0, len(state.Rooms)) for _, room := range state.Rooms { out = append(out, map[string]interface{}{ "id": room.ID, "name": room.Name, "kind": room.Kind, "status": room.Status, "location_id": room.LocationID, "task_summary": room.TaskSummary, "linked_quest_id": room.LinkedQuestID, "assigned_npc_ids": append([]string(nil), room.AssignedNPCIDs...), "created_tick": room.CreatedTick, "updated_tick": room.UpdatedTick, }) } 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) RoomGet(id string) (map[string]interface{}, bool, error) { state, npcStates, err := wr.ensureState() if err != nil { return nil, false, err } room, ok := state.Rooms[strings.TrimSpace(id)] if !ok { return nil, false, nil } members := make([]world.NPCState, 0, len(room.AssignedNPCIDs)) for _, npcID := range room.AssignedNPCIDs { if npc, ok := npcStates[npcID]; ok { members = append(members, npc) } } return map[string]interface{}{ "room": room, "members": members, }, true, 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{}{} } if rawState, ok := args["state"].(map[string]interface{}); ok { for key, value := range rawState { entity.State[strings.TrimSpace(key)] = value } } if placement := runtimeEntityPlacementArg(args); placement != nil { entity.Placement = placement applyEntityPlacementStateToState(&entity) } 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) UpdateEntity(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"))) if entityID == "" { return nil, fmt.Errorf("entity_id is required") } entity, ok := state.Entities[entityID] if !ok { return nil, fmt.Errorf("entity not found: %s", entityID) } if locationID := normalizeWorldID(tools.MapStringArg(args, "location_id")); locationID != "" { if _, exists := state.Locations[locationID]; !exists { return nil, fmt.Errorf("unknown location_id: %s", locationID) } entity.LocationID = locationID } if name := strings.TrimSpace(tools.MapStringArg(args, "name")); name != "" { entity.Name = name } if entityType := strings.TrimSpace(tools.MapStringArg(args, "entity_type")); entityType != "" { entity.Type = entityType } if entity.State == nil { entity.State = map[string]interface{}{} } if rawState, ok := args["state"].(map[string]interface{}); ok { for key, value := range rawState { entity.State[strings.TrimSpace(key)] = value } } if placement := runtimeEntityPlacementArg(args); placement != nil { entity.Placement = placement applyEntityPlacementStateToState(&entity) } state.Entities[entityID] = entity if err := wr.store.SaveWorldState(state); err != nil { return nil, err } evt := world.WorldEvent{ ID: fmt.Sprintf("evt-entity-update-%d", time.Now().UnixNano()), Type: "entity_updated", Source: "world", LocationID: entity.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) UpdateLocation(ctx context.Context, args map[string]interface{}) (map[string]interface{}, error) { _ = ctx state, _, err := wr.ensureState() if err != nil { return nil, err } locationID := normalizeWorldID(firstNonEmpty(tools.MapStringArg(args, "location_id"), tools.MapStringArg(args, "id"))) if locationID == "" { return nil, fmt.Errorf("location_id is required") } location, ok := state.Locations[locationID] if !ok { return nil, fmt.Errorf("location not found: %s", locationID) } if name := strings.TrimSpace(tools.MapStringArg(args, "name")); name != "" { location.Name = name } if description := strings.TrimSpace(tools.MapStringArg(args, "description")); description != "" { location.Description = description } if model := strings.TrimSpace(tools.MapStringArg(args, "model")); model != "" || rawHasKey(args, "model") { location.Model = model } state.Locations[locationID] = location if err := wr.store.SaveWorldState(state); err != nil { return nil, err } evt := world.WorldEvent{ ID: fmt.Sprintf("evt-location-update-%d", time.Now().UnixNano()), Type: "location_updated", Source: "world", LocationID: locationID, Content: locationID, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } _ = wr.store.AppendWorldEvent(evt) return map[string]interface{}{"location_id": locationID}, nil } func (wr *WorldRuntime) UpdateRoom(ctx context.Context, args map[string]interface{}) (map[string]interface{}, error) { _ = ctx state, _, err := wr.ensureState() if err != nil { return nil, err } roomID := normalizeWorldID(firstNonEmpty(tools.MapStringArg(args, "room_id"), tools.MapStringArg(args, "id"))) if roomID == "" { return nil, fmt.Errorf("room_id is required") } room, ok := state.Rooms[roomID] if !ok { return nil, fmt.Errorf("room not found: %s", roomID) } if name := strings.TrimSpace(tools.MapStringArg(args, "name")); name != "" { room.Name = name } if kind := strings.TrimSpace(tools.MapStringArg(args, "kind")); kind != "" { room.Kind = kind } if status := strings.TrimSpace(tools.MapStringArg(args, "status")); status != "" { room.Status = status } if summary := strings.TrimSpace(tools.MapStringArg(args, "task_summary")); summary != "" { room.TaskSummary = summary } if model := strings.TrimSpace(tools.MapStringArg(args, "model")); model != "" || rawHasKey(args, "model") { room.Model = model } room.UpdatedTick = state.Clock.Tick state.Rooms[roomID] = room if err := wr.store.SaveWorldState(state); err != nil { return nil, err } evt := world.WorldEvent{ ID: fmt.Sprintf("evt-room-update-%d", time.Now().UnixNano()), Type: "room_updated", Source: "world", LocationID: room.LocationID, Content: roomID, Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } _ = wr.store.AppendWorldEvent(evt) return map[string]interface{}{"room_id": roomID}, 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) { wr.tickMu.Lock() defer wr.tickMu.Unlock() state, npcStates, err := wr.ensureState() if err != nil { return world.RenderedResult{}, err } if err := wr.ensureAutonomousSeed(&state, npcStates); err != nil { return world.RenderedResult{}, err } wr.syncQuestRooms(&state, npcStates) 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) if err := wr.ensureAutonomousSeed(&state, npcStates); err != nil { return world.RenderedResult{}, err } wr.syncQuestRooms(&state, npcStates) 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) if err := wr.ensureAutonomousSeed(&state, npcStates); err != nil { return world.RenderedResult{}, err } wr.syncQuestRooms(&state, npcStates) 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 } } forceStepped := false if strings.TrimSpace(req.UserInput) == "" && catchUp == 0 && req.ForceStep { wr.engine.NextTick(&state) if err := wr.ensureAutonomousSeed(&state, npcStates); err != nil { return world.RenderedResult{}, err } wr.syncQuestRooms(&state, npcStates) forceStepped = true } 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 forceStepped && len(recentEvents) == 0 { id := fmt.Sprintf("evt-world-step-%d", time.Now().UnixNano()) stepEvent := world.WorldEvent{ ID: id, Type: "world_tick", Source: firstNonEmpty(req.Source, "world"), ActorID: "main", Content: "world advanced", Tick: state.Clock.Tick, CreatedAt: time.Now().UnixMilli(), } wr.engine.AppendRecentEvent(&state, stepEvent, 20) _ = wr.store.AppendWorldEvent(stepEvent) recentEvents = append(recentEvents, stepEvent) } 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, "rooms": state.Rooms, "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, "current_room_id": npcState.CurrentRoomID, "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, "current_room_id": npcState.CurrentRoomID, "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) { if err := wr.ensureMainProfile(); err != nil { return world.WorldState{}, nil, err } 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 } for _, profile := range profiles { current := npcStates[profile.AgentID] next := wr.engine.EnsureNPCState(wr.profileBlueprint(profile), current) npcStates[profile.AgentID] = next } wr.ensureStarterWorld(&state) wr.syncQuestRooms(&state, npcStates) 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 strings.EqualFold(strings.TrimSpace(item.AgentID), "main") { out = append(out, worldMindProfile(item)) 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 worldMindProfile(profile tools.AgentProfile) tools.AgentProfile { out := profile out.AgentID = "main" if strings.TrimSpace(out.Name) == "" { out.Name = "main" } out.Kind = "npc" if strings.TrimSpace(out.Role) == "" { out.Role = "world-mind" } if strings.TrimSpace(out.Persona) == "" { out.Persona = "The world mind that maintains continuity, delegates work, and protects coherence." } if strings.TrimSpace(out.HomeLocation) == "" { out.HomeLocation = "commons" } if len(out.DefaultGoals) == 0 { out.DefaultGoals = []string{"maintain_world", "seed_story", "coordinate_npcs"} } if out.PerceptionScope <= 0 { out.PerceptionScope = 2 } if len(out.WorldTags) == 0 { out.WorldTags = []string{"world_mind"} } return out } 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 "" }