Extract Persona turn routing
This commit is contained in:
parent
a7ac9b51e9
commit
c2f1e58fce
7 changed files with 144 additions and 104 deletions
|
|
@ -73,22 +73,13 @@ import {
|
|||
type BifrostGovernanceDigest,
|
||||
} from "../apps/persona-scheduler/dist/bifrost-governance-source.js";
|
||||
import { submitPersonaTurn } from "../apps/persona-scheduler/dist/turn-actuator.js";
|
||||
|
||||
interface RepoFaceChannelPlan {
|
||||
primaryChannelId?: string;
|
||||
snapshotChannelIds: string[];
|
||||
options: RepoFaceChannelOption[];
|
||||
lowThresholdTopics: string[];
|
||||
}
|
||||
|
||||
interface RepoFaceChannelOption {
|
||||
channelId: string;
|
||||
label: string;
|
||||
topic: string;
|
||||
speechThreshold: "very_low" | "low" | "medium" | "high";
|
||||
speedMultiplier: number;
|
||||
posture?: string;
|
||||
}
|
||||
import {
|
||||
buildPersonaChannelPlan as buildChannelPlan,
|
||||
newestPendingMentionChannel,
|
||||
personaChannelSpeedMultiplier as channelSpeedMultiplierFor,
|
||||
type PersonaChannelOption as RepoFaceChannelOption,
|
||||
type PersonaChannelPlan as RepoFaceChannelPlan,
|
||||
} from "../apps/persona-scheduler/dist/turn-routing.js";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
|
|
@ -3930,76 +3921,6 @@ function buildInspectionParticipant(
|
|||
};
|
||||
}
|
||||
|
||||
function buildChannelPlan(
|
||||
identity: RepoDiscordIdentity,
|
||||
defaultChannelId?: string,
|
||||
preferredChannelId?: string,
|
||||
): RepoFaceChannelPlan {
|
||||
const explicit = identity.channelPermissions.map((permission): RepoFaceChannelOption => ({
|
||||
channelId: permission.channelId,
|
||||
label: permission.label ?? permission.channelId,
|
||||
topic: permission.topic ?? "general",
|
||||
speechThreshold: permission.speechThreshold,
|
||||
speedMultiplier: permission.speedMultiplier,
|
||||
posture: permission.posture,
|
||||
}));
|
||||
const explicitChannelIds = new Set(explicit.map((permission) => permission.channelId));
|
||||
const legacy = identity.allowedChannelIds
|
||||
.filter((channelId) => !explicitChannelIds.has(channelId))
|
||||
.map((channelId): RepoFaceChannelOption => ({
|
||||
channelId,
|
||||
label: channelId === defaultChannelId ? "default" : channelId,
|
||||
topic: channelId === defaultChannelId ? "casual Aquarium musing" : "registered channel",
|
||||
speechThreshold: channelId === defaultChannelId ? "very_low" : "medium",
|
||||
speedMultiplier: channelId === defaultChannelId ? 1.5 : 1,
|
||||
posture: channelId === defaultChannelId
|
||||
? "Low-stakes casual chatter, half-formed fascinations, jokes, little observations, and friendly asides are welcome here."
|
||||
: undefined,
|
||||
}));
|
||||
const fallback = explicit.length === 0 && legacy.length === 0 && defaultChannelId
|
||||
? [{
|
||||
channelId: defaultChannelId,
|
||||
label: "aquarium",
|
||||
topic: "casual Aquarium musing",
|
||||
speechThreshold: "very_low" as const,
|
||||
speedMultiplier: 1.5,
|
||||
posture: "Low-stakes casual chatter, half-formed fascinations, jokes, little observations, and friendly asides are welcome here.",
|
||||
}]
|
||||
: [];
|
||||
const options = [...explicit, ...legacy, ...fallback];
|
||||
const preferred = preferredChannelId
|
||||
? options.find((option) => option.channelId === preferredChannelId)
|
||||
: undefined;
|
||||
const defaultOption = defaultChannelId
|
||||
? options.find((option) => option.channelId === defaultChannelId)
|
||||
: undefined;
|
||||
const primary = preferred ?? defaultOption ?? options
|
||||
.slice()
|
||||
.sort((left, right) => thresholdRank(left.speechThreshold) - thresholdRank(right.speechThreshold))
|
||||
[0];
|
||||
|
||||
return {
|
||||
primaryChannelId: primary?.channelId,
|
||||
snapshotChannelIds: [
|
||||
...new Set([
|
||||
...options.map((option) => option.channelId),
|
||||
...(preferredChannelId ? [preferredChannelId] : []),
|
||||
]),
|
||||
],
|
||||
options,
|
||||
lowThresholdTopics: options
|
||||
.filter((option) => thresholdRank(option.speechThreshold) <= thresholdRank("low"))
|
||||
.map((option) => option.topic),
|
||||
};
|
||||
}
|
||||
|
||||
function newestPendingMentionChannel(pendingMentions: RepoFacePendingMention[]): string | undefined {
|
||||
return pendingMentions
|
||||
.slice()
|
||||
.sort((left, right) => Date.parse(right.queuedAt) - Date.parse(left.queuedAt))
|
||||
[0]?.channelId;
|
||||
}
|
||||
|
||||
function renderChannelPermissionDirective(plan: RepoFaceChannelPlan): string {
|
||||
const options = plan.options.length > 0
|
||||
? plan.options.map((option) =>
|
||||
|
|
@ -4391,19 +4312,6 @@ function normalizeForRepetition(value: string): string {
|
|||
.trim();
|
||||
}
|
||||
|
||||
function thresholdRank(threshold: RepoFaceChannelOption["speechThreshold"]): number {
|
||||
switch (threshold) {
|
||||
case "very_low":
|
||||
return 0;
|
||||
case "low":
|
||||
return 1;
|
||||
case "medium":
|
||||
return 2;
|
||||
case "high":
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
function collapseWhitespace(value: string, maxLength?: number): string {
|
||||
const normalized = value.replace(/\s+/g, " ").trim();
|
||||
return maxLength && normalized.length > maxLength
|
||||
|
|
@ -4568,11 +4476,6 @@ async function readSwarmControlState(): Promise<{ globalHeat: number } | null> {
|
|||
}
|
||||
}
|
||||
|
||||
function channelSpeedMultiplierFor(identity: RepoDiscordIdentity): number {
|
||||
const multipliers = identity.channelPermissions.map((permission) => permission.speedMultiplier);
|
||||
return multipliers.length > 0 ? clamp(Math.max(...multipliers), 0.5, 3) : 1;
|
||||
}
|
||||
|
||||
function mergeStrings(values: string[], value: string): string[] {
|
||||
return Array.from(new Set([...values, value]));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
readRepoFaceMentionInbox,
|
||||
type CreateJobInput,
|
||||
type JobQueue,
|
||||
type RepoDiscordIdentity,
|
||||
type RepoFacePendingMention,
|
||||
} from "@voidbot/core";
|
||||
import type { JobRecord } from "@voidbot/shared";
|
||||
|
||||
|
|
@ -47,6 +49,11 @@ import {
|
|||
} from "../apps/persona-scheduler/dist/turn-context-source.js";
|
||||
import { readBifrostGovernanceDigest } from "../apps/persona-scheduler/dist/bifrost-governance-source.js";
|
||||
import { submitPersonaTurn } from "../apps/persona-scheduler/dist/turn-actuator.js";
|
||||
import {
|
||||
buildPersonaChannelPlan,
|
||||
newestPendingMentionChannel,
|
||||
personaChannelSpeedMultiplier,
|
||||
} from "../apps/persona-scheduler/dist/turn-routing.js";
|
||||
|
||||
function participant(identityId: string, nextTurnAt: number): InitiativeParticipant {
|
||||
return {
|
||||
|
|
@ -375,6 +382,35 @@ assert.equal(submittedJobs[0].initialState, "approved", "Persona turns enter the
|
|||
assert.equal(submittedJobs[0].contextBundle.recentMessages.length, 2, "the actuator lowers supplied evidence into the worker context bundle");
|
||||
assert.equal(submittedJobs[0].requester.id, "voidbot-agent-turn", "the actuator owns the worker-facing actor identity");
|
||||
|
||||
const routedIdentity = {
|
||||
id: "nibu",
|
||||
repoName: "AetheriaLore",
|
||||
displayName: "Nibu",
|
||||
allowedChannelIds: ["legacy"],
|
||||
channelPermissions: [{
|
||||
channelId: "aquarium",
|
||||
label: "Aquarium",
|
||||
topic: "casual conversation",
|
||||
speechThreshold: "very_low",
|
||||
speedMultiplier: 4,
|
||||
}, {
|
||||
channelId: "lore",
|
||||
label: "Lore",
|
||||
topic: "setting work",
|
||||
speechThreshold: "medium",
|
||||
speedMultiplier: 1,
|
||||
}],
|
||||
} as RepoDiscordIdentity;
|
||||
const channelPlan = buildPersonaChannelPlan(routedIdentity, "aquarium", "lore");
|
||||
assert.equal(channelPlan.primaryChannelId, "lore", "fresh direct attention chooses its permitted source room");
|
||||
assert.deepEqual(channelPlan.snapshotChannelIds, ["aquarium", "lore", "legacy"], "routing exposes one deduplicated evidence neighborhood");
|
||||
assert.deepEqual(channelPlan.lowThresholdTopics, ["casual conversation"], "prompt renderers consume routing policy instead of recomputing thresholds");
|
||||
assert.equal(personaChannelSpeedMultiplier(routedIdentity), 3, "scheduler speed projection uses the routing organ's bounded channel policy");
|
||||
assert.equal(newestPendingMentionChannel([
|
||||
{ identityId: "nibu", channelId: "older", queuedAt: "2026-07-15T20:00:00.000Z" },
|
||||
{ identityId: "nibu", channelId: "newer", queuedAt: "2026-07-15T21:00:00.000Z" },
|
||||
] as RepoFacePendingMention[]), "newer", "routing follows the newest pending room obligation");
|
||||
|
||||
const stateDirectory = await mkdtemp(join(tmpdir(), "voidbot-persona-scheduler-"));
|
||||
try {
|
||||
const statePath = join(stateDirectory, "scheduler.json");
|
||||
|
|
|
|||
Loading…
Reference in a new issue