Extract person-shaped rumination organ
This commit is contained in:
parent
11bacf1c61
commit
c0cf87520b
7 changed files with 273 additions and 37 deletions
21
apps/persona-scheduler/src/relative-chronology-projector.ts
Normal file
21
apps/persona-scheduler/src/relative-chronology-projector.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export function projectRelativeChronology<T>(value: T, observedAt: Date): T {
|
||||
return projectValue(value, observedAt) as T;
|
||||
}
|
||||
|
||||
function projectValue(value: unknown, observedAt: Date, key = ""): unknown {
|
||||
if (Array.isArray(value)) return value.map((entry) => projectValue(entry, observedAt));
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, projectValue(entry, observedAt, entryKey)]));
|
||||
if (typeof value === "string" && /(?:at|timestamp)$/i.test(key) && Number.isFinite(Date.parse(value))) return relativeTime(value, observedAt);
|
||||
return value;
|
||||
}
|
||||
|
||||
function relativeTime(value: string, observedAt: Date): string {
|
||||
const difference = observedAt.getTime() - Date.parse(value);
|
||||
const future = difference < 0;
|
||||
const minutes = Math.max(0, Math.round(Math.abs(difference) / 60_000));
|
||||
const phrase = (amount: number, unit: string) => future ? `in ${amount} ${unit}${amount === 1 ? "" : "s"}` : `${amount} ${unit}${amount === 1 ? "" : "s"} ago`;
|
||||
if (minutes < 60) return phrase(minutes, "minute");
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 24) return phrase(hours, "hour");
|
||||
return phrase(Math.round(hours / 24), "day");
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core";
|
||||
import { projectRelativeChronology } from "./relative-chronology-projector.js";
|
||||
|
||||
const ALLOWED_OPERATIONS = new Set([
|
||||
"merge_incubation_support", "queue_candidate_intervention", "retire_candidate_intervention",
|
||||
|
|
@ -39,7 +40,7 @@ export function projectVoidMemoryMaintenanceContext(input: {
|
|||
const activeAgency = state.agencyPressure.pressures.filter((entry) => ["active", "cooling", "ready_to_act"].includes(entry.status));
|
||||
const activeCandidates = state.candidateInterventions.interventions.filter((entry) => entry.status === "queued" || entry.status === "deferred");
|
||||
const maintenancePressure = activeShortTerm.length + state.thoughtMemory.incubation.length + activeAgency.length + activeCandidates.length;
|
||||
const relative = <T>(value: T): T => projectRelativeChronology(value, input.observedAt) as T;
|
||||
const relative = <T>(value: T): T => projectRelativeChronology(value, input.observedAt);
|
||||
return {
|
||||
mode: sleeping ? "sleep_maintenance" : "awake_memory_maintenance",
|
||||
operationTimestamp: input.observedAt.toISOString(),
|
||||
|
|
@ -85,19 +86,3 @@ export function parseVoidMemoryMaintenanceOperations(outputText: string): VoidSe
|
|||
return operation;
|
||||
});
|
||||
}
|
||||
|
||||
function projectRelativeChronology(value: unknown, observedAt: Date, key = ""): unknown {
|
||||
if (Array.isArray(value)) return value.map((entry) => projectRelativeChronology(entry, observedAt));
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, projectRelativeChronology(entry, observedAt, entryKey)]));
|
||||
if (typeof value === "string" && /(?:At|Timestamp)$/.test(key) && Number.isFinite(Date.parse(value))) return relativeTime(value, observedAt);
|
||||
return value;
|
||||
}
|
||||
|
||||
function relativeTime(value: string, observedAt: Date): string {
|
||||
const differenceMs = observedAt.getTime() - Date.parse(value);
|
||||
if (!Number.isFinite(differenceMs)) return "unknown time";
|
||||
const future = differenceMs < 0;
|
||||
const minutes = Math.max(0, Math.round(Math.abs(differenceMs) / 60_000));
|
||||
const amount = minutes < 60 ? `${minutes} minute${minutes === 1 ? "" : "s"}` : minutes < 1_440 ? `${Math.round(minutes / 60)} hour${Math.round(minutes / 60) === 1 ? "" : "s"}` : `${Math.round(minutes / 1_440)} day${Math.round(minutes / 1_440) === 1 ? "" : "s"}`;
|
||||
return future ? `in ${amount}` : `${amount} ago`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core";
|
||||
import { z } from "zod";
|
||||
import { projectRelativeChronology } from "./relative-chronology-projector.js";
|
||||
|
||||
const ALLOWED_MODERATION_TAGS = new Set(["moderation:instaban", "moderation:strike", "moderation:case_only"]);
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ export function projectVoidModerationHeartbeatContext(input: {
|
|||
enforcementMode: string;
|
||||
rules: string;
|
||||
}): VoidModerationHeartbeatContext {
|
||||
const relative = <T>(value: T): T => projectRelativeChronology(value, input.observedAt) as T;
|
||||
const relative = <T>(value: T): T => projectRelativeChronology(value, input.observedAt);
|
||||
return {
|
||||
mode: "moderation_heartbeat",
|
||||
operationTimestamp: input.observedAt.toISOString(),
|
||||
|
|
@ -112,22 +113,3 @@ function validateOperation(entry: unknown, index: number, state: VoidSelfStateTy
|
|||
if (moderationTags.length !== 1) throw new Error("Moderation heartbeat open cases must include exactly one moderation classification tag.");
|
||||
return operation;
|
||||
}
|
||||
|
||||
function projectRelativeChronology(value: unknown, observedAt: Date, key = ""): unknown {
|
||||
if (Array.isArray(value)) return value.map((entry) => projectRelativeChronology(entry, observedAt));
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, projectRelativeChronology(entry, observedAt, entryKey)]));
|
||||
if (typeof value === "string" && /(?:at|timestamp)$/i.test(key) && Number.isFinite(Date.parse(value))) return relativeTime(value, observedAt);
|
||||
return value;
|
||||
}
|
||||
|
||||
function relativeTime(value: string, observedAt: Date): string {
|
||||
const difference = observedAt.getTime() - Date.parse(value);
|
||||
const future = difference < 0;
|
||||
const minutes = Math.max(0, Math.round(Math.abs(difference) / 60_000));
|
||||
const phrase = (amount: number, unit: string) => future ? `in ${amount} ${unit}${amount === 1 ? "" : "s"}` : `${amount} ${unit}${amount === 1 ? "" : "s"} ago`;
|
||||
if (minutes < 60) return phrase(minutes, "minute");
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 24) return phrase(hours, "hour");
|
||||
const days = Math.round(hours / 24);
|
||||
return phrase(days, "day");
|
||||
}
|
||||
|
|
|
|||
53
apps/persona-scheduler/src/void-rumination-organ.ts
Normal file
53
apps/persona-scheduler/src/void-rumination-organ.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { applyVoidSelfStateOperation, loadVoidSelfStateTypedDocuments } from "@voidbot/core";
|
||||
import { buildVoidRuminationPrompt, parseVoidRuminationOperations, projectVoidRuminationContext } from "./void-rumination-projector.js";
|
||||
|
||||
export type VoidRuminationResult =
|
||||
| { status: "skipped"; reason: "already_attempted_this_pressure" | "napping_without_pressure"; runnerId?: string }
|
||||
| { status: "ok"; runnerId: string; appliedOperationCount: number; queuedCandidateIds: string[] };
|
||||
|
||||
export async function runVoidRumination(input: {
|
||||
statePath: string;
|
||||
observedAt?: Date;
|
||||
pendingMentions?: unknown[];
|
||||
recentHistory?: unknown;
|
||||
repoActivity?: unknown;
|
||||
recentRoomLinks?: unknown;
|
||||
recentConversationTarget?: unknown;
|
||||
publicSpeechTarget?: unknown;
|
||||
doctrine: string;
|
||||
rules: string;
|
||||
voice: string;
|
||||
}, dependencies: {
|
||||
loadState?: typeof loadVoidSelfStateTypedDocuments;
|
||||
applyOperation?: typeof applyVoidSelfStateOperation;
|
||||
projectText: (prompt: string) => Promise<string>;
|
||||
}): Promise<VoidRuminationResult> {
|
||||
const loadState = dependencies.loadState ?? loadVoidSelfStateTypedDocuments;
|
||||
const applyOperation = dependencies.applyOperation ?? applyVoidSelfStateOperation;
|
||||
const state = await loadState({ canonicalPath: input.statePath });
|
||||
const observedAt = input.observedAt ?? new Date();
|
||||
const context = projectVoidRuminationContext({ ...input, state, observedAt });
|
||||
const hasExternalPressure = (input.pendingMentions?.length ?? 0) > 0 || hasObservedItems(input.recentHistory) || hasObservedItems(input.repoActivity) || hasObservedItems(input.recentRoomLinks);
|
||||
const hasInternalPressure = context.speechPressureObligationIds.length > 0 || state.moderationCursor.openCases.some((entry) => ["pending", "watching"].includes(entry.status)) || state.candidateInterventions.interventions.some((entry) => entry.status === "queued");
|
||||
if (state.scheduledRuntime.sleepCycle.isNapping && !hasExternalPressure && !hasInternalPressure) return { status: "skipped", reason: "napping_without_pressure" };
|
||||
const runnerId = `void-rumination-attempt:${context.pressureFingerprint}`;
|
||||
if (state.scheduledRuntime.lastRuns.some((run) => run.runner === runnerId)) return { status: "skipped", reason: "already_attempted_this_pressure", runnerId };
|
||||
await applyOperation({ canonicalPath: input.statePath }, {
|
||||
operation: "record_scheduled_run",
|
||||
run: { runner: runnerId, ranAt: observedAt.toISOString(), summary: "Started one bounded person-shaped rumination attempt for this pressure fingerprint." },
|
||||
});
|
||||
const operations = parseVoidRuminationOperations({ outputText: await dependencies.projectText(buildVoidRuminationPrompt(context)), state });
|
||||
for (const operation of operations) await applyOperation({ canonicalPath: input.statePath }, operation);
|
||||
return {
|
||||
status: "ok", runnerId, appliedOperationCount: operations.length,
|
||||
queuedCandidateIds: operations.flatMap((operation) => operation.operation === "queue_candidate_intervention" ? [operation.intervention.interventionId] : []),
|
||||
};
|
||||
}
|
||||
|
||||
function hasObservedItems(value: unknown): boolean {
|
||||
if (Array.isArray(value)) return value.length > 0;
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const record = value as Record<string, unknown>;
|
||||
for (const key of ["messages", "items", "commits", "repos"]) if (Array.isArray(record[key]) && record[key].length > 0) return true;
|
||||
return false;
|
||||
}
|
||||
127
apps/persona-scheduler/src/void-rumination-projector.ts
Normal file
127
apps/persona-scheduler/src/void-rumination-projector.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core";
|
||||
import { projectRelativeChronology } from "./relative-chronology-projector.js";
|
||||
|
||||
const ALLOWED_OPERATIONS = new Set([
|
||||
"upsert_open_case", "close_open_case", "record_short_term_memory", "merge_incubation_support",
|
||||
"upsert_agency_pressure", "retire_agency_pressure", "upsert_affect_need", "retire_affect_need",
|
||||
"upsert_social_bond", "retire_social_bond", "upsert_status_read", "retire_status_read",
|
||||
"update_mood_dimensions", "queue_candidate_intervention", "retire_candidate_intervention",
|
||||
]);
|
||||
|
||||
export interface VoidRuminationContext {
|
||||
mode: "person_shaped_rumination";
|
||||
operationTimestamp: string;
|
||||
pressureFingerprint: string;
|
||||
selfProfile: unknown;
|
||||
affect: unknown;
|
||||
memories: unknown[];
|
||||
shortTermMemories: unknown[];
|
||||
incubation: unknown[];
|
||||
agencyPressure: unknown[];
|
||||
speechPressureObligationIds: string[];
|
||||
candidateInterventions: unknown[];
|
||||
speechReceipts: unknown[];
|
||||
openCases: unknown[];
|
||||
scheduledRuntime: unknown;
|
||||
pendingMentions: unknown[];
|
||||
recentHistory: unknown;
|
||||
repoActivity: unknown;
|
||||
recentRoomLinks: unknown;
|
||||
recentConversationTarget: unknown;
|
||||
publicSpeechTarget: unknown;
|
||||
doctrine: string;
|
||||
rules: string;
|
||||
voice: string;
|
||||
}
|
||||
|
||||
export function projectVoidRuminationContext(input: {
|
||||
state: VoidSelfStateTypedProjection;
|
||||
observedAt: Date;
|
||||
pendingMentions?: unknown[];
|
||||
recentHistory?: unknown;
|
||||
repoActivity?: unknown;
|
||||
recentRoomLinks?: unknown;
|
||||
recentConversationTarget?: unknown;
|
||||
publicSpeechTarget?: unknown;
|
||||
doctrine: string;
|
||||
rules: string;
|
||||
voice: string;
|
||||
}): VoidRuminationContext {
|
||||
const obligationIds = speechPressureObligationIds(input.state);
|
||||
const fingerprint = fingerprintVoidRuminationPressure({
|
||||
state: input.state,
|
||||
pendingMentions: input.pendingMentions ?? [], recentHistory: input.recentHistory ?? null,
|
||||
repoActivity: input.repoActivity ?? null, recentRoomLinks: input.recentRoomLinks ?? null,
|
||||
});
|
||||
const relative = <T>(value: T): T => projectRelativeChronology(value, input.observedAt);
|
||||
return {
|
||||
mode: "person_shaped_rumination", operationTimestamp: input.observedAt.toISOString(), pressureFingerprint: fingerprint,
|
||||
selfProfile: relative(input.state.selfProfile), affect: relative(input.state.faceAffect),
|
||||
memories: relative(input.state.thoughtMemory.memories), shortTermMemories: relative(input.state.thoughtMemory.shortTerm),
|
||||
incubation: relative(input.state.thoughtMemory.incubation), agencyPressure: relative(input.state.agencyPressure.pressures),
|
||||
speechPressureObligationIds: obligationIds, candidateInterventions: relative(input.state.candidateInterventions.interventions),
|
||||
speechReceipts: relative(input.state.speechReceipts.recentReceipts), openCases: relative(input.state.moderationCursor.openCases),
|
||||
scheduledRuntime: relative(input.state.scheduledRuntime), pendingMentions: relative(input.pendingMentions ?? []),
|
||||
recentHistory: relative(input.recentHistory ?? null), repoActivity: relative(input.repoActivity ?? null),
|
||||
recentRoomLinks: relative(input.recentRoomLinks ?? null), recentConversationTarget: input.recentConversationTarget ?? null,
|
||||
publicSpeechTarget: input.publicSpeechTarget ?? null, doctrine: input.doctrine, rules: input.rules, voice: input.voice,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildVoidRuminationPrompt(context: VoidRuminationContext): string {
|
||||
return [
|
||||
"You are Void's private person-shaped rumination organ. Think as a participant with values and relationships, not as a maintenance report.",
|
||||
"Return only a JSON array of complete typed Void self-state operation objects. No markdown, commentary, tools, file writes, or direct transport calls.",
|
||||
`Allowed operations: ${[...ALLOWED_OPERATIONS].join(", ")}.`,
|
||||
"New memories must be short-term and meaning-preserving. Queue speech only as candidate interventions; the parent delivery organ owns transport and receipts.",
|
||||
"A reply candidate must retain the source channel and message in deliveryTarget. publicSpeechTarget is only for unanchored public artifacts or herald speech.",
|
||||
"For every speechPressureObligationId, queue a live candidate tagged source_pressure:<id>, retire the pressure, or upsert it with cooling/resolved status. Silent [] is invalid while obligations exist.",
|
||||
"Use operationTimestamp for fresh exact timestamps. Relative chronology is evidence only.",
|
||||
"If no anchored state change or sayable pressure exists, return [].",
|
||||
"Rumination context:", JSON.stringify(context, null, 2),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
export function parseVoidRuminationOperations(input: { outputText: string; state: VoidSelfStateTypedProjection }): VoidSelfStateOperation[] {
|
||||
const trimmed = input.outputText.trim();
|
||||
const candidate = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim() ?? trimmed;
|
||||
let raw: unknown;
|
||||
try { raw = JSON.parse(candidate); } catch (error) { throw new Error(`Void rumination output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`); }
|
||||
if (!Array.isArray(raw)) throw new Error("Void rumination output must be a JSON array.");
|
||||
const operations = raw.map((entry, index) => {
|
||||
const operation = voidSelfStateOperationSchema.parse(entry);
|
||||
if (!ALLOWED_OPERATIONS.has(operation.operation)) throw new Error(`Void rumination operation ${index + 1} is not allowed: ${operation.operation}.`);
|
||||
return operation;
|
||||
});
|
||||
assertSpeechPressureResolved(input.state, operations);
|
||||
return operations;
|
||||
}
|
||||
|
||||
export function fingerprintVoidRuminationPressure(input: { state: VoidSelfStateTypedProjection; pendingMentions: unknown[]; recentHistory: unknown; repoActivity: unknown; recentRoomLinks: unknown }): string {
|
||||
const state = input.state;
|
||||
const pressure = {
|
||||
affect: state.faceAffect, shortTerm: state.thoughtMemory.shortTerm, incubation: state.thoughtMemory.incubation,
|
||||
agency: state.agencyPressure.pressures, candidates: state.candidateInterventions.interventions,
|
||||
openCases: state.moderationCursor.openCases, receipts: state.speechReceipts.recentReceipts.slice(-16),
|
||||
pendingMentions: input.pendingMentions, recentHistory: input.recentHistory, repoActivity: input.repoActivity, recentRoomLinks: input.recentRoomLinks,
|
||||
};
|
||||
return createHash("sha256").update(JSON.stringify(pressure)).digest("hex").slice(0, 24);
|
||||
}
|
||||
|
||||
function speechPressureObligationIds(state: VoidSelfStateTypedProjection): string[] {
|
||||
return state.agencyPressure.pressures
|
||||
.filter((pressure) => ["active", "ready_to_act"].includes(pressure.status) && pressure.intensity >= 0.55)
|
||||
.filter((pressure) => !state.candidateInterventions.interventions.some((candidate) => ["queued", "spoken"].includes(candidate.status) && candidate.tags.includes(`source_pressure:${pressure.pressureId}`)))
|
||||
.map((pressure) => pressure.pressureId);
|
||||
}
|
||||
|
||||
function assertSpeechPressureResolved(state: VoidSelfStateTypedProjection, operations: VoidSelfStateOperation[]): void {
|
||||
for (const pressureId of speechPressureObligationIds(state)) {
|
||||
const resolved = operations.some((operation) =>
|
||||
(operation.operation === "queue_candidate_intervention" && operation.intervention.status === "queued" && operation.intervention.tags.includes(`source_pressure:${pressureId}`))
|
||||
|| (operation.operation === "retire_agency_pressure" && operation.pressureId === pressureId)
|
||||
|| (operation.operation === "upsert_agency_pressure" && operation.pressure.pressureId === pressureId && ["cooling", "resolved", "retired"].includes(operation.pressure.status)));
|
||||
if (!resolved) throw new Error(`Active advocacy pressure '${pressureId}' requires a candidate intervention or explicit cooling/retirement.`);
|
||||
}
|
||||
}
|
||||
10
apps/persona-scheduler/src/void-rumination-text-actuator.ts
Normal file
10
apps/persona-scheduler/src/void-rumination-text-actuator.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { loadConfig } from "@voidbot/config";
|
||||
import { requestBoundedTextCompletion } from "./bounded-text-completion-actuator.js";
|
||||
|
||||
export async function projectVoidRuminationOperations(input: { prompt: string; config: ReturnType<typeof loadConfig>; model?: string }, dependencies: { fetch?: typeof fetch; readText?: (path: string) => Promise<string> } = {}): Promise<string> {
|
||||
return requestBoundedTextCompletion({
|
||||
prompt: input.prompt, config: input.config, purpose: "Void person-shaped rumination",
|
||||
model: input.model ?? process.env.VOID_RUMINATION_MODEL,
|
||||
maxCompletionTokens: Number(process.env.VOID_RUMINATION_MAX_COMPLETION_TOKENS) || 3072,
|
||||
}, dependencies);
|
||||
}
|
||||
|
|
@ -89,6 +89,9 @@ import { readVoidModerationEvidence } from "../apps/persona-scheduler/dist/void-
|
|||
import { runVoidModerationHeartbeat } from "../apps/persona-scheduler/dist/void-moderation-heartbeat-organ.js";
|
||||
import { projectVoidModerationOperations } from "../apps/persona-scheduler/dist/void-moderation-text-actuator.js";
|
||||
import { runVoidModerationEnforcement } from "../apps/persona-scheduler/dist/void-moderation-enforcement-organ.js";
|
||||
import { buildVoidRuminationPrompt, parseVoidRuminationOperations, projectVoidRuminationContext } from "../apps/persona-scheduler/dist/void-rumination-projector.js";
|
||||
import { runVoidRumination } from "../apps/persona-scheduler/dist/void-rumination-organ.js";
|
||||
import { projectVoidRuminationOperations } from "../apps/persona-scheduler/dist/void-rumination-text-actuator.js";
|
||||
import { projectGamecultPersonaState } from "../apps/persona-scheduler/dist/persona-standard-state-projector.js";
|
||||
import { projectPersonaStatePacket } from "../apps/persona-scheduler/dist/persona-state-packet-projector.js";
|
||||
import { extractLastPersonaProjectionMessage, isRetryablePersonaProjectionFailure } from "../apps/persona-scheduler/dist/persona-text-projection-actuator.js";
|
||||
|
|
@ -924,6 +927,51 @@ assert.equal(bannedUserId, "human-1", "policy enforcement lowers an instaban thr
|
|||
assert.equal(moderationEnforcement.actions[0]?.status, "instaban_applied", "successful sanction closes the typed case with an enforcement result");
|
||||
const repeatedEnforcement = await runVoidModerationEnforcement({ statePath: "void-self-state.cc", mode: "policy", botToken: "token", guildId: "guild", observedAt: physiologyObservedAt }, { loadState: async () => moderationState });
|
||||
assert.equal(repeatedEnforcement.evaluatedCaseCount, 0, "resolved moderation debt cannot be sanctioned twice on later cadences");
|
||||
const ruminationState = createEmptyVoidSelfState({ createdAt: "2026-07-15T00:00:00.000Z" });
|
||||
ruminationState.scheduledRuntime.sleepCycle.isNapping = false;
|
||||
ruminationState.agencyPressure.pressures.push({
|
||||
pressureId: "pressure-1", kind: "world_advocacy_request", status: "active", target: { kind: "room", id: "aquarium" },
|
||||
summary: "A direct room claim deserves an answer.", claim: "The reply belongs in its source room.", tension: "Silence would leave the claim unanswered.",
|
||||
actionImplication: "Queue a source-anchored reply.", intensity: 0.8, anchorRefs: [{ ref: "discord:message-1" }], evidenceRefs: [], sourceMemoryIds: [],
|
||||
createdAt: physiologyObservedAt.toISOString(), updatedAt: physiologyObservedAt.toISOString(), tags: [],
|
||||
} as never);
|
||||
const ruminationContext = projectVoidRuminationContext({ state: ruminationState, observedAt: physiologyObservedAt, pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." });
|
||||
const laterRuminationContext = projectVoidRuminationContext({ state: ruminationState, observedAt: new Date("2026-07-16T04:00:00.000Z"), pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." });
|
||||
assert.equal(ruminationContext.pressureFingerprint, laterRuminationContext.pressureFingerprint, "wall-clock projection cannot manufacture a fresh rumination intent");
|
||||
assert.match(buildVoidRuminationPrompt(ruminationContext), /source_pressure:<id>/, "rumination prompt projects explicit speech-pressure accountability");
|
||||
assert.throws(() => parseVoidRuminationOperations({ outputText: "[]", state: ruminationState }), /requires a candidate/, "strong advocacy pressure cannot be silently acknowledged");
|
||||
assert.throws(() => parseVoidRuminationOperations({ outputText: JSON.stringify([{ operation: "propose_memory_distillation", proposal: {} }]), state: ruminationState }), /invalid|not allowed/i, "awake rumination cannot annex sleep-owned durable-memory policy");
|
||||
const ruminationCandidate = {
|
||||
operation: "queue_candidate_intervention", intervention: {
|
||||
interventionId: "candidate-1", kind: "world_advocacy", status: "queued", target: { kind: "room", id: "room-1" },
|
||||
summary: "Answer the source-room claim.", draft: "This answer stays with the conversation that made it necessary.",
|
||||
deliveryTarget: { channelId: "room-1", replyToMessageId: "message-1" }, priority: 0.8, mustEventuallyShare: true,
|
||||
createdAt: physiologyObservedAt.toISOString(), updatedAt: physiologyObservedAt.toISOString(), tags: ["source_pressure:pressure-1"],
|
||||
},
|
||||
};
|
||||
const parsedRumination = parseVoidRuminationOperations({ outputText: JSON.stringify([ruminationCandidate]), state: ruminationState });
|
||||
assert.equal(parsedRumination[0]?.operation, "queue_candidate_intervention", "person-shaped rumination crosses the typed operation boundary without delivering speech");
|
||||
let ruminationModelCalls = 0;
|
||||
const ruminationDependencies = {
|
||||
loadState: async () => ruminationState,
|
||||
projectText: async () => { ruminationModelCalls += 1; return ruminationModelCalls === 1 ? JSON.stringify([ruminationCandidate]) : "[]"; },
|
||||
applyOperation: async (_store: unknown, operation: { operation: string; run?: { runner: string; ranAt: string; summary: string }; intervention?: unknown }) => {
|
||||
if (operation.operation === "record_scheduled_run" && operation.run) ruminationState.scheduledRuntime.lastRuns.push(operation.run);
|
||||
if (operation.operation === "queue_candidate_intervention" && operation.intervention) {
|
||||
const index = ruminationState.candidateInterventions.interventions.findIndex((entry) => entry.interventionId === (operation.intervention as { interventionId: string }).interventionId);
|
||||
if (index >= 0) ruminationState.candidateInterventions.interventions[index] = operation.intervention as never;
|
||||
else ruminationState.candidateInterventions.interventions.push(operation.intervention as never);
|
||||
}
|
||||
return ruminationState;
|
||||
},
|
||||
};
|
||||
const ruminated = await runVoidRumination({ statePath: "void-self-state.cc", observedAt: physiologyObservedAt, pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }, ruminationDependencies as never);
|
||||
assert.equal(ruminated.status, "ok", "the rumination coordinator applies one bounded pressure pass");
|
||||
const repeatedRumination = await runVoidRumination({ statePath: "void-self-state.cc", observedAt: new Date("2026-07-16T04:00:00.000Z"), pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }, ruminationDependencies as never);
|
||||
assert.equal(repeatedRumination.status, "ok", "a meaning-bearing state change may authorize the next rumination pressure fingerprint");
|
||||
const settledRumination = await runVoidRumination({ statePath: "void-self-state.cc", observedAt: new Date("2026-07-16T05:00:00.000Z"), pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }, ruminationDependencies as never);
|
||||
assert.equal(settledRumination.status, "skipped", "an unchanged settled pressure cannot become a timer-driven inference loop");
|
||||
assert.equal(ruminationModelCalls, 2, "rumination cadence follows changed pressure rather than elapsed clock");
|
||||
const maintainedState = structuredClone(physiologyState);
|
||||
let memoryModelCalls = 0;
|
||||
const memoryMaintenanceDependencies = {
|
||||
|
|
@ -955,6 +1003,16 @@ const memoryModelOutput = await projectVoidMemoryOperations({ prompt: "Maintain
|
|||
assert.equal(memoryModelOutput, "[]", "the portable memory actuator returns only model text to the operation parser");
|
||||
assert.equal(memoryRequestBody?.model, "memory-model", "the memory actuator uses one explicit configured model and bounded completion budget");
|
||||
assert.equal(memoryRequestBody?.tools, undefined, "memory maintenance cannot acquire filesystem or side-effect tools through the model actuator");
|
||||
let ruminationRequestBody: Record<string, unknown> | undefined;
|
||||
const ruminationModelOutput = await projectVoidRuminationOperations({ prompt: "Ruminate from typed pressure.", config: { openAiApi: { baseUrl: "https://model.invalid/v1", apiKey: "secret", model: "rumination-model", timeoutMs: 1000, authHeader: "Authorization", maxCompletionTokens: 512 } } as never }, {
|
||||
fetch: async (_url, init) => {
|
||||
ruminationRequestBody = JSON.parse(String(init?.body));
|
||||
return new Response(JSON.stringify({ choices: [{ message: { content: "[]" } }] }), { status: 200 });
|
||||
},
|
||||
});
|
||||
assert.equal(ruminationModelOutput, "[]", "the rumination actuator returns only model text to the typed parser");
|
||||
assert.equal(ruminationRequestBody?.model, "rumination-model", "rumination uses one bounded configured text model");
|
||||
assert.equal(ruminationRequestBody?.tools, undefined, "person-shaped rumination cannot acquire transport or filesystem tools");
|
||||
let moderationRequestBody: Record<string, unknown> | undefined;
|
||||
const moderationModelOutput = await projectVoidModerationOperations({ prompt: "Review moderation evidence.", config: { openAiApi: { baseUrl: "https://model.invalid/v1", apiKey: "secret", model: "moderation-model", timeoutMs: 1000, authHeader: "Authorization", maxCompletionTokens: 512 } } as never }, {
|
||||
fetch: async (_url, init) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue