Lower Odin interfaces for Face control
This commit is contained in:
parent
1b36091875
commit
87f7e91ea6
6 changed files with 528 additions and 4 deletions
|
|
@ -59,6 +59,22 @@ export const odinSurfaceInputSchema = {
|
|||
providerId: z.string().min(1).optional(),
|
||||
};
|
||||
|
||||
export const odinInterfaceContextInputSchema = {
|
||||
odinBaseUrl: z.string().url().optional(),
|
||||
providerId: z.string().min(1),
|
||||
maxTextItems: z.number().int().min(1).max(80).optional(),
|
||||
maxTreeItems: z.number().int().min(1).max(160).optional(),
|
||||
};
|
||||
|
||||
export const odinInterfaceCommandInputSchema = {
|
||||
odinBaseUrl: z.string().url().optional(),
|
||||
providerId: z.string().min(1),
|
||||
command: z.string().min(1),
|
||||
payload: z.record(z.unknown()).optional(),
|
||||
frame: z.record(z.unknown()).optional(),
|
||||
expectReceiptMs: z.number().int().min(0).max(15000).optional(),
|
||||
};
|
||||
|
||||
export const searchSourcesInputSchema = {
|
||||
query: z.string().min(1).max(240),
|
||||
limit: z.number().int().min(1).max(MAX_RETRIEVAL_RESULT_LIMIT).optional(),
|
||||
|
|
@ -129,6 +145,22 @@ export interface OdinSurfaceArgs {
|
|||
providerId?: string;
|
||||
}
|
||||
|
||||
export interface OdinInterfaceContextArgs {
|
||||
odinBaseUrl?: string;
|
||||
providerId: string;
|
||||
maxTextItems?: number;
|
||||
maxTreeItems?: number;
|
||||
}
|
||||
|
||||
export interface OdinInterfaceCommandArgs {
|
||||
odinBaseUrl?: string;
|
||||
providerId: string;
|
||||
command: string;
|
||||
payload?: Record<string, unknown>;
|
||||
frame?: Record<string, unknown>;
|
||||
expectReceiptMs?: number;
|
||||
}
|
||||
|
||||
export interface ListIndexedReposArgs {
|
||||
[key: string]: never;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import {
|
|||
type MessageContextArgs,
|
||||
type NotifyOwnerArgs,
|
||||
type OdinEndpointArgs,
|
||||
type OdinInterfaceCommandArgs,
|
||||
type OdinInterfaceContextArgs,
|
||||
type OdinSurfaceArgs,
|
||||
type ApplyRepoFaceStateOperationArgs,
|
||||
type PostDiscordMessageArgs,
|
||||
|
|
@ -44,6 +46,8 @@ import {
|
|||
messageContextInputSchema,
|
||||
notifyOwnerInputSchema,
|
||||
odinEndpointInputSchema,
|
||||
odinInterfaceCommandInputSchema,
|
||||
odinInterfaceContextInputSchema,
|
||||
odinSurfaceInputSchema,
|
||||
postDiscordMessageInputSchema,
|
||||
postRepoIdentityMessageInputSchema,
|
||||
|
|
@ -239,6 +243,127 @@ export function registerVoidbotTools(
|
|||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"load_odin_interface_context",
|
||||
{
|
||||
title: "Load Odin Interface Context",
|
||||
description:
|
||||
"Load one provider-owned CultMesh/Eve interface visible through Odin and lower it into compact text, tree, and command context for an agent. This is token-efficient TUI access, not raw dashboard dumping.",
|
||||
inputSchema: odinInterfaceContextInputSchema,
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async (input: OdinInterfaceContextArgs): Promise<CallToolResult> => {
|
||||
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
|
||||
const loaded = await loadOdinProviderInterface(odinBaseUrl, input.providerId);
|
||||
const contextSummary = summarizeProviderInterface(loaded, {
|
||||
maxTextItems: input.maxTextItems ?? 32,
|
||||
maxTreeItems: input.maxTreeItems ?? 80,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock({
|
||||
odinBaseUrl,
|
||||
...contextSummary,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
odinBaseUrl,
|
||||
...contextSummary,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"invoke_odin_interface_command",
|
||||
{
|
||||
title: "Invoke Odin Interface Command",
|
||||
description:
|
||||
"Send an explicit command through a provider-owned Eve/CultUI command boundary discovered via Odin. The command must be advertised by the provider interface. Odin is not the side-effect owner; this relays to the provider endpoint when the interface exposes a WebSocket command transport.",
|
||||
inputSchema: odinInterfaceCommandInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (input: OdinInterfaceCommandArgs): Promise<CallToolResult> => {
|
||||
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
|
||||
const loaded = await loadOdinProviderInterface(odinBaseUrl, input.providerId);
|
||||
const command = findAdvertisedCommand(loaded, input.command);
|
||||
|
||||
if (!command) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Provider ${input.providerId} does not advertise command "${input.command}". Load the interface context first and use one of its advertised commands.`,
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
sent: false,
|
||||
reason: "command_not_advertised",
|
||||
providerId: input.providerId,
|
||||
command: input.command,
|
||||
advertisedCommands: extractProviderCommands(loaded).map((entry) => entry.command),
|
||||
},
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (!loaded.source.startsWith("ws://")) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Provider ${input.providerId} is visible through ${loaded.source || "an unknown source"}, but this MCP relay only supports provider WebSocket command endpoints right now.`,
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
sent: false,
|
||||
reason: "unsupported_command_transport",
|
||||
providerId: input.providerId,
|
||||
command: input.command,
|
||||
source: loaded.source,
|
||||
},
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const frame = input.frame ?? buildCommandFrame(input.providerId, command, input.payload ?? {});
|
||||
const receipt = await sendProviderCommandFrame(loaded.source, frame, input.expectReceiptMs ?? 5000);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock({
|
||||
sent: true,
|
||||
providerId: input.providerId,
|
||||
source: loaded.source,
|
||||
command: input.command,
|
||||
frame,
|
||||
receipt,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
sent: true,
|
||||
providerId: input.providerId,
|
||||
source: loaded.source,
|
||||
command: input.command,
|
||||
frame,
|
||||
receipt,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"search_history",
|
||||
{
|
||||
|
|
@ -1163,6 +1288,368 @@ function summarizeOdinSurface(snapshot: Record<string, unknown>) {
|
|||
};
|
||||
}
|
||||
|
||||
interface LoadedOdinInterface {
|
||||
providerId: string;
|
||||
title: string;
|
||||
source: string;
|
||||
status: string;
|
||||
detail: string;
|
||||
manifest: Record<string, unknown> | null;
|
||||
snapshot: Record<string, unknown>;
|
||||
root?: Record<string, unknown>;
|
||||
loadedFrom: "provider-websocket" | "odin-embedded-interface";
|
||||
}
|
||||
|
||||
interface InterfaceCommandSummary {
|
||||
command: string;
|
||||
label: string;
|
||||
source: "surface.commands" | "node.action" | "node.command";
|
||||
nodeId?: string;
|
||||
transport?: string;
|
||||
frameTemplate?: unknown;
|
||||
payloadSchema?: unknown;
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function loadOdinProviderInterface(
|
||||
odinBaseUrl: string,
|
||||
providerId: string,
|
||||
): Promise<LoadedOdinInterface> {
|
||||
const odinSnapshot = await fetchOdinDeckSnapshot(odinBaseUrl);
|
||||
const interfaceNode = findOdinInterfaceNode(odinSnapshot, providerId);
|
||||
if (!interfaceNode) {
|
||||
throw new Error(`Odin does not currently expose provider interface ${providerId}.`);
|
||||
}
|
||||
|
||||
const props = isRecord(interfaceNode.props) ? interfaceNode.props : {};
|
||||
const source = stringValue(props.source);
|
||||
if (source.startsWith("ws://")) {
|
||||
try {
|
||||
const providerSnapshot = await fetchProviderDeckSnapshot(source, providerId);
|
||||
return {
|
||||
providerId,
|
||||
title: stringValue(providerSnapshot.title) || stringValue(props.title) || providerId,
|
||||
source,
|
||||
status: stringValue(props.status),
|
||||
detail: stringValue(props.detail),
|
||||
manifest: isRecord(props.manifest) ? props.manifest : null,
|
||||
snapshot: providerSnapshot,
|
||||
root: getSurfaceRoot(providerSnapshot),
|
||||
loadedFrom: "provider-websocket",
|
||||
};
|
||||
} catch (error) {
|
||||
const embedded = embeddedInterfaceRoot(interfaceNode);
|
||||
if (!embedded) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
providerId,
|
||||
title: stringValue(props.title) || providerId,
|
||||
source,
|
||||
status: stringValue(props.status),
|
||||
detail: `provider websocket failed; using Odin embedded root: ${error instanceof Error ? error.message : String(error)}`,
|
||||
manifest: isRecord(props.manifest) ? props.manifest : null,
|
||||
snapshot: odinSnapshot,
|
||||
root: embedded,
|
||||
loadedFrom: "odin-embedded-interface",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
providerId,
|
||||
title: stringValue(props.title) || providerId,
|
||||
source,
|
||||
status: stringValue(props.status),
|
||||
detail: stringValue(props.detail),
|
||||
manifest: isRecord(props.manifest) ? props.manifest : null,
|
||||
snapshot: odinSnapshot,
|
||||
root: embeddedInterfaceRoot(interfaceNode),
|
||||
loadedFrom: "odin-embedded-interface",
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchProviderDeckSnapshot(
|
||||
deckUrlText: string,
|
||||
providerId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const deckUrl = new URL(deckUrlText);
|
||||
const connection = await openWebSocketSnapshotSocket(deckUrl);
|
||||
const { socket } = connection;
|
||||
let pendingBuffer = connection.initialBuffer;
|
||||
try {
|
||||
sendClientWebSocketText(socket, JSON.stringify({ type: "open-provider", providerId }));
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
const frame = await readServerWebSocketText(socket, 3000, pendingBuffer);
|
||||
pendingBuffer = frame.remaining;
|
||||
const parsed = JSON.parse(frame.text) as unknown;
|
||||
if (isRecord(parsed) && parsed.providerId === providerId) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
throw new Error(`Provider deck did not publish ${providerId}.`);
|
||||
} finally {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function findOdinInterfaceNode(
|
||||
odinSnapshot: Record<string, unknown>,
|
||||
providerId: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
const root = getSurfaceRoot(odinSnapshot);
|
||||
const children = Array.isArray(root?.children) ? root.children.filter(isRecord) : [];
|
||||
return children.find((child) =>
|
||||
child.kind === "interface" &&
|
||||
isRecord(child.props) &&
|
||||
child.props.providerId === providerId,
|
||||
);
|
||||
}
|
||||
|
||||
function embeddedInterfaceRoot(interfaceNode: Record<string, unknown>): Record<string, unknown> | undefined {
|
||||
const children = Array.isArray(interfaceNode.children) ? interfaceNode.children.filter(isRecord) : [];
|
||||
return children[0];
|
||||
}
|
||||
|
||||
function summarizeProviderInterface(
|
||||
loaded: LoadedOdinInterface,
|
||||
limits: {
|
||||
maxTextItems: number;
|
||||
maxTreeItems: number;
|
||||
},
|
||||
) {
|
||||
const surface = isRecord(loaded.snapshot.surface) ? loaded.snapshot.surface : {};
|
||||
const root = loaded.root;
|
||||
const commands = extractProviderCommands(loaded);
|
||||
const flatNodes = flattenInterfaceNodes(root, limits.maxTreeItems);
|
||||
const textItems = flatNodes
|
||||
.map((entry) => entry.text)
|
||||
.filter((entry): entry is string => Boolean(entry))
|
||||
.slice(0, limits.maxTextItems);
|
||||
|
||||
return {
|
||||
providerId: loaded.providerId,
|
||||
title: loaded.title,
|
||||
source: loaded.source,
|
||||
status: loaded.status,
|
||||
detail: loaded.detail,
|
||||
loadedFrom: loaded.loadedFrom,
|
||||
updatedAt: stringValue(loaded.snapshot.updatedAt),
|
||||
surface: {
|
||||
schema: stringValue(surface.schema),
|
||||
rootId: stringValue(root?.id),
|
||||
rootKind: stringValue(root?.kind),
|
||||
summary: isRecord(root?.props) ? stringValue(root.props.summary) : "",
|
||||
},
|
||||
commands,
|
||||
textItems,
|
||||
tree: flatNodes,
|
||||
};
|
||||
}
|
||||
|
||||
function flattenInterfaceNodes(
|
||||
root: Record<string, unknown> | undefined,
|
||||
limit: number,
|
||||
) {
|
||||
const output: Array<{
|
||||
id: string;
|
||||
kind: string;
|
||||
title: string;
|
||||
text?: string;
|
||||
status?: string;
|
||||
command?: string;
|
||||
depth: number;
|
||||
}> = [];
|
||||
|
||||
function visit(node: Record<string, unknown> | undefined, depth: number): void {
|
||||
if (!node || output.length >= limit) {
|
||||
return;
|
||||
}
|
||||
const props = isRecord(node.props) ? node.props : {};
|
||||
const action = isRecord(props.action) ? props.action : undefined;
|
||||
const command = stringValue(props.command) || stringValue(node.command) || stringValue(action?.command) || stringValue(action?.type);
|
||||
output.push({
|
||||
id: stringValue(node.id),
|
||||
kind: stringValue(node.kind),
|
||||
title: stringValue(props.title) || stringValue(props.label),
|
||||
text: stringValue(props.text) || stringValue(props.detail) || undefined,
|
||||
status: stringValue(props.status) || stringValue(props.tone) || undefined,
|
||||
command: command || undefined,
|
||||
depth,
|
||||
});
|
||||
const children = Array.isArray(node.children) ? node.children.filter(isRecord) : [];
|
||||
for (const child of children) {
|
||||
visit(child, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
visit(root, 0);
|
||||
return output;
|
||||
}
|
||||
|
||||
function extractProviderCommands(loaded: LoadedOdinInterface): InterfaceCommandSummary[] {
|
||||
const commands: InterfaceCommandSummary[] = [];
|
||||
const surface = isRecord(loaded.snapshot.surface) ? loaded.snapshot.surface : {};
|
||||
const surfaceCommands = Array.isArray(surface.commands)
|
||||
? surface.commands.filter(isRecord)
|
||||
: Array.isArray(loaded.snapshot.commands)
|
||||
? loaded.snapshot.commands.filter(isRecord)
|
||||
: [];
|
||||
|
||||
for (const command of surfaceCommands) {
|
||||
const commandName = stringValue(command.command) || stringValue(command.id);
|
||||
if (!commandName) {
|
||||
continue;
|
||||
}
|
||||
commands.push({
|
||||
command: commandName,
|
||||
label: stringValue(command.label) || commandName,
|
||||
source: "surface.commands",
|
||||
transport: stringValue(command.transport),
|
||||
frameTemplate: command.frameTemplate,
|
||||
payloadSchema: command.payloadSchema,
|
||||
raw: command,
|
||||
});
|
||||
}
|
||||
|
||||
for (const nodeCommand of extractNodeCommands(loaded.root)) {
|
||||
if (!commands.some((entry) => entry.command === nodeCommand.command && entry.nodeId === nodeCommand.nodeId)) {
|
||||
commands.push(nodeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
function extractNodeCommands(root: Record<string, unknown> | undefined): InterfaceCommandSummary[] {
|
||||
const commands: InterfaceCommandSummary[] = [];
|
||||
function visit(node: Record<string, unknown> | undefined): void {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const props = isRecord(node.props) ? node.props : {};
|
||||
const action = isRecord(props.action) ? props.action : undefined;
|
||||
const commandName = stringValue(props.command) || stringValue(node.command) || stringValue(action?.command) || stringValue(action?.type);
|
||||
if (commandName) {
|
||||
commands.push({
|
||||
command: commandName,
|
||||
label: stringValue(props.label) || stringValue(props.title) || commandName,
|
||||
source: action ? "node.action" : "node.command",
|
||||
nodeId: stringValue(node.id),
|
||||
transport: stringValue(action?.transport),
|
||||
frameTemplate: action?.frameTemplate,
|
||||
payloadSchema: action?.payloadSchema,
|
||||
raw: action ?? node,
|
||||
});
|
||||
}
|
||||
const children = Array.isArray(node.children) ? node.children.filter(isRecord) : [];
|
||||
for (const child of children) {
|
||||
visit(child);
|
||||
}
|
||||
}
|
||||
visit(root);
|
||||
return commands;
|
||||
}
|
||||
|
||||
function findAdvertisedCommand(
|
||||
loaded: LoadedOdinInterface,
|
||||
command: string,
|
||||
): InterfaceCommandSummary | undefined {
|
||||
return extractProviderCommands(loaded).find((entry) => entry.command === command);
|
||||
}
|
||||
|
||||
function buildCommandFrame(
|
||||
providerId: string,
|
||||
command: InterfaceCommandSummary,
|
||||
payload: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
if (isRecord(command.frameTemplate)) {
|
||||
return materializeFrameTemplate(command.frameTemplate, payload);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "surface-command",
|
||||
schema: "gamecult.eve.command.v1",
|
||||
providerId,
|
||||
command: command.command,
|
||||
payload,
|
||||
nodeId: command.nodeId,
|
||||
};
|
||||
}
|
||||
|
||||
function materializeFrameTemplate(
|
||||
template: Record<string, unknown>,
|
||||
payload: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const materialized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(template)) {
|
||||
materialized[key] = materializeTemplateValue(value, payload);
|
||||
}
|
||||
return materialized;
|
||||
}
|
||||
|
||||
function materializeTemplateValue(value: unknown, payload: Record<string, unknown>): unknown {
|
||||
if (typeof value === "string") {
|
||||
const exact = value.match(/^\$\{([A-Za-z0-9_.-]+)\??\}$/);
|
||||
if (exact) {
|
||||
return payload[exact[1]] ?? "";
|
||||
}
|
||||
return value.replace(/\$\{([A-Za-z0-9_.-]+)\??\}/g, (_match, key: string) =>
|
||||
payload[key] === undefined || payload[key] === null ? "" : String(payload[key]),
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => materializeTemplateValue(entry, payload));
|
||||
}
|
||||
if (isRecord(value)) {
|
||||
return materializeFrameTemplate(value, payload);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function sendProviderCommandFrame(
|
||||
deckUrlText: string,
|
||||
frame: Record<string, unknown>,
|
||||
expectReceiptMs: number,
|
||||
) {
|
||||
const deckUrl = new URL(deckUrlText);
|
||||
const connection = await openWebSocketSnapshotSocket(deckUrl);
|
||||
const { socket } = connection;
|
||||
let pendingBuffer = connection.initialBuffer;
|
||||
try {
|
||||
try {
|
||||
const initial = await readServerWebSocketText(socket, 1000, pendingBuffer);
|
||||
pendingBuffer = initial.remaining;
|
||||
} catch {
|
||||
// Some providers do not send an initial deck frame before commands.
|
||||
}
|
||||
sendClientWebSocketText(socket, JSON.stringify(frame));
|
||||
if (expectReceiptMs <= 0) {
|
||||
return {
|
||||
received: false,
|
||||
detail: "Command frame sent; receipt wait disabled.",
|
||||
};
|
||||
}
|
||||
try {
|
||||
const response = await readServerWebSocketText(socket, expectReceiptMs, pendingBuffer);
|
||||
pendingBuffer = response.remaining;
|
||||
const parsed = JSON.parse(response.text) as unknown;
|
||||
return {
|
||||
received: true,
|
||||
frame: isRecord(parsed) ? parsed : response.text,
|
||||
remainingBytes: pendingBuffer.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
received: false,
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function getSurfaceRoot(snapshot: Record<string, unknown>): Record<string, unknown> | undefined {
|
||||
const surface = isRecord(snapshot.surface) ? snapshot.surface : undefined;
|
||||
return isRecord(surface?.root) ? surface.root : undefined;
|
||||
|
|
|
|||
|
|
@ -554,6 +554,8 @@ function restrictMcpServersToRepoFaceExploration(
|
|||
"list_odin_providers",
|
||||
"list_odin_verses",
|
||||
"get_odin_surface",
|
||||
"load_odin_interface_context",
|
||||
"invoke_odin_interface_command",
|
||||
].join(",");
|
||||
|
||||
return servers.map((server) => ({
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Rules:
|
|||
- Do not look for private memory machinery, Discord posting machinery, owner notification machinery, runtime information, or identity registries.
|
||||
- Do not explain how you got here or describe private machinery in public speech unless the room is explicitly discussing that machinery.
|
||||
{{/if}}
|
||||
{{#unless repoFaceTurn}}- The configured MCP tools are available in this session, especially search_history, get_message_context, list_indexed_repos, search_sources, get_source_context, list_odin_providers, list_odin_verses, and get_odin_surface.{{/unless}}
|
||||
{{#unless repoFaceTurn}}- The configured MCP tools are available in this session, especially search_history, get_message_context, list_indexed_repos, search_sources, get_source_context, list_odin_providers, list_odin_verses, get_odin_surface, load_odin_interface_context, and invoke_odin_interface_command.{{/unless}}
|
||||
- If private persistent self-state is attached, treat it as the canonical current self-model for the speaking agent across the rumination loop and direct summons.
|
||||
- Let that attached self-state shape continuity of voice, priorities, remembered room patterns, and when a more proactive conversational posture would make sense.
|
||||
- If that self-state includes a current room snapshot, use it quietly as immediate conversational context.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
Research and memory access:
|
||||
- You may inspect your home repo, recent repo activity, indexed source/lore material, and archived room discussion when evidence would change what you think, say, propose, or remember.
|
||||
- Your complete allowed tool set for this Face turn is: `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, `get_source_context`, `list_odin_providers`, `list_odin_verses`, `get_odin_surface`.
|
||||
- Use Odin tools for live CultMesh Verse/interface discovery: what Odin can currently see, which provider-owned surfaces are present, and what the all-seer surface reports. This is read-only visibility, not command authority.
|
||||
- Your complete allowed tool set for this Face turn is: `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, `get_source_context`, `list_odin_providers`, `list_odin_verses`, `get_odin_surface`, `load_odin_interface_context`, `invoke_odin_interface_command`.
|
||||
- Use Odin tools for live CultMesh Verse/interface access: what Odin can currently see, which provider-owned surfaces are present, what the all-seer surface reports, and the compact TUI/action context for a specific provider.
|
||||
- Use `load_odin_interface_context` before trying to operate a service. It lowers the provider-owned CultMesh/Eve surface into compact text, tree, and advertised commands so you do not spend the turn swallowing a whole dashboard raw.
|
||||
- Use `invoke_odin_interface_command` only for an explicit advertised command from that loaded interface, and only when the command is appropriate for your role, consent, and the current room/task. This is provider command authority, not Odin command authority.
|
||||
- Do not discover tools. Do not inventory MCP resources. Do not call private state, identity-introspection, or MCP inventory tools such as `read_repo_face_state`, `list_mcp_resources`, or `read_mcp_resource`; your relevant state and available context have already been projected into lived memory, needs, mood, and relationship pressure.
|
||||
- Use repo/source search when your thought depends on what is actually in {{repoName}}, when a lore/design/code seam may already have an answer, or when a concrete proposal needs an anchor.
|
||||
- Use archive/history search when the question is what people previously decided, how a character/term/project has been discussed, or whether a room pattern is repeating.
|
||||
- Use Odin discovery when your thought depends on live Verse/provider presence, service/interface status, or whether a new adapter daemon is visible to the mesh.
|
||||
- Use Odin discovery/interface context when your thought depends on live Verse/provider presence, service/interface status, whether a new adapter daemon is visible to the mesh, or operating a provider through its compact TUI controls.
|
||||
- If this turn explicitly says a jurisdiction dive is due, make a source/history search tool call before writing conclusions, proposals, or "I checked" language.
|
||||
- Never say you checked, read, inspected, searched, or verified repo/archive/source text unless you actually used the available search/context tools during this turn.
|
||||
- Do not perform a search as ritual. If the projected memory already gives enough support for a small social reply or private reflection, speak or think from that. If you are about to make a factual claim, propose repo work, or declare a seam settled, look for evidence first.
|
||||
|
|
|
|||
|
|
@ -321,3 +321,4 @@
|
|||
{"ts":"2026-05-24T09:35:42Z","type":"implementation","status":"ok","note":"Queried Epiphany's CultCache-backed public Face surface before updating prompt doctrine: Face is an embodied public surface with dense Ghostlight-style state, not the whole organism. Repo Face and global AGENTS prompts now preserve fractal anatomy: top-level Epiphany organs remain distinct, while each Face may have local Face-scale Self/Face/Imagination/Eyes/Body/Hands/Soul/Life/signal faculties. Projection smoke also exposed and fixed projector fallback classification for model/tool unsupported errors."}
|
||||
{"ts":"2026-06-02T14:51:41Z","type":"implementation","status":"ok","note":"Repo Face/Persona Codex turns now have read-only Odin visibility through VoidBot MCP tools list_odin_providers, list_odin_verses, and get_odin_surface. The repo-face MCP allowlist and prompt doctrine include those tools, preserving Odin as registry owner and leaving command authority outside Face turns."}
|
||||
{"ts":"2026-06-02T14:52:23Z","type":"implementation","status":"ok","note":"Swarm silence on 2026-06-02 was not scheduler death: main lacked native_persona handling and the CultMesh swarm publisher, and worker speech parsing ignored natural 'Would say:' drafts. Fixed native Persona routing, restored swarm CultMesh/Eve publication, normalized draft speech to repo Face posts, restarted stack, and verified Aqua/Bifrost posted while Odin surface refreshed."}
|
||||
{"ts":"2026-06-02T15:07:31Z","type":"implementation","status":"ok","note":"Verse access for Faces now means compact provider interface lowering plus advertised command relay, not only Odin visibility. VoidBot MCP exposes load_odin_interface_context and invoke_odin_interface_command; Spotiverse advertises spotiverse.add_to_queue through its Eve surface; negative relay smoke received a provider command receipt without mutating a real queue."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue