Skip to content
Merged
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
64 changes: 50 additions & 14 deletions plugins/memory/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,33 @@ function extractEntities(text) {
return entities;
}

// ─── Helper: coerce a tags value to an array ─────────────────────────────────

/**
* LLMs occasionally serialize array arguments as a JSON string
* (e.g. '["work","urgent"]' instead of ["work","urgent"]).
* This helper normalises both forms so downstream code always receives
* a plain JS array (or undefined/null for missing values).
*/
function coerceToArray(value) {
if (value == null) return value;
if (Array.isArray(value)) return value;
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.startsWith("[")) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) return parsed;
} catch {
// Not valid JSON — fall through and treat as a single tag
}
}
// Single tag provided as a plain string (e.g. "work")
return trimmed ? [trimmed] : [];
}
return value;
}

// ─── Helper: parse tags from content and explicit list ───────────────────────

function parseTags(content, extraTags) {
Expand All @@ -148,9 +175,10 @@ function parseTags(content, extraTags) {
tags.add(m[1].toLowerCase());
}

// Explicitly provided tags
if (Array.isArray(extraTags)) {
for (const t of extraTags) {
// Explicitly provided tags — coerce to array first to handle JSON strings
const normalised = coerceToArray(extraTags);
if (Array.isArray(normalised)) {
for (const t of normalised) {
if (typeof t === "string" && t.trim()) {
tags.add(t.replace(/^#/, "").toLowerCase().trim());
}
Expand Down Expand Up @@ -205,9 +233,11 @@ export const tools = (sdk) => [
description: "The text to remember. May include inline #tags and @mentions.",
},
tags: {
type: "array",
items: { type: "string" },
description: "Optional list of tags to attach (e.g. [\"work\", \"urgent\"]). #prefix is optional.",
oneOf: [
{ type: "array", items: { type: "string" } },
{ type: "string" },
],
description: "Optional list of tags to attach (e.g. [\"work\", \"urgent\"]). #prefix is optional. May also be provided as a JSON-encoded string.",
},
},
required: ["content"],
Expand Down Expand Up @@ -358,9 +388,11 @@ export const tools = (sdk) => [
description: "Free-text search within memory content (case-insensitive substring match).",
},
tags: {
type: "array",
items: { type: "string" },
description: "Filter entries that have ALL of the specified tags.",
oneOf: [
{ type: "array", items: { type: "string" } },
{ type: "string" },
],
description: "Filter entries that have ALL of the specified tags. May also be provided as a JSON-encoded string.",
},
entity: {
type: "string",
Expand Down Expand Up @@ -438,8 +470,10 @@ export const tools = (sdk) => [
}

// Tag filtering: entry must have ALL requested tags
const normalizedTags = Array.isArray(filterTags)
? filterTags.map((t) => t.replace(/^#/, "").toLowerCase().trim()).filter(Boolean)
// coerceToArray handles JSON-string inputs from LLMs
const coercedTags = coerceToArray(filterTags);
const normalizedTags = Array.isArray(coercedTags)
? coercedTags.map((t) => t.replace(/^#/, "").toLowerCase().trim()).filter(Boolean)
: [];

for (const tag of normalizedTags) {
Expand Down Expand Up @@ -515,9 +549,11 @@ export const tools = (sdk) => [
description: "New content to replace the existing entry. May include inline #tags and @mentions.",
},
tags: {
type: "array",
items: { type: "string" },
description: "Optional list of tags to attach (replaces existing tags). #prefix is optional.",
oneOf: [
{ type: "array", items: { type: "string" } },
{ type: "string" },
],
description: "Optional list of tags to attach (replaces existing tags). #prefix is optional. May also be provided as a JSON-encoded string.",
},
},
required: ["id"],
Expand Down
34 changes: 34 additions & 0 deletions plugins/memory/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,29 @@ describe("memory plugin", () => {
assert.ok(personEntity, "should extract @anton as person entity");
});

it("accepts tags as a JSON-encoded string (LLM serialization quirk)", async () => {
const sdk = makeSdk();
const tool = mod.tools(sdk).find((t) => t.name === "memory_store");
const result = await tool.execute(
{ content: "Important rule", tags: '["rules", "github"]' },
makeContext()
);
assert.equal(result.success, true);
assert.ok(result.data.tags.includes("#rules"), "should parse #rules from JSON string");
assert.ok(result.data.tags.includes("#github"), "should parse #github from JSON string");
});

it("accepts tags as a single plain string", async () => {
const sdk = makeSdk();
const tool = mod.tools(sdk).find((t) => t.name === "memory_store");
const result = await tool.execute(
{ content: "Note", tags: "work" },
makeContext()
);
assert.equal(result.success, true);
assert.ok(result.data.tags.includes("#work"), "should treat plain string as a single tag");
});

it("returns error when content is empty", async () => {
const sdk = makeSdk();
const tool = mod.tools(sdk).find((t) => t.name === "memory_store");
Expand Down Expand Up @@ -462,6 +485,17 @@ describe("memory plugin", () => {
assert.equal(result.success, false);
assert.ok(result.error.includes("end_date"));
});

it("accepts tags filter as a JSON-encoded string (LLM serialization quirk)", async () => {
const sdk = makeSdk();
const store = mod.tools(sdk).find((t) => t.name === "memory_store");
await store.execute({ content: "GitHub workflow note", tags: ["github"] }, makeContext());

const search = mod.tools(sdk).find((t) => t.name === "memory_search");
const result = await search.execute({ tags: '["github"]' }, makeContext());
assert.equal(result.success, true);
assert.ok(result.data.count >= 1, "should find entry via JSON-string tags filter");
});
});

// ── memory_update ────────────────────────────────────────────────────────────
Expand Down
Loading