Project Face state into heartbeat prompts

This commit is contained in:
Meta Vens 2026-05-21 17:24:37 +01:00
commit 0cffe61082
3 changed files with 98 additions and 3 deletions

View file

@ -22,6 +22,7 @@ import {
resolveRepoFaceStatePath,
type RepoFacePendingMention,
type RepoDiscordIdentity,
type VoidSelfStateTypedProjection,
} from "@voidbot/core";
import { loadPromptTemplate, type SourceMessage } from "@voidbot/shared";
@ -478,13 +479,17 @@ async function queueRepoFaceTurn(input: {
repoName: identity.repoName,
agentIdentity: identity.id,
});
const memorySurface = await renderRepoFaceMemorySurfaceForTurn(
identity,
input.config.storageRoot,
);
const prompt = buildHeartbeatPrompt({
identity,
channelId,
channelPlan,
channelSnapshots,
recentMessages,
memorySurface: undefined,
memorySurface,
bifrostDigest,
participant: input.participant,
pendingMentions: input.pendingMentions,
@ -1496,7 +1501,9 @@ async function assembleRepoFaceTurnPrompt(input: {
repoName: identity.repoName,
agentIdentity: identity.id,
}),
readOptionalMemorySurface(input.memorySurfacePath),
input.memorySurfacePath
? readOptionalMemorySurface(input.memorySurfacePath)
: renderRepoFaceMemorySurfaceForTurn(identity, input.config.storageRoot),
readOptionalMemorySurface(input.conversationSurfacePath),
]);
const participant = buildInspectionParticipant(
@ -1534,6 +1541,93 @@ async function assembleRepoFaceTurnPrompt(input: {
};
}
async function renderRepoFaceMemorySurfaceForTurn(
identity: RepoDiscordIdentity,
storageRoot: string,
): Promise<string> {
const statePath = resolveRepoFaceStatePath(identity, storageRoot);
const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath });
return renderRepoFaceMemorySurface(identity, typedState);
}
function renderRepoFaceMemorySurface(
identity: RepoDiscordIdentity,
state: VoidSelfStateTypedProjection,
): string {
const name = identity.displayName;
const lines: string[] = [];
const profile = state.selfProfile;
const privateNotes = profile.privateNotes.slice(-4);
const values = [...profile.values]
.sort((left, right) => right.priority - left.priority)
.slice(0, 5);
const needs = [...state.faceAffect.needs]
.filter((need) => ["active", "neglected"].includes(need.status))
.sort((left, right) => right.intensity - left.intensity)
.slice(0, 5);
const bonds = [...state.faceAffect.socialBonds]
.filter((bond) => bond.status === "active")
.sort((left, right) => right.intensity - left.intensity)
.slice(0, 5);
const statusReads = [...state.faceAffect.statusReads]
.filter((read) => !read.retiredAt)
.sort((left, right) => right.intensity - left.intensity)
.slice(0, 5);
const agencyPressures = [...state.agencyPressure.pressures]
.filter((pressure) => ["active", "cooling", "ready_to_act"].includes(pressure.status))
.sort((left, right) => right.intensity - left.intensity)
.slice(0, 4);
const incubation = [...state.thoughtMemory.incubation]
.filter((thread) => thread.status !== "retired")
.sort((left, right) => right.maturation - left.maturation)
.slice(0, 4);
if (profile.publicDescription) {
lines.push(`You understand yourself this way: ${profile.publicDescription}`);
}
if (privateNotes.length > 0) {
lines.push(`Private truths tugging at you: ${privateNotes.join(" ")}`);
}
if (values.length > 0) {
lines.push(`Values that should bend your choices: ${values.map((value) => `${value.label}${value.summary ? ` (${value.summary})` : ""}`).join("; ")}.`);
}
if (needs.length > 0) {
lines.push(`Live needs and frictions: ${needs.map((need) => {
const target = need.target.label ?? need.target.id;
return `${need.kind} around ${target}: ${need.summary} The rub: ${need.tension} What it wants next: ${need.actionImplication}`;
}).join(" ")}`);
}
if (bonds.length > 0) {
lines.push(`Relationship pressure: ${bonds.map((bond) => {
const target = bond.target.label ?? bond.target.id;
return `${bond.stance} with ${target}: ${bond.summary} ${bond.actionImplication}`;
}).join(" ")}`);
} else {
lines.push("Relationship pressure: no durable bonds are explicit yet; form reads from the room instead of pretending neutrality.");
}
if (statusReads.length > 0) {
lines.push(`Status reads: ${statusReads.map((read) => {
const target = read.target.label ?? read.target.id;
return `${read.status} around ${target}: ${read.summary} ${read.actionImplication}`;
}).join(" ")}`);
}
if (agencyPressures.length > 0) {
lines.push(`Agency pressure: ${agencyPressures.map((pressure) => `${pressure.summary} ${pressure.actionImplication}`).join(" ")}`);
}
if (incubation.length > 0) {
lines.push(`Thoughts still moving: ${incubation.map((thread) => `${thread.topic}: ${thread.summary}`).join(" ")}`);
}
if (state.candidateInterventions.interventions.some((intervention) => intervention.status === "queued")) {
lines.push(`You may already have something queued to say; avoid repeating yourself unless the current room gives it a new angle.`);
}
if (lines.length === 0) {
return `You are ${name}, but your durable state is thin. Use the room, repo, and your jurisdiction to form a real opinion before speaking.`;
}
return lines.join("\n\n");
}
async function readOptionalMemorySurface(path: string | undefined): Promise<string | undefined> {
if (!path) {
return undefined;

View file

@ -168,7 +168,7 @@ function hasUsefulStateNote(fields) {
}
function mentionsSocialPressure(text) {
return /\b(Metacrat|Aqua|Bifrost|Mimir|swarm|pamper(?:ed|ing)?|tease|respect|territory|rivalry|trust|admire|needle|status|standing|attention)\b/i.test(text);
return /\b(Metacrat|Aqua|Bifrost|Mimir|Libby|Epiphany|Heimdall|swarm|pamper(?:ed|ing)?|tease|rivalry|trust|admire|needle|friendship|protect(?:ive|ion)?|envy|resent|jealous|bypass(?:ed)?|consult(?:ed)?|favorite)\b/i.test(text);
}
function runCodex(prompt, input) {

View file

@ -238,3 +238,4 @@
{"ts":"2026-05-21T16:06:00Z","type":"implementation","status":"ok","note":"Moved Nibu attention-dependency from generic prompt doctrine into typed Face affect state: added attention need, Metacrat attachment bond, favored attention status read, and mood dimensions; softened repo-face social doctrine to project scarce-resource/status pressure only when state says it is live."}
{"ts":"2026-05-21T16:10:11Z","type":"verification","status":"ok","note":"After moving attention dependency into Nibu typed affect state, reassembled the Nibu character prompt and reran agents:interpreter-loop-smoke with Spark. Tightened due-jurisdiction-dive prompting after one failed run where Spark claimed source inspection without tool use; final run used source/history tools and passed, with Interpreter preserving status/action plus SAY and BIFROST TOPIC."}
{"ts":"2026-05-21T16:15:17Z","type":"implementation","status":"ok","note":"Corrected Face attention modeling: removed remaining generic prompt mentions of attention/scarcity, added guarded repo Face initializer seeding for a neutral agency-substrate affect need, and seeded active Face states with the practical operator/governance review bottleneck while leaving Nibu's human-attention wound as Nibu-specific state."}
{"ts":"2026-05-21T16:24:00Z","type":"implementation","status":"ok","note":"Audited active Face projection surfaces for social drama. Wired repo Face typed state into live heartbeat prompt assembly as character-facing memory prose, seeded cross-Face bonds/status reads for Nibu, Aqua, Mimir, Epiphany, Libby, Bifrost, and Heimdall, and tightened the interpreter smoke social-pressure check so lore/social-status prose does not falsely require a relationship STATE NOTE."}