/**
 * Memory tools exposed to OpenClaw agents.
 *
 * Each tool is a thin wrapper around a `MemoryCore` method. We use the
 * **factory form** of `registerTool` (see `openclaw/src/plugins/tool-types.ts`)
 * so each tool has access to the trusted `OpenClawPluginToolContext`
 * (agentId / sessionKey / sessionId / workspaceDir), which lets us scope
 * searches to the current agent or session on demand.
 *
 * Tool execution signature follows pi-agent-core's `AnyAgentTool`:
 *
 *   execute(toolCallId: string, params: Static<typeof parameters>) => unknown
 *
 * Tools stay *stateless*: the bridge owns cursors, the core owns memory.
 * Each tool is idempotent and re-entrant.
 */
import { Type } from "@sinclair/typebox";
import { bridgeSessionId } from "./bridge.js";
const DEFAULT_BODY_CAP = 1200;
// ─── Parameter schemas ─────────────────────────────────────────────────────
const MemorySearchParams = Type.Object({
    query: Type.String({ minLength: 1, description: "Free-text query (2–5 key words)." }),
    maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
    tier1topK: Type.Optional(Type.Integer({
        minimum: 0,
        maximum: 100,
        description: "Override Skill (Tier 1) topK for this search only.",
    })),
    tier2topK: Type.Optional(Type.Integer({
        minimum: 0,
        maximum: 100,
        description: "Override trace/episode (Tier 2) topK for this search only.",
    })),
    tier3topK: Type.Optional(Type.Integer({
        minimum: 0,
        maximum: 100,
        description: "Override world-model (Tier 3) topK for this search only.",
    })),
    sessionScope: Type.Optional(Type.Boolean({
        default: false,
        description: "Restrict results to the current session only.",
    })),
});
const MemoryGetParams = Type.Object({
    id: Type.String({ minLength: 1 }),
    kind: Type.Optional(Type.Union([Type.Literal("trace"), Type.Literal("policy"), Type.Literal("world_model")], { default: "trace" })),
});
const MemoryTimelineParams = Type.Object({
    episodeId: Type.String({ minLength: 1 }),
    limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),
});
const SkillListParams = Type.Object({
    status: Type.Optional(Type.Union([
        Type.Literal("candidate"),
        Type.Literal("active"),
        Type.Literal("archived"),
    ])),
    limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50, default: 10 })),
});
const SkillGetParams = Type.Object({ id: Type.String({ minLength: 1 }) });
const EnvironmentQueryParams = Type.Object({
    query: Type.Optional(Type.String({
        description: "Free-text keyword to narrow down (optional; omit to list all environments).",
    })),
    limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 30, default: 5 })),
});
// ─── Helpers ───────────────────────────────────────────────────────────────
function clip(s, n) {
    if (!s)
        return "";
    return s.length > n ? s.slice(0, n) + "…" : s;
}
function textToolResult(details, text) {
    return {
        ...details,
        content: [{ type: "text", text }],
        details,
    };
}
function formatHitList(hits) {
    if (hits.length === 0)
        return "No relevant memories found.";
    const lines = hits.map((h, i) => {
        const snippet = h.snippet.trim() || "(empty snippet)";
        return `${i + 1}. [${h.refKind}:${h.refId}] ${snippet} (score=${h.score.toFixed(3)})`;
    });
    return `Found ${hits.length} memories:\n\n${lines.join("\n")}`;
}
function sessionFromCtx(ctx) {
    const sessionKey = ctx?.sessionKey;
    if (!sessionKey)
        return undefined;
    const agentId = ctx?.agentId ?? "main";
    return bridgeSessionId(agentId, sessionKey);
}
function namespaceFromCtx(ctx) {
    const profileId = (ctx?.agentId || "main").trim() || "main";
    return {
        agentKind: "openclaw",
        profileId,
        profileLabel: profileId,
        workspacePath: ctx?.workspaceDir || ctx?.agentDir,
        sessionKey: ctx?.sessionKey,
    };
}
async function resolveCore(opts) {
    const core = opts.core ?? (await opts.getCore?.());
    if (!core) {
        throw new Error("MemOS Local runtime is not ready yet");
    }
    return core;
}
// ─── Registration ──────────────────────────────────────────────────────────
export function registerOpenClawTools(api, opts) {
    const bodyCap = opts.maxBodyChars ?? DEFAULT_BODY_CAP;
    // ── memos_search ──
    api.registerTool((ctx) => ({
        name: "memos_search",
        label: "Memory Search",
        description: "Search MemOS memory (local traces + policies + world models + skills, plus connected Team Hub memories). " +
            "Returns a ranked list of grounded snippets. Prefer this before claiming prior context is unavailable.",
        parameters: MemorySearchParams,
        async execute(_toolCallId, params) {
            const started = Date.now();
            const core = await resolveCore(opts);
            const sessionId = params.sessionScope ? sessionFromCtx(ctx) : undefined;
            const maxResults = params.maxResults !== undefined
                ? Math.min(params.maxResults, 50)
                : undefined;
            const result = await core.searchMemory({
                agent: opts.agent,
                namespace: namespaceFromCtx(ctx),
                sessionId: sessionId,
                query: params.query,
                topK: topKParams(params, maxResults),
            });
            const details = {
                hits: result.hits.map((h) => ({
                    tier: h.tier,
                    refKind: h.refKind,
                    refId: h.refId,
                    score: h.score,
                    snippet: clip(h.snippet, bodyCap),
                })),
                totalMs: Date.now() - started,
            };
            return textToolResult(details, formatHitList(details.hits));
        },
    }), { name: "memos_search" });
    // ── memos_get ──
    api.registerTool((ctx) => ({
        name: "memos_get",
        label: "Memory Get",
        description: 'Fetch the full body of a memory item by id. `kind` can be "trace" (default), ' +
            '"policy", or "world_model".',
        parameters: MemoryGetParams,
        async execute(_toolCallId, params) {
            const core = await resolveCore(opts);
            const kind = params.kind ?? "trace";
            if (kind === "trace") {
                const trace = await core.getTrace(params.id, namespaceFromCtx(ctx));
                if (!trace) {
                    const details = { found: false, kind, id: params.id, body: "", meta: {} };
                    return textToolResult(details, `No ${kind} memory found for id "${params.id}".`);
                }
                const details = {
                    found: true,
                    kind,
                    id: trace.id,
                    body: clip(trace.agentText, bodyCap),
                    meta: {
                        episodeId: trace.episodeId,
                        ts: trace.ts,
                        value: trace.value,
                        reflection: clip(trace.reflection, bodyCap),
                        userText: clip(trace.userText, bodyCap),
                        toolCalls: trace.toolCalls.map((tc) => ({
                            name: tc.name,
                            success: !tc.errorCode,
                            errorCode: tc.errorCode,
                        })),
                    },
                };
                return textToolResult(details, details.body || trace.summary || trace.userText || `Found trace ${trace.id}.`);
            }
            if (kind === "policy") {
                const policy = await core.getPolicy(params.id, namespaceFromCtx(ctx));
                if (!policy) {
                    const details = { found: false, kind, id: params.id, body: "", meta: {} };
                    return textToolResult(details, `No ${kind} memory found for id "${params.id}".`);
                }
                const details = {
                    found: true,
                    kind,
                    id: policy.id,
                    body: `${policy.title}\n\n${policy.procedure}`,
                    meta: {
                        trigger: policy.trigger,
                        verification: policy.verification,
                        boundary: policy.boundary,
                        gain: policy.gain,
                        support: policy.support,
                        status: policy.status,
                    },
                };
                return textToolResult(details, details.body);
            }
            const wm = await core.getWorldModel(params.id, namespaceFromCtx(ctx));
            if (!wm) {
                const details = { found: false, kind, id: params.id, body: "", meta: {} };
                return textToolResult(details, `No ${kind} memory found for id "${params.id}".`);
            }
            const details = {
                found: true,
                kind,
                id: wm.id,
                body: clip(wm.body, bodyCap),
                meta: { title: wm.title, policyIds: wm.policyIds },
            };
            return textToolResult(details, `${wm.title}\n\n${details.body}`.trim());
        },
    }), { name: "memos_get" });
    // ── memos_timeline ──
    api.registerTool((ctx) => ({
        name: "memos_timeline",
        label: "Memory Timeline",
        description: "Return the ordered traces inside a single episode. Useful for reconstructing " +
            "conversation flow and debugging.",
        parameters: MemoryTimelineParams,
        async execute(_toolCallId, params) {
            const core = await resolveCore(opts);
            const traces = await core.timeline({ episodeId: params.episodeId, namespace: namespaceFromCtx(ctx) });
            const limited = traces.slice(0, params.limit ?? 20);
            const details = {
                episodeId: params.episodeId,
                traces: limited.map((t) => ({
                    id: t.id,
                    ts: t.ts,
                    userText: clip(t.userText, bodyCap),
                    agentText: clip(t.agentText, bodyCap),
                    toolCalls: t.toolCalls.map((tc) => ({ name: tc.name, error: tc.errorCode })),
                    value: t.value,
                })),
            };
            const text = details.traces.length === 0
                ? `No traces found for episode "${params.episodeId}".`
                : `Episode ${params.episodeId} timeline:\n\n` +
                    details.traces
                        .map((t, i) => `${i + 1}. ${t.userText || t.agentText || t.id}`)
                        .join("\n");
            return textToolResult(details, text);
        },
    }), { name: "memos_timeline" });
    // ── memos_skill_list ──
    api.registerTool((ctx) => ({
        name: "memos_skill_list",
        label: "Skill List",
        description: "List callable skills the agent can invoke. Filter by status (candidate | active | archived).",
        parameters: SkillListParams,
        async execute(_toolCallId, params) {
            const core = await resolveCore(opts);
            const skills = await core.listSkills({
                status: params.status,
                limit: params.limit,
                namespace: namespaceFromCtx(ctx),
            });
            const details = {
                skills: skills.map((s) => ({
                    id: s.id,
                    name: s.name,
                    status: s.status,
                    eta: s.eta,
                    support: s.support,
                    gain: s.gain,
                    invocationGuide: clip(s.invocationGuide, bodyCap),
                })),
            };
            const text = details.skills.length === 0
                ? "No skills found."
                : `Found ${details.skills.length} skills:\n\n` +
                    details.skills.map((s, i) => `${i + 1}. ${s.name} (${s.id}, ${s.status})`).join("\n");
            return textToolResult(details, text);
        },
    }), { name: "memos_skill_list" });
    // ── memos_environment ──
    //
    // Dedicated Tier-3 lookup. The turn-start injector already folds
    // environment knowledge into `prependContext`, but during a long
    // tool-driven chain the model may want to re-fetch a specific
    // domain ("what did we learn about this project's build system?")
    // without re-triggering a full tier1+2+3 search. This tool returns
    // only the world-model snippets so the agent can inject domain
    // knowledge on demand.
    api.registerTool((ctx) => ({
        name: "memos_environment",
        label: "Environment Knowledge",
        description: "Return the agent's accumulated environment knowledge (L3 world models) — " +
            "structural facts, behavioural rules and constraints learnt across episodes. " +
            "Use this before deciding how to navigate an unfamiliar project: you already " +
            "know where code lives, which commands run, and what to avoid.",
        parameters: EnvironmentQueryParams,
        async execute(_toolCallId, params) {
            const query = (params.query ?? "").trim();
            const cap = Math.min(Math.max(1, params.limit ?? 5), 30);
            // No query → return the most recently updated world models
            // directly. Avoids paying for an LLM filter pass when the
            // agent just wants a quick "what do we know about here?"
            // dump.
            const core = await resolveCore(opts);
            if (!query) {
                const rows = await core.listWorldModels({ limit: cap, offset: 0, namespace: namespaceFromCtx(ctx) });
                const details = {
                    worldModels: rows.map((w) => ({
                        id: w.id,
                        title: w.title,
                        body: clip(w.body, bodyCap),
                        policyIds: w.policyIds,
                        updatedAt: w.updatedAt,
                    })),
                    queried: false,
                };
                const text = details.worldModels.length === 0
                    ? "No environment knowledge found."
                    : `Environment knowledge:\n\n` +
                        details.worldModels.map((w, i) => `${i + 1}. ${w.title}\n${w.body}`).join("\n\n");
                return textToolResult(details, text);
            }
            // With a query, go through `searchMemory` so tag filters +
            // cosine ranking apply, then keep only the tier-3 hits.
            const res = await core.searchMemory({
                agent: opts.agent,
                namespace: namespaceFromCtx(ctx),
                query,
                topK: { tier1: 0, tier2: 0, tier3: cap },
            });
            const tier3 = res.hits.filter((h) => h.tier === 3);
            const details = {
                worldModels: tier3.map((h) => ({
                    id: h.refId,
                    title: (h.snippet ?? "").split("\n")[0]?.replace(/^World model:\s*/, "") ?? "",
                    body: clip(h.snippet ?? "", bodyCap),
                    policyIds: [],
                    score: h.score,
                })),
                queried: true,
            };
            const text = details.worldModels.length === 0
                ? "No matching environment knowledge found."
                : `Environment knowledge for "${query}":\n\n` +
                    details.worldModels.map((w, i) => `${i + 1}. ${w.title}\n${w.body}`).join("\n\n");
            return textToolResult(details, text);
        },
    }), { name: "memos_environment" });
    // ── memos_skill_get ──
    api.registerTool((ctx) => ({
        name: "memos_skill_get",
        label: "Skill Get",
        description: "Return the full invocation guide for a crystallized skill.",
        parameters: SkillGetParams,
        async execute(toolCallId, params) {
            const core = await resolveCore(opts);
            const skill = await core.getSkill(params.id, {
                recordUse: true,
                recordTrial: true,
                sessionId: sessionFromCtx(ctx),
                namespace: namespaceFromCtx(ctx),
                toolCallId,
            });
            if (!skill) {
                const details = { found: false, skill: null };
                return textToolResult(details, `No skill found for id "${params.id}".`);
            }
            const details = {
                found: true,
                skill: {
                    id: skill.id,
                    name: skill.name,
                    status: skill.status,
                    eta: skill.eta,
                    gain: skill.gain,
                    support: skill.support,
                    invocationGuide: skill.invocationGuide,
                    sourcePolicyIds: skill.sourcePolicyIds,
                    sourceWorldModelIds: skill.sourceWorldModelIds,
                    createdAt: skill.createdAt,
                    updatedAt: skill.updatedAt,
                    usageCount: skill.usageCount,
                    lastUsedAt: skill.lastUsedAt,
                },
            };
            return textToolResult(details, `${skill.name}\n\n${skill.invocationGuide}`.trim());
        },
    }), { name: "memos_skill_get" });
}
function topKParams(params, maxResults) {
    if (params.tier1topK === undefined &&
        params.tier2topK === undefined &&
        params.tier3topK === undefined &&
        maxResults === undefined) {
        return undefined;
    }
    return {
        tier1: params.tier1topK ?? maxResults,
        tier2: params.tier2topK ?? maxResults,
        tier3: params.tier3topK ?? maxResults,
    };
}
/** Exposed for tests + documentation. */
export const TOOL_SCHEMAS = {
    memos_search: MemorySearchParams,
    memos_get: MemoryGetParams,
    memos_timeline: MemoryTimelineParams,
    memos_environment: EnvironmentQueryParams,
    memos_skill_list: SkillListParams,
    memos_skill_get: SkillGetParams,
};
//# sourceMappingURL=tools.js.map