Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ This file is the primary context for any AI coding agent working on this reposit
3. **Time-as-Currency** - Time is both the current body's survival resource and a spendable economy resource, adapted from MetaDOS and the `In Time` inspiration. Running out of body time triggers death/reincarnation; spending time creates hard tactical tradeoffs.
4. **Consciousness transfer to NPC/synthetic bodies** - Sci-fi explanation (mind upload, synthetic bodies, Nibirium-enhanced cloning). NOT spiritual reincarnation.

## Actor and Body Model (CORE)

- A player is a durable consciousness / soul profile, not a blank avatar shell.
- On spawn, the player inhabits a current NPC-like synthetic body. That body may already have its own profile, constraints, stats, traits, memory hooks, soul imprint, BodyTime, lifecycle state, and agent runtime state.
- The game must support many NPC-like actors and many player-inhabited bodies using one broad actor-profile model. Ownership and authority decide whether an actor is a world NPC, a player current body, an offline player agent, or an OpenClaw-connected actor.
- Each important actor body should eventually resolve to a bundle: `BodyProfile`, `CharacterStats`, `CharacterTraits`, `SoulProfile`, `MemoryRecord`, `AgentPolicy` or NPC policy, `AgentRuntime`, and `AgentActivity`.
- Reincarnation destroys or retires the current body. The durable player consciousness transfers into a new body, with only explicitly designed layers carrying over.

## Cultivation System (sci-fi, not Chinese-style)

6 tiers:
Expand Down
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ This file is the primary context for any AI coding agent working on this reposit
3. **Time-as-Currency** - Time is both the current body's survival resource and a spendable economy resource, adapted from MetaDOS and the `In Time` inspiration. Running out of body time triggers death/reincarnation; spending time creates hard tactical tradeoffs.
4. **Consciousness transfer to NPC/synthetic bodies** - Sci-fi explanation (mind upload, synthetic bodies, Nibirium-enhanced cloning). NOT spiritual reincarnation.

## Actor and Body Model (CORE)

- A player is a durable consciousness / soul profile, not a blank avatar shell.
- On spawn, the player inhabits a current NPC-like synthetic body. That body may already have its own profile, constraints, stats, traits, memory hooks, soul imprint, BodyTime, lifecycle state, and agent runtime state.
- The game must support many NPC-like actors and many player-inhabited bodies using one broad actor-profile model. Ownership and authority decide whether an actor is a world NPC, a player current body, an offline player agent, or an OpenClaw-connected actor.
- Each important actor body should eventually resolve to a bundle: `BodyProfile`, `CharacterStats`, `CharacterTraits`, `SoulProfile`, `MemoryRecord`, `AgentPolicy` or NPC policy, `AgentRuntime`, and `AgentActivity`.
- Reincarnation destroys or retires the current body. The durable player consciousness transfers into a new body, with only explicitly designed layers carrying over.

## Cultivation System (sci-fi, not Chinese-style)

6 tiers:
Expand Down
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ versioned release tag yet, so entries are organized as pre-alpha snapshots.
and endpoint decider injection.
- Fallback observability for model-backed decisions, including decision source
metadata and structured warning logs.
- Nakama agent runtime counters for profile bootstrap, fallback decisions,
action intent counts, activity count, and offline-agent seconds.
- Bounded Nakama `agent_activity` log with `secondspawn_agent_activity_add`.

### Changed

Expand All @@ -27,6 +30,10 @@ versioned release tag yet, so entries are organized as pre-alpha snapshots.
implemented prototype foundation.
- Unity prototype brain now warns on gateway decision failures and escalates
repeated failures to errors.
- Unity Nakama auth now bootstraps the player profile immediately and records a
profile activity event after successful authentication.
- Nakama deterministic decision RPC now records runtime decision counters before
returning prototype fallback intent.

### Verification

Expand All @@ -36,7 +43,8 @@ versioned release tag yet, so entries are organized as pre-alpha snapshots.

### Known Issues

- PR #5 is still in review and has not merged into `dev`.
- PR #5 has merged into `dev`; the next review gate is the profile bootstrap
and agent activity branch.
- Gateway route-level JWT enforcement is not complete.
- LLM rate limiting and token budget enforcement are tracked in issue #6.
- Real voice still waits for an ephemeral-token provider flow.
Expand Down Expand Up @@ -94,4 +102,4 @@ versioned release tag yet, so entries are organized as pre-alpha snapshots.

- No release tags exist yet.
- Main foundation merge: `154ac15`.
- Current review branch commit: `deea2d4`.
- Current merged model-decision branch commit: `998637a`.
10 changes: 7 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ especially `docs/design/02-vertical-slice-spec.md` and

## Current Review Gate

- [ ] Review PR #5: model-backed agent decisions and brain phase logging.
- [ ] Address reviewer feedback.
- [ ] Merge PR #5 into `dev` after review and smoke verification.
- [x] Merge PR #5: model-backed agent decisions and brain phase logging.
- [ ] Review the profile bootstrap and agent activity branch.
- [ ] Merge the profile bootstrap and agent activity branch into `dev` after
backend tests, Unity compile, and reviewer verification.

## Vertical Slice - Current Milestone

Expand All @@ -58,6 +59,8 @@ MVP, and a visible offline-agent prototype.
visual using the existing visual prefab catalog.
- [ ] Add a proper hub NPC prefab using the prototype NPC brain contract.
- [ ] Add Nakama channel-based basic chat for the vertical slice.
- [ ] Surface agent runtime stats and recent activity in an in-editor or
prototype debug UI.
- [ ] Add route-level gateway authentication before non-local AI or voice
playtests.
- [ ] Add per-player LLM rate limit and token-budget enforcement.
Expand All @@ -72,6 +75,7 @@ MVP, and a visible offline-agent prototype.
- [ ] Add one Hunter NFT skin equip placeholder with DOS Chain escrow design
still server-authoritative.
- [ ] Run Multiplayer Play Mode smoke for 2-4 local clients.
- [ ] Resolve Unity Fusion CodeGen Play Mode smoke blocker tracked in issue #7.
- [ ] Prepare Linux headless dedicated server build path.

## Alpha
Expand Down
41 changes: 41 additions & 0 deletions Unity/Assets/_SecondSpawn/Scripts/AI/AgentContextDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public sealed class BodyProfileDto
public AgentPolicyDto agent_policy;
public SoulProfileDto soul;
public MemoryRecordDto[] memory;
public AgentRuntimeDto agent_runtime;
public AgentActivityRecordDto[] agent_activity;
}

[Serializable]
Expand Down Expand Up @@ -100,6 +102,45 @@ public sealed class MemoryRecordDto
public int importance = 5;
}

[Serializable]
public sealed class AgentRuntimeDto
{
public string profile_bootstrapped_at;
public string last_profile_bootstrap_at;
public string last_activity_at;
public long activity_count;
public long decision_count;
public long fallback_decision_count;
public long move_intent_count;
public long say_intent_count;
public long stop_intent_count;
public long interact_intent_count;
public long offline_seconds;
}

[Serializable]
public sealed class AgentActivityRecordDto
{
public string id;
public string kind = "manual_note";
public string summary;
public string occurred_at;
public string source = "client";
public AgentActivityMetricsDto metrics;
}

[Serializable]
public sealed class AgentActivityMetricsDto
{
public long offline_seconds;
public long decisions_made;
public long fallback_decisions;
public long move_intents;
public long say_intents;
public long stop_intents;
public long interact_intents;
}

[Serializable]
public sealed class UpdateSoulRequestDto
{
Expand Down
172 changes: 171 additions & 1 deletion Unity/Assets/_SecondSpawn/Scripts/AI/SecondSpawnGatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ public sealed class SecondSpawnGatewayClient : MonoBehaviour
[SerializeField, Tooltip("Use Nakama device auth when Supabase is not configured yet. Local prototype only.")]
private bool _allowNakamaDeviceFallback = true;

[SerializeField, Tooltip("Create or refresh the Nakama character profile immediately after authentication.")]
private bool _bootstrapProfileAfterAuth = true;

[SerializeField, Min(1), Tooltip("Seconds before gateway or Nakama HTTP requests fail fast in Play Mode.")]
private int _requestTimeoutSeconds = 10;

private bool _authAttempted;
private bool _authInProgress;
private string _supabaseAccessToken;
Expand Down Expand Up @@ -126,6 +132,7 @@ public IEnumerator Authenticate(Action onSuccess = null, Action<string> onError

_authInProgress = false;
Debug.Log($"[SecondSpawnGatewayClient] Authenticated Nakama user {PlayerId}.");
yield return BootstrapNakamaProfileAfterAuth("custom_auth");
onSuccess?.Invoke();
}

Expand All @@ -144,6 +151,11 @@ public IEnumerator AddNakamaMemory(MemoryRecordDto memory, Action<AgentContextDt
yield return SendNakamaRpc("secondspawn_memory_add", memory, onSuccess, onError);
}

public IEnumerator AddNakamaAgentActivity(AgentActivityRecordDto activity, Action<AgentContextDto> onSuccess = null, Action<string> onError = null)
{
yield return SendNakamaRpc("secondspawn_agent_activity_add", activity, onSuccess, onError);
}

public IEnumerator UpdateNakamaSoul(UpdateSoulRequestDto request, Action<AgentContextDto> onSuccess = null, Action<string> onError = null)
{
yield return SendNakamaRpc("secondspawn_soul_update", request, onSuccess, onError);
Expand Down Expand Up @@ -194,7 +206,29 @@ public IEnumerator UpdateSoulForPlayer(string playerId, UpdateSoulRequestDto req

public IEnumerator Decide(AgentDecisionRequestDto request, Action<AgentDecisionDto> onSuccess, Action<string> onError = null)
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Decide method should validate that the request parameter is not null before proceeding. Passing a null request to the gateway DTO factory will result in a malformed payload that the server may fail to process.

        {
            if (request == null)
            {
                onError?.Invoke("Agent decision request cannot be null.");
                yield break;
            }

yield return SendJson("POST", "/v1/agent/decide", request, onSuccess, onError);
AgentDecisionDto decision = null;
string gatewayError = null;
yield return SendJson<AgentDecisionDto>(
"POST",
"/v1/agent/decide",
GatewayAgentDecisionRequestDto.From(request),
response => decision = response,
error => gatewayError = error);

if (decision == null)
{
if (!string.IsNullOrWhiteSpace(gatewayError))
{
onError?.Invoke(gatewayError);
}
yield break;
}
Comment on lines +218 to +225
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the gateway returns an empty or invalid response that results in a null decision, the onError callback might not be invoked if gatewayError is also null or whitespace. It is better to provide a default error message to ensure the caller is always notified of the failure.

            if (decision == null)
            {
                onError?.Invoke(!string.IsNullOrWhiteSpace(gatewayError) ? gatewayError : "Gateway returned an invalid or empty decision.");
                yield break;
            }


onSuccess?.Invoke(decision);
if (HasNakamaSession)
{
StartCoroutine(RecordGatewayDecisionActivity(decision));
}
Comment on lines +228 to +231
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Decide coroutine now waits for the AddNakamaAgentActivity RPC to complete before finishing. While onSuccess is called immediately after the gateway response, any logic that yield returns on Decide will be delayed by this additional network call. Consider if this activity logging should be fire-and-forget or if the latency is acceptable for the current prototype.

}

public IEnumerator Chat(NpcChatRequestDto request, Action<NpcChatResponseDto> onSuccess, Action<string> onError = null)
Expand Down Expand Up @@ -312,9 +346,28 @@ private IEnumerator AuthenticateNakamaDeviceFallback(Action onSuccess, Action<st

_authInProgress = false;
Debug.Log($"[SecondSpawnGatewayClient] Authenticated Nakama device fallback user {PlayerId}.");
yield return BootstrapNakamaProfileAfterAuth("device_auth");
onSuccess?.Invoke();
}

private IEnumerator BootstrapNakamaProfileAfterAuth(string authSource)
{
if (!_bootstrapProfileAfterAuth || !HasNakamaSession)
{
yield break;
}

yield return AddNakamaAgentActivity(new AgentActivityRecordDto
{
kind = "profile_bootstrap",
summary = $"Unity client authenticated through {authSource} and confirmed the Nakama character profile.",
source = "unity"
}, null, error =>
{
Debug.LogWarning($"[SecondSpawnGatewayClient] Nakama profile activity write failed: {error}");
});
}
Comment on lines +353 to +369
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The BootstrapNakamaProfileAfterAuth method performs two sequential network calls: GetNakamaContext followed by AddNakamaAgentActivity. Since AddNakamaAgentActivity triggers the same getOrCreateAgentContext logic on the backend (which creates the profile if it doesn't exist) and also returns the full AgentContextDto, the initial GetNakamaContext call is redundant and adds unnecessary latency to the authentication flow.

        private IEnumerator BootstrapNakamaProfileAfterAuth(string authSource)
        {
            if (!_bootstrapProfileAfterAuth || !HasNakamaSession)
            {
                yield break;
            }

            yield return AddNakamaAgentActivity(new AgentActivityRecordDto
            {
                kind = "profile_bootstrap",
                summary = $"Unity client authenticated through {authSource} and confirmed the Nakama character profile.",
                source = "unity"
            }, null, error =>
            {
                Debug.LogWarning($"[SecondSpawnGatewayClient] Nakama profile activity write failed: {error}");
            });
        }


private IEnumerator AuthenticateNakamaDevice(string deviceId, string username, Action<NakamaSessionDto> onSuccess, Action<string> onError)
{
var payload = new NakamaDeviceAuthRequest { id = deviceId };
Expand All @@ -332,6 +385,7 @@ private IEnumerator AuthenticateNakamaDevice(string deviceId, string username, A

private IEnumerator Send<TResponse>(UnityWebRequest request, Action<TResponse> onSuccess, Action<string> onError)
{
request.timeout = Mathf.Max(1, _requestTimeoutSeconds);
yield return request.SendWebRequest();

if (request.result != UnityWebRequest.Result.Success)
Expand Down Expand Up @@ -404,6 +458,122 @@ private static string TrimTrailingSlash(string value)
return string.IsNullOrWhiteSpace(value) ? "" : value.Trim().TrimEnd('/');
}

private static AgentActivityRecordDto BuildGatewayDecisionActivity(AgentDecisionDto decision)
{
var action = NormalizeDecisionAction(decision?.action);
var reason = string.IsNullOrWhiteSpace(decision?.reason) ? "no reason provided" : decision.reason.Trim();
return new AgentActivityRecordDto
{
kind = "agent_decision",
summary = $"Gateway chose {action}: {reason}",
source = "unity_gateway",
metrics = BuildGatewayDecisionMetrics(decision)
};
}

private IEnumerator RecordGatewayDecisionActivity(AgentDecisionDto decision)
{
yield return AddNakamaAgentActivity(BuildGatewayDecisionActivity(decision), null, error =>
{
Debug.LogWarning($"[SecondSpawnGatewayClient] Gateway decision activity write failed: {error}");
});
}

private static AgentActivityMetricsDto BuildGatewayDecisionMetrics(AgentDecisionDto decision)
{
var action = NormalizeDecisionAction(decision?.action);
return new AgentActivityMetricsDto
{
decisions_made = 1,
fallback_decisions = IsFallbackDecision(decision) ? 1 : 0,
move_intents = action == "move" ? 1 : 0,
say_intents = action == "say" ? 1 : 0,
stop_intents = action == "stop" ? 1 : 0,
interact_intents = action == "interact" ? 1 : 0
};
}

private static bool IsFallbackDecision(AgentDecisionDto decision)
{
return string.Equals(decision?.source?.Trim(), "fallback", StringComparison.OrdinalIgnoreCase);
}

private static string NormalizeDecisionAction(string action)
{
return string.IsNullOrWhiteSpace(action) ? "unknown" : action.Trim().ToLowerInvariant();
}

[Serializable]
private sealed class GatewayAgentDecisionRequestDto
{
public GatewayAgentContextDto context;
public WorldSnapshotDto world_snapshot;
public string[] allowed;

public static GatewayAgentDecisionRequestDto From(AgentDecisionRequestDto request)
{
return new GatewayAgentDecisionRequestDto
{
context = GatewayAgentContextDto.From(request?.context),
world_snapshot = request?.world_snapshot,
allowed = request?.allowed
};
}
}

[Serializable]
private sealed class GatewayAgentContextDto
{
public PlayerProfileDto player;
public GatewayBodyProfileDto body;

public static GatewayAgentContextDto From(AgentContextDto context)
{
return new GatewayAgentContextDto
{
player = context?.player,
body = GatewayBodyProfileDto.From(context?.body)
};
}
}

[Serializable]
private sealed class GatewayBodyProfileDto
{
public string body_id;
public string archetype_id;
public string visual_prefab_key;
public EquipmentLoadoutDto equipment;
public CharacterTraitsDto characteristics;
public BodyTimeDto time;
public CultivationDto cultivation;
public AgentPolicyDto agent_policy;
public SoulProfileDto soul;
public MemoryRecordDto[] memory;

public static GatewayBodyProfileDto From(BodyProfileDto body)
{
if (body == null)
{
return null;
}

return new GatewayBodyProfileDto
{
body_id = body.body_id,
archetype_id = body.archetype_id,
visual_prefab_key = body.visual_prefab_key,
equipment = body.equipment,
characteristics = body.characteristics,
time = body.time,
cultivation = body.cultivation,
agent_policy = body.agent_policy,
soul = body.soul,
memory = body.memory
};
}
}
Comment on lines +507 to +575
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The introduction of GatewayAgentDecisionRequestDto and its nested classes creates significant maintenance overhead. These classes are nearly identical to the existing AgentDecisionRequestDto hierarchy and are only used to omit the agent_runtime and agent_activity fields. Since the Go-based Gateway safely ignores extra JSON fields, it is more maintainable to send the original DTOs directly rather than maintaining a parallel set of filtered DTOs that must be manually synchronized whenever the character profile schema changes.


private static string ExtractJwtStringClaim(string jwt, string claimName)
{
if (string.IsNullOrWhiteSpace(jwt))
Expand Down
Loading
Loading