-
Notifications
You must be signed in to change notification settings - Fork 0
feat(nakama): persist agent runtime activity #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2a12b9b
adc2c0d
0ec3304
8aafca9
eeeec4d
55c3c11
6bbed9d
14af2a8
73ef2de
5316e7c
b17deac
2f8c38b
2dffe65
0fc87e9
e833ad9
daa89b8
99fcae6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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(); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -194,7 +206,29 @@ public IEnumerator UpdateSoulForPlayer(string playerId, UpdateSoulRequestDto req | |
|
|
||
| public IEnumerator Decide(AgentDecisionRequestDto request, Action<AgentDecisionDto> onSuccess, Action<string> onError = null) | ||
| { | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the gateway returns an empty or invalid response that results in a 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| } | ||
|
|
||
| public IEnumerator Chat(NpcChatRequestDto request, Action<NpcChatResponseDto> onSuccess, Action<string> onError = null) | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 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 }; | ||
|
|
@@ -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) | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The introduction of |
||
|
|
||
| private static string ExtractJwtStringClaim(string jwt, string claimName) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(jwt)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Decidemethod should validate that therequestparameter 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.