Import verified AzerothAI snapshot from GitHub
Core tests / core (push) Successful in 9m57s
Full verification / application (push) Failing after 5m16s
Full verification / local-brain (push) Failing after 6m15s
Realm simulation / simulated-live-world (push) Failing after 5m32s
WoW integration lab / real-worldserver (push) Canceled after 17m23s
Core tests / core (push) Successful in 9m57s
Full verification / application (push) Failing after 5m16s
Full verification / local-brain (push) Failing after 6m15s
Realm simulation / simulated-live-world (push) Failing after 5m32s
WoW integration lab / real-worldserver (push) Canceled after 17m23s
Source branch: feature/ai-brain-router Source commit: c2a09e0460465201a58aaab56c71424e9a96405e
This commit is contained in:
+303
@@ -0,0 +1,303 @@
|
||||
import { runGM } from './gm';
|
||||
import { actionSchema } from './validation';
|
||||
import { connectors } from './connectors';
|
||||
import { providerForBot } from './llm';
|
||||
import { applySimulationPopulationPlan,chooseHomeForNewResident,runPopulationDirector } from './world';
|
||||
import type { State,StateStore,RuntimeConfig,Bot,RealmEvent,Status,Observation,DeathRecord } from './types';
|
||||
|
||||
function event(s:State,kind:RealmEvent['kind'],message:string,botId?:string){
|
||||
s.events.push({id:crypto.randomUUID(),seq:++s.sequence,at:new Date().toISOString(),kind,message,botId,mode:s.settings.connector});
|
||||
s.events=s.events.slice(-500);
|
||||
}
|
||||
function botById(s:State,id:string){const b=s.bots.find(b=>b.id===id);if(!b)throw new Error('Bot not found');return b;}
|
||||
|
||||
function observedText(payload:Record<string,unknown>,key:string){
|
||||
const value=payload[key];
|
||||
return typeof value==='string'&&value.trim()?value.trim():undefined;
|
||||
}
|
||||
function recipeForObservedBot(s:State,name:string,role:string){
|
||||
if(name.toLowerCase()==='zaku')return s.recipes.find(r=>r.id==='zaku')??s.recipes[0];
|
||||
if(['Priest','Paladin','Druid','Shaman'].includes(role))return s.recipes.find(r=>r.id==='healer')??s.recipes[0];
|
||||
if(['Warrior','Death Knight'].includes(role))return s.recipes.find(r=>r.id==='veteran')??s.recipes[0];
|
||||
if(role==='Rogue')return s.recipes.find(r=>r.id==='trader')??s.recipes[0];
|
||||
return s.recipes.find(r=>r.id==='explorer')??s.recipes[0];
|
||||
}
|
||||
|
||||
function memorialize(s:State,dead:Bot){
|
||||
if(!s.settings.hardcore.memorialize)return;
|
||||
const text=`${dead.name} died permanently at level ${dead.level} in ${dead.death?.zone??dead.zone}. ${dead.death?.cause??'Cause unknown.'}`;
|
||||
for(const bot of s.bots){
|
||||
if(bot.id===dead.id||bot.status==='dead'||bot.status==='retired')continue;
|
||||
const knewThem=bot.relationships.some(r=>r.targetId===dead.id)||dead.relationships.some(r=>r.targetId===bot.id)||bot.tier==='hero';
|
||||
if(!knewThem)continue;
|
||||
bot.memories.push({id:crypto.randomUUID(),text,importance:9,source:s.settings.connector==='simulation'?'simulation':'observation',at:new Date().toISOString()});
|
||||
bot.memories=bot.memories.slice(-100);
|
||||
}
|
||||
}
|
||||
|
||||
function recordDeath(s:State,b:Bot,death:DeathRecord){
|
||||
if(b.status==='dead')return;
|
||||
if(b.plotArmorHpFloor>=1)throw new Error(`${b.name} has plot armor and cannot enter the death state; live combat must clamp HP to ${b.plotArmorHpFloor}.`);
|
||||
if(!b.mortal)throw new Error(`${b.name} is configured as immortal and cannot enter the hardcore death state`);
|
||||
b.status='dead';
|
||||
b.death=death;
|
||||
memorialize(s,b);
|
||||
event(s,'death',`${b.name} died at level ${death.level} in ${death.zone}. One life ended permanently. Cause: ${death.cause}${death.killer?` · Killer: ${death.killer}`:''}`,b.id);
|
||||
}
|
||||
|
||||
function observedHardcoreDeath(s:State,b:Bot,observation:Observation){
|
||||
if(observation.alive!==false)return false;
|
||||
if(b.plotArmorHpFloor>=1)throw new Error(`Plot armor breach: ${b.name} was reported dead by the game bridge. The worldserver must keep this character at or above ${b.plotArmorHpFloor} HP.`);
|
||||
if(!s.settings.hardcore.enabled){
|
||||
event(s,'lifecycle',`${b.name} is currently dead in-game; decision skipped while standard resurrection rules are enabled.`,b.id);
|
||||
return true;
|
||||
}
|
||||
if(s.settings.hardcore.requireVerifiedDeath&&(!observation.death||!observation.death.verified)){
|
||||
throw new Error('Game bridge reported a death without a verified death event. Hardcore identity was NOT deleted.');
|
||||
}
|
||||
const signal=observation.death;
|
||||
recordDeath(s,b,{
|
||||
id:signal?.id??crypto.randomUUID(),
|
||||
at:signal?.at??new Date().toISOString(),
|
||||
zone:signal?.zone??observation.zone,
|
||||
level:observation.level,
|
||||
cause:signal?.cause??'Game server reported character death',
|
||||
killer:signal?.killer,
|
||||
source:s.settings.connector,
|
||||
verified:signal?.verified??false
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function applyAuthoritativeAleEvent(store:StateStore,kind:string,payload:Record<string,unknown>){
|
||||
let s=await store.load();
|
||||
const revision=s.revision;
|
||||
if(kind==='bridge.ready'){
|
||||
let changed=false;
|
||||
for(const bot of s.bots){
|
||||
if(bot.status==='dead'||bot.status==='retired'||bot.autonomy?.managed)continue;
|
||||
if(bot.status==='online'||bot.status==='paused'){
|
||||
bot.status='offline';
|
||||
bot.simulationBand='dormant';
|
||||
changed=true;
|
||||
}
|
||||
}
|
||||
if(changed){
|
||||
event(s,'system','Authoritative AzerothCore bridge came online. Unobserved prototype residents were moved offline until a real Playerbot is observed.');
|
||||
await store.save(s,revision);
|
||||
}
|
||||
return {matched:false,bridgeReady:true};
|
||||
}
|
||||
const name=typeof payload.name==='string'?payload.name.trim():'';
|
||||
if(payload.isBot!==true||!name)return {matched:false};
|
||||
const characterGuid=typeof payload.characterGuid==='number'&&Number.isInteger(payload.characterGuid)&&payload.characterGuid>0?payload.characterGuid:undefined;
|
||||
const accountId=typeof payload.accountId==='number'&&Number.isInteger(payload.accountId)&&payload.accountId>0?payload.accountId:undefined;
|
||||
let b=(characterGuid?s.bots.find(x=>x.autonomy?.characterGuid===characterGuid):undefined)??s.bots.find(x=>x.name.toLowerCase()===name.toLowerCase());
|
||||
if(!b){
|
||||
const role=observedText(payload,'className');
|
||||
const race=observedText(payload,'raceName');
|
||||
if(!role||!race)return {matched:false,reason:'Observed Playerbot is missing class/race identity metadata'};
|
||||
const zone=observedText(payload,'zoneName')?.slice(0,120)??'Unknown';
|
||||
const level=typeof payload.level==='number'&&Number.isInteger(payload.level)&&payload.level>0&&payload.level<=100?payload.level:1;
|
||||
const recipe=recipeForObservedBot(s,name,role);
|
||||
const isZaku=name.toLowerCase()==='zaku';
|
||||
b={
|
||||
id:isZaku?'hero-zaku':`live-${crypto.randomUUID()}`,
|
||||
name,role,race,level,zone,status:'online',
|
||||
tier:isZaku?'hero':'resident',mortal:true,plotArmorHpFloor:isZaku?1:0,
|
||||
simulationBand:'background',
|
||||
routine:{wakeHour:isZaku?10:7,sleepHour:isZaku?3:23,preferredZones:[zone]},
|
||||
autonomy:{managed:true,characterGuid,accountId,observedAt:new Date().toISOString()},
|
||||
recipeId:recipe.id,recipeVersion:recipe.version,personality:structuredClone(recipe),
|
||||
goal:isZaku
|
||||
?'Survive, build a life in Azeroth, return home when I need safety, form real relationships, and pursue goals that matter even when no human player is online.'
|
||||
:'Build a life in Azeroth, pursue personal goals, and respond to the people and events I actually encounter.',
|
||||
memories:[{id:crypto.randomUUID(),text:`I am in ${zone}. This is where my current life in Azeroth is unfolding.`,importance:6,source:'observation',at:new Date().toISOString()}],
|
||||
relationships:[],decisions:0
|
||||
};
|
||||
const home=chooseHomeForNewResident(s,b);
|
||||
if(home){b.homeId=home.id;home.residentIds.push(b.id);}
|
||||
s.bots.push(b);
|
||||
event(s,'lifecycle',`${b.name} was adopted from the authoritative Playerbot world as a persistent AzerothAI resident.`,b.id);
|
||||
}
|
||||
const zone=(typeof payload.zoneName==='string'&&payload.zoneName.trim())?payload.zoneName.trim().slice(0,120):b.zone;
|
||||
const level=typeof payload.level==='number'&&Number.isInteger(payload.level)&&payload.level>0&&payload.level<=100?payload.level:b.level;
|
||||
b.autonomy={...(b.autonomy??{}),managed:true,characterGuid:characterGuid??b.autonomy?.characterGuid,accountId:accountId??b.autonomy?.accountId,observedAt:new Date().toISOString()};
|
||||
|
||||
if(b.status==='dead'){
|
||||
if(kind==='player.resurrect'||kind==='player.repop'){
|
||||
event(s,'error',`Authoritative server event attempted to return permanently dead resident ${b.name} to play. Hardcore identity remains closed.`,b.id);
|
||||
await store.save(s,revision);
|
||||
}
|
||||
return {matched:true,botId:b.id,dead:true};
|
||||
}
|
||||
if(b.status==='retired')return {matched:true,botId:b.id,retired:true};
|
||||
|
||||
b.level=level;
|
||||
if(zone)b.zone=zone;
|
||||
|
||||
if(kind==='player.login'){
|
||||
b.status='online';
|
||||
b.simulationBand='foreground';
|
||||
event(s,'lifecycle',`${b.name} logged into the authoritative AzerothCore world.`,b.id);
|
||||
}else if(kind==='player.logout'){
|
||||
b.status='offline';
|
||||
b.simulationBand='dormant';
|
||||
event(s,'lifecycle',`${b.name} logged out of the authoritative AzerothCore world.`,b.id);
|
||||
}else if(kind==='player.zone'||kind==='player.map'){
|
||||
if(b.status==='offline')b.status='online';
|
||||
b.simulationBand='foreground';
|
||||
event(s,'lifecycle',`${b.name} is now in ${b.zone} (authoritative ALE observation).`,b.id);
|
||||
}else if(kind==='player.snapshot'){
|
||||
if(b.status==='offline')b.status='online';
|
||||
if(b.simulationBand==='dormant')b.simulationBand='background';
|
||||
}else if(kind==='player.level'){
|
||||
const text=`I reached level ${b.level} while living in ${b.zone}.`;
|
||||
b.memories.push({id:crypto.randomUUID(),text,importance:8,source:'observation',at:new Date().toISOString()});
|
||||
b.memories=b.memories.slice(-100);
|
||||
event(s,'lifecycle',`${b.name} reached level ${b.level}.`,b.id);
|
||||
}else if(kind==='player.quest.accept'){
|
||||
const quest=typeof payload.quest==='string'?payload.quest.slice(0,160):'a new quest';
|
||||
b.memories.push({id:crypto.randomUUID(),text:`I accepted ${quest} in ${b.zone}.`,importance:5,source:'observation',at:new Date().toISOString()});
|
||||
b.memories=b.memories.slice(-100);
|
||||
event(s,'memory',`${b.name} accepted a quest in ${b.zone}.`,b.id);
|
||||
}else if(kind==='player.achievement'){
|
||||
const achievement=typeof payload.achievement==='string'?payload.achievement.slice(0,160):'an achievement';
|
||||
b.memories.push({id:crypto.randomUUID(),text:`I earned ${achievement}.`,importance:7,source:'observation',at:new Date().toISOString()});
|
||||
b.memories=b.memories.slice(-100);
|
||||
event(s,'memory',`${b.name} earned an achievement.`,b.id);
|
||||
}else if(kind==='player.combat.enter'){
|
||||
b.simulationBand='foreground';
|
||||
event(s,'system',`${b.name} entered combat in ${b.zone}.`,b.id);
|
||||
}else if(kind==='player.combat.leave'){
|
||||
event(s,'system',`${b.name} left combat in ${b.zone}.`,b.id);
|
||||
}else if(kind==='player.close-call'){
|
||||
const text=`I barely survived a fight in ${b.zone}. I came close enough to death that I need to take danger more seriously.`;
|
||||
const recent=b.memories.some(m=>m.source==='observation'&&m.text===text&&Date.now()-Date.parse(m.at)<60_000);
|
||||
if(!recent){
|
||||
b.memories.push({id:crypto.randomUUID(),text,importance:10,source:'observation',at:new Date().toISOString()});
|
||||
b.memories=b.memories.slice(-100);
|
||||
}
|
||||
event(s,'memory',`${b.name} survived a close call in ${b.zone}; the experience was committed to memory.`,b.id);
|
||||
}else if(kind==='player.killed-by-creature'){
|
||||
if(b.plotArmorHpFloor>=1){
|
||||
event(s,'error',`Plot armor breach: authoritative ALE reported ${b.name} dead. Worldserver damage enforcement must prevent this transition.`,b.id);
|
||||
}else if(s.settings.hardcore.enabled){
|
||||
recordDeath(s,b,{
|
||||
id:typeof payload.deathId==='string'?payload.deathId:crypto.randomUUID(),
|
||||
at:new Date().toISOString(),
|
||||
zone:b.zone,
|
||||
level:b.level,
|
||||
cause:'Killed in combat',
|
||||
killer:typeof payload.killer==='string'?payload.killer.slice(0,120):undefined,
|
||||
source:'azerothcore',
|
||||
verified:true
|
||||
});
|
||||
}else{
|
||||
event(s,'lifecycle',`${b.name} died in-game; standard resurrection rules remain active.`,b.id);
|
||||
}
|
||||
}
|
||||
|
||||
await store.save(s,revision);
|
||||
return {matched:true,botId:b.id,status:b.status,zone:b.zone,level:b.level,characterGuid:b.autonomy?.characterGuid,accountId:b.autonomy?.accountId};
|
||||
}
|
||||
|
||||
export async function execute(store:StateStore,input:unknown,config:RuntimeConfig={}) {
|
||||
const a=actionSchema.parse(input);let s=await store.load();
|
||||
const connector=connectors[s.settings.connector](config);
|
||||
if(a.type==='health')return {health:await connector.health()};
|
||||
const active=Object.values(s.operations).find(o=>Date.now()-o.started<60000);
|
||||
if(active)throw new Error('A realm operation is in progress. Wait for it to finish.');
|
||||
if(Object.keys(s.operations).length){s.operations={};event(s,'error','An interrupted operation expired. Verify live server state before retrying.');}
|
||||
const revision=s.revision;
|
||||
if(a.type==='gm'){
|
||||
const id=crypto.randomUUID();s.operations[id]={started:Date.now(),botId:'gm'};event(s,'gm',`${a.live?'LIVE':'SIMULATED'} command requested: ${a.command}`);await store.save(s,revision);
|
||||
try{const result=await runGM(a.command,a.live,a.confirmed,config);delete s.operations[id];event(s,'gm',`${a.live?'LIVE':'SIMULATED'} ${result.command}: ${result.output}`);await store.save(s,s.revision);return result;}catch(e){delete s.operations[id];event(s,'error',`GM command failed: ${e instanceof Error?e.message:'Unknown error'}`);await store.save(s,s.revision);throw e;}
|
||||
}else if(a.type==='director'){
|
||||
s.director={experience:a.experience,zone:a.zone,playerName:a.playerName,brief:a.brief};
|
||||
for(const b of s.bots.filter(b=>b.status!=='retired'&&b.status!=='dead')) b.goal=`${a.experience} in ${a.zone}${a.playerName?' around '+a.playerName:''}: ${a.brief}`.slice(0,200);
|
||||
event(s,'system',`Realm direction updated: ${a.experience} in ${a.zone}. Living bot goals updated; movement remains the connector's responsibility.`);
|
||||
}else if(a.type==='createBot'){
|
||||
if(s.bots.length>=100)throw new Error('Prototype limit: 100 bot identities');
|
||||
if(s.bots.some(b=>b.name.toLowerCase()===a.name.toLowerCase()))throw new Error('That name is already in use');
|
||||
const p=s.recipes.find(r=>r.id===a.recipeId);if(!p)throw new Error('Recipe not found');
|
||||
const newBot:Bot={id:crypto.randomUUID(),name:a.name,role:a.role,race:a.race,level:1,zone:s.director.zone,status:'offline',tier:'resident',mortal:true,plotArmorHpFloor:0,simulationBand:'dormant',routine:{wakeHour:7,sleepHour:23,preferredZones:[s.director.zone]},recipeId:p.id,recipeVersion:p.version,personality:structuredClone(p),goal:a.goal,memories:[],relationships:[],decisions:0};
|
||||
const home=chooseHomeForNewResident(s,newBot);
|
||||
if(home){newBot.homeId=home.id;home.residentIds.push(newBot.id);newBot.memories.push({id:crypto.randomUUID(),text:`I live at ${home.name} in ${home.zone}. It is the place I return to.`,importance:8,source:'simulation',at:new Date().toISOString()});}
|
||||
s.bots.push(newBot);
|
||||
event(s,'lifecycle',`${a.name} created as a mortal resident${home?` with a home at ${home.name}`:''}. Ready to enter the world.`);
|
||||
}else if(a.type==='recordDeath'){
|
||||
if(!s.settings.hardcore.enabled)throw new Error('Hardcore mode is disabled. Permanent death recording is unavailable.');
|
||||
const b=botById(s,a.botId);
|
||||
if(b.status==='dead')throw new Error(`${b.name} is already permanently dead`);
|
||||
recordDeath(s,b,{id:crypto.randomUUID(),at:new Date().toISOString(),zone:a.zone||b.zone,level:b.level,cause:a.cause,killer:a.killer,source:'operator',verified:true});
|
||||
}else if(a.type==='worldTrigger'){
|
||||
const trigger={id:crypto.randomUUID(),kind:a.kind,zone:a.zone,playerName:a.playerName,detail:a.detail,at:new Date().toISOString()};
|
||||
const plan=runPopulationDirector(s,trigger);
|
||||
s.population.lastPlan=plan;
|
||||
if(s.settings.connector==='simulation')applySimulationPopulationPlan(s,plan);
|
||||
const names=plan.selectedBotIds.map(id=>s.bots.find(b=>b.id===id)?.name).filter(Boolean).join(', ');
|
||||
event(s,'population',`World trigger ${a.kind} in ${a.zone}: requested ${plan.requested}, capacity ${plan.capacity}, activated/planned ${plan.selectedBotIds.length}${names?` (${names})`:''}. No synthetic identities were created.`);
|
||||
}else if(a.type==='assignHome'){
|
||||
const b=botById(s,a.botId);
|
||||
if(b.status==='dead'||b.status==='retired')throw new Error('Only living active identities can move home.');
|
||||
const home=s.homes.find(h=>h.id===a.homeId);if(!home)throw new Error('Home not found');
|
||||
if(!home.residentIds.includes(b.id)&&home.residentIds.length>=home.capacity)throw new Error('That home is at capacity');
|
||||
for(const h of s.homes)h.residentIds=h.residentIds.filter(id=>id!==b.id);
|
||||
home.residentIds.push(b.id);b.homeId=home.id;
|
||||
b.memories.push({id:crypto.randomUUID(),text:`I moved to ${home.name} in ${home.zone}. This is home now.`,importance:8,source:'operator',at:new Date().toISOString()});b.memories=b.memories.slice(-100);
|
||||
event(s,'housing',`${b.name} now lives at ${home.name} in ${home.zone}.`,b.id);
|
||||
}else if(a.type==='saveRecipe'){
|
||||
const old=s.recipes.find(r=>r.id===a.recipe.id);if(a.recipe.id&&!old)throw new Error('Recipe not found');
|
||||
if(old&&a.recipe.version!==old.version)throw new Error('Recipe changed. Reopen it before saving.');
|
||||
if(!old&&s.recipes.length>=100)throw new Error('Prototype limit: 100 recipes');
|
||||
const recipe={...a.recipe,id:old?.id??crypto.randomUUID(),version:(old?.version??0)+1};
|
||||
s.recipes=old?s.recipes.map(r=>r.id===old.id?recipe:r):[...s.recipes,recipe];event(s,'recipe',`${recipe.name} saved as version ${recipe.version}. Existing bots retain their snapshot.`);
|
||||
}else if(a.type==='settings'){
|
||||
if(a.connector!==s.settings.connector&&s.bots.some(b=>b.status==='online'||b.status==='paused'))throw new Error('Despawn all online and paused bots before switching connectors.');
|
||||
if(a.connector==='azerothcore'&&(!config.BRIDGE_URL||!config.BRIDGE_TOKEN))throw new Error('AzerothCore bridge is not configured on the server.');
|
||||
if(a.provider!=='deterministic'&&!config.LLM_URL)throw new Error('Local LLM endpoint is not configured on the server.');
|
||||
s.settings={connector:a.connector,provider:a.provider,model:a.model,brains:a.brains,hardcore:a.hardcore,population:a.population};
|
||||
event(s,'system',`Realm connector and model settings updated. Hardcore one-life mode is ${a.hardcore.enabled?'enabled':'disabled'}.`);
|
||||
}else{
|
||||
const b=botById(s,a.botId);
|
||||
if(b.status==='dead')throw new Error(`${b.name} is permanently dead. This identity cannot act, spawn, resume, or be recycled.`);
|
||||
if(a.type==='memory'){
|
||||
b.memories.push({id:crypto.randomUUID(),text:a.text,importance:a.importance,source:'operator',at:new Date().toISOString()});b.memories=b.memories.slice(-100);event(s,'memory',`${b.name} received an operator memory.`,b.id);
|
||||
}else if(a.type==='relationship'){
|
||||
botById(s,a.targetId);if(a.targetId===b.id)throw new Error('A bot cannot have a relationship with itself');
|
||||
b.relationships=[...b.relationships.filter(r=>r.targetId!==a.targetId),{targetId:a.targetId,affinity:a.affinity,note:a.note}];event(s,'relationship',`${b.name}'s relationship updated.`,b.id);
|
||||
}else if(a.type==='applyRecipe'){
|
||||
const p=s.recipes.find(r=>r.id===a.recipeId);if(!p)throw new Error('Recipe not found');b.personality=structuredClone(p);b.recipeId=p.id;b.recipeVersion=p.version;event(s,'recipe',`${b.name} now uses ${p.name} v${p.version}.`,b.id);
|
||||
}else if(a.type==='lifecycle'||a.type==='think'){
|
||||
const transitions:Record<string,{from:Status[];to:Status}>={spawn:{from:['offline'],to:'online'},pause:{from:['online'],to:'paused'},resume:{from:['paused'],to:'online'},despawn:{from:['online','paused'],to:'offline'},retire:{from:['offline'],to:'retired'}};
|
||||
if(a.type==='think'&&b.status!=='online')throw new Error('Only online bots can make decisions');
|
||||
if(a.type==='lifecycle'&&!transitions[a.action].from.includes(b.status))throw new Error(`Cannot ${a.action} a ${b.status} bot`);
|
||||
const requestId=crypto.randomUUID();s.operations[requestId]={started:Date.now(),botId:b.id};await store.save(s,revision);
|
||||
try{
|
||||
if(a.type==='lifecycle'){
|
||||
await connector.lifecycle(b,a.action,requestId);
|
||||
b.status=transitions[a.action].to;if(a.action==='spawn'){b.zone=s.settings.connector==='simulation'?s.director.zone:'Awaiting observation';b.simulationBand='foreground';}if(a.action==='despawn'||a.action==='retire')b.simulationBand='dormant';if(a.action==='pause')b.simulationBand='background';if(a.action==='resume')b.simulationBand='foreground';event(s,'lifecycle',`${b.name}: ${a.action} acknowledged (${s.settings.connector}).`,b.id);
|
||||
}else{
|
||||
const observation=await connector.observe(b);
|
||||
if(observedHardcoreDeath(s,b,observation)){
|
||||
delete s.operations[requestId];
|
||||
await store.save(s,s.revision);
|
||||
return {ok:true,dead:true};
|
||||
}
|
||||
const brain=providerForBot(b,s.settings,config);
|
||||
const decision=await brain.provider.decide(b,observation);
|
||||
event(s,'system',`${b.name} brain route: ${brain.route.provider} · ${brain.route.model} · ${brain.route.reason}.`,b.id);
|
||||
const result=await connector.decide(b,decision,requestId);
|
||||
b.lastDecision=decision;b.decisions++;b.goal=decision.goal;b.zone=result.zone??observation.zone;b.level=observation.level;
|
||||
b.memories.push({id:crypto.randomUUID(),text:result.summary,importance:5,source:s.settings.connector==='simulation'?'simulation':'observation',at:new Date().toISOString()});b.memories=b.memories.slice(-100);
|
||||
event(s,'decision',result.summary,b.id);
|
||||
}
|
||||
}catch(err){
|
||||
delete s.operations[requestId];event(s,'error',`${b.name}: operation failed; no local lifecycle change. ${err instanceof Error?err.message:'Unknown error'}`,b.id);await store.save(s,s.revision);throw err;
|
||||
}
|
||||
delete s.operations[requestId];await store.save(s,s.revision);return {ok:true};
|
||||
}
|
||||
}
|
||||
await store.save(s,revision);return {ok:true};
|
||||
}
|
||||
Reference in New Issue
Block a user