Skip to navigation

NPC State And Protocol

View as Markdown

NPC state in the UE plugin lives in NPCSlice, not in a standalone immutable FNPC value. The slice mirrors the entity-adapter pattern from @forbocai/core.

Primary Type

USTRUCT(BlueprintType)
struct FNPCInternalState {
UPROPERTY(BlueprintReadOnly) FString Id;
UPROPERTY(BlueprintReadOnly) FString Persona;
UPROPERTY(BlueprintReadOnly) FAgentState State;
UPROPERTY(BlueprintReadOnly) TArray<FNPCHistoryEntry> History;
FAgentAction LastAction;
bool bHasLastAction;
UPROPERTY(BlueprintReadOnly) bool bIsBlocked;
UPROPERTY(BlueprintReadOnly) FString BlockReason;
UPROPERTY(BlueprintReadOnly) TArray<FNPCStateLogEntry> StateLog;
};

The slice keeps an entity map plus ActiveNpcId.

Register An NPC

#include "RuntimeStore.h"
#include "NPC/NPCId.h"
#include "NPC/NPCSlice.h"
auto Store = createSDKStore();
FNPCInternalState Npc;
Npc.Id = NPCId::GenerateNPCId();
Npc.Persona = TEXT("A careful quartermaster.");
Npc.State = TypeFactory::AgentState(TEXT("{\"mood\":\"guarded\"}"));
Store.dispatch(NPCSlice::Actions::SetNPCInfo(Npc));

SetNPCInfo upserts the entity and also makes it active.

Selectors

  • NPCSlice::SelectNPCById(State, Id)
  • NPCSlice::SelectAllNPCs(State)
  • NPCSlice::SelectActiveNpcId(State)
  • NPCSlice::SelectActiveNPC(State)
const auto Active = NPCSlice::SelectActiveNPC(Store.getState().NPCs);
if (Active.hasValue) {
UE_LOG(LogTemp, Display, TEXT("%s"), *Active.value.Persona);
}

Update State

Use slice actions for direct state changes.

Store.dispatch(NPCSlice::Actions::UpdateNPCState(
Npc.Id,
TypeFactory::AgentState(TEXT("{\"mood\":\"friendly\"}"))));
  • SetNPCState replaces the full agent state
  • UpdateNPCState merges a JSON delta into the current state
  • AddToHistory, SetHistory, SetLastAction, BlockAction, and ClearBlock support the protocol runtime

Run The Protocol

For synchronous tooling, use SDKOps::ProcessNpc(...).

#include "CLI/CliOperations.h"
#include "Memory/MemoryThunks.h"
#include "Cortex/CortexThunks.h"
SDKOps::InitNodeMemory(Store, TEXT("/tmp/quartermaster.db"));
SDKOps::InitCortex(Store, TEXT("smollm2-135m"));
FAgentResponse Response =
SDKOps::ProcessNpc(Store, Npc.Id, TEXT("What supplies are left?"));

For direct parity with packages/core, dispatch rtk::processNPC(...) yourself from Protocol/ProtocolThunks.h.

Protocol Semantics

  • The API can return IdentifyActor, QueryVector, ExecuteInference, and Finalize instructions.
  • QueryVector requires local memory support in the runtime.
  • ExecuteInference requires local cortex support in the runtime.
  • On successful finalize, the runtime persists memory instructions, merges state deltas, records the last action, and appends user/assistant history.
  • On invalid finalize, the slice records bIsBlocked and BlockReason.

Convenience Wrappers

SDKOps exposes the same NPC lifecycle in synchronous form:

  • SDKOps::CreateNpc(Store, Persona)
  • SDKOps::GetActiveNpc(Store)
  • SDKOps::UpdateNpc(Store, NpcId, Delta)
  • SDKOps::ProcessNpc(Store, NpcId, Text)
  • SDKOps::ListNpcs(Store)