Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions examples/openclaw-plugin/auto-recall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
resolveRecallSearchPlan,
type RecallResourceType,
} from "./registries/recall-resource-types.js";
import { isPreviewableImageResourceUri, withPreviewUrls } from "./preview-url.js";
import {
type RecallTraceEntry,
type RecallTraceResult,
Expand Down Expand Up @@ -99,6 +100,12 @@ export type BuildMemoryLinesOptions = {
};

function memoryCategory(item: FindResultItem): string {
if (item.preview_url && isPreviewableImageResourceUri(item.uri)) {
return item.category?.trim() || "resource:image";
}
if (item.uri.startsWith("viking://resources/")) {
return item.category?.trim() || "resource";
}
return item.category?.trim() || "memory";
}

Expand All @@ -116,14 +123,18 @@ function formatMemoryLine(
): string {
const category = memoryCategory(item);
if (!options.includeUri) {
return `- [${category}] ${content}`;
return [
`- [${category}] ${content}`,
item.preview_url ? ` <preview_url>${item.preview_url}</preview_url>` : "",
].filter(Boolean).join("\n");
}

return [
`- [${category}]`,
` <uri>${item.uri}</uri>`,
item.preview_url ? ` <preview_url>${item.preview_url}</preview_url>` : "",
indentContent(content),
].join("\n");
].filter(Boolean).join("\n");
}

async function resolveMemoryContent(
Expand Down Expand Up @@ -212,13 +223,17 @@ export async function buildMemoryLinesWithBudget(
}

export function buildRecallContextBlock(memoryLines: string[]): string {
const previewInstruction = memoryLines.some((line) => line.includes("<preview_url>"))
? "If you reference an image result, copy its exact <preview_url> into the answer; do not invent or rewrite preview URLs."
: "";
return [
"<relevant-memories>",
AUTO_RECALL_SOURCE_MARKER,
"The following OpenViking memories may be relevant:",
previewInstruction,
...memoryLines,
"</relevant-memories>",
].join("\n");
].filter(Boolean).join("\n");
}

function newTraceId(): string {
Expand Down Expand Up @@ -482,8 +497,11 @@ export async function buildAutoRecallContext(params: {
return { memoryCount: 0, estimatedTokens: 0 };
}

const memoriesWithPreview = cfg.enableResourcePreviewUrls
? await withPreviewUrls(memories, client, agentId)
: memories;
const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(
memories,
memoriesWithPreview,
(uri) => client.read(uri, agentId),
{
recallPreferAbstract,
Expand All @@ -505,10 +523,10 @@ export async function buildAutoRecallContext(params: {
`openviking: injecting ${memoryLines.length} memories (${block.length} chars, ~${estimatedTokens} tokens, maxInjectedChars=${maxInjectedChars})`,
);
verbose?.(
`openviking: inject-detail ${toJsonLog({ count: memories.length, memories: summarizeInjectionMemories(memories) })}`,
`openviking: inject-detail ${toJsonLog({ count: memoriesWithPreview.length, memories: summarizeInjectionMemories(memoriesWithPreview) })}`,
);

await recordTrace(memories.slice(0, memoryLines.length), memoryLines.length, estimatedTokens);
await recordTrace(memoriesWithPreview.slice(0, memoryLines.length), memoryLines.length, estimatedTokens);
return { block, memoryCount: memoryLines.length, estimatedTokens };
})(),
cfg.autoRecallTimeoutMs,
Expand Down
15 changes: 15 additions & 0 deletions examples/openclaw-plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type FindResultItem = {
category?: string;
score?: number;
match_reason?: string;
preview_url?: string;
};

export type FindResult = {
Expand All @@ -30,6 +31,10 @@ export type FindResult = {
total?: number;
};

export type PreviewUrlResult = {
preview_url: string;
};

export type FsListEntry = string | Record<string, unknown>;

export type FsListResult = FsListEntry[];
Expand Down Expand Up @@ -485,6 +490,16 @@ export class OpenVikingClient {
);
}

async getPreviewUrl(uri: string, actorPeerId?: string): Promise<string> {
const result = await this.request<PreviewUrlResult>(
`/api/v1/content/preview_url?uri=${encodeURIComponent(uri)}`,
{},
undefined,
actorPeerId,
);
return result.preview_url;
}

async list(
uri: string,
options?: {
Expand Down
4 changes: 4 additions & 0 deletions examples/openclaw-plugin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type MemoryOpenVikingConfig = {
autoRecall?: boolean;
/** Outer time budget for the whole auto-recall flow, including search, ranking, and reads. */
autoRecallTimeoutMs?: number;
/** Hidden switch for temporary preview URLs on recalled image resources. Default false. */
enableResourcePreviewUrls?: boolean;
/** Include resources in auto-recall and default memory_recall search. Default false. */
recallResources?: boolean;
recallLimit?: number;
Expand Down Expand Up @@ -394,6 +396,7 @@ export const memoryOpenVikingConfigSchema = {
"captureMaxLength",
"autoRecall",
"autoRecallTimeoutMs",
"enableResourcePreviewUrls",
"recallResources",
"recallLimit",
"recallScoreThreshold",
Expand Down Expand Up @@ -503,6 +506,7 @@ export const memoryOpenVikingConfigSchema = {
1000,
Math.min(300_000, Math.floor(toNumber(cfg.autoRecallTimeoutMs, DEFAULT_AUTO_RECALL_TIMEOUT_MS))),
),
enableResourcePreviewUrls: cfg.enableResourcePreviewUrls === true,
recallResources,
recallLimit: Math.max(1, Math.floor(toNumber(cfg.recallLimit, DEFAULT_RECALL_LIMIT))),
recallScoreThreshold: Math.min(
Expand Down
1 change: 1 addition & 0 deletions examples/openclaw-plugin/install-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"context-engine.ts",
"auto-recall.ts",
"client.ts",
"preview-url.ts",
"process-manager.ts",
"memory-ranking.ts",
"token-estimator.ts",
Expand Down
13 changes: 11 additions & 2 deletions examples/openclaw-plugin/plugin/openviking-memory-recall-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { BuildMemoryLinesWithBudgetOptions } from "../auto-recall.js";
import type { RankingOptions } from "../memory-ranking.js";
import type { EffectiveQueryConfig, QueryConfigContext, RuntimeQueryParams } from "../query-config.js";
import type { RecallResourceType } from "../registries/recall-resource-types.js";
import { withPreviewUrls } from "../preview-url.js";
import type {
RecallTraceEntry,
RecallTraceResult,
Expand Down Expand Up @@ -36,6 +37,7 @@ export type OpenVikingMemoryRecallClient = {
},
) => Promise<FindResult>;
read: (uri: string, agentId?: string) => Promise<string>;
getPreviewUrl?: (uri: string, actorPeerId?: string) => Promise<string>;
getDefaultAgentId: () => string;
};

Expand Down Expand Up @@ -89,6 +91,7 @@ export type OpenVikingMemoryRecallToolsDeps = {
traceRecallPreviewChars: number;
traceRecallQueryMaxChars: number;
logFindRequests: boolean;
enableResourcePreviewUrls?: boolean;
};
logger: {
info?: (message: string) => void;
Expand Down Expand Up @@ -254,16 +257,22 @@ export function registerOpenVikingMemoryRecallTools(
};
}

const leafOnly = (result.memories ?? []).filter((m) => !m.level || m.level === 2);
const leafOnly = [
...(result.memories ?? []),
...(result.resources ?? []),
].filter((m) => !m.level || m.level === 2);
const processed = deps.postProcessMemories(leafOnly, {
limit: requestLimit,
scoreThreshold,
});
const memories = deps.pickMemoriesForInjection(processed, limit, query, scoreThreshold, {
const pickedMemories = deps.pickMemoriesForInjection(processed, limit, query, scoreThreshold, {
weights: queryConfig.rankingWeights,
categoryWeights: queryConfig.categoryWeights,
resourceTypeWeights: queryConfig.resourceTypeWeights,
});
const memories = deps.cfg.enableResourcePreviewUrls
? await withPreviewUrls(pickedMemories, recallClient, session.agentId)
: pickedMemories;
const candidateTraceResults = leafOnly
.map((item) => toTraceResult(item, deps.inferRecallResourceType(item.uri) === "resource" ? "resource" : "memory", deps))
.slice(0, deps.cfg.traceRecallMaxResultsPerSearch);
Expand Down
10 changes: 8 additions & 2 deletions examples/openclaw-plugin/plugin/openviking-query-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function formatOVSearchRows(result: FindResult): string[] {
if (items.length === 0) {
return [];
}
const includePreviewUrls = items.some(({ item }) => Boolean(item.preview_url));
const numberHeader = "no";
const numberWidth = Math.max(numberHeader.length, String(items.length).length);
const typeWidth = Math.max("type".length, ...items.map(({ contextType }) => contextType.length));
Expand All @@ -31,12 +32,17 @@ export function formatOVSearchRows(result: FindResult): string[] {
"score".length,
...items.map(({ item }) => (typeof item.score === "number" ? item.score.toFixed(2).length : 0)),
);
const previewWidth = includePreviewUrls
? Math.max("preview_url".length, ...items.map(({ item }) => item.preview_url?.length ?? 0))
: 0;
const previewHeader = includePreviewUrls ? ` ${"preview_url".padEnd(previewWidth)}` : "";
return [
`${numberHeader.padEnd(numberWidth)} ${"type".padEnd(typeWidth)} ${"uri".padEnd(uriWidth)} ${"level".padEnd(levelWidth)} ${"score".padEnd(scoreWidth)} abstract`,
`${numberHeader.padEnd(numberWidth)} ${"type".padEnd(typeWidth)} ${"uri".padEnd(uriWidth)} ${"level".padEnd(levelWidth)} ${"score".padEnd(scoreWidth)}${previewHeader} abstract`,
...items.map(({ contextType, item }, index) => {
const score = typeof item.score === "number" ? item.score.toFixed(2) : "";
const previewColumn = includePreviewUrls ? ` ${(item.preview_url ?? "").padEnd(previewWidth)}` : "";
const summary = truncateSummary(item.abstract || item.overview || "(no summary)");
return `${String(index + 1).padEnd(numberWidth)} ${contextType.padEnd(typeWidth)} ${item.uri.padEnd(uriWidth)} ${String(item.level ?? "").padEnd(levelWidth)} ${score.padEnd(scoreWidth)} ${summary}`;
return `${String(index + 1).padEnd(numberWidth)} ${contextType.padEnd(typeWidth)} ${item.uri.padEnd(uriWidth)} ${String(item.level ?? "").padEnd(levelWidth)} ${score.padEnd(scoreWidth)}${previewColumn} ${summary}`;
}),
];
}
Expand Down
16 changes: 14 additions & 2 deletions examples/openclaw-plugin/plugin/openviking-query-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FindResult, FindResultItem, FsListResult } from "../client.js";
import { withFindResultPreviewUrls } from "../preview-url.js";
import type { RecallResourceType } from "../registries/recall-resource-types.js";
import type { RecallTraceEntry, RecallTraceResult } from "../recall-trace.js";

Expand Down Expand Up @@ -52,6 +53,7 @@ type OpenVikingQueryClient = {
agentId?: string,
) => Promise<FindResult>;
read: (uri: string, agentId?: string) => Promise<unknown>;
getPreviewUrl?: (uri: string, actorPeerId?: string) => Promise<string>;
list: (
uri: string,
options?: { recursive?: boolean; simple?: boolean; nodeLimit?: number },
Expand All @@ -77,6 +79,7 @@ export type OpenVikingQueryRuntimeDeps<TQueryConfigContext> = {
traceRecallMaxResultsPerSearch: number;
traceRecallPreviewChars: number;
traceRecallQueryMaxChars: number;
enableResourcePreviewUrls?: boolean;
};
};

Expand Down Expand Up @@ -117,6 +120,7 @@ function formatOVSearchRows(result: FindResult): string[] {
if (items.length === 0) {
return [];
}
const includePreviewUrls = items.some(({ item }) => Boolean(item.preview_url));
const numberHeader = "no";
const numberWidth = Math.max(numberHeader.length, String(items.length).length);
const typeWidth = Math.max("type".length, ...items.map(({ contextType }) => contextType.length));
Expand All @@ -126,12 +130,17 @@ function formatOVSearchRows(result: FindResult): string[] {
"score".length,
...items.map(({ item }) => (typeof item.score === "number" ? item.score.toFixed(2).length : 0)),
);
const previewWidth = includePreviewUrls
? Math.max("preview_url".length, ...items.map(({ item }) => item.preview_url?.length ?? 0))
: 0;
const previewHeader = includePreviewUrls ? ` ${"preview_url".padEnd(previewWidth)}` : "";
return [
`${numberHeader.padEnd(numberWidth)} ${"type".padEnd(typeWidth)} ${"uri".padEnd(uriWidth)} ${"level".padEnd(levelWidth)} ${"score".padEnd(scoreWidth)} abstract`,
`${numberHeader.padEnd(numberWidth)} ${"type".padEnd(typeWidth)} ${"uri".padEnd(uriWidth)} ${"level".padEnd(levelWidth)} ${"score".padEnd(scoreWidth)}${previewHeader} abstract`,
...items.map(({ contextType, item }, index) => {
const score = typeof item.score === "number" ? item.score.toFixed(2) : "";
const previewColumn = includePreviewUrls ? ` ${(item.preview_url ?? "").padEnd(previewWidth)}` : "";
const summary = truncateSummary(item.abstract || item.overview || "(no summary)");
return `${String(index + 1).padEnd(numberWidth)} ${contextType.padEnd(typeWidth)} ${item.uri.padEnd(uriWidth)} ${String(item.level ?? "").padEnd(levelWidth)} ${score.padEnd(scoreWidth)} ${summary}`;
return `${String(index + 1).padEnd(numberWidth)} ${contextType.padEnd(typeWidth)} ${item.uri.padEnd(uriWidth)} ${String(item.level ?? "").padEnd(levelWidth)} ${score.padEnd(scoreWidth)}${previewColumn} ${summary}`;
}),
];
}
Expand Down Expand Up @@ -412,6 +421,9 @@ export function createOpenVikingQueryRuntime<TQueryConfigContext>(deps: OpenViki
}
result = mergeFindResults(successful);
}
if (deps.cfg.enableResourcePreviewUrls) {
result = await withFindResultPreviewUrls(result, client, agentId);
}
const selected = [
...(result.memories ?? []).map((item) => ({
uri: item.uri,
Expand Down
68 changes: 68 additions & 0 deletions examples/openclaw-plugin/preview-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { FindResult, FindResultItem } from "./client.js";

const IMAGE_EXTENSIONS = new Set([
".png",
".jpg",
".jpeg",
".webp",
".gif",
".bmp",
".svg",
".avif",
]);

export type PreviewUrlClient = {
getPreviewUrl?: (uri: string, actorPeerId?: string) => Promise<string>;
};

export function stripUriFragment(uri: string): string {
const hashIndex = uri.indexOf("#");
return hashIndex >= 0 ? uri.slice(0, hashIndex) : uri;
}

export function isPreviewableImageResourceUri(uri: string): boolean {
const normalized = stripUriFragment(uri).toLowerCase();
if (!normalized.startsWith("viking://resources/")) {
return false;
}
return [...IMAGE_EXTENSIONS].some((ext) => normalized.endsWith(ext));
}

export async function withPreviewUrls(
items: FindResultItem[],
client: PreviewUrlClient,
actorPeerId?: string,
): Promise<FindResultItem[]> {
const getPreviewUrl = client.getPreviewUrl;
if (typeof getPreviewUrl !== "function") {
return items;
}

const enriched = await Promise.all(items.map(async (item) => {
if (item.preview_url || !isPreviewableImageResourceUri(item.uri)) {
return item;
}
try {
const previewUrl = await getPreviewUrl(stripUriFragment(item.uri), actorPeerId);
return previewUrl ? { ...item, preview_url: previewUrl } : item;
} catch {
return item;
}
}));

return enriched;
}

export async function withFindResultPreviewUrls(
result: FindResult,
client: PreviewUrlClient,
actorPeerId?: string,
): Promise<FindResult> {
const resources = result.resources
? await withPreviewUrls(result.resources, client, actorPeerId)
: result.resources;
return {
...result,
resources,
};
}
1 change: 1 addition & 0 deletions examples/openclaw-plugin/setup-helper/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const FALLBACK_CURRENT = {
"context-engine.ts",
"auto-recall.ts",
"client.ts",
"preview-url.ts",
"process-manager.ts",
"memory-ranking.ts",
"text-utils.ts",
Expand Down
Loading