Ground bot intentions in live game knowledge and verify outcomes
Core tests / core (push) Successful in 28s
Core tests / core (push) Successful in 28s
This commit is contained in:
+30
-6
@@ -1,3 +1,5 @@
|
||||
import {startIntentCheck} from './intent-evidence';
|
||||
import {intentCompanions,partyIntentBusy,recordPartyIntent,compileCommunityAction} from './community';
|
||||
import {DeterministicProvider,providerForBot} from './llm';
|
||||
import {luaString} from './ale';
|
||||
import {queueAleCommand,recentAleEvents} from './ale-store';
|
||||
@@ -26,7 +28,7 @@ function latestObservationFor(bot:Bot,events:Awaited<ReturnType<typeof recentAle
|
||||
});
|
||||
}
|
||||
|
||||
function observationFrom(bot:Bot,event:Awaited<ReturnType<typeof recentAleEvents>>[number],events:Awaited<ReturnType<typeof recentAleEvents>>,hardcore=true):Observation{
|
||||
export function observationFrom(bot:Bot,event:Awaited<ReturnType<typeof recentAleEvents>>[number],events:Awaited<ReturnType<typeof recentAleEvents>>,hardcore=true):Observation{
|
||||
const p=payloadObject(event.payload);
|
||||
const zone=typeof p.zoneName==='string'&&p.zoneName.trim()?p.zoneName.trim().slice(0,120):bot.zone;
|
||||
const level=typeof p.level==='number'&&Number.isInteger(p.level)&&p.level>0&&p.level<=100?p.level:bot.level;
|
||||
@@ -38,11 +40,23 @@ function observationFrom(bot:Bot,event:Awaited<ReturnType<typeof recentAleEvents
|
||||
zone,
|
||||
level,
|
||||
alive:bot.status!=='dead',
|
||||
availableActions:[...bot.personality.activities],
|
||||
summary:`Realm death rules: ${hardcore?'permanent death':'normal resurrection; recover and resume progression after death'}. Authoritative live Playerbot observation in ${zone} at level ${level}. Recent server events: ${recent.join(', ')||'snapshot'}.`
|
||||
availableActions:availableIntentActions(bot),
|
||||
summary:`Realm death rules: ${hardcore?'permanent death':'normal resurrection; recover and resume progression after death'}. Authoritative live Playerbot observation in ${zone} at level ${level}. Recent server events: ${recent.join(', ')||'snapshot'}. ${decisionContext(bot)}`
|
||||
};
|
||||
}
|
||||
|
||||
export function availableIntentActions(bot:Bot){
|
||||
const actions=[...bot.personality.activities],w=bot.world;
|
||||
if(w&&Date.now()/1000-w.at>=0&&Date.now()/1000-w.at<90&&(w.healthPct??0)>=95&&(w.manaPct===undefined||w.manaPct>=65)&&actions.some(a=>a!=='rest'))return actions.filter(a=>a!=='rest');
|
||||
return actions;
|
||||
}
|
||||
export function decisionContext(bot:Bot){
|
||||
const w=bot.world;if(!w||Date.now()/1000-w.at>90)return 'Live condition details unavailable; do not invent them.';
|
||||
const ready=w.readinessAt&&Date.now()/1000-w.readinessAt<180;
|
||||
const last=bot.autonomy?.intentChecks?.filter(c=>c.status!=='pending').slice(-2).map(c=>`${c.action}: ${c.status}; ${c.detail}`).join(' | ');
|
||||
return JSON.stringify({health:w.healthPct,mana:w.manaPct,dead:w.isDead||w.isGhost,inCombat:w.inCombat,nearbyThreats:w.threatBrief??'unknown',activeQuestLog:w.questBrief??'unknown',supplies:ready?{food:w.foodCount,drinks:w.drinkCount,durability:w.minDurabilityPct,brokenItems:w.brokenItems}:null,party:w.groupNames,nearby:w.nearby,recentOutcomes:last||'No outcome verified yet',capabilities:'quest/explore request native progression; trade requests native supplies/repair; socialize can invite an eligible nearby companion. Never claim an action completed until observed.'});
|
||||
}
|
||||
|
||||
function strategy(ai:string,change:string){
|
||||
return `${ai}:ChangeStrategy(${luaString(change)},1)`;
|
||||
}
|
||||
@@ -55,6 +69,7 @@ export function compileLivePlayerbotIntent(botName:string,decision:Decision){
|
||||
`if not ${p}:IsPlayerbot() then error("Managed character is not controlled by PlayerbotAI") end`,
|
||||
`local ${ai}=${p}:GetBotAI()`,
|
||||
`if not ${ai} then error("PlayerbotAI unavailable") end`,
|
||||
`if ${p}:IsDead() then return "Native resurrection takes priority" end`,
|
||||
`if ${ai}:HasGameClientMaster() then return "Human control preserved" end`,
|
||||
`if ${p}:HasUnitState(0x00000100) then return "Native taxi flight takes priority" end`,
|
||||
`if AzerothAISurvivalOverride and AzerothAISurvivalOverride(${p}) then return "Native survival action takes priority" end`,
|
||||
@@ -82,7 +97,7 @@ export function compileLivePlayerbotIntent(botName:string,decision:Decision){
|
||||
case 'trade':
|
||||
clearStay();
|
||||
body.push(strategy(ai,'+rpg'));
|
||||
body.push(`${ai}:DoSpecificAction("rpg sell",true,"")`);
|
||||
body.push(`${ai}:DoSpecificAction("rpg sell",true,"");${ai}:DoSpecificAction("rpg buy",true,"");${ai}:DoSpecificAction("repair",true,"")`);
|
||||
body.push(`${ai}:DoNextAction(false)`);
|
||||
break;
|
||||
case 'rest':
|
||||
@@ -112,7 +127,7 @@ export async function runLiveAutonomyTick(
|
||||
const events=await recentAleEvents(db,400);
|
||||
|
||||
const candidates=s.bots
|
||||
.filter(b=>b.status!=='dead'&&b.status!=='retired'&&b.autonomy?.managed===true)
|
||||
.filter(b=>b.status==='online'&&b.autonomy?.managed===true&&!b.world?.isDead&&!b.world?.isGhost&&!b.world?.humanControlled&&!b.world?.taxi)
|
||||
.map(bot=>({bot,obs:latestObservationFor(bot,events)}))
|
||||
.filter((x):x is {bot:Bot;obs:NonNullable<typeof x.obs>}=>!!x.obs)
|
||||
.filter(x=>now-Date.parse(x.obs.at)<=OBSERVATION_MAX_AGE_MS)
|
||||
@@ -147,6 +162,8 @@ export async function runLiveAutonomyTick(
|
||||
const observation=observationFrom(bot,pick.obs,events,s.settings.hardcore.enabled);
|
||||
const guild=s.community?.guilds.find(g=>g.id===bot.world?.guildId);
|
||||
if(guild)observation.summary+=` Your guild ${guild.name} works toward ${guild.goal}; confirmed progress ${guild.progress}/${guild.target}. Favor helpful actions toward that goal when safe.`;
|
||||
const companions=intentCompanions(s,bot).slice(0,3);
|
||||
observation.summary+=' Eligible nearby party candidates: '+(companions.map(p=>`${p.name} (level ${p.level}, ${p.world?.combatRole??p.role})`).join(', ')||'none; do not invent a companion or promise a party')+'.';
|
||||
const brain=providerForBot(bot,s.settings,config);
|
||||
let decision:Decision;
|
||||
let usedFallback=false;
|
||||
@@ -168,8 +185,14 @@ export async function runLiveAutonomyTick(
|
||||
}
|
||||
bot=currentBot;
|
||||
if(usedFallback)realmEvent(s,'system',`${bot.name} LLM route unavailable; used deterministic fallback for this heartbeat.`,bot.id);
|
||||
const source=compileLivePlayerbotIntent(bot.name,decision);
|
||||
// Recheck eligibility after inference; native guards recheck once more at execution.
|
||||
let peer=decision.action==='socialize'?intentCompanions(s,bot)[0]:undefined;
|
||||
if(peer&&await partyIntentBusy(db,bot,peer))peer=undefined;
|
||||
const source=peer?compileCommunityAction('party',bot,peer):compileLivePlayerbotIntent(bot.name,decision);
|
||||
const queued=await queueAleCommand(db,source);
|
||||
if(peer)await recordPartyIntent(db,s,queued.id,bot,peer);
|
||||
const check=startIntentCheck(bot,decision,queued.id,Date.now()/1000,peer?.name);
|
||||
|
||||
|
||||
bot.lastDecision=decision;
|
||||
bot.decisions++;
|
||||
@@ -182,6 +205,7 @@ export async function runLiveAutonomyTick(
|
||||
observedAt:newerObservation?bot.autonomy?.observedAt:pick.obs.at,
|
||||
lastIntentAt:new Date(now).toISOString(),
|
||||
nextIntentAt:new Date(now+nextIntervalMs(bot)).toISOString(),
|
||||
intentChecks:[...(bot.autonomy?.intentChecks??[]),...(check?[check]:[])].slice(-12),
|
||||
lastAleCommandId:queued.id
|
||||
};
|
||||
realmEvent(s,'system',`${bot.name} brain route: ${brain.route.provider} · ${brain.route.model} · autonomous live heartbeat.`,bot.id);
|
||||
|
||||
Reference in New Issue
Block a user