Stage guarded Classic migration and native bot adapter
Core tests / core (push) Successful in 29s

This commit is contained in:
Codex
2026-09-25 22:57:11 +02:00
parent 58b2019015
commit 02d21abcbd
17 changed files with 616 additions and 11 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
import {classicMode,classicCommand} from './classic';
import {startIntentCheck} from './intent-evidence';
import {intentCompanions,partyIntentBusy,recordPartyIntent,compileCommunityAction} from './community';
import {DeterministicProvider,providerForBot} from './llm';
@@ -188,7 +189,7 @@ export async function runLiveAutonomyTick(
// 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 source=classicMode(config)?(peer?classicCommand('party',bot,'',peer):classicCommand('intent',bot,decision.action)):(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);
+11
View File
@@ -0,0 +1,11 @@
import type {Bot,RuntimeConfig} from './types';
export const classicMode=(config:RuntimeConfig)=>config.GAME_BACKEND==='cmangos-classic';
/** Typed native actions only; the Classic bridge never evaluates Lua. */
export function classicCommand(kind:'party'|'leave'|'recruit'|'intent'|'say',bot:Bot,argument='',peer?:Bot){
const identity=bot.autonomy;
if(!identity?.managed||!Number.isInteger(identity.characterGuid)||!identity.characterGuid||identity.characterGuid<1||!Number.isInteger(identity.accountId)||!identity.accountId||identity.accountId<1)throw new Error('Verified Classic character identity required');
if(['party','recruit','say'].includes(kind)&&(!peer?.autonomy?.managed||!peer.autonomy.characterGuid||!peer.autonomy.accountId))throw new Error('Verified Classic peer required');
return JSON.stringify({version:1,backend:'cmangos-classic',kind,guid:identity.characterGuid,accountId:identity.accountId,name:bot.name,argument,peerGuid:peer?.autonomy?.characterGuid??0,peerAccountId:peer?.autonomy?.accountId??0,peerName:peer?.name??'',expiresAt:Math.floor(Date.now()/1000)+90});
}
+4 -3
View File
@@ -1,5 +1,6 @@
import {classicMode,classicCommand} from './classic';
import {observeIntentChecks} from './intent-evidence';
import type {Bot,State,StateStore,SocialEntry} from './types';
import type {Bot,State,StateStore,SocialEntry,RuntimeConfig} from './types';
import {luaString} from './ale';
import {queueAleCommand} from './ale-store';
@@ -103,7 +104,7 @@ async function ensure(db:D1Database){
await db.prepare('CREATE TABLE IF NOT EXISTS community_actions (id TEXT PRIMARY KEY,created_at TEXT,kind TEXT,a TEXT,b TEXT,command_id TEXT,status TEXT,reason TEXT)').run();
await db.prepare('CREATE TABLE IF NOT EXISTS community_clock (id TEXT PRIMARY KEY,at INTEGER)').run();
}
export async function communityTick(store:StateStore,db:D1Database){
export async function communityTick(store:StateStore,db:D1Database,config:RuntimeConfig={}){
await ensure(db);
const pending=await db.prepare("SELECT * FROM community_actions WHERE status='pending' LIMIT 20").all<any>();
for(const action of pending.results){
@@ -147,7 +148,7 @@ export async function communityTick(store:StateStore,db:D1Database){
}
}
async function propose(kind:'party'|'leave'|'recruit',a:Bot,b:Bot|undefined,reason:string){
const command=await queueAleCommand(db,compileCommunityAction(kind,a,b));
const command=await queueAleCommand(db,classicMode(config)?classicCommand(kind,a,kind==='leave'?a.world!.groupId:'',b):compileCommunityAction(kind,a,b));
await db.prepare('INSERT INTO community_actions VALUES (?,?,?,?,?,?,?,?)').bind(command.id,new Date().toISOString(),kind,a.id,b?.id??'',command.id,'pending',reason).run();
await changeCommunity(store,s=>socialEntry(s,{id:command.id+':proposal',at:new Date().toISOString(),kind:'invitation',names:[a.name,...(b?[b.name]:[])],text:reason+' Awaiting game confirmation.',status:'pending'}));
}
+5 -2
View File
@@ -1,3 +1,4 @@
import {classicMode,classicCommand} from './classic';
import {bondEvidence,socialEntry,communityTick} from './community';
import { z } from 'zod';
import { queueAleCommand } from './ale-store';
@@ -180,7 +181,7 @@ export async function runConversationTick(store:StateStore,db:D1Database,config:
}
}
await cleanupConversations(db);
await communityTick(store,db);
await communityTick(store,db,config);
const request=await leaseConversation(db);
if(!request)return {ok:true,ran:false,reason:'No player conversation waiting'};
@@ -202,7 +203,9 @@ export async function runConversationTick(store:StateStore,db:D1Database,config:
reply=cleanReply(reply);
if(!reply)throw new Error('Conversation reply was empty');
const command=await queueAleCommand(db,compileReply(bot.name,request.speakerName,reply,request.channel,request.socialTurn!==undefined));
if(classicMode(config)&&request.socialTurn===undefined)throw new Error('Classic player chat delivery is not enabled yet');
const source=classicMode(config)?classicCommand('say',bot,reply,byName(state,request.speakerName)):compileReply(bot.name,request.speakerName,reply,request.channel,request.socialTurn!==undefined);
const command=await queueAleCommand(db,source);
await awaitDelivery(db,request,bot.id,reply,command.id);
await cleanupConversations(db);
+1 -1
View File
@@ -29,4 +29,4 @@ export type Observation = {zone:string; level:number; availableActions:Activity[
export interface GameConnector {id:string; health():Promise<{ok:boolean; detail:string}>; observe(bot:Bot):Promise<Observation>; lifecycle(bot:Bot,action:'spawn'|'pause'|'resume'|'despawn'|'retire',requestId:string):Promise<void>; decide(bot:Bot,decision:Decision,requestId:string):Promise<{summary:string;zone?:string}>;}
export interface LLMProvider {id:string; decide(bot:Bot,observation:Observation):Promise<Decision>}
export interface StateStore {load():Promise<State>; save(state:State,expectedRevision:number):Promise<void>}
export type RuntimeConfig = {SOAP_URL?:string; SOAP_USER?:string; SOAP_PASSWORD?:string; BRIDGE_URL?:string; BRIDGE_TOKEN?:string; ALE_BRIDGE_TOKEN?:string; ALE_OPERATOR_TOKEN?:string; ALE_RAW_EXEC_ENABLED?:string; AUTONOMY_TOKEN?:string; LLM_URL?:string; LLM_API_KEY?:string};
export type RuntimeConfig = {GAME_BACKEND?:string;SOAP_URL?:string; SOAP_USER?:string; SOAP_PASSWORD?:string; BRIDGE_URL?:string; BRIDGE_TOKEN?:string; ALE_BRIDGE_TOKEN?:string; ALE_OPERATOR_TOKEN?:string; ALE_RAW_EXEC_ENABLED?:string; AUTONOMY_TOKEN?:string; LLM_URL?:string; LLM_API_KEY?:string};