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
70 changes: 16 additions & 54 deletions examples/openclaw-plugin/auto-recall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ import { estimateTextTokens } from "./token-estimator.js";
import type {
RecallResourceType,
RecallTraceEntry,
RecallTraceResult,
} from "./recall-trace.js";
import { resolveRecallSearchPlan } from "./recall-trace.js";
import {
boundTraceQuery,
createTraceId,
inferRecallResourceType,
toTraceSelectedItem,
traceResultsFromFindResult,
} from "./recall-trace-utils.js";

const RECALL_QUERY_MAX_CHARS = 4_000;
export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
Expand Down Expand Up @@ -245,40 +251,8 @@ export function buildOpenVikingContextBlock(params: {
].join("\n");
}

function newTraceId(): string {
return `auto_recall_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
}

function preview(value: string | undefined | null, maxChars: number): string | undefined {
const normalized = typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
if (!normalized) {
return undefined;
}
return normalized.length > maxChars ? normalized.slice(0, maxChars) : normalized;
}

function traceResourceTypeForUri(uri: string | undefined): RecallResourceType {
if (uri?.startsWith("viking://resources")) return "resource";
if (uri?.startsWith("viking://session/") || uri?.includes("/sessions/")) return "session";
return "user";
}

function toTraceResults(items: FindResultItem[], resourceType: RecallResourceType): RecallTraceResult[] {
return items.map((item) => ({
uri: item.uri,
resourceType,
category: item.category,
score: item.score,
level: item.level,
abstractPreview: preview(item.abstract ?? item.overview, 240),
resultType: resourceType === "resource" ? "resource" : "memory",
}));
}

function boundTraceQuery(query: string, maxChars: number): { query: string; queryTruncated?: boolean } {
return query.length <= maxChars
? { query }
: { query: query.slice(0, maxChars), queryTruncated: true };
return inferRecallResourceType(uri) ?? "user";
}

function runtimeFlag(runtimeContext: unknown, key: string): unknown {
Expand Down Expand Up @@ -578,28 +552,20 @@ export async function buildLongTermMemoryRecallContext(params: {
const memories = result.memories ?? [];
const resources = result.resources ?? [];
allMemories.push(...memories, ...resources);
const resourceType = s.value.search.resourceType;
traceSearches.push({
resourceType: s.value.search.resourceType,
resourceType,
contextType: s.value.search.contextType,
targetUriInput: s.value.search.targetUri,
targetUriResolved: s.value.search.targetUri,
limit: candidateLimit,
scoreThreshold: 0,
durationMs: s.value.durationMs,
total: result.total ?? memories.length + resources.length + (result.skills?.length ?? 0),
results: [
...toTraceResults(memories, s.value.search.resourceType),
...toTraceResults(resources, "resource"),
...(result.skills ?? []).map((item): RecallTraceResult => ({
uri: item.uri,
resourceType: s.value.search.resourceType,
category: item.category,
score: item.score,
level: item.level,
abstractPreview: preview(item.abstract ?? item.overview, 240),
resultType: "skill",
})),
].slice(0, cfg.traceRecallMaxResultsPerSearch),
results: traceResultsFromFindResult(result, 240, cfg.traceRecallMaxResultsPerSearch, {
memory: resourceType,
skill: resourceType,
}),
});
} else {
logger.warn?.(`openviking: auto-recall search failed: ${String(s.reason)}`);
Expand Down Expand Up @@ -630,7 +596,7 @@ export async function buildLongTermMemoryRecallContext(params: {
const recordTrace = (injectedMemories: FindResultItem[], injectedCount: number, estimatedTokens?: number) => {
params.traceRecorder?.record({
schemaVersion: "1.0",
traceId: newTraceId(),
traceId: createTraceId("auto_recall"),
ts: Date.now(),
sessionId: params.sessionId,
sessionKey: params.sessionKey,
Expand All @@ -645,12 +611,8 @@ export async function buildLongTermMemoryRecallContext(params: {
queryTruncated: params.queryTruncated || queryText.length > cfg.traceRecallQueryMaxChars,
},
searches: traceSearches,
selected: injectedMemories.map((memory) => ({
uri: memory.uri,
selected: injectedMemories.map((memory) => toTraceSelectedItem(memory, cfg.traceRecallPreviewChars, {
resourceType: traceResourceTypeForUri(memory.uri),
category: memory.category,
score: memory.score,
abstractPreview: preview(memory.abstract ?? memory.overview, cfg.traceRecallPreviewChars),
injected: true,
})),
stats: {
Expand Down
167 changes: 167 additions & 0 deletions examples/openclaw-plugin/command-args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
export type AddResourceToolInput = {
source?: string; to?: string; parent?: string; reason?: string;
instruction?: string; wait?: boolean; timeout?: number;
};
export type AddSkillToolInput = { source?: string; data?: unknown; wait?: boolean; timeout?: number };
export type OVSearchInput = { query: string; uri?: string; limit?: number };
type ParsedFlagArgs = { positionals: string[]; flags: Map<string, string | boolean> };

export function tokenizeCommandArgs(args: string): string[] {
const tokens: string[] = [];
let current = "";
let quote: "'" | '"' | null = null;
let escaping = false;

for (let i = 0; i < args.length; i += 1) {
const ch = args[i]!;
const next = args[i + 1];
if (escaping) {
current += ch;
escaping = false;
continue;
}
if (ch === "\\") {
const shouldEscape =
quote === '"'
? next === '"' || next === "\\"
: !quote && Boolean(next && (/\s/.test(next) || next === '"' || next === "'"));
if (shouldEscape) {
escaping = true;
continue;
}
current += ch;
continue;
}
if ((ch === '"' || ch === "'") && (!quote || quote === ch)) {
quote = quote ? null : ch;
continue;
}
if (!quote && /\s/.test(ch)) {
if (current) {
tokens.push(current);
current = "";
}
continue;
}
current += ch;
}

if (escaping) {
current += "\\";
}
if (quote) {
throw new Error("Unterminated quoted argument");
}
if (current) {
tokens.push(current);
}
return tokens;
}

export function parseFlagArgs(args: string): ParsedFlagArgs {
const tokens = tokenizeCommandArgs(args);
const positionals: string[] = [];
const flags = new Map<string, string | boolean>();

for (let i = 0; i < tokens.length; i += 1) {
const token = tokens[i]!;
if (!token.startsWith("--")) {
positionals.push(token);
continue;
}
const raw = token.slice(2);
if (!raw) {
continue;
}
const eqIndex = raw.indexOf("=");
if (eqIndex >= 0) {
flags.set(raw.slice(0, eqIndex), raw.slice(eqIndex + 1));
continue;
}
const next = tokens[i + 1];
if (next && !next.startsWith("--")) {
flags.set(raw, next);
i += 1;
} else {
flags.set(raw, true);
}
}

return { positionals, flags };
}

export function getStringFlag(flags: Map<string, string | boolean>, name: string): string | undefined {
const value = flags.get(name);
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}

export function getNumberFlag(flags: Map<string, string | boolean>, name: string): number | undefined {
const raw = getStringFlag(flags, name);
if (!raw) {
return undefined;
}
const value = Number(raw);
if (!Number.isFinite(value)) {
throw new Error(`--${name} must be a number`);
}
return value;
}

export function getBoolFlag(flags: Map<string, string | boolean>, name: string): boolean {
return flags.get(name) === true;
}

export function parseAddResourceCommandArgs(args: string): AddResourceToolInput {
const parsed = parseFlagArgs(args);
const source = parsed.positionals.length <= 1
? parsed.positionals[0]
: parsed.positionals.join(" ").trim();
if (!source) {
throw new Error("Usage: /add-resource <source> [--to URI] [--parent URI] [--reason TEXT] [--instruction TEXT] [--wait] [--timeout SEC]");
}
const to = getStringFlag(parsed.flags, "to");
const parent = getStringFlag(parsed.flags, "parent");
if (to && parent) {
throw new Error("Cannot specify both --to and --parent.");
}
return {
source,
to,
parent,
reason: getStringFlag(parsed.flags, "reason"),
instruction: getStringFlag(parsed.flags, "instruction"),
wait: getBoolFlag(parsed.flags, "wait"),
timeout: getNumberFlag(parsed.flags, "timeout"),
};
}

export function parseAddSkillCommandArgs(args: string): AddSkillToolInput {
const parsed = parseFlagArgs(args);
const source = parsed.positionals.length <= 1
? parsed.positionals[0]
: parsed.positionals.join(" ").trim();
if (!source) {
throw new Error("Usage: /add-skill <source> [--wait] [--timeout SEC]");
}
if (parsed.flags.has("to") || parsed.flags.has("parent") || parsed.flags.has("reason") || parsed.flags.has("instruction")) {
throw new Error("--to, --parent, --reason, and --instruction are resource-only options.");
}
return {
source,
wait: getBoolFlag(parsed.flags, "wait"),
timeout: getNumberFlag(parsed.flags, "timeout"),
};
}

export function parseOVSearchCommandArgs(args: string): OVSearchInput {
const parsed = parseFlagArgs(args);
const query = parsed.positionals.join(" ").trim();
if (!query) {
throw new Error('Usage: /ov-search "<query>" [--uri URI] [--limit N]');
}
return {
query,
uri: getStringFlag(parsed.flags, "uri"),
limit: getNumberFlag(parsed.flags, "limit"),
};
}
Loading