Extract Persona social guidance source

This commit is contained in:
Meta Vens 2026-07-16 02:27:30 +01:00
commit 89166d4456
7 changed files with 106 additions and 78 deletions

View file

@ -22,7 +22,6 @@ import {
import { createTextEmbedder, createVectorStores, RetrievalService } from "@voidbot/rag";
import {
loadPromptTemplate,
type InteractionMemoryProfile,
type PromptImageAttachment,
type RepoFaceConversationFocus,
type SourceMessage,
@ -66,6 +65,7 @@ import { readPersonaCuriosityEvidence } from "../apps/persona-scheduler/dist/per
import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/persona-curiosity-projector.js";
import { significantPersonaTopicTerms } from "../apps/persona-scheduler/dist/persona-curiosity-terms.js";
import { observePersonaRoomTexture, projectPersonaSocialContext, renderPersonaHumanPronounFacts, renderPersonaRoomWeather, type PersonaHumanPronounGuidance as RepoFaceHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
import { readPersonaHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-source.js";
import {
readDiscordActivitySnapshot,
type IdleCoolingSnapshot,
@ -1248,83 +1248,23 @@ async function loadRepoFaceHumanPronounGuidance(
channelSnapshots: ChannelSnapshot[];
},
): Promise<RepoFaceHumanPronounGuidance[]> {
const visibleHumans = new Map<string, string>();
for (const message of [
...(roomContext?.recentMessages ?? []),
...(roomContext?.channelSnapshots.flatMap((snapshot) => snapshot.messages) ?? []),
]) {
if (!message.isBot && message.authorId) {
visibleHumans.set(message.authorId, message.authorName || message.authorId);
}
}
visibleHumans.set(config.ownerDiscordId, visibleHumans.get(config.ownerDiscordId) ?? "Metacrat");
const storage = await createStateStorage({
backend: config.stateStorageBackend,
databaseDsn: config.databaseDsn,
jobsFile: config.jobsFile,
auditLogFile: config.auditLogFile,
interactionMemoryFile: config.interactionMemoryFile,
rateLimitStateFile: config.rateLimitStateFile,
return readPersonaHumanPronounGuidance({
ownerActorId: config.ownerDiscordId,
ownerFallbackName: "Metacrat",
recentMessages: roomContext?.recentMessages ?? [],
channelSnapshots: roomContext?.channelSnapshots ?? [],
openProfiles: async () => {
const storage = await createStateStorage({
backend: config.stateStorageBackend,
databaseDsn: config.databaseDsn,
jobsFile: config.jobsFile,
auditLogFile: config.auditLogFile,
interactionMemoryFile: config.interactionMemoryFile,
rateLimitStateFile: config.rateLimitStateFile,
});
return { getProfile: (actorId) => storage.interactionMemory.getProfile(actorId), close: () => storage.close() };
},
});
try {
const profiles = await Promise.all(
[...visibleHumans.entries()].map(async ([actorId, fallbackName]) => ({
actorId,
fallbackName,
profile: await storage.interactionMemory.getProfile(actorId),
})),
);
return profiles
.map(({ actorId, fallbackName, profile }) =>
profile ? repoFacePronounGuidanceFromProfile(actorId, fallbackName, profile) : undefined,
)
.filter((entry): entry is RepoFaceHumanPronounGuidance => entry !== undefined);
} finally {
await storage.close();
}
}
function repoFacePronounGuidanceFromProfile(
actorId: string,
fallbackName: string,
profile: InteractionMemoryProfile,
): RepoFaceHumanPronounGuidance | undefined {
if (profile.pronounPolicy === "unknown" || profile.resolvedPronounSets.length === 0) {
return undefined;
}
const evidence = [...profile.pronounEvidence]
.filter((entry) => entry.stance === "prefer" || entry.stance === "avoid")
.sort((left, right) => pronounEvidenceRank(profile, right) - pronounEvidenceRank(profile, left))[0];
return {
actorId,
actorName: profile.actorName || fallbackName,
guidance: profile.pronounGuidance,
resolvedPronounSet: profile.resolvedPronounSet,
policy: profile.pronounPolicy,
confidence: profile.pronounConfidence,
evidenceExcerpt: evidence?.excerpt,
};
}
function pronounEvidenceRank(profile: InteractionMemoryProfile, entry: InteractionMemoryProfile["pronounEvidence"][number]): number {
const sourceRank: Record<string, number> = {
explicit_self_statement: 10_000,
explicit_correction: 9_000,
direct_third_party_statement: 7_000,
contextual_relational_inference: 3_000,
ambient_usage: 1_000,
};
const resolvedSetBonus = profile.resolvedPronounSets.includes(entry.pronounSet) ? 50_000 : 0;
const stanceBonus = entry.stance === "prefer" ? 1_000 : 0;
const confidenceBonus = Math.round(entry.confidence * 100);
const timestampMs = Date.parse(entry.timestamp);
const recencyBonus = Number.isFinite(timestampMs) ? timestampMs / 10_000_000_000 : 0;
return resolvedSetBonus + (sourceRank[entry.source] ?? 0) + stanceBonus + confidenceBonus + recencyBonus;
}
function normalizeSocialLabel(value: string | undefined): string {

View file

@ -69,6 +69,7 @@ import { composePersonaMemoryPacket, projectPersonaMemorySurface, renderPersonaP
import { readPersonaCuriosityEvidence } from "../apps/persona-scheduler/dist/persona-curiosity-context-source.js";
import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/persona-curiosity-projector.js";
import { observePersonaRoomTexture, projectPersonaSocialContext, renderPersonaHumanClarityPressure, renderPersonaHumanPronounFacts, renderPersonaRoomWeather, renderPersonaSocialGraph } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
import { readPersonaHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-source.js";
function participant(identityId: string, nextTurnAt: number): InitiativeParticipant {
return {
@ -456,6 +457,30 @@ const roomObservation = observePersonaRoomTexture({
assert.equal(roomObservation?.texture, "light", "social-context projection derives room weather from supplied messages without acquiring Discord state");
assert.match(renderPersonaRoomWeather({ identity: routedIdentity, recentMessages: [], channelSnapshots: [] }), /No current room weather/, "absent room evidence stays explicit");
assert.match(renderPersonaHumanPronounFacts([{ actorId: "human", actorName: "Operator", guidance: "use they/them", policy: "explicit", confidence: 1 }]) ?? "", /Confidence: 1\.00/, "pronoun guidance presentation belongs to the social-context projector");
let pronounReaderClosed = false;
const pronounGuidance = await readPersonaHumanPronounGuidance({
ownerActorId: "owner",
ownerFallbackName: "Metacrat",
recentMessages: [{ id: "human-pronouns", authorId: "human", authorName: "Operator", content: "hello", timestamp: "2026-07-15T21:00:00.000Z", isBot: false, channelId: "aquarium" }],
channelSnapshots: [],
openProfiles: async () => ({
getProfile: async (actorId) => actorId === "human" ? {
actorName: "Operator",
pronounPolicy: "explicit",
resolvedPronounSets: ["they/them"],
resolvedPronounSet: "they/them",
pronounGuidance: "use they/them",
pronounConfidence: 0.95,
pronounEvidence: [
{ pronounSet: "they/them", stance: "prefer", source: "explicit_correction", confidence: 0.9, timestamp: "2026-07-14T20:00:00.000Z", excerpt: "please use they/them" },
{ pronounSet: "they/them", stance: "prefer", source: "ambient_usage", confidence: 1, timestamp: "2026-07-15T20:00:00.000Z", excerpt: "someone said they" },
],
} as never : undefined,
close: async () => { pronounReaderClosed = true; },
}),
});
assert.match(pronounGuidance[0]?.evidenceExcerpt ?? "", /please use they\/them/, "the social-context source owns pronoun evidence precedence rather than favoring merely newer ambient usage");
assert.equal(pronounReaderClosed, true, "the social-context source closes its injected interaction-profile reader");
const clarityPressure = renderPersonaHumanClarityPressure({
identity: routedIdentity,
recentMessages: [