import { v } from 'convex/values'; import { internalAction } from '../_generated/server'; import { WorldMap, serializedWorldMap } from './worldMap'; import { rememberConversation } from '../agent/memory'; import { GameId, agentId, conversationId, playerId } from './ids'; import { continueConversationMessage, leaveConversationMessage, startConversationMessage, } from '../agent/conversation'; import { assertNever } from '../util/assertNever'; import { serializedAgent } from './agent'; import { ACTIVITIES, ACTIVITY_COOLDOWN, CONVERSATION_COOLDOWN } from '../constants'; import { api, internal } from '../_generated/api'; import { sleep } from '../util/sleep'; import { serializedPlayer } from './player'; export const agentRememberConversation = internalAction({ args: { worldId: v.id('worlds'), playerId, agentId, conversationId, operationId: v.string(), }, handler: async (ctx, args) => { await rememberConversation( ctx, args.worldId, args.agentId as GameId<'agents'>, args.playerId as GameId<'players'>, args.conversationId as GameId<'conversations'>, ); await sleep(Math.random() * 1000); await ctx.runMutation(api.aiTown.main.sendInput, { worldId: args.worldId, name: 'finishRememberConversation', args: { agentId: args.agentId, operationId: args.operationId, }, }); }, }); export const agentGenerateMessage = internalAction({ args: { worldId: v.id('worlds'), playerId, agentId, conversationId, otherPlayerId: playerId, operationId: v.string(), type: v.union(v.literal('start'), v.literal('continue'), v.literal('leave')), messageUuid: v.string(), }, handler: async (ctx, args) => { let completionFn; switch (args.type) { case 'start': completionFn = startConversationMessage; break; case 'continue': completionFn = continueConversationMessage; break; case 'leave': completionFn = leaveConversationMessage; break; default: assertNever(args.type); } const completion = await completionFn( ctx, args.worldId, args.conversationId as GameId<'conversations'>, args.playerId as GameId<'players'>, args.otherPlayerId as GameId<'players'>, ); // TODO: stream in the text instead of reading it all at once. const text = await completion.readAll(); await ctx.runMutation(internal.aiTown.agent.agentSendMessage, { worldId: args.worldId, conversationId: args.conversationId, agentId: args.agentId, playerId: args.playerId, text, messageUuid: args.messageUuid, leaveConversation: args.type === 'leave', operationId: args.operationId, }); }, }); export const agentDoSomething = internalAction({ args: { worldId: v.id('worlds'), player: v.object(serializedPlayer), agent: v.object(serializedAgent), map: v.object(serializedWorldMap), otherFreePlayers: v.array(v.object(serializedPlayer)), operationId: v.string(), }, handler: async (ctx, args) => { const { player, agent } = args; const map = new WorldMap(args.map); const now = Date.now(); // Don't try to start a new conversation if we were just in one. const justLeftConversation = agent.lastConversation && now < agent.lastConversation + CONVERSATION_COOLDOWN; // Don't try again if we recently tried to find someone to invite. const recentlyAttemptedInvite = agent.lastInviteAttempt && now < agent.lastInviteAttempt + CONVERSATION_COOLDOWN; const recentActivity = player.activity && now < player.activity.until + ACTIVITY_COOLDOWN; // Decide whether to do an activity or wander somewhere. if (!player.pathfinding) { if (recentActivity || justLeftConversation) { await sleep(Math.random() * 1000); await ctx.runMutation(api.aiTown.main.sendInput, { worldId: args.worldId, name: 'finishDoSomething', args: { operationId: args.operationId, agentId: agent.id, destination: wanderDestination(map), }, }); return; } else { // TODO: have LLM choose the activity & emoji const activity = ACTIVITIES[Math.floor(Math.random() * ACTIVITIES.length)]; await sleep(Math.random() * 1000); await ctx.runMutation(api.aiTown.main.sendInput, { worldId: args.worldId, name: 'finishDoSomething', args: { operationId: args.operationId, agentId: agent.id, activity: { description: activity.description, emoji: activity.emoji, until: Date.now() + activity.duration, }, }, }); return; } } const invitee = justLeftConversation || recentlyAttemptedInvite ? undefined : await ctx.runQuery(internal.aiTown.agent.findConversationCandidate, { now, worldId: args.worldId, player: args.player, otherFreePlayers: args.otherFreePlayers, }); // TODO: We hit a lot of OCC errors on sending inputs in this file. It's // easy for them to get scheduled at the same time and line up in time. await sleep(Math.random() * 1000); await ctx.runMutation(api.aiTown.main.sendInput, { worldId: args.worldId, name: 'finishDoSomething', args: { operationId: args.operationId, agentId: args.agent.id, invitee, }, }); }, }); function wanderDestination(worldMap: WorldMap) { // Wander someonewhere at least one tile away from the edge. return { x: 1 + Math.floor(Math.random() * (worldMap.width - 2)), y: 1 + Math.floor(Math.random() * (worldMap.height - 2)), }; }